handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
1,133 lines (1,090 loc) • 199 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/es.object.from-entries.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.filter.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.map.js";
import "core-js/modules/web.immediate.js";
import { addClass, empty, observeVisibilityChangeOnce, removeClass } from "./helpers/dom/element.mjs";
import { isFunction } from "./helpers/function.mjs";
import { isDefined, isUndefined, isRegExp, _injectProductInfo, isEmpty } from "./helpers/mixed.mjs";
import { isMobileBrowser, isIpadOS } from "./helpers/browser.mjs";
import EditorManager from "./editorManager.mjs";
import EventManager from "./eventManager.mjs";
import { deepClone, duckSchema, isObjectEqual, isObject, deepObjectSize, hasOwnProperty, createObjectPropListener, objectEach } from "./helpers/object.mjs";
import { FocusManager } from "./focusManager.mjs";
import { arrayMap, arrayEach, arrayReduce, getDifferenceOfArrays, stringToArray, pivot } from "./helpers/array.mjs";
import { instanceToHTML } from "./utils/parseTable.mjs";
import { staticRegister } from "./utils/staticRegister.mjs";
import { getPlugin, getPluginsNames } from "./plugins/registry.mjs";
import { getRenderer } from "./renderers/registry.mjs";
import { getEditor } from "./editors/registry.mjs";
import { getValidator } from "./validators/registry.mjs";
import { randomString, toUpperCaseFirst } from "./helpers/string.mjs";
import { rangeEach, rangeEachReverse } from "./helpers/number.mjs";
import TableView from "./tableView.mjs";
import DataSource from "./dataMap/dataSource.mjs";
import { spreadsheetColumnLabel } from "./helpers/data.mjs";
import { IndexMapper } from "./translations/index.mjs";
import { registerAsRootInstance, hasValidParameter, isRootInstance } from "./utils/rootInstance.mjs";
import { DEFAULT_COLUMN_WIDTH } from "./3rdparty/walkontable/src/index.mjs";
import { Hooks } from "./core/hooks/index.mjs";
import { hasLanguageDictionary, getValidLanguageCode, getTranslatedPhrase } from "./i18n/registry.mjs";
import { warnUserAboutLanguageRegistration, normalizeLanguageCode } from "./i18n/utils.mjs";
import { Selection } from "./selection/index.mjs";
import { MetaManager, DynamicCellMetaMod, ExtendMetaPropertiesMod, replaceData } from "./dataMap/index.mjs";
import { installFocusCatcher, createViewportScroller } from "./core/index.mjs";
import { createUniqueMap } from "./utils/dataStructures/uniqueMap.mjs";
import { createShortcutManager } from "./shortcuts/index.mjs";
import { registerAllShortcutContexts } from "./shortcutContexts/index.mjs";
import { getThemeClassName } from "./helpers/themes.mjs";
import { StylesHandler } from "./utils/stylesHandler.mjs";
import { deprecatedWarn, warn } from "./helpers/console.mjs";
import { CellRangeToRenderableMapper } from "./core/coordsMapper/rangeToRenderableMapper.mjs";
import { install as installAccessibilityAnnouncer, uninstall as uninstallAccessibilityAnnouncer } from "./utils/a11yAnnouncer.mjs";
import { getValueSetterValue } from "./utils/valueAccessors.mjs";
let activeGuid = null;
/**
* A set of deprecated warn instances.
*
* @type {Set<string>}
*/
const deprecatedWarnInstances = new WeakSet();
/**
* Keeps the collection of the all Handsontable instances created on the same page. The
* list is then used to trigger the "afterUnlisten" hook when the "listen()" method was
* called on another instance.
*
* @type {Map<string, Core>}
*/
const foreignHotInstances = new Map();
/**
* A set of deprecated feature names.
*
* @type {Set<string>}
*/
// eslint-disable-next-line no-unused-vars
const deprecationWarns = new Set();
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* Handsontable constructor.
*
* @core
* @class Core
* @description
*
* The `Handsontable` class (known as the `Core`) lets you modify the grid's behavior by using Handsontable's public API methods.
*
* ::: only-for react
* To use these methods, associate a Handsontable instance with your instance
* of the [`HotTable` component](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component),
* by using React's `ref` feature (read more on the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page).
* :::
*
* ::: only-for angular
* To use these methods, associate a Handsontable instance with your instance
* of the [`HotTable` component](@/guides/getting-started/installation/installation.md#5-use-the-hottable-component),
* by using `@ViewChild` decorator (read more on the [Instance access](@/guides/getting-started/angular-hot-instance/angular-hot-instance.md) page).
* :::
*
* ## How to call a method
*
* ::: only-for javascript
* ```js
* // create a Handsontable instance
* const hot = new Handsontable(document.getElementById('example'), options);
*
* // call a method
* hot.setDataAtCell(0, 0, 'new value');
* ```
* :::
*
* ::: only-for react
* ```jsx
* import { useRef } from 'react';
*
* const hotTableComponent = useRef(null);
*
* <HotTable
* // associate your `HotTable` component with a Handsontable instance
* ref={hotTableComponent}
* settings={options}
* />
*
* // access the Handsontable instance, under the `.current.hotInstance` property
* // call a method
* hotTableComponent.current.hotInstance.setDataAtCell(0, 0, 'new value');
* ```
* :::
*
* ::: only-for angular
* ```ts
* import { Component, ViewChild, AfterViewInit } from "@angular/core";
* import {
* GridSettings,
* HotTableComponent,
* HotTableModule,
* } from "@handsontable/angular-wrapper";
*
* `@Component`({
* standalone: true,
* imports: [HotTableModule],
* template: ` <div>
* <hot-table themeName="ht-theme-main" [settings]="gridSettings" />
* </div>`,
* })
* export class ExampleComponent implements AfterViewInit {
* `@ViewChild`(HotTableComponent, { static: false })
* readonly hotTable!: HotTableComponent;
*
* readonly gridSettings = <GridSettings>{
* columns: [{}],
* };
*
* ngAfterViewInit(): void {
* // Access the Handsontable instance
* // Call a method
* this.hotTable?.hotInstance?.setDataAtCell(0, 0, "new value");
* }
* }
* ```
* :::
*
* @param {HTMLElement} rootContainer The element to which the Handsontable instance is injected.
* @param {object} userSettings The user defined options.
* @param {boolean} [rootInstanceSymbol=false] Indicates if the instance is root of all later instances created.
*/
export default function Core(rootContainer, userSettings) {
var _mergedUserSettings$l,
_this$rootWrapperElem,
_this = this;
let rootInstanceSymbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
let instance = this;
const eventManager = new EventManager(instance);
let datamap;
let dataSource;
let grid;
let editorManager;
let focusManager;
let viewportScroller;
let firstRun = true;
const mergedUserSettings = {
...userSettings.initialState,
...userSettings
};
if (hasValidParameter(rootInstanceSymbol)) {
registerAsRootInstance(this);
}
/**
* Reference to the root container.
*
* @private
* @type {HTMLElement}
*/
this.rootContainer = rootContainer;
/**
* Reference to the wrapper element.
*
* @private
* @type {HTMLElement}
*/
this.rootWrapperElement = undefined;
/**
* Reference to the grid element.
*
* @private
* @type {HTMLElement}
*/
this.rootGridElement = undefined;
/**
* Reference to the portal element.
*
* @private
* @type {HTMLElement}
*/
this.rootPortalElement = undefined;
// TODO: check if references to DOM elements should be move to UI layer (Walkontable)
/**
* Reference to the container element.
*
* @private
* @type {HTMLElement}
*/
this.rootElement = isRootInstance(this) ? rootContainer.ownerDocument.createElement('div') : rootContainer;
/**
* The nearest document over container.
*
* @private
* @type {Document}
*/
this.rootDocument = rootContainer.ownerDocument;
/**
* Window object over container's document.
*
* @private
* @type {Window}
*/
this.rootWindow = this.rootDocument.defaultView;
if (isRootInstance(this)) {
this.rootWrapperElement = this.rootDocument.createElement('div');
this.rootGridElement = this.rootDocument.createElement('div');
this.rootPortalElement = this.rootDocument.createElement('div');
addClass(this.rootElement, ['ht-wrapper', 'handsontable']);
addClass(this.rootWrapperElement, 'ht-root-wrapper');
addClass(this.rootGridElement, 'ht-grid');
this.rootGridElement.appendChild(this.rootElement);
this.rootWrapperElement.appendChild(this.rootGridElement);
this.rootContainer.appendChild(this.rootWrapperElement);
addClass(this.rootPortalElement, 'ht-portal');
this.rootDocument.body.appendChild(this.rootPortalElement);
}
/**
* A boolean to tell if the Handsontable has been fully destroyed. This is set to `true`
* after `afterDestroy` hook is called.
*
* @memberof Core#
* @member isDestroyed
* @type {boolean}
*/
this.isDestroyed = false;
/**
* The counter determines how many times the render suspending was called. It allows
* tracking the nested suspending calls. For each render suspend resuming call the
* counter is decremented. The value equal to 0 means the render suspending feature
* is disabled.
*
* @private
* @type {number}
*/
this.renderSuspendedCounter = 0;
/**
* The counter determines how many times the execution suspending was called. It allows
* tracking the nested suspending calls. For each execution suspend resuming call the
* counter is decremented. The value equal to 0 means the execution suspending feature
* is disabled.
*
* @private
* @type {number}
*/
this.executionSuspendedCounter = 0;
const layoutDirection = (_mergedUserSettings$l = mergedUserSettings === null || mergedUserSettings === void 0 ? void 0 : mergedUserSettings.layoutDirection) !== null && _mergedUserSettings$l !== void 0 ? _mergedUserSettings$l : 'inherit';
const rootElementDirection = ['rtl', 'ltr'].includes(layoutDirection) ? layoutDirection : this.rootWindow.getComputedStyle(this.rootElement).direction;
this.rootElement.setAttribute('dir', rootElementDirection);
(_this$rootWrapperElem = this.rootWrapperElement) === null || _this$rootWrapperElem === void 0 || _this$rootWrapperElem.setAttribute('dir', rootElementDirection);
/**
* Checks if the grid is rendered using the right-to-left layout direction.
*
* @since 12.0.0
* @memberof Core#
* @function isRtl
* @returns {boolean} True if RTL.
*/
this.isRtl = function () {
return rootElementDirection === 'rtl';
};
/**
* Checks if the grid is rendered using the left-to-right layout direction.
*
* @since 12.0.0
* @memberof Core#
* @function isLtr
* @returns {boolean} True if LTR.
*/
this.isLtr = function () {
return !instance.isRtl();
};
/**
* Returns 1 for LTR; -1 for RTL. Useful for calculations.
*
* @since 12.0.0
* @memberof Core#
* @function getDirectionFactor
* @returns {number} Returns 1 for LTR; -1 for RTL.
*/
this.getDirectionFactor = function () {
return instance.isLtr() ? 1 : -1;
};
/**
* Styles handler instance.
*
* @private
* @type {StylesHandler}
*/
this.stylesHandler = new StylesHandler({
rootElement: instance.rootElement,
rootDocument: instance.rootDocument,
onThemeChange: validThemeName => {
if (isRootInstance(this)) {
removeClass(this.rootWrapperElement, /ht-theme-.*/g);
removeClass(this.rootPortalElement, /ht-theme-.*/g);
if (validThemeName) {
addClass(this.rootWrapperElement, validThemeName);
addClass(this.rootPortalElement, validThemeName);
if (!getComputedStyle(this.rootWrapperElement).getPropertyValue('--ht-line-height')) {
warn(`The "${validThemeName}" theme is enabled, but its stylesheets are missing or not imported correctly. \
Import the correct CSS files in order to use that theme.`);
}
}
}
}
});
mergedUserSettings.language = getValidLanguageCode(mergedUserSettings.language);
const settingsWithoutHooks = Object.fromEntries(Object.entries(mergedUserSettings).filter(_ref => {
let [key] = _ref;
return !(Hooks.getSingleton().isRegistered(key) || Hooks.getSingleton().isDeprecated(key));
}));
const metaManager = new MetaManager(instance, settingsWithoutHooks, [DynamicCellMetaMod, ExtendMetaPropertiesMod]);
const tableMeta = metaManager.getTableMeta();
const globalMeta = metaManager.getGlobalMeta();
const pluginsRegistry = createUniqueMap();
this.container = this.rootDocument.createElement('div');
this.rootElement.insertBefore(this.container, this.rootElement.firstChild);
this.guid = `ht_${randomString()}`; // this is the namespace for global events
foreignHotInstances.set(this.guid, this);
/**
* Instance of index mapper which is responsible for managing the column indexes.
*
* @memberof Core#
* @member columnIndexMapper
* @type {IndexMapper}
*/
this.columnIndexMapper = new IndexMapper();
/**
* Instance of index mapper which is responsible for managing the row indexes.
*
* @memberof Core#
* @member rowIndexMapper
* @type {IndexMapper}
*/
this.rowIndexMapper = new IndexMapper();
this.columnIndexMapper.addLocalHook('indexesSequenceChange', source => {
instance.runHooks('afterColumnSequenceChange', source);
});
this.rowIndexMapper.addLocalHook('indexesSequenceChange', source => {
instance.runHooks('afterRowSequenceChange', source);
});
eventManager.addEventListener(this.rootDocument.documentElement, 'compositionstart', event => {
instance.runHooks('beforeCompositionStart', event);
});
dataSource = new DataSource(instance);
const moduleRegisterer = staticRegister(this.guid);
moduleRegisterer.register('cellRangeMapper', new CellRangeToRenderableMapper({
rowIndexMapper: this.rowIndexMapper,
columnIndexMapper: this.columnIndexMapper
}));
if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === 'ht_') {
this.rootElement.id = this.guid; // if root element does not have an id, assign a random id
}
const visualToRenderableCoords = coords => {
const {
row: visualRow,
col: visualColumn
} = coords;
return instance._createCellCoords(
// We just store indexes for rows and columns without headers.
visualRow >= 0 ? instance.rowIndexMapper.getRenderableFromVisualIndex(visualRow) : visualRow, visualColumn >= 0 ? instance.columnIndexMapper.getRenderableFromVisualIndex(visualColumn) : visualColumn);
};
const renderableToVisualCoords = coords => {
const {
row: renderableRow,
col: renderableColumn
} = coords;
return instance._createCellCoords(
// We just store indexes for rows and columns without headers.
renderableRow >= 0 ? instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow, renderableColumn >= 0 ? instance.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn) : renderableColumn // eslint-disable-line max-len
);
};
const findFirstNonHiddenRenderableRow = (visualRowFrom, visualRowTo) => {
const dir = visualRowTo > visualRowFrom ? 1 : -1;
const minIndex = Math.min(visualRowFrom, visualRowTo);
const maxIndex = Math.max(visualRowFrom, visualRowTo);
const rowIndex = instance.rowIndexMapper.getNearestNotHiddenIndex(visualRowFrom, dir);
if (rowIndex === null || dir === 1 && rowIndex > maxIndex || dir === -1 && rowIndex < minIndex) {
return null;
}
return rowIndex >= 0 ? instance.rowIndexMapper.getRenderableFromVisualIndex(rowIndex) : rowIndex;
};
const findFirstNonHiddenRenderableColumn = (visualColumnFrom, visualColumnTo) => {
const dir = visualColumnTo > visualColumnFrom ? 1 : -1;
const minIndex = Math.min(visualColumnFrom, visualColumnTo);
const maxIndex = Math.max(visualColumnFrom, visualColumnTo);
const columnIndex = instance.columnIndexMapper.getNearestNotHiddenIndex(visualColumnFrom, dir);
if (columnIndex === null || dir === 1 && columnIndex > maxIndex || dir === -1 && columnIndex < minIndex) {
return null;
}
return columnIndex >= 0 ? instance.columnIndexMapper.getRenderableFromVisualIndex(columnIndex) : columnIndex;
};
let selection = new Selection(tableMeta, {
rowIndexMapper: instance.rowIndexMapper,
columnIndexMapper: instance.columnIndexMapper,
countCols: () => instance.countCols(),
countRows: () => instance.countRows(),
propToCol: prop => datamap.propToCol(prop),
isEditorOpened: () => instance.getActiveEditor() ? instance.getActiveEditor().isOpened() : false,
countRenderableColumns: () => this.view.countRenderableColumns(),
countRenderableRows: () => this.view.countRenderableRows(),
countRowHeaders: () => this.countRowHeaders(),
countColHeaders: () => this.countColHeaders(),
countRenderableRowsInRange: function () {
return _this.view.countRenderableRowsInRange(...arguments);
},
countRenderableColumnsInRange: function () {
return _this.view.countRenderableColumnsInRange(...arguments);
},
getShortcutManager: () => instance.getShortcutManager(),
createCellCoords: (row, column) => instance._createCellCoords(row, column),
createCellRange: (highlight, from, to) => instance._createCellRange(highlight, from, to),
visualToRenderableCoords,
renderableToVisualCoords,
findFirstNonHiddenRenderableRow,
findFirstNonHiddenRenderableColumn,
isDisabledCellSelection: (visualRow, visualColumn) => {
if (visualRow < 0 || visualColumn < 0) {
return instance.getSettings().disableVisualSelection;
}
return instance.getCellMeta(visualRow, visualColumn).disableVisualSelection;
}
});
this.selection = selection;
const onIndexMapperCacheUpdate = _ref2 => {
let {
hiddenIndexesChanged
} = _ref2;
this.forceFullRender = true;
if (hiddenIndexesChanged) {
this.selection.commit();
}
};
this.columnIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
this.rowIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
this.selection.addLocalHook('afterSetRangeEnd', (cellCoords, isLastSelectionLayer) => {
const preventScrolling = createObjectPropListener(false);
const selectionRange = this.selection.getSelectedRange();
const {
from,
to
} = selectionRange.current();
const selectionLayerLevel = selectionRange.size() - 1;
this.runHooks('afterSelection', from.row, from.col, to.row, to.col, preventScrolling, selectionLayerLevel);
this.runHooks('afterSelectionByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), preventScrolling, selectionLayerLevel);
if (isLastSelectionLayer && (!preventScrolling.isTouched() || preventScrolling.isTouched() && !preventScrolling.value)) {
viewportScroller.scrollTo(cellCoords);
}
const isSelectedByRowHeader = selection.isSelectedByRowHeader();
const isSelectedByColumnHeader = selection.isSelectedByColumnHeader();
// @TODO: These CSS classes are no longer needed anymore. They are used only as a indicator of the selected
// rows/columns in the MergedCells plugin (via border.js#L520 in the walkontable module). After fixing
// the Border class this should be removed.
if (isSelectedByRowHeader && isSelectedByColumnHeader) {
addClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
} else if (isSelectedByRowHeader) {
removeClass(this.rootElement, 'ht__selection--columns');
addClass(this.rootElement, 'ht__selection--rows');
} else if (isSelectedByColumnHeader) {
removeClass(this.rootElement, 'ht__selection--rows');
addClass(this.rootElement, 'ht__selection--columns');
} else {
removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
}
if (!['shift', 'refresh'].includes(selection.getSelectionSource())) {
editorManager.closeEditor(null);
}
if (selection.getSelectionSource() !== 'refresh') {
instance.view.render();
editorManager.prepareEditor();
}
});
this.selection.addLocalHook('beforeSetFocus', cellCoords => {
this.runHooks('beforeSelectionFocusSet', cellCoords.row, cellCoords.col);
});
this.selection.addLocalHook('afterSetFocus', cellCoords => {
const preventScrolling = createObjectPropListener(false);
this.runHooks('afterSelectionFocusSet', cellCoords.row, cellCoords.col, preventScrolling);
if (!preventScrolling.isTouched() || preventScrolling.isTouched() && !preventScrolling.value) {
viewportScroller.scrollTo(cellCoords);
}
editorManager.closeEditor();
instance.view.render();
editorManager.prepareEditor();
});
this.selection.addLocalHook('afterSelectionFinished', cellRanges => {
const selectionLayerLevel = cellRanges.length - 1;
const {
from,
to
} = cellRanges[selectionLayerLevel];
this.runHooks('afterSelectionEnd', from.row, from.col, to.row, to.col, selectionLayerLevel);
this.runHooks('afterSelectionEndByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), selectionLayerLevel);
if (selection.getSelectionSource() === 'refresh') {
instance.view.render();
editorManager.prepareEditor();
}
});
this.selection.addLocalHook('afterIsMultipleSelection', isMultiple => {
const changedIsMultiple = this.runHooks('afterIsMultipleSelection', isMultiple.value);
if (isMultiple.value) {
isMultiple.value = changedIsMultiple;
}
});
this.selection.addLocalHook('afterDeselect', () => {
editorManager.closeEditor();
instance.view.render();
removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
this.runHooks('afterDeselect');
});
this.selection.addLocalHook('beforeHighlightSet', () => this.runHooks('beforeSelectionHighlightSet')).addLocalHook('beforeSetRangeStart', function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _this.runHooks('beforeSetRangeStart', ...args);
}).addLocalHook('beforeSetRangeStartOnly', function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _this.runHooks('beforeSetRangeStartOnly', ...args);
}).addLocalHook('beforeSetRangeEnd', function () {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return _this.runHooks('beforeSetRangeEnd', ...args);
}).addLocalHook('beforeSelectColumns', function () {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
return _this.runHooks('beforeSelectColumns', ...args);
}).addLocalHook('afterSelectColumns', function () {
for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
args[_key5] = arguments[_key5];
}
return _this.runHooks('afterSelectColumns', ...args);
}).addLocalHook('beforeSelectRows', function () {
for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
args[_key6] = arguments[_key6];
}
return _this.runHooks('beforeSelectRows', ...args);
}).addLocalHook('afterSelectRows', function () {
for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
args[_key7] = arguments[_key7];
}
return _this.runHooks('afterSelectRows', ...args);
}).addLocalHook('beforeSelectAll', function () {
for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
args[_key8] = arguments[_key8];
}
return _this.runHooks('beforeSelectAll', ...args);
}).addLocalHook('afterSelectAll', function () {
for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
args[_key9] = arguments[_key9];
}
return _this.runHooks('afterSelectAll', ...args);
}).addLocalHook('beforeModifyTransformStart', function () {
for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) {
args[_key0] = arguments[_key0];
}
return _this.runHooks('modifyTransformStart', ...args);
}).addLocalHook('afterModifyTransformStart', function () {
for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) {
args[_key1] = arguments[_key1];
}
return _this.runHooks('afterModifyTransformStart', ...args);
}).addLocalHook('beforeModifyTransformFocus', function () {
for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
args[_key10] = arguments[_key10];
}
return _this.runHooks('modifyTransformFocus', ...args);
}).addLocalHook('afterModifyTransformFocus', function () {
for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
args[_key11] = arguments[_key11];
}
return _this.runHooks('afterModifyTransformFocus', ...args);
}).addLocalHook('beforeModifyTransformEnd', function () {
for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
args[_key12] = arguments[_key12];
}
return _this.runHooks('modifyTransformEnd', ...args);
}).addLocalHook('afterModifyTransformEnd', function () {
for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
args[_key13] = arguments[_key13];
}
return _this.runHooks('afterModifyTransformEnd', ...args);
}).addLocalHook('beforeRowWrap', function () {
for (var _len14 = arguments.length, args = new Array(_len14), _key14 = 0; _key14 < _len14; _key14++) {
args[_key14] = arguments[_key14];
}
return _this.runHooks('beforeRowWrap', ...args);
}).addLocalHook('beforeColumnWrap', function () {
for (var _len15 = arguments.length, args = new Array(_len15), _key15 = 0; _key15 < _len15; _key15++) {
args[_key15] = arguments[_key15];
}
return _this.runHooks('beforeColumnWrap', ...args);
}).addLocalHook('insertRowRequire', totalRows => this.alter('insert_row_above', totalRows, 1, 'auto')).addLocalHook('insertColRequire', totalCols => this.alter('insert_col_start', totalCols, 1, 'auto'));
grid = {
/**
* Inserts or removes rows and columns.
*
* @private
* @param {string} action Possible values: "insert_row_above", "insert_row_below", "insert_col_start", "insert_col_end",
* "remove_row", "remove_col".
* @param {number|Array} index Row or column visual index which from the alter action will be triggered.
* Alter actions such as "remove_row" and "remove_col" support array indexes in the
* format `[[index, amount], [index, amount]...]` this can be used to remove
* non-consecutive columns or rows in one call.
* @param {number} [amount=1] Amount of rows or columns to remove.
* @param {string} [source] Optional. Source of hook runner.
* @param {boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows.
*/
alter(action, index) {
let amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
let source = arguments.length > 3 ? arguments[3] : undefined;
let keepEmptyRows = arguments.length > 4 ? arguments[4] : undefined;
const normalizeIndexesGroup = indexes => {
if (indexes.length === 0) {
return [];
}
const sortedIndexes = [...indexes];
// Sort the indexes in ascending order.
sortedIndexes.sort((_ref3, _ref4) => {
let [indexA] = _ref3;
let [indexB] = _ref4;
if (indexA === indexB) {
return 0;
}
return indexA > indexB ? 1 : -1;
});
// Normalize the {index, amount} groups into bigger groups.
const normalizedIndexes = arrayReduce(sortedIndexes, (acc, _ref5) => {
let [groupIndex, groupAmount] = _ref5;
const previousItem = acc[acc.length - 1];
const [prevIndex, prevAmount] = previousItem;
const prevLastIndex = prevIndex + prevAmount;
if (groupIndex <= prevLastIndex) {
const amountToAdd = Math.max(groupAmount - (prevLastIndex - groupIndex), 0);
previousItem[1] += amountToAdd;
} else {
acc.push([groupIndex, groupAmount]);
}
return acc;
}, [sortedIndexes[0]]);
return normalizedIndexes;
};
/* eslint-disable no-case-declarations */
switch (action) {
case 'insert_row_below':
case 'insert_row_above':
const numberOfSourceRows = instance.countSourceRows();
if (tableMeta.maxRows === numberOfSourceRows) {
return;
}
// `above` is the default behavior for creating new rows
const insertRowMode = action === 'insert_row_below' ? 'below' : 'above';
// Calling the `insert_row_above` action adds a new row at the beginning of the data set.
// eslint-disable-next-line no-param-reassign
index = index !== null && index !== void 0 ? index : insertRowMode === 'below' ? numberOfSourceRows : 0;
const {
delta: rowDelta,
startPhysicalIndex: startRowPhysicalIndex
} = datamap.createRow(index, amount, {
source,
mode: insertRowMode
});
selection.shiftRows(instance.toVisualRow(startRowPhysicalIndex), rowDelta);
break;
case 'insert_col_start':
case 'insert_col_end':
// "start" is a default behavior for creating new columns
const insertColumnMode = action === 'insert_col_end' ? 'end' : 'start';
// Calling the `insert_col_start` action adds a new column to the left of the data set.
// eslint-disable-next-line no-param-reassign
index = index !== null && index !== void 0 ? index : insertColumnMode === 'end' ? instance.countSourceCols() : 0;
const {
delta: colDelta,
startPhysicalIndex: startColumnPhysicalIndex
} = datamap.createCol(index, amount, {
source,
mode: insertColumnMode
});
if (colDelta) {
if (Array.isArray(tableMeta.colHeaders)) {
const spliceArray = [instance.toVisualColumn(startColumnPhysicalIndex), 0];
spliceArray.length += colDelta; // inserts empty (undefined) elements at the end of an array
Array.prototype.splice.apply(tableMeta.colHeaders, spliceArray); // inserts empty (undefined) elements into the colHeader array
}
selection.shiftColumns(instance.toVisualColumn(startColumnPhysicalIndex), colDelta);
}
break;
case 'remove_row':
const removeRow = indexes => {
let offset = 0;
// Normalize the {index, amount} groups into bigger groups.
arrayEach(indexes, _ref6 => {
let [groupIndex, groupAmount] = _ref6;
const calcIndex = isEmpty(groupIndex) ? instance.countRows() - 1 : Math.max(groupIndex - offset, 0);
// If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
// compatible with datamap.removeCol method.
if (Number.isInteger(groupIndex)) {
// eslint-disable-next-line no-param-reassign
groupIndex = Math.max(groupIndex - offset, 0);
}
// TODO: for datamap.removeRow index should be passed as it is (with undefined and null values). If not, the logic
// inside the datamap.removeRow breaks the removing functionality.
const wasRemoved = datamap.removeRow(groupIndex, groupAmount, source);
if (!wasRemoved) {
return;
}
if (selection.isSelected()) {
const {
row
} = instance.getSelectedRangeActive().highlight;
if (row >= groupIndex && row <= groupIndex + groupAmount - 1) {
editorManager.closeEditor(true);
}
}
const totalRows = instance.countRows();
const fixedRowsTop = tableMeta.fixedRowsTop;
if (fixedRowsTop >= calcIndex + 1) {
tableMeta.fixedRowsTop -= Math.min(groupAmount, fixedRowsTop - calcIndex);
}
const fixedRowsBottom = tableMeta.fixedRowsBottom;
if (fixedRowsBottom && calcIndex >= totalRows - fixedRowsBottom) {
tableMeta.fixedRowsBottom -= Math.min(groupAmount, fixedRowsBottom);
}
if (totalRows === 0) {
selection.deselect();
} else if (source === 'ContextMenu.removeRow') {
const selectionRange = selection.getSelectedRange();
const lastSelection = selectionRange.pop();
selectionRange.clear().set(lastSelection.from).current().setTo(lastSelection.to);
selection.refresh();
} else {
selection.shiftRows(groupIndex, -groupAmount);
}
offset += groupAmount;
});
};
if (Array.isArray(index)) {
removeRow(normalizeIndexesGroup(index));
} else {
removeRow([[index, amount]]);
}
break;
case 'remove_col':
const removeCol = indexes => {
let offset = 0;
// Normalize the {index, amount} groups into bigger groups.
arrayEach(indexes, _ref7 => {
let [groupIndex, groupAmount] = _ref7;
const calcIndex = isEmpty(groupIndex) ? instance.countCols() - 1 : Math.max(groupIndex - offset, 0);
let physicalColumnIndex = instance.toPhysicalColumn(calcIndex);
// If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
// compatible with datamap.removeCol method.
if (Number.isInteger(groupIndex)) {
// eslint-disable-next-line no-param-reassign
groupIndex = Math.max(groupIndex - offset, 0);
}
// TODO: for datamap.removeCol index should be passed as it is (with undefined and null values). If not, the logic
// inside the datamap.removeCol breaks the removing functionality.
const wasRemoved = datamap.removeCol(groupIndex, groupAmount, source);
if (!wasRemoved) {
return;
}
if (selection.isSelected()) {
const {
col
} = instance.getSelectedRangeActive().highlight;
if (col >= groupIndex && col <= groupIndex + groupAmount - 1) {
editorManager.closeEditor(true);
}
}
const totalColumns = instance.countCols();
if (totalColumns === 0) {
selection.deselect();
} else if (source === 'ContextMenu.removeColumn') {
const selectionRange = selection.getSelectedRange();
const lastSelection = selectionRange.pop();
selectionRange.clear().set(lastSelection.from).current().setTo(lastSelection.to);
selection.refresh();
} else {
selection.shiftColumns(groupIndex, -groupAmount);
}
const fixedColumnsStart = tableMeta.fixedColumnsStart;
if (fixedColumnsStart >= calcIndex + 1) {
tableMeta.fixedColumnsStart -= Math.min(groupAmount, fixedColumnsStart - calcIndex);
}
if (Array.isArray(tableMeta.colHeaders)) {
if (typeof physicalColumnIndex === 'undefined') {
physicalColumnIndex = -1;
}
tableMeta.colHeaders.splice(physicalColumnIndex, groupAmount);
}
offset += groupAmount;
});
};
if (Array.isArray(index)) {
removeCol(normalizeIndexesGroup(index));
} else {
removeCol([[index, amount]]);
}
break;
default:
throw new Error(`There is no such action "${action}"`);
}
if (!keepEmptyRows) {
grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh
}
instance.view.adjustElementsSize();
instance.view.render();
},
/**
* Makes sure there are empty rows at the bottom of the table.
*
* @private
*/
adjustRowsAndCols() {
const minRows = tableMeta.minRows;
const minSpareRows = tableMeta.minSpareRows;
const minCols = tableMeta.minCols;
const minSpareCols = tableMeta.minSpareCols;
if (minRows) {
// should I add empty rows to data source to meet minRows?
const nrOfRows = instance.countRows();
if (nrOfRows < minRows) {
// The synchronization with cell meta is not desired here. For `minRows` option,
// we don't want to touch/shift cell meta objects.
datamap.createRow(nrOfRows, minRows - nrOfRows, {
source: 'auto'
});
}
}
if (minSpareRows) {
const emptyRows = instance.countEmptyRows(true);
// should I add empty rows to meet minSpareRows?
if (emptyRows < minSpareRows) {
const emptyRowsMissing = minSpareRows - emptyRows;
const rowsToCreate = Math.min(emptyRowsMissing, tableMeta.maxRows - instance.countSourceRows());
// The synchronization with cell meta is not desired here. For `minSpareRows` option,
// we don't want to touch/shift cell meta objects.
datamap.createRow(instance.countRows(), rowsToCreate, {
source: 'auto'
});
}
}
{
let emptyCols;
// count currently empty cols
if (minCols || minSpareCols) {
emptyCols = instance.countEmptyCols(true);
}
let nrOfColumns = instance.countCols();
// should I add empty cols to meet minCols?
if (minCols && !tableMeta.columns && nrOfColumns < minCols) {
// The synchronization with cell meta is not desired here. For `minCols` option,
// we don't want to touch/shift cell meta objects.
const colsToCreate = minCols - nrOfColumns;
emptyCols += colsToCreate;
datamap.createCol(nrOfColumns, colsToCreate, {
source: 'auto'
});
}
// should I add empty cols to meet minSpareCols?
if (minSpareCols && !tableMeta.columns && instance.dataType === 'array' && emptyCols < minSpareCols) {
nrOfColumns = instance.countCols();
const emptyColsMissing = minSpareCols - emptyCols;
const colsToCreate = Math.min(emptyColsMissing, tableMeta.maxCols - nrOfColumns);
// The synchronization with cell meta is not desired here. For `minSpareCols` option,
// we don't want to touch/shift cell meta objects.
datamap.createCol(nrOfColumns, colsToCreate, {
source: 'auto'
});
}
}
},
/**
* Populate the data from the provided 2d array from the given cell coordinates.
*
* @private
* @param {object} start Start selection position. Visual indexes.
* @param {Array} input 2d data array.
* @param {object} [end] End selection position (only for drag-down mode). Visual indexes.
* @param {string} [source="populateFromArray"] Source information string.
* @param {string} [method="overwrite"] Populate method. Possible options: `shift_down`, `shift_right`, `overwrite`.
* @returns {object|undefined} Ending td in pasted area (only if any cell was changed).
*/
populateFromArray(start, input, end, source, method) {
let r;
let rlen;
let c;
let clen;
const setData = [];
const current = {};
const newDataByColumns = [];
const startRow = start.row;
const startColumn = start.col;
rlen = input.length;
if (rlen === 0) {
return false;
}
let columnsPopulationEnd = 0;
let rowsPopulationEnd = 0;
if (isObject(end)) {
columnsPopulationEnd = end.col - startColumn + 1;
rowsPopulationEnd = end.row - startRow + 1;
}
// insert data with specified pasteMode method
switch (method) {
case 'shift_down':
// translate data from a list of rows to a list of columns
const populatedDataByColumns = pivot(input);
const numberOfDataColumns = populatedDataByColumns.length;
// method's argument can extend the range of data population (data would be repeated)
const numberOfColumnsToPopulate = Math.max(numberOfDataColumns, columnsPopulationEnd);
const pushedDownDataByRows = instance.getData().slice(startRow);
// translate data from a list of rows to a list of columns
const pushedDownDataByColumns = pivot(pushedDownDataByRows).slice(startColumn, startColumn + numberOfColumnsToPopulate);
for (c = 0; c < numberOfColumnsToPopulate; c += 1) {
if (c < numberOfDataColumns) {
for (r = 0, rlen = populatedDataByColumns[c].length; r < rowsPopulationEnd - rlen; r += 1) {
// repeating data for rows
populatedDataByColumns[c].push(populatedDataByColumns[c][r % rlen]);
}
if (c < pushedDownDataByColumns.length) {
newDataByColumns.push(populatedDataByColumns[c].concat(pushedDownDataByColumns[c]));
} else {
// if before data population, there was no data in the column
// we fill the required rows' newly-created cells with `null` values
newDataByColumns.push(populatedDataByColumns[c].concat(new Array(pushedDownDataByRows.length).fill(null)));
}
} else {
// Repeating data for columns.
newDataByColumns.push(populatedDataByColumns[c % numberOfDataColumns].concat(pushedDownDataByColumns[c]));
}
}
instance.populateFromArray(startRow, startColumn, pivot(newDataByColumns));
break;
case 'shift_right':
const numberOfDataRows = input.length;
// method's argument can extend the range of data population (data would be repeated)
const numberOfRowsToPopulate = Math.max(numberOfDataRows, rowsPopulationEnd);
const pushedRightDataByRows = instance.getData().slice(startRow).map(rowData => rowData.slice(startColumn));
for (r = 0; r < numberOfRowsToPopulate; r += 1) {
if (r < numberOfDataRows) {
for (c = 0, clen = input[r].length; c < columnsPopulationEnd - clen; c += 1) {
// repeating data for rows
input[r].push(input[r][c % clen]);
}
if (r < pushedRightDataByRows.length) {
for (let i = 0; i < pushedRightDataByRows[r].length; i += 1) {
input[r].push(pushedRightDataByRows[r][i]);
}
} else {
// if before data population, there was no data in the row
// we fill the required columns' newly-created cells with `null` values
input[r].push(...new Array(pushedRightDataByRows[0].length).fill(null));
}
} else {
// Repeating data for columns.
input.push(input[r % rlen].slice(0, numberOfRowsToPopulate).concat(pushedRightDataByRows[r]));
}
}
instance.populateFromArray(startRow, startColumn, input);
break;
case 'overwrite':
default:
// overwrite and other not specified options
current.row = start.row;
current.col = start.col;
let skippedRow = 0;
let skippedColumn = 0;
let pushData = true;
let cellMeta;
const getInputValue = function getInputValue(row) {
let col = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
const rowValue = input[row % input.length];
if (col !== null) {
return rowValue[col % rowValue.length];
}
return rowValue;
};
const rowInputLength = input.length;
const rowSelectionLength = end ? end.row - start.row + 1 : 0;
if (end) {
rlen = rowSelectionLength;
} else {
rlen = Math.max(rowInputLength, rowSelectionLength);
}
for (r = 0; r < rlen; r++) {
if (end && current.row > end.row && rowSelectionLength > rowInputLength || !tableMeta.allowInsertRow && current.row > instance.countRows() - 1 || current.row >= tableMeta.maxRows) {
break;
}
const visualRow = r - skippedRow;
const colInputLength = getInputValue(visualRow).length;
const colSelectionLength = end ? end.col - start.col + 1 : 0;
if (end) {
clen = colSelectionLength;
} else {
clen = Math.max(colInputLength, colSelectionLength);
}
current.col = start.col;
cellMeta = instance.getCellMeta(current.row, current.col);
if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipRowOnPaste) {
skippedRow += 1;
current.row += 1;
rlen += 1;
/* eslint-disable no-continue */
continue;
}
skippedColumn = 0;
for (c = 0; c < clen; c++) {
var _instance$getSourceDa;
if (end && current.col > end.col && colSelectionLength > colInputLength || !tableMeta.allowInsertColumn && current.col > instance.countCols() - 1 || current.col >= tableMeta.maxCols) {
break;
}
cellMeta = instance.getCellMeta(current.row, current.col);
if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipColumnOnPaste) {
skippedColumn += 1;
current.col += 1;
clen += 1;
continue;
}
if (cellMeta.readOnly && source !== 'UndoRedo.undo') {
current.col += 1;
/* eslint-disable no-continue */
continue;
}
const visualColumn = c - skippedColumn;
const hasValueSetter = !!cellMeta.valueSetter;
let value = getInputValue(visualRow, visualColumn);
let orgValue = (_instance$getSourceDa = instance.getSourceDataAtCell(current.row, current.col)) !== null && _instance$getSourceDa !== void 0 ? _instance$getSourceDa : null;
if (value !== null && typeof value === 'object') {
// when 'value' is array and 'orgValue' is null, set 'orgValue' to
// an empty array so that the null value can be compared to 'value'
// as an empty value for the array context
if (Array.isArray(value) && orgValue === null) {
orgValue = [];
}
if (!hasValueSetter && (typeof orgValue !== 'object' || orgValue === null)) {
pushData = false;
} else if (orgValue !== null) {
const orgValueSchema = duckSchema(Array.isArray(orgValue) ? orgValue : orgValue[0] || orgValue);
const valueSchema = duckSchema(Array.isArray(value) ? value : value[0] || value);
// Allow overwriting values with the same object-based schema or any array-based schema.
if (hasValueSetter ||
// If the cell has a value setter, we don't know the value schema (it's dynamic)
isObjectEqual(orgValueSchema, valueSchema) || Array.isArray(orgValueSchema) && Array.isArray(valueSchema)) {
value = deepClone(value);
} else {
pushData = false;
}
}
} else if (!hasValueSetter && orgValue !== null && typeof orgValue === 'object') {
pushData = false;
}
if (pushData) {
setData.push([current.row, current.co