handsontable
Version:
Handsontable is a JavaScript Spreadsheet Component available for React, Angular and Vue.
1,245 lines (1,005 loc) • 177 kB
JavaScript
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