UNPKG

186 kBJavaScriptView Raw
1import "core-js/modules/es.error.cause.js";
2import "core-js/modules/es.array.push.js";
3import "core-js/modules/es.set.difference.v2.js";
4import "core-js/modules/es.set.intersection.v2.js";
5import "core-js/modules/es.set.is-disjoint-from.v2.js";
6import "core-js/modules/es.set.is-subset-of.v2.js";
7import "core-js/modules/es.set.is-superset-of.v2.js";
8import "core-js/modules/es.set.symmetric-difference.v2.js";
9import "core-js/modules/es.set.union.v2.js";
10import "core-js/modules/web.immediate.js";
11import { addClass, empty, observeVisibilityChangeOnce, removeClass } from "./helpers/dom/element.mjs";
12import { isFunction } from "./helpers/function.mjs";
13import { isDefined, isUndefined, isRegExp, _injectProductInfo, isEmpty } from "./helpers/mixed.mjs";
14import { isMobileBrowser, isIpadOS } from "./helpers/browser.mjs";
15import EditorManager from "./editorManager.mjs";
16import EventManager from "./eventManager.mjs";
17import { deepClone, duckSchema, isObjectEqual, isObject, deepObjectSize, hasOwnProperty, createObjectPropListener, objectEach } from "./helpers/object.mjs";
18import { FocusManager } from "./focusManager.mjs";
19import { arrayMap, arrayEach, arrayReduce, getDifferenceOfArrays, stringToArray, pivot } from "./helpers/array.mjs";
20import { instanceToHTML } from "./utils/parseTable.mjs";
21import { getPlugin, getPluginsNames } from "./plugins/registry.mjs";
22import { getRenderer } from "./renderers/registry.mjs";
23import { getEditor } from "./editors/registry.mjs";
24import { getValidator } from "./validators/registry.mjs";
25import { randomString, toUpperCaseFirst } from "./helpers/string.mjs";
26import { rangeEach, rangeEachReverse, isNumericLike } from "./helpers/number.mjs";
27import TableView from "./tableView.mjs";
28import DataSource from "./dataMap/dataSource.mjs";
29import { spreadsheetColumnLabel } from "./helpers/data.mjs";
30import { IndexMapper } from "./translations/index.mjs";
31import { registerAsRootInstance, hasValidParameter, isRootInstance } from "./utils/rootInstance.mjs";
32import { DEFAULT_COLUMN_WIDTH } from "./3rdparty/walkontable/src/index.mjs";
33import Hooks from "./pluginHooks.mjs";
34import { hasLanguageDictionary, getValidLanguageCode, getTranslatedPhrase } from "./i18n/registry.mjs";
35import { warnUserAboutLanguageRegistration, normalizeLanguageCode } from "./i18n/utils.mjs";
36import { Selection } from "./selection/index.mjs";
37import { MetaManager, DynamicCellMetaMod, ExtendMetaPropertiesMod, replaceData } from "./dataMap/index.mjs";
38import { installFocusCatcher, createViewportScroller } from "./core/index.mjs";
39import { createUniqueMap } from "./utils/dataStructures/uniqueMap.mjs";
40import { createShortcutManager } from "./shortcuts/index.mjs";
41import { registerAllShortcutContexts } from "./shortcutContexts/index.mjs";
42let activeGuid = null;
43
44/**
45 * Keeps the collection of the all Handsontable instances created on the same page. The
46 * list is then used to trigger the "afterUnlisten" hook when the "listen()" method was
47 * called on another instance.
48 *
49 * @type {Map<string, Core>}
50 */
51const foreignHotInstances = new Map();
52
53/**
54 * A set of deprecated feature names.
55 *
56 * @type {Set<string>}
57 */
58// eslint-disable-next-line no-unused-vars
59const deprecationWarns = new Set();
60
61/* eslint-disable jsdoc/require-description-complete-sentence */
62/**
63 * Handsontable constructor.
64 *
65 * @core
66 * @class Core
67 * @description
68 *
69 * The `Handsontable` class (known as the `Core`) lets you modify the grid's behavior by using Handsontable's public API methods.
70 *
71 * ::: only-for react
72 * To use these methods, associate a Handsontable instance with your instance
73 * of the [`HotTable` component](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component),
74 * by using React's `ref` feature (read more on the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page).
75 * :::
76 *
77 * ## How to call a method
78 *
79 * ::: only-for javascript
80 * ```js
81 * // create a Handsontable instance
82 * const hot = new Handsontable(document.getElementById('example'), options);
83 *
84 * // call a method
85 * hot.setDataAtCell(0, 0, 'new value');
86 * ```
87 * :::
88 *
89 * ::: only-for react
90 * ```jsx
91 * import { useRef } from 'react';
92 *
93 * const hotTableComponent = useRef(null);
94 *
95 * <HotTable
96 * // associate your `HotTable` component with a Handsontable instance
97 * ref={hotTableComponent}
98 * settings={options}
99 * />
100 *
101 * // access the Handsontable instance, under the `.current.hotInstance` property
102 * // call a method
103 * hotTableComponent.current.hotInstance.setDataAtCell(0, 0, 'new value');
104 * ```
105 * :::
106 *
107 * @param {HTMLElement} rootElement The element to which the Handsontable instance is injected.
108 * @param {object} userSettings The user defined options.
109 * @param {boolean} [rootInstanceSymbol=false] Indicates if the instance is root of all later instances created.
110 */
111export default function Core(rootElement, userSettings) {
112 var _userSettings$layoutD,
113 _this = this;
114 let rootInstanceSymbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
115 let instance = this;
116 const eventManager = new EventManager(instance);
117 let datamap;
118 let dataSource;
119 let grid;
120 let editorManager;
121 let focusManager;
122 let viewportScroller;
123 let firstRun = true;
124 if (hasValidParameter(rootInstanceSymbol)) {
125 registerAsRootInstance(this);
126 }
127
128 // TODO: check if references to DOM elements should be move to UI layer (Walkontable)
129 /**
130 * Reference to the container element.
131 *
132 * @private
133 * @type {HTMLElement}
134 */
135 this.rootElement = rootElement;
136 /**
137 * The nearest document over container.
138 *
139 * @private
140 * @type {Document}
141 */
142 this.rootDocument = rootElement.ownerDocument;
143 /**
144 * Window object over container's document.
145 *
146 * @private
147 * @type {Window}
148 */
149 this.rootWindow = this.rootDocument.defaultView;
150 /**
151 * A boolean to tell if the Handsontable has been fully destroyed. This is set to `true`
152 * after `afterDestroy` hook is called.
153 *
154 * @memberof Core#
155 * @member isDestroyed
156 * @type {boolean}
157 */
158 this.isDestroyed = false;
159 /**
160 * The counter determines how many times the render suspending was called. It allows
161 * tracking the nested suspending calls. For each render suspend resuming call the
162 * counter is decremented. The value equal to 0 means the render suspending feature
163 * is disabled.
164 *
165 * @private
166 * @type {number}
167 */
168 this.renderSuspendedCounter = 0;
169 /**
170 * The counter determines how many times the execution suspending was called. It allows
171 * tracking the nested suspending calls. For each execution suspend resuming call the
172 * counter is decremented. The value equal to 0 means the execution suspending feature
173 * is disabled.
174 *
175 * @private
176 * @type {number}
177 */
178 this.executionSuspendedCounter = 0;
179 const layoutDirection = (_userSettings$layoutD = userSettings === null || userSettings === void 0 ? void 0 : userSettings.layoutDirection) !== null && _userSettings$layoutD !== void 0 ? _userSettings$layoutD : 'inherit';
180 const rootElementDirection = ['rtl', 'ltr'].includes(layoutDirection) ? layoutDirection : this.rootWindow.getComputedStyle(this.rootElement).direction;
181 this.rootElement.setAttribute('dir', rootElementDirection);
182
183 /**
184 * Checks if the grid is rendered using the right-to-left layout direction.
185 *
186 * @since 12.0.0
187 * @memberof Core#
188 * @function isRtl
189 * @returns {boolean} True if RTL.
190 */
191 this.isRtl = function () {
192 return rootElementDirection === 'rtl';
193 };
194
195 /**
196 * Checks if the grid is rendered using the left-to-right layout direction.
197 *
198 * @since 12.0.0
199 * @memberof Core#
200 * @function isLtr
201 * @returns {boolean} True if LTR.
202 */
203 this.isLtr = function () {
204 return !instance.isRtl();
205 };
206
207 /**
208 * Returns 1 for LTR; -1 for RTL. Useful for calculations.
209 *
210 * @since 12.0.0
211 * @memberof Core#
212 * @function getDirectionFactor
213 * @returns {number} Returns 1 for LTR; -1 for RTL.
214 */
215 this.getDirectionFactor = function () {
216 return instance.isLtr() ? 1 : -1;
217 };
218 userSettings.language = getValidLanguageCode(userSettings.language);
219 const metaManager = new MetaManager(instance, userSettings, [DynamicCellMetaMod, ExtendMetaPropertiesMod]);
220 const tableMeta = metaManager.getTableMeta();
221 const globalMeta = metaManager.getGlobalMeta();
222 const pluginsRegistry = createUniqueMap();
223 this.container = this.rootDocument.createElement('div');
224 this.renderCall = false;
225 rootElement.insertBefore(this.container, rootElement.firstChild);
226 if (isRootInstance(this)) {
227 _injectProductInfo(userSettings.licenseKey, rootElement);
228 }
229 this.guid = `ht_${randomString()}`; // this is the namespace for global events
230
231 foreignHotInstances.set(this.guid, this);
232
233 /**
234 * Instance of index mapper which is responsible for managing the column indexes.
235 *
236 * @memberof Core#
237 * @member columnIndexMapper
238 * @type {IndexMapper}
239 */
240 this.columnIndexMapper = new IndexMapper();
241 /**
242 * Instance of index mapper which is responsible for managing the row indexes.
243 *
244 * @memberof Core#
245 * @member rowIndexMapper
246 * @type {IndexMapper}
247 */
248 this.rowIndexMapper = new IndexMapper();
249 this.columnIndexMapper.addLocalHook('indexesSequenceChange', source => {
250 instance.runHooks('afterColumnSequenceChange', source);
251 });
252 this.rowIndexMapper.addLocalHook('indexesSequenceChange', source => {
253 instance.runHooks('afterRowSequenceChange', source);
254 });
255 dataSource = new DataSource(instance);
256 if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === 'ht_') {
257 this.rootElement.id = this.guid; // if root element does not have an id, assign a random id
258 }
259 const visualToRenderableCoords = coords => {
260 const {
261 row: visualRow,
262 col: visualColumn
263 } = coords;
264 return instance._createCellCoords(
265 // We just store indexes for rows and columns without headers.
266 visualRow >= 0 ? instance.rowIndexMapper.getRenderableFromVisualIndex(visualRow) : visualRow, visualColumn >= 0 ? instance.columnIndexMapper.getRenderableFromVisualIndex(visualColumn) : visualColumn);
267 };
268 const renderableToVisualCoords = coords => {
269 const {
270 row: renderableRow,
271 col: renderableColumn
272 } = coords;
273 return instance._createCellCoords(
274 // We just store indexes for rows and columns without headers.
275 renderableRow >= 0 ? instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow, renderableColumn >= 0 ? instance.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn) : renderableColumn // eslint-disable-line max-len
276 );
277 };
278 const findFirstNonHiddenRenderableRow = (visualRowFrom, visualRowTo) => {
279 const dir = visualRowTo > visualRowFrom ? 1 : -1;
280 const minIndex = Math.min(visualRowFrom, visualRowTo);
281 const maxIndex = Math.max(visualRowFrom, visualRowTo);
282 const rowIndex = instance.rowIndexMapper.getNearestNotHiddenIndex(visualRowFrom, dir);
283 if (rowIndex === null || dir === 1 && rowIndex > maxIndex || dir === -1 && rowIndex < minIndex) {
284 return null;
285 }
286 return rowIndex >= 0 ? instance.rowIndexMapper.getRenderableFromVisualIndex(rowIndex) : rowIndex;
287 };
288 const findFirstNonHiddenRenderableColumn = (visualColumnFrom, visualColumnTo) => {
289 const dir = visualColumnTo > visualColumnFrom ? 1 : -1;
290 const minIndex = Math.min(visualColumnFrom, visualColumnTo);
291 const maxIndex = Math.max(visualColumnFrom, visualColumnTo);
292 const columnIndex = instance.columnIndexMapper.getNearestNotHiddenIndex(visualColumnFrom, dir);
293 if (columnIndex === null || dir === 1 && columnIndex > maxIndex || dir === -1 && columnIndex < minIndex) {
294 return null;
295 }
296 return columnIndex >= 0 ? instance.columnIndexMapper.getRenderableFromVisualIndex(columnIndex) : columnIndex;
297 };
298 let selection = new Selection(tableMeta, {
299 rowIndexMapper: instance.rowIndexMapper,
300 columnIndexMapper: instance.columnIndexMapper,
301 countCols: () => instance.countCols(),
302 countRows: () => instance.countRows(),
303 propToCol: prop => datamap.propToCol(prop),
304 isEditorOpened: () => instance.getActiveEditor() ? instance.getActiveEditor().isOpened() : false,
305 countRenderableColumns: () => this.view.countRenderableColumns(),
306 countRenderableRows: () => this.view.countRenderableRows(),
307 countRowHeaders: () => this.countRowHeaders(),
308 countColHeaders: () => this.countColHeaders(),
309 countRenderableRowsInRange: function () {
310 return _this.view.countRenderableRowsInRange(...arguments);
311 },
312 countRenderableColumnsInRange: function () {
313 return _this.view.countRenderableColumnsInRange(...arguments);
314 },
315 getShortcutManager: () => instance.getShortcutManager(),
316 createCellCoords: (row, column) => instance._createCellCoords(row, column),
317 createCellRange: (highlight, from, to) => instance._createCellRange(highlight, from, to),
318 visualToRenderableCoords,
319 renderableToVisualCoords,
320 findFirstNonHiddenRenderableRow,
321 findFirstNonHiddenRenderableColumn,
322 isDisabledCellSelection: (visualRow, visualColumn) => {
323 if (visualRow < 0 || visualColumn < 0) {
324 return instance.getSettings().disableVisualSelection;
325 }
326 return instance.getCellMeta(visualRow, visualColumn).disableVisualSelection;
327 }
328 });
329 this.selection = selection;
330 const onIndexMapperCacheUpdate = _ref => {
331 let {
332 hiddenIndexesChanged
333 } = _ref;
334 if (hiddenIndexesChanged) {
335 this.selection.commit();
336 }
337 };
338 this.columnIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
339 this.rowIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
340 this.selection.addLocalHook('afterSetRangeEnd', (cellCoords, isLastSelectionLayer) => {
341 const preventScrolling = createObjectPropListener(false);
342 const selectionRange = this.selection.getSelectedRange();
343 const {
344 from,
345 to
346 } = selectionRange.current();
347 const selectionLayerLevel = selectionRange.size() - 1;
348 this.runHooks('afterSelection', from.row, from.col, to.row, to.col, preventScrolling, selectionLayerLevel);
349 this.runHooks('afterSelectionByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), preventScrolling, selectionLayerLevel);
350 if (isLastSelectionLayer && (!preventScrolling.isTouched() || preventScrolling.isTouched() && !preventScrolling.value)) {
351 viewportScroller.scrollTo(cellCoords);
352 }
353 const isSelectedByRowHeader = selection.isSelectedByRowHeader();
354 const isSelectedByColumnHeader = selection.isSelectedByColumnHeader();
355
356 // @TODO: These CSS classes are no longer needed anymore. They are used only as a indicator of the selected
357 // rows/columns in the MergedCells plugin (via border.js#L520 in the walkontable module). After fixing
358 // the Border class this should be removed.
359 if (isSelectedByRowHeader && isSelectedByColumnHeader) {
360 addClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
361 } else if (isSelectedByRowHeader) {
362 removeClass(this.rootElement, 'ht__selection--columns');
363 addClass(this.rootElement, 'ht__selection--rows');
364 } else if (isSelectedByColumnHeader) {
365 removeClass(this.rootElement, 'ht__selection--rows');
366 addClass(this.rootElement, 'ht__selection--columns');
367 } else {
368 removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
369 }
370 if (selection.getSelectionSource() !== 'shift') {
371 editorManager.closeEditor(null);
372 }
373 instance.view.render();
374 editorManager.prepareEditor();
375 });
376 this.selection.addLocalHook('beforeSetFocus', cellCoords => {
377 this.runHooks('beforeSelectionFocusSet', cellCoords.row, cellCoords.col);
378 });
379 this.selection.addLocalHook('afterSetFocus', cellCoords => {
380 const preventScrolling = createObjectPropListener(false);
381 this.runHooks('afterSelectionFocusSet', cellCoords.row, cellCoords.col, preventScrolling);
382 if (!preventScrolling.isTouched() || preventScrolling.isTouched() && !preventScrolling.value) {
383 viewportScroller.scrollTo(cellCoords);
384 }
385 editorManager.closeEditor();
386 instance.view.render();
387 editorManager.prepareEditor();
388 });
389 this.selection.addLocalHook('afterSelectionFinished', cellRanges => {
390 const selectionLayerLevel = cellRanges.length - 1;
391 const {
392 from,
393 to
394 } = cellRanges[selectionLayerLevel];
395 this.runHooks('afterSelectionEnd', from.row, from.col, to.row, to.col, selectionLayerLevel);
396 this.runHooks('afterSelectionEndByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), selectionLayerLevel);
397 });
398 this.selection.addLocalHook('afterIsMultipleSelection', isMultiple => {
399 const changedIsMultiple = this.runHooks('afterIsMultipleSelection', isMultiple.value);
400 if (isMultiple.value) {
401 isMultiple.value = changedIsMultiple;
402 }
403 });
404 this.selection.addLocalHook('afterDeselect', () => {
405 editorManager.closeEditor();
406 instance.view.render();
407 removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
408 this.runHooks('afterDeselect');
409 });
410 this.selection.addLocalHook('beforeHighlightSet', () => this.runHooks('beforeSelectionHighlightSet')).addLocalHook('beforeSetRangeStart', function () {
411 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
412 args[_key] = arguments[_key];
413 }
414 return _this.runHooks('beforeSetRangeStart', ...args);
415 }).addLocalHook('beforeSetRangeStartOnly', function () {
416 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
417 args[_key2] = arguments[_key2];
418 }
419 return _this.runHooks('beforeSetRangeStartOnly', ...args);
420 }).addLocalHook('beforeSetRangeEnd', function () {
421 for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
422 args[_key3] = arguments[_key3];
423 }
424 return _this.runHooks('beforeSetRangeEnd', ...args);
425 }).addLocalHook('beforeSelectColumns', function () {
426 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
427 args[_key4] = arguments[_key4];
428 }
429 return _this.runHooks('beforeSelectColumns', ...args);
430 }).addLocalHook('afterSelectColumns', function () {
431 for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
432 args[_key5] = arguments[_key5];
433 }
434 return _this.runHooks('afterSelectColumns', ...args);
435 }).addLocalHook('beforeSelectRows', function () {
436 for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
437 args[_key6] = arguments[_key6];
438 }
439 return _this.runHooks('beforeSelectRows', ...args);
440 }).addLocalHook('afterSelectRows', function () {
441 for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
442 args[_key7] = arguments[_key7];
443 }
444 return _this.runHooks('afterSelectRows', ...args);
445 }).addLocalHook('beforeModifyTransformStart', function () {
446 for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
447 args[_key8] = arguments[_key8];
448 }
449 return _this.runHooks('modifyTransformStart', ...args);
450 }).addLocalHook('afterModifyTransformStart', function () {
451 for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
452 args[_key9] = arguments[_key9];
453 }
454 return _this.runHooks('afterModifyTransformStart', ...args);
455 }).addLocalHook('beforeModifyTransformFocus', function () {
456 for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
457 args[_key10] = arguments[_key10];
458 }
459 return _this.runHooks('modifyTransformFocus', ...args);
460 }).addLocalHook('afterModifyTransformFocus', function () {
461 for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
462 args[_key11] = arguments[_key11];
463 }
464 return _this.runHooks('afterModifyTransformFocus', ...args);
465 }).addLocalHook('beforeModifyTransformEnd', function () {
466 for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
467 args[_key12] = arguments[_key12];
468 }
469 return _this.runHooks('modifyTransformEnd', ...args);
470 }).addLocalHook('afterModifyTransformEnd', function () {
471 for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
472 args[_key13] = arguments[_key13];
473 }
474 return _this.runHooks('afterModifyTransformEnd', ...args);
475 }).addLocalHook('beforeRowWrap', function () {
476 for (var _len14 = arguments.length, args = new Array(_len14), _key14 = 0; _key14 < _len14; _key14++) {
477 args[_key14] = arguments[_key14];
478 }
479 return _this.runHooks('beforeRowWrap', ...args);
480 }).addLocalHook('beforeColumnWrap', function () {
481 for (var _len15 = arguments.length, args = new Array(_len15), _key15 = 0; _key15 < _len15; _key15++) {
482 args[_key15] = arguments[_key15];
483 }
484 return _this.runHooks('beforeColumnWrap', ...args);
485 }).addLocalHook('insertRowRequire', totalRows => this.alter('insert_row_above', totalRows, 1, 'auto')).addLocalHook('insertColRequire', totalCols => this.alter('insert_col_start', totalCols, 1, 'auto'));
486 grid = {
487 /**
488 * Inserts or removes rows and columns.
489 *
490 * @private
491 * @param {string} action Possible values: "insert_row_above", "insert_row_below", "insert_col_start", "insert_col_end",
492 * "remove_row", "remove_col".
493 * @param {number|Array} index Row or column visual index which from the alter action will be triggered.
494 * Alter actions such as "remove_row" and "remove_col" support array indexes in the
495 * format `[[index, amount], [index, amount]...]` this can be used to remove
496 * non-consecutive columns or rows in one call.
497 * @param {number} [amount=1] Amount of rows or columns to remove.
498 * @param {string} [source] Optional. Source of hook runner.
499 * @param {boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows.
500 */
501 alter(action, index) {
502 var _index, _index2;
503 let amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
504 let source = arguments.length > 3 ? arguments[3] : undefined;
505 let keepEmptyRows = arguments.length > 4 ? arguments[4] : undefined;
506 const normalizeIndexesGroup = indexes => {
507 if (indexes.length === 0) {
508 return [];
509 }
510 const sortedIndexes = [...indexes];
511
512 // Sort the indexes in ascending order.
513 sortedIndexes.sort((_ref2, _ref3) => {
514 let [indexA] = _ref2;
515 let [indexB] = _ref3;
516 if (indexA === indexB) {
517 return 0;
518 }
519 return indexA > indexB ? 1 : -1;
520 });
521
522 // Normalize the {index, amount} groups into bigger groups.
523 const normalizedIndexes = arrayReduce(sortedIndexes, (acc, _ref4) => {
524 let [groupIndex, groupAmount] = _ref4;
525 const previousItem = acc[acc.length - 1];
526 const [prevIndex, prevAmount] = previousItem;
527 const prevLastIndex = prevIndex + prevAmount;
528 if (groupIndex <= prevLastIndex) {
529 const amountToAdd = Math.max(groupAmount - (prevLastIndex - groupIndex), 0);
530 previousItem[1] += amountToAdd;
531 } else {
532 acc.push([groupIndex, groupAmount]);
533 }
534 return acc;
535 }, [sortedIndexes[0]]);
536 return normalizedIndexes;
537 };
538
539 /* eslint-disable no-case-declarations */
540 switch (action) {
541 case 'insert_row_below':
542 case 'insert_row_above':
543 const numberOfSourceRows = instance.countSourceRows();
544 if (tableMeta.maxRows === numberOfSourceRows) {
545 return;
546 }
547
548 // `above` is the default behavior for creating new rows
549 const insertRowMode = action === 'insert_row_below' ? 'below' : 'above';
550
551 // Calling the `insert_row_above` action adds a new row at the beginning of the data set.
552 // eslint-disable-next-line no-param-reassign
553 index = (_index = index) !== null && _index !== void 0 ? _index : insertRowMode === 'below' ? numberOfSourceRows : 0;
554 const {
555 delta: rowDelta,
556 startPhysicalIndex: startRowPhysicalIndex
557 } = datamap.createRow(index, amount, {
558 source,
559 mode: insertRowMode
560 });
561 selection.shiftRows(instance.toVisualRow(startRowPhysicalIndex), rowDelta);
562 break;
563 case 'insert_col_start':
564 case 'insert_col_end':
565 // "start" is a default behavior for creating new columns
566 const insertColumnMode = action === 'insert_col_end' ? 'end' : 'start';
567
568 // Calling the `insert_col_start` action adds a new column to the left of the data set.
569 // eslint-disable-next-line no-param-reassign
570 index = (_index2 = index) !== null && _index2 !== void 0 ? _index2 : insertColumnMode === 'end' ? instance.countSourceCols() : 0;
571 const {
572 delta: colDelta,
573 startPhysicalIndex: startColumnPhysicalIndex
574 } = datamap.createCol(index, amount, {
575 source,
576 mode: insertColumnMode
577 });
578 if (colDelta) {
579 if (Array.isArray(tableMeta.colHeaders)) {
580 const spliceArray = [instance.toVisualColumn(startColumnPhysicalIndex), 0];
581 spliceArray.length += colDelta; // inserts empty (undefined) elements at the end of an array
582 Array.prototype.splice.apply(tableMeta.colHeaders, spliceArray); // inserts empty (undefined) elements into the colHeader array
583 }
584 selection.shiftColumns(instance.toVisualColumn(startColumnPhysicalIndex), colDelta);
585 }
586 break;
587 case 'remove_row':
588 const removeRow = indexes => {
589 let offset = 0;
590
591 // Normalize the {index, amount} groups into bigger groups.
592 arrayEach(indexes, _ref5 => {
593 let [groupIndex, groupAmount] = _ref5;
594 const calcIndex = isEmpty(groupIndex) ? instance.countRows() - 1 : Math.max(groupIndex - offset, 0);
595
596 // If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
597 // compatible with datamap.removeCol method.
598 if (Number.isInteger(groupIndex)) {
599 // eslint-disable-next-line no-param-reassign
600 groupIndex = Math.max(groupIndex - offset, 0);
601 }
602
603 // TODO: for datamap.removeRow index should be passed as it is (with undefined and null values). If not, the logic
604 // inside the datamap.removeRow breaks the removing functionality.
605 const wasRemoved = datamap.removeRow(groupIndex, groupAmount, source);
606 if (!wasRemoved) {
607 return;
608 }
609 if (selection.isSelected()) {
610 const {
611 row
612 } = instance.getSelectedRangeLast().highlight;
613 if (row >= groupIndex && row <= groupIndex + groupAmount - 1) {
614 editorManager.closeEditor(true);
615 }
616 }
617 const totalRows = instance.countRows();
618 if (totalRows === 0) {
619 selection.deselect();
620 } else if (source === 'ContextMenu.removeRow') {
621 selection.refresh();
622 } else {
623 selection.shiftRows(groupIndex, -groupAmount);
624 }
625 const fixedRowsTop = tableMeta.fixedRowsTop;
626 if (fixedRowsTop >= calcIndex + 1) {
627 tableMeta.fixedRowsTop -= Math.min(groupAmount, fixedRowsTop - calcIndex);
628 }
629 const fixedRowsBottom = tableMeta.fixedRowsBottom;
630 if (fixedRowsBottom && calcIndex >= totalRows - fixedRowsBottom) {
631 tableMeta.fixedRowsBottom -= Math.min(groupAmount, fixedRowsBottom);
632 }
633 offset += groupAmount;
634 });
635 };
636 if (Array.isArray(index)) {
637 removeRow(normalizeIndexesGroup(index));
638 } else {
639 removeRow([[index, amount]]);
640 }
641 break;
642 case 'remove_col':
643 const removeCol = indexes => {
644 let offset = 0;
645
646 // Normalize the {index, amount} groups into bigger groups.
647 arrayEach(indexes, _ref6 => {
648 let [groupIndex, groupAmount] = _ref6;
649 const calcIndex = isEmpty(groupIndex) ? instance.countCols() - 1 : Math.max(groupIndex - offset, 0);
650 let physicalColumnIndex = instance.toPhysicalColumn(calcIndex);
651
652 // If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
653 // compatible with datamap.removeCol method.
654 if (Number.isInteger(groupIndex)) {
655 // eslint-disable-next-line no-param-reassign
656 groupIndex = Math.max(groupIndex - offset, 0);
657 }
658
659 // TODO: for datamap.removeCol index should be passed as it is (with undefined and null values). If not, the logic
660 // inside the datamap.removeCol breaks the removing functionality.
661 const wasRemoved = datamap.removeCol(groupIndex, groupAmount, source);
662 if (!wasRemoved) {
663 return;
664 }
665 if (selection.isSelected()) {
666 const {
667 col
668 } = instance.getSelectedRangeLast().highlight;
669 if (col >= groupIndex && col <= groupIndex + groupAmount - 1) {
670 editorManager.closeEditor(true);
671 }
672 }
673 const totalColumns = instance.countCols();
674 if (totalColumns === 0) {
675 selection.deselect();
676 } else if (source === 'ContextMenu.removeColumn') {
677 selection.refresh();
678 } else {
679 selection.shiftColumns(groupIndex, -groupAmount);
680 }
681 const fixedColumnsStart = tableMeta.fixedColumnsStart;
682 if (fixedColumnsStart >= calcIndex + 1) {
683 tableMeta.fixedColumnsStart -= Math.min(groupAmount, fixedColumnsStart - calcIndex);
684 }
685 if (Array.isArray(tableMeta.colHeaders)) {
686 if (typeof physicalColumnIndex === 'undefined') {
687 physicalColumnIndex = -1;
688 }
689 tableMeta.colHeaders.splice(physicalColumnIndex, groupAmount);
690 }
691 offset += groupAmount;
692 });
693 };
694 if (Array.isArray(index)) {
695 removeCol(normalizeIndexesGroup(index));
696 } else {
697 removeCol([[index, amount]]);
698 }
699 break;
700 default:
701 throw new Error(`There is no such action "${action}"`);
702 }
703 instance.view.render();
704 if (!keepEmptyRows) {
705 grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh
706 }
707 },
708 /**
709 * Makes sure there are empty rows at the bottom of the table.
710 *
711 * @private
712 */
713 adjustRowsAndCols() {
714 const minRows = tableMeta.minRows;
715 const minSpareRows = tableMeta.minSpareRows;
716 const minCols = tableMeta.minCols;
717 const minSpareCols = tableMeta.minSpareCols;
718 if (minRows) {
719 // should I add empty rows to data source to meet minRows?
720 const nrOfRows = instance.countRows();
721 if (nrOfRows < minRows) {
722 // The synchronization with cell meta is not desired here. For `minRows` option,
723 // we don't want to touch/shift cell meta objects.
724 datamap.createRow(nrOfRows, minRows - nrOfRows, {
725 source: 'auto'
726 });
727 }
728 }
729 if (minSpareRows) {
730 const emptyRows = instance.countEmptyRows(true);
731
732 // should I add empty rows to meet minSpareRows?
733 if (emptyRows < minSpareRows) {
734 const emptyRowsMissing = minSpareRows - emptyRows;
735 const rowsToCreate = Math.min(emptyRowsMissing, tableMeta.maxRows - instance.countSourceRows());
736
737 // The synchronization with cell meta is not desired here. For `minSpareRows` option,
738 // we don't want to touch/shift cell meta objects.
739 datamap.createRow(instance.countRows(), rowsToCreate, {
740 source: 'auto'
741 });
742 }
743 }
744 {
745 let emptyCols;
746
747 // count currently empty cols
748 if (minCols || minSpareCols) {
749 emptyCols = instance.countEmptyCols(true);
750 }
751 let nrOfColumns = instance.countCols();
752
753 // should I add empty cols to meet minCols?
754 if (minCols && !tableMeta.columns && nrOfColumns < minCols) {
755 // The synchronization with cell meta is not desired here. For `minSpareRows` option,
756 // we don't want to touch/shift cell meta objects.
757 const colsToCreate = minCols - nrOfColumns;
758 emptyCols += colsToCreate;
759 datamap.createCol(nrOfColumns, colsToCreate, {
760 source: 'auto'
761 });
762 }
763 // should I add empty cols to meet minSpareCols?
764 if (minSpareCols && !tableMeta.columns && instance.dataType === 'array' && emptyCols < minSpareCols) {
765 nrOfColumns = instance.countCols();
766 const emptyColsMissing = minSpareCols - emptyCols;
767 const colsToCreate = Math.min(emptyColsMissing, tableMeta.maxCols - nrOfColumns);
768
769 // The synchronization with cell meta is not desired here. For `minSpareRows` option,
770 // we don't want to touch/shift cell meta objects.
771 datamap.createCol(nrOfColumns, colsToCreate, {
772 source: 'auto'
773 });
774 }
775 }
776 if (instance.view) {
777 instance.view.adjustElementsSize();
778 }
779 },
780 /**
781 * Populate the data from the provided 2d array from the given cell coordinates.
782 *
783 * @private
784 * @param {object} start Start selection position. Visual indexes.
785 * @param {Array} input 2d data array.
786 * @param {object} [end] End selection position (only for drag-down mode). Visual indexes.
787 * @param {string} [source="populateFromArray"] Source information string.
788 * @param {string} [method="overwrite"] Populate method. Possible options: `shift_down`, `shift_right`, `overwrite`.
789 * @returns {object|undefined} Ending td in pasted area (only if any cell was changed).
790 */
791 populateFromArray(start, input, end, source, method) {
792 let r;
793 let rlen;
794 let c;
795 let clen;
796 const setData = [];
797 const current = {};
798 const newDataByColumns = [];
799 const startRow = start.row;
800 const startColumn = start.col;
801 rlen = input.length;
802 if (rlen === 0) {
803 return false;
804 }
805 let columnsPopulationEnd = 0;
806 let rowsPopulationEnd = 0;
807 if (isObject(end)) {
808 columnsPopulationEnd = end.col - startColumn + 1;
809 rowsPopulationEnd = end.row - startRow + 1;
810 }
811
812 // insert data with specified pasteMode method
813 switch (method) {
814 case 'shift_down':
815 // translate data from a list of rows to a list of columns
816 const populatedDataByColumns = pivot(input);
817 const numberOfDataColumns = populatedDataByColumns.length;
818 // method's argument can extend the range of data population (data would be repeated)
819 const numberOfColumnsToPopulate = Math.max(numberOfDataColumns, columnsPopulationEnd);
820 const pushedDownDataByRows = instance.getData().slice(startRow);
821
822 // translate data from a list of rows to a list of columns
823 const pushedDownDataByColumns = pivot(pushedDownDataByRows).slice(startColumn, startColumn + numberOfColumnsToPopulate);
824 for (c = 0; c < numberOfColumnsToPopulate; c += 1) {
825 if (c < numberOfDataColumns) {
826 for (r = 0, rlen = populatedDataByColumns[c].length; r < rowsPopulationEnd - rlen; r += 1) {
827 // repeating data for rows
828 populatedDataByColumns[c].push(populatedDataByColumns[c][r % rlen]);
829 }
830 if (c < pushedDownDataByColumns.length) {
831 newDataByColumns.push(populatedDataByColumns[c].concat(pushedDownDataByColumns[c]));
832 } else {
833 // if before data population, there was no data in the column
834 // we fill the required rows' newly-created cells with `null` values
835 newDataByColumns.push(populatedDataByColumns[c].concat(new Array(pushedDownDataByRows.length).fill(null)));
836 }
837 } else {
838 // Repeating data for columns.
839 newDataByColumns.push(populatedDataByColumns[c % numberOfDataColumns].concat(pushedDownDataByColumns[c]));
840 }
841 }
842 instance.populateFromArray(startRow, startColumn, pivot(newDataByColumns));
843 break;
844 case 'shift_right':
845 const numberOfDataRows = input.length;
846 // method's argument can extend the range of data population (data would be repeated)
847 const numberOfRowsToPopulate = Math.max(numberOfDataRows, rowsPopulationEnd);
848 const pushedRightDataByRows = instance.getData().slice(startRow).map(rowData => rowData.slice(startColumn));
849 for (r = 0; r < numberOfRowsToPopulate; r += 1) {
850 if (r < numberOfDataRows) {
851 for (c = 0, clen = input[r].length; c < columnsPopulationEnd - clen; c += 1) {
852 // repeating data for rows
853 input[r].push(input[r][c % clen]);
854 }
855 if (r < pushedRightDataByRows.length) {
856 for (let i = 0; i < pushedRightDataByRows[r].length; i += 1) {
857 input[r].push(pushedRightDataByRows[r][i]);
858 }
859 } else {
860 // if before data population, there was no data in the row
861 // we fill the required columns' newly-created cells with `null` values
862 input[r].push(...new Array(pushedRightDataByRows[0].length).fill(null));
863 }
864 } else {
865 // Repeating data for columns.
866 input.push(input[r % rlen].slice(0, numberOfRowsToPopulate).concat(pushedRightDataByRows[r]));
867 }
868 }
869 instance.populateFromArray(startRow, startColumn, input);
870 break;
871 case 'overwrite':
872 default:
873 // overwrite and other not specified options
874 current.row = start.row;
875 current.col = start.col;
876 let skippedRow = 0;
877 let skippedColumn = 0;
878 let pushData = true;
879 let cellMeta;
880 const getInputValue = function getInputValue(row) {
881 let col = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
882 const rowValue = input[row % input.length];
883 if (col !== null) {
884 return rowValue[col % rowValue.length];
885 }
886 return rowValue;
887 };
888 const rowInputLength = input.length;
889 const rowSelectionLength = end ? end.row - start.row + 1 : 0;
890 if (end) {
891 rlen = rowSelectionLength;
892 } else {
893 rlen = Math.max(rowInputLength, rowSelectionLength);
894 }
895 for (r = 0; r < rlen; r++) {
896 if (end && current.row > end.row && rowSelectionLength > rowInputLength || !tableMeta.allowInsertRow && current.row > instance.countRows() - 1 || current.row >= tableMeta.maxRows) {
897 break;
898 }
899 const visualRow = r - skippedRow;
900 const colInputLength = getInputValue(visualRow).length;
901 const colSelectionLength = end ? end.col - start.col + 1 : 0;
902 if (end) {
903 clen = colSelectionLength;
904 } else {
905 clen = Math.max(colInputLength, colSelectionLength);
906 }
907 current.col = start.col;
908 cellMeta = instance.getCellMeta(current.row, current.col);
909 if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipRowOnPaste) {
910 skippedRow += 1;
911 current.row += 1;
912 rlen += 1;
913 /* eslint-disable no-continue */
914 continue;
915 }
916 skippedColumn = 0;
917 for (c = 0; c < clen; c++) {
918 if (end && current.col > end.col && colSelectionLength > colInputLength || !tableMeta.allowInsertColumn && current.col > instance.countCols() - 1 || current.col >= tableMeta.maxCols) {
919 break;
920 }
921 cellMeta = instance.getCellMeta(current.row, current.col);
922 if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipColumnOnPaste) {
923 skippedColumn += 1;
924 current.col += 1;
925 clen += 1;
926 continue;
927 }
928 if (cellMeta.readOnly && source !== 'UndoRedo.undo') {
929 current.col += 1;
930 /* eslint-disable no-continue */
931 continue;
932 }
933 const visualColumn = c - skippedColumn;
934 let value = getInputValue(visualRow, visualColumn);
935 let orgValue = instance.getDataAtCell(current.row, current.col);
936 if (value !== null && typeof value === 'object') {
937 // when 'value' is array and 'orgValue' is null, set 'orgValue' to
938 // an empty array so that the null value can be compared to 'value'
939 // as an empty value for the array context
940 if (Array.isArray(value) && orgValue === null) orgValue = [];
941 if (orgValue === null || typeof orgValue !== 'object') {
942 pushData = false;
943 } else {
944 const orgValueSchema = duckSchema(Array.isArray(orgValue) ? orgValue : orgValue[0] || orgValue);
945 const valueSchema = duckSchema(Array.isArray(value) ? value : value[0] || value);
946
947 // Allow overwriting values with the same object-based schema or any array-based schema.
948 if (isObjectEqual(orgValueSchema, valueSchema) || Array.isArray(orgValueSchema) && Array.isArray(valueSchema)) {
949 value = deepClone(value);
950 } else {
951 pushData = false;
952 }
953 }
954 } else if (orgValue !== null && typeof orgValue === 'object') {
955 pushData = false;
956 }
957 if (pushData) {
958 setData.push([current.row, current.col, value]);
959 }
960 pushData = true;
961 current.col += 1;
962 }
963 current.row += 1;
964 }
965 instance.setDataAtCell(setData, null, null, source || 'populateFromArray');
966 break;
967 }
968 }
969 };
970
971 /**
972 * Internal function to set `language` key of settings.
973 *
974 * @private
975 * @param {string} languageCode Language code for specific language i.e. 'en-US', 'pt-BR', 'de-DE'.
976 * @fires Hooks#afterLanguageChange
977 */
978 function setLanguage(languageCode) {
979 const normalizedLanguageCode = normalizeLanguageCode(languageCode);
980 if (hasLanguageDictionary(normalizedLanguageCode)) {
981 instance.runHooks('beforeLanguageChange', normalizedLanguageCode);
982 globalMeta.language = normalizedLanguageCode;
983 instance.runHooks('afterLanguageChange', normalizedLanguageCode);
984 } else {
985 warnUserAboutLanguageRegistration(languageCode);
986 }
987 }
988
989 /**
990 * Internal function to set `className` or `tableClassName`, depending on the key from the settings object.
991 *
992 * @private
993 * @param {string} className `className` or `tableClassName` from the key in the settings object.
994 * @param {string|string[]} classSettings String or array of strings. Contains class name(s) from settings object.
995 */
996 function setClassName(className, classSettings) {
997 const element = className === 'className' ? instance.rootElement : instance.table;
998 if (firstRun) {
999 addClass(element, classSettings);
1000 } else {
1001 let globalMetaSettingsArray = [];
1002 let settingsArray = [];
1003 if (globalMeta[className]) {
1004 globalMetaSettingsArray = Array.isArray(globalMeta[className]) ? globalMeta[className] : stringToArray(globalMeta[className]);
1005 }
1006 if (classSettings) {
1007 settingsArray = Array.isArray(classSettings) ? classSettings : stringToArray(classSettings);
1008 }
1009 const classNameToRemove = getDifferenceOfArrays(globalMetaSettingsArray, settingsArray);
1010 const classNameToAdd = getDifferenceOfArrays(settingsArray, globalMetaSettingsArray);
1011 if (classNameToRemove.length) {
1012 removeClass(element, classNameToRemove);
1013 }
1014 if (classNameToAdd.length) {
1015 addClass(element, classNameToAdd);
1016 }
1017 }
1018 globalMeta[className] = classSettings;
1019 }
1020 this.init = function () {
1021 dataSource.setData(tableMeta.data);
1022 instance.runHooks('beforeInit');
1023 if (isMobileBrowser() || isIpadOS()) {
1024 addClass(instance.rootElement, 'mobile');
1025 }
1026 this.updateSettings(tableMeta, true);
1027 this.view = new TableView(this);
1028 editorManager = EditorManager.getInstance(instance, tableMeta, selection);
1029 viewportScroller = createViewportScroller(instance);
1030 focusManager = new FocusManager(instance);
1031 if (isRootInstance(this)) {
1032 installFocusCatcher(instance);
1033 }
1034 instance.runHooks('init');
1035 this.forceFullRender = true; // used when data was changed
1036 this.view.render();
1037
1038 // Run the logic only if it's the table's initialization and the root element is not visible.
1039 if (!!firstRun && instance.rootElement.offsetParent === null) {
1040 observeVisibilityChangeOnce(instance.rootElement, () => {
1041 // Update the spreader size cache before rendering.
1042 instance.view._wt.wtOverlays.updateLastSpreaderSize();
1043 instance.render();
1044 instance.view.adjustElementsSize();
1045 });
1046 }
1047 if (typeof firstRun === 'object') {
1048 instance.runHooks('afterChange', firstRun[0], firstRun[1]);
1049 firstRun = false;
1050 }
1051 instance.runHooks('afterInit');
1052 };
1053
1054 /**
1055 * @ignore
1056 * @returns {object}
1057 */
1058 function ValidatorsQueue() {
1059 // moved this one level up so it can be used in any function here. Probably this should be moved to a separate file
1060 let resolved = false;
1061 return {
1062 validatorsInQueue: 0,
1063 valid: true,
1064 addValidatorToQueue() {
1065 this.validatorsInQueue += 1;
1066 resolved = false;
1067 },
1068 removeValidatorFormQueue() {
1069 this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1;
1070 this.checkIfQueueIsEmpty();
1071 },
1072 onQueueEmpty() {},
1073 checkIfQueueIsEmpty() {
1074 if (this.validatorsInQueue === 0 && resolved === false) {
1075 resolved = true;
1076 this.onQueueEmpty(this.valid);
1077 }
1078 }
1079 };
1080 }
1081
1082 /**
1083 * Get parsed number from numeric string.
1084 *
1085 * @private
1086 * @param {string} numericData Float (separated by a dot or a comma) or integer.
1087 * @returns {number} Number if we get data in parsable format, not changed value otherwise.
1088 */
1089 function getParsedNumber(numericData) {
1090 // Unifying "float like" string. Change from value with comma determiner to value with dot determiner,
1091 // for example from `450,65` to `450.65`.
1092 const unifiedNumericData = numericData.replace(',', '.');
1093 if (isNaN(parseFloat(unifiedNumericData)) === false) {
1094 return parseFloat(unifiedNumericData);
1095 }
1096 return numericData;
1097 }
1098
1099 /**
1100 * @ignore
1101 * @param {Array} changes The 2D array containing information about each of the edited cells.
1102 * @param {string} source The string that identifies source of validation.
1103 * @param {Function} callback The callback function fot async validation.
1104 */
1105 function validateChanges(changes, source, callback) {
1106 if (!changes.length) {
1107 callback();
1108 return;
1109 }
1110 const activeEditor = instance.getActiveEditor();
1111 const waitingForValidator = new ValidatorsQueue();
1112 let shouldBeCanceled = true;
1113 waitingForValidator.onQueueEmpty = () => {
1114 if (activeEditor && shouldBeCanceled) {
1115 activeEditor.cancelChanges();
1116 }
1117 callback(); // called when async validators are resolved and beforeChange was not async
1118 };
1119 for (let i = changes.length - 1; i >= 0; i--) {
1120 const [row, prop,, newValue] = changes[i];
1121 const visualCol = datamap.propToCol(prop);
1122 let cellProperties;
1123 if (Number.isInteger(visualCol)) {
1124 cellProperties = instance.getCellMeta(row, visualCol);
1125 } else {
1126 // If there's no requested visual column, we can use the table meta as the cell properties when retrieving
1127 // the cell validator.
1128 cellProperties = {
1129 ...Object.getPrototypeOf(tableMeta),
1130 ...tableMeta
1131 };
1132 }
1133 if (cellProperties.type === 'numeric' && typeof newValue === 'string' && isNumericLike(newValue)) {
1134 changes[i][3] = getParsedNumber(newValue);
1135 }
1136
1137 /* eslint-disable no-loop-func */
1138 if (instance.getCellValidator(cellProperties)) {
1139 waitingForValidator.addValidatorToQueue();
1140 instance.validateCell(changes[i][3], cellProperties, function (index, cellPropertiesReference) {
1141 return function (result) {
1142 if (typeof result !== 'boolean') {
1143 throw new Error('Validation error: result is not boolean');
1144 }
1145 if (result === false && cellPropertiesReference.allowInvalid === false) {
1146 shouldBeCanceled = false;
1147 changes.splice(index, 1); // cancel the change
1148 cellPropertiesReference.valid = true; // we cancelled the change, so cell value is still valid
1149 }
1150 waitingForValidator.removeValidatorFormQueue();
1151 };
1152 }(i, cellProperties), source);
1153 }
1154 }
1155 waitingForValidator.checkIfQueueIsEmpty();
1156 }
1157
1158 /**
1159 * Internal function to apply changes. Called after validateChanges.
1160 *
1161 * @private
1162 * @param {Array} changes Array in form of [row, prop, oldValue, newValue].
1163 * @param {string} source String that identifies how this change will be described in changes array (useful in onChange callback).
1164 * @fires Hooks#beforeChangeRender
1165 * @fires Hooks#afterChange
1166 */
1167 function applyChanges(changes, source) {
1168 for (let i = changes.length - 1; i >= 0; i--) {
1169 let skipThisChange = false;
1170 if (changes[i] === null) {
1171 changes.splice(i, 1);
1172 /* eslint-disable no-continue */
1173 continue;
1174 }
1175 if ((changes[i][2] === null || changes[i][2] === undefined) && (changes[i][3] === null || changes[i][3] === undefined)) {
1176 /* eslint-disable no-continue */
1177 continue;
1178 }
1179 if (tableMeta.allowInsertRow) {
1180 while (changes[i][0] > instance.countRows() - 1) {
1181 const {
1182 delta: numberOfCreatedRows
1183 } = datamap.createRow(undefined, undefined, {
1184 source
1185 });
1186 if (numberOfCreatedRows === 0) {
1187 skipThisChange = true;
1188 break;
1189 }
1190 }
1191 }
1192 if (instance.dataType === 'array' && (!tableMeta.columns || tableMeta.columns.length === 0) && tableMeta.allowInsertColumn) {
1193 while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) {
1194 const {
1195 delta: numberOfCreatedColumns
1196 } = datamap.createCol(undefined, undefined, {
1197 source
1198 });
1199 if (numberOfCreatedColumns === 0) {
1200 skipThisChange = true;
1201 break;
1202 }
1203 }
1204 }
1205 if (skipThisChange) {
1206 /* eslint-disable no-continue */
1207 continue;
1208 }
1209 datamap.set(changes[i][0], changes[i][1], changes[i][3]);
1210 }
1211 const hasChanges = changes.length > 0;
1212 instance.forceFullRender = true; // used when data was changed or when all cells need to be re-rendered
1213
1214 if (hasChanges) {
1215 grid.adjustRowsAndCols();
1216 instance.runHooks('beforeChangeRender', changes, source);
1217 editorManager.closeEditor();
1218 instance.view.render();
1219 editorManager.prepareEditor();
1220 instance.view.adjustElementsSize();
1221 instance.runHooks('afterChange', changes, source || 'edit');
1222 const activeEditor = instance.getActiveEditor();
1223 if (activeEditor && isDefined(activeEditor.refreshValue)) {
1224 activeEditor.refreshValue();
1225 }
1226 } else {
1227 instance.view.render();
1228 }
1229 }
1230
1231 /**
1232 * Creates and returns the CellCoords object.
1233 *
1234 * @private
1235 * @memberof Core#
1236 * @function _createCellCoords
1237 * @param {number} row The row index.
1238 * @param {number} column The column index.
1239 * @returns {CellCoords}
1240 */
1241 this._createCellCoords = function (row, column) {
1242 return instance.view._wt.createCellCoords(row, column);
1243 };
1244
1245 /**
1246 * Creates and returns the CellRange object.
1247 *
1248 * @private
1249 * @memberof Core#
1250 * @function _createCellRange
1251 * @param {CellCoords} highlight Defines the border around a cell where selection was started and to edit the cell
1252 * when you press Enter. The highlight cannot point to headers (negative values).
1253 * @param {CellCoords} from Initial coordinates.
1254 * @param {CellCoords} to Final coordinates.
1255 * @returns {CellRange}
1256 */
1257 this._createCellRange = function (highlight, from, to) {
1258 return instance.view._wt.createCellRange(highlight, from, to);
1259 };
1260
1261 /**
1262 * Validate a single cell.
1263 *
1264 * @memberof Core#
1265 * @function validateCell
1266 * @param {string|number} value The value to validate.
1267 * @param {object} cellProperties The cell meta which corresponds with the value.
1268 * @param {Function} callback The callback function.
1269 * @param {string} source The string that identifies source of the validation.
1270 */
1271 this.validateCell = function (value, cellProperties, callback, source) {
1272 let validator = instance.getCellValidator(cellProperties);
1273
1274 // the `canBeValidated = false` argument suggests, that the cell passes validation by default.
1275 /**
1276 * @private
1277 * @function done
1278 * @param {boolean} valid Indicates if the validation was successful.
1279 * @param {boolean} [canBeValidated=true] Flag which controls the validation process.
1280 */
1281 function done(valid) {
1282 let canBeValidated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
1283 // Fixes GH#3903
1284 if (!canBeValidated || cellProperties.hidden === true) {
1285 callback(valid);
1286 return;
1287 }
1288 const col = cellProperties.visualCol;
1289 const row = cellProperties.visualRow;
1290 const td = instance.getCell(row, col, true);
1291 if (td && td.nodeName !== 'TH') {
1292 const renderableRow = instance.rowIndexMapper.getRenderableFromVisualIndex(row);
1293 const renderableColumn = instance.columnIndexMapper.getRenderableFromVisualIndex(col);
1294 instance.view._wt.getSetting('cellRenderer', renderableRow, renderableColumn, td);
1295 }
1296 callback(valid);
1297 }
1298 if (isRegExp(validator)) {
1299 validator = function (expression) {
1300 return function (cellValue, validatorCallback) {
1301 validatorCallback(expression.test(cellValue));
1302 };
1303 }(validator);
1304 }
1305 if (isFunction(validator)) {
1306 // eslint-disable-next-line no-param-reassign
1307 value = instance.runHooks('beforeValidate', value, cellProperties.visualRow, cellProperties.prop, source);
1308
1309 // To provide consistent behaviour, validation should be always asynchronous
1310 instance._registerImmediate(() => {
1311 validator.call(cellProperties, value, valid => {
1312 if (!instance) {
1313 return;
1314 }
1315 // eslint-disable-next-line no-param-reassign
1316 valid = instance.runHooks('afterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
1317 cellProperties.valid = valid;
1318 done(valid);
1319 instance.runHooks('postAfterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
1320 });
1321 });
1322 } else {
1323 // resolve callback even if validator function was not found
1324 instance._registerImmediate(() => {
1325 cellProperties.valid = true;
1326 done(cellProperties.valid, false);
1327 });
1328 }
1329 };
1330
1331 /**
1332 * @ignore
1333 * @param {number} row The visual row index.
1334 * @param {string|number} propOrCol The visual prop or column index.
1335 * @param {*} value The cell value.
1336 * @returns {Array}
1337 */
1338 function setDataInputToArray(row, propOrCol, value) {
1339 if (Array.isArray(row)) {
1340 // it's an array of changes
1341 return row;
1342 }
1343 return [[row, propOrCol, value]];
1344 }
1345
1346 /**
1347 * Process changes prepared for applying to the dataset (unifying list of changes, closing an editor - when needed,
1348 * calling a hook).
1349 *
1350 * @private
1351 * @param {Array} changes Array of changes in format `[[row, col, value],...]`.
1352 * @param {string} [source] String that identifies how this change will be described in the changes array (useful in afterChange or beforeChange callback). Set to 'edit' if left empty.
1353 * @returns {Array} List of changes finally applied to the dataset.
1354 */
1355 function processChanges(changes, source) {
1356 const activeEditor = instance.getActiveEditor();
1357 const beforeChangeResult = instance.runHooks('beforeChange', changes, source || 'edit');
1358 // The `beforeChange` hook could add a `null` for purpose of cancelling some dataset's change.
1359 const filteredChanges = changes.filter(change => change !== null);
1360 if (beforeChangeResult === false || filteredChanges.length === 0) {
1361 if (activeEditor) {
1362 activeEditor.cancelChanges();
1363 }
1364 return [];
1365 }
1366 return filteredChanges;
1367 }
1368
1369 /**
1370 * @description
1371 * Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
1372 * `[[row, col, value],...]` as the first argument.
1373 *
1374 * @memberof Core#
1375 * @function setDataAtCell
1376 * @param {number|Array} row Visual row index or array of changes in format `[[row, col, value],...]`.
1377 * @param {number} [column] Visual column index.
1378 * @param {string} [value] New value.
1379 * @param {string} [source] String that identifies how this change will be described in the changes array (useful in afterChange or beforeChange callback). Set to 'edit' if left empty.
1380 */
1381 this.setDataAtCell = function (row, column, value, source) {
1382 const input = setDataInputToArray(row, column, value);
1383 const changes = [];
1384 let changeSource = source;
1385 let i;
1386 let ilen;
1387 let prop;
1388 for (i = 0, ilen = input.length; i < ilen; i++) {
1389 if (typeof input[i] !== 'object') {
1390 throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter');
1391 }
1392 if (typeof input[i][1] !== 'number') {
1393 throw new Error('Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`'); // eslint-disable-line max-len
1394 }
1395 if (input[i][1] >= this.countCols()) {
1396 prop = input[i][1];
1397 } else {
1398 prop = datamap.colToProp(input[i][1]);
1399 }
1400 changes.push([input[i][0], prop, dataSource.getAtCell(this.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]);
1401 }
1402 if (!changeSource && typeof row === 'object') {
1403 changeSource = column;
1404 }
1405 const processedChanges = processChanges(changes, source);
1406 instance.runHooks('afterSetDataAtCell', processedChanges, changeSource);
1407 validateChanges(processedChanges, changeSource, () => {
1408 applyChanges(processedChanges, changeSource);
1409 });
1410 };
1411
1412 /**
1413 * @description
1414 * Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
1415 * `[[row, prop, value],...]` as the first argument.
1416 *
1417 * @memberof Core#
1418 * @function setDataAtRowProp
1419 * @param {number|Array} row Visual row index or array of changes in format `[[row, prop, value], ...]`.
1420 * @param {string} prop Property name or the source string (e.g. `'first.name'` or `'0'`).
1421 * @param {string} value Value to be set.
1422 * @param {string} [source] String that identifies how this change will be described in changes array (useful in onChange callback).
1423 */
1424 this.setDataAtRowProp = function (row, prop, value, source) {
1425 const input = setDataInputToArray(row, prop, value);
1426 const changes = [];
1427 let changeSource = source;
1428 let i;
1429 let ilen;
1430 for (i = 0, ilen = input.length; i < ilen; i++) {
1431 changes.push([input[i][0], input[i][1], dataSource.getAtCell(this.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]);
1432 }
1433
1434 // TODO: I don't think `prop` should be used as `changeSource` here, but removing it would be a breaking change.
1435 // We should remove it with the next major release.
1436 if (!changeSource && typeof row === 'object') {
1437 changeSource = prop;
1438 }
1439 const processedChanges = processChanges(changes, source);
1440 instance.runHooks('afterSetDataAtRowProp', processedChanges, changeSource);
1441 validateChanges(processedChanges, changeSource, () => {
1442 applyChanges(processedChanges, changeSource);
1443 });
1444 };
1445
1446 /**
1447 * Listen to the keyboard input on document body. This allows Handsontable to capture keyboard events and respond
1448 * in the right way.
1449 *
1450 * @memberof Core#
1451 * @function listen
1452 * @fires Hooks#afterListen
1453 */
1454 this.listen = function () {
1455 if (instance && !instance.isListening()) {
1456 foreignHotInstances.forEach(foreignHot => {
1457 if (instance !== foreignHot) {
1458 foreignHot.unlisten();
1459 }
1460 });
1461 activeGuid = instance.guid;
1462 instance.runHooks('afterListen');
1463 }
1464 };
1465
1466 /**
1467 * Stop listening to keyboard input on the document body. Calling this method makes the Handsontable inactive for
1468 * any keyboard events.
1469 *
1470 * @memberof Core#
1471 * @function unlisten
1472 */
1473 this.unlisten = function () {
1474 if (this.isListening()) {
1475 activeGuid = null;
1476 instance.runHooks('afterUnlisten');
1477 }
1478 };
1479
1480 /**
1481 * Returns `true` if the current Handsontable instance is listening to keyboard input on document body.
1482 *
1483 * @memberof Core#
1484 * @function isListening
1485 * @returns {boolean} `true` if the instance is listening, `false` otherwise.
1486 */
1487 this.isListening = function () {
1488 return activeGuid === instance.guid;
1489 };
1490
1491 /**
1492 * Destroys the current editor, render the table and prepares the editor of the newly selected cell.
1493 *
1494 * @memberof Core#
1495 * @function destroyEditor
1496 * @param {boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved.
1497 * @param {boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open.
1498 */
1499 this.destroyEditor = function () {
1500 let revertOriginal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
1501 let prepareEditorIfNeeded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
1502 editorManager.closeEditor(revertOriginal);
1503 instance.view.render();
1504 if (prepareEditorIfNeeded && selection.isSelected()) {
1505 editorManager.prepareEditor();
1506 }
1507 };
1508
1509 /**
1510 * Populates cells at position with 2D input array (e.g. `[[1, 2], [3, 4]]`). Use `endRow`, `endCol` when you
1511 * want to cut input when a certain row is reached.
1512 *
1513 * The `populateFromArray()` method can't change [`readOnly`](@/api/options.md#readonly) cells.
1514 *
1515 * Optional `method` argument has the same effect as pasteMode option (see {@link Options#pasteMode}).
1516 *
1517 * @memberof Core#
1518 * @function populateFromArray
1519 * @param {number} row Start visual row index.
1520 * @param {number} column Start visual column index.
1521 * @param {Array} input 2d array.
1522 * @param {number} [endRow] End visual row index (use when you want to cut input when certain row is reached).
1523 * @param {number} [endCol] End visual column index (use when you want to cut input when certain column is reached).
1524 * @param {string} [source=populateFromArray] Used to identify this call in the resulting events (beforeChange, afterChange).
1525 * @param {string} [method=overwrite] Populate method, possible values: `'shift_down'`, `'shift_right'`, `'overwrite'`.
1526 * @returns {object|undefined} Ending td in pasted area (only if any cell was changed).
1527 */
1528 this.populateFromArray = function (row, column, input, endRow, endCol, source, method) {
1529 if (!(typeof input === 'object' && typeof input[0] === 'object')) {
1530 throw new Error('populateFromArray parameter `input` must be an array of arrays'); // API changed in 0.9-beta2, let's check if you use it correctly
1531 }
1532 const c = typeof endRow === 'number' ? instance._createCellCoords(endRow, endCol) : null;
1533 return grid.populateFromArray(instance._createCellCoords(row, column), input, c, source, method);
1534 };
1535
1536 /**
1537 * Adds/removes data from the column. This method works the same as Array.splice for arrays.
1538 *
1539 * @memberof Core#
1540 * @function spliceCol
1541 * @param {number} column Index of the column in which do you want to do splice.
1542 * @param {number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
1543 * @param {number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
1544 * @param {...number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
1545 * @returns {Array} Returns removed portion of columns.
1546 */
1547 this.spliceCol = function (column, index, amount) {
1548 for (var _len16 = arguments.length, elements = new Array(_len16 > 3 ? _len16 - 3 : 0), _key16 = 3; _key16 < _len16; _key16++) {
1549 elements[_key16 - 3] = arguments[_key16];
1550 }
1551 return datamap.spliceCol(column, index, amount, ...elements);
1552 };
1553
1554 /**
1555 * Adds/removes data from the row. This method works the same as Array.splice for arrays.
1556 *
1557 * @memberof Core#
1558 * @function spliceRow
1559 * @param {number} row Index of column in which do you want to do splice.
1560 * @param {number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
1561 * @param {number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
1562 * @param {...number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
1563 * @returns {Array} Returns removed portion of rows.
1564 */
1565 this.spliceRow = function (row, index, amount) {
1566 for (var _len17 = arguments.length, elements = new Array(_len17 > 3 ? _len17 - 3 : 0), _key17 = 3; _key17 < _len17; _key17++) {
1567 elements[_key17 - 3] = arguments[_key17];
1568 }
1569 return datamap.spliceRow(row, index, amount, ...elements);
1570 };
1571
1572 /**
1573 * Returns indexes of the currently selected cells as an array of arrays `[[startRow, startCol, endRow, endCol],...]`.
1574 *
1575 * Start row and start column are the coordinates of the active cell (where the selection was started).
1576 *
1577 * The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
1578 * Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
1579 * you need to use `getSelectedLast` method.
1580 *
1581 * @memberof Core#
1582 * @function getSelected
1583 * @returns {Array[]|undefined} An array of arrays of the selection's coordinates.
1584 */
1585 this.getSelected = function () {
1586 // https://github.com/handsontable/handsontable/issues/44 //cjl
1587 if (selection.isSelected()) {
1588 return arrayMap(selection.getSelectedRange(), _ref7 => {
1589 let {
1590 from,
1591 to
1592 } = _ref7;
1593 return [from.row, from.col, to.row, to.col];
1594 });
1595 }
1596 };
1597
1598 /**
1599 * Returns the last coordinates applied to the table as a an array `[startRow, startCol, endRow, endCol]`.
1600 *
1601 * @since 0.36.0
1602 * @memberof Core#
1603 * @function getSelectedLast
1604 * @returns {Array|undefined} An array of the selection's coordinates.
1605 */
1606 this.getSelectedLast = function () {
1607 const selected = this.getSelected();
1608 let result;
1609 if (selected && selected.length > 0) {
1610 result = selected[selected.length - 1];
1611 }
1612 return result;
1613 };
1614
1615 /**
1616 * Returns the current selection as an array of CellRange objects.
1617 *
1618 * The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
1619 * Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
1620 * you need to use `getSelectedRangeLast` method.
1621 *
1622 * @memberof Core#
1623 * @function getSelectedRange
1624 * @returns {CellRange[]|undefined} Selected range object or undefined if there is no selection.
1625 */
1626 this.getSelectedRange = function () {
1627 // https://github.com/handsontable/handsontable/issues/44 //cjl
1628 if (selection.isSelected()) {
1629 return Array.from(selection.getSelectedRange());
1630 }
1631 };
1632
1633 /**
1634 * Returns the last coordinates applied to the table as a CellRange object.
1635 *
1636 * @memberof Core#
1637 * @function getSelectedRangeLast
1638 * @since 0.36.0
1639 * @returns {CellRange|undefined} Selected range object or undefined` if there is no selection.
1640 */
1641 this.getSelectedRangeLast = function () {
1642 const selectedRange = this.getSelectedRange();
1643 let result;
1644 if (selectedRange && selectedRange.length > 0) {
1645 result = selectedRange[selectedRange.length - 1];
1646 }
1647 return result;
1648 };
1649
1650 /**
1651 * Erases content from cells that have been selected in the table.
1652 *
1653 * @memberof Core#
1654 * @function emptySelectedCells
1655 * @param {string} [source] String that identifies how this change will be described in the changes array (useful in afterChange or beforeChange callback). Set to 'edit' if left empty.
1656 * @since 0.36.0
1657 */
1658 this.emptySelectedCells = function (source) {
1659 if (!selection.isSelected() || this.countRows() === 0 || this.countCols() === 0) {
1660 return;
1661 }
1662 const changes = [];
1663 arrayEach(selection.getSelectedRange(), cellRange => {
1664 if (cellRange.isSingleHeader()) {
1665 return;
1666 }
1667 const topStart = cellRange.getTopStartCorner();
1668 const bottomEnd = cellRange.getBottomEndCorner();
1669 rangeEach(topStart.row, bottomEnd.row, row => {
1670 rangeEach(topStart.col, bottomEnd.col, column => {
1671 if (!this.getCellMeta(row, column).readOnly) {
1672 changes.push([row, column, null]);
1673 }
1674 });
1675 });
1676 });
1677 if (changes.length > 0) {
1678 this.setDataAtCell(changes, source);
1679 }
1680 };
1681
1682 /**
1683 * Checks if the table rendering process was suspended. See explanation in {@link Core#suspendRender}.
1684 *
1685 * @memberof Core#
1686 * @function isRenderSuspended
1687 * @since 8.3.0
1688 * @returns {boolean}
1689 */
1690 this.isRenderSuspended = function () {
1691 return this.renderSuspendedCounter > 0;
1692 };
1693
1694 /**
1695 * Suspends the rendering process. It's helpful to wrap the table render
1696 * cycles triggered by API calls or UI actions (or both) and call the "render"
1697 * once in the end. As a result, it improves the performance of wrapped operations.
1698 * When the table is in the suspend state, most operations will have no visual
1699 * effect until the rendering state is resumed. Resuming the state automatically
1700 * invokes the table rendering. To make sure that after executing all operations,
1701 * the table will be rendered, it's highly recommended to use the {@link Core#batchRender}
1702 * method or {@link Core#batch}, which additionally aggregates the logic execution
1703 * that happens behind the table.
1704 *
1705 * The method is intended to be used by advanced users. Suspending the rendering
1706 * process could cause visual glitches when wrongly implemented.
1707 *
1708 * Every [`suspendRender()`](@/api/core.md#suspendrender) call needs to correspond with one [`resumeRender()`](@/api/core.md#resumerender) call.
1709 * For example, if you call [`suspendRender()`](@/api/core.md#suspendrender) 5 times, you need to call [`resumeRender()`](@/api/core.md#resumerender) 5 times as well.
1710 *
1711 * @memberof Core#
1712 * @function suspendRender
1713 * @since 8.3.0
1714 * @example
1715 * ```js
1716 * hot.suspendRender();
1717 * hot.alter('insert_row_above', 5, 45);
1718 * hot.alter('insert_col_start', 10, 40);
1719 * hot.setDataAtCell(1, 1, 'John');
1720 * hot.setDataAtCell(2, 2, 'Mark');
1721 * hot.setDataAtCell(3, 3, 'Ann');
1722 * hot.setDataAtCell(4, 4, 'Sophia');
1723 * hot.setDataAtCell(5, 5, 'Mia');
1724 * hot.selectCell(0, 0);
1725 * hot.resumeRender(); // It re-renders the table internally
1726 * ```
1727 */
1728 this.suspendRender = function () {
1729 this.renderSuspendedCounter += 1;
1730 };
1731
1732 /**
1733 * Resumes the rendering process. In combination with the {@link Core#suspendRender}
1734 * method it allows aggregating the table render cycles triggered by API calls or UI
1735 * actions (or both) and calls the "render" once in the end. When the table is in
1736 * the suspend state, most operations will have no visual effect until the rendering
1737 * state is resumed. Resuming the state automatically invokes the table rendering.
1738 *
1739 * The method is intended to be used by advanced users. Suspending the rendering
1740 * process could cause visual glitches when wrongly implemented.
1741 *
1742 * Every [`suspendRender()`](@/api/core.md#suspendrender) call needs to correspond with one [`resumeRender()`](@/api/core.md#resumerender) call.
1743 * For example, if you call [`suspendRender()`](@/api/core.md#suspendrender) 5 times, you need to call [`resumeRender()`](@/api/core.md#resumerender) 5 times as well.
1744 *
1745 * @memberof Core#
1746 * @function resumeRender
1747 * @since 8.3.0
1748 * @example
1749 * ```js
1750 * hot.suspendRender();
1751 * hot.alter('insert_row_above', 5, 45);
1752 * hot.alter('insert_col_start', 10, 40);
1753 * hot.setDataAtCell(1, 1, 'John');
1754 * hot.setDataAtCell(2, 2, 'Mark');
1755 * hot.setDataAtCell(3, 3, 'Ann');
1756 * hot.setDataAtCell(4, 4, 'Sophia');
1757 * hot.setDataAtCell(5, 5, 'Mia');
1758 * hot.selectCell(0, 0);
1759 * hot.resumeRender(); // It re-renders the table internally
1760 * ```
1761 */
1762 this.resumeRender = function () {
1763 const nextValue = this.renderSuspendedCounter - 1;
1764 this.renderSuspendedCounter = Math.max(nextValue, 0);
1765 if (!this.isRenderSuspended() && nextValue === this.renderSuspendedCounter) {
1766 if (this.renderCall) {
1767 this.render();
1768 } else {
1769 instance.view.render();
1770 }
1771 }
1772 };
1773
1774 /**
1775 * Rerender the table. Calling this method starts the process of recalculating, redrawing and applying the changes
1776 * to the DOM. While rendering the table all cell renderers are recalled.
1777 *
1778 * Calling this method manually is not recommended. Handsontable tries to render itself by choosing the most
1779 * optimal moments in its lifecycle.
1780 *
1781 * @memberof Core#
1782 * @function render
1783 */
1784 this.render = function () {
1785 if (this.view) {
1786 this.renderCall = true;
1787 this.forceFullRender = true; // used when data was changed or when all cells need to be re-rendered
1788
1789 if (!this.isRenderSuspended()) {
1790 instance.view.render();
1791 }
1792 }
1793 };
1794
1795 /**
1796 * The method aggregates multi-line API calls into a callback and postpones the
1797 * table rendering process. After the execution of the operations, the table is
1798 * rendered once. As a result, it improves the performance of wrapped operations.
1799 * Without batching, a similar case could trigger multiple table render calls.
1800 *
1801 * @memberof Core#
1802 * @function batchRender
1803 * @param {Function} wrappedOperations Batched operations wrapped in a function.
1804 * @returns {*} Returns result from the wrappedOperations callback.
1805 * @since 8.3.0
1806 * @example
1807 * ```js
1808 * hot.batchRender(() => {
1809 * hot.alter('insert_row_above', 5, 45);
1810 * hot.alter('insert_col_start', 10, 40);
1811 * hot.setDataAtCell(1, 1, 'John');
1812 * hot.setDataAtCell(2, 2, 'Mark');
1813 * hot.setDataAtCell(3, 3, 'Ann');
1814 * hot.setDataAtCell(4, 4, 'Sophia');
1815 * hot.setDataAtCell(5, 5, 'Mia');
1816 * hot.selectCell(0, 0);
1817 * // The table will be rendered once after executing the callback
1818 * });
1819 * ```
1820 */
1821 this.batchRender = function (wrappedOperations) {
1822 this.suspendRender();
1823 const result = wrappedOperations();
1824 this.resumeRender();
1825 return result;
1826 };
1827
1828 /**
1829 * Checks if the table indexes recalculation process was suspended. See explanation
1830 * in {@link Core#suspendExecution}.
1831 *
1832 * @memberof Core#
1833 * @function isExecutionSuspended
1834 * @since 8.3.0
1835 * @returns {boolean}
1836 */
1837 this.isExecutionSuspended = function () {
1838 return this.executionSuspendedCounter > 0;
1839 };
1840
1841 /**
1842 * Suspends the execution process. It's helpful to wrap the table logic changes
1843 * such as index changes into one call after which the cache is updated. As a result,
1844 * it improves the performance of wrapped operations.
1845 *
1846 * The method is intended to be used by advanced users. Suspending the execution
1847 * process could cause visual glitches caused by not updated the internal table cache.
1848 *
1849 * @memberof Core#
1850 * @function suspendExecution
1851 * @since 8.3.0
1852 * @example
1853 * ```js
1854 * hot.suspendExecution();
1855 * const filters = hot.getPlugin('filters');
1856 *
1857 * filters.addCondition(2, 'contains', ['3']);
1858 * filters.filter();
1859 * hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
1860 * hot.resumeExecution(); // It updates the cache internally
1861 * ```
1862 */
1863 this.suspendExecution = function () {
1864 this.executionSuspendedCounter += 1;
1865 this.columnIndexMapper.suspendOperations();
1866 this.rowIndexMapper.suspendOperations();
1867 };
1868
1869 /**
1870 * Resumes the execution process. In combination with the {@link Core#suspendExecution}
1871 * method it allows aggregating the table logic changes after which the cache is
1872 * updated. Resuming the state automatically invokes the table cache updating process.
1873 *
1874 * The method is intended to be used by advanced users. Suspending the execution
1875 * process could cause visual glitches caused by not updated the internal table cache.
1876 *
1877 * @memberof Core#
1878 * @function resumeExecution
1879 * @param {boolean} [forceFlushChanges=false] If `true`, the table internal data cache
1880 * is recalculated after the execution of the batched operations. For nested
1881 * {@link Core#batchExecution} calls, it can be desire to recalculate the table
1882 * after each batch.
1883 * @since 8.3.0
1884 * @example
1885 * ```js
1886 * hot.suspendExecution();
1887 * const filters = hot.getPlugin('filters');
1888 *
1889 * filters.addCondition(2, 'contains', ['3']);
1890 * filters.filter();
1891 * hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
1892 * hot.resumeExecution(); // It updates the cache internally
1893 * ```
1894 */
1895 this.resumeExecution = function () {
1896 let forceFlushChanges = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
1897 const nextValue = this.executionSuspendedCounter - 1;
1898 this.executionSuspendedCounter = Math.max(nextValue, 0);
1899 if (!this.isExecutionSuspended() && nextValue === this.executionSuspendedCounter || forceFlushChanges) {
1900 this.columnIndexMapper.resumeOperations();
1901 this.rowIndexMapper.resumeOperations();
1902 }
1903 };
1904
1905 /**
1906 * The method aggregates multi-line API calls into a callback and postpones the
1907 * table execution process. After the execution of the operations, the internal table
1908 * cache is recalculated once. As a result, it improves the performance of wrapped
1909 * operations. Without batching, a similar case could trigger multiple table cache rebuilds.
1910 *
1911 * @memberof Core#
1912 * @function batchExecution
1913 * @param {Function} wrappedOperations Batched operations wrapped in a function.
1914 * @param {boolean} [forceFlushChanges=false] If `true`, the table internal data cache
1915 * is recalculated after the execution of the batched operations. For nested calls,
1916 * it can be a desire to recalculate the table after each batch.
1917 * @returns {*} Returns result from the wrappedOperations callback.
1918 * @since 8.3.0
1919 * @example
1920 * ```js
1921 * hot.batchExecution(() => {
1922 * const filters = hot.getPlugin('filters');
1923 *
1924 * filters.addCondition(2, 'contains', ['3']);
1925 * filters.filter();
1926 * hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
1927 * // The table cache will be recalculated once after executing the callback
1928 * });
1929 * ```
1930 */
1931 this.batchExecution = function (wrappedOperations) {
1932 let forceFlushChanges = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
1933 this.suspendExecution();
1934 const result = wrappedOperations();
1935 this.resumeExecution(forceFlushChanges);
1936 return result;
1937 };
1938
1939 /**
1940 * It batches the rendering process and index recalculations. The method aggregates
1941 * multi-line API calls into a callback and postpones the table rendering process
1942 * as well aggregates the table logic changes such as index changes into one call
1943 * after which the cache is updated. After the execution of the operations, the
1944 * table is rendered, and the cache is updated once. As a result, it improves the
1945 * performance of wrapped operations.
1946 *
1947 * @memberof Core#
1948 * @function batch
1949 * @param {Function} wrappedOperations Batched operations wrapped in a function.
1950 * @returns {*} Returns result from the wrappedOperations callback.
1951 * @since 8.3.0
1952 * @example
1953 * ```js
1954 * hot.batch(() => {
1955 * hot.alter('insert_row_above', 5, 45);
1956 * hot.alter('insert_col_start', 10, 40);
1957 * hot.setDataAtCell(1, 1, 'x');
1958 * hot.setDataAtCell(2, 2, 'c');
1959 * hot.setDataAtCell(3, 3, 'v');
1960 * hot.setDataAtCell(4, 4, 'b');
1961 * hot.setDataAtCell(5, 5, 'n');
1962 * hot.selectCell(0, 0);
1963 *
1964 * const filters = hot.getPlugin('filters');
1965 *
1966 * filters.addCondition(2, 'contains', ['3']);
1967 * filters.filter();
1968 * hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
1969 * // The table will be re-rendered and cache will be recalculated once after executing the callback
1970 * });
1971 * ```
1972 */
1973 this.batch = function (wrappedOperations) {
1974 this.suspendRender();
1975 this.suspendExecution();
1976 const result = wrappedOperations();
1977 this.resumeExecution();
1978 this.resumeRender();
1979 return result;
1980 };
1981
1982 /**
1983 * Updates dimensions of the table. The method compares previous dimensions with the current ones and updates accordingly.
1984 *
1985 * @memberof Core#
1986 * @function refreshDimensions
1987 * @fires Hooks#beforeRefreshDimensions
1988 * @fires Hooks#afterRefreshDimensions
1989 */
1990 this.refreshDimensions = function () {
1991 if (!instance.view) {
1992 return;
1993 }
1994 const {
1995 width: lastWidth,
1996 height: lastHeight
1997 } = instance.view.getLastSize();
1998 const {
1999 width,
2000 height
2001 } = instance.rootElement.getBoundingClientRect();
2002 const isSizeChanged = width !== lastWidth || height !== lastHeight;
2003 const isResizeBlocked = instance.runHooks('beforeRefreshDimensions', {
2004 width: lastWidth,
2005 height: lastHeight
2006 }, {
2007 width,
2008 height
2009 }, isSizeChanged) === false;
2010 if (isResizeBlocked) {
2011 return;
2012 }
2013 if (isSizeChanged || instance.view._wt.wtOverlays.scrollableElement === instance.rootWindow) {
2014 instance.view.setLastSize(width, height);
2015 instance.render();
2016 }
2017 instance.runHooks('afterRefreshDimensions', {
2018 width: lastWidth,
2019 height: lastHeight
2020 }, {
2021 width,
2022 height
2023 }, isSizeChanged);
2024 };
2025
2026 /**
2027 * The `updateData()` method replaces Handsontable's [`data`](@/api/options.md#data) with a new dataset.
2028 *
2029 * The `updateData()` method:
2030 * - Keeps cells' states (e.g. cells' [formatting](@/guides/cell-features/formatting-cells/formatting-cells.md) and cells' [`readOnly`](@/api/options.md#readonly) states)
2031 * - Keeps rows' states (e.g. row order)
2032 * - Keeps columns' states (e.g. column order)
2033 *
2034 * To replace Handsontable's [`data`](@/api/options.md#data) and reset states, use the [`loadData()`](#loaddata) method.
2035 *
2036 * Read more:
2037 * - [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md)
2038 * - [Saving data](@/guides/getting-started/saving-data/saving-data.md)
2039 *
2040 * @memberof Core#
2041 * @function updateData
2042 * @since 11.1.0
2043 * @param {Array} data An [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), or an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), that contains Handsontable's data
2044 * @param {string} [source] The source of the `updateData()` call
2045 * @fires Hooks#beforeUpdateData
2046 * @fires Hooks#afterUpdateData
2047 * @fires Hooks#afterChange
2048 */
2049 this.updateData = function (data, source) {
2050 replaceData(data, newDataMap => {
2051 datamap = newDataMap;
2052 }, newDataMap => {
2053 datamap = newDataMap;
2054 instance.columnIndexMapper.fitToLength(this.getInitialColumnCount());
2055 instance.rowIndexMapper.fitToLength(this.countSourceRows());
2056 grid.adjustRowsAndCols();
2057 selection.refresh();
2058 }, {
2059 hotInstance: instance,
2060 dataMap: datamap,
2061 dataSource,
2062 internalSource: 'updateData',
2063 source,
2064 metaManager,
2065 firstRun
2066 });
2067 };
2068
2069 /**
2070 * The `loadData()` method replaces Handsontable's [`data`](@/api/options.md#data) with a new dataset.
2071 *
2072 * Additionally, the `loadData()` method:
2073 * - Resets cells' states (e.g. cells' [formatting](@/guides/cell-features/formatting-cells/formatting-cells.md) and cells' [`readOnly`](@/api/options.md#readonly) states)
2074 * - Resets rows' states (e.g. row order)
2075 * - Resets columns' states (e.g. column order)
2076 *
2077 * To replace Handsontable's [`data`](@/api/options.md#data) without resetting states, use the [`updateData()`](#updatedata) method.
2078 *
2079 * Read more:
2080 * - [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md)
2081 * - [Saving data](@/guides/getting-started/saving-data/saving-data.md)
2082 *
2083 * @memberof Core#
2084 * @function loadData
2085 * @param {Array} data An [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), or an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), that contains Handsontable's data
2086 * @param {string} [source] The source of the `loadData()` call
2087 * @fires Hooks#beforeLoadData
2088 * @fires Hooks#afterLoadData
2089 * @fires Hooks#afterChange
2090 */
2091 this.loadData = function (data, source) {
2092 replaceData(data, newDataMap => {
2093 datamap = newDataMap;
2094 }, () => {
2095 metaManager.clearCellsCache();
2096 instance.initIndexMappers();
2097 grid.adjustRowsAndCols();
2098 selection.refresh();
2099 if (firstRun) {
2100 firstRun = [null, 'loadData'];
2101 }
2102 }, {
2103 hotInstance: instance,
2104 dataMap: datamap,
2105 dataSource,
2106 internalSource: 'loadData',
2107 source,
2108 metaManager,
2109 firstRun
2110 });
2111 };
2112
2113 /**
2114 * Gets the initial column count, calculated based on the `columns` setting.
2115 *
2116 * @private
2117 * @returns {number} The calculated number of columns.
2118 */
2119 this.getInitialColumnCount = function () {
2120 const columnsSettings = tableMeta.columns;
2121 let finalNrOfColumns = 0;
2122
2123 // We will check number of columns when the `columns` property was defined as an array. Columns option may
2124 // narrow down or expand displayed dataset in that case.
2125 if (Array.isArray(columnsSettings)) {
2126 finalNrOfColumns = columnsSettings.length;
2127 } else if (isFunction(columnsSettings)) {
2128 if (instance.dataType === 'array') {
2129 const nrOfSourceColumns = this.countSourceCols();
2130 for (let columnIndex = 0; columnIndex < nrOfSourceColumns; columnIndex += 1) {
2131 if (columnsSettings(columnIndex)) {
2132 finalNrOfColumns += 1;
2133 }
2134 }
2135
2136 // Extended dataset by the `columns` property? Moved code right from the refactored `countCols` method.
2137 } else if (instance.dataType === 'object' || instance.dataType === 'function') {
2138 finalNrOfColumns = datamap.colToPropCache.length;
2139 }
2140
2141 // In some cases we need to check columns length from the schema, i.e. `data` may be empty.
2142 } else if (isDefined(tableMeta.dataSchema)) {
2143 const schema = datamap.getSchema();
2144
2145 // Schema may be defined as an array of objects. Each object will define column.
2146 finalNrOfColumns = Array.isArray(schema) ? schema.length : deepObjectSize(schema);
2147 } else {
2148 // We init index mappers by length of source data to provide indexes also for skipped indexes.
2149 finalNrOfColumns = this.countSourceCols();
2150 }
2151 return finalNrOfColumns;
2152 };
2153
2154 /**
2155 * Init index mapper which manage indexes assigned to the data.
2156 *
2157 * @private
2158 */
2159 this.initIndexMappers = function () {
2160 this.columnIndexMapper.initToLength(this.getInitialColumnCount());
2161 this.rowIndexMapper.initToLength(this.countSourceRows());
2162 };
2163
2164 /**
2165 * Returns the current data object (the same one that was passed by `data` configuration option or `loadData` method,
2166 * unless some modifications have been applied (i.e. Sequence of rows/columns was changed, some row/column was skipped).
2167 * If that's the case - use the {@link Core#getSourceData} method.).
2168 *
2169 * Optionally you can provide cell range by defining `row`, `column`, `row2`, `column2` to get only a fragment of table data.
2170 *
2171 * @memberof Core#
2172 * @function getData
2173 * @param {number} [row] From visual row index.
2174 * @param {number} [column] From visual column index.
2175 * @param {number} [row2] To visual row index.
2176 * @param {number} [column2] To visual column index.
2177 * @returns {Array[]} Array with the data.
2178 * @example
2179 * ```js
2180 * // Get all data (in order how it is rendered in the table).
2181 * hot.getData();
2182 * // Get data fragment (from top-left 0, 0 to bottom-right 3, 3).
2183 * hot.getData(3, 3);
2184 * // Get data fragment (from top-left 2, 1 to bottom-right 3, 3).
2185 * hot.getData(2, 1, 3, 3);
2186 * ```
2187 */
2188 this.getData = function (row, column, row2, column2) {
2189 if (isUndefined(row)) {
2190 return datamap.getAll();
2191 }
2192 return datamap.getRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2), datamap.DESTINATION_RENDERER);
2193 };
2194
2195 /**
2196 * Returns a string value of the selected range. Each column is separated by tab, each row is separated by a new
2197 * line character.
2198 *
2199 * @memberof Core#
2200 * @function getCopyableText
2201 * @param {number} startRow From visual row index.
2202 * @param {number} startCol From visual column index.
2203 * @param {number} endRow To visual row index.
2204 * @param {number} endCol To visual column index.
2205 * @returns {string}
2206 */
2207 this.getCopyableText = function (startRow, startCol, endRow, endCol) {
2208 return datamap.getCopyableText(instance._createCellCoords(startRow, startCol), instance._createCellCoords(endRow, endCol));
2209 };
2210
2211 /**
2212 * Returns the data's copyable value at specified `row` and `column` index.
2213 *
2214 * @memberof Core#
2215 * @function getCopyableData
2216 * @param {number} row Visual row index.
2217 * @param {number} column Visual column index.
2218 * @returns {string}
2219 */
2220 this.getCopyableData = function (row, column) {
2221 return datamap.getCopyable(row, datamap.colToProp(column));
2222 };
2223
2224 /**
2225 * Returns schema provided by constructor settings. If it doesn't exist then it returns the schema based on the data
2226 * structure in the first row.
2227 *
2228 * @memberof Core#
2229 * @function getSchema
2230 * @returns {object} Schema object.
2231 */
2232 this.getSchema = function () {
2233 return datamap.getSchema();
2234 };
2235
2236 /**
2237 * Use it if you need to change configuration after initialization. The `settings` argument is an object containing the changed
2238 * settings, declared the same way as in the initial settings object.
2239 *
2240 * __Note__, that although the `updateSettings` method doesn't overwrite the previously declared settings, it might reset
2241 * the settings made post-initialization. (for example - ignore changes made using the columnResize feature).
2242 *
2243 * Since 8.0.0 passing `columns` or `data` inside `settings` objects will result in resetting states corresponding to rows and columns
2244 * (for example, row/column sequence, column width, row height, frozen columns etc.).
2245 *
2246 * Since 12.0.0 passing `data` inside `settings` objects no longer results in resetting states corresponding to rows and columns
2247 * (for example, row/column sequence, column width, row height, frozen columns etc.).
2248 *
2249 * @memberof Core#
2250 * @function updateSettings
2251 * @param {object} settings A settings object (see {@link Options}). Only provide the settings that are changed, not the whole settings object that was used for initialization.
2252 * @param {boolean} [init=false] Internally used for in initialization mode.
2253 * @example
2254 * ```js
2255 * hot.updateSettings({
2256 * contextMenu: true,
2257 * colHeaders: true,
2258 * fixedRowsTop: 2
2259 * });
2260 * ```
2261 * @fires Hooks#afterCellMetaReset
2262 * @fires Hooks#afterUpdateSettings
2263 */
2264 this.updateSettings = function (settings) {
2265 let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
2266 const dataUpdateFunction = (firstRun ? instance.loadData : instance.updateData).bind(this);
2267 let columnsAsFunc = false;
2268 let i;
2269 let j;
2270 if (isDefined(settings.rows)) {
2271 throw new Error('The "rows" setting is no longer supported. Do you mean startRows, minRows or maxRows?');
2272 }
2273 if (isDefined(settings.cols)) {
2274 throw new Error('The "cols" setting is no longer supported. Do you mean startCols, minCols or maxCols?');
2275 }
2276 if (isDefined(settings.ganttChart)) {
2277 throw new Error('Since 8.0.0 the "ganttChart" setting is no longer supported.');
2278 }
2279
2280 // eslint-disable-next-line no-restricted-syntax
2281 for (i in settings) {
2282 if (i === 'data') {
2283 // Do nothing. loadData will be triggered later
2284 } else if (i === 'language') {
2285 setLanguage(settings.language);
2286 } else if (i === 'className') {
2287 setClassName('className', settings.className);
2288 } else if (i === 'tableClassName' && instance.table) {
2289 setClassName('tableClassName', settings.tableClassName);
2290 instance.view._wt.wtOverlays.syncOverlayTableClassNames();
2291 } else if (Hooks.getSingleton().isRegistered(i) || Hooks.getSingleton().isDeprecated(i)) {
2292 if (isFunction(settings[i]) || Array.isArray(settings[i])) {
2293 settings[i].initialHook = true;
2294 instance.addHook(i, settings[i]);
2295 }
2296 } else if (!init && hasOwnProperty(settings, i)) {
2297 // Update settings
2298 globalMeta[i] = settings[i];
2299 }
2300 }
2301
2302 // Load data or create data map
2303 if (settings.data === undefined && tableMeta.data === undefined) {
2304 dataUpdateFunction(null, 'updateSettings'); // data source created just now
2305 } else if (settings.data !== undefined) {
2306 dataUpdateFunction(settings.data, 'updateSettings'); // data source given as option
2307 } else if (settings.columns !== undefined) {
2308 datamap.createMap();
2309
2310 // The `column` property has changed - dataset may be expanded or narrowed down. The `loadData` do the same.
2311 instance.initIndexMappers();
2312 }
2313 const clen = instance.countCols();
2314 const columnSetting = tableMeta.columns;
2315
2316 // Init columns constructors configuration
2317 if (columnSetting && isFunction(columnSetting)) {
2318 columnsAsFunc = true;
2319 }
2320
2321 // Clear cell meta cache
2322 if (settings.cell !== undefined || settings.cells !== undefined || settings.columns !== undefined) {
2323 metaManager.clearCache();
2324 }
2325 if (clen > 0) {
2326 for (i = 0, j = 0; i < clen; i++) {
2327 // Use settings provided by user
2328 if (columnSetting) {
2329 const column = columnsAsFunc ? columnSetting(i) : columnSetting[j];
2330 if (column) {
2331 metaManager.updateColumnMeta(j, column);
2332 }
2333 }
2334 j += 1;
2335 }
2336 }
2337 if (isDefined(settings.cell)) {
2338 objectEach(settings.cell, cell => {
2339 instance.setCellMetaObject(cell.row, cell.col, cell);
2340 });
2341 }
2342 instance.runHooks('afterCellMetaReset');
2343 let currentHeight = instance.rootElement.style.height;
2344 if (currentHeight !== '') {
2345 currentHeight = parseInt(instance.rootElement.style.height, 10);
2346 }
2347 let height = settings.height;
2348 if (isFunction(height)) {
2349 height = height();
2350 }
2351 if (init) {
2352 const initialStyle = instance.rootElement.getAttribute('style');
2353 if (initialStyle) {
2354 instance.rootElement.setAttribute('data-initialstyle', instance.rootElement.getAttribute('style'));
2355 }
2356 }
2357 if (height === null) {
2358 const initialStyle = instance.rootElement.getAttribute('data-initialstyle');
2359 if (initialStyle && (initialStyle.indexOf('height') > -1 || initialStyle.indexOf('overflow') > -1)) {
2360 instance.rootElement.setAttribute('style', initialStyle);
2361 } else {
2362 instance.rootElement.style.height = '';
2363 instance.rootElement.style.overflow = '';
2364 }
2365 } else if (height !== undefined) {
2366 instance.rootElement.style.height = isNaN(height) ? `${height}` : `${height}px`;
2367 instance.rootElement.style.overflow = 'hidden';
2368 }
2369 if (typeof settings.width !== 'undefined') {
2370 let width = settings.width;
2371 if (isFunction(width)) {
2372 width = width();
2373 }
2374 instance.rootElement.style.width = isNaN(width) ? `${width}` : `${width}px`;
2375 }
2376 if (!init) {
2377 if (instance.view) {
2378 instance.view._wt.wtViewport.resetHasOversizedColumnHeadersMarked();
2379 instance.view._wt.exportSettingsAsClassNames();
2380 }
2381 instance.runHooks('afterUpdateSettings', settings);
2382 }
2383 grid.adjustRowsAndCols();
2384 if (instance.view && !firstRun) {
2385 instance.forceFullRender = true; // used when data was changed
2386 instance.view.render();
2387 instance.view._wt.wtOverlays.adjustElementsSize();
2388 }
2389 if (!init && instance.view && (currentHeight === '' || height === '' || height === undefined) && currentHeight !== height) {
2390 instance.view._wt.wtOverlays.updateMainScrollableElements();
2391 }
2392 };
2393
2394 /**
2395 * Gets the value of the currently focused cell.
2396 *
2397 * For column headers and row headers, returns `null`.
2398 *
2399 * @memberof Core#
2400 * @function getValue
2401 * @returns {*} The value of the focused cell.
2402 */
2403 this.getValue = function () {
2404 const sel = instance.getSelectedLast();
2405 if (tableMeta.getValue) {
2406 if (isFunction(tableMeta.getValue)) {
2407 return tableMeta.getValue.call(instance);
2408 } else if (sel) {
2409 return instance.getData()[sel[0][0]][tableMeta.getValue];
2410 }
2411 } else if (sel) {
2412 return instance.getDataAtCell(sel[0], sel[1]);
2413 }
2414 };
2415
2416 /**
2417 * Returns the object settings.
2418 *
2419 * @memberof Core#
2420 * @function getSettings
2421 * @returns {TableMeta} Object containing the current table settings.
2422 */
2423 this.getSettings = function () {
2424 return tableMeta;
2425 };
2426
2427 /**
2428 * Clears the data from the table (the table settings remain intact).
2429 *
2430 * @memberof Core#
2431 * @function clear
2432 */
2433 this.clear = function () {
2434 this.selectAll();
2435 this.emptySelectedCells();
2436 };
2437
2438 /**
2439 * The `alter()` method lets you alter the grid's structure
2440 * by adding or removing rows and columns at specified positions.
2441 *
2442 * ::: tip
2443 * If you use an array of objects in your [`data`](@/api/options.md#data), the column-related actions won't work.
2444 * :::
2445 *
2446 * ```js
2447 * // above row 10 (by visual index), insert 1 new row
2448 * hot.alter('insert_row_above', 10);
2449 * ```
2450 *
2451 * | Action | With `index` | Without `index` |
2452 * | -------------------- | ------------ | --------------- |
2453 * | `'insert_row_above'` | Inserts rows above the `index` row. | Inserts rows above the first row. |
2454 * | `'insert_row_below'` | Inserts rows below the `index` row. | Inserts rows below the last row. |
2455 * | `'remove_row'` | Removes rows, starting from the `index` row. | Removes rows, starting from the last row. |
2456 * | `'insert_col_start'` | Inserts columns before the `index` column. | Inserts columns before the first column. |
2457 * | `'insert_col_end'` | Inserts columns after the `index` column. | Inserts columns after the last column. |
2458 * | `'remove_col'` | Removes columns, starting from the `index` column. | Removes columns, starting from the last column. |
2459 *
2460 * Additional information about `'insert_col_start'` and `'insert_col_end'`:
2461 * - Their behavior depends on your [`layoutDirection`](@/api/options.md#layoutdirection).
2462 * - If the provided `index` is higher than the actual number of columns, Handsontable doesn't generate
2463 * the columns missing in between. Instead, the new columns are inserted next to the last column.
2464 *
2465 * @memberof Core#
2466 * @function alter
2467 * @param {string} action Available operations:
2468 * <ul>
2469 * <li> `'insert_row_above'` </li>
2470 * <li> `'insert_row_below'` </li>
2471 * <li> `'remove_row'` </li> </li>
2472 * <li> `'insert_col_start'` </li>
2473 * <li> `'insert_col_end'` </li>
2474 * <li> `'remove_col'` </li>
2475 * </ul>
2476 * @param {number|number[]} [index] A visual index of the row/column before or after which the new row/column will be
2477 * inserted or removed. Can also be an array of arrays, in format `[[index, amount],...]`.
2478 * @param {number} [amount] The amount of rows or columns to be inserted or removed (default: `1`).
2479 * @param {string} [source] Source indicator.
2480 * @param {boolean} [keepEmptyRows] If set to `true`: prevents removing empty rows.
2481 * @example
2482 * ```js
2483 * // above row 10 (by visual index), insert 1 new row
2484 * hot.alter('insert_row_above', 10);
2485 *
2486 * // below row 10 (by visual index), insert 3 new rows
2487 * hot.alter('insert_row_below', 10, 3);
2488 *
2489 * // in the LTR layout direction: to the left of column 10 (by visual index), insert 3 new columns
2490 * // in the RTL layout direction: to the right of column 10 (by visual index), insert 3 new columns
2491 * hot.alter('insert_col_start', 10, 3);
2492 *
2493 * // in the LTR layout direction: to the right of column 10 (by visual index), insert 1 new column
2494 * // in the RTL layout direction: to the left of column 10 (by visual index), insert 1 new column
2495 * hot.alter('insert_col_end', 10);
2496 *
2497 * // remove 2 rows, starting from row 10 (by visual index)
2498 * hot.alter('remove_row', 10, 2);
2499 *
2500 * // remove 3 rows, starting from row 1 (by visual index)
2501 * // remove 2 rows, starting from row 5 (by visual index)
2502 * hot.alter('remove_row', [[1, 3], [5, 2]]);
2503 * ```
2504 */
2505 this.alter = function (action, index, amount, source, keepEmptyRows) {
2506 grid.alter(action, index, amount, source, keepEmptyRows);
2507 };
2508
2509 /**
2510 * Returns a TD element for the given `row` and `column` arguments, if it is rendered on screen.
2511 * Returns `null` if the TD is not rendered on screen (probably because that part of the table is not visible).
2512 *
2513 * @memberof Core#
2514 * @function getCell
2515 * @param {number} row Visual row index.
2516 * @param {number} column Visual column index.
2517 * @param {boolean} [topmost=false] If set to `true`, it returns the TD element from the topmost overlay. For example,
2518 * if the wanted cell is in the range of fixed rows, it will return a TD element from the `top` overlay.
2519 * @returns {HTMLTableCellElement|null} The cell's TD element.
2520 */
2521 this.getCell = function (row, column) {
2522 let topmost = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
2523 let renderableColumnIndex = column; // Handling also column headers.
2524 let renderableRowIndex = row; // Handling also row headers.
2525
2526 if (column >= 0) {
2527 if (this.columnIndexMapper.isHidden(this.toPhysicalColumn(column))) {
2528 return null;
2529 }
2530 renderableColumnIndex = this.columnIndexMapper.getRenderableFromVisualIndex(column);
2531 }
2532 if (row >= 0) {
2533 if (this.rowIndexMapper.isHidden(this.toPhysicalRow(row))) {
2534 return null;
2535 }
2536 renderableRowIndex = this.rowIndexMapper.getRenderableFromVisualIndex(row);
2537 }
2538 if (renderableRowIndex === null || renderableColumnIndex === null || renderableRowIndex === undefined || renderableColumnIndex === undefined) {
2539 return null;
2540 }
2541 return instance.view.getCellAtCoords(instance._createCellCoords(renderableRowIndex, renderableColumnIndex), topmost);
2542 };
2543
2544 /**
2545 * Returns the coordinates of the cell, provided as a HTML table cell element.
2546 *
2547 * @memberof Core#
2548 * @function getCoords
2549 * @param {HTMLTableCellElement} element The HTML Element representing the cell.
2550 * @returns {CellCoords|null} Visual coordinates object.
2551 * @example
2552 * ```js
2553 * hot.getCoords(hot.getCell(1, 1));
2554 * // it returns CellCoords object instance with props row: 1 and col: 1.
2555 * ```
2556 */
2557 this.getCoords = function (element) {
2558 const renderableCoords = this.view._wt.wtTable.getCoords(element);
2559 if (renderableCoords === null) {
2560 return null;
2561 }
2562 const {
2563 row: renderableRow,
2564 col: renderableColumn
2565 } = renderableCoords;
2566 let visualRow = renderableRow;
2567 let visualColumn = renderableColumn;
2568 if (renderableRow >= 0) {
2569 visualRow = this.rowIndexMapper.getVisualFromRenderableIndex(renderableRow);
2570 }
2571 if (renderableColumn >= 0) {
2572 visualColumn = this.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn);
2573 }
2574 return instance._createCellCoords(visualRow, visualColumn);
2575 };
2576
2577 /**
2578 * Returns the property name that corresponds with the given column index.
2579 * If the data source is an array of arrays, it returns the columns index.
2580 *
2581 * @memberof Core#
2582 * @function colToProp
2583 * @param {number} column Visual column index.
2584 * @returns {string|number} Column property or physical column index.
2585 */
2586 this.colToProp = function (column) {
2587 return datamap.colToProp(column);
2588 };
2589
2590 /**
2591 * Returns column index that corresponds with the given property.
2592 *
2593 * @memberof Core#
2594 * @function propToCol
2595 * @param {string|number} prop Property name or physical column index.
2596 * @returns {number} Visual column index.
2597 */
2598 this.propToCol = function (prop) {
2599 return datamap.propToCol(prop);
2600 };
2601
2602 /**
2603 * Translate physical row index into visual.
2604 *
2605 * This method is useful when you want to retrieve visual row index which can be reordered, moved or trimmed
2606 * based on a physical index.
2607 *
2608 * @memberof Core#
2609 * @function toVisualRow
2610 * @param {number} row Physical row index.
2611 * @returns {number} Returns visual row index.
2612 */
2613 this.toVisualRow = row => this.rowIndexMapper.getVisualFromPhysicalIndex(row);
2614
2615 /**
2616 * Translate physical column index into visual.
2617 *
2618 * This method is useful when you want to retrieve visual column index which can be reordered, moved or trimmed
2619 * based on a physical index.
2620 *
2621 * @memberof Core#
2622 * @function toVisualColumn
2623 * @param {number} column Physical column index.
2624 * @returns {number} Returns visual column index.
2625 */
2626 this.toVisualColumn = column => this.columnIndexMapper.getVisualFromPhysicalIndex(column);
2627
2628 /**
2629 * Translate visual row index into physical.
2630 *
2631 * This method is useful when you want to retrieve physical row index based on a visual index which can be
2632 * reordered, moved or trimmed.
2633 *
2634 * @memberof Core#
2635 * @function toPhysicalRow
2636 * @param {number} row Visual row index.
2637 * @returns {number} Returns physical row index.
2638 */
2639 this.toPhysicalRow = row => this.rowIndexMapper.getPhysicalFromVisualIndex(row);
2640
2641 /**
2642 * Translate visual column index into physical.
2643 *
2644 * This method is useful when you want to retrieve physical column index based on a visual index which can be
2645 * reordered, moved or trimmed.
2646 *
2647 * @memberof Core#
2648 * @function toPhysicalColumn
2649 * @param {number} column Visual column index.
2650 * @returns {number} Returns physical column index.
2651 */
2652 this.toPhysicalColumn = column => this.columnIndexMapper.getPhysicalFromVisualIndex(column);
2653
2654 /**
2655 * @description
2656 * Returns the cell value at `row`, `column`.
2657 *
2658 * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
2659 *
2660 * @memberof Core#
2661 * @function getDataAtCell
2662 * @param {number} row Visual row index.
2663 * @param {number} column Visual column index.
2664 * @returns {*} Data at cell.
2665 */
2666 this.getDataAtCell = function (row, column) {
2667 return datamap.get(row, datamap.colToProp(column));
2668 };
2669
2670 /**
2671 * Returns value at visual `row` and `prop` indexes.
2672 *
2673 * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
2674 *
2675 * @memberof Core#
2676 * @function getDataAtRowProp
2677 * @param {number} row Visual row index.
2678 * @param {string} prop Property name.
2679 * @returns {*} Cell value.
2680 */
2681 this.getDataAtRowProp = function (row, prop) {
2682 return datamap.get(row, prop);
2683 };
2684
2685 /**
2686 * @description
2687 * Returns array of column values from the data source.
2688 *
2689 * __Note__: If columns were reordered or sorted, the currently visible order will be used.
2690 *
2691 * @memberof Core#
2692 * @function getDataAtCol
2693 * @param {number} column Visual column index.
2694 * @returns {Array} Array of cell values.
2695 */
2696 this.getDataAtCol = function (column) {
2697 const columnData = [];
2698 const dataByRows = datamap.getRange(instance._createCellCoords(0, column), instance._createCellCoords(tableMeta.data.length - 1, column), datamap.DESTINATION_RENDERER);
2699 for (let i = 0; i < dataByRows.length; i += 1) {
2700 for (let j = 0; j < dataByRows[i].length; j += 1) {
2701 columnData.push(dataByRows[i][j]);
2702 }
2703 }
2704 return columnData;
2705 };
2706
2707 /**
2708 * Given the object property name (e.g. `'first.name'` or `'0'`), returns an array of column's values from the table data.
2709 * You can also provide a column index as the first argument.
2710 *
2711 * @memberof Core#
2712 * @function getDataAtProp
2713 * @param {string|number} prop Property name or physical column index.
2714 * @returns {Array} Array of cell values.
2715 */
2716 // TODO: Getting data from `datamap` should work on visual indexes.
2717 this.getDataAtProp = function (prop) {
2718 const columnData = [];
2719 const dataByRows = datamap.getRange(instance._createCellCoords(0, datamap.propToCol(prop)), instance._createCellCoords(tableMeta.data.length - 1, datamap.propToCol(prop)), datamap.DESTINATION_RENDERER);
2720 for (let i = 0; i < dataByRows.length; i += 1) {
2721 for (let j = 0; j < dataByRows[i].length; j += 1) {
2722 columnData.push(dataByRows[i][j]);
2723 }
2724 }
2725 return columnData;
2726 };
2727
2728 /**
2729 * Returns a clone of the source data object.
2730 * Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
2731 * fragment of the table data.
2732 *
2733 * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
2734 * sorted or trimmed only physical indexes are correct.
2735 *
2736 * __Note__: This method may return incorrect values for cells that contain
2737 * [formulas](@/guides/formulas/formula-calculation/formula-calculation.md). This is because `getSourceData()`
2738 * operates on source data ([physical indexes](@/api/indexMapper.md)),
2739 * whereas formulas operate on visual data (visual indexes).
2740 *
2741 * @memberof Core#
2742 * @function getSourceData
2743 * @param {number} [row] From physical row index.
2744 * @param {number} [column] From physical column index (or visual index, if data type is an array of objects).
2745 * @param {number} [row2] To physical row index.
2746 * @param {number} [column2] To physical column index (or visual index, if data type is an array of objects).
2747 * @returns {Array[]|object[]} The table data.
2748 */
2749 this.getSourceData = function (row, column, row2, column2) {
2750 let data;
2751 if (row === undefined) {
2752 data = dataSource.getData();
2753 } else {
2754 data = dataSource.getByRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2));
2755 }
2756 return data;
2757 };
2758
2759 /**
2760 * Returns the source data object as an arrays of arrays format even when source data was provided in another format.
2761 * Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
2762 * fragment of the table data.
2763 *
2764 * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
2765 * sorted or trimmed only physical indexes are correct.
2766 *
2767 * @memberof Core#
2768 * @function getSourceDataArray
2769 * @param {number} [row] From physical row index.
2770 * @param {number} [column] From physical column index (or visual index, if data type is an array of objects).
2771 * @param {number} [row2] To physical row index.
2772 * @param {number} [column2] To physical column index (or visual index, if data type is an array of objects).
2773 * @returns {Array} An array of arrays.
2774 */
2775 this.getSourceDataArray = function (row, column, row2, column2) {
2776 let data;
2777 if (row === undefined) {
2778 data = dataSource.getData(true);
2779 } else {
2780 data = dataSource.getByRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2), true);
2781 }
2782 return data;
2783 };
2784
2785 /**
2786 * Returns an array of column values from the data source.
2787 *
2788 * @memberof Core#
2789 * @function getSourceDataAtCol
2790 * @param {number} column Visual column index.
2791 * @returns {Array} Array of the column's cell values.
2792 */
2793 // TODO: Getting data from `sourceData` should work always on physical indexes.
2794 this.getSourceDataAtCol = function (column) {
2795 return dataSource.getAtColumn(column);
2796 };
2797
2798 /* eslint-disable jsdoc/require-param */
2799 /**
2800 * Set the provided value in the source data set at the provided coordinates.
2801 *
2802 * @memberof Core#
2803 * @function setSourceDataAtCell
2804 * @param {number|Array} row Physical row index or array of changes in format `[[row, prop, value], ...]`.
2805 * @param {number|string} column Physical column index / prop name.
2806 * @param {*} value The value to be set at the provided coordinates.
2807 * @param {string} [source] Source of the change as a string.
2808 */
2809 /* eslint-enable jsdoc/require-param */
2810 this.setSourceDataAtCell = function (row, column, value, source) {
2811 const input = setDataInputToArray(row, column, value);
2812 const isThereAnySetSourceListener = this.hasHook('afterSetSourceDataAtCell');
2813 const changesForHook = [];
2814 if (isThereAnySetSourceListener) {
2815 arrayEach(input, _ref8 => {
2816 let [changeRow, changeProp, changeValue] = _ref8;
2817 changesForHook.push([changeRow, changeProp, dataSource.getAtCell(changeRow, changeProp),
2818 // The previous value.
2819 changeValue]);
2820 });
2821 }
2822 arrayEach(input, _ref9 => {
2823 let [changeRow, changeProp, changeValue] = _ref9;
2824 dataSource.setAtCell(changeRow, changeProp, changeValue);
2825 });
2826 if (isThereAnySetSourceListener) {
2827 this.runHooks('afterSetSourceDataAtCell', changesForHook, source);
2828 }
2829 this.render();
2830 const activeEditor = instance.getActiveEditor();
2831 if (activeEditor && isDefined(activeEditor.refreshValue)) {
2832 activeEditor.refreshValue();
2833 }
2834 };
2835
2836 /**
2837 * Returns a single row of the data (array or object, depending on what data format you use).
2838 *
2839 * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
2840 * sorted or trimmed only physical indexes are correct.
2841 *
2842 * @memberof Core#
2843 * @function getSourceDataAtRow
2844 * @param {number} row Physical row index.
2845 * @returns {Array|object} Single row of data.
2846 */
2847 this.getSourceDataAtRow = function (row) {
2848 return dataSource.getAtRow(row);
2849 };
2850
2851 /**
2852 * Returns a single value from the data source.
2853 *
2854 * @memberof Core#
2855 * @function getSourceDataAtCell
2856 * @param {number} row Physical row index.
2857 * @param {number} column Visual column index.
2858 * @returns {*} Cell data.
2859 */
2860 // TODO: Getting data from `sourceData` should work always on physical indexes.
2861 this.getSourceDataAtCell = function (row, column) {
2862 return dataSource.getAtCell(row, column);
2863 };
2864
2865 /**
2866 * @description
2867 * Returns a single row of the data.
2868 *
2869 * __Note__: If rows were reordered, sorted or trimmed, the currently visible order will be used.
2870 *
2871 * @memberof Core#
2872 * @function getDataAtRow
2873 * @param {number} row Visual row index.
2874 * @returns {Array} Array of row's cell data.
2875 */
2876 this.getDataAtRow = function (row) {
2877 const data = datamap.getRange(instance._createCellCoords(row, 0), instance._createCellCoords(row, this.countCols() - 1), datamap.DESTINATION_RENDERER);
2878 return data[0] || [];
2879 };
2880
2881 /**
2882 * @description
2883 * Returns a data type defined in the Handsontable settings under the `type` key ({@link Options#type}).
2884 * If there are cells with different types in the selected range, it returns `'mixed'`.
2885 *
2886 * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
2887 *
2888 * @memberof Core#
2889 * @function getDataType
2890 * @param {number} rowFrom From visual row index.
2891 * @param {number} columnFrom From visual column index.
2892 * @param {number} rowTo To visual row index.
2893 * @param {number} columnTo To visual column index.
2894 * @returns {string} Cell type (e.q: `'mixed'`, `'text'`, `'numeric'`, `'autocomplete'`).
2895 */
2896 this.getDataType = function (rowFrom, columnFrom, rowTo, columnTo) {
2897 const coords = rowFrom === undefined ? [0, 0, this.countRows(), this.countCols()] : [rowFrom, columnFrom, rowTo, columnTo];
2898 const [rowStart, columnStart] = coords;
2899 let [,, rowEnd, columnEnd] = coords;
2900 let previousType = null;
2901 let currentType = null;
2902 if (rowEnd === undefined) {
2903 rowEnd = rowStart;
2904 }
2905 if (columnEnd === undefined) {
2906 columnEnd = columnStart;
2907 }
2908 let type = 'mixed';
2909 rangeEach(Math.max(Math.min(rowStart, rowEnd), 0), Math.max(rowStart, rowEnd), row => {
2910 let isTypeEqual = true;
2911 rangeEach(Math.max(Math.min(columnStart, columnEnd), 0), Math.max(columnStart, columnEnd), column => {
2912 const cellType = this.getCellMeta(row, column);
2913 currentType = cellType.type;
2914 if (previousType) {
2915 isTypeEqual = previousType === currentType;
2916 } else {
2917 previousType = currentType;
2918 }
2919 return isTypeEqual;
2920 });
2921 type = isTypeEqual ? currentType : 'mixed';
2922 return isTypeEqual;
2923 });
2924 return type;
2925 };
2926
2927 /**
2928 * Remove a property defined by the `key` argument from the cell meta object for the provided `row` and `column` coordinates.
2929 *
2930 * @memberof Core#
2931 * @function removeCellMeta
2932 * @param {number} row Visual row index.
2933 * @param {number} column Visual column index.
2934 * @param {string} key Property name.
2935 * @fires Hooks#beforeRemoveCellMeta
2936 * @fires Hooks#afterRemoveCellMeta
2937 */
2938 this.removeCellMeta = function (row, column, key) {
2939 const [physicalRow, physicalColumn] = [this.toPhysicalRow(row), this.toPhysicalColumn(column)];
2940 let cachedValue = metaManager.getCellMetaKeyValue(physicalRow, physicalColumn, key);
2941 const hookResult = instance.runHooks('beforeRemoveCellMeta', row, column, key, cachedValue);
2942 if (hookResult !== false) {
2943 metaManager.removeCellMeta(physicalRow, physicalColumn, key);
2944 instance.runHooks('afterRemoveCellMeta', row, column, key, cachedValue);
2945 }
2946 cachedValue = null;
2947 };
2948
2949 /**
2950 * Removes or adds one or more rows of the cell meta objects to the cell meta collections.
2951 *
2952 * @since 0.30.0
2953 * @memberof Core#
2954 * @function spliceCellsMeta
2955 * @param {number} visualIndex A visual index that specifies at what position to add/remove items.
2956 * @param {number} [deleteAmount=0] The number of items to be removed. If set to 0, no cell meta objects will be removed.
2957 * @param {...object} [cellMetaRows] The new cell meta row objects to be added to the cell meta collection.
2958 */
2959 this.spliceCellsMeta = function (visualIndex) {
2960 let deleteAmount = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
2961 for (var _len18 = arguments.length, cellMetaRows = new Array(_len18 > 2 ? _len18 - 2 : 0), _key18 = 2; _key18 < _len18; _key18++) {
2962 cellMetaRows[_key18 - 2] = arguments[_key18];
2963 }
2964 if (cellMetaRows.length > 0 && !Array.isArray(cellMetaRows[0])) {
2965 throw new Error('The 3rd argument (cellMetaRows) has to be passed as an array of cell meta objects array.');
2966 }
2967 if (deleteAmount > 0) {
2968 metaManager.removeRow(this.toPhysicalRow(visualIndex), deleteAmount);
2969 }
2970 if (cellMetaRows.length > 0) {
2971 arrayEach(cellMetaRows.reverse(), cellMetaRow => {
2972 metaManager.createRow(this.toPhysicalRow(visualIndex));
2973 arrayEach(cellMetaRow, (cellMeta, columnIndex) => this.setCellMetaObject(visualIndex, columnIndex, cellMeta));
2974 });
2975 }
2976 instance.render();
2977 };
2978
2979 /**
2980 * Set cell meta data object defined by `prop` to the corresponding params `row` and `column`.
2981 *
2982 * @memberof Core#
2983 * @function setCellMetaObject
2984 * @param {number} row Visual row index.
2985 * @param {number} column Visual column index.
2986 * @param {object} prop Meta object.
2987 */
2988 this.setCellMetaObject = function (row, column, prop) {
2989 if (typeof prop === 'object') {
2990 objectEach(prop, (value, key) => {
2991 this.setCellMeta(row, column, key, value);
2992 });
2993 }
2994 };
2995
2996 /**
2997 * Sets a property defined by the `key` property to the meta object of a cell corresponding to params `row` and `column`.
2998 *
2999 * @memberof Core#
3000 * @function setCellMeta
3001 * @param {number} row Visual row index.
3002 * @param {number} column Visual column index.
3003 * @param {string} key Property name.
3004 * @param {string} value Property value.
3005 * @fires Hooks#beforeSetCellMeta
3006 * @fires Hooks#afterSetCellMeta
3007 */
3008 this.setCellMeta = function (row, column, key, value) {
3009 const allowSetCellMeta = instance.runHooks('beforeSetCellMeta', row, column, key, value);
3010 if (allowSetCellMeta === false) {
3011 return;
3012 }
3013 let physicalRow = row;
3014 let physicalColumn = column;
3015 if (row < this.countRows()) {
3016 physicalRow = this.toPhysicalRow(row);
3017 }
3018 if (column < this.countCols()) {
3019 physicalColumn = this.toPhysicalColumn(column);
3020 }
3021 metaManager.setCellMeta(physicalRow, physicalColumn, key, value);
3022 instance.runHooks('afterSetCellMeta', row, column, key, value);
3023 };
3024
3025 /**
3026 * Get all the cells meta settings at least once generated in the table (in order of cell initialization).
3027 *
3028 * @memberof Core#
3029 * @function getCellsMeta
3030 * @returns {Array} Returns an array of ColumnSettings object instances.
3031 */
3032 this.getCellsMeta = function () {
3033 return metaManager.getCellsMeta();
3034 };
3035
3036 /**
3037 * Returns the cell properties object for the given `row` and `column` coordinates.
3038 *
3039 * @memberof Core#
3040 * @function getCellMeta
3041 * @param {number} row Visual row index.
3042 * @param {number} column Visual column index.
3043 * @returns {object} The cell properties object.
3044 * @fires Hooks#beforeGetCellMeta
3045 * @fires Hooks#afterGetCellMeta
3046 */
3047 this.getCellMeta = function (row, column) {
3048 let physicalRow = this.toPhysicalRow(row);
3049 let physicalColumn = this.toPhysicalColumn(column);
3050 if (physicalRow === null) {
3051 physicalRow = row;
3052 }
3053 if (physicalColumn === null) {
3054 physicalColumn = column;
3055 }
3056 return metaManager.getCellMeta(physicalRow, physicalColumn, {
3057 visualRow: row,
3058 visualColumn: column
3059 });
3060 };
3061
3062 /**
3063 * Returns the meta information for the provided column.
3064 *
3065 * @since 14.5.0
3066 * @memberof Core#
3067 * @function getColumnMeta
3068 * @param {number} column Visual column index.
3069 * @returns {object}
3070 */
3071 this.getColumnMeta = function (column) {
3072 return metaManager.getColumnMeta(this.toPhysicalColumn(column));
3073 };
3074
3075 /**
3076 * Returns an array of cell meta objects for specified physical row index.
3077 *
3078 * @memberof Core#
3079 * @function getCellMetaAtRow
3080 * @param {number} row Physical row index.
3081 * @returns {Array}
3082 */
3083 this.getCellMetaAtRow = function (row) {
3084 return metaManager.getCellsMetaAtRow(row);
3085 };
3086
3087 /**
3088 * Checks if your [data format](@/guides/getting-started/binding-to-data/binding-to-data.md#compatible-data-types)
3089 * and [configuration options](@/guides/getting-started/configuration-options/configuration-options.md)
3090 * allow for changing the number of columns.
3091 *
3092 * Returns `false` when your data is an array of objects,
3093 * or when you use the [`columns`](@/api/options.md#columns) option.
3094 * Otherwise, returns `true`.
3095 *
3096 * @memberof Core#
3097 * @function isColumnModificationAllowed
3098 * @returns {boolean}
3099 */
3100 this.isColumnModificationAllowed = function () {
3101 return !(instance.dataType === 'object' || tableMeta.columns);
3102 };
3103
3104 /**
3105 * Returns the cell renderer function by given `row` and `column` arguments.
3106 *
3107 * @memberof Core#
3108 * @function getCellRenderer
3109 * @param {number|object} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
3110 * @param {number} column Visual column index.
3111 * @returns {Function} Returns the renderer function.
3112 * @example
3113 * ```js
3114 * // Get cell renderer using `row` and `column` coordinates.
3115 * hot.getCellRenderer(1, 1);
3116 * // Get cell renderer using cell meta object.
3117 * hot.getCellRenderer(hot.getCellMeta(1, 1));
3118 * ```
3119 */
3120 this.getCellRenderer = function (rowOrMeta, column) {
3121 const cellRenderer = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).renderer : rowOrMeta.renderer;
3122 if (typeof cellRenderer === 'string') {
3123 return getRenderer(cellRenderer);
3124 }
3125 return isUndefined(cellRenderer) ? getRenderer('text') : cellRenderer;
3126 };
3127
3128 /**
3129 * Returns the cell editor class by the provided `row` and `column` arguments.
3130 *
3131 * @memberof Core#
3132 * @function getCellEditor
3133 * @param {number} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
3134 * @param {number} column Visual column index.
3135 * @returns {Function|boolean} Returns the editor class or `false` is cell editor is disabled.
3136 * @example
3137 * ```js
3138 * // Get cell editor class using `row` and `column` coordinates.
3139 * hot.getCellEditor(1, 1);
3140 * // Get cell editor class using cell meta object.
3141 * hot.getCellEditor(hot.getCellMeta(1, 1));
3142 * ```
3143 */
3144 this.getCellEditor = function (rowOrMeta, column) {
3145 const cellEditor = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).editor : rowOrMeta.editor;
3146 if (typeof cellEditor === 'string') {
3147 return getEditor(cellEditor);
3148 }
3149 return isUndefined(cellEditor) ? getEditor('text') : cellEditor;
3150 };
3151
3152 /**
3153 * Returns the cell validator by `row` and `column`.
3154 *
3155 * @memberof Core#
3156 * @function getCellValidator
3157 * @param {number|object} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
3158 * @param {number} column Visual column index.
3159 * @returns {Function|RegExp|undefined} The validator function.
3160 * @example
3161 * ```js
3162 * // Get cell validator using `row` and `column` coordinates.
3163 * hot.getCellValidator(1, 1);
3164 * // Get cell validator using cell meta object.
3165 * hot.getCellValidator(hot.getCellMeta(1, 1));
3166 * ```
3167 */
3168 this.getCellValidator = function (rowOrMeta, column) {
3169 const cellValidator = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).validator : rowOrMeta.validator;
3170 if (typeof cellValidator === 'string') {
3171 return getValidator(cellValidator);
3172 }
3173 return cellValidator;
3174 };
3175
3176 /**
3177 * Validates every cell in the data set,
3178 * using a [validator function](@/guides/cell-functions/cell-validator/cell-validator.md) configured for each cell.
3179 *
3180 * Doesn't validate cells that are currently [trimmed](@/guides/rows/row-trimming/row-trimming.md),
3181 * [hidden](@/guides/rows/row-hiding/row-hiding.md), or [filtered](@/guides/columns/column-filter/column-filter.md),
3182 * as such cells are not included in the data set until you bring them back again.
3183 *
3184 * After the validation, the `callback` function is fired, with the `valid` argument set to:
3185 * - `true` for valid cells
3186 * - `false` for invalid cells
3187 *
3188 * @memberof Core#
3189 * @function validateCells
3190 * @param {Function} [callback] The callback function.
3191 * @example
3192 * ```js
3193 * hot.validateCells((valid) => {
3194 * if (valid) {
3195 * // ... code for validated cells
3196 * }
3197 * })
3198 * ```
3199 */
3200 this.validateCells = function (callback) {
3201 this._validateCells(callback);
3202 };
3203
3204 /**
3205 * Validates rows using their validator functions and calls callback when finished.
3206 *
3207 * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
3208 * would equal `true`.
3209 *
3210 * @memberof Core#
3211 * @function validateRows
3212 * @param {Array} [rows] Array of validation target visual row indexes.
3213 * @param {Function} [callback] The callback function.
3214 * @example
3215 * ```js
3216 * hot.validateRows([3, 4, 5], (valid) => {
3217 * if (valid) {
3218 * // ... code for validated rows
3219 * }
3220 * })
3221 * ```
3222 */
3223 this.validateRows = function (rows, callback) {
3224 if (!Array.isArray(rows)) {
3225 throw new Error('validateRows parameter `rows` must be an array');
3226 }
3227 this._validateCells(callback, rows);
3228 };
3229
3230 /**
3231 * Validates columns using their validator functions and calls callback when finished.
3232 *
3233 * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
3234 * would equal `true`.
3235 *
3236 * @memberof Core#
3237 * @function validateColumns
3238 * @param {Array} [columns] Array of validation target visual columns indexes.
3239 * @param {Function} [callback] The callback function.
3240 * @example
3241 * ```js
3242 * hot.validateColumns([3, 4, 5], (valid) => {
3243 * if (valid) {
3244 * // ... code for validated columns
3245 * }
3246 * })
3247 * ```
3248 */
3249 this.validateColumns = function (columns, callback) {
3250 if (!Array.isArray(columns)) {
3251 throw new Error('validateColumns parameter `columns` must be an array');
3252 }
3253 this._validateCells(callback, undefined, columns);
3254 };
3255
3256 /**
3257 * Validates all cells using their validator functions and calls callback when finished.
3258 *
3259 * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it would equal `true`.
3260 *
3261 * Private use intended.
3262 *
3263 * @private
3264 * @memberof Core#
3265 * @function _validateCells
3266 * @param {Function} [callback] The callback function.
3267 * @param {Array} [rows] An array of validation target visual row indexes.
3268 * @param {Array} [columns] An array of validation target visual column indexes.
3269 */
3270 this._validateCells = function (callback, rows, columns) {
3271 const waitingForValidator = new ValidatorsQueue();
3272 if (callback) {
3273 waitingForValidator.onQueueEmpty = callback;
3274 }
3275 let i = instance.countRows() - 1;
3276 while (i >= 0) {
3277 if (rows !== undefined && rows.indexOf(i) === -1) {
3278 i -= 1;
3279 continue;
3280 }
3281 let j = instance.countCols() - 1;
3282 while (j >= 0) {
3283 if (columns !== undefined && columns.indexOf(j) === -1) {
3284 j -= 1;
3285 continue;
3286 }
3287 waitingForValidator.addValidatorToQueue();
3288 instance.validateCell(instance.getDataAtCell(i, j), instance.getCellMeta(i, j), result => {
3289 if (typeof result !== 'boolean') {
3290 throw new Error('Validation error: result is not boolean');
3291 }
3292 if (result === false) {
3293 waitingForValidator.valid = false;
3294 }
3295 waitingForValidator.removeValidatorFormQueue();
3296 }, 'validateCells');
3297 j -= 1;
3298 }
3299 i -= 1;
3300 }
3301 waitingForValidator.checkIfQueueIsEmpty();
3302 };
3303
3304 /**
3305 * Returns an array of row headers' values (if they are enabled). If param `row` was given, it returns the header of the given row as a string.
3306 *
3307 * @memberof Core#
3308 * @function getRowHeader
3309 * @param {number} [row] Visual row index.
3310 * @fires Hooks#modifyRowHeader
3311 * @returns {Array|string|number} Array of header values / single header value.
3312 */
3313 this.getRowHeader = function (row) {
3314 let rowHeader = tableMeta.rowHeaders;
3315 let physicalRow = row;
3316 if (physicalRow !== undefined) {
3317 physicalRow = instance.runHooks('modifyRowHeader', physicalRow);
3318 }
3319 if (physicalRow === undefined) {
3320 rowHeader = [];
3321 rangeEach(instance.countRows() - 1, i => {
3322 rowHeader.push(instance.getRowHeader(i));
3323 });
3324 } else if (Array.isArray(rowHeader) && rowHeader[physicalRow] !== undefined) {
3325 rowHeader = rowHeader[physicalRow];
3326 } else if (isFunction(rowHeader)) {
3327 rowHeader = rowHeader(physicalRow);
3328 } else if (rowHeader && typeof rowHeader !== 'string' && typeof rowHeader !== 'number') {
3329 rowHeader = physicalRow + 1;
3330 }
3331 return rowHeader;
3332 };
3333
3334 /**
3335 * Returns information about if this table is configured to display row headers.
3336 *
3337 * @memberof Core#
3338 * @function hasRowHeaders
3339 * @returns {boolean} `true` if the instance has the row headers enabled, `false` otherwise.
3340 */
3341 this.hasRowHeaders = function () {
3342 return !!tableMeta.rowHeaders;
3343 };
3344
3345 /**
3346 * Returns information about if this table is configured to display column headers.
3347 *
3348 * @memberof Core#
3349 * @function hasColHeaders
3350 * @returns {boolean} `true` if the instance has the column headers enabled, `false` otherwise.
3351 */
3352 this.hasColHeaders = function () {
3353 if (tableMeta.colHeaders !== undefined && tableMeta.colHeaders !== null) {
3354 // Polymer has empty value = null
3355 return !!tableMeta.colHeaders;
3356 }
3357 for (let i = 0, ilen = instance.countCols(); i < ilen; i++) {
3358 if (instance.getColHeader(i)) {
3359 return true;
3360 }
3361 }
3362 return false;
3363 };
3364
3365 /**
3366 * Gets the values of column headers (if column headers are [enabled](@/api/options.md#colheaders)).
3367 *
3368 * To get an array with the values of all
3369 * [bottom-most](@/guides/cell-features/clipboard/clipboard.md#copy-with-headers) column headers,
3370 * call `getColHeader()` with no arguments.
3371 *
3372 * To get the value of the bottom-most header of a specific column, use the `column` parameter.
3373 *
3374 * To get the value of a [specific-level](@/guides/columns/column-groups/column-groups.md) header
3375 * of a specific column, use the `column` and `headerLevel` parameters.
3376 *
3377 * Read more:
3378 * - [Guides: Column groups](@/guides/columns/column-groups/column-groups.md)
3379 * - [Options: `colHeaders`](@/api/options.md#colheaders)
3380 * - [Guides: Copy with headers](@/guides/cell-features/clipboard/clipboard.md#copy-with-headers)
3381 *
3382 * ```js
3383 * // get the contents of all bottom-most column headers
3384 * hot.getColHeader();
3385 *
3386 * // get the contents of the bottom-most header of a specific column
3387 * hot.getColHeader(5);
3388 *
3389 * // get the contents of a specific column header at a specific level
3390 * hot.getColHeader(5, -2);
3391 * ```
3392 *
3393 * @memberof Core#
3394 * @function getColHeader
3395 * @param {number} [column] A visual column index.
3396 * @param {number} [headerLevel=-1] (Since 12.3.0) Header level index. Accepts positive (0 to n)
3397 * and negative (-1 to -n) values. For positive values, 0 points to the
3398 * topmost header. For negative values, -1 points to the bottom-most
3399 * header (the header closest to the cells).
3400 * @fires Hooks#modifyColHeader
3401 * @fires Hooks#modifyColumnHeaderValue
3402 * @returns {Array|string|number} Column header values.
3403 */
3404 this.getColHeader = function (column) {
3405 let headerLevel = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -1;
3406 const columnIndex = instance.runHooks('modifyColHeader', column);
3407 if (columnIndex === undefined) {
3408 const out = [];
3409 const ilen = instance.countCols();
3410 for (let i = 0; i < ilen; i++) {
3411 out.push(instance.getColHeader(i));
3412 }
3413 return out;
3414 }
3415 let result = tableMeta.colHeaders;
3416 const translateVisualIndexToColumns = function (visualColumnIndex) {
3417 const arr = [];
3418 const columnsLen = instance.countCols();
3419 let index = 0;
3420 for (; index < columnsLen; index++) {
3421 if (isFunction(tableMeta.columns) && tableMeta.columns(index)) {
3422 arr.push(index);
3423 }
3424 }
3425 return arr[visualColumnIndex];
3426 };
3427 const physicalColumn = instance.toPhysicalColumn(columnIndex);
3428 const prop = translateVisualIndexToColumns(physicalColumn);
3429 if (tableMeta.colHeaders === false) {
3430 result = null;
3431 } else if (tableMeta.columns && isFunction(tableMeta.columns) && tableMeta.columns(prop) && tableMeta.columns(prop).title) {
3432 result = tableMeta.columns(prop).title;
3433 } else if (tableMeta.columns && tableMeta.columns[physicalColumn] && tableMeta.columns[physicalColumn].title) {
3434 result = tableMeta.columns[physicalColumn].title;
3435 } else if (Array.isArray(tableMeta.colHeaders) && tableMeta.colHeaders[physicalColumn] !== undefined) {
3436 result = tableMeta.colHeaders[physicalColumn];
3437 } else if (isFunction(tableMeta.colHeaders)) {
3438 result = tableMeta.colHeaders(physicalColumn);
3439 } else if (tableMeta.colHeaders && typeof tableMeta.colHeaders !== 'string' && typeof tableMeta.colHeaders !== 'number') {
3440 result = spreadsheetColumnLabel(columnIndex); // see #1458
3441 }
3442 result = instance.runHooks('modifyColumnHeaderValue', result, column, headerLevel);
3443 return result;
3444 };
3445
3446 /**
3447 * Return column width from settings (no guessing). Private use intended.
3448 *
3449 * @private
3450 * @memberof Core#
3451 * @function _getColWidthFromSettings
3452 * @param {number} col Visual col index.
3453 * @returns {number}
3454 */
3455 this._getColWidthFromSettings = function (col) {
3456 let width;
3457
3458 // We currently don't support cell meta objects for headers (negative values)
3459 if (col >= 0) {
3460 const cellProperties = instance.getCellMeta(0, col);
3461 width = cellProperties.width;
3462 }
3463 if (width === undefined || width === tableMeta.width) {
3464 width = tableMeta.colWidths;
3465 }
3466 if (width !== undefined && width !== null) {
3467 switch (typeof width) {
3468 case 'object':
3469 // array
3470 width = width[col];
3471 break;
3472 case 'function':
3473 width = width(col);
3474 break;
3475 default:
3476 break;
3477 }
3478 if (typeof width === 'string') {
3479 width = parseInt(width, 10);
3480 }
3481 }
3482 return width;
3483 };
3484
3485 /**
3486 * Returns the width of the requested column.
3487 *
3488 * @memberof Core#
3489 * @function getColWidth
3490 * @param {number} column Visual column index.
3491 * @returns {number} Column width.
3492 * @fires Hooks#modifyColWidth
3493 */
3494 this.getColWidth = function (column) {
3495 let width = instance._getColWidthFromSettings(column);
3496 width = instance.runHooks('modifyColWidth', width, column);
3497 if (width === undefined) {
3498 width = DEFAULT_COLUMN_WIDTH;
3499 }
3500 return width;
3501 };
3502
3503 /**
3504 * Return row height from settings (no guessing). Private use intended.
3505 *
3506 * @private
3507 * @memberof Core#
3508 * @function _getRowHeightFromSettings
3509 * @param {number} row Visual row index.
3510 * @returns {number}
3511 */
3512 this._getRowHeightFromSettings = function (row) {
3513 let height = tableMeta.rowHeights;
3514 if (height !== undefined && height !== null) {
3515 switch (typeof height) {
3516 case 'object':
3517 // array
3518 height = height[row];
3519 break;
3520 case 'function':
3521 height = height(row);
3522 break;
3523 default:
3524 break;
3525 }
3526 if (typeof height === 'string') {
3527 height = parseInt(height, 10);
3528 }
3529 }
3530 return height;
3531 };
3532
3533 /**
3534 * Returns a row's height, as recognized by Handsontable.
3535 *
3536 * Depending on your configuration, the method returns (in order of priority):
3537 * 1. The row height set by the [`ManualRowResize`](@/api/manualRowResize.md) plugin
3538 * (if the plugin is enabled).
3539 * 2. The row height set by the [`rowHeights`](@/api/options.md#rowheights) configuration option
3540 * (if the option is set).
3541 * 3. The row height as measured in the DOM by the [`AutoRowSize`](@/api/autoRowSize.md) plugin
3542 * (if the plugin is enabled).
3543 * 4. `undefined`, if neither [`ManualRowResize`](@/api/manualRowResize.md),
3544 * nor [`rowHeights`](@/api/options.md#rowheights),
3545 * nor [`AutoRowSize`](@/api/autoRowSize.md) is used.
3546 *
3547 * The height returned includes 1 px of the row's bottom border.
3548 *
3549 * Mind that this method is different from the
3550 * [`getRowHeight()`](@/api/autoRowSize.md#getrowheight) method
3551 * of the [`AutoRowSize`](@/api/autoRowSize.md) plugin.
3552 *
3553 * @memberof Core#
3554 * @function getRowHeight
3555 * @param {number} row A visual row index.
3556 * @returns {number|undefined} The height of the specified row, in pixels.
3557 * @fires Hooks#modifyRowHeight
3558 */
3559 this.getRowHeight = function (row) {
3560 let height = instance._getRowHeightFromSettings(row);
3561 height = instance.runHooks('modifyRowHeight', height, row);
3562 return height;
3563 };
3564
3565 /**
3566 * Returns the total number of rows in the data source.
3567 *
3568 * @memberof Core#
3569 * @function countSourceRows
3570 * @returns {number} Total number of rows.
3571 */
3572 this.countSourceRows = function () {
3573 return dataSource.countRows();
3574 };
3575
3576 /**
3577 * Returns the total number of columns in the data source.
3578 *
3579 * @memberof Core#
3580 * @function countSourceCols
3581 * @returns {number} Total number of columns.
3582 */
3583 this.countSourceCols = function () {
3584 return dataSource.countFirstRowKeys();
3585 };
3586
3587 /**
3588 * Returns the total number of visual rows in the table.
3589 *
3590 * @memberof Core#
3591 * @function countRows
3592 * @returns {number} Total number of rows.
3593 */
3594 this.countRows = function () {
3595 return datamap.getLength();
3596 };
3597
3598 /**
3599 * Returns the total number of visible columns in the table.
3600 *
3601 * @memberof Core#
3602 * @function countCols
3603 * @returns {number} Total number of columns.
3604 */
3605 this.countCols = function () {
3606 const maxCols = tableMeta.maxCols;
3607 const dataLen = this.columnIndexMapper.getNotTrimmedIndexesLength();
3608 return Math.min(maxCols, dataLen);
3609 };
3610
3611 /**
3612 * Returns the number of rendered rows including rows that are partially or fully rendered
3613 * outside the table viewport.
3614 *
3615 * @memberof Core#
3616 * @function countRenderedRows
3617 * @returns {number} Returns -1 if table is not visible.
3618 */
3619 this.countRenderedRows = function () {
3620 return instance.view._wt.drawn ? instance.view._wt.wtTable.getRenderedRowsCount() : -1;
3621 };
3622
3623 /**
3624 * Returns the number of rendered rows that are only visible in the table viewport.
3625 * The rows that are partially visible are not counted.
3626 *
3627 * @memberof Core#
3628 * @function countVisibleRows
3629 * @returns {number} Number of visible rows or -1.
3630 */
3631 this.countVisibleRows = function () {
3632 return instance.view._wt.drawn ? instance.view._wt.wtTable.getVisibleRowsCount() : -1;
3633 };
3634
3635 /**
3636 * Returns the number of rendered rows including columns that are partially or fully rendered
3637 * outside the table viewport.
3638 *
3639 * @memberof Core#
3640 * @function countRenderedCols
3641 * @returns {number} Returns -1 if table is not visible.
3642 */
3643 this.countRenderedCols = function () {
3644 return instance.view._wt.drawn ? instance.view._wt.wtTable.getRenderedColumnsCount() : -1;
3645 };
3646
3647 /**
3648 * Returns the number of rendered columns that are only visible in the table viewport.
3649 * The columns that are partially visible are not counted.
3650 *
3651 * @memberof Core#
3652 * @function countVisibleCols
3653 * @returns {number} Number of visible columns or -1.
3654 */
3655 this.countVisibleCols = function () {
3656 return instance.view._wt.drawn ? instance.view._wt.wtTable.getVisibleColumnsCount() : -1;
3657 };
3658
3659 /**
3660 * Returns the number of rendered row headers.
3661 *
3662 * @since 14.0.0
3663 * @memberof Core#
3664 * @function countRowHeaders
3665 * @returns {number} Number of row headers.
3666 */
3667 this.countRowHeaders = function () {
3668 return this.view.getRowHeadersCount();
3669 };
3670
3671 /**
3672 * Returns the number of rendered column headers.
3673 *
3674 * @since 14.0.0
3675 * @memberof Core#
3676 * @function countColHeaders
3677 * @returns {number} Number of column headers.
3678 */
3679 this.countColHeaders = function () {
3680 return this.view.getColumnHeadersCount();
3681 };
3682
3683 /**
3684 * Returns the number of empty rows. If the optional ending parameter is `true`, returns the
3685 * number of empty rows at the bottom of the table.
3686 *
3687 * @memberof Core#
3688 * @function countEmptyRows
3689 * @param {boolean} [ending=false] If `true`, will only count empty rows at the end of the data source.
3690 * @returns {number} Count empty rows.
3691 */
3692 this.countEmptyRows = function () {
3693 let ending = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
3694 let emptyRows = 0;
3695 rangeEachReverse(instance.countRows() - 1, visualIndex => {
3696 if (instance.isEmptyRow(visualIndex)) {
3697 emptyRows += 1;
3698 } else if (ending === true) {
3699 return false;
3700 }
3701 });
3702 return emptyRows;
3703 };
3704
3705 /**
3706 * Returns the number of empty columns. If the optional ending parameter is `true`, returns the number of empty
3707 * columns at right hand edge of the table.
3708 *
3709 * @memberof Core#
3710 * @function countEmptyCols
3711 * @param {boolean} [ending=false] If `true`, will only count empty columns at the end of the data source row.
3712 * @returns {number} Count empty cols.
3713 */
3714 this.countEmptyCols = function () {
3715 let ending = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
3716 let emptyColumns = 0;
3717 rangeEachReverse(instance.countCols() - 1, visualIndex => {
3718 if (instance.isEmptyCol(visualIndex)) {
3719 emptyColumns += 1;
3720 } else if (ending === true) {
3721 return false;
3722 }
3723 });
3724 return emptyColumns;
3725 };
3726
3727 /**
3728 * Check if all cells in the row declared by the `row` argument are empty.
3729 *
3730 * @memberof Core#
3731 * @function isEmptyRow
3732 * @param {number} row Visual row index.
3733 * @returns {boolean} `true` if the row at the given `row` is empty, `false` otherwise.
3734 */
3735 this.isEmptyRow = function (row) {
3736 return tableMeta.isEmptyRow.call(instance, row);
3737 };
3738
3739 /**
3740 * Check if all cells in the the column declared by the `column` argument are empty.
3741 *
3742 * @memberof Core#
3743 * @function isEmptyCol
3744 * @param {number} column Column index.
3745 * @returns {boolean} `true` if the column at the given `col` is empty, `false` otherwise.
3746 */
3747 this.isEmptyCol = function (column) {
3748 return tableMeta.isEmptyCol.call(instance, column);
3749 };
3750
3751 /**
3752 * Select a single cell, or a single range of adjacent cells.
3753 *
3754 * To select a cell, pass its visual row and column indexes, for example: `selectCell(2, 4)`.
3755 *
3756 * To select a range, pass the visual indexes of the first and last cell in the range, for example: `selectCell(2, 4, 3, 5)`.
3757 *
3758 * If your columns have properties, you can pass those properties' values instead of column indexes, for example: `selectCell(2, 'first_name')`.
3759 *
3760 * By default, `selectCell()` also:
3761 * - Scrolls the viewport to the newly-selected cells.
3762 * - Switches the keyboard focus to Handsontable (by calling Handsontable's [`listen()`](#listen) method).
3763 *
3764 * @example
3765 * ```js
3766 * // select a single cell
3767 * hot.selectCell(2, 4);
3768 *
3769 * // select a range of cells
3770 * hot.selectCell(2, 4, 3, 5);
3771 *
3772 * // select a single cell, using a column property
3773 * hot.selectCell(2, 'first_name');
3774 *
3775 * // select a range of cells, using column properties
3776 * hot.selectCell(2, 'first_name', 3, 'last_name');
3777 *
3778 * // select a range of cells, without scrolling to them
3779 * hot.selectCell(2, 4, 3, 5, false);
3780 *
3781 * // select a range of cells, without switching the keyboard focus to Handsontable
3782 * hot.selectCell(2, 4, 3, 5, null, false);
3783 * ```
3784 *
3785 * @memberof Core#
3786 * @function selectCell
3787 * @param {number} row A visual row index.
3788 * @param {number|string} column A visual column index (`number`), or a column property's value (`string`).
3789 * @param {number} [endRow] If selecting a range: the visual row index of the last cell in the range.
3790 * @param {number|string} [endColumn] If selecting a range: the visual column index (or a column property's value) of the last cell in the range.
3791 * @param {boolean} [scrollToCell=true] `true`: scroll the viewport to the newly-selected cells. `false`: keep the previous viewport.
3792 * @param {boolean} [changeListener=true] `true`: switch the keyboard focus to Handsontable. `false`: keep the previous keyboard focus.
3793 * @returns {boolean} `true`: the selection was successful, `false`: the selection failed.
3794 */
3795 this.selectCell = function (row, column, endRow, endColumn) {
3796 let scrollToCell = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
3797 let changeListener = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
3798 if (isUndefined(row) || isUndefined(column)) {
3799 return false;
3800 }
3801 return this.selectCells([[row, column, endRow, endColumn]], scrollToCell, changeListener);
3802 };
3803
3804 /**
3805 * Select multiple cells or ranges of cells, adjacent or non-adjacent.
3806 *
3807 * You can pass one of the below:
3808 * - An array of arrays (which matches the output of Handsontable's [`getSelected()`](#getselected) method).
3809 * - An array of [`CellRange`](@/api/cellRange.md) objects (which matches the output of Handsontable's [`getSelectedRange()`](#getselectedrange) method).
3810 *
3811 * To select multiple cells, pass the visual row and column indexes of each cell, for example: `hot.selectCells([[1, 1], [5, 5]])`.
3812 *
3813 * To select multiple ranges, pass the visual indexes of the first and last cell in each range, for example: `hot.selectCells([[1, 1, 2, 2], [6, 2, 0, 2]])`.
3814 *
3815 * If your columns have properties, you can pass those properties' values instead of column indexes, for example: `hot.selectCells([[1, 'first_name'], [5, 'last_name']])`.
3816 *
3817 * By default, `selectCell()` also:
3818 * - Scrolls the viewport to the newly-selected cells.
3819 * - Switches the keyboard focus to Handsontable (by calling Handsontable's [`listen()`](#listen) method).
3820 *
3821 * @example
3822 * ```js
3823 * // select non-adjacent cells
3824 * hot.selectCells([[1, 1], [5, 5], [10, 10]]);
3825 *
3826 * // select non-adjacent ranges of cells
3827 * hot.selectCells([[1, 1, 2, 2], [10, 10, 20, 20]]);
3828 *
3829 * // select cells and ranges of cells
3830 * hot.selectCells([[1, 1, 2, 2], [3, 3], [6, 2, 0, 2]]);
3831 *
3832 * // select cells, using column properties
3833 * hot.selectCells([[1, 'id', 2, 'first_name'], [3, 'full_name'], [6, 'last_name', 0, 'first_name']]);
3834 *
3835 * // select multiple ranges, using an array of `CellRange` objects
3836 * const selected = hot.getSelectedRange();
3837 *
3838 * selected[0].from.row = 0;
3839 * selected[0].from.col = 0;
3840 * selected[0].to.row = 5;
3841 * selected[0].to.col = 5;
3842 *
3843 * selected[1].from.row = 10;
3844 * selected[1].from.col = 10;
3845 * selected[1].to.row = 20;
3846 * selected[1].to.col = 20;
3847 *
3848 * hot.selectCells(selected);
3849 * ```
3850 *
3851 * @memberof Core#
3852 * @since 0.38.0
3853 * @function selectCells
3854 * @param {Array[]|CellRange[]} coords Visual coordinates,
3855 * passed either as an array of arrays (`[[rowStart, columnStart, rowEnd, columnEnd], ...]`)
3856 * or as an array of [`CellRange`](@/api/cellRange.md) objects.
3857 * @param {boolean} [scrollToCell=true] `true`: scroll the viewport to the newly-selected cells. `false`: keep the previous viewport.
3858 * @param {boolean} [changeListener=true] `true`: switch the keyboard focus to Handsontable. `false`: keep the previous keyboard focus.
3859 * @returns {boolean} `true`: the selection was successful, `false`: the selection failed.
3860 */
3861 this.selectCells = function () {
3862 let coords = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [[]];
3863 let scrollToCell = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
3864 let changeListener = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
3865 if (scrollToCell === false) {
3866 viewportScroller.suspend();
3867 }
3868 const wasSelected = selection.selectCells(coords);
3869 if (wasSelected && changeListener) {
3870 instance.listen();
3871 }
3872 viewportScroller.resume();
3873 return wasSelected;
3874 };
3875
3876 /**
3877 * Select column specified by `startColumn` visual index, column property or a range of columns finishing at `endColumn`.
3878 *
3879 * @example
3880 * ```js
3881 * // Select column using visual index.
3882 * hot.selectColumns(1);
3883 * // Select column using column property.
3884 * hot.selectColumns('id');
3885 * // Select range of columns using visual indexes.
3886 * hot.selectColumns(1, 4);
3887 * // Select range of columns using visual indexes and mark the first header as highlighted.
3888 * hot.selectColumns(1, 2, -1);
3889 * // Select range of columns using visual indexes and mark the second cell as highlighted.
3890 * hot.selectColumns(2, 1, 1);
3891 * // Select range of columns using visual indexes and move the focus position somewhere in the middle of the range.
3892 * hot.selectColumns(2, 5, { row: 2, col: 3 });
3893 * // Select range of columns using column properties.
3894 * hot.selectColumns('id', 'last_name');
3895 * ```
3896 *
3897 * @memberof Core#
3898 * @since 0.38.0
3899 * @function selectColumns
3900 * @param {number} startColumn The visual column index from which the selection starts.
3901 * @param {number} [endColumn=startColumn] The visual column index to which the selection finishes. If `endColumn`
3902 * is not defined the column defined by `startColumn` will be selected.
3903 * @param {number | { row: number, col: number } | CellCoords} [focusPosition=0] The argument allows changing the cell/header focus
3904 * position. The value can take visual row index from -N to N, where negative values point to the headers and positive
3905 * values point to the cell range. An object with `row` and `col` properties also can be passed to change the focus
3906 * position horizontally.
3907 * @returns {boolean} `true` if selection was successful, `false` otherwise.
3908 */
3909 this.selectColumns = function (startColumn) {
3910 let endColumn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : startColumn;
3911 let focusPosition = arguments.length > 2 ? arguments[2] : undefined;
3912 return selection.selectColumns(startColumn, endColumn, focusPosition);
3913 };
3914
3915 /**
3916 * Select row specified by `startRow` visual index or a range of rows finishing at `endRow`.
3917 *
3918 * @example
3919 * ```js
3920 * // Select row using visual index.
3921 * hot.selectRows(1);
3922 * // select a range of rows, using visual indexes.
3923 * hot.selectRows(1, 4);
3924 * // select a range of rows, using visual indexes, and mark the header as highlighted.
3925 * hot.selectRows(1, 2, -1);
3926 * // Select range of rows using visual indexes and mark the second cell as highlighted.
3927 * hot.selectRows(2, 1, 1);
3928 * // Select range of rows using visual indexes and move the focus position somewhere in the middle of the range.
3929 * hot.selectRows(2, 5, { row: 2, col: 3 });
3930 * ```
3931 *
3932 * @memberof Core#
3933 * @since 0.38.0
3934 * @function selectRows
3935 * @param {number} startRow The visual row index from which the selection starts.
3936 * @param {number} [endRow=startRow] The visual row index to which the selection finishes. If `endRow`
3937 * is not defined the row defined by `startRow` will be selected.
3938 * @param {number | { row: number, col: number } | CellCoords} [focusPosition=0] The argument allows changing the cell/header focus
3939 * position. The value can take visual row index from -N to N, where negative values point to the headers and positive
3940 * values point to the cell range. An object with `row` and `col` properties also can be passed to change the focus
3941 * position vertically.
3942 * @returns {boolean} `true` if selection was successful, `false` otherwise.
3943 */
3944 this.selectRows = function (startRow) {
3945 let endRow = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : startRow;
3946 let focusPosition = arguments.length > 2 ? arguments[2] : undefined;
3947 return selection.selectRows(startRow, endRow, focusPosition);
3948 };
3949
3950 /**
3951 * Deselects the current cell selection on the table.
3952 *
3953 * @memberof Core#
3954 * @function deselectCell
3955 */
3956 this.deselectCell = function () {
3957 selection.deselect();
3958 };
3959
3960 /**
3961 * Select all cells in the table excluding headers and corner elements.
3962 *
3963 * The previous selection is overwritten.
3964 *
3965 * ```js
3966 * // Select all cells in the table along with row headers, including all headers and the corner cell.
3967 * // Doesn't select column headers and corner elements.
3968 * hot.selectAll();
3969 *
3970 * // Select all cells in the table, including row headers but excluding the corner cell and column headers.
3971 * hot.selectAll(true, false);
3972 *
3973 * // Select all cells in the table, including all headers and the corner cell, but move the focus.
3974 * // highlight to position 2, 1
3975 * hot.selectAll(-2, -1, {
3976 * focusPosition: { row: 2, col: 1 }
3977 * });
3978 *
3979 * // Select all cells in the table, without headers and corner elements.
3980 * hot.selectAll(false);
3981 * ```
3982 *
3983 * @since 0.38.2
3984 * @memberof Core#
3985 * @function selectAll
3986 * @param {boolean} [includeRowHeaders=false] `true` If the selection should include the row headers,
3987 * `false` otherwise.
3988 * @param {boolean} [includeColumnHeaders=false] `true` If the selection should include the column
3989 * headers, `false` otherwise.
3990 *
3991 * @param {object} [options] Additional object with options. Since 14.0.0
3992 * @param {{row: number, col: number} | boolean} [options.focusPosition] The argument allows changing the cell/header
3993 * focus position. The value takes an object with a `row` and `col` properties from -N to N, where
3994 * negative values point to the headers and positive values point to the cell range. If `false`, the focus
3995 * position won't be changed. Example:
3996 * ```js
3997 * hot.selectAll(0, 0, {
3998 * focusPosition: { row: 0, col: 1 },
3999 * disableHeadersHighlight: true
4000 * })
4001 * ```
4002 *
4003 * @param {boolean} [options.disableHeadersHighlight] If `true`, disables highlighting the headers even when
4004 * the logical coordinates points on them.
4005 */
4006 this.selectAll = function () {
4007 let includeRowHeaders = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
4008 let includeColumnHeaders = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : includeRowHeaders;
4009 let options = arguments.length > 2 ? arguments[2] : undefined;
4010 viewportScroller.skipNextScrollCycle();
4011 selection.selectAll(includeRowHeaders, includeColumnHeaders, options);
4012 };
4013 const getIndexToScroll = (indexMapper, visualIndex) => {
4014 // Looking for a visual index on the right and then (when not found) on the left.
4015 return indexMapper.getNearestNotHiddenIndex(visualIndex, 1, true);
4016 };
4017
4018 /**
4019 * Scroll viewport to coordinates specified by the `row` and/or `col` object properties.
4020 *
4021 * ```js
4022 * // scroll the viewport to the visual row index (leave the horizontal scroll untouched)
4023 * hot.scrollViewportTo({ row: 50 });
4024 *
4025 * // scroll the viewport to the passed coordinates so that the cell at 50, 50 will be snapped to
4026 * // the bottom-end table's edge.
4027 * hot.scrollViewportTo({
4028 * row: 50,
4029 * col: 50,
4030 * verticalSnap: 'bottom',
4031 * horizontalSnap: 'end',
4032 * });
4033 * ```
4034 *
4035 * @memberof Core#
4036 * @function scrollViewportTo
4037 * @param {object} options A dictionary containing the following parameters:
4038 * @param {number} [options.row] Specifies the number of visual rows along the Y axis to scroll the viewport.
4039 * @param {number} [options.col] Specifies the number of visual columns along the X axis to scroll the viewport.
4040 * @param {'top' | 'bottom'} [options.verticalSnap] Determines to which edge of the table the viewport will be scrolled based on the passed coordinates.
4041 * This option is a string which must take one of the following values:
4042 * - `top`: The viewport will be scrolled to a row in such a way that it will be positioned on the top of the viewport;
4043 * - `bottom`: The viewport will be scrolled to a row in such a way that it will be positioned on the bottom of the viewport;
4044 * - If the property is not defined the vertical auto-snapping is enabled. Depending on where the viewport is scrolled from, a row will
4045 * be positioned at the top or bottom of the viewport.
4046 * @param {'start' | 'end'} [options.horizontalSnap] Determines to which edge of the table the viewport will be scrolled based on the passed coordinates.
4047 * This option is a string which must take one of the following values:
4048 * - `start`: The viewport will be scrolled to a column in such a way that it will be positioned on the start (left edge or right, if the layout direction is set to `rtl`) of the viewport;
4049 * - `end`: The viewport will be scrolled to a column in such a way that it will be positioned on the end (right edge or left, if the layout direction is set to `rtl`) of the viewport;
4050 * - If the property is not defined the horizontal auto-snapping is enabled. Depending on where the viewport is scrolled from, a column will
4051 * be positioned at the start or end of the viewport.
4052 * @param {boolean} [options.considerHiddenIndexes=true] If `true`, we handle visual indexes, otherwise we handle only indexes which
4053 * may be rendered when they are in the viewport (we don't consider hidden indexes as they aren't rendered).
4054 * @returns {boolean} `true` if viewport was scrolled, `false` otherwise.
4055 */
4056 this.scrollViewportTo = function (options) {
4057 var _options;
4058 // Support for backward compatibility arguments: (row, col, snapToBottom, snapToRight, considerHiddenIndexes)
4059 if (typeof options === 'number') {
4060 var _arguments$;
4061 /* eslint-disable prefer-rest-params */
4062 options = {
4063 row: arguments[0],
4064 col: arguments[1],
4065 verticalSnap: arguments[2] ? 'bottom' : 'top',
4066 horizontalSnap: arguments[3] ? 'end' : 'start',
4067 considerHiddenIndexes: (_arguments$ = arguments[4]) !== null && _arguments$ !== void 0 ? _arguments$ : true
4068 };
4069 /* eslint-enable prefer-rest-params */
4070 }
4071 const {
4072 row,
4073 col,
4074 verticalSnap,
4075 horizontalSnap,
4076 considerHiddenIndexes
4077 } = (_options = options) !== null && _options !== void 0 ? _options : {};
4078 let snapToTop;
4079 let snapToBottom;
4080 let snapToInlineStart;
4081 let snapToInlineEnd;
4082 if (verticalSnap !== undefined) {
4083 snapToTop = verticalSnap === 'top';
4084 snapToBottom = !snapToTop;
4085 }
4086 if (horizontalSnap !== undefined) {
4087 snapToInlineStart = horizontalSnap === 'start';
4088 snapToInlineEnd = !snapToInlineStart;
4089 }
4090 let renderableRow = row;
4091 let renderableColumn = col;
4092 if (considerHiddenIndexes === undefined || considerHiddenIndexes) {
4093 const isValidRowGrid = Number.isInteger(row) && row >= 0;
4094 const isValidColumnGrid = Number.isInteger(col) && col >= 0;
4095 const visualRowToScroll = isValidRowGrid ? getIndexToScroll(this.rowIndexMapper, row) : undefined;
4096 const visualColumnToScroll = isValidColumnGrid ? getIndexToScroll(this.columnIndexMapper, col) : undefined;
4097 if (visualRowToScroll === null || visualColumnToScroll === null) {
4098 return false;
4099 }
4100 renderableRow = isValidRowGrid ? instance.rowIndexMapper.getRenderableFromVisualIndex(visualRowToScroll) : row;
4101 renderableColumn = isValidColumnGrid ? instance.columnIndexMapper.getRenderableFromVisualIndex(visualColumnToScroll) : col;
4102 }
4103 const isRowInteger = Number.isInteger(renderableRow);
4104 const isColumnInteger = Number.isInteger(renderableColumn);
4105 if (isRowInteger && renderableRow >= 0 && isColumnInteger && renderableColumn >= 0) {
4106 return instance.view.scrollViewport(instance._createCellCoords(renderableRow, renderableColumn), snapToTop, snapToInlineEnd, snapToBottom, snapToInlineStart);
4107 }
4108 if (isRowInteger && renderableRow >= 0 && (isColumnInteger && renderableColumn < 0 || !isColumnInteger)) {
4109 return instance.view.scrollViewportVertically(renderableRow, snapToTop, snapToBottom);
4110 }
4111 if (isColumnInteger && renderableColumn >= 0 && (isRowInteger && renderableRow < 0 || !isRowInteger)) {
4112 return instance.view.scrollViewportHorizontally(renderableColumn, snapToInlineEnd, snapToInlineStart);
4113 }
4114 return false;
4115 };
4116
4117 /**
4118 * Scrolls the viewport to coordinates specified by the currently focused cell.
4119 *
4120 * @since 14.0.0
4121 * @memberof Core#
4122 * @fires Hooks#afterScroll
4123 * @function scrollToFocusedCell
4124 * @param {Function} callback The callback function to call after the viewport is scrolled.
4125 */
4126 this.scrollToFocusedCell = function () {
4127 let callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : () => {};
4128 if (!this.selection.isSelected()) {
4129 return;
4130 }
4131 this.addHookOnce('afterScroll', callback);
4132 const {
4133 highlight
4134 } = this.getSelectedRangeLast();
4135 const isScrolled = this.scrollViewportTo(highlight.toObject());
4136 if (isScrolled) {
4137 this.view.render();
4138 } else {
4139 this.removeHook('afterScroll', callback);
4140 this._registerImmediate(() => callback());
4141 }
4142 };
4143
4144 /**
4145 * Removes the table from the DOM and destroys the instance of the Handsontable.
4146 *
4147 * @memberof Core#
4148 * @function destroy
4149 * @fires Hooks#afterDestroy
4150 */
4151 this.destroy = function () {
4152 instance._clearTimeouts();
4153 instance._clearImmediates();
4154 if (instance.view) {
4155 // in case HT is destroyed before initialization has finished
4156 instance.view.destroy();
4157 }
4158 if (dataSource) {
4159 dataSource.destroy();
4160 }
4161 dataSource = null;
4162 this.getShortcutManager().destroy();
4163 metaManager.clearCache();
4164 foreignHotInstances.delete(this.guid);
4165 if (isRootInstance(instance)) {
4166 const licenseInfo = this.rootDocument.querySelector('.hot-display-license-info');
4167 if (licenseInfo) {
4168 licenseInfo.parentNode.removeChild(licenseInfo);
4169 }
4170 }
4171 empty(instance.rootElement);
4172 eventManager.destroy();
4173 if (editorManager) {
4174 editorManager.destroy();
4175 }
4176
4177 // The plugin's `destroy` method is called as a consequence and it should handle
4178 // unregistration of plugin's maps. Some unregistered maps reset the cache.
4179 instance.batchExecution(() => {
4180 instance.rowIndexMapper.unregisterAll();
4181 instance.columnIndexMapper.unregisterAll();
4182 pluginsRegistry.getItems().forEach(_ref10 => {
4183 let [, plugin] = _ref10;
4184 plugin.destroy();
4185 });
4186 pluginsRegistry.clear();
4187 instance.runHooks('afterDestroy');
4188 }, true);
4189 Hooks.getSingleton().destroy(instance);
4190 objectEach(instance, (property, key, obj) => {
4191 // replace instance methods with post mortem
4192 if (isFunction(property)) {
4193 obj[key] = postMortem(key);
4194 } else if (key !== 'guid') {
4195 // replace instance properties with null (restores memory)
4196 // it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
4197 obj[key] = null;
4198 }
4199 });
4200 instance.isDestroyed = true;
4201
4202 // replace private properties with null (restores memory)
4203 // it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
4204 if (datamap) {
4205 datamap.destroy();
4206 }
4207 datamap = null;
4208 grid = null;
4209 selection = null;
4210 editorManager = null;
4211 instance = null;
4212 };
4213
4214 /**
4215 * Replacement for all methods after the Handsontable was destroyed.
4216 *
4217 * @private
4218 * @param {string} method The method name.
4219 * @returns {Function}
4220 */
4221 function postMortem(method) {
4222 return () => {
4223 throw new Error(`The "${method}" method cannot be called because this Handsontable instance has been destroyed`);
4224 };
4225 }
4226
4227 /**
4228 * Returns the active editor class instance.
4229 *
4230 * @memberof Core#
4231 * @function getActiveEditor
4232 * @returns {BaseEditor} The active editor instance.
4233 */
4234 this.getActiveEditor = function () {
4235 return editorManager.getActiveEditor();
4236 };
4237
4238 /**
4239 * Returns the first rendered row in the DOM (usually, it is not visible in the table's viewport).
4240 *
4241 * @since 14.6.0
4242 * @memberof Core#
4243 * @function getFirstRenderedVisibleRow
4244 * @returns {number | null}
4245 */
4246 this.getFirstRenderedVisibleRow = function () {
4247 return instance.view.getFirstRenderedVisibleRow();
4248 };
4249
4250 /**
4251 * Returns the last rendered row in the DOM (usually, it is not visible in the table's viewport).
4252 *
4253 * @since 14.6.0
4254 * @memberof Core#
4255 * @function getLastRenderedVisibleRow
4256 * @returns {number | null}
4257 */
4258 this.getLastRenderedVisibleRow = function () {
4259 return instance.view.getLastRenderedVisibleRow();
4260 };
4261
4262 /**
4263 * Returns the first rendered column in the DOM (usually, it is not visible in the table's viewport).
4264 *
4265 * @since 14.6.0
4266 * @memberof Core#
4267 * @function getFirstRenderedVisibleColumn
4268 * @returns {number | null}
4269 */
4270 this.getFirstRenderedVisibleColumn = function () {
4271 return instance.view.getFirstRenderedVisibleColumn();
4272 };
4273
4274 /**
4275 * Returns the last rendered column in the DOM (usually, it is not visible in the table's viewport).
4276 *
4277 * @since 14.6.0
4278 * @memberof Core#
4279 * @function getLastRenderedVisibleColumn
4280 * @returns {number | null}
4281 */
4282 this.getLastRenderedVisibleColumn = function () {
4283 return instance.view.getLastRenderedVisibleColumn();
4284 };
4285
4286 /**
4287 * Returns the first fully visible row in the table viewport. When the table has overlays the method returns
4288 * the first row of the main table that is not overlapped by overlay.
4289 *
4290 * @since 14.6.0
4291 * @memberof Core#
4292 * @function getFirstFullyVisibleRow
4293 * @returns {number | null}
4294 */
4295 this.getFirstFullyVisibleRow = function () {
4296 return instance.view.getFirstFullyVisibleRow();
4297 };
4298
4299 /**
4300 * Returns the last fully visible row in the table viewport. When the table has overlays the method returns
4301 * the first row of the main table that is not overlapped by overlay.
4302 *
4303 * @since 14.6.0
4304 * @memberof Core#
4305 * @function getLastFullyVisibleRow
4306 * @returns {number | null}
4307 */
4308 this.getLastFullyVisibleRow = function () {
4309 return instance.view.getLastFullyVisibleRow();
4310 };
4311
4312 /**
4313 * Returns the first fully visible column in the table viewport. When the table has overlays the method returns
4314 * the first row of the main table that is not overlapped by overlay.
4315 *
4316 * @since 14.6.0
4317 * @memberof Core#
4318 * @function getFirstFullyVisibleColumn
4319 * @returns {number | null}
4320 */
4321 this.getFirstFullyVisibleColumn = function () {
4322 return instance.view.getFirstFullyVisibleColumn();
4323 };
4324
4325 /**
4326 * Returns the last fully visible column in the table viewport. When the table has overlays the method returns
4327 * the first row of the main table that is not overlapped by overlay.
4328 *
4329 * @since 14.6.0
4330 * @memberof Core#
4331 * @function getLastFullyVisibleColumn
4332 * @returns {number | null}
4333 */
4334 this.getLastFullyVisibleColumn = function () {
4335 return instance.view.getLastFullyVisibleColumn();
4336 };
4337
4338 /**
4339 * Returns the first partially visible row in the table viewport. When the table has overlays the method returns
4340 * the first row of the main table that is not overlapped by overlay.
4341 *
4342 * @since 14.6.0
4343 * @memberof Core#
4344 * @function getFirstPartiallyVisibleRow
4345 * @returns {number | null}
4346 */
4347 this.getFirstPartiallyVisibleRow = function () {
4348 return instance.view.getFirstPartiallyVisibleRow();
4349 };
4350
4351 /**
4352 * Returns the last partially visible row in the table viewport. When the table has overlays the method returns
4353 * the first row of the main table that is not overlapped by overlay.
4354 *
4355 * @since 14.6.0
4356 * @memberof Core#
4357 * @function getLastPartiallyVisibleRow
4358 * @returns {number | null}
4359 */
4360 this.getLastPartiallyVisibleRow = function () {
4361 return instance.view.getLastPartiallyVisibleRow();
4362 };
4363
4364 /**
4365 * Returns the first partially visible column in the table viewport. When the table has overlays the method returns
4366 * the first row of the main table that is not overlapped by overlay.
4367 *
4368 * @since 14.6.0
4369 * @memberof Core#
4370 * @function getFirstPartiallyVisibleColumn
4371 * @returns {number | null}
4372 */
4373 this.getFirstPartiallyVisibleColumn = function () {
4374 return instance.view.getFirstPartiallyVisibleColumn();
4375 };
4376
4377 /**
4378 * Returns the last partially visible column in the table viewport. When the table has overlays the method returns
4379 * the first row of the main table that is not overlapped by overlay.
4380 *
4381 * @since 14.6.0
4382 * @memberof Core#
4383 * @function getLastPartiallyVisibleColumn
4384 * @returns {number | null}
4385 */
4386 this.getLastPartiallyVisibleColumn = function () {
4387 return instance.view.getLastPartiallyVisibleColumn();
4388 };
4389
4390 /**
4391 * Returns plugin instance by provided its name.
4392 *
4393 * @memberof Core#
4394 * @function getPlugin
4395 * @param {string} pluginName The plugin name.
4396 * @returns {BasePlugin|undefined} The plugin instance or undefined if there is no plugin.
4397 */
4398 this.getPlugin = function (pluginName) {
4399 const unifiedPluginName = toUpperCaseFirst(pluginName);
4400
4401 // Workaround for the UndoRedo plugin which, currently doesn't follow the plugin architecture.
4402 if (unifiedPluginName === 'UndoRedo') {
4403 return this.undoRedo;
4404 }
4405 return pluginsRegistry.getItem(unifiedPluginName);
4406 };
4407
4408 /**
4409 * Returns name of the passed plugin.
4410 *
4411 * @private
4412 * @memberof Core#
4413 * @param {BasePlugin} plugin The plugin instance.
4414 * @returns {string}
4415 */
4416 this.getPluginName = function (plugin) {
4417 // Workaround for the UndoRedo plugin which, currently doesn't follow the plugin architecture.
4418 if (plugin === this.undoRedo) {
4419 return this.undoRedo.constructor.PLUGIN_KEY;
4420 }
4421 return pluginsRegistry.getId(plugin);
4422 };
4423
4424 /**
4425 * Returns the Handsontable instance.
4426 *
4427 * @memberof Core#
4428 * @function getInstance
4429 * @returns {Handsontable} The Handsontable instance.
4430 */
4431 this.getInstance = function () {
4432 return instance;
4433 };
4434
4435 /**
4436 * Adds listener to the specified hook name (only for this Handsontable instance).
4437 *
4438 * @memberof Core#
4439 * @function addHook
4440 * @see Hooks#add
4441 * @param {string} key Hook name (see {@link Hooks}).
4442 * @param {Function|Array} callback Function or array of functions.
4443 * @param {number} [orderIndex] Order index of the callback.
4444 * If > 0, the callback will be added after the others, for example, with an index of 1, the callback will be added before the ones with an index of 2, 3, etc., but after the ones with an index of 0 and lower.
4445 * If < 0, the callback will be added before the others, for example, with an index of -1, the callback will be added after the ones with an index of -2, -3, etc., but before the ones with an index of 0 and higher.
4446 * If 0 or no order index is provided, the callback will be added between the "negative" and "positive" indexes.
4447 * @example
4448 * ```js
4449 * hot.addHook('beforeInit', myCallback);
4450 * ```
4451 */
4452 this.addHook = function (key, callback, orderIndex) {
4453 Hooks.getSingleton().add(key, callback, instance, orderIndex);
4454 };
4455
4456 /**
4457 * Check if for a specified hook name there are added listeners (only for this Handsontable instance). All available
4458 * hooks you will find {@link Hooks}.
4459 *
4460 * @memberof Core#
4461 * @function hasHook
4462 * @see Hooks#has
4463 * @param {string} key Hook name.
4464 * @returns {boolean}
4465 *
4466 * @example
4467 * ```js
4468 * const hasBeforeInitListeners = hot.hasHook('beforeInit');
4469 * ```
4470 */
4471 this.hasHook = function (key) {
4472 return Hooks.getSingleton().has(key, instance) || Hooks.getSingleton().has(key);
4473 };
4474
4475 /**
4476 * Adds listener to specified hook name (only for this Handsontable instance). After the listener is triggered,
4477 * it will be automatically removed.
4478 *
4479 * @memberof Core#
4480 * @function addHookOnce
4481 * @see Hooks#once
4482 * @param {string} key Hook name (see {@link Hooks}).
4483 * @param {Function|Array} callback Function or array of functions.
4484 * @param {number} [orderIndex] Order index of the callback.
4485 * If > 0, the callback will be added after the others, for example, with an index of 1, the callback will be added before the ones with an index of 2, 3, etc., but after the ones with an index of 0 and lower.
4486 * If < 0, the callback will be added before the others, for example, with an index of -1, the callback will be added after the ones with an index of -2, -3, etc., but before the ones with an index of 0 and higher.
4487 * If 0 or no order index is provided, the callback will be added between the "negative" and "positive" indexes.
4488 * @example
4489 * ```js
4490 * hot.addHookOnce('beforeInit', myCallback);
4491 * ```
4492 */
4493 this.addHookOnce = function (key, callback, orderIndex) {
4494 Hooks.getSingleton().once(key, callback, instance, orderIndex);
4495 };
4496
4497 /**
4498 * Removes the hook listener previously registered with {@link Core#addHook}.
4499 *
4500 * @memberof Core#
4501 * @function removeHook
4502 * @see Hooks#remove
4503 * @param {string} key Hook name.
4504 * @param {Function} callback Reference to the function which has been registered using {@link Core#addHook}.
4505 *
4506 * @example
4507 * ```js
4508 * hot.removeHook('beforeInit', myCallback);
4509 * ```
4510 */
4511 this.removeHook = function (key, callback) {
4512 Hooks.getSingleton().remove(key, callback, instance);
4513 };
4514
4515 /**
4516 * Run the callbacks for the hook provided in the `key` argument using the parameters given in the other arguments.
4517 *
4518 * @memberof Core#
4519 * @function runHooks
4520 * @see Hooks#run
4521 * @param {string} key Hook name.
4522 * @param {*} [p1] Argument passed to the callback.
4523 * @param {*} [p2] Argument passed to the callback.
4524 * @param {*} [p3] Argument passed to the callback.
4525 * @param {*} [p4] Argument passed to the callback.
4526 * @param {*} [p5] Argument passed to the callback.
4527 * @param {*} [p6] Argument passed to the callback.
4528 * @returns {*}
4529 *
4530 * @example
4531 * ```js
4532 * // Run built-in hook
4533 * hot.runHooks('beforeInit');
4534 * // Run custom hook
4535 * hot.runHooks('customAction', 10, 'foo');
4536 * ```
4537 */
4538 this.runHooks = function (key, p1, p2, p3, p4, p5, p6) {
4539 return Hooks.getSingleton().run(instance, key, p1, p2, p3, p4, p5, p6);
4540 };
4541
4542 /**
4543 * Get language phrase for specified dictionary key.
4544 *
4545 * @memberof Core#
4546 * @function getTranslatedPhrase
4547 * @since 0.35.0
4548 * @param {string} dictionaryKey Constant which is dictionary key.
4549 * @param {*} extraArguments Arguments which will be handled by formatters.
4550 * @returns {string}
4551 */
4552 this.getTranslatedPhrase = function (dictionaryKey, extraArguments) {
4553 return getTranslatedPhrase(tableMeta.language, dictionaryKey, extraArguments);
4554 };
4555
4556 /**
4557 * Converts instance into outerHTML of HTMLTableElement.
4558 *
4559 * @memberof Core#
4560 * @function toHTML
4561 * @since 7.1.0
4562 * @returns {string}
4563 */
4564 this.toHTML = () => instanceToHTML(this);
4565
4566 /**
4567 * Converts instance into HTMLTableElement.
4568 *
4569 * @memberof Core#
4570 * @function toTableElement
4571 * @since 7.1.0
4572 * @returns {HTMLTableElement}
4573 */
4574 this.toTableElement = () => {
4575 const tempElement = this.rootDocument.createElement('div');
4576 tempElement.insertAdjacentHTML('afterbegin', instanceToHTML(this));
4577 return tempElement.firstElementChild;
4578 };
4579 this.timeouts = [];
4580
4581 /**
4582 * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called.
4583 *
4584 * @param {number|Function} handle Handler returned from setTimeout or function to execute (it will be automatically wraped
4585 * by setTimeout function).
4586 * @param {number} [delay=0] If first argument is passed as a function this argument set delay of the execution of that function.
4587 * @private
4588 */
4589 this._registerTimeout = function (handle) {
4590 let delay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
4591 let handleFunc = handle;
4592 if (typeof handleFunc === 'function') {
4593 handleFunc = setTimeout(handleFunc, delay);
4594 }
4595 this.timeouts.push(handleFunc);
4596 };
4597
4598 /**
4599 * Clears all known timeouts.
4600 *
4601 * @private
4602 */
4603 this._clearTimeouts = function () {
4604 arrayEach(this.timeouts, handler => {
4605 clearTimeout(handler);
4606 });
4607 };
4608 this.immediates = [];
4609
4610 /**
4611 * Execute function execution to the next event loop cycle. Purpose of this method is to clear all known timeouts when `destroy` method is called.
4612 *
4613 * @param {Function} callback Function to be delayed in execution.
4614 * @private
4615 */
4616 this._registerImmediate = function (callback) {
4617 this.immediates.push(setImmediate(callback));
4618 };
4619
4620 /**
4621 * Clears all known timeouts.
4622 *
4623 * @private
4624 */
4625 this._clearImmediates = function () {
4626 arrayEach(this.immediates, handler => {
4627 clearImmediate(handler);
4628 });
4629 };
4630
4631 /**
4632 * Gets the instance of the EditorManager.
4633 *
4634 * @private
4635 * @returns {EditorManager}
4636 */
4637 this._getEditorManager = function () {
4638 return editorManager;
4639 };
4640
4641 /**
4642 * Check if currently it is RTL direction.
4643 *
4644 * @private
4645 * @memberof Core#
4646 * @function isRtl
4647 * @returns {boolean} True if RTL.
4648 */
4649 this.isRtl = function () {
4650 return instance.rootWindow.getComputedStyle(instance.rootElement).direction === 'rtl';
4651 };
4652
4653 /**
4654 * Check if currently it is LTR direction.
4655 *
4656 * @private
4657 * @memberof Core#
4658 * @function isLtr
4659 * @returns {boolean} True if LTR.
4660 */
4661 this.isLtr = function () {
4662 return !instance.isRtl();
4663 };
4664
4665 /**
4666 * Returns 1 for LTR; -1 for RTL. Useful for calculations.
4667 *
4668 * @private
4669 * @memberof Core#
4670 * @function getDirectionFactor
4671 * @returns {number} Returns 1 for LTR; -1 for RTL.
4672 */
4673 this.getDirectionFactor = function () {
4674 return instance.isLtr() ? 1 : -1;
4675 };
4676 const shortcutManager = createShortcutManager({
4677 handleEvent() {
4678 return instance.isListening();
4679 },
4680 beforeKeyDown: event => {
4681 return this.runHooks('beforeKeyDown', event);
4682 },
4683 afterKeyDown: event => {
4684 if (this.isDestroyed) {
4685 // Handsontable could be destroyed after performing action (executing a callback).
4686 return;
4687 }
4688 instance.runHooks('afterDocumentKeyDown', event);
4689 },
4690 ownerWindow: this.rootWindow
4691 });
4692 this.addHook('beforeOnCellMouseDown', event => {
4693 // Releasing keys as some browser/system shortcuts break events sequence (thus the `keyup` event isn't triggered).
4694 if (event.ctrlKey === false && event.metaKey === false) {
4695 shortcutManager.releasePressedKeys();
4696 }
4697 });
4698
4699 /**
4700 * Returns instance of a manager responsible for handling shortcuts stored in some contexts. It run actions after
4701 * pressing key combination in active Handsontable instance.
4702 *
4703 * @memberof Core#
4704 * @since 12.0.0
4705 * @function getShortcutManager
4706 * @returns {ShortcutManager} Instance of {@link ShortcutManager}
4707 */
4708 this.getShortcutManager = function () {
4709 return shortcutManager;
4710 };
4711
4712 /**
4713 * Return the Focus Manager responsible for managing the browser's focus in the table.
4714 *
4715 * @memberof Core#
4716 * @since 14.0.0
4717 * @function getFocusManager
4718 * @returns {FocusManager}
4719 */
4720 this.getFocusManager = function () {
4721 return focusManager;
4722 };
4723 getPluginsNames().forEach(pluginName => {
4724 const PluginClass = getPlugin(pluginName);
4725 pluginsRegistry.addItem(pluginName, new PluginClass(this));
4726 });
4727 registerAllShortcutContexts(instance);
4728 shortcutManager.setActiveContextName('grid');
4729 Hooks.getSingleton().run(instance, 'construct');
4730}
\No newline at end of file