UNPKG

tui-grid

Version:

TOAST UI Grid : Powerful data grid control supported by TOAST UI

691 lines (584 loc) 22.3 kB
/** * @fileoverview TreeRowList grid data model implementation * @author NHN. FE Development Lab <dl_javascript@nhn.com> */ '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;