/**
* @fileoverview TreeRowList grid data model implementation
* @author NHN Ent. FE Development Team
*/
'use strict';
var _ = require('underscore');
var util = require('tui-code-snippet');
var RowList = require('./rowList');
var TreeRow = require('./treeRow');
var TreeRowList;
/**
* Create empty tree-row data
* @returns {object} tree data
* @ignore
*/
function createEmptyTreeRowData() {
return {
_treeData: {
hasNextSibling: []
}
};
}
/**
* TreeRowList class implementation
* @module model/data/treeModel
* @extends module:base/collection
* @ignore
*/
TreeRowList = RowList.extend(/** @lends module:model/data/treeRowList.prototype */{
initialize: function() {
RowList.prototype.initialize.apply(this, arguments);
/**
* root row which actually does not exist.
* it keeps depth 1 rows as it's children
* @type {object}
*/
this._rootRow = createEmptyTreeRowData();
},
model: TreeRow,
/**
* flattened tree row to grid row
* process _extraData then set rowSpanData value
* this function overrides RowList._formatData to deal with rowKey here
*
* @param {array|object} data - rowList
* @param {object} options - append options
* @returns {array} rowList with row
* @override
* @private
*/
_formatData: function(data, options) {
var rootRow = createEmptyTreeRowData();
var flattenedRow = [];
var rowList, parentRow, parentRowKey;
rowList = _.filter(data, _.isObject);
rowList = util.isArray(rowList) ? rowList : [rowList];
if (options) {
// probably an append operation
// which requires specific parent row
parentRowKey = options.parentRowKey;
if (_.isNumber(parentRowKey) || _.isString(parentRowKey)) {
parentRow = this.get(options.parentRowKey);
rootRow._treeData.childrenRowKeys
= parentRow.getTreeChildrenRowKeys();
rootRow._treeData.hasNextSibling
= parentRow.hasTreeNextSibling().slice(0);
rootRow.rowKey = options.parentRowKey;
} else {
// no parent row key means root row
rootRow = this._rootRow;
}
} else {
// from setOriginal or setData
// which requires to reset root row
this._rootRow = rootRow;
}
this._flattenRow(rowList, flattenedRow, [rootRow]);
if (parentRow) {
parentRow.setTreeChildrenRowKeys(rootRow._treeData.childrenRowKeys);
}
_.each(flattenedRow, function(row, i) {
if (this.isRowSpanEnable()) {
this._setExtraRowSpanData(flattenedRow, i);
}
}, this);
return flattenedRow;
},
/**
* Flatten nested tree data to 1-depth grid data.
* @param {array} treeRows - nested rows having children
* @param {array} flattenedRows - flattend rows. you should give an empty array at the initial call of this function
* @param {array} ancestors - ancester rows
*/
_flattenRow: function(treeRows, flattenedRows, ancestors) {
var parent;
var lastSibling = treeRows[treeRows.length - 1];
parent = ancestors[ancestors.length - 1];
parent._treeData.childrenRowKeys = parent._treeData.childrenRowKeys || [];
_.each(treeRows, function(row) {
// sets rowKey property
row = this._baseFormat(row);
row._treeData = {
parentRowKey: parent.rowKey,
hasNextSibling: parent._treeData.hasNextSibling.concat([lastSibling !== row]),
childrenRowKeys: row._children ? [] : null
};
parent._treeData.childrenRowKeys.push(row.rowKey);
flattenedRows.push(row);
if (util.isArray(row._children)) {
this._flattenRow(row._children, flattenedRows, ancestors.concat([row]));
delete row._children;
}
}, this);
},
/**
* calculate index of given parent row key and offset
* @param {number|string} parentRowKey - parent row key
* @param {number} offset - offset
* @returns {number} - calculated index
* @private
*/
_indexOfParentRowKeyAndOffset: function(parentRowKey, offset) {
var at, parentRow, childrenRowKeys;
parentRow = this.get(parentRowKey);
if (parentRow) {
childrenRowKeys = parentRow.getTreeChildrenRowKeys();
at = this.indexOf(parentRow);
} else {
childrenRowKeys = this._rootRow._treeData.childrenRowKeys;
at = -1; // root row actually doesn't exist
}
offset = Math.max(0, offset);
offset = Math.min(offset, childrenRowKeys.length);
if (childrenRowKeys.length === 0 || offset === 0) {
// first sibling
// then the `at` is right after the parent row
at = at + 1;
} else if (childrenRowKeys.length > offset) {
// not the last sibling
// right before the next sibling
at = this.indexOf(this.get(childrenRowKeys[offset]));
} else {
// last sibling
at = this.indexOf(this.get(childrenRowKeys[childrenRowKeys.length - 1]));
// and after all it's descendant rows and itself
at += this.getTreeDescendantRowKeys(at).length + 1;
}
return at;
},
/**
* update hasNextSibling value of previous sibling and of itself
* @param {number|string} rowKey - row key
* @private
*/
_syncHasTreeNextSiblingData: function(rowKey) {
var currentRow = this.get(rowKey);
var currentDepth, prevSiblingRow, nextSiblingRow;
if (!currentRow) {
return;
}
currentDepth = currentRow.getTreeDepth();
prevSiblingRow = this.get(this.getTreePrevSiblingRowKey(rowKey));
nextSiblingRow = this.get(this.getTreeNextSiblingRowKey(rowKey));
currentRow.hasTreeNextSibling()[currentDepth - 1] = !!nextSiblingRow;
if (prevSiblingRow) {
prevSiblingRow.hasTreeNextSibling()[currentDepth - 1] = true;
}
},
/**
* Insert the new row with specified data to the end of table.
* @param {array|object} [rowData] - The data for the new row
* @param {object} [options] - Options
* @param {number|string} [options.parentRowKey] - row key of the parent which appends given rows
* @param {number} [options.offset] - offset from first sibling
* @param {boolean} [options.focus] - If set to true, move focus to the new row after appending
* @returns {Array.<module:model/data/treeTow>} Row model list
* @override
*/
appendRow: function(rowData, options) {
var modelList;
options = _.extend({
at: this._indexOfParentRowKeyAndOffset(options.parentRowKey, options.offset)
}, options);
modelList = this._appendRow(rowData, options);
this._syncHasTreeNextSiblingData(modelList[0].get('rowKey'));
if (modelList.length > 1) {
this._syncHasTreeNextSiblingData(modelList[modelList.length - 1].get('rowKey'));
}
this.trigger('add', modelList, options);
return modelList;
},
/**
* Insert the given data into the very first row of root
* @param {array|object} [rowData] - The data for the new row
* @param {object} [options] - Options
* @param {boolean} [options.focus] - If set to true, move focus to the new row after appending
* @returns {Array.<module:model/data/treeTow>} Row model list
*/
prependRow: function(rowData, options) {
options = options || {};
options.parentRowKey = null;
options.offset = 0;
return this.appendRow(rowData, options);
},
_removeChildFromParent: function(childRowKey) {
var parentRowKey = this.get(childRowKey).getTreeParentRowKey();
var parentRow = this.get(parentRowKey);
var rootTreeData = this._rootRow._treeData;
if (parentRow) {
parentRow.removeTreeChildrenRowKey(childRowKey);
} else {
rootTreeData.childrenRowKeys = _.filter(rootTreeData.childrenRowKeys, function(rootChildRowKey) {
return rootChildRowKey !== childRowKey;
}, this);
}
},
_removeRow: function(rowKey, options) {
this._removeChildFromParent(rowKey);
RowList.prototype._removeRow.call(this, rowKey, options);
},
/**
* remove row of given row key. it will also remove it's descendant
* @param {number|string} rowKey - 행 데이터의 고유 키
* @param {object} options - 삭제 옵션
* @param {boolean} options.removeOriginalData - 원본 데이터도 함께 삭제할 지 여부
* @param {boolean} options.keepRowSpanData - rowSpan이 mainRow를 삭제하는 경우 데이터를 유지할지 여부
* @override
*/
removeRow: function(rowKey, options) {
var row = this.get(rowKey);
var parentRowKey = row.getTreeParentRowKey();
var currentIndex = this.indexOf(row);
var prevSiblingRowKey = this.getTreePrevSiblingRowKey(rowKey);
var descendantRowKeys;
if (!row) {
return;
}
// remove descendant rows including itself
descendantRowKeys = this.getTreeDescendantRowKeys(rowKey);
descendantRowKeys.reverse().push(rowKey);
_.each(descendantRowKeys, function(descendantRowKey) {
this._removeRow(descendantRowKey, options);
}, this);
if (_.isNumber(prevSiblingRowKey) || _.isString(prevSiblingRowKey)) {
this._syncHasTreeNextSiblingData(prevSiblingRowKey);
}
if (options && options.removeOriginalData) {
this.setOriginalRowList();
}
this.trigger('remove', rowKey, currentIndex, descendantRowKeys, parentRowKey);
},
/**
* get row keys of sibling and of itself
* @param {number|string} rowKey - row key
* @returns {Array.<number|string>} - sibling row keys
*/
getTreeSiblingRowKeys: function(rowKey) {
var parentRow = this.get(this.get(rowKey).getTreeParentRowKey());
var childrenRowKeys;
if (parentRow) {
childrenRowKeys = parentRow.getTreeChildrenRowKeys();
} else {
childrenRowKeys = this._rootRow._treeData.childrenRowKeys.slice(0);
}
return childrenRowKeys;
},
/**
* get row key of previous sibling
* @param {number|string} rowKey - row key
* @returns {number|string} - previous sibling row key
*/
getTreePrevSiblingRowKey: function(rowKey) {
var siblingRowKeys = this.getTreeSiblingRowKeys(rowKey);
var currentIndex = siblingRowKeys.indexOf(rowKey);
return currentIndex > 0 ? siblingRowKeys[currentIndex - 1] : null;
},
/**
* get row key of next sibling
* @param {number|string} rowKey - row key
* @returns {number|string} - next sibling row key
*/
getTreeNextSiblingRowKey: function(rowKey) {
var siblingRowKeys = this.getTreeSiblingRowKeys(rowKey);
var currentIndex = siblingRowKeys.indexOf(rowKey);
return (currentIndex + 1 >= siblingRowKeys.length)
? null : siblingRowKeys[currentIndex + 1];
},
/**
* get top most row keys
* @returns {Array.<number|string>} - row keys
*/
getTopMostRowKeys: function() {
return this._rootRow._treeData.childrenRowKeys;
},
/**
* get tree children of row of given rowKey
* @param {number|string} rowKey - row key
* @returns {Array.<number|string>} - children of found row
*/
getTreeChildrenRowKeys: function(rowKey) {
var row = this.get(rowKey);
return row.getTreeChildrenRowKeys();
},
/**
* get tree descendant of row of given rowKey
* @param {number|string} rowKey - row key
* @returns {Array.<number|string>} - descendant of found row
*/
getTreeDescendantRowKeys: function(rowKey) {
var index = 0;
var rowKeys = [rowKey];
while (index < rowKeys.length) {
rowKeys = rowKeys.concat(this.getTreeChildrenRowKeys(rowKeys[index]));
index += 1;
}
rowKeys.shift();
return rowKeys;
},
/**
* expand tree row
* @param {number|string} rowKey - row key
* @param {boolean} recursive - true for recursively expand all descendant
* @param {boolean} silent - true to mute event
* @returns {Array.<number|string>} - children or descendant of given row
*/
treeExpand: function(rowKey, recursive, silent) {
var descendantRowKeys = this.getTreeDescendantRowKeys(rowKey);
var row = this.get(rowKey);
row.setTreeExpanded(true);
if (recursive) {
_.each(descendantRowKeys, function(descendantRowKey) {
var descendantRow = this.get(descendantRowKey);
if (descendantRow.hasTreeChildren()) {
descendantRow.setTreeExpanded(true);
}
}, this);
} else {
descendantRowKeys = _.filter(descendantRowKeys, function(descendantRowKey) {
return this.isTreeVisible(descendantRowKey);
}, this);
}
if (!silent) {
/**
* Occurs when the row having child rows is expanded
* @event Grid#expanded
* @type {module:event/gridEvent}
* @property {number|string} rowKey - rowKey of the expanded row
* @property {Array.<number|string>} descendantRowKeys - rowKey list of all descendant rows
* @property {Grid} instance - Current grid instance
*/
this.trigger('expanded', {
rowKey: rowKey,
descendantRowKeys: descendantRowKeys.slice(0)
});
}
return descendantRowKeys;
},
/**
* expand all rows
*/
treeExpandAll: function() {
var topMostRowKeys = this.getTopMostRowKeys();
_.each(topMostRowKeys, function(topMostRowKey) {
this.treeExpand(topMostRowKey, true, true);
}, this);
/**
* Occurs when all rows having child rows are expanded
* @event Grid#expandedAll
*/
this.trigger('expandedAll');
},
/**
* collapse tree row
* @param {number|string} rowKey - row key
* @param {boolean} recursive - true for recursively expand all descendant
* @param {boolean} silent - true to mute event
* @returns {Array.<number|string>} - children or descendant of given row
*/
treeCollapse: function(rowKey, recursive, silent) {
var row = this.get(rowKey);
var descendantRowKeys = this.getTreeDescendantRowKeys(rowKey);
if (recursive) {
_.each(descendantRowKeys, function(descendantRowKey) {
var descendantRow = this.get(descendantRowKey);
if (descendantRow.hasTreeChildren()) {
descendantRow.setTreeExpanded(false);
}
}, this);
} else {
descendantRowKeys = _.filter(descendantRowKeys, function(descendantRowKey) {
return this.isTreeVisible(descendantRowKey);
}, this);
}
row.setTreeExpanded(false);
if (!silent) {
/**
* Occurs when the row having child rows is collapsed
* @event Grid#collapsed
* @type {module:event/gridEvent}
* @property {number|string} rowKey - rowKey of the collapsed row
* @property {Array.<number|string>} descendantRowKeys - rowKey list of all descendant rows
* @property {Grid} instance - Current grid instance
*/
this.trigger('collapsed', {
rowKey: rowKey,
descendantRowKeys: descendantRowKeys.slice(0)
});
}
return descendantRowKeys;
},
/**
* collapse all rows
*/
treeCollapseAll: function() {
var topMostRowKeys = this.getTopMostRowKeys();
_.each(topMostRowKeys, function(topMostRowKey) {
this.treeCollapse(topMostRowKey, true, true);
}, this);
/**
* Occurs when all rows having child rows are collapsed
* @event Grid#collapsedAll
*/
this.trigger('collapsedAll');
},
/**
* get the parent of the row which has the given row key
* @param {number|string} rowKey - row key
* @returns {TreeRow} - the parent row
*/
getTreeParent: function(rowKey) {
var row = this.get(rowKey);
if (!row) {
return null;
}
return this.get(row.getTreeParentRowKey());
},
/**
* get the ancestors of the row which has the given row key
* @param {number|string} rowKey - row key
* @returns {Array.<TreeRow>} - the ancestor rows
*/
getTreeAncestors: function(rowKey) {
var ancestors = [];
var row = this.getTreeParent(rowKey);
while (row) {
ancestors.push(row);
row = this.getTreeParent(row.get('rowKey'));
}
return ancestors.reverse();
},
/**
* get the children of the row which has the given row key
* @param {number|string} rowKey - row key
* @returns {Array.<TreeRow>} - the children rows
*/
getTreeChildren: function(rowKey) {
var childrenRowKeys = this.getTreeChildrenRowKeys(rowKey);
return _.map(childrenRowKeys, function(childRowKey) {
return this.get(childRowKey);
}, this);
},
/**
* get the descendants of the row which has the given row key
* @param {number|string} rowKey - row key
* @returns {Array.<TreeRow>} - the descendant rows
*/
getTreeDescendants: function(rowKey) {
var descendantRowKeys = this.getTreeDescendantRowKeys(rowKey);
return _.map(descendantRowKeys, function(descendantRowKey) {
return this.get(descendantRowKey);
}, this);
},
/**
* get the depth of the row which has the given row key
* @param {number|string} rowKey - row key
* @returns {number} - the depth
*/
getTreeDepth: function(rowKey) {
var row = this.get(rowKey);
var depth;
if (row) {
return row.getTreeDepth();
}
return depth;
},
/**
* test if the row of given key should be visible
* @param {string|number} rowKey - row key to test
* @returns {boolean} - true if visible
*/
isTreeVisible: function(rowKey) {
var visible = true;
_.each(this.getTreeAncestors(rowKey), function(ancestor) {
visible = visible && ancestor.getTreeExpanded();
}, this);
return visible;
},
/**
* Check whether the row is visible or not
* @returns {boolean} state
* @override
* @todo Change the method name from isTreeVisible to isVisibleRow
*/
isVisibleRow: function(rowKey) {
return this.isTreeVisible(rowKey);
},
/**
* Check the checkbox input in the row header
* @param {number} rowKey - Current row key
* @override
*/
check: function(rowKey) {
var selectType = this.columnModel.get('selectType');
if (selectType === 'radio') {
this.uncheckAll();
}
this._setCheckedState(rowKey, true);
},
/**
* Uncheck the checkbox input in the row header
* @param {number} rowKey - Current row key
* @override
*/
uncheck: function(rowKey) {
this._setCheckedState(rowKey, false);
},
/**
* Set checked state by using a cascading option
* @param {number} rowKey - Current row key
* @param {boolean} state - Whether checking the input button or not
* @private
*/
_setCheckedState: function(rowKey, state) {
var useCascadingCheckbox = this.columnModel.useCascadingCheckbox();
this.setValue(rowKey, '_button', state);
if (useCascadingCheckbox) {
this._updateDecendantsCheckedState(rowKey, state);
this._updateAncestorsCheckedState(rowKey);
}
},
/**
* Update checked state of all descendant rows
* @param {number} rowKey - Current row key
* @param {boolean} state - Whether checking the input button or not
* @private
*/
_updateDecendantsCheckedState: function(rowKey, state) {
var descendants = this.getTreeDescendants(rowKey);
_.each(descendants, function(descendantRowKey) {
this.setValue(descendantRowKey, '_button', state);
}, this);
},
/**
* Update checked state of all ancestor rows
* @param {number} rowKey - Current row key
* @param {boolean} state - Whether checking the input button or not
* @private
*/
_updateAncestorsCheckedState: function(rowKey) {
var parentRowKey = this.get(rowKey).getTreeParentRowKey();
while (parentRowKey > -1) {
this._setCheckedStateToParent(parentRowKey);
parentRowKey = this.get(parentRowKey).getTreeParentRowKey();
}
},
/**
* Set checked state of the parent row according to the checked children rows
* @param {number} rowKey - Current row key
* @private
*/
_setCheckedStateToParent: function(rowKey) {
var childernRowKeys = this.get(rowKey).getTreeChildrenRowKeys();
var checkedChildrenCnt = 0;
var checkedState;
_.each(childernRowKeys, function(childRowKey) {
if (this.get(childRowKey).get('_button')) {
checkedChildrenCnt += 1;
}
}, this);
checkedState = checkedChildrenCnt === childernRowKeys.length;
this.setValue(rowKey, '_button', checkedState);
}
});
module.exports = TreeRowList;