summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/tree
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/tree')
-rw-r--r--devtools/client/shared/components/tree/label-cell.js66
-rw-r--r--devtools/client/shared/components/tree/moz.build14
-rw-r--r--devtools/client/shared/components/tree/object-provider.js90
-rw-r--r--devtools/client/shared/components/tree/tree-cell.js101
-rw-r--r--devtools/client/shared/components/tree/tree-header.js100
-rw-r--r--devtools/client/shared/components/tree/tree-row.js184
-rw-r--r--devtools/client/shared/components/tree/tree-view.css157
-rw-r--r--devtools/client/shared/components/tree/tree-view.js352
8 files changed, 1064 insertions, 0 deletions
diff --git a/devtools/client/shared/components/tree/label-cell.js b/devtools/client/shared/components/tree/label-cell.js
new file mode 100644
index 000000000..e14875b4d
--- /dev/null
+++ b/devtools/client/shared/components/tree/label-cell.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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;
+
+ /**
+ * Render the default cell used for toggle buttons
+ */
+ let LabelCell = React.createClass({
+ displayName: "LabelCell",
+
+ // See the TreeView component for details related
+ // to the 'member' object.
+ propTypes: {
+ member: PropTypes.object.isRequired
+ },
+
+ 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 = {
+ "paddingInlineStart": (level * 16) + "px",
+ };
+
+ let iconClassList = ["treeIcon"];
+ if (member.hasChildren && member.loading) {
+ iconClassList.push("devtools-throbber");
+ } else if (member.hasChildren) {
+ iconClassList.push("theme-twisty");
+ }
+ if (member.open) {
+ iconClassList.push("open");
+ }
+
+ return (
+ td({
+ className: "treeLabelCell",
+ key: "default",
+ style: rowStyle},
+ span({ className: iconClassList.join(" ") }),
+ span({
+ className: "treeLabel " + member.type + "Label",
+ "data-level": level
+ }, member.name)
+ )
+ );
+ }
+ });
+
+ // Exports from this module
+ module.exports = LabelCell;
+});
diff --git a/devtools/client/shared/components/tree/moz.build b/devtools/client/shared/components/tree/moz.build
new file mode 100644
index 000000000..a7413f25a
--- /dev/null
+++ b/devtools/client/shared/components/tree/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'label-cell.js',
+ 'object-provider.js',
+ 'tree-cell.js',
+ 'tree-header.js',
+ 'tree-row.js',
+ 'tree-view.css',
+ 'tree-view.js',
+)
diff --git a/devtools/client/shared/components/tree/object-provider.js b/devtools/client/shared/components/tree/object-provider.js
new file mode 100644
index 000000000..58519f81f
--- /dev/null
+++ b/devtools/client/shared/components/tree/object-provider.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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);
+ }
+ }
+ 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 > 0;
+ },
+
+ 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;
+ }
+
+ // 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
new file mode 100644
index 000000000..f3c48510f
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-cell.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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 <td> element (the row is <tr> and the entire tree is <table>).
+ */
+ let TreeCell = React.createClass({
+ displayName: "TreeCell",
+
+ // 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
+ },
+
+ /**
+ * Optimize cell rendering. Rerender cell content only if
+ * the value or expanded state changes.
+ */
+ shouldComponentUpdate: function (nextProps) {
+ return (this.props.value != nextProps.value) ||
+ (this.props.member.open != nextProps.member.open);
+ },
+
+ 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 <td> 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))
+ )
+ );
+ }
+ });
+
+ // Default value rendering.
+ let 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
new file mode 100644
index 000000000..eec5363dd
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-header.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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;
+
+ /**
+ * This component is responsible for rendering tree header.
+ * It's based on <thead> element.
+ */
+ let TreeHeader = React.createClass({
+ displayName: "TreeHeader",
+
+ // 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
+ },
+
+ 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 : "",
+ };
+
+ 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
+ ))
+ );
+ }
+ });
+
+ // 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
new file mode 100644
index 000000000..adfb1f3ae
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-row.js
@@ -0,0 +1,184 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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"));
+
+ // 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",
+
+ // 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
+ },
+
+ 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");
+ }
+ },
+
+ /**
+ * 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;
+ },
+
+ 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 [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ },
+
+ render: function () {
+ let member = this.props.member;
+ let decorator = this.props.decorator;
+
+ // Compute class name list for the <tr> element.
+ let classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ 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 = [];
+
+ // 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)
+ });
+
+ 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));
+ }
+ });
+
+ // Render tree row
+ return (
+ tr({
+ className: classNames.join(" "),
+ onClick: this.props.onClick},
+ cells
+ )
+ );
+ }
+ });
+
+ // Helpers
+
+ let RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ let RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});
diff --git a/devtools/client/shared/components/tree/tree-view.css b/devtools/client/shared/components/tree/tree-view.css
new file mode 100644
index 000000000..850533872
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -0,0 +1,157 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url('resource://devtools/client/shared/components/reps/reps.css');
+
+/******************************************************************************/
+/* TreeView Colors */
+
+:root {
+ --tree-link-color: blue;
+ --tree-header-background: #C8D2DC;
+ --tree-header-sorted-background: #AAC3DC;
+}
+
+/******************************************************************************/
+/* TreeView Table*/
+
+.treeTable .treeLabelCell {
+ padding: 2px 0;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.treeTable .treeLabelCell::after {
+ content: ":";
+ color: var(--object-color);
+}
+
+.treeTable .treeValueCell {
+ padding: 2px 0;
+ padding-inline-start: 5px;
+ overflow: hidden;
+}
+
+.treeTable .treeLabel {
+ cursor: default;
+ overflow: hidden;
+ padding-inline-start: 4px;
+ white-space: nowrap;
+}
+
+/* No paddding if there is actually no label */
+.treeTable .treeLabel:empty {
+ padding-inline-start: 0;
+}
+
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ cursor: pointer;
+ color: var(--tree-link-color);
+ text-decoration: underline;
+}
+
+/* Filtering */
+.treeTable .treeRow.hidden {
+ display: none;
+}
+
+/******************************************************************************/
+/* Toggle Icon */
+
+.treeTable .treeRow .treeIcon {
+ height: 14px;
+ width: 14px;
+ font-size: 10px; /* Set the size of loading spinner */
+ display: inline-block;
+ vertical-align: bottom;
+ margin-inline-start: 3px;
+ padding-top: 1px;
+}
+
+/* All expanded/collapsed styles need to apply on immediate children
+ since there might be nested trees within a tree. */
+.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+ cursor: pointer;
+ background-repeat: no-repeat;
+}
+
+/******************************************************************************/
+/* Header */
+
+.treeTable .treeHeaderRow {
+ height: 18px;
+}
+
+.treeTable .treeHeaderCell {
+ cursor: pointer;
+ -moz-user-select: none;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 0 !important;
+ background: linear-gradient(
+ rgba(255, 255, 255, 0.05),
+ rgba(0, 0, 0, 0.05)),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%) repeat-x var(--tree-header-background);
+ color: var(--theme-body-color);
+ white-space: nowrap;
+}
+
+.treeTable .treeHeaderCellBox {
+ padding: 2px 0;
+ padding-inline-start: 10px;
+ padding-inline-end: 14px;
+}
+
+.treeTable .treeHeaderRow > .treeHeaderCell:first-child > .treeHeaderCellBox {
+ padding: 0;
+}
+
+.treeTable .treeHeaderSorted {
+ background-color: var(--tree-header-sorted-background);
+}
+
+.treeTable .treeHeaderSorted > .treeHeaderCellBox {
+ background: url(chrome://devtools/skin/images/firebug/arrow-down.svg) no-repeat calc(100% - 4px);
+}
+
+.treeTable .treeHeaderSorted.sortedAscending > .treeHeaderCellBox {
+ background-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
+}
+
+.treeTable .treeHeaderCell:hover:active {
+ background-image: linear-gradient(
+ rgba(0, 0, 0, 0.1),
+ transparent),
+ radial-gradient(1px 60% at right,
+ rgba(0, 0, 0, 0.8) 0%,
+ transparent 80%);
+}
+
+/******************************************************************************/
+/* Themes */
+
+.theme-light .treeTable .treeRow:hover,
+.theme-dark .treeTable .treeRow:hover {
+ background-color: var(--theme-selection-background-semitransparent) !important;
+}
+
+.theme-firebug .treeTable .treeRow:hover {
+ background-color: var(--theme-body-background);
+}
+
+.theme-light .treeTable .treeLabel,
+.theme-dark .treeTable .treeLabel {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover,
+.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
+ color: var(--theme-highlight-pink);
+}
+
+.theme-firebug .treeTable .treeLabel {
+ color: var(--theme-body-color);
+}
diff --git a/devtools/client/shared/components/tree/tree-view.js b/devtools/client/shared/components/tree/tree-view.js
new file mode 100644
index 000000000..9fae9addb
--- /dev/null
+++ b/devtools/client/shared/components/tree/tree-view.js
@@ -0,0 +1,352 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// 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"));
+
+ // Shortcuts
+ const DOM = React.DOM;
+ const PropTypes = React.PropTypes;
+
+ /**
+ * This component represents a tree view with expandable/collapsible nodes.
+ * The tree is rendered using <table> element where every node is represented
+ * by <tr> element. The tree is one big table where nodes (rows) are properly
+ * indented from the left to mimic hierarchical structure of the data.
+ *
+ * The tree can have arbitrary number of columns and so, might be use
+ * 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({
+ displayName: "TreeView",
+
+ // 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,
+ // 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
+ }))
+ },
+
+ 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)
+ };
+ },
+
+ // 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;
+ }
+
+ // 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 <table>.
+ 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
+ )
+ )
+ );
+ }
+ });
+
+ // 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;
+});