UNPKG

handsontable

Version:

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

1,133 lines (1,090 loc) • 199 kB
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