UNPKG

handsontable

Version:

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

1,170 lines (1,113 loc) • 3.73 MB
/*! * Copyright (c) HANDSONCODE sp. z o. o. * * HANDSONTABLE is a software distributed by HANDSONCODE sp. z o. o., a Polish corporation based in * Gdynia, Poland, at Aleja Zwyciestwa 96-98, registered by the District Court in Gdansk under number * 538651, EU tax ID number: PL5862294002, share capital: PLN 62,800.00. * * This software is protected by applicable copyright laws, including international treaties, and dual- * licensed - depending on whether your use for commercial purposes, meaning intended for or * resulting in commercial advantage or monetary compensation, or not. * * If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing * the suitability, performance, and usefulness of this software outside the production environment, * you agree to be bound by the terms included in the "handsontable-non-commercial-license.pdf" file. * * Your use of this software for commercial purposes is subject to the terms included in an applicable * license agreement. * * In any case, you must not make any such use of this software as to develop software which may be * considered competitive with this software. * * UNLESS EXPRESSLY AGREED OTHERWISE, HANDSONCODE PROVIDES THIS SOFTWARE ON AN "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO * LEGAL THEORY, SHALL HANDSONCODE BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM * USE OR INABILITY TO USE THIS SOFTWARE. * * Version: 16.1.0 * Release date: 15/09/2025 (built at 15/09/2025 10:06:15) */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("dompurify"), require("@handsontable/pikaday"), require("moment"), require("numbro")); else if(typeof define === 'function' && define.amd) define("Handsontable", ["dompurify", "@handsontable/pikaday", "moment", "numbro"], factory); else if(typeof exports === 'object') exports["Handsontable"] = factory(require("dompurify"), require("@handsontable/pikaday"), require("moment"), require("numbro")); else root["Handsontable"] = factory(root["DOMPurify"], root["Pikaday"], root["moment"], root["numbro"]); })(typeof self !== 'undefined' ? self : this, (__WEBPACK_EXTERNAL_MODULE__161__, __WEBPACK_EXTERNAL_MODULE__454__, __WEBPACK_EXTERNAL_MODULE__163__, __WEBPACK_EXTERNAL_MODULE__478__) => { return /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ([ /* 0 */, /* 1 */ /***/ ((module) => { function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; } module.exports = _interopRequireDefault, module.exports.__esModule = true, module.exports["default"] = module.exports; /***/ }), /* 2 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; var _interopRequireDefault = __webpack_require__(1); exports.__esModule = true; __webpack_require__(3); var _core = _interopRequireDefault(__webpack_require__(4)); var _rootInstance = __webpack_require__(290); var _dataMap = __webpack_require__(342); var _hooks = __webpack_require__(183); var _registry = __webpack_require__(316); var _registry2 = __webpack_require__(350); var _textType = __webpack_require__(434); var _baseEditor = __webpack_require__(368); var _src = __webpack_require__(211); exports.CellCoords = _src.CellCoords; exports.CellRange = _src.CellRange; // FIXME: Bug in eslint-plugin-import: https://github.com/benmosher/eslint-plugin-import/issues/1883 /* eslint-disable import/named */ /* eslint-enable import/named */ // register default mandatory cell type for the Base package (0, _registry2.registerCellType)(_textType.TextCellType); // export the `BaseEditor` class to the Handsontable global namespace Handsontable.editors = { BaseEditor: _baseEditor.BaseEditor }; /** * @param {HTMLElement} rootElement The element to which the Handsontable instance is injected. * @param {object} userSettings The user defined options. * @returns {Core} */ function Handsontable(rootElement, userSettings) { const instance = new _core.default(rootElement, userSettings || {}, _rootInstance.rootInstanceSymbol); instance.init(); return instance; } Handsontable.Core = function (rootElement) { let userSettings = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return new _core.default(rootElement, userSettings, _rootInstance.rootInstanceSymbol); }; Handsontable.DefaultSettings = (0, _dataMap.metaSchemaFactory)(); Handsontable.hooks = _hooks.Hooks.getSingleton(); Handsontable.CellCoords = _src.CellCoords; Handsontable.CellRange = _src.CellRange; Handsontable.packageName = 'handsontable'; Handsontable.buildDate = "15/09/2025 10:06:15"; Handsontable.version = "16.1.0"; Handsontable.languages = { dictionaryKeys: _registry.dictionaryKeys, getLanguageDictionary: _registry.getLanguageDictionary, getLanguagesDictionaries: _registry.getLanguagesDictionaries, registerLanguageDictionary: _registry.registerLanguageDictionary, getTranslatedPhrase: _registry.getTranslatedPhrase }; var _default = exports["default"] = Handsontable; /***/ }), /* 3 */ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); // extracted by mini-css-extract-plugin /***/ }), /* 4 */ /***/ ((__unused_webpack_module, exports, __webpack_require__) => { "use strict"; var _interopRequireDefault = __webpack_require__(1); exports.__esModule = true; exports["default"] = Core; __webpack_require__(5); __webpack_require__(87); __webpack_require__(91); __webpack_require__(101); __webpack_require__(112); __webpack_require__(114); __webpack_require__(116); __webpack_require__(118); __webpack_require__(120); __webpack_require__(123); __webpack_require__(125); __webpack_require__(136); __webpack_require__(145); __webpack_require__(147); __webpack_require__(149); var _element = __webpack_require__(159); var _function = __webpack_require__(174); var _mixed = __webpack_require__(162); var _browser = __webpack_require__(169); var _editorManager = _interopRequireDefault(__webpack_require__(175)); var _eventManager = _interopRequireDefault(__webpack_require__(194)); var _object = __webpack_require__(170); var _focusManager = __webpack_require__(195); var _array = __webpack_require__(167); var _parseTable = __webpack_require__(196); var _staticRegister = __webpack_require__(193); var _registry = __webpack_require__(203); var _registry2 = __webpack_require__(208); var _registry3 = __webpack_require__(182); var _registry4 = __webpack_require__(209); var _string = __webpack_require__(160); var _number = __webpack_require__(205); var _tableView = _interopRequireDefault(__webpack_require__(210)); var _dataSource = _interopRequireDefault(__webpack_require__(291)); var _data = __webpack_require__(292); var _translations = __webpack_require__(295); var _rootInstance = __webpack_require__(290); var _src = __webpack_require__(211); var _hooks = __webpack_require__(183); var _registry5 = __webpack_require__(316); var _utils = __webpack_require__(317); var _selection = __webpack_require__(323); var _dataMap = __webpack_require__(342); var _index = __webpack_require__(359); var _uniqueMap = __webpack_require__(206); var _shortcuts = __webpack_require__(424); var _shortcutContexts = __webpack_require__(361); var _themes = __webpack_require__(430); var _stylesHandler = __webpack_require__(431); var _console = __webpack_require__(184); var _rangeToRenderableMapper = __webpack_require__(432); var _a11yAnnouncer = __webpack_require__(433); var _valueAccessors = __webpack_require__(346); 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. */ 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.default(instance); let datamap; let dataSource; let grid; let editorManager; let focusManager; let viewportScroller; let firstRun = true; const mergedUserSettings = { ...userSettings.initialState, ...userSettings }; if ((0, _rootInstance.hasValidParameter)(rootInstanceSymbol)) { (0, _rootInstance.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 = (0, _rootInstance.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 ((0, _rootInstance.isRootInstance)(this)) { this.rootWrapperElement = this.rootDocument.createElement('div'); this.rootGridElement = this.rootDocument.createElement('div'); this.rootPortalElement = this.rootDocument.createElement('div'); (0, _element.addClass)(this.rootElement, ['ht-wrapper', 'handsontable']); (0, _element.addClass)(this.rootWrapperElement, 'ht-root-wrapper'); (0, _element.addClass)(this.rootGridElement, 'ht-grid'); this.rootGridElement.appendChild(this.rootElement); this.rootWrapperElement.appendChild(this.rootGridElement); this.rootContainer.appendChild(this.rootWrapperElement); (0, _element.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.StylesHandler({ rootElement: instance.rootElement, rootDocument: instance.rootDocument, onThemeChange: validThemeName => { if ((0, _rootInstance.isRootInstance)(this)) { (0, _element.removeClass)(this.rootWrapperElement, /ht-theme-.*/g); (0, _element.removeClass)(this.rootPortalElement, /ht-theme-.*/g); if (validThemeName) { (0, _element.addClass)(this.rootWrapperElement, validThemeName); (0, _element.addClass)(this.rootPortalElement, validThemeName); if (!getComputedStyle(this.rootWrapperElement).getPropertyValue('--ht-line-height')) { (0, _console.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 = (0, _registry5.getValidLanguageCode)(mergedUserSettings.language); const settingsWithoutHooks = Object.fromEntries(Object.entries(mergedUserSettings).filter(_ref => { let [key] = _ref; return !(_hooks.Hooks.getSingleton().isRegistered(key) || _hooks.Hooks.getSingleton().isDeprecated(key)); })); const metaManager = new _dataMap.MetaManager(instance, settingsWithoutHooks, [_dataMap.DynamicCellMetaMod, _dataMap.ExtendMetaPropertiesMod]); const tableMeta = metaManager.getTableMeta(); const globalMeta = metaManager.getGlobalMeta(); const pluginsRegistry = (0, _uniqueMap.createUniqueMap)(); this.container = this.rootDocument.createElement('div'); this.rootElement.insertBefore(this.container, this.rootElement.firstChild); this.guid = `ht_${(0, _string.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 _translations.IndexMapper(); /** * Instance of index mapper which is responsible for managing the row indexes. * * @memberof Core# * @member rowIndexMapper * @type {IndexMapper} */ this.rowIndexMapper = new _translations.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.default(instance); const moduleRegisterer = (0, _staticRegister.staticRegister)(this.guid); moduleRegisterer.register('cellRangeMapper', new _rangeToRenderableMapper.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.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 = (0, _object.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) { (0, _element.addClass)(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']); } else if (isSelectedByRowHeader) { (0, _element.removeClass)(this.rootElement, 'ht__selection--columns'); (0, _element.addClass)(this.rootElement, 'ht__selection--rows'); } else if (isSelectedByColumnHeader) { (0, _element.removeClass)(this.rootElement, 'ht__selection--rows'); (0, _element.addClass)(this.rootElement, 'ht__selection--columns'); } else { (0, _element.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 = (0, _object.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(); (0, _element.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 = (0, _array.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. (0, _array.arrayEach)(indexes, _ref6 => { let [groupIndex, groupAmount] = _ref6; const calcIndex = (0, _mixed.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. (0, _array.arrayEach)(indexes, _ref7 => { let [groupIndex, groupAmount] = _ref7; const calcIndex = (0, _mixed.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 ((0, _object.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 = (0, _array.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 = (0, _array.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, (0, _array.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, numberOfRowsT