--- a/devtools/client/shared/components/AutoCompletePopup.js
+++ b/devtools/client/shared/components/AutoCompletePopup.js
@@ -1,72 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
-
-module.exports = createClass({
- displayName: "AutocompletePopup",
+const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
- propTypes: {
- /**
- * autocompleteProvider takes search-box's entire input text as `filter` argument
- * ie. "is:cached pr"
- * returned value is array of objects like below
- * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
- * `value` is used to update the search-box input box for given item
- * `displayValue` is used to render the autocomplete list
- */
- autocompleteProvider: PropTypes.func.isRequired,
- filter: PropTypes.string.isRequired,
- onItemSelected: PropTypes.func.isRequired,
- },
+class AutocompletePopup extends Component {
+ static get propTypes() {
+ return {
+ /**
+ * autocompleteProvider takes search-box's entire input text as `filter` argument
+ * ie. "is:cached pr"
+ * returned value is array of objects like below
+ * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
+ * `value` is used to update the search-box input box for given item
+ * `displayValue` is used to render the autocomplete list
+ */
+ autocompleteProvider: PropTypes.func.isRequired,
+ filter: PropTypes.string.isRequired,
+ onItemSelected: PropTypes.func.isRequired,
+ };
+ }
- getInitialState() {
- return this.computeState(this.props);
- },
+ constructor(props, context) {
+ super(props, context);
+ this.state = this.computeState(props);
+ this.computeState = this.computeState.bind(this);
+ this.jumpToTop = this.jumpToTop.bind(this);
+ this.jumpToBottom = this.jumpToBottom.bind(this);
+ this.jumpBy = this.jumpBy.bind(this);
+ this.select = this.select.bind(this);
+ this.onMouseDown = this.onMouseDown.bind(this);
+ }
componentWillReceiveProps(nextProps) {
if (this.props.filter === nextProps.filter) {
return;
}
this.setState(this.computeState(nextProps));
- },
+ }
componentDidUpdate() {
if (this.refs.selected) {
this.refs.selected.scrollIntoView(false);
}
- },
+ }
computeState({ autocompleteProvider, filter }) {
let list = autocompleteProvider(filter);
let selectedIndex = list.length == 1 ? 0 : -1;
return { list, selectedIndex };
- },
+ }
/**
* Use this method to select the top-most item
* This method is public, called outside of the autocomplete-popup component.
*/
jumpToTop() {
this.setState({ selectedIndex: 0 });
- },
+ }
/**
* Use this method to select the bottom-most item
* This method is public.
*/
jumpToBottom() {
this.setState({ selectedIndex: this.state.list.length - 1 });
- },
+ }
/**
* Increment the selected index with the provided increment value. Will cycle to the
* beginning/end of the list if the index exceeds the list boundaries.
* This method is public.
*
* @param {number} increment - No. of hops in the direction
*/
@@ -76,32 +83,32 @@ module.exports = createClass({
if (increment > 0) {
// Positive cycling
nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex;
} else if (increment < 0) {
// Inverse cycling
nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
}
this.setState({selectedIndex: nextIndex});
- },
+ }
/**
* Submit the currently selected item to the onItemSelected callback
* This method is public.
*/
select() {
if (this.refs.selected) {
this.props.onItemSelected(this.refs.selected.dataset.value);
}
- },
+ }
onMouseDown(e) {
e.preventDefault();
this.setState({ selectedIndex: Number(e.target.dataset.index) }, this.select);
- },
+ }
render() {
let { list } = this.state;
return list.length > 0 && dom.div(
{ className: "devtools-autocomplete-popup devtools-monospace" },
dom.ul(
{ className: "devtools-autocomplete-listbox" },
@@ -119,9 +126,11 @@ module.exports = createClass({
className: itemClassList.join(" "),
ref: isSelected ? "selected" : null,
onMouseDown: this.onMouseDown,
}, item.displayValue);
})
)
);
}
-});
+}
+
+module.exports = AutocompletePopup;
--- a/devtools/client/shared/components/Frame.js
+++ b/devtools/client/shared/components/Frame.js
@@ -1,106 +1,112 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
const { getSourceNames, parseURL,
isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
-module.exports = createClass({
- displayName: "Frame",
+class Frame extends Component {
+ static get propTypes() {
+ return {
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ source: PropTypes.string.isRequired,
+ line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ }).isRequired,
+ // Clicking on the frame link -- probably should link to the debugger.
+ onClick: PropTypes.func.isRequired,
+ // Option to display a function name before the source link.
+ showFunctionName: PropTypes.bool,
+ // Option to display a function name even if it's anonymous.
+ showAnonymousFunctionName: PropTypes.bool,
+ // Option to display a host name after the source link.
+ showHost: PropTypes.bool,
+ // Option to display a host name if the filename is empty or just '/'
+ showEmptyPathAsHost: PropTypes.bool,
+ // Option to display a full source instead of just the filename.
+ showFullSourceUrl: PropTypes.bool,
+ // Service to enable the source map feature for console.
+ sourceMapService: PropTypes.object,
+ };
+ }
- propTypes: {
- // SavedFrame, or an object containing all the required properties.
- frame: PropTypes.shape({
- functionDisplayName: PropTypes.string,
- source: PropTypes.string.isRequired,
- line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
- column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
- }).isRequired,
- // Clicking on the frame link -- probably should link to the debugger.
- onClick: PropTypes.func.isRequired,
- // Option to display a function name before the source link.
- showFunctionName: PropTypes.bool,
- // Option to display a function name even if it's anonymous.
- showAnonymousFunctionName: PropTypes.bool,
- // Option to display a host name after the source link.
- showHost: PropTypes.bool,
- // Option to display a host name if the filename is empty or just '/'
- showEmptyPathAsHost: PropTypes.bool,
- // Option to display a full source instead of just the filename.
- showFullSourceUrl: PropTypes.bool,
- // Service to enable the source map feature for console.
- sourceMapService: PropTypes.object,
- },
-
- getDefaultProps() {
+ static get defaultProps() {
return {
showFunctionName: false,
showAnonymousFunctionName: false,
showHost: false,
showEmptyPathAsHost: false,
showFullSourceUrl: false,
};
- },
+ }
+
+ constructor(props) {
+ super(props);
+ this._locationChanged = this._locationChanged.bind(this);
+ this.getSourceForClick = this.getSourceForClick.bind(this);
+ }
componentWillMount() {
if (this.props.sourceMapService) {
const { source, line, column } = this.props.frame;
this.props.sourceMapService.subscribe(source, line, column,
this._locationChanged);
}
- },
+ }
componentWillUnmount() {
if (this.props.sourceMapService) {
const { source, line, column } = this.props.frame;
this.props.sourceMapService.unsubscribe(source, line, column,
this._locationChanged);
}
- },
+ }
_locationChanged(isSourceMapped, url, line, column) {
let newState = {
isSourceMapped,
};
if (isSourceMapped) {
newState.frame = {
source: url,
line,
column,
functionDisplayName: this.props.frame.functionDisplayName,
};
}
this.setState(newState);
- },
+ }
/**
* Utility method to convert the Frame object model to the
* object model required by the onClick callback.
* @param Frame frame
* @returns {{url: *, line: *, column: *, functionDisplayName: *}}
*/
getSourceForClick(frame) {
const { source, line, column } = frame;
return {
url: source,
line,
column,
functionDisplayName: this.props.frame.functionDisplayName,
};
- },
+ }
render() {
let frame, isSourceMapped;
let {
onClick,
showFunctionName,
showAnonymousFunctionName,
showHost,
@@ -230,9 +236,11 @@ module.exports = createClass({
elements.push(dom.span({
key: "host",
className: "frame-link-host",
}, host));
}
return dom.span(attributes, ...elements);
}
-});
+}
+
+module.exports = Frame;
--- a/devtools/client/shared/components/HSplitBox.js
+++ b/devtools/client/shared/components/HSplitBox.js
@@ -20,105 +20,111 @@
// | e |
// | r |
// | | |
// | | |
// +-----------------------+---------------------+
const {
DOM: dom,
- createClass,
+ Component,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { assert } = require("devtools/shared/DevToolsUtils");
-module.exports = createClass({
- displayName: "HSplitBox",
+class HSplitBox extends Component {
+ static get propTypes() {
+ return {
+ // The contents of the start pane.
+ start: PropTypes.any.isRequired,
- propTypes: {
- // The contents of the start pane.
- start: PropTypes.any.isRequired,
-
- // The contents of the end pane.
- end: PropTypes.any.isRequired,
+ // The contents of the end pane.
+ end: PropTypes.any.isRequired,
- // The relative width of the start pane, expressed as a number between 0 and
- // 1. The relative width of the end pane is 1 - startWidth. For example,
- // with startWidth = .5, both panes are of equal width; with startWidth =
- // .25, the start panel will take up 1/4 width and the end panel will take
- // up 3/4 width.
- startWidth: PropTypes.number,
+ // The relative width of the start pane, expressed as a number between 0 and
+ // 1. The relative width of the end pane is 1 - startWidth. For example,
+ // with startWidth = .5, both panes are of equal width; with startWidth =
+ // .25, the start panel will take up 1/4 width and the end panel will take
+ // up 3/4 width.
+ startWidth: PropTypes.number,
- // A minimum css width value for the start and end panes.
- minStartWidth: PropTypes.any,
- minEndWidth: PropTypes.any,
+ // A minimum css width value for the start and end panes.
+ minStartWidth: PropTypes.any,
+ minEndWidth: PropTypes.any,
- // A callback fired when the user drags the splitter to resize the relative
- // pane widths. The function is passed the startWidth value that would put
- // the splitter underneath the users mouse.
- onResize: PropTypes.func.isRequired,
- },
+ // A callback fired when the user drags the splitter to resize the relative
+ // pane widths. The function is passed the startWidth value that would put
+ // the splitter underneath the users mouse.
+ onResize: PropTypes.func.isRequired,
+ };
+ }
- getDefaultProps() {
+ static get defaultProps() {
return {
startWidth: 0.5,
minStartWidth: "20px",
minEndWidth: "20px",
};
- },
+ }
- getInitialState() {
- return {
+ constructor(props) {
+ super(props);
+
+ this.state = {
mouseDown: false
};
- },
+
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ }
componentDidMount() {
document.defaultView.top.addEventListener("mouseup", this._onMouseUp);
document.defaultView.top.addEventListener("mousemove", this._onMouseMove);
- },
+ }
componentWillUnmount() {
document.defaultView.top.removeEventListener("mouseup", this._onMouseUp);
document.defaultView.top.removeEventListener("mousemove", this._onMouseMove);
- },
+ }
_onMouseDown(event) {
if (event.button !== 0) {
return;
}
this.setState({ mouseDown: true });
event.preventDefault();
- },
+ }
_onMouseUp(event) {
if (event.button !== 0 || !this.state.mouseDown) {
return;
}
this.setState({ mouseDown: false });
event.preventDefault();
- },
+ }
_onMouseMove(event) {
if (!this.state.mouseDown) {
return;
}
const rect = this.refs.box.getBoundingClientRect();
const { left, right } = rect;
const width = right - left;
const direction = this.refs.box.ownerDocument.dir;
const relative = direction == "rtl" ? right - event.clientX
: event.clientX - left;
this.props.onResize(relative / width);
event.preventDefault();
- },
+ }
render() {
/* eslint-disable no-shadow */
const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
assert(startWidth => 0 && startWidth <= 1,
"0 <= this.props.startWidth <= 1");
/* eslint-enable */
return dom.div(
@@ -144,9 +150,11 @@ module.exports = createClass({
{
className: "h-split-box-pane",
style: { flex: 1 - startWidth, minWidth: minEndWidth },
},
end
)
);
}
-});
+}
+
+module.exports = HSplitBox;
--- a/devtools/client/shared/components/NotificationBox.js
+++ b/devtools/client/shared/components/NotificationBox.js
@@ -5,17 +5,17 @@
"use strict";
const React = require("devtools/client/shared/vendor/react");
const Immutable = require("devtools/client/shared/vendor/immutable");
const { LocalizationHelper } = require("devtools/shared/l10n");
const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
// Shortcuts
-const { PropTypes, createClass, DOM } = React;
+const { PropTypes, Component, DOM } = React;
const { div, span, button } = DOM;
// Priority Levels
const PriorityLevels = {
PRIORITY_INFO_LOW: 1,
PRIORITY_INFO_MEDIUM: 2,
PRIORITY_INFO_HIGH: 3,
PRIORITY_WARNING_LOW: 4,
@@ -29,82 +29,91 @@ const PriorityLevels = {
/**
* This component represents Notification Box - HTML alternative for
* <xul:notificationbox> binding.
*
* See also MDN for more info about <xul:notificationbox>:
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
*/
-var NotificationBox = createClass({
- displayName: "NotificationBox",
-
- propTypes: {
- // List of notifications appended into the box.
- notifications: PropTypes.arrayOf(PropTypes.shape({
- // label to appear on the notification.
- label: PropTypes.string.isRequired,
-
- // Value used to identify the notification
- value: PropTypes.string.isRequired,
-
- // URL of image to appear on the notification. If "" then an icon
- // appropriate for the priority level is used.
- image: PropTypes.string.isRequired,
-
- // Notification priority; see Priority Levels.
- priority: PropTypes.number.isRequired,
-
- // Array of button descriptions to appear on the notification.
- buttons: PropTypes.arrayOf(PropTypes.shape({
- // Function to be called when the button is activated.
- // This function is passed three arguments:
- // 1) the NotificationBox component the button is associated with
- // 2) the button description as passed to appendNotification.
- // 3) the element which was the target of the button press event.
- // If the return value from this function is not True, then the
- // notification is closed. The notification is also not closed
- // if an error is thrown.
- callback: PropTypes.func.isRequired,
-
- // The label to appear on the button.
+class NotificationBox extends Component {
+ static get propTypes() {
+ return {
+ // List of notifications appended into the box.
+ notifications: PropTypes.arrayOf(PropTypes.shape({
+ // label to appear on the notification.
label: PropTypes.string.isRequired,
- // The accesskey attribute set on the <button> element.
- accesskey: PropTypes.string,
+ // Value used to identify the notification
+ value: PropTypes.string.isRequired,
+
+ // URL of image to appear on the notification. If "" then an icon
+ // appropriate for the priority level is used.
+ image: PropTypes.string.isRequired,
+
+ // Notification priority; see Priority Levels.
+ priority: PropTypes.number.isRequired,
+
+ // Array of button descriptions to appear on the notification.
+ buttons: PropTypes.arrayOf(PropTypes.shape({
+ // Function to be called when the button is activated.
+ // This function is passed three arguments:
+ // 1) the NotificationBox component the button is associated with
+ // 2) the button description as passed to appendNotification.
+ // 3) the element which was the target of the button press event.
+ // If the return value from this function is not True, then the
+ // notification is closed. The notification is also not closed
+ // if an error is thrown.
+ callback: PropTypes.func.isRequired,
+
+ // The label to appear on the button.
+ label: PropTypes.string.isRequired,
+
+ // The accesskey attribute set on the <button> element.
+ accesskey: PropTypes.string,
+ })),
+
+ // A function to call to notify you of interesting things that happen
+ // with the notification box.
+ eventCallback: PropTypes.func,
})),
- // A function to call to notify you of interesting things that happen
- // with the notification box.
- eventCallback: PropTypes.func,
- })),
+ // Message that should be shown when hovering over the close button
+ closeButtonTooltip: PropTypes.string
+ };
+ }
- // Message that should be shown when hovering over the close button
- closeButtonTooltip: PropTypes.string
- },
-
- getDefaultProps() {
+ static get defaultProps() {
return {
closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip")
};
- },
+ }
- getInitialState() {
- return {
+ constructor(props) {
+ super(props);
+
+ this.state = {
notifications: new Immutable.OrderedMap()
};
- },
+
+ this.appendNotification = this.appendNotification.bind(this);
+ this.removeNotification = this.removeNotification.bind(this);
+ this.getNotificationWithValue = this.getNotificationWithValue.bind(this);
+ this.getCurrentNotification = this.getCurrentNotification.bind(this);
+ this.close = this.close.bind(this);
+ this.renderButton = this.renderButton.bind(this);
+ this.renderNotification = this.renderNotification.bind(this);
+ }
/**
* Create a new notification and display it. If another notification is
* already present with a higher priority, the new notification will be
* added behind it. See `propTypes` for arguments description.
*/
- appendNotification(label, value, image, priority, buttons = [],
- eventCallback) {
+ appendNotification(label, value, image, priority, buttons = [], eventCallback) {
// Priority level must be within expected interval
// (see priority levels at the top of this file).
if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK) {
throw new Error("Invalid notification priority " + priority);
}
// Custom image URL is not supported yet.
@@ -132,24 +141,24 @@ var NotificationBox = createClass({
// High priorities must be on top.
notifications = notifications.sortBy((val, key) => {
return -val.priority;
});
this.setState({
notifications: notifications
});
- },
+ }
/**
* Remove specific notification from the list.
*/
removeNotification(notification) {
this.close(this.state.notifications.get(notification.value));
- },
+ }
/**
* Returns an object that represents a notification. It can be
* used to close it.
*/
getNotificationWithValue(value) {
let notification = this.state.notifications.get(value);
if (!notification) {
@@ -158,38 +167,38 @@ var NotificationBox = createClass({
// Return an object that can be used to remove the notification
// later (using `removeNotification` method) or directly close it.
return Object.assign({}, notification, {
close: () => {
this.close(notification);
}
});
- },
+ }
getCurrentNotification() {
return this.state.notifications.first();
- },
+ }
/**
* Close specified notification.
*/
close(notification) {
if (!notification) {
return;
}
if (notification.eventCallback) {
notification.eventCallback("removed");
}
this.setState({
notifications: this.state.notifications.remove(notification.value)
});
- },
+ }
/**
* Render a button. A notification can have a set of custom buttons.
* These are used to execute custom callback.
*/
renderButton(props, notification) {
let onClick = event => {
if (props.callback) {
@@ -205,17 +214,17 @@ var NotificationBox = createClass({
button({
key: props.label,
className: "notification-button",
accesskey: props.accesskey,
onClick: onClick},
props.label
)
);
- },
+ }
/**
* Render a notification.
*/
renderNotification(notification) {
return (
div({
key: notification.value,
@@ -236,28 +245,28 @@ var NotificationBox = createClass({
div({
className: "messageCloseButton",
title: this.props.closeButtonTooltip,
onClick: this.close.bind(this, notification)}
)
)
)
);
- },
+ }
/**
* Render the top (highest priority) notification. Only one
* notification is rendered at a time.
*/
render() {
let notification = this.state.notifications.first();
let content = notification ?
this.renderNotification(notification) :
null;
return div({className: "notificationbox"},
content
);
- },
-});
+ }
+}
module.exports.NotificationBox = NotificationBox;
module.exports.PriorityLevels = PriorityLevels;
--- a/devtools/client/shared/components/SearchBox.js
+++ b/devtools/client/shared/components/SearchBox.js
@@ -1,66 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global window */
"use strict";
-const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { DOM: dom, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const AutocompletePopup = createFactory(require("devtools/client/shared/components/AutoCompletePopup"));
-/**
- * A generic search box component for use across devtools
- */
-module.exports = createClass({
- displayName: "SearchBox",
+class SearchBox extends Component {
+ static get propTypes() {
+ return {
+ delay: PropTypes.number,
+ keyShortcut: PropTypes.string,
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ type: PropTypes.string,
+ autocompleteProvider: PropTypes.func,
+ };
+ }
- propTypes: {
- delay: PropTypes.number,
- keyShortcut: PropTypes.string,
- onChange: PropTypes.func,
- placeholder: PropTypes.string,
- type: PropTypes.string,
- autocompleteProvider: PropTypes.func,
- },
+ constructor(props) {
+ super(props);
- getInitialState() {
- return {
+ this.state = {
value: "",
focused: false,
};
- },
+
+ this.onChange = this.onChange.bind(this);
+ this.onClearButtonClick = this.onClearButtonClick.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ }
componentDidMount() {
if (!this.props.keyShortcut) {
return;
}
this.shortcuts = new KeyShortcuts({
window
});
this.shortcuts.on(this.props.keyShortcut, (name, event) => {
event.preventDefault();
this.refs.input.focus();
});
- },
+ }
componentWillUnmount() {
if (this.shortcuts) {
this.shortcuts.destroy();
}
// Clean up an existing timeout.
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
- },
+ }
onChange() {
if (this.state.value !== this.refs.input.value) {
this.setState({
focused: true,
value: this.refs.input.value,
});
}
@@ -76,30 +81,30 @@ module.exports = createClass({
}
// Execute the search after a timeout. It makes the UX
// smoother if the user is typing quickly.
this.searchTimeout = setTimeout(() => {
this.searchTimeout = null;
this.props.onChange(this.state.value);
}, this.props.delay);
- },
+ }
onClearButtonClick() {
this.refs.input.value = "";
this.onChange();
- },
+ }
onFocus() {
this.setState({ focused: true });
- },
+ }
onBlur() {
this.setState({ focused: false });
- },
+ }
onKeyDown(e) {
let { autocomplete } = this.refs;
if (!autocomplete || autocomplete.state.list.length <= 0) {
return;
}
switch (e.key) {
@@ -126,17 +131,17 @@ module.exports = createClass({
break;
case "Home":
autocomplete.jumpToTop();
break;
case "End":
autocomplete.jumpToBottom();
break;
}
- },
+ }
render() {
let {
type = "search",
placeholder,
autocompleteProvider,
} = this.props;
let { value } = this.state;
@@ -170,9 +175,11 @@ module.exports = createClass({
ref: "autocomplete",
onItemSelected: (itemValue) => {
this.setState({ value: itemValue });
this.onChange();
}
})
);
}
-});
+}
+
+module.exports = SearchBox;
--- a/devtools/client/shared/components/SidebarToggle.js
+++ b/devtools/client/shared/components/SidebarToggle.js
@@ -1,54 +1,58 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
-const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
+const { DOM, Component, PropTypes } = require("devtools/client/shared/vendor/react");
// Shortcuts
const { button } = DOM;
/**
* Sidebar toggle button. This button is used to exapand
* and collapse Sidebar.
*/
-var SidebarToggle = createClass({
- displayName: "SidebarToggle",
+class SidebarToggle extends Component {
+ static get propTypes() {
+ return {
+ // Set to true if collapsed.
+ collapsed: PropTypes.bool.isRequired,
+ // Tooltip text used when the button indicates expanded state.
+ collapsePaneTitle: PropTypes.string.isRequired,
+ // Tooltip text used when the button indicates collapsed state.
+ expandPaneTitle: PropTypes.string.isRequired,
+ // Click callback
+ onClick: PropTypes.func.isRequired,
+ };
+ }
- propTypes: {
- // Set to true if collapsed.
- collapsed: PropTypes.bool.isRequired,
- // Tooltip text used when the button indicates expanded state.
- collapsePaneTitle: PropTypes.string.isRequired,
- // Tooltip text used when the button indicates collapsed state.
- expandPaneTitle: PropTypes.string.isRequired,
- // Click callback
- onClick: PropTypes.func.isRequired,
- },
+ constructor(props) {
+ super(props);
- getInitialState: function () {
- return {
- collapsed: this.props.collapsed,
+ this.state = {
+ collapsed: props.collapsed,
};
- },
+
+ this.onClick = this.onClick.bind(this);
+ }
// Events
- onClick: function (event) {
+ onClick(event) {
this.props.onClick(event);
- },
+ }
// Rendering
- render: function () {
+ render() {
let title = this.state.collapsed ?
this.props.expandPaneTitle :
this.props.collapsePaneTitle;
let classNames = ["devtools-button", "sidebar-toggle"];
if (this.state.collapsed) {
classNames.push("pane-collapsed");
}
@@ -56,11 +60,11 @@ var SidebarToggle = createClass({
return (
button({
className: classNames.join(" "),
title: title,
onClick: this.onClick
})
);
}
-});
+}
module.exports = SidebarToggle;
--- a/devtools/client/shared/components/StackTrace.js
+++ b/devtools/client/shared/components/StackTrace.js
@@ -1,48 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
-const { DOM: dom, createClass, createFactory, PropTypes } = React;
+const { DOM: dom, Component, createFactory, PropTypes } = React;
const { LocalizationHelper } = require("devtools/shared/l10n");
const Frame = createFactory(require("./Frame"));
const l10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
-const AsyncFrame = createFactory(createClass({
- displayName: "AsyncFrame",
-
- propTypes: {
- asyncCause: PropTypes.string.isRequired
- },
+class AsyncFrameClass extends Component {
+ static get propTypes() {
+ return {
+ asyncCause: PropTypes.string.isRequired
+ };
+ }
render() {
let { asyncCause } = this.props;
return dom.span(
{ className: "frame-link-async-cause" },
l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
);
}
-}));
-
-const StackTrace = createClass({
- displayName: "StackTrace",
+}
- propTypes: {
- stacktrace: PropTypes.array.isRequired,
- onViewSourceInDebugger: PropTypes.func.isRequired,
- onViewSourceInScratchpad: PropTypes.func,
- // Service to enable the source map feature.
- sourceMapService: PropTypes.object,
- },
+class StackTrace extends Component {
+ static get propTypes() {
+ return {
+ stacktrace: PropTypes.array.isRequired,
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ onViewSourceInScratchpad: PropTypes.func,
+ // Service to enable the source map feature.
+ sourceMapService: PropTypes.object,
+ };
+ }
render() {
let {
stacktrace,
onViewSourceInDebugger,
onViewSourceInScratchpad,
sourceMapService,
} = this.props;
@@ -72,11 +72,13 @@ const StackTrace = createClass({
? onViewSourceInScratchpad
: onViewSourceInDebugger,
sourceMapService,
}), "\n");
});
return dom.div({ className: "stack-trace" }, frames);
}
-});
+}
+
+const AsyncFrame = createFactory(AsyncFrameClass);
module.exports = StackTrace;
--- a/devtools/client/shared/components/Tree.js
+++ b/devtools/client/shared/components/Tree.js
@@ -1,16 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env browser */
"use strict";
const React = require("devtools/client/shared/vendor/react");
-const { DOM: dom, createClass, createFactory, PropTypes } = React;
+const { DOM: dom, Component, createFactory, PropTypes } = React;
const AUTO_EXPAND_DEPTH = 0;
const NUMBER_OF_OFFSCREEN_ITEMS = 1;
/**
* A fast, generic, expandable and collapsible tree component.
*
* This tree component is fast: it can handle trees with *many* items. It only
@@ -92,168 +92,186 @@ const NUMBER_OF_OFFSCREEN_ITEMS = 1;
* },
*
* onExpand: item => dispatchExpandActionToRedux(item),
* onCollapse: item => dispatchCollapseActionToRedux(item),
* });
* }
* });
*/
-module.exports = createClass({
- displayName: "Tree",
-
- propTypes: {
- // Required props
+class Tree extends Component {
+ static get propTypes() {
+ return {
+ // Required props
- // A function to get an item's parent, or null if it is a root.
- //
- // Type: getParent(item: Item) -> Maybe<Item>
- //
- // Example:
- //
- // // The parent of this item is stored in its `parent` property.
- // getParent: item => item.parent
- getParent: PropTypes.func.isRequired,
+ // A function to get an item's parent, or null if it is a root.
+ //
+ // Type: getParent(item: Item) -> Maybe<Item>
+ //
+ // Example:
+ //
+ // // The parent of this item is stored in its `parent` property.
+ // getParent: item => item.parent
+ getParent: PropTypes.func.isRequired,
- // A function to get an item's children.
- //
- // Type: getChildren(item: Item) -> [Item]
- //
- // Example:
- //
- // // This item's children are stored in its `children` property.
- // getChildren: item => item.children
- getChildren: PropTypes.func.isRequired,
+ // A function to get an item's children.
+ //
+ // Type: getChildren(item: Item) -> [Item]
+ //
+ // Example:
+ //
+ // // This item's children are stored in its `children` property.
+ // getChildren: item => item.children
+ getChildren: PropTypes.func.isRequired,
- // A function which takes an item and ArrowExpander component instance and
- // returns a component, or text, or anything else that React considers
- // renderable.
- //
- // Type: renderItem(item: Item,
- // depth: Number,
- // isFocused: Boolean,
- // arrow: ReactComponent,
- // isExpanded: Boolean) -> ReactRenderable
- //
- // Example:
- //
- // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
- // let className = "my-tree-item";
- // if (isFocused) {
- // className += " focused";
- // }
- // return dom.div(
- // {
- // className,
- // style: { marginLeft: depth * 10 + "px" }
- // },
- // arrow,
- // dom.span({ className: "my-tree-item-label" }, item.label)
- // );
- // },
- renderItem: PropTypes.func.isRequired,
+ // A function which takes an item and ArrowExpander component instance and
+ // returns a component, or text, or anything else that React considers
+ // renderable.
+ //
+ // Type: renderItem(item: Item,
+ // depth: Number,
+ // isFocused: Boolean,
+ // arrow: ReactComponent,
+ // isExpanded: Boolean) -> ReactRenderable
+ //
+ // Example:
+ //
+ // renderItem: (item, depth, isFocused, arrow, isExpanded) => {
+ // let className = "my-tree-item";
+ // if (isFocused) {
+ // className += " focused";
+ // }
+ // return dom.div(
+ // {
+ // className,
+ // style: { marginLeft: depth * 10 + "px" }
+ // },
+ // arrow,
+ // dom.span({ className: "my-tree-item-label" }, item.label)
+ // );
+ // },
+ renderItem: PropTypes.func.isRequired,
- // A function which returns the roots of the tree (forest).
- //
- // Type: getRoots() -> [Item]
- //
- // Example:
- //
- // // In this case, we only have one top level, root item. You could
- // // return multiple items if you have many top level items in your
- // // tree.
- // getRoots: () => [this.props.rootOfMyTree]
- getRoots: PropTypes.func.isRequired,
+ // A function which returns the roots of the tree (forest).
+ //
+ // Type: getRoots() -> [Item]
+ //
+ // Example:
+ //
+ // // In this case, we only have one top level, root item. You could
+ // // return multiple items if you have many top level items in your
+ // // tree.
+ // getRoots: () => [this.props.rootOfMyTree]
+ getRoots: PropTypes.func.isRequired,
- // A function to get a unique key for the given item. This helps speed up
- // React's rendering a *TON*.
- //
- // Type: getKey(item: Item) -> String
- //
- // Example:
- //
- // getKey: item => `my-tree-item-${item.uniqueId}`
- getKey: PropTypes.func.isRequired,
+ // A function to get a unique key for the given item. This helps speed up
+ // React's rendering a *TON*.
+ //
+ // Type: getKey(item: Item) -> String
+ //
+ // Example:
+ //
+ // getKey: item => `my-tree-item-${item.uniqueId}`
+ getKey: PropTypes.func.isRequired,
- // A function to get whether an item is expanded or not. If an item is not
- // expanded, then it must be collapsed.
- //
- // Type: isExpanded(item: Item) -> Boolean
- //
- // Example:
- //
- // isExpanded: item => item.expanded,
- isExpanded: PropTypes.func.isRequired,
+ // A function to get whether an item is expanded or not. If an item is not
+ // expanded, then it must be collapsed.
+ //
+ // Type: isExpanded(item: Item) -> Boolean
+ //
+ // Example:
+ //
+ // isExpanded: item => item.expanded,
+ isExpanded: PropTypes.func.isRequired,
- // The height of an item in the tree including margin and padding, in
- // pixels.
- itemHeight: PropTypes.number.isRequired,
+ // The height of an item in the tree including margin and padding, in
+ // pixels.
+ itemHeight: PropTypes.number.isRequired,
- // Optional props
+ // Optional props
- // The currently focused item, if any such item exists.
- focused: PropTypes.any,
+ // The currently focused item, if any such item exists.
+ focused: PropTypes.any,
- // Handle when a new item is focused.
- onFocus: PropTypes.func,
+ // Handle when a new item is focused.
+ onFocus: PropTypes.func,
- // The depth to which we should automatically expand new items.
- autoExpandDepth: PropTypes.number,
+ // The depth to which we should automatically expand new items.
+ autoExpandDepth: PropTypes.number,
- // Note: the two properties below are mutually exclusive. Only one of the
- // label properties is necessary.
- // ID of an element whose textual content serves as an accessible label for
- // a tree.
- labelledby: PropTypes.string,
- // Accessibility label for a tree widget.
- label: PropTypes.string,
+ // Note: the two properties below are mutually exclusive. Only one of the
+ // label properties is necessary.
+ // ID of an element whose textual content serves as an accessible label for
+ // a tree.
+ labelledby: PropTypes.string,
+ // Accessibility label for a tree widget.
+ label: PropTypes.string,
- // Optional event handlers for when items are expanded or collapsed. Useful
- // for dispatching redux events and updating application state, maybe lazily
- // loading subtrees from a worker, etc.
- //
- // Type:
- // onExpand(item: Item)
- // onCollapse(item: Item)
- //
- // Example:
- //
- // onExpand: item => dispatchExpandActionToRedux(item)
- onExpand: PropTypes.func,
- onCollapse: PropTypes.func,
- },
+ // Optional event handlers for when items are expanded or collapsed. Useful
+ // for dispatching redux events and updating application state, maybe lazily
+ // loading subtrees from a worker, etc.
+ //
+ // Type:
+ // onExpand(item: Item)
+ // onCollapse(item: Item)
+ //
+ // Example:
+ //
+ // onExpand: item => dispatchExpandActionToRedux(item)
+ onExpand: PropTypes.func,
+ onCollapse: PropTypes.func,
+ };
+ }
- getDefaultProps() {
+ static get defaultProps() {
return {
autoExpandDepth: AUTO_EXPAND_DEPTH,
};
- },
+ }
- getInitialState() {
- return {
+ constructor(props) {
+ super(props);
+
+ this.state = {
scroll: 0,
height: window.innerHeight,
seen: new Set(),
};
- },
+
+ this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
+ this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
+ this._onScroll = oncePerAnimationFrame(this._onScroll).bind(this);
+ this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
+ this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
+ this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(this);
+
+ this._autoExpand = this._autoExpand.bind(this);
+ this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
+ this._updateHeight = this._updateHeight.bind(this);
+ this._dfs = this._dfs.bind(this);
+ this._dfsFromRoots = this._dfsFromRoots.bind(this);
+ this._focus = this._focus.bind(this);
+ this._onBlur = this._onBlur.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
componentDidMount() {
window.addEventListener("resize", this._updateHeight);
this._autoExpand();
this._updateHeight();
- },
+ }
componentWillReceiveProps(nextProps) {
this._autoExpand();
this._updateHeight();
- },
+ }
componentWillUnmount() {
window.removeEventListener("resize", this._updateHeight);
- },
+ }
_autoExpand() {
if (!this.props.autoExpandDepth) {
return;
}
// Automatically expand the first autoExpandDepth levels for new items. Do
// not use the usual DFS infrastructure because we don't want to ignore
@@ -274,17 +292,17 @@ module.exports = createClass({
}
};
const roots = this.props.getRoots();
const length = roots.length;
for (let i = 0; i < length; i++) {
autoExpand(roots[i], 0);
}
- },
+ }
_preventArrowKeyScrolling(e) {
switch (e.key) {
case "ArrowUp":
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
e.preventDefault();
@@ -293,26 +311,26 @@ module.exports = createClass({
if (e.nativeEvent.preventDefault) {
e.nativeEvent.preventDefault();
}
if (e.nativeEvent.stopPropagation) {
e.nativeEvent.stopPropagation();
}
}
}
- },
+ }
/**
* Updates the state's height based on clientHeight.
*/
_updateHeight() {
this.setState({
height: this.refs.tree.clientHeight
});
- },
+ }
/**
* Perform a pre-order depth-first search from item.
*/
_dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
traversal.push({ item, depth: _depth });
if (!this.props.isExpanded(item)) {
@@ -327,63 +345,63 @@ module.exports = createClass({
const children = this.props.getChildren(item);
const length = children.length;
for (let i = 0; i < length; i++) {
this._dfs(children[i], maxDepth, traversal, nextDepth);
}
return traversal;
- },
+ }
/**
* Perform a pre-order depth-first search over the whole forest.
*/
_dfsFromRoots(maxDepth = Infinity) {
const traversal = [];
const roots = this.props.getRoots();
const length = roots.length;
for (let i = 0; i < length; i++) {
this._dfs(roots[i], maxDepth, traversal);
}
return traversal;
- },
+ }
/**
* Expands current row.
*
* @param {Object} item
* @param {Boolean} expandAllChildren
*/
- _onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
+ _onExpand(item, expandAllChildren) {
if (this.props.onExpand) {
this.props.onExpand(item);
if (expandAllChildren) {
const children = this._dfs(item);
const length = children.length;
for (let i = 0; i < length; i++) {
this.props.onExpand(children[i].item);
}
}
}
- }),
+ }
/**
* Collapses current row.
*
* @param {Object} item
*/
- _onCollapse: oncePerAnimationFrame(function (item) {
+ _onCollapse(item) {
if (this.props.onCollapse) {
this.props.onCollapse(item);
}
- }),
+ }
/**
* Sets the passed in item to be the focused item.
*
* @param {Number} index
* The index of the item in a full DFS traversal (ignoring collapsed
* nodes). Ignored if `item` is undefined.
*
@@ -406,37 +424,37 @@ module.exports = createClass({
} else if ((this.state.scroll + this.state.height) < itemEndPosition) {
this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
}
}
if (this.props.onFocus) {
this.props.onFocus(item);
}
- },
+ }
/**
* Sets the state to have no focused item.
*/
_onBlur() {
this._focus(0, undefined);
- },
+ }
/**
* Fired on a scroll within the tree's container, updates
* the stored position of the view port to handle virtual view rendering.
*
* @param {Event} e
*/
- _onScroll: oncePerAnimationFrame(function (e) {
+ _onScroll(e) {
this.setState({
scroll: Math.max(this.refs.tree.scrollTop, 0),
height: this.refs.tree.clientHeight
});
- }),
+ }
/**
* Handles key down events in the tree's container.
*
* @param {Event} e
*/
_onKeyDown(e) {
if (this.props.focused == null) {
@@ -471,22 +489,22 @@ module.exports = createClass({
case "ArrowRight":
if (!this.props.isExpanded(this.props.focused)) {
this._onExpand(this.props.focused);
} else {
this._focusNextNode();
}
break;
}
- },
+ }
/**
* Sets the previous node relative to the currently focused item, to focused.
*/
- _focusPrevNode: oncePerAnimationFrame(function () {
+ _focusPrevNode() {
// Start a depth first search and keep going until we reach the currently
// focused node. Focus the previous node in the DFS, if it exists. If it
// doesn't exist, we're at the first node already.
let prev;
let prevIndex;
const traversal = this._dfsFromRoots();
@@ -500,23 +518,23 @@ module.exports = createClass({
prevIndex = i;
}
if (prev === undefined) {
return;
}
this._focus(prevIndex, prev);
- }),
+ }
/**
* Handles the down arrow key which will focus either the next child
* or sibling row.
*/
- _focusNextNode: oncePerAnimationFrame(function () {
+ _focusNextNode() {
// Start a depth first search and keep going until we reach the currently
// focused node. Focus the next node in the DFS, if it exists. If it
// doesn't exist, we're at the last node already.
const traversal = this._dfsFromRoots();
const length = traversal.length;
let i = 0;
@@ -525,39 +543,39 @@ module.exports = createClass({
break;
}
i++;
}
if (i + 1 < traversal.length) {
this._focus(i + 1, traversal[i + 1].item);
}
- }),
+ }
/**
* Handles the left arrow key, going back up to the current rows'
* parent row.
*/
- _focusParentNode: oncePerAnimationFrame(function () {
+ _focusParentNode() {
const parent = this.props.getParent(this.props.focused);
if (!parent) {
return;
}
const traversal = this._dfsFromRoots();
const length = traversal.length;
let parentIndex = 0;
for (; parentIndex < length; parentIndex++) {
if (traversal[parentIndex].item === parent) {
break;
}
}
this._focus(parentIndex, parent);
- }),
+ }
render() {
const traversal = this._dfsFromRoots();
// 'begin' and 'end' are the index of the first (at least partially) visible item
// and the index after the last (at least partially) visible item, respectively.
// `NUMBER_OF_OFFSCREEN_ITEMS` is removed from `begin` and added to `end` so that
// the top and bottom of the page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS`
@@ -651,38 +669,38 @@ module.exports = createClass({
style: {
padding: 0,
margin: 0
}
},
nodes
);
}
-});
+}
/**
* An arrow that displays whether its node is expanded (â–¼) or collapsed
* (â–¶). When its node has no children, it is hidden.
*/
-const ArrowExpander = createFactory(createClass({
- displayName: "ArrowExpander",
-
- propTypes: {
- item: PropTypes.any.isRequired,
- visible: PropTypes.bool.isRequired,
- expanded: PropTypes.bool.isRequired,
- onCollapse: PropTypes.func.isRequired,
- onExpand: PropTypes.func.isRequired,
- },
+class ArrowExpanderClass extends Component {
+ static get propTypes() {
+ return {
+ item: PropTypes.any.isRequired,
+ visible: PropTypes.bool.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ onCollapse: PropTypes.func.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ };
+ }
shouldComponentUpdate(nextProps, nextState) {
return this.props.item !== nextProps.item
|| this.props.visible !== nextProps.visible
|| this.props.expanded !== nextProps.expanded;
- },
+ }
render() {
const attrs = {
className: "arrow theme-twisty",
onClick: this.props.expanded
? () => this.props.onCollapse(this.props.item)
: e => this.props.onExpand(this.props.item, e.altKey)
};
@@ -694,34 +712,36 @@ const ArrowExpander = createFactory(crea
if (!this.props.visible) {
attrs.style = {
visibility: "hidden"
};
}
return dom.div(attrs);
}
-}));
+}
-const TreeNode = createFactory(createClass({
- propTypes: {
- id: PropTypes.any.isRequired,
- focused: PropTypes.bool.isRequired,
- item: PropTypes.any.isRequired,
- expanded: PropTypes.bool.isRequired,
- hasChildren: PropTypes.bool.isRequired,
- onExpand: PropTypes.func.isRequired,
- index: PropTypes.number.isRequired,
- first: PropTypes.bool,
- last: PropTypes.bool,
- onClick: PropTypes.func,
- onCollapse: PropTypes.func.isRequired,
- depth: PropTypes.number.isRequired,
- renderItem: PropTypes.func.isRequired,
- },
+class TreeNodeClass extends Component {
+ static get propTypes() {
+ return {
+ id: PropTypes.any.isRequired,
+ focused: PropTypes.bool.isRequired,
+ item: PropTypes.any.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ hasChildren: PropTypes.bool.isRequired,
+ onExpand: PropTypes.func.isRequired,
+ index: PropTypes.number.isRequired,
+ first: PropTypes.bool,
+ last: PropTypes.bool,
+ onClick: PropTypes.func,
+ onCollapse: PropTypes.func.isRequired,
+ depth: PropTypes.number.isRequired,
+ renderItem: PropTypes.func.isRequired,
+ };
+ }
render() {
const arrow = ArrowExpander({
item: this.props.item,
expanded: this.props.expanded,
visible: this.props.hasChildren,
onExpand: this.props.onExpand,
onCollapse: this.props.onCollapse,
@@ -764,17 +784,20 @@ const TreeNode = createFactory(createCla
this.props.renderItem(this.props.item,
this.props.depth,
this.props.focused,
arrow,
this.props.expanded),
);
}
-}));
+}
+
+const ArrowExpander = createFactory(ArrowExpanderClass);
+const TreeNode = createFactory(TreeNodeClass);
/**
* Create a function that calls the given function `fn` only once per animation
* frame.
*
* @param {Function} fn
* @returns {Function}
*/
@@ -789,8 +812,10 @@ function oncePerAnimationFrame(fn) {
animationId = requestAnimationFrame(() => {
fn.call(this, ...argsToPass);
animationId = null;
argsToPass = null;
});
};
}
+
+module.exports = Tree;
--- a/devtools/client/shared/components/splitter/Draggable.js
+++ b/devtools/client/shared/components/splitter/Draggable.js
@@ -1,54 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-const { DOM: dom, PropTypes } = React;
-
-const Draggable = React.createClass({
- displayName: "Draggable",
+const { Component, DOM: dom, PropTypes } = React;
- propTypes: {
- onMove: PropTypes.func.isRequired,
- onStart: PropTypes.func,
- onStop: PropTypes.func,
- style: PropTypes.object,
- className: PropTypes.string
- },
+class Draggable extends Component {
+ static get propTypes() {
+ return {
+ onMove: PropTypes.func.isRequired,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ className: PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.startDragging = this.startDragging.bind(this);
+ this.onMove = this.onMove.bind(this);
+ this.onUp = this.onUp.bind(this);
+ }
startDragging(ev) {
ev.preventDefault();
const doc = ReactDOM.findDOMNode(this).ownerDocument;
doc.addEventListener("mousemove", this.onMove);
doc.addEventListener("mouseup", this.onUp);
this.props.onStart && this.props.onStart();
- },
+ }
onMove(ev) {
ev.preventDefault();
// Use viewport coordinates so, moving mouse over iframes
// doesn't mangle (relative) coordinates.
this.props.onMove(ev.clientX, ev.clientY);
- },
+ }
onUp(ev) {
ev.preventDefault();
const doc = ReactDOM.findDOMNode(this).ownerDocument;
doc.removeEventListener("mousemove", this.onMove);
doc.removeEventListener("mouseup", this.onUp);
this.props.onStop && this.props.onStop();
- },
+ }
render() {
return dom.div({
style: this.props.style,
className: this.props.className,
onMouseDown: this.startDragging
});
}
-});
+}
module.exports = Draggable;
--- a/devtools/client/shared/components/splitter/SplitBox.js
+++ b/devtools/client/shared/components/splitter/SplitBox.js
@@ -2,92 +2,98 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/Draggable"));
-const { DOM: dom, PropTypes } = React;
+const { Component, DOM: dom, PropTypes } = React;
/**
* This component represents a Splitter. The splitter supports vertical
* as well as horizontal mode.
*/
-const SplitBox = React.createClass({
- displayName: "SplitBox",
+class SplitBox extends Component {
+ static get propTypes() {
+ return {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.string,
+ // Initial width of controlled panel.
+ initialWidth: PropTypes.string,
+ // Initial height of controlled panel.
+ initialHeight: PropTypes.string,
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Min panel size.
+ minSize: PropTypes.string,
+ // Max panel size.
+ maxSize: PropTypes.string,
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.string,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool,
+ // Style object.
+ style: PropTypes.object,
+ };
+ }
- propTypes: {
- // Custom class name. You can use more names separated by a space.
- className: PropTypes.string,
- // Initial size of controlled panel.
- initialSize: PropTypes.string,
- // Initial width of controlled panel.
- initialWidth: PropTypes.string,
- // Initial height of controlled panel.
- initialHeight: PropTypes.string,
- // Left/top panel
- startPanel: PropTypes.any,
- // Min panel size.
- minSize: PropTypes.string,
- // Max panel size.
- maxSize: PropTypes.string,
- // Right/bottom panel
- endPanel: PropTypes.any,
- // True if the right/bottom panel should be controlled.
- endPanelControl: PropTypes.bool,
- // Size of the splitter handle bar.
- splitterSize: PropTypes.string,
- // True if the splitter bar is vertical (default is vertical).
- vert: PropTypes.bool,
- // Style object.
- style: PropTypes.object,
- },
-
- getDefaultProps() {
+ static get defaultProps() {
return {
splitterSize: 5,
vert: true,
endPanelControl: false
};
- },
+ }
+
+ constructor(props) {
+ super(props);
- /**
- * The state stores the current orientation (vertical or horizontal)
- * and the current size (width/height). All these values can change
- * during the component's life time.
- */
- getInitialState() {
- return {
- vert: this.props.vert,
- width: this.props.initialWidth || this.props.initialSize,
- height: this.props.initialHeight || this.props.initialSize
+ /**
+ * The state stores the current orientation (vertical or horizontal)
+ * and the current size (width/height). All these values can change
+ * during the component's life time.
+ */
+ this.state = {
+ vert: props.vert,
+ width: props.initialWidth || props.initialSize,
+ height: props.initialHeight || props.initialSize
};
- },
+
+ this.onStartMove = this.onStartMove.bind(this);
+ this.onStopMove = this.onStopMove.bind(this);
+ this.onMove = this.onMove.bind(this);
+ }
componentWillReceiveProps(nextProps) {
let { vert } = nextProps;
if (vert !== this.props.vert) {
this.setState({ vert });
}
- },
+ }
shouldComponentUpdate(nextProps, nextState) {
return nextState.width != this.state.width ||
nextState.height != this.state.height ||
nextState.vert != this.state.vert ||
nextProps.startPanel != this.props.startPanel ||
nextProps.endPanel != this.props.endPanel ||
nextProps.endPanelControl != this.props.endPanelControl ||
nextProps.minSize != this.props.minSize ||
nextProps.maxSize != this.props.maxSize ||
nextProps.splitterSize != this.props.splitterSize;
- },
+ }
// Dragging Events
/**
* Set 'resizing' cursor on entire document during splitter dragging.
* This avoids cursor-flickering that happens when the mouse leaves
* the splitter bar area (happens frequently).
*/
@@ -97,25 +103,25 @@ const SplitBox = React.createClass({
let defaultCursor = doc.documentElement.style.cursor;
doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
splitBox.classList.add("dragging");
this.setState({
defaultCursor: defaultCursor
});
- },
+ }
onStopMove() {
const splitBox = ReactDOM.findDOMNode(this);
const doc = splitBox.ownerDocument;
doc.documentElement.style.cursor = this.state.defaultCursor;
splitBox.classList.remove("dragging");
- },
+ }
/**
* Adjust size of the controlled panel. Depending on the current
* orientation we either remember the width or height of
* the splitter box.
*/
onMove(x, y) {
const node = ReactDOM.findDOMNode(this);
@@ -144,17 +150,17 @@ const SplitBox = React.createClass({
size = endPanelControl ?
(node.offsetTop + node.offsetHeight) - y :
y - node.offsetTop;
this.setState({
height: size
});
}
- },
+ }
// Rendering
render() {
const vert = this.state.vert;
const { startPanel, endPanel, endPanelControl, minSize,
maxSize, splitterSize } = this.props;
@@ -221,11 +227,11 @@ const SplitBox = React.createClass({
dom.div({
className: endPanelControl ? "controlled" : "uncontrolled",
style: rightPanelStyle},
endPanel
) : null
)
);
}
-});
+}
module.exports = SplitBox;
--- a/devtools/client/shared/components/tabs/TabBar.js
+++ b/devtools/client/shared/components/tabs/TabBar.js
@@ -3,87 +3,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env browser */
"use strict";
-const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { DOM, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
const Tabs = createFactory(require("devtools/client/shared/components/tabs/Tabs").Tabs);
const Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
// Shortcuts
const { div } = DOM;
/**
* Renders Tabbar component.
*/
-let Tabbar = createClass({
- displayName: "Tabbar",
+class Tabbar extends Component {
+ static get propTypes() {
+ return {
+ children: PropTypes.array,
+ menuDocument: PropTypes.object,
+ onSelect: PropTypes.func,
+ showAllTabsMenu: PropTypes.bool,
+ activeTabId: PropTypes.string,
+ renderOnlySelected: PropTypes.bool,
+ };
+ }
- propTypes: {
- children: PropTypes.array,
- menuDocument: PropTypes.object,
- onSelect: PropTypes.func,
- showAllTabsMenu: PropTypes.bool,
- activeTabId: PropTypes.string,
- renderOnlySelected: PropTypes.bool,
- },
-
- getDefaultProps: function () {
+ static get defaultProps() {
return {
menuDocument: window.parent.document,
showAllTabsMenu: false,
};
- },
+ }
- getInitialState: function () {
- let { activeTabId, children = [] } = this.props;
+ constructor(props, context) {
+ super(props, context);
+ let { activeTabId, children = [] } = props;
let tabs = this.createTabs(children);
let activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
- return {
+ this.state = {
activeTab: activeTab === -1 ? 0 : activeTab,
tabs,
};
- },
- componentWillReceiveProps: function (nextProps) {
+ this.createTabs = this.createTabs.bind(this);
+ this.addTab = this.addTab.bind(this);
+ this.toggleTab = this.toggleTab.bind(this);
+ this.removeTab = this.removeTab.bind(this);
+ this.select = this.select.bind(this);
+ this.getTabIndex = this.getTabIndex.bind(this);
+ this.getTabId = this.getTabId.bind(this);
+ this.getCurrentTabId = this.getCurrentTabId.bind(this);
+ this.onTabChanged = this.onTabChanged.bind(this);
+ this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
+ this.renderTab = this.renderTab.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps) {
let { activeTabId, children = [] } = nextProps;
let tabs = this.createTabs(children);
let activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
if (activeTab !== this.state.activeTab ||
(children !== this.props.children)) {
this.setState({
activeTab: activeTab === -1 ? 0 : activeTab,
tabs,
});
}
- },
+ }
- createTabs: function (children) {
+ createTabs(children) {
return children
.filter((panel) => panel)
.map((panel, index) =>
Object.assign({}, children[index], {
id: panel.props.id || index,
panel,
title: panel.props.title,
})
);
- },
+ }
// Public API
- addTab: function (id, title, selected = false, panel, url, index = -1) {
+ addTab(id, title, selected = false, panel, url, index = -1) {
let tabs = this.state.tabs.slice();
if (index >= 0) {
tabs.splice(index, 0, {id, title, panel, url});
} else {
tabs.push({id, title, panel, url});
}
@@ -95,35 +108,35 @@ let Tabbar = createClass({
newState.activeTab = index >= 0 ? index : tabs.length - 1;
}
this.setState(newState, () => {
if (this.props.onSelect && selected) {
this.props.onSelect(id);
}
});
- },
+ }
- toggleTab: function (tabId, isVisible) {
+ toggleTab(tabId, isVisible) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
}
let tabs = this.state.tabs.slice();
tabs[index] = Object.assign({}, tabs[index], {
isVisible: isVisible
});
this.setState(Object.assign({}, this.state, {
tabs: tabs,
}));
- },
+ }
- removeTab: function (tabId) {
+ removeTab(tabId) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
}
let tabs = this.state.tabs.slice();
tabs.splice(index, 1);
@@ -132,68 +145,68 @@ let Tabbar = createClass({
if (activeTab >= tabs.length) {
activeTab = tabs.length - 1;
}
this.setState(Object.assign({}, this.state, {
tabs,
activeTab,
}));
- },
+ }
- select: function (tabId) {
+ select(tabId) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
}
let newState = Object.assign({}, this.state, {
activeTab: index,
});
this.setState(newState, () => {
if (this.props.onSelect) {
this.props.onSelect(tabId);
}
});
- },
+ }
// Helpers
- getTabIndex: function (tabId) {
+ getTabIndex(tabId) {
let tabIndex = -1;
this.state.tabs.forEach((tab, index) => {
if (tab.id === tabId) {
tabIndex = index;
}
});
return tabIndex;
- },
+ }
- getTabId: function (index) {
+ getTabId(index) {
return this.state.tabs[index].id;
- },
+ }
- getCurrentTabId: function () {
+ getCurrentTabId() {
return this.state.tabs[this.state.activeTab].id;
- },
+ }
// Event Handlers
- onTabChanged: function (index) {
+ onTabChanged(index) {
this.setState({
activeTab: index
});
if (this.props.onSelect) {
this.props.onSelect(this.state.tabs[index].id);
}
- },
+ }
- onAllTabsMenuClick: function (event) {
+ onAllTabsMenuClick(event) {
let menu = new Menu();
let target = event.target;
// Generate list of menu items from the list of tabs.
this.state.tabs.forEach((tab) => {
menu.append(new MenuItem({
label: tab.title,
type: "checkbox",
@@ -209,45 +222,45 @@ let Tabbar = createClass({
// https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
let rect = target.getBoundingClientRect();
let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
menu.popup(rect.left + screenX, rect.bottom + screenY,
{ doc: this.props.menuDocument });
return menu;
- },
+ }
// Rendering
- renderTab: function (tab) {
+ renderTab(tab) {
if (typeof tab.panel === "function") {
return tab.panel({
key: tab.id,
title: tab.title,
id: tab.id,
url: tab.url,
});
}
return tab.panel;
- },
+ }
- render: function () {
+ render() {
let tabs = this.state.tabs.map((tab) => this.renderTab(tab));
return (
div({className: "devtools-sidebar-tabs"},
Tabs({
onAllTabsMenuClick: this.onAllTabsMenuClick,
renderOnlySelected: this.props.renderOnlySelected,
showAllTabsMenu: this.props.showAllTabsMenu,
tabActive: this.state.activeTab,
onAfterChange: this.onTabChanged,
},
tabs
)
)
);
- },
-});
+ }
+}
module.exports = Tabbar;
--- a/devtools/client/shared/components/tabs/Tabs.js
+++ b/devtools/client/shared/components/tabs/Tabs.js
@@ -3,17 +3,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function (require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
- const { DOM } = React;
+ const { Component, DOM } = React;
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
/**
* Renders simple 'tab' widget.
*
* Based on ReactSimpleTabs component
* https://github.com/pedronauck/react-simpletabs
*
@@ -26,89 +26,99 @@ define(function (require, exports, modul
* <li class='tabs-menu-item'>Tab #2</li>
* </ul>
* </nav>
* <div class='panels'>
* The content of active panel here
* </div>
* <div>
*/
- let Tabs = React.createClass({
- displayName: "Tabs",
+ class Tabs extends Component {
+ static get propTypes() {
+ return {
+ className: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.string,
+ React.PropTypes.object
+ ]),
+ tabActive: React.PropTypes.number,
+ onMount: React.PropTypes.func,
+ onBeforeChange: React.PropTypes.func,
+ onAfterChange: React.PropTypes.func,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired,
+ showAllTabsMenu: React.PropTypes.bool,
+ onAllTabsMenuClick: React.PropTypes.func,
- propTypes: {
- className: React.PropTypes.oneOfType([
- React.PropTypes.array,
- React.PropTypes.string,
- React.PropTypes.object
- ]),
- tabActive: React.PropTypes.number,
- onMount: React.PropTypes.func,
- onBeforeChange: React.PropTypes.func,
- onAfterChange: React.PropTypes.func,
- children: React.PropTypes.oneOfType([
- React.PropTypes.array,
- React.PropTypes.element
- ]).isRequired,
- showAllTabsMenu: React.PropTypes.bool,
- onAllTabsMenuClick: React.PropTypes.func,
+ // Set true will only render selected panel on DOM. It's complete
+ // opposite of the created array, and it's useful if panels content
+ // is unpredictable and update frequently.
+ renderOnlySelected: React.PropTypes.bool,
+ };
+ }
- // Set true will only render selected panel on DOM. It's complete
- // opposite of the created array, and it's useful if panels content
- // is unpredictable and update frequently.
- renderOnlySelected: React.PropTypes.bool,
- },
-
- getDefaultProps: function () {
+ static get defaultProps() {
return {
tabActive: 0,
showAllTabsMenu: false,
renderOnlySelected: false,
};
- },
+ }
- getInitialState: function () {
- return {
- tabActive: this.props.tabActive,
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ tabActive: props.tabActive,
// This array is used to store an information whether a tab
// at specific index has already been created (e.g. selected
// at least once).
// If yes, it's rendered even if not currently selected.
// This is because in some cases we don't want to re-create
// tab content when it's being unselected/selected.
// E.g. in case of an iframe being used as a tab-content
// we want the iframe to stay in the DOM.
created: [],
// True if tabs can't fit into available horizontal space.
overflow: false,
};
- },
- componentDidMount: function () {
+ this.onOverflow = this.onOverflow.bind(this);
+ this.onUnderflow = this.onUnderflow.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onClickTab = this.onClickTab.bind(this);
+ this.setActive = this.setActive.bind(this);
+ this.renderMenuItems = this.renderMenuItems.bind(this);
+ this.renderPanels = this.renderPanels.bind(this);
+ }
+
+ componentDidMount() {
let node = findDOMNode(this);
node.addEventListener("keydown", this.onKeyDown);
// Register overflow listeners to manage visibility
// of all-tabs-menu. This menu is displayed when there
// is not enough h-space to render all tabs.
// It allows the user to select a tab even if it's hidden.
if (this.props.showAllTabsMenu) {
node.addEventListener("overflow", this.onOverflow);
node.addEventListener("underflow", this.onUnderflow);
}
let index = this.state.tabActive;
if (this.props.onMount) {
this.props.onMount(index);
}
- },
+ }
- componentWillReceiveProps: function (nextProps) {
+ componentWillReceiveProps(nextProps) {
let { children, tabActive } = nextProps;
// Check type of 'tabActive' props to see if it's valid
// (it's 0-based index).
if (typeof tabActive === "number") {
let panels = children.filter((panel) => panel);
// Reset to index 0 if index overflows the range of panel array
@@ -118,47 +128,47 @@ define(function (require, exports, modul
let created = [...this.state.created];
created[tabActive] = true;
this.setState({
created,
tabActive,
});
}
- },
+ }
- componentWillUnmount: function () {
+ componentWillUnmount() {
let node = findDOMNode(this);
node.removeEventListener("keydown", this.onKeyDown);
if (this.props.showAllTabsMenu) {
node.removeEventListener("overflow", this.onOverflow);
node.removeEventListener("underflow", this.onUnderflow);
}
- },
+ }
// DOM Events
- onOverflow: function (event) {
+ onOverflow(event) {
if (event.target.classList.contains("tabs-menu")) {
this.setState({
overflow: true
});
}
- },
+ }
- onUnderflow: function (event) {
+ onUnderflow(event) {
if (event.target.classList.contains("tabs-menu")) {
this.setState({
overflow: false
});
}
- },
+ }
- onKeyDown: function (event) {
+ onKeyDown(event) {
// Bail out if the focus isn't on a tab.
if (!event.target.closest(".tabs-menu-item")) {
return;
}
let tabActive = this.state.tabActive;
let tabCount = this.props.children.length;
@@ -169,29 +179,29 @@ define(function (require, exports, modul
case "ArrowLeft":
tabActive = Math.max(0, tabActive - 1);
break;
}
if (this.state.tabActive != tabActive) {
this.setActive(tabActive);
}
- },
+ }
- onClickTab: function (index, event) {
+ onClickTab(index, event) {
this.setActive(index);
if (event) {
event.preventDefault();
}
- },
+ }
// API
- setActive: function (index) {
+ setActive(index) {
let onAfterChange = this.props.onAfterChange;
let onBeforeChange = this.props.onBeforeChange;
if (onBeforeChange) {
let cancel = onBeforeChange(index);
if (cancel) {
return;
}
@@ -212,21 +222,21 @@ define(function (require, exports, modul
if (selectedTab) {
selectedTab.focus();
}
if (onAfterChange) {
onAfterChange(index);
}
});
- },
+ }
// Rendering
- renderMenuItems: function () {
+ renderMenuItems() {
if (!this.props.children) {
throw new Error("There must be at least one Tab");
}
if (!Array.isArray(this.props.children)) {
this.props.children = [this.props.children];
}
@@ -294,19 +304,19 @@ define(function (require, exports, modul
return (
DOM.nav({className: "tabs-navigation"},
DOM.ul({className: "tabs-menu", role: "tablist"},
tabs
),
allTabsMenu
)
);
- },
+ }
- renderPanels: function () {
+ renderPanels() {
let { children, renderOnlySelected } = this.props;
if (!children) {
throw new Error("There must be at least one Tab");
}
if (!Array.isArray(children)) {
children = [children];
@@ -354,45 +364,45 @@ define(function (require, exports, modul
);
});
return (
DOM.div({className: "panels"},
panels
)
);
- },
+ }
- render: function () {
+ render() {
return (
DOM.div({ className: ["tabs", this.props.className].join(" ") },
this.renderMenuItems(),
this.renderPanels()
)
);
- },
- });
+ }
+ }
/**
* Renders simple tab 'panel'.
*/
- let Panel = React.createClass({
- displayName: "Panel",
+ class Panel extends Component {
+ static get propTypes() {
+ return {
+ title: React.PropTypes.string.isRequired,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.element
+ ]).isRequired
+ };
+ }
- propTypes: {
- title: React.PropTypes.string.isRequired,
- children: React.PropTypes.oneOfType([
- React.PropTypes.array,
- React.PropTypes.element
- ]).isRequired
- },
-
- render: function () {
+ render() {
return DOM.div({className: "tab-panel"},
this.props.children
);
}
- });
+ }
// Exports from this module
exports.TabPanel = Panel;
exports.Tabs = Tabs;
});
--- a/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
@@ -21,18 +21,18 @@ Test all-tabs menu.
</head>
<body>
<pre id="test">
<script src="head.js" type="application/javascript"></script>
<script type="application/javascript">
window.onload = Task.async(function* () {
try {
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
- const React = browserRequire("devtools/client/shared/vendor/react");
- const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
+ const { Component, createFactory, DOM } = browserRequire("devtools/client/shared/vendor/react");
+ const Tabbar = createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
// Create container for the TabBar. Set smaller width
// to ensure that tabs won't fit and the all-tabs menu
// needs to appear.
const tabBarBox = document.createElement("div");
tabBarBox.style.width = "200px";
tabBarBox.style.height = "200px";
tabBarBox.style.border = "1px solid lightgray";
@@ -40,22 +40,24 @@ window.onload = Task.async(function* ()
// Render the tab-bar.
const tabbar = Tabbar({
showAllTabsMenu: true,
});
const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+ class TabPanelClass extends Component {
+ render() {
+ return DOM.div({}, "content");
+ }
+ }
+
// Test panel.
- let TabPanel = React.createFactory(React.createClass({
- render: function () {
- return React.DOM.div({}, "content");
- }
- }));
+ let TabPanel = createFactory(TabPanelClass);
// Create a few panels.
yield addTabWithPanel(1);
yield addTabWithPanel(2);
yield addTabWithPanel(3);
yield addTabWithPanel(4);
yield addTabWithPanel(5);
--- a/devtools/client/shared/components/tree/LabelCell.js
+++ b/devtools/client/shared/components/tree/LabelCell.js
@@ -3,36 +3,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
// ReactJS
- const React = require("devtools/client/shared/vendor/react");
-
- // Shortcuts
- const { td, span } = React.DOM;
- const PropTypes = React.PropTypes;
+ const { Component, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
/**
* Render the default cell used for toggle buttons
*/
- let LabelCell = React.createClass({
- displayName: "LabelCell",
-
+ class LabelCell extends Component {
// See the TreeView component for details related
// to the 'member' object.
- propTypes: {
- id: PropTypes.string.isRequired,
- member: PropTypes.object.isRequired
- },
+ static get propTypes() {
+ return {
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired
+ };
+ }
- render: function () {
+ render() {
let id = this.props.id;
let member = this.props.member;
let level = member.level || 0;
// Compute indentation dynamically. The deeper the item is
// inside the hierarchy, the bigger is the left padding.
let rowStyle = {
"paddingInlineStart": (level * 16) + "px",
@@ -44,30 +41,30 @@ define(function (require, exports, modul
} else if (member.hasChildren) {
iconClassList.push("theme-twisty");
}
if (member.open) {
iconClassList.push("open");
}
return (
- td({
+ dom.td({
className: "treeLabelCell",
key: "default",
style: rowStyle,
role: "presentation"},
- span({
+ dom.span({
className: iconClassList.join(" "),
role: "presentation"
}),
- span({
+ dom.span({
className: "treeLabel " + member.type + "Label",
"aria-labelledby": id,
"data-level": level
}, member.name)
)
);
}
- });
+ }
// Exports from this module
module.exports = LabelCell;
});
--- a/devtools/client/shared/components/tree/TreeCell.js
+++ b/devtools/client/shared/components/tree/TreeCell.js
@@ -3,80 +3,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
-
- // Shortcuts
+ const { Component, PropTypes } = React;
const { input, span, td } = React.DOM;
- const PropTypes = React.PropTypes;
/**
* This template represents a cell in TreeView row. It's rendered
* using <td> element (the row is <tr> and the entire tree is <table>).
*/
- let TreeCell = React.createClass({
- displayName: "TreeCell",
-
+ class TreeCell extends Component {
// See TreeView component for detailed property explanation.
- propTypes: {
- value: PropTypes.any,
- decorator: PropTypes.object,
- id: PropTypes.string.isRequired,
- member: PropTypes.object.isRequired,
- renderValue: PropTypes.func.isRequired,
- enableInput: PropTypes.bool,
- },
+ static get propTypes() {
+ return {
+ value: PropTypes.any,
+ decorator: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ member: PropTypes.object.isRequired,
+ renderValue: PropTypes.func.isRequired,
+ enableInput: PropTypes.bool,
+ };
+ }
- getInitialState: function () {
- return {
+ constructor(props) {
+ super(props);
+
+ this.state = {
inputEnabled: false,
};
- },
+
+ this.getCellClass = this.getCellClass.bind(this);
+ this.updateInputEnabled = this.updateInputEnabled.bind(this);
+ }
/**
* Optimize cell rendering. Rerender cell content only if
* the value or expanded state changes.
*/
- shouldComponentUpdate: function (nextProps, nextState) {
+ shouldComponentUpdate(nextProps, nextState) {
return (this.props.value != nextProps.value) ||
(this.state !== nextState) ||
(this.props.member.open != nextProps.member.open);
- },
+ }
- getCellClass: function (object, id) {
+ getCellClass(object, id) {
let decorator = this.props.decorator;
if (!decorator || !decorator.getCellClass) {
return [];
}
// Decorator can return a simple string or array of strings.
let classNames = decorator.getCellClass(object, id);
if (!classNames) {
return [];
}
if (typeof classNames == "string") {
classNames = [classNames];
}
return classNames;
- },
+ }
- updateInputEnabled: function (evt) {
+ updateInputEnabled(evt) {
this.setState(Object.assign({}, this.state, {
inputEnabled: evt.target.nodeName.toLowerCase() !== "input",
}));
- },
+ }
- render: function () {
+ render() {
let {
member,
id,
value,
decorator,
renderValue,
enableInput,
} = this.props;
@@ -122,17 +125,17 @@ define(function (require, exports, modul
td({
className: classNames.join(" "),
role: "presentation"
},
cellElement
)
);
}
- });
+ }
// Default value rendering.
let defaultRenderValue = props => {
return (
props.object + ""
);
};
--- a/devtools/client/shared/components/tree/TreeHeader.js
+++ b/devtools/client/shared/components/tree/TreeHeader.js
@@ -2,68 +2,70 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
- // ReactJS
const React = require("devtools/client/shared/vendor/react");
-
- // Shortcuts
+ const { Component, PropTypes } = React;
const { thead, tr, td, div } = React.DOM;
- const PropTypes = React.PropTypes;
/**
* This component is responsible for rendering tree header.
* It's based on <thead> element.
*/
- let TreeHeader = React.createClass({
- displayName: "TreeHeader",
-
+ class TreeHeader extends Component {
// See also TreeView component for detailed info about properties.
- propTypes: {
- // Custom tree decorator
- decorator: PropTypes.object,
- // True if the header should be visible
- header: PropTypes.bool,
- // Array with column definition
- columns: PropTypes.array
- },
+ static get propTypes() {
+ return {
+ // Custom tree decorator
+ decorator: PropTypes.object,
+ // True if the header should be visible
+ header: PropTypes.bool,
+ // Array with column definition
+ columns: PropTypes.array
+ };
+ }
- getDefaultProps: function () {
+ static get defaultProps() {
return {
columns: [{
id: "default"
}]
};
- },
+ }
- getHeaderClass: function (colId) {
+ constructor(props) {
+ super(props);
+ this.getHeaderClass = this.getHeaderClass.bind(this);
+ }
+
+ getHeaderClass(colId) {
let decorator = this.props.decorator;
if (!decorator || !decorator.getHeaderClass) {
return [];
}
// Decorator can return a simple string or array of strings.
let classNames = decorator.getHeaderClass(colId);
if (!classNames) {
return [];
}
if (typeof classNames == "string") {
classNames = [classNames];
}
return classNames;
- },
+ }
- render: function () {
+ render() {
let cells = [];
let visible = this.props.header;
// Render the rest of the columns (if any)
this.props.columns.forEach(col => {
let cellStyle = {
"width": col.width ? col.width : "",
};
@@ -92,13 +94,13 @@ define(function (require, exports, modul
thead({
role: "presentation"
}, tr({
className: visible ? "treeHeaderRow" : "",
role: "presentation"
}, cells))
);
}
- });
+ }
// Exports from this module
module.exports = TreeHeader;
});
--- a/devtools/client/shared/components/tree/TreeRow.js
+++ b/devtools/client/shared/components/tree/TreeRow.js
@@ -2,124 +2,126 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
- // ReactJS
const React = require("devtools/client/shared/vendor/react");
- const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+ const { Component, createFactory, PropTypes } = React;
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+ const { tr } = React.DOM;
// Tree
- const TreeCell = React.createFactory(require("./TreeCell"));
- const LabelCell = React.createFactory(require("./LabelCell"));
+ const TreeCell = createFactory(require("./TreeCell"));
+ const LabelCell = createFactory(require("./LabelCell"));
// Scroll
const { scrollIntoViewIfNeeded } = require("devtools/client/shared/scroll");
- // Shortcuts
- const { tr } = React.DOM;
- const PropTypes = React.PropTypes;
-
/**
* This template represents a node in TreeView component. It's rendered
* using <tr> element (the entire tree is one big <table>).
*/
- let TreeRow = React.createClass({
- displayName: "TreeRow",
-
+ class TreeRow extends Component {
// See TreeView component for more details about the props and
// the 'member' object.
- propTypes: {
- member: PropTypes.shape({
- object: PropTypes.obSject,
- name: PropTypes.sring,
- type: PropTypes.string.isRequired,
- rowClass: PropTypes.string.isRequired,
- level: PropTypes.number.isRequired,
- hasChildren: PropTypes.bool,
- value: PropTypes.any,
- open: PropTypes.bool.isRequired,
- path: PropTypes.string.isRequired,
- hidden: PropTypes.bool,
- selected: PropTypes.bool,
- }),
- decorator: PropTypes.object,
- renderCell: PropTypes.object,
- renderLabelCell: PropTypes.object,
- columns: PropTypes.array.isRequired,
- id: PropTypes.string.isRequired,
- provider: PropTypes.object.isRequired,
- onClick: PropTypes.func.isRequired,
- onMouseOver: PropTypes.func,
- onMouseOut: PropTypes.func
- },
+ static get propTypes() {
+ return {
+ member: PropTypes.shape({
+ object: PropTypes.obSject,
+ name: PropTypes.sring,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ selected: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.object,
+ renderLabelCell: PropTypes.object,
+ columns: PropTypes.array.isRequired,
+ id: PropTypes.string.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired,
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.getRowClass = this.getRowClass.bind(this);
+ }
componentWillReceiveProps(nextProps) {
// I don't like accessing the underlying DOM elements directly,
// but this optimization makes the filtering so damn fast!
// The row doesn't have to be re-rendered, all we really need
// to do is toggling a class name.
// The important part is that DOM elements don't need to be
// re-created when they should appear again.
if (nextProps.member.hidden != this.props.member.hidden) {
- let row = ReactDOM.findDOMNode(this);
+ let row = findDOMNode(this);
row.classList.toggle("hidden");
}
- },
+ }
/**
* Optimize row rendering. If props are the same do not render.
* This makes the rendering a lot faster!
*/
- shouldComponentUpdate: function (nextProps) {
+ shouldComponentUpdate(nextProps) {
let props = ["name", "open", "value", "loading", "selected", "hasChildren"];
for (let p in props) {
if (nextProps.member[props[p]] != this.props.member[props[p]]) {
return true;
}
}
return false;
- },
+ }
- componentDidUpdate: function () {
+ componentDidUpdate() {
if (this.props.member.selected) {
- let row = ReactDOM.findDOMNode(this);
+ let row = findDOMNode(this);
// Because this is called asynchronously, context window might be
// already gone.
if (row.ownerDocument.defaultView) {
scrollIntoViewIfNeeded(row);
}
}
- },
+ }
- getRowClass: function (object) {
+ getRowClass(object) {
let decorator = this.props.decorator;
if (!decorator || !decorator.getRowClass) {
return [];
}
// Decorator can return a simple string or array of strings.
let classNames = decorator.getRowClass(object);
if (!classNames) {
return [];
}
if (typeof classNames == "string") {
classNames = [classNames];
}
return classNames;
- },
+ }
- render: function () {
+ render() {
let member = this.props.member;
let decorator = this.props.decorator;
let props = {
id: this.props.id,
role: "treeitem",
"aria-level": member.level,
"aria-selected": !!member.selected,
onClick: this.props.onClick,
@@ -193,17 +195,17 @@ define(function (require, exports, modul
}
});
// Render tree row
return (
tr(props, cells)
);
}
- });
+ }
// Helpers
let RenderCell = props => {
return TreeCell(props);
};
let RenderLabelCell = props => {
--- a/devtools/client/shared/components/tree/TreeView.js
+++ b/devtools/client/shared/components/tree/TreeView.js
@@ -2,27 +2,32 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
- // ReactJS
- const React = require("devtools/client/shared/vendor/react");
+ const { cloneElement, Component, createFactory, DOM: dom, PropTypes } =
+ require("devtools/client/shared/vendor/react");
// Reps
const { ObjectProvider } = require("./ObjectProvider");
- const TreeRow = React.createFactory(require("./TreeRow"));
- const TreeHeader = React.createFactory(require("./TreeHeader"));
+ const TreeRow = createFactory(require("./TreeRow"));
+ const TreeHeader = createFactory(require("./TreeHeader"));
- // Shortcuts
- const DOM = React.DOM;
- const PropTypes = React.PropTypes;
+ const defaultProps = {
+ object: null,
+ renderRow: null,
+ provider: ObjectProvider,
+ expandedNodes: new Set(),
+ expandableStrings: true,
+ columns: []
+ };
/**
* This component represents a tree view with expandable/collapsible nodes.
* The tree is rendered using <table> element where every node is represented
* by <tr> element. The tree is one big table where nodes (rows) are properly
* indented from the left to mimic hierarchical structure of the data.
*
* The tree can have arbitrary number of columns and so, might be use
@@ -48,137 +53,192 @@ define(function (require, exports, modul
* getCellClass: function(object, colId);
* getHeaderClass: function(colId);
* renderValue: function(object, colId);
* renderRow: function(object);
* renderCell: function(object, colId);
* renderLabelCell: function(object);
* }
*/
- let TreeView = React.createClass({
- displayName: "TreeView",
-
+ class TreeView extends Component {
// The only required property (not set by default) is the input data
// object that is used to puputate the tree.
- propTypes: {
- // The input data object.
- object: PropTypes.any,
- className: PropTypes.string,
- label: PropTypes.string,
- // Data provider (see also the interface above)
- provider: PropTypes.shape({
- getChildren: PropTypes.func,
- hasChildren: PropTypes.func,
- getLabel: PropTypes.func,
- getValue: PropTypes.func,
- getKey: PropTypes.func,
- getType: PropTypes.func,
- }).isRequired,
- // Tree decorator (see also the interface above)
- decorator: PropTypes.shape({
- getRowClass: PropTypes.func,
- getCellClass: PropTypes.func,
- getHeaderClass: PropTypes.func,
+ static get propTypes() {
+ return {
+ // The input data object.
+ object: PropTypes.any,
+ className: PropTypes.string,
+ label: PropTypes.string,
+ // Data provider (see also the interface above)
+ provider: PropTypes.shape({
+ getChildren: PropTypes.func,
+ hasChildren: PropTypes.func,
+ getLabel: PropTypes.func,
+ getValue: PropTypes.func,
+ getKey: PropTypes.func,
+ getType: PropTypes.func,
+ }).isRequired,
+ // Tree decorator (see also the interface above)
+ decorator: PropTypes.shape({
+ getRowClass: PropTypes.func,
+ getCellClass: PropTypes.func,
+ getHeaderClass: PropTypes.func,
+ renderValue: PropTypes.func,
+ renderRow: PropTypes.func,
+ renderCell: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ }),
+ // Custom tree row (node) renderer
+ renderRow: PropTypes.func,
+ // Custom cell renderer
+ renderCell: PropTypes.func,
+ // Custom value renderef
renderValue: PropTypes.func,
- renderRow: PropTypes.func,
- renderCell: PropTypes.func,
+ // Custom tree label (including a toggle button) renderer
renderLabelCell: PropTypes.func,
- }),
- // Custom tree row (node) renderer
- renderRow: PropTypes.func,
- // Custom cell renderer
- renderCell: PropTypes.func,
- // Custom value renderef
- renderValue: PropTypes.func,
- // Custom tree label (including a toggle button) renderer
- renderLabelCell: PropTypes.func,
- // Set of expanded nodes
- expandedNodes: PropTypes.object,
- // Custom filtering callback
- onFilter: PropTypes.func,
- // Custom sorting callback
- onSort: PropTypes.func,
- // A header is displayed if set to true
- header: PropTypes.bool,
- // Long string is expandable by a toggle button
- expandableStrings: PropTypes.bool,
- // Array of columns
- columns: PropTypes.arrayOf(PropTypes.shape({
- id: PropTypes.string.isRequired,
- title: PropTypes.string,
- width: PropTypes.string
- }))
- },
+ // Set of expanded nodes
+ expandedNodes: PropTypes.object,
+ // Custom filtering callback
+ onFilter: PropTypes.func,
+ // Custom sorting callback
+ onSort: PropTypes.func,
+ // A header is displayed if set to true
+ header: PropTypes.bool,
+ // Long string is expandable by a toggle button
+ expandableStrings: PropTypes.bool,
+ // Array of columns
+ columns: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ width: PropTypes.string
+ }))
+ };
+ }
- getDefaultProps: function () {
- return {
- object: null,
- renderRow: null,
- provider: ObjectProvider,
- expandedNodes: new Set(),
- expandableStrings: true,
- columns: []
- };
- },
+ static get defaultProps() {
+ return defaultProps;
+ }
- getInitialState: function () {
- return {
- expandedNodes: this.props.expandedNodes,
- columns: ensureDefaultColumn(this.props.columns),
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expandedNodes: props.expandedNodes,
+ columns: ensureDefaultColumn(props.columns),
selected: null
};
- },
- componentWillReceiveProps: function (nextProps) {
+ this.toggle = this.toggle.bind(this);
+ this.isExpanded = this.isExpanded.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.onClickRow = this.onClickRow.bind(this);
+ this.getSelectedRow = this.getSelectedRow.bind(this);
+ this.selectRow = this.selectRow.bind(this);
+ this.isSelected = this.isSelected.bind(this);
+ this.onFilter = this.onFilter.bind(this);
+ this.onSort = this.onSort.bind(this);
+ this.getMembers = this.getMembers.bind(this);
+ this.renderRows = this.renderRows.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps) {
let { expandedNodes } = nextProps;
this.setState(Object.assign({}, this.state, {
expandedNodes,
}));
- },
+ }
- componentDidUpdate: function () {
+ componentDidUpdate() {
let selected = this.getSelectedRow(this.rows);
if (!selected && this.rows.length > 0) {
// TODO: Do better than just selecting the first row again. We want to
// select (in order) previous, next or parent in case when selected
// row is removed.
this.selectRow(this.rows[0].props.member.path);
}
- },
+ }
+
+ static subPath(path, subKey) {
+ return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
+ }
+
+ /**
+ * Creates a set with the paths of the nodes that should be expanded by default
+ * according to the passed options.
+ * @param {Object} The root node of the tree.
+ * @param {Object} [optional] An object with the following optional parameters:
+ * - maxLevel: nodes nested deeper than this level won't be expanded.
+ * - maxNodes: maximum number of nodes that can be expanded. The traversal is
+ breadth-first, so expanding nodes nearer to the root will be preferred.
+ Sibling nodes will either be all expanded or none expanded.
+ * }
+ */
+ static getExpandedNodes(rootObj, { maxLevel = Infinity, maxNodes = Infinity } = {}) {
+ let expandedNodes = new Set();
+ let queue = [{
+ object: rootObj,
+ level: 1,
+ path: ""
+ }];
+ while (queue.length) {
+ let {object, level, path} = queue.shift();
+ if (Object(object) !== object) {
+ continue;
+ }
+ let keys = Object.keys(object);
+ if (expandedNodes.size + keys.length > maxNodes) {
+ // Avoid having children half expanded.
+ break;
+ }
+ for (let key of keys) {
+ let nodePath = TreeView.subPath(path, key);
+ expandedNodes.add(nodePath);
+ if (level < maxLevel) {
+ queue.push({
+ object: object[key],
+ level: level + 1,
+ path: nodePath
+ });
+ }
+ }
+ }
+ return expandedNodes;
+ }
// Node expand/collapse
- toggle: function (nodePath) {
+ toggle(nodePath) {
let nodes = this.state.expandedNodes;
if (this.isExpanded(nodePath)) {
nodes.delete(nodePath);
} else {
nodes.add(nodePath);
}
// Compute new state and update the tree.
this.setState(Object.assign({}, this.state, {
expandedNodes: nodes
}));
- },
+ }
- isExpanded: function (nodePath) {
+ isExpanded(nodePath) {
return this.state.expandedNodes.has(nodePath);
- },
+ }
// Event Handlers
- onKeyDown: function (event) {
+ onKeyDown(event) {
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(
event.key)) {
event.preventDefault();
}
- },
+ }
- onKeyUp: function (event) {
+ onKeyUp(event) {
let row = this.getSelectedRow(this.rows);
if (!row) {
return;
}
let index = this.rows.indexOf(row);
switch (event.key) {
case "ArrowRight":
@@ -204,67 +264,67 @@ define(function (require, exports, modul
this.selectRow(previousRow.props.member.path);
}
break;
default:
return;
}
event.preventDefault();
- },
+ }
- onClickRow: function (nodePath, event) {
+ onClickRow(nodePath, event) {
event.stopPropagation();
let cell = event.target.closest("td");
if (cell && cell.classList.contains("treeLabelCell")) {
this.toggle(nodePath);
}
this.selectRow(nodePath);
- },
+ }
- getSelectedRow: function (rows) {
+ getSelectedRow(rows) {
if (!this.state.selected || rows.length === 0) {
return null;
}
return rows.find(row => this.isSelected(row.props.member.path));
- },
+ }
- selectRow: function (nodePath) {
+ selectRow(nodePath) {
this.setState(Object.assign({}, this.state, {
selected: nodePath
}));
- },
+ }
- isSelected: function (nodePath) {
+ isSelected(nodePath) {
return nodePath === this.state.selected;
- },
+ }
// Filtering & Sorting
/**
* Filter out nodes that don't correspond to the current filter.
* @return {Boolean} true if the node should be visible otherwise false.
*/
- onFilter: function (object) {
+ onFilter(object) {
let onFilter = this.props.onFilter;
return onFilter ? onFilter(object) : true;
- },
+ }
- onSort: function (parent, children) {
+ onSort(parent, children) {
let onSort = this.props.onSort;
return onSort ? onSort(parent, children) : children;
- },
+ }
// Members
/**
* Return children node objects (so called 'members') for given
* parent object.
*/
- getMembers: function (parent, level, path) {
+ getMembers(parent, level, path) {
// Strings don't have children. Note that 'long' strings are using
// the expander icon (+/-) to display the entire original value,
// but there are no child items.
if (typeof parent == "string") {
return [];
}
let { expandableStrings, provider } = this.props;
@@ -315,22 +375,22 @@ define(function (require, exports, modul
// Node path
path: nodePath,
// True if the node is hidden (used for filtering)
hidden: !this.onFilter(child),
// True if the node is selected with keyboard
selected: this.isSelected(nodePath)
};
});
- },
+ }
/**
* Render tree rows/nodes.
*/
- renderRows: function (parent, level = 0, path = "") {
+ renderRows(parent, level = 0, path = "") {
let rows = [];
let decorator = this.props.decorator;
let renderRow = this.props.renderRow || TreeRow;
// Get children for given parent node, iterate over them and render
// a row for every one. Use row template (a component) from properties.
// If the return value is non-array, the children are being loaded
// asynchronously.
@@ -362,27 +422,27 @@ define(function (require, exports, modul
member.path);
// If children needs to be asynchronously fetched first,
// set 'loading' property to the parent row. Otherwise
// just append children rows to the array of all rows.
if (!Array.isArray(childRows)) {
let lastIndex = rows.length - 1;
props.member.loading = true;
- rows[lastIndex] = React.cloneElement(rows[lastIndex], props);
+ rows[lastIndex] = cloneElement(rows[lastIndex], props);
} else {
rows = rows.concat(childRows);
}
}
});
return rows;
- },
+ }
- render: function () {
+ render() {
let root = this.props.object;
let classNames = ["treeTable"];
this.rows = [];
// Use custom class name from props.
let className = this.props.className;
if (className) {
classNames.push(...className.split(" "));
@@ -398,83 +458,34 @@ define(function (require, exports, modul
rows = [];
}
let props = Object.assign({}, this.props, {
columns: this.state.columns
});
return (
- DOM.table({
+ dom.table({
className: classNames.join(" "),
role: "tree",
tabIndex: 0,
onKeyDown: this.onKeyDown,
onKeyUp: this.onKeyUp,
"aria-label": this.props.label || "",
"aria-activedescendant": this.state.selected,
cellPadding: 0,
cellSpacing: 0},
TreeHeader(props),
- DOM.tbody({
+ dom.tbody({
role: "presentation"
}, rows)
)
);
}
- });
-
- TreeView.subPath = function (path, subKey) {
- return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
- };
-
- /**
- * Creates a set with the paths of the nodes that should be expanded by default
- * according to the passed options.
- * @param {Object} The root node of the tree.
- * @param {Object} [optional] An object with the following optional parameters:
- * - maxLevel: nodes nested deeper than this level won't be expanded.
- * - maxNodes: maximum number of nodes that can be expanded. The traversal is
- breadth-first, so expanding nodes nearer to the root will be preferred.
- Sibling nodes will either be all expanded or none expanded.
- * }
- */
- TreeView.getExpandedNodes = function (rootObj,
- { maxLevel = Infinity, maxNodes = Infinity } = {}
- ) {
- let expandedNodes = new Set();
- let queue = [{
- object: rootObj,
- level: 1,
- path: ""
- }];
- while (queue.length) {
- let {object, level, path} = queue.shift();
- if (Object(object) !== object) {
- continue;
- }
- let keys = Object.keys(object);
- if (expandedNodes.size + keys.length > maxNodes) {
- // Avoid having children half expanded.
- break;
- }
- for (let key of keys) {
- let nodePath = TreeView.subPath(path, key);
- expandedNodes.add(nodePath);
- if (level < maxLevel) {
- queue.push({
- object: object[key],
- level: level + 1,
- path: nodePath
- });
- }
- }
- }
- return expandedNodes;
- };
+ }
// Helpers
/**
* There should always be at least one column (the one with toggle buttons)
* and this function ensures that it's true.
*/
function ensureDefaultColumn(columns) {