From 298e20ef4f14c8f0d66e7d400444e9814e6442d0 Mon Sep 17 00:00:00 2001 From: Jan Odvarko Date: Mon, 21 Mar 2016 13:29:17 +0100 Subject: [PATCH] Bug 1256757 - Support AMD in tree modules. r=jryans --- .../shared/components/tree/label-cell.js | 77 ++- .../shared/components/tree/object-provider.js | 151 +++-- .../shared/components/tree/tree-cell.js | 161 ++--- .../shared/components/tree/tree-header.js | 159 ++--- .../client/shared/components/tree/tree-row.js | 297 ++++---- .../shared/components/tree/tree-view.js | 640 +++++++++--------- 6 files changed, 752 insertions(+), 733 deletions(-) diff --git a/devtools/client/shared/components/tree/label-cell.js b/devtools/client/shared/components/tree/label-cell.js index de2889ea2d7c..2927df9d494d 100644 --- a/devtools/client/shared/components/tree/label-cell.js +++ b/devtools/client/shared/components/tree/label-cell.js @@ -5,48 +5,51 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -// ReactJS -const React = require("devtools/client/shared/vendor/react"); +// 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; + // Shortcuts + const { td, span } = React.DOM; + const PropTypes = React.PropTypes; -/** - * Render the default cell used for toggle buttons - */ -var LabelCell = React.createClass({ - // See the TreeView component for details related - // to the 'member' object. - propTypes: { - member: PropTypes.object.isRequired - }, + /** + * Render the default cell used for toggle buttons + */ + let LabelCell = React.createClass({ + // See the TreeView component for details related + // to the 'member' object. + propTypes: { + member: PropTypes.object.isRequired + }, - displayName: "LabelCell", + displayName: "LabelCell", - render: function() { - let member = this.props.member; - let level = member.level || 0; + render: function() { + 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 = { - "paddingLeft": (level * 16) + "px", - }; + // Compute indentation dynamically. The deeper the item is + // inside the hierarchy, the bigger is the left padding. + let rowStyle = { + "paddingLeft": (level * 16) + "px", + }; - return ( - td({ - className: "treeLabelCell", - key: "default", - style: rowStyle}, - span({ className: "treeIcon" }), - span({ className: "treeLabel " + member.type + "Label" }, - member.name + return ( + td({ + className: "treeLabelCell", + key: "default", + style: rowStyle}, + span({ className: "treeIcon" }), + span({ className: "treeLabel " + member.type + "Label" }, + member.name + ) ) - ) - ); - } -}); + ); + } + }); -// Exports from this module -module.exports = LabelCell; + // Exports from this module + module.exports = LabelCell; +}); diff --git a/devtools/client/shared/components/tree/object-provider.js b/devtools/client/shared/components/tree/object-provider.js index bcd40cc701c1..eb6706c88021 100644 --- a/devtools/client/shared/components/tree/object-provider.js +++ b/devtools/client/shared/components/tree/object-provider.js @@ -5,83 +5,86 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -/** - * Implementation of the default data provider. A provider is state less - * object responsible for transformation data (usually a state) to - * a structure that can be directly consumed by the tree-view component. - */ -var ObjectProvider = { - getChildren: function(object) { - let children = []; +// Make this available to both AMD and CJS environments +define(function(require, exports, module) { + /** + * Implementation of the default data provider. A provider is state less + * object responsible for transformation data (usually a state) to + * a structure that can be directly consumed by the tree-view component. + */ + let ObjectProvider = { + getChildren: function(object) { + let children = []; - if (object instanceof ObjectProperty) { - object = object.value; - } - - if (!object) { - return []; - } - - if (typeof (object) == "string") { - return []; - } - - for (let prop in object) { - try { - children.push(new ObjectProperty(prop, object[prop])); - } catch (e) { - console.error(e); + if (object instanceof ObjectProperty) { + object = object.value; } + + if (!object) { + return []; + } + + if (typeof (object) == "string") { + return []; + } + + for (let prop in object) { + try { + children.push(new ObjectProperty(prop, object[prop])); + } catch (e) { + console.error(e); + } + } + return children; + }, + + hasChildren: function(object) { + if (object instanceof ObjectProperty) { + object = object.value; + } + + if (!object) { + return false; + } + + if (typeof object == "string") { + return false; + } + + if (typeof object !== "object") { + return false; + } + + return Object.keys(object).length > 1; + }, + + getLabel: function(object) { + return (object instanceof ObjectProperty) ? + object.name : null; + }, + + getValue: function(object) { + return (object instanceof ObjectProperty) ? + object.value : null; + }, + + getKey: function(object) { + return (object instanceof ObjectProperty) ? + object.name : null; + }, + + getType: function(object) { + return (object instanceof ObjectProperty) ? + typeof object.value : typeof object; } - return children; - }, + }; - hasChildren: function(object) { - if (object instanceof ObjectProperty) { - object = object.value; - } - - if (!object) { - return false; - } - - if (typeof object == "string") { - return false; - } - - if (typeof object !== "object") { - return false; - } - - return Object.keys(object).length > 1; - }, - - getLabel: function(object) { - return (object instanceof ObjectProperty) ? - object.name : null; - }, - - getValue: function(object) { - return (object instanceof ObjectProperty) ? - object.value : null; - }, - - getKey: function(object) { - return (object instanceof ObjectProperty) ? - object.name : null; - }, - - getType: function(object) { - return (object instanceof ObjectProperty) ? - typeof object.value : typeof object; + function ObjectProperty(name, value) { + this.name = name; + this.value = value; } -}; -function ObjectProperty(name, value) { - this.name = name; - this.value = value; -} - -// Exports from this module -exports.ObjectProperty = ObjectProperty; -exports.ObjectProvider = ObjectProvider; + // Exports from this module + exports.ObjectProperty = ObjectProperty; + exports.ObjectProvider = ObjectProvider; +}); diff --git a/devtools/client/shared/components/tree/tree-cell.js b/devtools/client/shared/components/tree/tree-cell.js index 9b364a70e68e..40b25a878fdb 100644 --- a/devtools/client/shared/components/tree/tree-cell.js +++ b/devtools/client/shared/components/tree/tree-cell.js @@ -5,92 +5,95 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const React = require("devtools/client/shared/vendor/react"); +// Make this available to both AMD and CJS environments +define(function(require, exports, module) { + const React = require("devtools/client/shared/vendor/react"); -// Shortcuts -const { td, span } = React.DOM; -const PropTypes = React.PropTypes; - -/** - * This template represents a cell in TreeView row. It's rendered - * using element (the row is and the entire tree is ). - */ -var TreeCell = React.createClass({ - // 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 - }, - - displayName: "TreeCell", + // Shortcuts + const { td, span } = React.DOM; + const PropTypes = React.PropTypes; /** - * Optimize cell rendering. If value is the same do not render. + * This template represents a cell in TreeView row. It's rendered + * using and the entire tree is
element (the row is
). */ - shouldComponentUpdate: function(nextProps) { - return (this.props.value != nextProps.value); - }, + let TreeCell = React.createClass({ + // 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 + }, - getCellClass: function(object, id) { - let decorator = this.props.decorator; - if (!decorator || !decorator.getCellClass) { - return []; + displayName: "TreeCell", + + /** + * Optimize cell rendering. If value is the same do not render. + */ + shouldComponentUpdate: function(nextProps) { + return (this.props.value != nextProps.value); + }, + + getCellClass: function(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; + }, + + render: function() { + let member = this.props.member; + let type = member.type || ""; + let id = this.props.id; + let value = this.props.value; + let decorator = this.props.decorator; + + // Compute class name list for the element. - */ -var TreeHeader = React.createClass({ - // 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 - }, + /** + * This component is responsible for rendering tree header. + * It's based on element. + */ + let TreeHeader = React.createClass({ + // 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 + }, - displayName: "TreeHeader", + displayName: "TreeHeader", - getDefaultProps: function() { - return { - columns: [{ - id: "default" - }] - }; - }, - - getHeaderClass: function(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() { - 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 : "", + getDefaultProps: function() { + return { + columns: [{ + id: "default" + }] }; + }, - let classNames = []; - - if (visible) { - classNames = this.getHeaderClass(col.id); - classNames.push("treeHeaderCell"); + getHeaderClass: function(colId) { + let decorator = this.props.decorator; + if (!decorator || !decorator.getHeaderClass) { + return []; } - cells.push( - td({ - className: classNames.join(" "), - style: cellStyle, - key: col.id}, - div({ className: visible ? "treeHeaderCellBox" : "" }, - visible ? col.title : "" + // 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() { + 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 : "", + }; + + let classNames = []; + + if (visible) { + classNames = this.getHeaderClass(col.id); + classNames.push("treeHeaderCell"); + } + + cells.push( + td({ + className: classNames.join(" "), + style: cellStyle, + key: col.id}, + div({ className: visible ? "treeHeaderCellBox" : "" }, + visible ? col.title : "" + ) ) - ) + ); + }); + + return ( + thead({}, tr({ className: visible ? "treeHeaderRow" : "" }, + cells + )) ); - }); + } + }); - return ( - thead({}, tr({ className: visible ? "treeHeaderRow" : "" }, - cells - )) - ); - } + // Exports from this module + module.exports = TreeHeader; }); - -// Exports from this module -module.exports = TreeHeader; diff --git a/devtools/client/shared/components/tree/tree-row.js b/devtools/client/shared/components/tree/tree-row.js index 1e5c7deae64d..7eb71bae7665 100644 --- a/devtools/client/shared/components/tree/tree-row.js +++ b/devtools/client/shared/components/tree/tree-row.js @@ -5,177 +5,180 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -// ReactJS -const React = require("devtools/client/shared/vendor/react"); -const ReactDOM = require("devtools/client/shared/vendor/react-dom"); +// 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"); -// Tree -const TreeCell = React.createFactory(require("./tree-cell")); -const LabelCell = React.createFactory(require("./label-cell")); + // Tree + const TreeCell = React.createFactory(require("./tree-cell")); + const LabelCell = React.createFactory(require("./label-cell")); -// Shortcuts -const { tr } = React.DOM; -const PropTypes = React.PropTypes; - -/** - * This template represents a node in TreeView component. It's rendered - * using element (the entire tree is one big
element. + let classNames = this.getCellClass(member.object, id) || []; + classNames.push("treeValueCell"); + classNames.push(type + "Cell"); + + // Render value using a default render function or custom + // provided function from props or a decorator. + let renderValue = this.props.renderValue || defaultRenderValue; + if (decorator && decorator.renderValue) { + renderValue = decorator.renderValue(member.object, id) || renderValue; + } + + let props = Object.assign({}, this.props, { + object: value, + }); + + // Render me! + return ( + td({ className: classNames.join(" ") }, + span({}, renderValue(props)) + ) + ); } + }); - // 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; - }, - - render: function() { - let member = this.props.member; - let type = member.type || ""; - let id = this.props.id; - let value = this.props.value; - let decorator = this.props.decorator; - - // Compute class name list for the element. - let classNames = this.getCellClass(member.object, id) || []; - classNames.push("treeValueCell"); - classNames.push(type + "Cell"); - - // Render value using a default render function or custom - // provided function from props or a decorator. - let renderValue = this.props.renderValue || defaultRenderValue; - if (decorator && decorator.renderValue) { - renderValue = decorator.renderValue(member.object, id) || renderValue; - } - - let props = Object.assign({}, this.props, { - object: value, - }); - - // Render me! + // Default value rendering. + let defaultRenderValue = props => { return ( - td({ className: classNames.join(" ") }, - span({}, renderValue(props)) - ) + props.object + "" ); - } + }; + + // Exports from this module + module.exports = TreeCell; }); - -// Default value rendering. -var defaultRenderValue = props => { - return ( - props.object + "" - ); -}; - -// Exports from this module -module.exports = TreeCell; diff --git a/devtools/client/shared/components/tree/tree-header.js b/devtools/client/shared/components/tree/tree-header.js index f41797a001a3..9f760b9ff12d 100644 --- a/devtools/client/shared/components/tree/tree-header.js +++ b/devtools/client/shared/components/tree/tree-header.js @@ -5,93 +5,96 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -// ReactJS -const React = require("devtools/client/shared/vendor/react"); +// 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 { thead, tr, td, div } = React.DOM; -const PropTypes = React.PropTypes; + // Shortcuts + const { thead, tr, td, div } = React.DOM; + const PropTypes = React.PropTypes; -/** - * This component is responsible for rendering tree header. - * It's based on
). - */ -var TreeRow = React.createClass({ - // 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, - }), - decorator: PropTypes.object, - renderCell: PropTypes.object, - renderLabelCell: PropTypes.object, - columns: PropTypes.array.isRequired, - provider: PropTypes.object.isRequired, - onClick: PropTypes.func.isRequired - }, - - displayName: "TreeRow", + // Shortcuts + const { tr } = React.DOM; + const PropTypes = React.PropTypes; /** - * Optimize row rendering. If props are the same do not render. - * This makes the rendering a lot faster! + * This template represents a node in TreeView component. It's rendered + * using element (the entire tree is one big
). */ - shouldComponentUpdate: function(nextProps) { - let props = ["name", "open", "value", "loading"]; - for (let p in props) { - if (nextProps.member[props[p]] != this.props.member[props[p]]) { - return true; + let TreeRow = React.createClass({ + // 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, + }), + decorator: PropTypes.object, + renderCell: PropTypes.object, + renderLabelCell: PropTypes.object, + columns: PropTypes.array.isRequired, + provider: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired + }, + + displayName: "TreeRow", + + /** + * Optimize row rendering. If props are the same do not render. + * This makes the rendering a lot faster! + */ + shouldComponentUpdate: function(nextProps) { + let props = ["name", "open", "value", "loading"]; + for (let p in props) { + if (nextProps.member[props[p]] != this.props.member[props[p]]) { + return true; + } } - } - return false; - }, + return false; + }, - 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); - row.classList.toggle("hidden"); - } - }, + 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); + row.classList.toggle("hidden"); + } + }, - getRowClass: function(object) { - let decorator = this.props.decorator; - if (!decorator || !decorator.getRowClass) { - return []; - } + getRowClass: function(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 []; - } + // Decorator can return a simple string or array of strings. + let classNames = decorator.getRowClass(object); + if (!classNames) { + return []; + } - if (typeof classNames == "string") { - classNames = [classNames]; - } + if (typeof classNames == "string") { + classNames = [classNames]; + } - return classNames; - }, + return classNames; + }, - render: function() { - let member = this.props.member; - let decorator = this.props.decorator; + render: function() { + let member = this.props.member; + let decorator = this.props.decorator; - // Compute class name list for the element. - let classNames = this.getRowClass(member.object) || []; - classNames.push("treeRow"); - classNames.push(member.type + "Row"); + // Compute class name list for the element. + let classNames = this.getRowClass(member.object) || []; + classNames.push("treeRow"); + classNames.push(member.type + "Row"); - if (member.hasChildren) { - classNames.push("hasChildren"); - } + if (member.hasChildren) { + classNames.push("hasChildren"); + } - if (member.open) { - classNames.push("opened"); - } + if (member.open) { + classNames.push("opened"); + } - if (member.loading) { - classNames.push("loading"); - } + if (member.loading) { + classNames.push("loading"); + } - if (member.hidden) { - classNames.push("hidden"); - } + if (member.hidden) { + classNames.push("hidden"); + } - // The label column (with toggle buttons) is usually - // the first one, but there might be cases (like in - // the Memory panel) where the toggling is done - // in the last column. - let cells = []; + // The label column (with toggle buttons) is usually + // the first one, but there might be cases (like in + // the Memory panel) where the toggling is done + // in the last column. + let cells = []; - // Get components for rendering cells. - let renderCell = this.props.renderCell || RenderCell; - let renderLabelCell = this.props.renderLabelCell || RenderLabelCell; - if (decorator && decorator.renderLabelCell) { - renderLabelCell = decorator.renderLabelCell(member.object) || - renderLabelCell; - } + // Get components for rendering cells. + let renderCell = this.props.renderCell || RenderCell; + let renderLabelCell = this.props.renderLabelCell || RenderLabelCell; + if (decorator && decorator.renderLabelCell) { + renderLabelCell = decorator.renderLabelCell(member.object) || + renderLabelCell; + } - // Render a cell for every column. - this.props.columns.forEach(col => { - let props = Object.assign({}, this.props, { - key: col.id, - id: col.id, - value: this.props.provider.getValue(member.object, col.id) + // Render a cell for every column. + this.props.columns.forEach(col => { + let props = Object.assign({}, this.props, { + key: col.id, + id: col.id, + value: this.props.provider.getValue(member.object, col.id) + }); + + if (decorator && decorator.renderCell) { + renderCell = decorator.renderCell(member.object, col.id); + } + + let render = (col.id == "default") ? renderLabelCell : renderCell; + + // Some cells don't have to be rendered. This happens when some + // other cells span more columns. Note that the label cells contains + // toggle buttons and should be usually there unless we are rendering + // a simple non-expandable table. + if (render) { + cells.push(render(props)); + } }); - if (decorator && decorator.renderCell) { - renderCell = decorator.renderCell(member.object, col.id); - } + // Render tree row + return ( + tr({ + className: classNames.join(" "), + onClick: this.props.onClick}, + cells + ) + ); + } + }); - let render = (col.id == "default") ? renderLabelCell : renderCell; + // Helpers - // Some cells don't have to be rendered. This happens when some - // other cells span more columns. Note that the label cells contains - // toggle buttons and should be usually there unless we are rendering - // a simple non-expandable table. - if (render) { - cells.push(render(props)); - } - }); + let RenderCell = props => { + return TreeCell(props); + }; - // Render tree row - return ( - tr({ - className: classNames.join(" "), - onClick: this.props.onClick}, - cells - ) - ); - } + let RenderLabelCell = props => { + return LabelCell(props); + }; + + // Exports from this module + module.exports = TreeRow; }); - -// Helpers - -var RenderCell = props => { - return TreeCell(props); -}; - -var RenderLabelCell = props => { - return LabelCell(props); -}; - -// Exports from this module -module.exports = TreeRow; diff --git a/devtools/client/shared/components/tree/tree-view.js b/devtools/client/shared/components/tree/tree-view.js index cb6e4c8c7c55..04d74a05a88d 100644 --- a/devtools/client/shared/components/tree/tree-view.js +++ b/devtools/client/shared/components/tree/tree-view.js @@ -5,344 +5,348 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -// ReactJS -const React = require("devtools/client/shared/vendor/react"); +// Make this available to both AMD and CJS environments +define(function(require, exports, module) { + // ReactJS + const React = require("devtools/client/shared/vendor/react"); -// Reps -const { ObjectProvider } = require("./object-provider"); -const TreeRow = React.createFactory(require("./tree-row")); -const TreeHeader = React.createFactory(require("./tree-header")); + // Reps + const { ObjectProvider } = require("./object-provider"); + const TreeRow = React.createFactory(require("./tree-row")); + const TreeHeader = React.createFactory(require("./tree-header")); -// Shortcuts -const DOM = React.DOM; -const PropTypes = React.PropTypes; + // Shortcuts + const DOM = React.DOM; + const PropTypes = React.PropTypes; -/** - * This component represents a tree view with expandable/collapsible nodes. - * The tree is rendered using
element where every node is represented - * by 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 - * as an expandable tree-table UI widget as well. By default, there is - * one column for node label and one for node value. - * - * The tree is maintaining its (presentation) state, which consists - * from list of expanded nodes and list of columns. - * - * Complete data provider interface: - * var TreeProvider = { - * getChildren: function(object); - * hasChildren: function(object); - * getLabel: function(object, colId); - * getValue: function(object, colId); - * getKey: function(object); - * getType: function(object); - * } - * - * Complete tree decorator interface: - * var TreeDecorator = { - * getRowClass: function(object); - * getCellClass: function(object, colId); - * getHeaderClass: function(colId); - * renderValue: function(object, colId); - * renderRow: function(object); - * renderCelL: function(object, colId); - * renderLabelCell: function(object); - * } - */ -var TreeView = React.createClass({ - // 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, - // 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, + /** + * This component represents a tree view with expandable/collapsible nodes. + * The tree is rendered using
element where every node is represented + * by 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 + * as an expandable tree-table UI widget as well. By default, there is + * one column for node label and one for node value. + * + * The tree is maintaining its (presentation) state, which consists + * from list of expanded nodes and list of columns. + * + * Complete data provider interface: + * var TreeProvider = { + * getChildren: function(object); + * hasChildren: function(object); + * getLabel: function(object, colId); + * getValue: function(object, colId); + * getKey: function(object); + * getType: function(object); + * } + * + * Complete tree decorator interface: + * var TreeDecorator = { + * getRowClass: function(object); + * 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({ + // 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, + // 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, - renderCelL: 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, - // 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, + // Array of columns + columns: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string, + width: PropTypes.string + })) + }, - displayName: "TreeView", + displayName: "TreeView", - getDefaultProps: function() { - return { - object: null, - renderRow: null, - provider: ObjectProvider, - expandedNodes: new Set(), - columns: [] - }; - }, + getDefaultProps: function() { + return { + object: null, + renderRow: null, + provider: ObjectProvider, + expandedNodes: new Set(), + columns: [] + }; + }, - getInitialState: function() { - return { - expandedNodes: this.props.expandedNodes, - columns: ensureDefaultColumn(this.props.columns) - }; - }, + getInitialState: function() { + return { + expandedNodes: this.props.expandedNodes, + columns: ensureDefaultColumn(this.props.columns) + }; + }, - // Node expand/collapse + // Node expand/collapse - toggle: function(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) { - return this.state.expandedNodes.has(nodePath); - }, - - // Event Handlers - - onClickRow: function(nodePath, event) { - event.stopPropagation(); - this.toggle(nodePath); - }, - - // 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) { - let onFilter = this.props.onFilter; - return onFilter ? onFilter(object) : true; - }, - - onSort: function(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) { - // 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 provider = this.props.provider; - let children = provider.getChildren(parent) || []; - - // If the return value is non-array, the children - // are being loaded asynchronously. - if (!Array.isArray(children)) { - return children; - } - - children = this.onSort(parent, children) || children; - - return children.map(child => { - let key = provider.getKey(child); - let nodePath = path + "/" + key; - let type = provider.getType(child); - let hasChildren = provider.hasChildren(child); - - // Value with no column specified is used for optimization. - // The row is re-rendered only if this value changes. - // Value for actual column is get when a cell is rendered. - let value = provider.getValue(child); - - if (isLongString(value)) { - hasChildren = true; + toggle: function(nodePath) { + let nodes = this.state.expandedNodes; + if (this.isExpanded(nodePath)) { + nodes.delete(nodePath); + } else { + nodes.add(nodePath); } - // Return value is a 'member' object containing meta-data about - // tree node. It describes node label, value, type, etc. - return { - // An object associated with this node. - object: child, - // A label for the child node - name: provider.getLabel(child), - // Data type of the child node (used for CSS customization) - type: type, - // Class attribute computed from the type. - rowClass: "treeRow-" + type, - // Level of the child within the hierarchy (top == 0) - level: level, - // True if this node has children. - hasChildren: hasChildren, - // Value associated with this node (as provided by the data provider) - value: value, - // True if the node is expanded. - open: this.isExpanded(nodePath), - // Node path - path: nodePath, - // True if the node is hidden (used for filtering) - hidden: !this.onFilter(child) - }; - }); - }, + // Compute new state and update the tree. + this.setState(Object.assign({}, this.state, { + expandedNodes: nodes + })); + }, - /** - * Render tree rows/nodes. - */ - renderRows: function(parent, level = 0, path = "") { - let rows = []; - let decorator = this.props.decorator; - let renderRow = this.props.renderRow || TreeRow; + isExpanded: function(nodePath) { + return this.state.expandedNodes.has(nodePath); + }, - // 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. - let members = this.getMembers(parent, level, path); - if (!Array.isArray(members)) { - return members; - } + // Event Handlers - members.forEach(member => { - if (decorator && decorator.renderRow) { - renderRow = decorator.renderRow(member.object) || renderRow; + onClickRow: function(nodePath, event) { + event.stopPropagation(); + this.toggle(nodePath); + }, + + // 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) { + let onFilter = this.props.onFilter; + return onFilter ? onFilter(object) : true; + }, + + onSort: function(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) { + // 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 provider = this.props.provider; + let children = provider.getChildren(parent) || []; + + // If the return value is non-array, the children + // are being loaded asynchronously. + if (!Array.isArray(children)) { + return children; + } + + children = this.onSort(parent, children) || children; + + return children.map(child => { + let key = provider.getKey(child); + let nodePath = path + "/" + key; + let type = provider.getType(child); + let hasChildren = provider.hasChildren(child); + + // Value with no column specified is used for optimization. + // The row is re-rendered only if this value changes. + // Value for actual column is get when a cell is rendered. + let value = provider.getValue(child); + + if (isLongString(value)) { + hasChildren = true; + } + + // Return value is a 'member' object containing meta-data about + // tree node. It describes node label, value, type, etc. + return { + // An object associated with this node. + object: child, + // A label for the child node + name: provider.getLabel(child), + // Data type of the child node (used for CSS customization) + type: type, + // Class attribute computed from the type. + rowClass: "treeRow-" + type, + // Level of the child within the hierarchy (top == 0) + level: level, + // True if this node has children. + hasChildren: hasChildren, + // Value associated with this node (as provided by the data provider) + value: value, + // True if the node is expanded. + open: this.isExpanded(nodePath), + // Node path + path: nodePath, + // True if the node is hidden (used for filtering) + hidden: !this.onFilter(child) + }; + }); + }, + + /** + * Render tree rows/nodes. + */ + renderRows: function(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. + let members = this.getMembers(parent, level, path); + if (!Array.isArray(members)) { + return members; + } + + members.forEach(member => { + if (decorator && decorator.renderRow) { + renderRow = decorator.renderRow(member.object) || renderRow; + } + + let props = Object.assign({}, this.props, { + key: member.path, + member: member, + columns: this.state.columns, + onClick: this.onClickRow.bind(this, member.path) + }); + + // Render single row. + rows.push(renderRow(props)); + + // If a child node is expanded render its rows too. + if (member.hasChildren && member.open) { + let childRows = this.renderRows(member.object, level + 1, + 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); + } else { + rows = rows.concat(childRows); + } + } + }); + + return rows; + }, + + render: function() { + let root = this.props.object; + let classNames = ["treeTable"]; + + // Use custom class name from props. + let className = this.props.className; + if (className) { + classNames.push(...className.split(" ")); + } + + // Alright, let's render all tree rows. The tree is one big
. + let rows = this.renderRows(root, 0, ""); + + // This happens when the view needs to do initial asynchronous + // fetch for the root object. The tree might provide a hook API + // for rendering animated spinner (just like for tree nodes). + if (!Array.isArray(rows)) { + rows = []; } let props = Object.assign({}, this.props, { - key: member.path, - member: member, - columns: this.state.columns, - onClick: this.onClickRow.bind(this, member.path) + columns: this.state.columns }); - // Render single row. - rows.push(renderRow(props)); - - // If a child node is expanded render its rows too. - if (member.hasChildren && member.open) { - let childRows = this.renderRows(member.object, level + 1, 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); - } else { - rows = rows.concat(childRows); - } - } - }); - - return rows; - }, - - render: function() { - let root = this.props.object; - let classNames = ["treeTable"]; - - // Use custom class name from props. - let className = this.props.className; - if (className) { - classNames.push(...className.split(" ")); - } - - // Alright, let's render all tree rows. The tree is one big
. - let rows = this.renderRows(root, 0, ""); - - // This happens when the view needs to do initial asynchronous - // fetch for the root object. The tree might provide a hook API - // for rendering animated spinner (just like for tree nodes). - if (!Array.isArray(rows)) { - rows = []; - } - - let props = Object.assign({}, this.props, { - columns: this.state.columns - }); - - return ( - DOM.table({ - className: classNames.join(" "), - cellPadding: 0, - cellSpacing: 0}, - TreeHeader(props), - DOM.tbody({}, - rows + return ( + DOM.table({ + className: classNames.join(" "), + cellPadding: 0, + cellSpacing: 0}, + TreeHeader(props), + DOM.tbody({}, + rows + ) ) - ) - ); + ); + } + }); + + // 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) { + if (!columns) { + columns = []; + } + + let defaultColumn = columns.filter(col => col.id == "default"); + if (defaultColumn.length) { + return columns; + } + + // The default column is usually the first one. + return [{id: "default"}, ...columns]; } + + function isLongString(value) { + return typeof value == "string" && value.length > 50; + } + + // Exports from this module + module.exports = TreeView; }); - -// 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) { - if (!columns) { - columns = []; - } - - let defaultColumn = columns.filter(col => col.id == "default"); - if (defaultColumn.length) { - return columns; - } - - // The default column is usually the first one. - return [{id: "default"}, ...columns]; -} - -function isLongString(value) { - return typeof value == "string" && value.length > 50; -} - -// Exports from this module -module.exports = TreeView;