/**
* @copyright 2016 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/**
* @module nmodule/webEditors/rc/wb/table/tree/TreeTableModel
*/
define([ 'Promise',
'underscore',
'nmodule/js/rc/switchboard/switchboard',
'nmodule/webEditors/rc/wb/table/model/Row',
'nmodule/webEditors/rc/wb/table/model/TableModel',
'nmodule/webEditors/rc/wb/table/tree/TreeNodeRow',
'nmodule/webEditors/rc/wb/tree/TreeNode' ], function (
Promise,
_,
switchboard,
Row,
TableModel,
TreeNodeRow,
TreeNode) {
'use strict';
const DEPTH_KEY = 'TreeTableModel.depth';
const EXPANDED_KEY = 'TreeTableModel.expanded';
const UNKNOWN_ROW_MSG = 'row not contained in this model';
const SB_QUEUE_UP = { allow: 'oneAtATime', onRepeat: 'queue' };
function isDescendantOf(ancestor, node) {
while ((node = node.getParent())) {
if (node === ancestor) { return true; }
}
}
/**
* API Status: **Development**
*
* A `TableModel` backed by a `TreeNode`. Each Row in the table must also be
* backed by a TreeNode.
*
* You should not typically call this constructor directly - use the `.make()`
* method instead.
*
* @class
* @alias module:nmodule/webEditors/rc/wb/table/tree/TreeTableModel
* @extends module:nmodule/webEditors/rc/wb/table/model/TableModel
* @param {Object} params
* @param {module:nmodule/webEditors/rc/wb/tree/TreeNode} [params.node] if given, the child nodes
* of this node will be used as the initial rows of the table model.
*
* @example
* //when deciding which columns to use in the model, remember that rows
* //will return the actual values via getSubject(), so ordinary
* //Columns/MgrColumns can be used. custom column types can call
* //row.getTreeNode() if necessary.
* TreeTableModel.make({
* columns: [
* new PropertyMgrColumn('prop1'),
* new PropertyMgrColumn('prop2')
* ]
* });
*
* //when inserting, values must be TreeNodes.
* treeTableModel.insertRows(components.map(function (comp) {
* var node = new TreeNode('component:' + comp.getName());
* node.value = function () { return comp; };
* return node;
* });
*/
const TreeTableModel = function TreeTableModel(params) {
TableModel.apply(this, arguments);
this.$node = (params && params.node) || new TreeNode('root');
switchboard(this, {
expand: _.extend(SB_QUEUE_UP, { notWhile: 'collapse' }),
collapse: _.extend(SB_QUEUE_UP, { notWhile: 'expand' })
});
};
TreeTableModel.prototype = Object.create(TableModel.prototype);
TreeTableModel.prototype.constructor = TreeTableModel;
/**
* Create a new `TreeTableModel` instance.
*
* @param {Object} params
* @param {module:nmodule/webEditors/rc/wb/tree/TreeNode} params.node the root
* node backing this model
* @returns {Promise.<module:nmodule/webEditors/rc/wb/table/tree/TreeTableModel>}
* promise to be resolved with the new `TreeTableModel` instance, containing
* one row per child node of the root node passed in
*/
TreeTableModel.make = function (params) {
return Promise.try(function () {
return new TreeTableModel(params);
})
.then((model) => {
return model.$expandForNode(model.$node, 0).then(() => model);
});
};
/**
* Create new `Row`s for the given `TreeNode`'s kids and insert them at the specified
* index.
* @private
* @param {module:nmodule/webEditors/rc/wb/tree/TreeNode} parentNode
* @param {number} index
* @returns {Promise.<Array.<module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow>>}
* promise to be resolved with an array of the inserted rows
*/
TreeTableModel.prototype.$expandForNode = function (parentNode, index) {
return parentNode.getKids()
.then((kids) => {
const kidRows = kids.map((kid) => this.makeRow(kid));
return this.insertRows(kidRows, index).then(() => kidRows);
});
};
/**
* When inserting rows, store away their depth value for future calculations.
* @private
* @override
* @inheritDoc
*/
TreeTableModel.prototype.$validateRowsToInsert = function (rows) {
rows = TableModel.prototype.$validateRowsToInsert.apply(this, arguments);
rows.forEach((row) => row.data(DEPTH_KEY, this.$calculateDepth(row.getTreeNode())));
return rows;
};
/**
* @private
* @param {module:nmodule/webEditors/rc/wb/tree/TreeNode} node
* @returns {number} this node's depth in the TreeTableModel. A depth of 0 indicates a "top-level"
* node - either it has no parent, or it's a direct child of the root node passed to the
* constructor.
*/
TreeTableModel.prototype.$calculateDepth = function (node) {
const parent = node.getParent();
if (parent === this.$node || !parent) {
return 0;
} else {
return this.$calculateDepth(parent) + 1;
}
};
/**
* Get the root node backing this `TreeTableModel`.
* @deprecated as of Niagara 4.14. TreeTableModel has always supported adding freestanding nodes that
* do not all belong to the same root node.
* @returns {module:nmodule/webEditors/rc/wb/tree/TreeNode}
*/
TreeTableModel.prototype.getRootNode = function () { return this.$node; };
/**
* Create a new `Row` instance with a `TreeNode` as the subject.
* @param {module:nmodule/webEditors/rc/wb/tree/TreeNode} subject
* @returns {module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow}
*/
TreeTableModel.prototype.makeRow = function (subject) {
return new TreeNodeRow(subject);
};
/**
* Return true if the `Row`'s `TreeNode` might have child nodes.
* @param {module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow} row
* @returns {boolean}
*/
TreeTableModel.prototype.isExpandable = function (row) {
return row.getTreeNode().mayHaveKids();
};
/**
* Return true if the given `Row` is marked as expanded.
* @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
* @returns {boolean}
*/
TreeTableModel.prototype.isExpanded = function (row) {
return !!row.data(EXPANDED_KEY);
};
/**
* Get the depth of this `Row`'s `TreeNode` from the `TreeTableModel`'s root
* node. A direct child of the root will have a depth of 0, etc.
* @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
* @returns {number|null} the depth of the `Row`, or `null` if the depth could
* not be determined (e.g. the `Row` is not actually contained in this model).
*/
TreeTableModel.prototype.getDepth = function (row) {
const depth = row.data(DEPTH_KEY);
return typeof depth === 'number' ? depth : null;
};
/**
* Get child nodes of the given `Row`'s `TreeNode` and insert new `Row`s for
* each one.
* @param {module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow} row
* @returns {Promise.<Array.<module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow>>}
* promise to be resolved with an array of the inserted rows
*/
TreeTableModel.prototype.expand = function (row) {
const i = this.getRowIndex(row);
if (i < 0) {
return Promise.reject(new Error(UNKNOWN_ROW_MSG));
}
if (row.data(EXPANDED_KEY)) { return Promise.resolve(); }
row.data(EXPANDED_KEY, true);
return this.$expandForNode(row.getTreeNode(), i + 1);
};
/**
* Get child nodes of the given `Row`'s `TreeNode` and remove their
* corresponding `Row`s.
* @param {module:nmodule/webEditors/rc/wb/table/tree/TreeNodeRow} row
* @returns {Promise.<Array.<module:nmodule/webEditors/rc/wb/table/model/Row>>}
* promise to be resolved with an array of the rows that were removed
*/
TreeTableModel.prototype.collapse = function (row) {
if (this.getRowIndex(row) < 0) {
return Promise.reject(new Error(UNKNOWN_ROW_MSG));
}
row.data(EXPANDED_KEY, false);
const node = row.getTreeNode();
const descendantRows = _.filter(this.getRows(), function (row) {
return isDescendantOf(node, row.getTreeNode());
});
return this.removeRows(descendantRows).then(_.constant(descendantRows));
};
/**
* Collapse the row if it is expanded, and vice versa.
* @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
* @returns {Promise}
*/
TreeTableModel.prototype.toggle = function (row) {
return this[this.isExpanded(row) ? 'collapse' : 'expand'](row);
};
return TreeTableModel;
});