/**
* @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';
var DEPTH_KEY = 'TreeTableModel.depth',
EXPANDED_KEY = 'TreeTableModel.expanded',
UNKNOWN_ROW_MSG = 'row not contained in this model',
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] the
* root `TreeNode` backing this 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;
* });
*/
var 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(function (model) {
return model.$expandForNode(model.$node, 0).then(_.constant(model));
});
};
/**
* Create new `Row`s for the given `TreeNode` 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/model/Row>>}
* promise to be resolved with an array of the inserted rows
*/
TreeTableModel.prototype.$expandForNode = function (parentNode, index) {
var that = this,
depth = parentNode.getFullPath().length - 1;
return parentNode.getKids()
.then(function (kids) {
var rows = _.map(kids, function (kid) {
var row = that.makeRow(kid);
row.data(DEPTH_KEY, depth);
return row;
});
return that.insertRows(rows, index).then(_.constant(rows));
});
};
/**
* Get the root node backing this `TreeTableModel`.
* @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) {
var 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) {
var 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);
var node = row.getTreeNode(),
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;
});