/**
* Base component to generate tree view in a vertical row. It basically
* controls nodes and its state (collapsed and expanded), loading nodes in an
* asynchronous way (using promises) and gives the parent
* the role of rendering the row.
*/
import React from 'react';
import Fa from './fa';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { Alert } from 'react-bootstrap';
import './tree-view.less';
export default class TreeView extends React.Component {
constructor(props) {
super(props);
this.nodeClick = this.nodeClick.bind(this);
if (props.root) {
this.state = { root: this.createNodes(null, props.root) };
}
else {
this.state = {};
}
}
componentWillMount() {
if (!this.props.root) {
const self = this;
this.loadNodes()
.then(res => self.setState({ root: res }));
}
}
componentDidMount() {
this.mounted = true;
if (this.props.onInit) {
this.props.onInit(this.createHandler());
}
}
componentWillUnmount() {
this.mounted = false;
}
expand(item) {
const node = this.findNode(item);
this._expandNode(node);
}
collapse(item) {
const node = this.findNode(item);
this._collapseNode(node);
}
/**
* Create a handler that will be sent to the parent to have control over the items
* @return {[type]} [description]
*/
createHandler() {
const self = this;
return {
self: self,
/**
* Add a new node to the tree
* @param {[type]} parent Object representing the parent node
* @param {[type]} node Object representing the new child
*/
addNode: (parent, item) => {
const pnode = parent ? self.findNode(parent) : null;
const cnode = self.createNode(pnode, item);
// children are loaded ?
if (pnode && !pnode.leaf && !pnode.children) {
// if no, just load them and refresh it
self._expandNode(pnode);
return;
}
// check if children are initialized
if (pnode && !pnode.children) {
pnode.children = [];
pnode.state = 'expanded';
pnode.leaf = false;
}
const children = pnode ? pnode.children : self.getRoots();
children.push(cnode);
self.forceUpdate();
},
/**
* Remove a node
* @param {[type]} item [description]
* @return {[type]} [description]
*/
remNode: item => {
const node = self.findNode(item);
const lst = node.parent ? node.parent.children : this.getRoots();
const index = lst.indexOf(node);
lst.splice(index, 1);
self.forceUpdate();
},
/**
* Update a node
* @param {[type]} item [description]
* @return {[type]} [description]
*/
updateNode: (olditem, newitem) => {
const node = self.findNode(olditem);
const newnode = self.createNode(node.parent, newitem);
// preserve the current state
newnode.state = node.state;
newnode.children = node.children;
// get the parent list
const lst = node.parent ? node.parent.children : this.getRoots();
// replace in the list
const index = lst.indexOf(node);
lst.splice(index, 1, newnode);
// refresh tree view
self.forceUpdate();
},
/**
* Expand a node, passing the item as argument
* @param {[type]} item [description]
* @return {[type]} [description]
*/
expand: item => {
const node = self.findNode(item);
self._expandNode(node);
},
/**
* Collapse a node, passing the item as argument
* @param {[type]} item [description]
* @return {[type]} [description]
*/
collapse: item => {
const node = self.findNode(item);
self._expandNode(node);
},
/**
* Toggle the state of the node, i.e, collapse or expand it
*/
toggle: item => {
const node = self.findNode(item);
if (node.state === 'collapsed') {
self._expandNode(node);
} else {
self._collapseNode(node);
}
}
};
}
/**
* Search a node by its data. It travesses the whole tree searching for the node
* @param {[type]} data The data representing the node
* @param {[type]} nodes The list of nodes
* @return {[type]} The node or null if no node was found
*/
findNode(data, nodes) {
const lst = nodes ? nodes : this.getRoots();
for (var i = 0; i < lst.length; i++) {
const n = lst[i];
if (n.item === data) {
return n;
}
if (n.children) {
const res = this.findNode(data, n.children);
if (res) {
return res;
}
}
}
return null;
}
/**
* Load the nodes of the tree
* @param {parent} parent The parent node to load items into
*/
loadNodes(parent) {
if (!parent && this.props.root) {
return this.createNodes(parent, this.props.root);
}
const func = this.props.onGetNodes;
if (!func) {
return null;
}
const pitem = parent ? parent.item : undefined;
let res = func(pitem);
// no nodes, then return an empty list
if (!res) {
return Promise.resolve([]);
}
// is not a promise ?
if (!res.then) {
// force node resolution by promises
res = Promise.resolve(res);
}
// create nodes wrapper when nodes are resolved
const self = this;
return res.then(items => {
const nodes = self.createNodes(parent, items);
return nodes;
});
}
/**
* Create nodes from the list of items to add in the tree
* @param {[type]} items [description]
* @return {[type]} [description]
*/
createNodes(parent, items) {
const self = this;
return items.map(item => self.createNode(parent, item));
}
/**
* Create a node object from the data representing the node
* @param {[type]} item [description]
* @return {[type]} [description]
*/
createNode(parent, item) {
const info = this.props.nodeInfo ? this.props.nodeInfo(item) : { leaf: false, expanded: false };
const node = { item: item,
parent: parent,
state: 'collapsed',
children: null,
leaf: info.leaf };
if (info.expanded) {
this._expandNode(node);
}
return node;
}
/**
* Create the React components of the nodes to be displayed in the tree
* @param {[type]} nodes [description]
* @return {[type]} [description]
*/
createNodesView() {
const self = this;
// recursive function to create the expanded tree in a list
const mountList = function(nlist, level, parentkey) {
let count = 0;
const lst = [];
// is the root being rendered
if (!parentkey && self.props.title) {
lst.push(self.props.title);
}
nlist.forEach(node => {
const key = (parentkey ? parentkey + '.' : '') + count;
const row = self.createNodeRow(node, level, key);
lst.push(row);
if (node.state !== 'collapsed' && !node.leaf && node.children) {
lst.push(mountList(node.children, level + 1, key));
}
count++;
});
// the children div key
const divkey = (parentkey ? parentkey : '') + 'ch';
// children are inside a div, in order to animate collapsing/expanding
return (