1 | import "core-js/modules/es.error.cause.js";
|
2 | import "core-js/modules/es.array.push.js";
|
3 | import "core-js/modules/es.set.difference.v2.js";
|
4 | import "core-js/modules/es.set.intersection.v2.js";
|
5 | import "core-js/modules/es.set.is-disjoint-from.v2.js";
|
6 | import "core-js/modules/es.set.is-subset-of.v2.js";
|
7 | import "core-js/modules/es.set.is-superset-of.v2.js";
|
8 | import "core-js/modules/es.set.symmetric-difference.v2.js";
|
9 | import "core-js/modules/es.set.union.v2.js";
|
10 | import "core-js/modules/web.immediate.js";
|
11 | import { addClass, empty, observeVisibilityChangeOnce, removeClass } from "./helpers/dom/element.mjs";
|
12 | import { isFunction } from "./helpers/function.mjs";
|
13 | import { isDefined, isUndefined, isRegExp, _injectProductInfo, isEmpty } from "./helpers/mixed.mjs";
|
14 | import { isMobileBrowser, isIpadOS } from "./helpers/browser.mjs";
|
15 | import EditorManager from "./editorManager.mjs";
|
16 | import EventManager from "./eventManager.mjs";
|
17 | import { deepClone, duckSchema, isObjectEqual, isObject, deepObjectSize, hasOwnProperty, createObjectPropListener, objectEach } from "./helpers/object.mjs";
|
18 | import { FocusManager } from "./focusManager.mjs";
|
19 | import { arrayMap, arrayEach, arrayReduce, getDifferenceOfArrays, stringToArray, pivot } from "./helpers/array.mjs";
|
20 | import { instanceToHTML } from "./utils/parseTable.mjs";
|
21 | import { getPlugin, getPluginsNames } from "./plugins/registry.mjs";
|
22 | import { getRenderer } from "./renderers/registry.mjs";
|
23 | import { getEditor } from "./editors/registry.mjs";
|
24 | import { getValidator } from "./validators/registry.mjs";
|
25 | import { randomString, toUpperCaseFirst } from "./helpers/string.mjs";
|
26 | import { rangeEach, rangeEachReverse, isNumericLike } from "./helpers/number.mjs";
|
27 | import TableView from "./tableView.mjs";
|
28 | import DataSource from "./dataMap/dataSource.mjs";
|
29 | import { spreadsheetColumnLabel } from "./helpers/data.mjs";
|
30 | import { IndexMapper } from "./translations/index.mjs";
|
31 | import { registerAsRootInstance, hasValidParameter, isRootInstance } from "./utils/rootInstance.mjs";
|
32 | import { DEFAULT_COLUMN_WIDTH } from "./3rdparty/walkontable/src/index.mjs";
|
33 | import Hooks from "./pluginHooks.mjs";
|
34 | import { hasLanguageDictionary, getValidLanguageCode, getTranslatedPhrase } from "./i18n/registry.mjs";
|
35 | import { warnUserAboutLanguageRegistration, normalizeLanguageCode } from "./i18n/utils.mjs";
|
36 | import { Selection } from "./selection/index.mjs";
|
37 | import { MetaManager, DynamicCellMetaMod, ExtendMetaPropertiesMod, replaceData } from "./dataMap/index.mjs";
|
38 | import { installFocusCatcher, createViewportScroller } from "./core/index.mjs";
|
39 | import { createUniqueMap } from "./utils/dataStructures/uniqueMap.mjs";
|
40 | import { createShortcutManager } from "./shortcuts/index.mjs";
|
41 | import { registerAllShortcutContexts } from "./shortcutContexts/index.mjs";
|
42 | let 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 | */
|
51 | const 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
|
59 | const 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 | */
|
111 | export 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 |