UNPKG

handsontable

Version:

Handsontable is a JavaScript Spreadsheet Component available for React, Angular and Vue.

1,245 lines (1,005 loc) • 177 kB
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } import "core-js/modules/es.array.includes.js"; import "core-js/modules/es.array.sort.js"; import "core-js/modules/es.array.splice.js"; import "core-js/modules/es.number.is-integer.js"; import "core-js/modules/es.number.constructor.js"; import "core-js/modules/es.array.slice.js"; import "core-js/modules/es.array.concat.js"; import "core-js/modules/es.array.fill.js"; import "core-js/modules/es.array.map.js"; import "core-js/modules/es.regexp.exec.js"; import "core-js/modules/es.string.replace.js"; import "core-js/modules/es.array.from.js"; import "core-js/modules/es.string.iterator.js"; import "core-js/modules/es.array.index-of.js"; import "core-js/modules/es.array.reverse.js"; import "core-js/modules/es.object.to-string.js"; import "core-js/modules/web.dom-collections.for-each.js"; import "core-js/modules/web.timers.js"; import "core-js/modules/web.immediate.js"; import "core-js/modules/es.array.iterator.js"; import "core-js/modules/es.map.js"; import "core-js/modules/web.dom-collections.iterator.js"; import "core-js/modules/es.symbol.js"; import "core-js/modules/es.symbol.description.js"; import "core-js/modules/es.symbol.iterator.js"; import "core-js/modules/es.function.name.js"; import { addClass, empty, 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 { arrayMap, arrayEach, arrayReduce, getDifferenceOfArrays, stringToArray, pivot } from "./helpers/array.mjs"; import { instanceToHTML } from "./utils/parseTable.mjs"; import { getPlugin, getPluginsNames } from "./plugins/registry.mjs"; import { getRenderer } from "./renderers/registry.mjs"; import { getValidator } from "./validators/registry.mjs"; import { randomString, toUpperCaseFirst } from "./helpers/string.mjs"; import { rangeEach, rangeEachReverse, isNumericLike } from "./helpers/number.mjs"; import TableView from "./tableView.mjs"; import DataSource from "./dataSource.mjs"; import { cellMethodLookupFactory, spreadsheetColumnLabel } from "./helpers/data.mjs"; import { IndexMapper } from "./translations/index.mjs"; import { registerAsRootInstance, hasValidParameter, isRootInstance } from "./utils/rootInstance.mjs"; import { ViewportColumnsCalculator } from "./3rdparty/walkontable/src/index.mjs"; import Hooks from "./pluginHooks.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 { createUniqueMap } from "./utils/dataStructures/uniqueMap.mjs"; import { createShortcutManager } from "./shortcuts/index.mjs"; var SHORTCUTS_GROUP = 'gridDefault'; var activeGuid = null; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * Handsontable constructor. * * @core * @class Core * @description * * The `Handsontable` class to which we refer as to `Core`, allows you to modify the grid's behavior by using one of the available public methods. * * ## How to call a method * * ```js * // First, let's contruct Handsontable * const hot = new Handsontable(document.getElementById('example'), options); * * // Then, let's use the setDataAtCell method * hot.setDataAtCell(0, 0, 'new value'); * ``` * * @param {HTMLElement} rootElement 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(rootElement, userSettings) { var _userSettings$layoutD, _this = this; var rootInstanceSymbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var preventScrollingToCell = false; var instance = this; var eventManager = new EventManager(instance); var datamap; var dataSource; var grid; var editorManager; var firstRun = true; if (hasValidParameter(rootInstanceSymbol)) { registerAsRootInstance(this); } // TODO: check if references to DOM elements should be move to UI layer (Walkontable) /** * Reference to the container element. * * @private * @type {HTMLElement} */ this.rootElement = rootElement; /** * The nearest document over container. * * @private * @type {Document} */ this.rootDocument = rootElement.ownerDocument; /** * Window object over container's document. * * @private * @type {Window} */ this.rootWindow = this.rootDocument.defaultView; /** * 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; var layoutDirection = (_userSettings$layoutD = userSettings === null || userSettings === void 0 ? void 0 : userSettings.layoutDirection) !== null && _userSettings$layoutD !== void 0 ? _userSettings$layoutD : 'inherit'; var rootElementDirection = ['rtl', 'ltr'].includes(layoutDirection) ? layoutDirection : this.rootWindow.getComputedStyle(this.rootElement).direction; this.rootElement.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; }; userSettings.language = getValidLanguageCode(userSettings.language); var metaManager = new MetaManager(instance, userSettings, [DynamicCellMetaMod, ExtendMetaPropertiesMod]); var tableMeta = metaManager.getTableMeta(); var globalMeta = metaManager.getGlobalMeta(); var pluginsRegistry = createUniqueMap(); this.container = this.rootDocument.createElement('div'); this.renderCall = false; rootElement.insertBefore(this.container, rootElement.firstChild); if (isRootInstance(this)) { _injectProductInfo(userSettings.licenseKey, rootElement); } this.guid = "ht_".concat(randomString()); // this is the namespace for global events /** * 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(); dataSource = new DataSource(instance); 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 } var visualToRenderableCoords = function visualToRenderableCoords(coords) { var visualRow = coords.row, visualColumn = coords.col; 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); }; var renderableToVisualCoords = function renderableToVisualCoords(coords) { var renderableRow = coords.row, renderableColumn = coords.col; 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 ); }; var selection = new Selection(tableMeta, { countCols: function countCols() { return instance.countCols(); }, countRows: function countRows() { return instance.countRows(); }, propToCol: function propToCol(prop) { return datamap.propToCol(prop); }, isEditorOpened: function isEditorOpened() { return instance.getActiveEditor() ? instance.getActiveEditor().isOpened() : false; }, countColsTranslated: function countColsTranslated() { return _this.view.countRenderableColumns(); }, countRowsTranslated: function countRowsTranslated() { return _this.view.countRenderableRows(); }, getShortcutManager: function getShortcutManager() { return instance.getShortcutManager(); }, createCellCoords: function createCellCoords(row, column) { return instance._createCellCoords(row, column); }, createCellRange: function createCellRange(highlight, from, to) { return instance._createCellRange(highlight, from, to); }, visualToRenderableCoords: visualToRenderableCoords, renderableToVisualCoords: renderableToVisualCoords, isDisabledCellSelection: function isDisabledCellSelection(visualRow, visualColumn) { return instance.getCellMeta(visualRow, visualColumn).disableVisualSelection; } }); this.selection = selection; var onIndexMapperCacheUpdate = function onIndexMapperCacheUpdate(_ref) { var hiddenIndexesChanged = _ref.hiddenIndexesChanged; if (hiddenIndexesChanged) { _this.selection.refresh(); } }; this.columnIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate); this.rowIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate); this.selection.addLocalHook('beforeSetRangeStart', function (cellCoords) { _this.runHooks('beforeSetRangeStart', cellCoords); }); this.selection.addLocalHook('beforeSetRangeStartOnly', function (cellCoords) { _this.runHooks('beforeSetRangeStartOnly', cellCoords); }); this.selection.addLocalHook('beforeSetRangeEnd', function (cellCoords) { _this.runHooks('beforeSetRangeEnd', cellCoords); if (cellCoords.row < 0) { cellCoords.row = _this.view._wt.wtTable.getFirstVisibleRow(); } if (cellCoords.col < 0) { cellCoords.col = _this.view._wt.wtTable.getFirstVisibleColumn(); } }); this.selection.addLocalHook('afterSetRangeEnd', function (cellCoords) { var preventScrolling = createObjectPropListener(false); var selectionRange = _this.selection.getSelectedRange(); var _selectionRange$curre = selectionRange.current(), from = _selectionRange$curre.from, to = _selectionRange$curre.to; var 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); // eslint-disable-line max-len var isSelectedByAnyHeader = _this.selection.isSelectedByAnyHeader(); var currentSelectedRange = _this.selection.selectedRange.current(); var scrollToCell = true; if (preventScrollingToCell) { scrollToCell = false; } if (preventScrolling.isTouched()) { scrollToCell = !preventScrolling.value; } var isSelectedByRowHeader = _this.selection.isSelectedByRowHeader(); var isSelectedByColumnHeader = _this.selection.isSelectedByColumnHeader(); if (scrollToCell !== false) { if (!isSelectedByAnyHeader) { if (currentSelectedRange && !_this.selection.isMultiple()) { _this.view.scrollViewport(visualToRenderableCoords(currentSelectedRange.from)); } else { _this.view.scrollViewport(visualToRenderableCoords(cellCoords)); } } else if (isSelectedByRowHeader) { _this.view.scrollViewportVertically(instance.rowIndexMapper.getRenderableFromVisualIndex(cellCoords.row)); } else if (isSelectedByColumnHeader) { _this.view.scrollViewportHorizontally(instance.columnIndexMapper.getRenderableFromVisualIndex(cellCoords.col)); } } // @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']); } _this._refreshBorders(null); }); this.selection.addLocalHook('afterSelectionFinished', function (cellRanges) { var selectionLayerLevel = cellRanges.length - 1; var _cellRanges$selection = cellRanges[selectionLayerLevel], from = _cellRanges$selection.from, to = _cellRanges$selection.to; _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); }); this.selection.addLocalHook('afterIsMultipleSelection', function (isMultiple) { var changedIsMultiple = _this.runHooks('afterIsMultipleSelection', isMultiple.value); if (isMultiple.value) { isMultiple.value = changedIsMultiple; } }); this.selection.addLocalHook('beforeModifyTransformStart', function (cellCoordsDelta) { _this.runHooks('modifyTransformStart', cellCoordsDelta); }); this.selection.addLocalHook('afterModifyTransformStart', function (coords, rowTransformDir, colTransformDir) { _this.runHooks('afterModifyTransformStart', coords, rowTransformDir, colTransformDir); }); this.selection.addLocalHook('beforeModifyTransformEnd', function (cellCoordsDelta) { _this.runHooks('modifyTransformEnd', cellCoordsDelta); }); this.selection.addLocalHook('afterModifyTransformEnd', function (coords, rowTransformDir, colTransformDir) { _this.runHooks('afterModifyTransformEnd', coords, rowTransformDir, colTransformDir); }); this.selection.addLocalHook('afterDeselect', function () { editorManager.destroyEditor(); _this._refreshBorders(); removeClass(_this.rootElement, ['ht__selection--rows', 'ht__selection--columns']); _this.runHooks('afterDeselect'); }); this.selection.addLocalHook('insertRowRequire', function (totalRows) { _this.alter('insert_row', totalRows, 1, 'auto'); }); this.selection.addLocalHook('insertColRequire', function (totalCols) { _this.alter('insert_col', totalCols, 1, 'auto'); }); grid = { /** * Inserts or removes rows and columns. * * @private * @param {string} action Possible values: "insert_row", "insert_col", "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] Ammount 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: function alter(action, index) { var amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; var source = arguments.length > 3 ? arguments[3] : undefined; var keepEmptyRows = arguments.length > 4 ? arguments[4] : undefined; var delta; var normalizeIndexesGroup = function normalizeIndexesGroup(indexes) { if (indexes.length === 0) { return []; } var sortedIndexes = _toConsumableArray(indexes); // Sort the indexes in ascending order. sortedIndexes.sort(function (_ref2, _ref3) { var _ref4 = _slicedToArray(_ref2, 1), indexA = _ref4[0]; var _ref5 = _slicedToArray(_ref3, 1), indexB = _ref5[0]; if (indexA === indexB) { return 0; } return indexA > indexB ? 1 : -1; }); // Normalize the {index, amount} groups into bigger groups. var normalizedIndexes = arrayReduce(sortedIndexes, function (acc, _ref6) { var _ref7 = _slicedToArray(_ref6, 2), groupIndex = _ref7[0], groupAmount = _ref7[1]; var previousItem = acc[acc.length - 1]; var _previousItem = _slicedToArray(previousItem, 2), prevIndex = _previousItem[0], prevAmount = _previousItem[1]; var prevLastIndex = prevIndex + prevAmount; if (groupIndex <= prevLastIndex) { var 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': var numberOfSourceRows = instance.countSourceRows(); if (tableMeta.maxRows === numberOfSourceRows) { return; } // eslint-disable-next-line no-param-reassign index = isDefined(index) ? index : numberOfSourceRows; delta = datamap.createRow(index, amount, source); if (delta) { metaManager.createRow(instance.toPhysicalRow(index), amount); var currentSelectedRange = selection.selectedRange.current(); var currentFromRange = currentSelectedRange === null || currentSelectedRange === void 0 ? void 0 : currentSelectedRange.from; var currentFromRow = currentFromRange === null || currentFromRange === void 0 ? void 0 : currentFromRange.row; // Moving down the selection (when it exist). It should be present on the "old" row. // TODO: The logic here should be handled by selection module. if (isDefined(currentFromRow) && currentFromRow >= index) { var _currentSelectedRange = currentSelectedRange.to, currentToRow = _currentSelectedRange.row, currentToColumn = _currentSelectedRange.col; var currentFromColumn = currentFromRange.col; // Workaround: headers are not stored inside selection. if (selection.isSelectedByRowHeader()) { currentFromColumn = -1; } // Remove from the stack the last added selection as that selection below will be // replaced by new transformed selection. selection.getSelectedRange().pop(); // I can't use transforms as they don't work in negative indexes. selection.setRangeStartOnly(instance._createCellCoords(currentFromRow + delta, currentFromColumn), true); selection.setRangeEnd(instance._createCellCoords(currentToRow + delta, currentToColumn)); // will call render() internally } else { instance._refreshBorders(); // it will call render and prepare methods } } break; case 'insert_col': delta = datamap.createCol(index, amount, source); if (delta) { metaManager.createColumn(instance.toPhysicalColumn(index), amount); if (Array.isArray(tableMeta.colHeaders)) { var spliceArray = [index, 0]; spliceArray.length += delta; // 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 } var _currentSelectedRange2 = selection.selectedRange.current(); var _currentFromRange = _currentSelectedRange2 === null || _currentSelectedRange2 === void 0 ? void 0 : _currentSelectedRange2.from; var _currentFromColumn = _currentFromRange === null || _currentFromRange === void 0 ? void 0 : _currentFromRange.col; // Moving right the selection (when it exist). It should be present on the "old" row. // TODO: The logic here should be handled by selection module. if (isDefined(_currentFromColumn) && _currentFromColumn >= index) { var _currentSelectedRange3 = _currentSelectedRange2.to, _currentToRow = _currentSelectedRange3.row, _currentToColumn = _currentSelectedRange3.col; var _currentFromRow = _currentFromRange.row; // Workaround: headers are not stored inside selection. if (selection.isSelectedByColumnHeader()) { _currentFromRow = -1; } // Remove from the stack the last added selection as that selection below will be // replaced by new transformed selection. selection.getSelectedRange().pop(); // I can't use transforms as they don't work in negative indexes. selection.setRangeStartOnly(instance._createCellCoords(_currentFromRow, _currentFromColumn + delta), true); selection.setRangeEnd(instance._createCellCoords(_currentToRow, _currentToColumn + delta)); // will call render() internally } else { instance._refreshBorders(); // it will call render and prepare methods } } break; case 'remove_row': var removeRow = function removeRow(indexes) { var offset = 0; // Normalize the {index, amount} groups into bigger groups. arrayEach(indexes, function (_ref8) { var _ref9 = _slicedToArray(_ref8, 2), groupIndex = _ref9[0], groupAmount = _ref9[1]; var 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. var wasRemoved = datamap.removeRow(groupIndex, groupAmount, source); if (!wasRemoved) { return; } metaManager.removeRow(instance.toPhysicalRow(calcIndex), groupAmount); var totalRows = instance.countRows(); var fixedRowsTop = tableMeta.fixedRowsTop; if (fixedRowsTop >= calcIndex + 1) { tableMeta.fixedRowsTop -= Math.min(groupAmount, fixedRowsTop - calcIndex); } var fixedRowsBottom = tableMeta.fixedRowsBottom; if (fixedRowsBottom && calcIndex >= totalRows - fixedRowsBottom) { tableMeta.fixedRowsBottom -= Math.min(groupAmount, fixedRowsBottom); } offset += groupAmount; }); }; if (Array.isArray(index)) { removeRow(normalizeIndexesGroup(index)); } else { removeRow([[index, amount]]); } grid.adjustRowsAndCols(); instance._refreshBorders(); // it will call render and prepare methods break; case 'remove_col': var removeCol = function removeCol(indexes) { var offset = 0; // Normalize the {index, amount} groups into bigger groups. arrayEach(indexes, function (_ref10) { var _ref11 = _slicedToArray(_ref10, 2), groupIndex = _ref11[0], groupAmount = _ref11[1]; var calcIndex = isEmpty(groupIndex) ? instance.countCols() - 1 : Math.max(groupIndex - offset, 0); var 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. var wasRemoved = datamap.removeCol(groupIndex, groupAmount, source); if (!wasRemoved) { return; } metaManager.removeColumn(physicalColumnIndex, groupAmount); var 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]]); } grid.adjustRowsAndCols(); instance._refreshBorders(); // it will call render and prepare methods break; default: throw new Error("There is no such action \"".concat(action, "\"")); } if (!keepEmptyRows) { grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh } }, /** * Makes sure there are empty rows at the bottom of the table. * * @private */ adjustRowsAndCols: function adjustRowsAndCols() { var minRows = tableMeta.minRows; var minSpareRows = tableMeta.minSpareRows; var minCols = tableMeta.minCols; var minSpareCols = tableMeta.minSpareCols; if (minRows) { // should I add empty rows to data source to meet minRows? var 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, 'auto'); } } if (minSpareRows) { var emptyRows = instance.countEmptyRows(true); // should I add empty rows to meet minSpareRows? if (emptyRows < minSpareRows) { var emptyRowsMissing = minSpareRows - emptyRows; var 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, 'auto'); } } { var emptyCols; // count currently empty cols if (minCols || minSpareCols) { emptyCols = instance.countEmptyCols(true); } var 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 `minSpareRows` option, // we don't want to touch/shift cell meta objects. var colsToCreate = minCols - nrOfColumns; emptyCols += colsToCreate; datamap.createCol(nrOfColumns, colsToCreate, 'auto'); } // should I add empty cols to meet minSpareCols? if (minSpareCols && !tableMeta.columns && instance.dataType === 'array' && emptyCols < minSpareCols) { nrOfColumns = instance.countCols(); var emptyColsMissing = minSpareCols - emptyCols; var _colsToCreate = Math.min(emptyColsMissing, tableMeta.maxCols - nrOfColumns); // The synchronization with cell meta is not desired here. For `minSpareRows` option, // we don't want to touch/shift cell meta objects. datamap.createCol(nrOfColumns, _colsToCreate, 'auto'); } } var rowCount = instance.countRows(); var colCount = instance.countCols(); if (rowCount === 0 || colCount === 0) { selection.deselect(); } if (selection.isSelected()) { arrayEach(selection.selectedRange, function (range) { var selectionChanged = false; var fromRow = range.from.row; var fromCol = range.from.col; var toRow = range.to.row; var toCol = range.to.col; // if selection is outside, move selection to last row if (fromRow > rowCount - 1) { fromRow = rowCount - 1; selectionChanged = true; if (toRow > fromRow) { toRow = fromRow; } } else if (toRow > rowCount - 1) { toRow = rowCount - 1; selectionChanged = true; if (fromRow > toRow) { fromRow = toRow; } } // if selection is outside, move selection to last row if (fromCol > colCount - 1) { fromCol = colCount - 1; selectionChanged = true; if (toCol > fromCol) { toCol = fromCol; } } else if (toCol > colCount - 1) { toCol = colCount - 1; selectionChanged = true; if (fromCol > toCol) { fromCol = toCol; } } if (selectionChanged) { instance.selectCell(fromRow, fromCol, toRow, toCol); } }); } if (instance.view) { instance.view.adjustElementsSize(); } }, /** * 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`. * @param {string} direction (left|right|up|down) String specifying the direction. * @param {Array} deltas The deltas array. A difference between values of adjacent cells. * Useful **only** when the type of handled cells is `numeric`. * @returns {object|undefined} Ending td in pasted area (only if any cell was changed). */ populateFromArray: function populateFromArray(start, input, end, source, method, direction, deltas) { // TODO: either remove or implement the `direction` argument. Currently it's not working at all. var r; var rlen; var c; var clen; var setData = []; var current = {}; var newDataByColumns = []; var startRow = start.row; var startColumn = start.col; rlen = input.length; if (rlen === 0) { return false; } var columnsPopulationEnd = 0; var 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 var populatedDataByColumns = pivot(input); var numberOfDataColumns = populatedDataByColumns.length; // method's argument can extend the range of data population (data would be repeated) var numberOfColumnsToPopulate = Math.max(numberOfDataColumns, columnsPopulationEnd); var pushedDownDataByRows = instance.getData().slice(startRow); // translate data from a list of rows to a list of columns var 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': var numberOfDataRows = input.length; // method's argument can extend the range of data population (data would be repeated) var numberOfRowsToPopulate = Math.max(numberOfDataRows, rowsPopulationEnd); var pushedRightDataByRows = instance.getData().slice(startRow).map(function (rowData) { return 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 (var i = 0; i < pushedRightDataByRows[r].length; i += 1) { input[r].push(pushedRightDataByRows[r][i]); } } else { var _input$r; // if before data population, there was no data in the row // we fill the required columns' newly-created cells with `null` values (_input$r = input[r]).push.apply(_input$r, _toConsumableArray(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; var selected = { // selected range row: end && start ? end.row - start.row + 1 : 1, col: end && start ? end.col - start.col + 1 : 1 }; var skippedRow = 0; var skippedColumn = 0; var pushData = true; var cellMeta; var getInputValue = function getInputValue(row) { var col = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var rowValue = input[row % input.length]; if (col !== null) { return rowValue[col % rowValue.length]; } return rowValue; }; var rowInputLength = input.length; var 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; } var visualRow = r - skippedRow; var colInputLength = getInputValue(visualRow).length; var 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++) { 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; } var visualColumn = c - skippedColumn; var value = getInputValue(visualRow, visualColumn); var orgValue = instance.getDataAtCell(current.row, current.col); var index = { row: visualRow, col: visualColumn }; if (source === 'Autofill.fill') { var result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, {}, selected); if (result) { value = isUndefined(result.value) ? value : result.value; } } 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 (orgValue === null || _typeof(orgValue) !== 'object') { pushData = false; } else { var orgValueSchema = duckSchema(Array.isArray(orgValue) ? orgValue : orgValue[0] || orgValue); var valueSchema = duckSchema(Array.isArray(value) ? value : value[0] || value); /* eslint-disable max-depth */ if (isObjectEqual(orgValueSchema, valueSchema)) { value = deepClone(value); } else { pushData = false; } } } else if (orgValue !== null && _typeof(orgValue) === 'object') { pushData = false; } if (pushData) { setData.push([current.row, current.col, value]); } pushData = true; current.col += 1; } current.row += 1; } instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); break; } } }; /** * Internal function to set `language` key of settings. * * @private * @param {string} languageCode Language code for specific language i.e. 'en-US', 'pt-BR', 'de-DE'. * @fires Hooks#afterLanguageChange */ function setLanguage(languageCode) { var normalizedLanguageCode = normalizeLanguageCode(languageCode); if (hasLanguageDictionary(normalizedLanguageCode)) { instance.runHooks('beforeLanguageChange', normalizedLanguageCode); globalMeta.language = normalizedLanguageCode; instance.runHooks('afterLanguageChange', normalizedLanguageCode); } else { warnUserAboutLanguageRegistration(languageCode); } } /** * Internal function to set `className` or `tableClassName`, depending on the key from the settings object. * * @private * @param {string} className `className` or `tableClassName` from the key in the settings object. * @param {string|string[]} classSettings String or array of strings. Contains class name(s) from settings object. */ function setClassName(className, classSettings) { var element = className === 'className' ? instance.rootElement : instance.table; if (firstRun) { addClass(element, classSettings); } else { var globalMetaSettingsArray = []; var settingsArray = []; if (globalMeta[className]) { globalMetaSettingsArray = Array.isArray(globalMeta[className]) ? globalMeta[className] : stringToArray(globalMeta[className]); } if (classSettings) { settingsArray = Array.isArray(classSettings) ? classSettings : stringToArray(classSettings); } var classNameToRemove = getDifferenceOfArrays(globalMetaSettingsArray, settingsArray); var classNameToAdd = getDifferenceOfArrays(settingsArray, globalMetaSettingsArray); if (classNameToRemove.length) { removeClass(element, classNameToRemove); } if (classNameToAdd.length) { addClass(element, classNameToAdd); } } globalMeta[className] = classSettings; } this.init = function () { dataSource.setData(tableMeta.data); instance.runHooks('beforeInit'); if (isMobileBrowser() || isIpadOS()) { addClass(instance.rootElement, 'mobile'); } this.updateSettings(tableMeta, true); this.view = new TableView(this); editorManager = EditorManager.getInstance(instance, tableMeta, selection); instance.runHooks('init'); this.forceFullRender = true; // used when data was changed this.view.render(); if (_typeof(firstRun) === 'object') { instance.runHooks('afterChange', firstRun[0], firstRun[1]); firstRun = false; } instance.runHooks('afterInit'); }; /** * @ignore * @returns {object} */ function ValidatorsQueue() { // moved this one level up so it can be used in any function here. Probably this should be moved to a separate file var resolved = false; return { validatorsInQueue: 0, valid: true, addValidatorToQueue: function addValidatorToQueue() { this.validatorsInQueue += 1; resolved = false; }, removeValidatorFormQueue: function removeValidatorFormQueue() { this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1; this.checkIfQueueIsEmpty(); }, onQueueEmpty: function onQueueEmpty() {}, checkIfQueueIsEmpty: function checkIfQueueIsEmpty() { if (this.validatorsInQueue === 0 && resolved === false) { resolved = true; this.onQueueEmpty(this.valid); } } }; } /** * Get parsed number from numeric string. * * @private * @param {string} numericData Float (separated by a dot or a comma) or integer. * @returns {number} Number if we get data in parsable format, not changed value otherwise. */ function getParsedNumber(numericData) { // Unifying "float like" string. Change from value with comma determiner to value with dot determiner, // for example from `450,65` to `450.65`. var unifiedNumericData = numericData.replace(',', '.'); if (isNaN(parseFloat(unifiedNumericData)) === false) { return parseFloat(unifiedNumericData); } return numericData; } /** * @ignore * @param {Array} changes The 2D array containing information about each of the edited cells. * @param {string} source The string that identifies source of validation. * @param {Function} callback The callback function fot async validation. */ function validateChanges(changes, source, callback) { if (!changes.length) { return; } var activeEditor = instance.getActiveEditor(); var beforeChangeResult = instance.runHooks('beforeChange', changes, source || 'edit'); v