author | Michael Ratcliffe <mratcliffe@mozilla.com> |
Fri, 27 Oct 2017 15:33:10 +0100 | |
changeset 440593 | 3fa859a78370f2df0639a63ba7accad45ff7f60a |
parent 440562 | 083a5838f76a418779c2f4fc01152bc3be355fc0 |
child 440594 | be60c2b625ae9a7c5d8711a70ac8722d936b1f27 |
push id | 8118 |
push user | ryanvm@gmail.com |
push date | Fri, 03 Nov 2017 00:38:34 +0000 |
treeherder | mozilla-beta@1c336e874ae8 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | nchevobbe |
bugs | 1412311, 1413167 |
milestone | 58.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- 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) {