UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

709 lines (672 loc) • 22.7 kB
import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.push.js"; function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import { rangeEach } from "../../../helpers/number.mjs"; import { objectEach } from "../../../helpers/object.mjs"; import { arrayEach } from "../../../helpers/array.mjs"; /** * Class responsible for making data operations. * * @private */ class DataManager { constructor(nestedRowsPlugin, hotInstance) { /** * Main Handsontable instance reference. * * @type {object} */ _defineProperty(this, "hot", void 0); /** * Reference to the source data object. * * @type {Handsontable.CellValue[][]|Handsontable.RowObject[]} */ _defineProperty(this, "data", null); /** * Reference to the NestedRows plugin. * * @type {object} */ _defineProperty(this, "plugin", void 0); /** * Map of row object parents. * * @type {WeakMap} */ _defineProperty(this, "parentReference", new WeakMap()); /** * Nested structure cache. * * @type {object} */ _defineProperty(this, "cache", { levels: [], levelCount: 0, rows: [], nodeInfo: new WeakMap() }); this.hot = hotInstance; this.plugin = nestedRowsPlugin; } /** * Set the data for the manager. * * @param {Handsontable.CellValue[][]|Handsontable.RowObject[]} data Data for the manager. */ setData(data) { this.data = data; } /** * Get the data cached in the manager. * * @returns {Handsontable.CellValue[][]|Handsontable.RowObject[]} */ getData() { return this.data; } /** * Load the "raw" source data, without NestedRows' modifications. * * @returns {Handsontable.CellValue[][]|Handsontable.RowObject[]} */ getRawSourceData() { let rawSourceData = null; this.plugin.disableCoreAPIModifiers(); rawSourceData = this.hot.getSourceData(); this.plugin.enableCoreAPIModifiers(); return rawSourceData; } /** * Update the Data Manager with new data and refresh cache. * * @param {Handsontable.CellValue[][]|Handsontable.RowObject[]} data Data for the manager. */ updateWithData(data) { this.setData(data); this.rewriteCache(); } /** * Rewrite the nested structure cache. * * @private */ rewriteCache() { this.cache = { levels: [], levelCount: 0, rows: [], nodeInfo: new WeakMap() }; rangeEach(0, this.data.length - 1, i => { this.cacheNode(this.data[i], 0, null); }); } /** * Cache a data node. * * @private * @param {object} node Node to cache. * @param {number} level Level of the node. * @param {object} parent Parent of the node. */ cacheNode(node, level, parent) { if (!this.cache.levels[level]) { this.cache.levels[level] = []; this.cache.levelCount += 1; } this.cache.levels[level].push(node); this.cache.rows.push(node); this.cache.nodeInfo.set(node, { parent, row: this.cache.rows.length - 1, level }); if (this.hasChildren(node)) { arrayEach(node.__children, elem => { this.cacheNode(elem, level + 1, node); }); } } /** * Get the date for the provided visual row number. * * @param {number} row Row index. * @returns {object} */ getDataObject(row) { return row === null || row === undefined ? null : this.cache.rows[row]; } /** * Read the row tree in search for a specific row index or row object. * * @private * @param {object} parent The initial parent object. * @param {number} readCount Number of read nodes. * @param {number} neededIndex The row index we search for. * @param {object} neededObject The row object we search for. * @returns {number|object} */ readTreeNodes(parent, readCount, neededIndex, neededObject) { let rootLevel = false; let readNodesCount = readCount; if (isNaN(readNodesCount) && readNodesCount.end) { return readNodesCount; } let parentObj = parent; if (!parentObj) { parentObj = { __children: this.data }; rootLevel = true; readNodesCount -= 1; } if (neededIndex !== null && neededIndex !== undefined && readNodesCount === neededIndex) { return { result: parentObj, end: true }; } if (neededObject !== null && neededObject !== undefined && parentObj === neededObject) { return { result: readNodesCount, end: true }; } readNodesCount += 1; if (parentObj.__children) { arrayEach(parentObj.__children, val => { this.parentReference.set(val, rootLevel ? null : parentObj); readNodesCount = this.readTreeNodes(val, readNodesCount, neededIndex, neededObject); if (isNaN(readNodesCount) && readNodesCount.end) { return false; } }); } return readNodesCount; } /** * Mock a parent node. * * @private * @returns {*} */ mockParent() { const fakeParent = this.mockNode(); fakeParent.__children = this.data; return fakeParent; } /** * Mock a data node. * * @private * @returns {{}} */ mockNode() { const fakeNode = {}; objectEach(this.data[0], (val, key) => { fakeNode[key] = null; }); return fakeNode; } /** * Get the row index for the provided row object. * * @param {object} rowObj The row object. * @returns {number} Row index. */ getRowIndex(rowObj) { return rowObj === null || rowObj === undefined ? null : this.cache.nodeInfo.get(rowObj).row; } /** * Get the index of the provided row index/row object within its parent. * * @param {number|object} row Row index / row object. * @returns {number} */ getRowIndexWithinParent(row) { let rowObj = null; if (isNaN(row)) { rowObj = row; } else { rowObj = this.getDataObject(row); } const parent = this.getRowParent(row); if (parent === null || parent === undefined) { return this.data.indexOf(rowObj); } return parent.__children.indexOf(rowObj); } /** * Count all rows (including all parents and children). * * @returns {number} */ countAllRows() { const rootNodeMock = { __children: this.data }; return this.countChildren(rootNodeMock); } /** * Count children of the provided parent. * * @param {object|number} parent Parent node. * @returns {number} Children count. */ countChildren(parent) { let rowCount = 0; let parentNode = parent; if (!isNaN(parentNode)) { parentNode = this.getDataObject(parentNode); } if (!parentNode || !parentNode.__children) { return 0; } arrayEach(parentNode.__children, elem => { rowCount += 1; if (elem.__children) { rowCount += this.countChildren(elem); } }); return rowCount; } /** * Get the parent of the row at the provided index. * * @param {number|object} row Physical row index. * @returns {object} */ getRowParent(row) { let rowObject; if (isNaN(row)) { rowObject = row; } else { rowObject = this.getDataObject(row); } return this.getRowObjectParent(rowObject); } /** * Get the parent of the provided row object. * * @private * @param {object} rowObject The row object (tree node). * @returns {object|null} */ getRowObjectParent(rowObject) { if (!rowObject || typeof rowObject !== 'object') { return null; } return this.cache.nodeInfo.get(rowObject).parent; } /** * Get the nesting level for the row with the provided row index. * * @param {number} row Row index. * @returns {number|null} Row level or null, when row doesn't exist. */ getRowLevel(row) { let rowObject = null; if (isNaN(row)) { rowObject = row; } else { rowObject = this.getDataObject(row); } return rowObject ? this.getRowObjectLevel(rowObject) : null; } /** * Get the nesting level for the row with the provided row index. * * @private * @param {object} rowObject Row object. * @returns {number} Row level. */ getRowObjectLevel(rowObject) { return rowObject === null || rowObject === undefined ? null : this.cache.nodeInfo.get(rowObject).level; } /** * Check if the provided row/row element has children. * * @param {number|object} row Row number or row element. * @returns {boolean} */ hasChildren(row) { let rowObj = row; if (!isNaN(rowObj)) { rowObj = this.getDataObject(rowObj); } return !!(rowObj.__children && rowObj.__children.length); } /** * Returns `true` if the row at the provided index has a parent. * * @param {number} index Row index. * @returns {boolean} `true` if the row at the provided index has a parent, `false` otherwise. */ isChild(index) { return this.getRowParent(index) !== null; } /** * Get child at a provided index from the parent element. * * @param {object} parent The parent row object. * @param {number} index Index of the child element to be retrieved. * @returns {object|null} The child element or `null` if the child doesn't exist. */ getChild(parent, index) { var _parent$__children; return ((_parent$__children = parent.__children) === null || _parent$__children === void 0 ? void 0 : _parent$__children[index]) || null; } /** * Return `true` of the row at the provided index is located at the topmost level. * * @param {number} index Row index. * @returns {boolean} `true` of the row at the provided index is located at the topmost level, `false` otherwise. */ isRowHighestLevel(index) { return !this.isChild(index); } /** * Return `true` if the provided row index / row object represents a parent in the nested structure. * * @param {number|object} row Row index / row object. * @returns {boolean} `true` if the row is a parent, `false` otherwise. */ isParent(row) { var _rowObj$__children; let rowObj = row; if (!isNaN(rowObj)) { rowObj = this.getDataObject(rowObj); } return rowObj && !!rowObj.__children && ((_rowObj$__children = rowObj.__children) === null || _rowObj$__children === void 0 ? void 0 : _rowObj$__children.length) !== 0; } /** * Add a child to the provided parent. It's optional to add a row object as the "element". * * @param {object} parent The parent row object. * @param {object} [element] The element to add as a child. */ addChild(parent, element) { let childElement = element; this.hot.runHooks('beforeAddChild', parent, childElement); let parentIndex = null; if (parent) { parentIndex = this.getRowIndex(parent); } this.hot.runHooks('beforeCreateRow', parentIndex + this.countChildren(parent) + 1, 1); let functionalParent = parent; if (!parent) { functionalParent = this.mockParent(); } if (!functionalParent.__children) { functionalParent.__children = []; } if (!childElement) { childElement = this.mockNode(); } functionalParent.__children.push(childElement); this.rewriteCache(); const newRowIndex = this.getRowIndex(childElement); this.hot.rowIndexMapper.insertIndexes(newRowIndex, 1); this.hot.runHooks('afterCreateRow', newRowIndex, 1); this.hot.runHooks('afterAddChild', parent, childElement); } /** * Add a child node to the provided parent at a specified index. * * @param {object} parent Parent node. * @param {number} index Index to insert the child element at. * @param {object} [element] Element (node) to insert. */ addChildAtIndex(parent, index, element) { let childElement = element; let flattenedIndex; if (!childElement) { childElement = this.mockNode(); } this.hot.runHooks('beforeAddChild', parent, childElement, index); if (parent) { const parentIndex = this.getRowIndex(parent); const finalChildIndex = parentIndex + index + 1; this.hot.runHooks('beforeCreateRow', finalChildIndex, 1); parent.__children.splice(index, null, childElement); this.rewriteCache(); this.plugin.disableCoreAPIModifiers(); this.hot.setSourceDataAtCell(this.getRowIndexWithinParent(parent), '__children', parent.__children, 'NestedRows.addChildAtIndex'); this.hot.rowIndexMapper.insertIndexes(finalChildIndex, 1); this.plugin.enableCoreAPIModifiers(); this.hot.runHooks('afterCreateRow', finalChildIndex, 1); flattenedIndex = finalChildIndex; } else { this.plugin.disableCoreAPIModifiers(); this.hot.alter('insert_row_above', index, 1, 'NestedRows.addChildAtIndex'); this.plugin.enableCoreAPIModifiers(); flattenedIndex = this.getRowIndex(this.data[index]); } // Workaround for refreshing cache losing the reference to the mocked row. childElement = this.getDataObject(flattenedIndex); this.hot.runHooks('afterAddChild', parent, childElement, index); } /** * Add a sibling element at the specified index. * * @param {number} index New element sibling's index. * @param {('above'|'below')} where Direction in which the sibling is to be created. */ addSibling(index) { let where = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'below'; const translatedIndex = this.translateTrimmedRow(index); const parent = this.getRowParent(translatedIndex); const indexWithinParent = this.getRowIndexWithinParent(translatedIndex); switch (where) { case 'below': this.addChildAtIndex(parent, indexWithinParent + 1, null); break; case 'above': this.addChildAtIndex(parent, indexWithinParent, null); break; default: break; } } /** * Detach the provided element from its parent and add it right after it. * * @param {object|Array} elements Row object or an array of selected coordinates. * @param {boolean} [forceRender=true] If true (default), it triggers render after finished. */ detachFromParent(elements) { let forceRender = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; let element = null; const rowObjects = []; if (Array.isArray(elements)) { rangeEach(elements[0], elements[2], i => { const translatedIndex = this.translateTrimmedRow(i); rowObjects.push(this.getDataObject(translatedIndex)); }); rangeEach(0, rowObjects.length - 2, i => { this.detachFromParent(rowObjects[i], false); }); element = rowObjects[rowObjects.length - 1]; } else { element = elements; } const childRowIndex = this.getRowIndex(element); const childCount = this.countChildren(element); const indexWithinParent = this.getRowIndexWithinParent(element); const parent = this.getRowParent(element); const grandparent = this.getRowParent(parent); const grandparentRowIndex = this.getRowIndex(grandparent); let movedElementRowIndex = null; this.hot.runHooks('beforeDetachChild', parent, element); if (indexWithinParent !== null && indexWithinParent !== undefined) { const removedRowIndexes = Array.from(new Array(childRowIndex + childCount + 1).keys()).splice(-1 * (childCount + 1)); this.hot.runHooks('beforeRemoveRow', childRowIndex, childCount + 1, removedRowIndexes, this.plugin.pluginName); parent.__children.splice(indexWithinParent, 1); this.rewriteCache(); this.hot.runHooks('afterRemoveRow', childRowIndex, childCount + 1, removedRowIndexes, this.plugin.pluginName); if (grandparent) { movedElementRowIndex = grandparentRowIndex + this.countChildren(grandparent); const lastGrandparentChild = this.getChild(grandparent, this.countChildren(grandparent) - 1); const lastGrandparentChildIndex = this.getRowIndex(lastGrandparentChild); this.hot.runHooks('beforeCreateRow', lastGrandparentChildIndex + 1, childCount + 1, this.plugin.pluginName); grandparent.__children.push(element); } else { movedElementRowIndex = this.hot.countRows() + 1; this.hot.runHooks('beforeCreateRow', movedElementRowIndex - 2, childCount + 1, this.plugin.pluginName); this.data.push(element); } } this.rewriteCache(); this.hot.runHooks('afterCreateRow', movedElementRowIndex - 2, childCount + 1, this.plugin.pluginName); this.hot.runHooks('afterDetachChild', parent, element, this.getRowIndex(element)); if (forceRender) { this.hot.render(); } } /** * Filter the data by the `logicRows` array. * * @private * @param {number} index Index of the first row to remove. * @param {number} amount Number of elements to remove. * @param {Array} logicRows Array of indexes to remove. */ filterData(index, amount, logicRows) { // TODO: why are the first 2 arguments not used? const elementsToRemove = []; arrayEach(logicRows, elem => { elementsToRemove.push(this.getDataObject(elem)); }); arrayEach(elementsToRemove, elem => { const indexWithinParent = this.getRowIndexWithinParent(elem); const tempParent = this.getRowParent(elem); if (tempParent === null) { this.data.splice(indexWithinParent, 1); } else { tempParent.__children.splice(indexWithinParent, 1); } }); this.rewriteCache(); } /** * Used to splice the source data. Needed to properly modify the nested structure, which wouldn't work with the * default script. * * @private * @param {number} index Physical index of the element at the splice beginning. * @param {number} amount Number of elements to be removed. * @param {object[]} elements Array of row objects to add. */ spliceData(index, amount, elements) { const previousElement = this.getDataObject(index - 1); let newRowParent = null; let indexWithinParent = index; if (previousElement && previousElement.__children && previousElement.__children.length === 0) { newRowParent = previousElement; indexWithinParent = 0; } else if (index < this.countAllRows()) { newRowParent = this.getRowParent(index); indexWithinParent = this.getRowIndexWithinParent(index); } if (newRowParent) { if (elements) { newRowParent.__children.splice(indexWithinParent, amount, ...elements); } else { newRowParent.__children.splice(indexWithinParent, amount); } } else if (elements) { this.data.splice(indexWithinParent, amount, ...elements); } else { this.data.splice(indexWithinParent, amount); } this.rewriteCache(); } /** * Update the `__children` key of the upmost parent of the provided row object. * * @private * @param {object} rowElement Row object. */ syncRowWithRawSource(rowElement) { let upmostParent = rowElement; let tempParent = upmostParent; do { tempParent = this.getRowParent(tempParent); if (tempParent !== null) { upmostParent = tempParent; } } while (tempParent !== null); this.plugin.disableCoreAPIModifiers(); this.hot.setSourceDataAtCell(this.getRowIndexWithinParent(upmostParent), '__children', upmostParent.__children, 'NestedRows.syncRowWithRawSource'); this.plugin.enableCoreAPIModifiers(); } /* eslint-disable jsdoc/require-param */ /** * Move a single row. * * @param {number} fromIndex Index of the row to be moved. * @param {number} toIndex Index of the destination. * @param {boolean} moveToCollapsed `true` if moving a row to a collapsed parent. * @param {boolean} moveToLastChild `true` if moving a row to be a last child of the new parent. */ /* eslint-enable jsdoc/require-param */ moveRow(fromIndex, toIndex, moveToCollapsed, moveToLastChild) { const moveToLastRow = toIndex === this.hot.countRows(); const fromParent = this.getRowParent(fromIndex); const indexInFromParent = this.getRowIndexWithinParent(fromIndex); const elemToMove = fromParent.__children.slice(indexInFromParent, indexInFromParent + 1); const movingUp = fromIndex > toIndex; let toParent = moveToLastRow ? this.getRowParent(toIndex - 1) : this.getRowParent(toIndex); if (toParent === null || toParent === undefined) { toParent = this.getRowParent(toIndex - 1); } if (toParent === null || toParent === undefined) { toParent = this.getDataObject(toIndex - 1); } if (!toParent) { toParent = this.getDataObject(toIndex); toParent.__children = []; } else if (!toParent.__children) { toParent.__children = []; } const indexInTargetParent = moveToLastRow || moveToCollapsed || moveToLastChild ? toParent.__children.length : this.getRowIndexWithinParent(toIndex); const sameParent = fromParent === toParent; toParent.__children.splice(indexInTargetParent, 0, elemToMove[0]); fromParent.__children.splice(indexInFromParent + (movingUp && sameParent ? 1 : 0), 1); // Sync the changes in the cached data with the actual data stored in HOT. this.syncRowWithRawSource(fromParent); if (!sameParent) { this.syncRowWithRawSource(toParent); } } /** * Translate the visual row index to the physical index, taking into consideration the state of collapsed rows. * * @private * @param {number} row Row index. * @returns {number} */ translateTrimmedRow(row) { if (this.plugin.collapsingUI) { return this.plugin.collapsingUI.translateTrimmedRow(row); } return row; } /** * Translate the physical row index to the visual index, taking into consideration the state of collapsed rows. * * @private * @param {number} row Row index. * @returns {number} */ untranslateTrimmedRow(row) { if (this.plugin.collapsingUI) { return this.plugin.collapsingUI.untranslateTrimmedRow(row); } return row; } } export default DataManager;