handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
1,170 lines (1,113 loc) • 3.73 MB
JavaScript
/*!
* 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