Bug 1412311 - DevTools Shared Components to ES6 classes r=nchevobbe

In devtools/client/shared/components/tree/TreeView.js I have had to leave defaultProps outside the getter as a temporary workaround for bug 1413167.

MozReview-Commit-ID: 1yaxqFnC92p

--HG--
extra : rebase_source : 64cae084e3edbb71e2b6948d69459bd82705c040
This commit is contained in:
Michael Ratcliffe 2017-10-27 15:33:10 +01:00
parent 0f0d2ca958
commit 1e9d21eb34
18 changed files with 960 additions and 835 deletions

View File

@ -4,48 +4,55 @@
"use strict";
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
module.exports = createClass({
displayName: "AutocompletePopup",
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,
};
}
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,
},
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
@ -53,7 +60,7 @@ module.exports = createClass({
*/
jumpToTop() {
this.setState({ selectedIndex: 0 });
},
}
/**
* Use this method to select the bottom-most item
@ -61,7 +68,7 @@ module.exports = createClass({
*/
jumpToBottom() {
this.setState({ selectedIndex: this.state.list.length - 1 });
},
}
/**
* Increment the selected index with the provided increment value. Will cycle to the
@ -81,7 +88,7 @@ module.exports = createClass({
nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
}
this.setState({selectedIndex: nextIndex});
},
}
/**
* Submit the currently selected item to the onItemSelected callback
@ -91,12 +98,12 @@ module.exports = createClass({
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;
@ -124,4 +131,6 @@ module.exports = createClass({
)
);
}
});
}
module.exports = AutocompletePopup;

View File

@ -4,7 +4,7 @@
"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");
@ -12,34 +12,34 @@ 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,
@ -47,7 +47,13 @@ module.exports = createClass({
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) {
@ -55,7 +61,7 @@ module.exports = createClass({
this.props.sourceMapService.subscribe(source, line, column,
this._locationChanged);
}
},
}
componentWillUnmount() {
if (this.props.sourceMapService) {
@ -63,7 +69,7 @@ module.exports = createClass({
this.props.sourceMapService.unsubscribe(source, line, column,
this._locationChanged);
}
},
}
_locationChanged(isSourceMapped, url, line, column) {
let newState = {
@ -79,7 +85,7 @@ module.exports = createClass({
}
this.setState(newState);
},
}
/**
* Utility method to convert the Frame object model to the
@ -95,7 +101,7 @@ module.exports = createClass({
column,
functionDisplayName: this.props.frame.functionDisplayName,
};
},
}
render() {
let frame, isSourceMapped;
@ -235,4 +241,6 @@ module.exports = createClass({
return dom.span(attributes, ...elements);
}
});
}
module.exports = Frame;

View File

@ -25,61 +25,67 @@
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) {
@ -88,7 +94,7 @@ module.exports = createClass({
this.setState({ mouseDown: true });
event.preventDefault();
},
}
_onMouseUp(event) {
if (event.button !== 0 || !this.state.mouseDown) {
@ -97,7 +103,7 @@ module.exports = createClass({
this.setState({ mouseDown: false });
event.preventDefault();
},
}
_onMouseMove(event) {
if (!this.state.mouseDown) {
@ -113,7 +119,7 @@ module.exports = createClass({
this.props.onResize(relative / width);
event.preventDefault();
},
}
render() {
/* eslint-disable no-shadow */
@ -149,4 +155,6 @@ module.exports = createClass({
)
);
}
});
}
module.exports = HSplitBox;

View File

@ -10,7 +10,7 @@ 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
@ -34,72 +34,81 @@ const PriorityLevels = {
* 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 ||
@ -137,14 +146,14 @@ var NotificationBox = createClass({
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
@ -163,11 +172,11 @@ var NotificationBox = createClass({
this.close(notification);
}
});
},
}
getCurrentNotification() {
return this.state.notifications.first();
},
}
/**
* Close specified notification.
@ -184,7 +193,7 @@ var NotificationBox = createClass({
this.setState({
notifications: this.state.notifications.remove(notification.value)
});
},
}
/**
* Render a button. A notification can have a set of custom buttons.
@ -210,7 +219,7 @@ var NotificationBox = createClass({
props.label
)
);
},
}
/**
* Render a notification.
@ -241,7 +250,7 @@ var NotificationBox = createClass({
)
)
);
},
}
/**
* Render the top (highest priority) notification. Only one
@ -256,8 +265,8 @@ var NotificationBox = createClass({
return div({className: "notificationbox"},
content
);
},
});
}
}
module.exports.NotificationBox = NotificationBox;
module.exports.PriorityLevels = PriorityLevels;

View File

@ -6,31 +6,36 @@
"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",
propTypes: {
delay: PropTypes.number,
keyShortcut: PropTypes.string,
onChange: PropTypes.func,
placeholder: PropTypes.string,
type: PropTypes.string,
autocompleteProvider: PropTypes.func,
},
getInitialState() {
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,
};
}
constructor(props) {
super(props);
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) {
@ -44,7 +49,7 @@ module.exports = createClass({
event.preventDefault();
this.refs.input.focus();
});
},
}
componentWillUnmount() {
if (this.shortcuts) {
@ -55,7 +60,7 @@ module.exports = createClass({
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
},
}
onChange() {
if (this.state.value !== this.refs.input.value) {
@ -81,20 +86,20 @@ module.exports = createClass({
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;
@ -131,7 +136,7 @@ module.exports = createClass({
autocomplete.jumpToBottom();
break;
}
},
}
render() {
let {
@ -175,4 +180,6 @@ module.exports = createClass({
})
);
}
});
}
module.exports = SearchBox;

View File

@ -6,7 +6,7 @@
"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;
@ -15,35 +15,39 @@ const { button } = DOM;
* Sidebar toggle button. This button is used to exapand
* and collapse Sidebar.
*/
var SidebarToggle = createClass({
displayName: "SidebarToggle",
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,
},
getInitialState: function () {
class SidebarToggle extends Component {
static get propTypes() {
return {
collapsed: this.props.collapsed,
// 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);
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;
@ -61,6 +65,6 @@ var SidebarToggle = createClass({
})
);
}
});
}
module.exports = SidebarToggle;

View File

@ -5,18 +5,18 @@
"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;
@ -26,18 +26,18 @@ const AsyncFrame = createFactory(createClass({
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 {
@ -77,6 +77,8 @@ const StackTrace = createClass({
return dom.div({ className: "stack-trace" }, frames);
}
});
}
const AsyncFrame = createFactory(AsyncFrameClass);
module.exports = StackTrace;

View File

@ -5,7 +5,7 @@
"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;
@ -97,158 +97,176 @@ const NUMBER_OF_OFFSCREEN_ITEMS = 1;
* }
* });
*/
module.exports = createClass({
displayName: "Tree",
class Tree extends Component {
static get propTypes() {
return {
// Required props
propTypes: {
// 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) {
@ -279,7 +297,7 @@ module.exports = createClass({
for (let i = 0; i < length; i++) {
autoExpand(roots[i], 0);
}
},
}
_preventArrowKeyScrolling(e) {
switch (e.key) {
@ -298,7 +316,7 @@ module.exports = createClass({
}
}
}
},
}
/**
* Updates the state's height based on clientHeight.
@ -307,7 +325,7 @@ module.exports = createClass({
this.setState({
height: this.refs.tree.clientHeight
});
},
}
/**
* Perform a pre-order depth-first search from item.
@ -332,7 +350,7 @@ module.exports = createClass({
}
return traversal;
},
}
/**
* Perform a pre-order depth-first search over the whole forest.
@ -347,7 +365,7 @@ module.exports = createClass({
}
return traversal;
},
}
/**
* Expands current row.
@ -355,7 +373,7 @@ module.exports = createClass({
* @param {Object} item
* @param {Boolean} expandAllChildren
*/
_onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
_onExpand(item, expandAllChildren) {
if (this.props.onExpand) {
this.props.onExpand(item);
@ -367,18 +385,18 @@ module.exports = createClass({
}
}
}
}),
}
/**
* 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.
@ -411,14 +429,14 @@ module.exports = createClass({
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
@ -426,12 +444,12 @@ module.exports = createClass({
*
* @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.
@ -476,12 +494,12 @@ module.exports = createClass({
}
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.
@ -505,13 +523,13 @@ module.exports = createClass({
}
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.
@ -530,13 +548,13 @@ module.exports = createClass({
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;
@ -552,7 +570,7 @@ module.exports = createClass({
}
this._focus(parentIndex, parent);
}),
}
render() {
const traversal = this._dfsFromRoots();
@ -656,28 +674,28 @@ module.exports = createClass({
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 = {
@ -699,24 +717,26 @@ const ArrowExpander = createFactory(createClass({
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({
@ -769,7 +789,10 @@ const TreeNode = createFactory(createClass({
this.props.expanded),
);
}
}));
}
const ArrowExpander = createFactory(ArrowExpanderClass);
const TreeNode = createFactory(TreeNodeClass);
/**
* Create a function that calls the given function `fn` only once per animation
@ -794,3 +817,5 @@ function oncePerAnimationFrame(fn) {
});
};
}
module.exports = Tree;

View File

@ -6,18 +6,25 @@
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { DOM: dom, PropTypes } = React;
const { Component, DOM: dom, PropTypes } = React;
const Draggable = React.createClass({
displayName: "Draggable",
class Draggable extends Component {
static get propTypes() {
return {
onMove: PropTypes.func.isRequired,
onStart: PropTypes.func,
onStop: PropTypes.func,
style: PropTypes.object,
className: PropTypes.string
};
}
propTypes: {
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();
@ -25,14 +32,14 @@ const Draggable = React.createClass({
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();
@ -40,7 +47,7 @@ const Draggable = React.createClass({
doc.removeEventListener("mousemove", this.onMove);
doc.removeEventListener("mouseup", this.onUp);
this.props.onStop && this.props.onStop();
},
}
render() {
return dom.div({
@ -49,6 +56,6 @@ const Draggable = React.createClass({
onMouseDown: this.startDragging
});
}
});
}
module.exports = Draggable;

View File

@ -7,62 +7,68 @@
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
};
},
}
/**
* 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
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.
*/
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;
@ -70,7 +76,7 @@ const SplitBox = React.createClass({
if (vert !== this.props.vert) {
this.setState({ vert });
}
},
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.width != this.state.width ||
@ -82,7 +88,7 @@ const SplitBox = React.createClass({
nextProps.minSize != this.props.minSize ||
nextProps.maxSize != this.props.maxSize ||
nextProps.splitterSize != this.props.splitterSize;
},
}
// Dragging Events
@ -102,7 +108,7 @@ const SplitBox = React.createClass({
this.setState({
defaultCursor: defaultCursor
});
},
}
onStopMove() {
const splitBox = ReactDOM.findDOMNode(this);
@ -110,7 +116,7 @@ const SplitBox = React.createClass({
doc.documentElement.style.cursor = this.state.defaultCursor;
splitBox.classList.remove("dragging");
},
}
/**
* Adjust size of the controlled panel. Depending on the current
@ -149,7 +155,7 @@ const SplitBox = React.createClass({
height: size
});
}
},
}
// Rendering
@ -226,6 +232,6 @@ const SplitBox = React.createClass({
)
);
}
});
}
module.exports = SplitBox;

View File

@ -8,7 +8,7 @@
"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");
@ -20,37 +20,50 @@ 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);
@ -62,9 +75,9 @@ let Tabbar = createClass({
tabs,
});
}
},
}
createTabs: function (children) {
createTabs(children) {
return children
.filter((panel) => panel)
.map((panel, index) =>
@ -74,11 +87,11 @@ let Tabbar = createClass({
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) {
@ -100,9 +113,9 @@ let Tabbar = createClass({
this.props.onSelect(id);
}
});
},
}
toggleTab: function (tabId, isVisible) {
toggleTab(tabId, isVisible) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
@ -116,9 +129,9 @@ let Tabbar = createClass({
this.setState(Object.assign({}, this.state, {
tabs: tabs,
}));
},
}
removeTab: function (tabId) {
removeTab(tabId) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
@ -137,9 +150,9 @@ let Tabbar = createClass({
tabs,
activeTab,
}));
},
}
select: function (tabId) {
select(tabId) {
let index = this.getTabIndex(tabId);
if (index < 0) {
return;
@ -154,11 +167,11 @@ let Tabbar = createClass({
this.props.onSelect(tabId);
}
});
},
}
// Helpers
getTabIndex: function (tabId) {
getTabIndex(tabId) {
let tabIndex = -1;
this.state.tabs.forEach((tab, index) => {
if (tab.id === tabId) {
@ -166,19 +179,19 @@ let Tabbar = createClass({
}
});
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
});
@ -186,9 +199,9 @@ let Tabbar = createClass({
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;
@ -214,11 +227,11 @@ let Tabbar = createClass({
{ doc: this.props.menuDocument });
return menu;
},
}
// Rendering
renderTab: function (tab) {
renderTab(tab) {
if (typeof tab.panel === "function") {
return tab.panel({
key: tab.id,
@ -229,9 +242,9 @@ let Tabbar = createClass({
}
return tab.panel;
},
}
render: function () {
render() {
let tabs = this.state.tabs.map((tab) => this.renderTab(tab));
return (
@ -247,7 +260,7 @@ let Tabbar = createClass({
)
)
);
},
});
}
}
module.exports = Tabbar;

View File

@ -8,7 +8,7 @@
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");
/**
@ -31,43 +31,45 @@ define(function (require, exports, module) {
* </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
@ -82,9 +84,17 @@ define(function (require, exports, module) {
// 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);
@ -101,9 +111,9 @@ define(function (require, exports, module) {
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
@ -123,9 +133,9 @@ define(function (require, exports, module) {
tabActive,
});
}
},
}
componentWillUnmount: function () {
componentWillUnmount() {
let node = findDOMNode(this);
node.removeEventListener("keydown", this.onKeyDown);
@ -133,27 +143,27 @@ define(function (require, exports, module) {
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;
@ -174,19 +184,19 @@ define(function (require, exports, module) {
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;
@ -217,11 +227,11 @@ define(function (require, exports, module) {
onAfterChange(index);
}
});
},
}
// Rendering
renderMenuItems: function () {
renderMenuItems() {
if (!this.props.children) {
throw new Error("There must be at least one Tab");
}
@ -299,9 +309,9 @@ define(function (require, exports, module) {
allTabsMenu
)
);
},
}
renderPanels: function () {
renderPanels() {
let { children, renderOnlySelected } = this.props;
if (!children) {
@ -359,38 +369,38 @@ define(function (require, exports, module) {
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;

View File

@ -26,8 +26,8 @@ Test all-tabs menu.
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
@ -45,12 +45,14 @@ window.onload = Task.async(function* () {
const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
// Test panel.
let TabPanel = React.createFactory(React.createClass({
render: function () {
return React.DOM.div({}, "content");
class TabPanelClass extends Component {
render() {
return DOM.div({}, "content");
}
}));
}
// Test panel.
let TabPanel = createFactory(TabPanelClass);
// Create a few panels.
yield addTabWithPanel(1);

View File

@ -8,26 +8,23 @@
// 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;
@ -49,16 +46,16 @@ define(function (require, exports, module) {
}
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
@ -66,7 +63,7 @@ define(function (require, exports, module) {
)
);
}
});
}
// Exports from this module
module.exports = LabelCell;

View File

@ -8,45 +8,48 @@
// 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,
},
getInitialState: function () {
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,
};
}
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 [];
@ -63,15 +66,15 @@ define(function (require, exports, module) {
}
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,
@ -127,7 +130,7 @@ define(function (require, exports, module) {
)
);
}
});
}
// Default value rendering.
let defaultRenderValue = props => {

View File

@ -7,39 +7,41 @@
// 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 [];
@ -56,9 +58,9 @@ define(function (require, exports, module) {
}
return classNames;
},
}
render: function () {
render() {
let cells = [];
let visible = this.props.header;
@ -97,7 +99,7 @@ define(function (require, exports, module) {
}, cells))
);
}
});
}
// Exports from this module
module.exports = TreeHeader;

View File

@ -7,54 +7,56 @@
// 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,
@ -64,16 +66,16 @@ define(function (require, exports, module) {
// 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]]) {
@ -82,20 +84,20 @@ define(function (require, exports, module) {
}
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 [];
@ -112,9 +114,9 @@ define(function (require, exports, module) {
}
return classNames;
},
}
render: function () {
render() {
let member = this.props.member;
let decorator = this.props.decorator;
let props = {
@ -198,7 +200,7 @@ define(function (require, exports, module) {
tr(props, cells)
);
}
});
}
// Helpers

View File

@ -7,17 +7,22 @@
// 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.
@ -53,88 +58,96 @@ define(function (require, exports, module) {
* 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,
renderValue: 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,
// 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
}))
},
getDefaultProps: function () {
return {
object: null,
renderRow: null,
provider: ObjectProvider,
expandedNodes: new Set(),
expandableStrings: true,
columns: []
// 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
}))
};
},
}
getInitialState: function () {
return {
expandedNodes: this.props.expandedNodes,
columns: ensureDefaultColumn(this.props.columns),
static get defaultProps() {
return defaultProps;
}
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
@ -142,11 +155,58 @@ define(function (require, exports, module) {
// 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);
@ -158,22 +218,22 @@ define(function (require, exports, module) {
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;
@ -209,33 +269,33 @@ define(function (require, exports, module) {
}
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
@ -243,15 +303,15 @@ define(function (require, exports, module) {
* 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
@ -259,7 +319,7 @@ define(function (require, exports, module) {
* 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.
@ -320,12 +380,12 @@ define(function (require, exports, module) {
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;
@ -367,7 +427,7 @@ define(function (require, exports, module) {
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);
}
@ -375,9 +435,9 @@ define(function (require, exports, module) {
});
return rows;
},
}
render: function () {
render() {
let root = this.props.object;
let classNames = ["treeTable"];
this.rows = [];
@ -403,7 +463,7 @@ define(function (require, exports, module) {
});
return (
DOM.table({
dom.table({
className: classNames.join(" "),
role: "tree",
tabIndex: 0,
@ -414,62 +474,13 @@ define(function (require, exports, module) {
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