reactui
Version:
A components library for ReactJS. This is part of the Gearz project
226 lines (194 loc) • 8.68 kB
JavaScript
;
var React = require("react/addons");
var TreeRow = require("./treeRow.jsx");
var gearzMixin = require("../../gearz.mixin");
var TreeView = React.createClass({
displayName: "TreeView",
mixins: [gearzMixin],
propTypes: {
nodes: React.PropTypes.object.isRequired
},
/**
* ReactJS function to get the component initial state.
* @returns {{nodesCache: {}}}
*/
getInitialState: function getInitialState() {
return {
nodesCache: {}
};
},
/**
* Returns a list containing information about all nodes in a nodes collection.
* @param nodes {Object | Array}
* An object or array representing a nodes collection.
* Each key of this object or array, contains a child node.
* A child node may have an arbitrary set of properties, but children must be in a property called 'nodes'.
* @param path {Array=}
* Path to the node to which the passed nodes collection belongs.
* @param output {Array=}
* Optional array to which items will be pushed, and then returned.
* If none is passed, then a new array is created, and returned.
* @returns {Array}
* An array containing information about all nodes in the tree analysed in pre-order.
* Take a look at http://en.wikipedia.org/wiki/Tree_traversal#Pre-order
*/
flattenNodes: function flattenNodes(nodes, path, output) {
var flatData = output || [];
for (var key in nodes) if (nodes.hasOwnProperty(key)) {
var info = {
node: nodes[key],
path: (path || []).concat(key)
};
flatData.push(info);
this.flattenNodes(info.node.nodes, info.path, flatData);
}
return flatData;
},
/**
* Gets the nodes corresponding to each key in a `path`.
* E.g.:
* `nodes` = {A: {B: {C: {}}}}
* `path` = ["A", "B", "C"]
* `return` => [{B: {C: {}}}]
* @param nodes {Object | Array}
* An object or array representing a nodes collection.
* Each key of this object or array, contains a child node.
* A child node may have an arbitrary set of properties, but children must be in a property called 'nodes'.
* @param path {Array}
* Path to get the nodes from. Each `path` key will be mapped to the corresponding node in the tree.
* @param output {Array=}
* Optional array to which items will be pushed, and then returned.
* If none is passed, then a new array is created, and returned.
* @returns {Array}
* An array containing the nodes that correspond to each `path` key.
*/
getPathNodes: function getPathNodes(nodes, path, output) {
var sequence = output || [];
for (var it = 0; it < path.length; it++) {
var key = path[it];
if (!nodes || !nodes.hasOwnProperty(key)) break;
var node = nodes[key];
sequence.push(node);
nodes = node.nodes;
}
return sequence;
},
/**
* Merges two nodes, the main node with all needed descendants and values,
* and another source with default descendants and values.
* @param main {Object|Array}
* An object or array containing the main descendants and values,
* that have precedence over the descendants and values from the default node.
* @param defaults {Object|Array}
* An object or array containing default descendants and values,
* that are alternatives for elements missing from the main node.
* @returns {Object|Array}
* An object or array containing the merged descendants and values from the main node and the default node.
*/
mergeNodeValues: function mergeNodeValues(main, defaults) {
var result = typeof main == "object" ? {} : Array.isArray(main) ? [] : null;
if (!result) throw new Error("Argument `main` must be an object or an array.");
if (typeof defaults != "object" && Array.isArray(defaults)) throw new Error("Argument `defaults` must be an object or an array.");
for (var key2 in defaults) if (defaults.hasOwnProperty(key2) && key2 != "nodes") result[key2] = defaults[key2];
for (var key3 in main) if (main.hasOwnProperty(key3)) if (defaults.hasOwnProperty(key3) && key3 == "nodes") result[key3] = this.mergeNodeCollections(main[key3], defaults[key3]);else result[key3] = main[key3];
return result;
},
/**
* Merges two node-collections.
* @param main {Object}
* @param defaults {Object}
* @returns {Object}
*/
mergeNodeCollections: function mergeNodeCollections(main, defaults) {
var result = {};
for (var key1 in main) if (main.hasOwnProperty(key1)) if (defaults.hasOwnProperty(key1)) result[key1] = this.mergeNodeValues(main[key1], defaults[key1]);else result[key1] = main[key1];
return result;
},
/**
* Handles all node changes and triggers events indicating what node changed,
* and what value of the node changed.
* @param eventObject {Object}
* An object containing information about the event.
*/
onNodeChange: function onNodeChange(eventObject) {
function mergeOrCreate(previousValue) {
return function (value) {
return React.addons.update(typeof value != "object" ? {} : value, previousValue);
};
}
// Input:
// eventObject.path = ["app","page","editPanel"]
// eventObject.key = "collapsed"
// eventObject.value = true
// Output:
// merger = {nodes: {$mergeOrCreate: {app: {$mergeOrCreate: {page: {$mergeOrCreate: {editPanel: {$mergeOrCreate: {collapsed: {$set: true}}}}}}}}}}
var setter = {};
setter[eventObject.key] = { $set: eventObject.value };
var merger = eventObject.path.reduceRight(function (innerMerger, currentPathItem) {
var itemMerger = {};
itemMerger[currentPathItem] = { $apply: mergeOrCreate(innerMerger) };
var nodesMerger = { nodes: { $apply: mergeOrCreate(itemMerger) } };
return nodesMerger;
}, setter);
// determining what is the new state
var newState = React.addons.update(this.state, { nodesCache: merger.nodes });
this.setState(newState);
// calling external event handlers
if (eventObject.trigger(eventObject.genericEventName)) {
return;
}if (eventObject.trigger(eventObject.specificEventName)) {
}
},
/**
* Determined whether a node visible or not.
* A node is invisible when any ancestor node is collapsed.
* @param nodes {Object|Array}
* An object or array that represents the root node, from which path nodes will be taken.
* @param path {Array}
* An array containing the path components into the passed node.
* @returns {boolean}
* True if the node is hidden; otherwise false.
*/
isNodeHidden: function isNodeHidden(nodes, path) {
var ancestors = this.getPathNodes(nodes, path);
ancestors.pop();
return ancestors.map(function (x) {
return x.collapsed;
}).reduce(function (a, b) {
return a || b;
}, false);
},
/**
* ReactJS rendering function.
* @returns {XML}
*/
render: function render() {
var _this = this;
var nodes = this.get("nodes");
var mergedNodes = this.mergeNodeCollections(nodes, this.state.nodesCache);
var flattenNodes = this.flattenNodes(mergedNodes);
var children = flattenNodes.map(function (info) {
return _this.isNodeHidden(mergedNodes, info.path) ? null : React.createElement(TreeRow, {
nodes: info.node.nodes,
collapsed: info.node.collapsed,
display: info.node.display,
path: info.path,
onAnyChange: function (e) {
var newPath = Object.freeze([].concat(info.path));
var eventData = e.merge({
target: _this,
path: newPath,
specificEventName: "Node" + e.specificEventName
});
_this.onNodeChange(eventData);
}
});
});
return React.createElement(
"ul",
{ className: "list-group" },
children
);
}
});
module.exports = TreeView;