handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
553 lines (533 loc) • 21.2 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/es.set.difference.v2.js";
import "core-js/modules/es.set.intersection.v2.js";
import "core-js/modules/es.set.is-disjoint-from.v2.js";
import "core-js/modules/es.set.is-subset-of.v2.js";
import "core-js/modules/es.set.is-superset-of.v2.js";
import "core-js/modules/es.set.symmetric-difference.v2.js";
import "core-js/modules/es.set.union.v2.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.reduce.js";
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
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); }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
import { BasePlugin } from "../base/index.mjs";
import DataManager from "./data/dataManager.mjs";
import CollapsingUI from "./ui/collapsing.mjs";
import HeadersUI from "./ui/headers.mjs";
import ContextMenuUI from "./ui/contextMenu.mjs";
import { error } from "../../helpers/console.mjs";
import { isArrayOfObjects } from "../../helpers/data.mjs";
import { TrimmingMap } from "../../translations/index.mjs";
import { EDITOR_EDIT_GROUP as SHORTCUTS_GROUP_EDITOR } from "../../shortcutContexts/index.mjs";
import RowMoveController from "./utils/rowMoveController.mjs";
export const PLUGIN_KEY = 'nestedRows';
export const PLUGIN_PRIORITY = 300;
const SHORTCUTS_GROUP = PLUGIN_KEY;
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* Error message for the wrong data type error.
*/
const WRONG_DATA_TYPE_ERROR = 'The Nested Rows plugin requires an Array of Objects as a dataset to be' + ' provided. The plugin has been disabled.';
/**
* @plugin NestedRows
* @class NestedRows
*
* @description
* Plugin responsible for displaying and operating on data sources with nested structures.
*/
var _skipRender = /*#__PURE__*/new WeakMap();
var _skipCoreAPIModifiers = /*#__PURE__*/new WeakMap();
var _NestedRows_brand = /*#__PURE__*/new WeakSet();
export class NestedRows extends BasePlugin {
constructor() {
super(...arguments);
/**
* `beforeRowMove` hook callback.
*
* @param {Array} rows Array of visual row indexes to be moved.
* @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements
* will be placed after the moving action. To check the visualization of the final index, please take a look at
* [documentation](@/guides/rows/row-summary/row-summary.md).
* @param {undefined|number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we
* are going to drop the moved elements. To check visualization of drop index please take a look at
* [documentation](@/guides/rows/row-summary/row-summary.md).
* @param {boolean} movePossible Indicates if it's possible to move rows to the desired position.
* @fires Hooks#afterRowMove
* @returns {boolean}
*/
_classPrivateMethodInitSpec(this, _NestedRows_brand);
/**
* Reference to the DataManager instance.
*
* @private
* @type {object}
*/
_defineProperty(this, "dataManager", null);
/**
* Reference to the HeadersUI instance.
*
* @private
* @type {object}
*/
_defineProperty(this, "headersUI", null);
/**
* Map of skipped rows by plugin.
*
* @private
* @type {null|TrimmingMap}
*/
_defineProperty(this, "collapsedRowsMap", null);
/**
* Allows skipping the render cycle if set as `true`.
*
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _skipRender, false);
/**
* Allows skipping the internal Core methods call if set as `true`.
*
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _skipCoreAPIModifiers, false);
}
static get PLUGIN_KEY() {
return PLUGIN_KEY;
}
static get PLUGIN_PRIORITY() {
return PLUGIN_PRIORITY;
}
/**
* Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
* hook and if it returns `true` then the {@link NestedRows#enablePlugin} method is called.
*
* @returns {boolean}
*/
isEnabled() {
return !!this.hot.getSettings()[PLUGIN_KEY];
}
/**
* Enables the plugin functionality for this Handsontable instance.
*/
enablePlugin() {
var _this = this;
if (this.enabled) {
return;
}
this.collapsedRowsMap = this.hot.rowIndexMapper.registerMap('nestedRows', new TrimmingMap());
this.dataManager = new DataManager(this, this.hot);
this.collapsingUI = new CollapsingUI(this, this.hot);
this.headersUI = new HeadersUI(this, this.hot);
this.contextMenuUI = new ContextMenuUI(this, this.hot);
this.rowMoveController = new RowMoveController(this);
this.addHook('afterInit', function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterInit).call(_this, ...args);
});
this.addHook('beforeViewRender', function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeViewRender).call(_this, ...args);
});
this.addHook('modifyRowData', function () {
return _this.onModifyRowData(...arguments);
});
this.addHook('modifySourceLength', function () {
return _this.onModifySourceLength(...arguments);
});
this.addHook('beforeDataSplice', function () {
return _this.onBeforeDataSplice(...arguments);
});
this.addHook('filterData', function () {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return _assertClassBrand(_NestedRows_brand, _this, _onFilterData).call(_this, ...args);
});
this.addHook('afterContextMenuDefaultOptions', function () {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterContextMenuDefaultOptions).call(_this, ...args);
});
this.addHook('afterGetRowHeader', function () {
for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
args[_key5] = arguments[_key5];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterGetRowHeader).call(_this, ...args);
});
this.addHook('beforeOnCellMouseDown', function () {
for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
args[_key6] = arguments[_key6];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeOnCellMouseDown).call(_this, ...args);
});
this.addHook('beforeRemoveRow', function () {
for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
args[_key7] = arguments[_key7];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeRemoveRow).call(_this, ...args);
});
this.addHook('afterRemoveRow', function () {
for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
args[_key8] = arguments[_key8];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterRemoveRow).call(_this, ...args);
});
this.addHook('beforeAddChild', function () {
for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
args[_key9] = arguments[_key9];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeAddChild).call(_this, ...args);
});
this.addHook('afterAddChild', function () {
for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) {
args[_key0] = arguments[_key0];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterAddChild).call(_this, ...args);
});
this.addHook('beforeDetachChild', function () {
for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) {
args[_key1] = arguments[_key1];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeDetachChild).call(_this, ...args);
});
this.addHook('afterDetachChild', function () {
for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
args[_key10] = arguments[_key10];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterDetachChild).call(_this, ...args);
});
this.addHook('modifyRowHeaderWidth', function () {
for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
args[_key11] = arguments[_key11];
}
return _assertClassBrand(_NestedRows_brand, _this, _onModifyRowHeaderWidth).call(_this, ...args);
});
this.addHook('afterCreateRow', function () {
for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
args[_key12] = arguments[_key12];
}
return _assertClassBrand(_NestedRows_brand, _this, _onAfterCreateRow).call(_this, ...args);
});
this.addHook('beforeRowMove', function () {
for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
args[_key13] = arguments[_key13];
}
return _assertClassBrand(_NestedRows_brand, _this, _onBeforeRowMove).call(_this, ...args);
});
this.addHook('beforeLoadData', data => _assertClassBrand(_NestedRows_brand, this, _onBeforeLoadData).call(this, data));
this.addHook('beforeUpdateData', data => _assertClassBrand(_NestedRows_brand, this, _onBeforeLoadData).call(this, data));
this.registerShortcuts();
super.enablePlugin();
}
/**
* Disables the plugin functionality for this Handsontable instance.
*/
disablePlugin() {
this.hot.rowIndexMapper.unregisterMap('nestedRows');
this.unregisterShortcuts();
super.disablePlugin();
}
/**
* Updates the plugin's state.
*
* This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options:
* - [`nestedRows`](@/api/options.md#nestedrows)
*/
updatePlugin() {
this.disablePlugin();
// We store a state of the data manager.
const currentSourceData = this.dataManager.getData();
this.enablePlugin();
// After enabling plugin previously stored data is restored.
this.dataManager.updateWithData(currentSourceData);
super.updatePlugin();
}
/**
* Register shortcuts responsible for toggling collapsible columns.
*
* @private
*/
registerShortcuts() {
this.hot.getShortcutManager().getContext('grid').addShortcut({
keys: [['Enter']],
callback: () => {
const {
highlight
} = this.hot.getSelectedRangeActive();
const row = this.collapsingUI.translateTrimmedRow(highlight.row);
if (this.collapsingUI.areChildrenCollapsed(row)) {
this.collapsingUI.expandChildren(row);
} else {
this.collapsingUI.collapseChildren(row);
}
// prevent default Enter behavior (move to the next row within a selection range)
return false;
},
runOnlyIf: () => {
var _this$hot$getSelected, _this$hot$getSelected2;
const highlight = (_this$hot$getSelected = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected === void 0 ? void 0 : _this$hot$getSelected.highlight;
return highlight && ((_this$hot$getSelected2 = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected2 === void 0 ? void 0 : _this$hot$getSelected2.isSingle()) && this.hot.selection.isCellVisible(highlight) && highlight.col === -1 && highlight.row >= 0;
},
group: SHORTCUTS_GROUP,
relativeToGroup: SHORTCUTS_GROUP_EDITOR,
position: 'before'
});
}
/**
* Unregister shortcuts responsible for toggling collapsible columns.
*
* @private
*/
unregisterShortcuts() {
this.hot.getShortcutManager().getContext('grid').removeShortcutsByGroup(SHORTCUTS_GROUP);
}
/**
* Enable the modify hook skipping flag - allows retrieving the data from Handsontable without this plugin's
* modifications.
*
* @private
*/
disableCoreAPIModifiers() {
_classPrivateFieldSet(_skipCoreAPIModifiers, this, true);
}
/**
* Disable the modify hook skipping flag.
*
* @private
*/
enableCoreAPIModifiers() {
_classPrivateFieldSet(_skipCoreAPIModifiers, this, false);
}
/**
* `beforeOnCellMousedown` hook callback.
*
* @param {MouseEvent} event Mousedown event.
* @param {object} coords Cell coords.
* @param {HTMLElement} TD Clicked cell.
*/
/**
* The modifyRowData hook callback.
*
* @private
* @param {number} row Visual row index.
* @returns {boolean}
*/
onModifyRowData(row) {
if (_classPrivateFieldGet(_skipCoreAPIModifiers, this)) {
return;
}
return this.dataManager.getDataObject(row);
}
/**
* Modify the source data length to match the length of the nested structure.
*
* @private
* @returns {number}
*/
onModifySourceLength() {
if (_classPrivateFieldGet(_skipCoreAPIModifiers, this)) {
return;
}
return this.dataManager.countAllRows();
}
/**
* @private
* @param {number} index The index where the data was spliced.
* @param {number} amount An amount of items to remove.
* @param {object} element An element to add.
* @returns {boolean}
*/
onBeforeDataSplice(index, amount, element) {
if (_classPrivateFieldGet(_skipCoreAPIModifiers, this) || this.dataManager.isRowHighestLevel(index)) {
return true;
}
this.dataManager.spliceData(index, amount, element);
return false;
}
/**
* Provide custom source data filtering. It's handled by core method and replaces the native filtering.
*
* @param {number} index The index where the data filtering starts.
* @param {number} amount An amount of rows which filtering applies to.
* @param {number} physicalRows Physical row indexes.
* @returns {Array}
*/
/**
* Destroys the plugin instance.
*/
destroy() {
super.destroy();
}
}
function _onBeforeRowMove(rows, finalIndex, dropIndex, movePossible) {
return this.rowMoveController.onBeforeRowMove(rows, finalIndex, dropIndex, movePossible);
}
function _onBeforeOnCellMouseDown(event, coords, TD) {
this.collapsingUI.toggleState(event, coords, TD);
}
function _onFilterData(index, amount, physicalRows) {
this.collapsingUI.collapsedRowsStash.stash();
this.collapsingUI.collapsedRowsStash.trimStash(physicalRows[0], amount);
this.collapsingUI.collapsedRowsStash.shiftStash(physicalRows[0], null, -1 * amount);
this.dataManager.filterData(index, amount, physicalRows);
_classPrivateFieldSet(_skipRender, this, true);
return this.dataManager.getData().slice(); // Data contains reference sometimes.
}
/**
* `afterContextMenuDefaultOptions` hook callback.
*
* @param {object} defaultOptions The default context menu items order.
* @returns {boolean}
*/
function _onAfterContextMenuDefaultOptions(defaultOptions) {
return this.contextMenuUI.appendOptions(defaultOptions);
}
/**
* `afterGetRowHeader` hook callback.
*
* @param {number} row Row index.
* @param {HTMLElement} TH Row header element.
*/
function _onAfterGetRowHeader(row, TH) {
this.headersUI.appendLevelIndicators(row, TH);
}
/**
* `modifyRowHeaderWidth` hook callback.
*
* @param {number} rowHeaderWidth The initial row header width(s).
* @returns {number}
*/
function _onModifyRowHeaderWidth(rowHeaderWidth) {
return Math.max(this.headersUI.rowHeaderWidthCache, rowHeaderWidth);
}
/**
* `onAfterRemoveRow` hook callback.
*
* @param {number} index Removed row.
* @param {number} amount Amount of removed rows.
* @param {Array} logicRows An array of the removed physical rows.
* @param {string} source Source of action.
*/
function _onAfterRemoveRow(index, amount, logicRows, source) {
if (source === this.pluginName) {
return;
}
this.hot._registerTimeout(() => {
_classPrivateFieldSet(_skipRender, this, false);
this.headersUI.updateRowHeaderWidth();
this.collapsingUI.collapsedRowsStash.applyStash();
});
}
/**
* Callback for the `beforeRemoveRow` change list of removed physical indexes by reference. Removing parent node
* has effect in removing children nodes.
*
* @param {number} index Visual index of starter row.
* @param {number} amount Amount of rows to be removed.
* @param {Array} physicalRows List of physical indexes.
*/
function _onBeforeRemoveRow(index, amount, physicalRows) {
const modifiedPhysicalRows = Array.from(physicalRows.reduce((removedRows, physicalIndex) => {
if (this.dataManager.isParent(physicalIndex)) {
const children = this.dataManager.getDataObject(physicalIndex).__children;
// Preserve a parent in the list of removed rows.
removedRows.add(physicalIndex);
if (Array.isArray(children)) {
// Add a children to the list of removed rows.
children.forEach(child => removedRows.add(this.dataManager.getRowIndex(child)));
}
return removedRows;
}
// Don't modify list of removed rows when already checked element isn't a parent.
return removedRows.add(physicalIndex);
}, new Set()));
// Modifying hook's argument by the reference.
physicalRows.length = 0;
physicalRows.push(...modifiedPhysicalRows);
}
/**
* `beforeAddChild` hook callback.
*/
function _onBeforeAddChild() {
this.collapsingUI.collapsedRowsStash.stash();
}
/**
* `afterAddChild` hook callback.
*
* @param {object} parent Parent element.
* @param {object} element New child element.
*/
function _onAfterAddChild(parent, element) {
this.collapsingUI.collapsedRowsStash.shiftStash(this.dataManager.getRowIndex(element));
this.collapsingUI.collapsedRowsStash.applyStash();
this.headersUI.updateRowHeaderWidth();
}
/**
* `beforeDetachChild` hook callback.
*/
function _onBeforeDetachChild() {
this.collapsingUI.collapsedRowsStash.stash();
}
/**
* `afterDetachChild` hook callback.
*
* @param {object} parent Parent element.
* @param {object} element New child element.
* @param {number} finalElementRowIndex The final row index of the detached element.
*/
function _onAfterDetachChild(parent, element, finalElementRowIndex) {
this.collapsingUI.collapsedRowsStash.shiftStash(finalElementRowIndex, null, -1);
this.collapsingUI.collapsedRowsStash.applyStash();
this.headersUI.updateRowHeaderWidth();
}
/**
* `afterCreateRow` hook callback.
*/
function _onAfterCreateRow() {
this.dataManager.rewriteCache();
}
/**
* `afterInit` hook callback.
*/
function _onAfterInit() {
this.headersUI.updateRowHeaderWidth();
}
/**
* `beforeViewRender` hook callback.
*
* @param {boolean} force Indicates if the render call was triggered by a change of settings or data.
* @param {object} skipRender An object, holder for skipRender functionality.
*/
function _onBeforeViewRender(force, skipRender) {
if (_classPrivateFieldGet(_skipRender, this)) {
skipRender.skipRender = true;
}
}
/**
* `beforeLoadData` hook callback.
*
* @param {Array} data The source data.
*/
function _onBeforeLoadData(data) {
if (!isArrayOfObjects(data)) {
error(WRONG_DATA_TYPE_ERROR);
this.hot.getSettings()[PLUGIN_KEY] = false;
this.disablePlugin();
return;
}
this.dataManager.setData(data);
this.dataManager.rewriteCache();
}