4636 lines
177 KiB
JavaScript
4636 lines
177 KiB
JavaScript
import "core-js/modules/es.array.push.js";
|
||
import "core-js/modules/es.error.cause.js";
|
||
import "core-js/modules/web.immediate.js";
|
||
import { addClass, empty, observeVisibilityChangeOnce, removeClass } from "./helpers/dom/element.mjs";
|
||
import { isFunction } from "./helpers/function.mjs";
|
||
import { isDefined, isUndefined, isRegExp, _injectProductInfo, isEmpty } from "./helpers/mixed.mjs";
|
||
import { isMobileBrowser, isIpadOS } from "./helpers/browser.mjs";
|
||
import EditorManager from "./editorManager.mjs";
|
||
import EventManager from "./eventManager.mjs";
|
||
import { deepClone, duckSchema, isObjectEqual, isObject, deepObjectSize, hasOwnProperty, createObjectPropListener, objectEach } from "./helpers/object.mjs";
|
||
import { arrayMap, arrayEach, arrayReduce, getDifferenceOfArrays, stringToArray, pivot } from "./helpers/array.mjs";
|
||
import { instanceToHTML } from "./utils/parseTable.mjs";
|
||
import { getPlugin, getPluginsNames } from "./plugins/registry.mjs";
|
||
import { getRenderer } from "./renderers/registry.mjs";
|
||
import { getEditor } from "./editors/registry.mjs";
|
||
import { getValidator } from "./validators/registry.mjs";
|
||
import { randomString, toUpperCaseFirst } from "./helpers/string.mjs";
|
||
import { rangeEach, rangeEachReverse, isNumericLike } from "./helpers/number.mjs";
|
||
import TableView from "./tableView.mjs";
|
||
import DataSource from "./dataMap/dataSource.mjs";
|
||
import { spreadsheetColumnLabel } from "./helpers/data.mjs";
|
||
import { IndexMapper } from "./translations/index.mjs";
|
||
import { registerAsRootInstance, hasValidParameter, isRootInstance } from "./utils/rootInstance.mjs";
|
||
import { ViewportColumnsCalculator } from "./3rdparty/walkontable/src/index.mjs";
|
||
import Hooks from "./pluginHooks.mjs";
|
||
import { hasLanguageDictionary, getValidLanguageCode, getTranslatedPhrase } from "./i18n/registry.mjs";
|
||
import { warnUserAboutLanguageRegistration, normalizeLanguageCode } from "./i18n/utils.mjs";
|
||
import { Selection } from "./selection/index.mjs";
|
||
import { MetaManager, DynamicCellMetaMod, ExtendMetaPropertiesMod, replaceData } from "./dataMap/index.mjs";
|
||
import { createUniqueMap } from "./utils/dataStructures/uniqueMap.mjs";
|
||
import { createShortcutManager } from "./shortcuts/index.mjs";
|
||
const SHORTCUTS_GROUP = 'gridDefault';
|
||
let activeGuid = null;
|
||
|
||
/**
|
||
* A set of deprecated feature names.
|
||
*
|
||
* @type {Set<string>}
|
||
*/
|
||
// eslint-disable-next-line no-unused-vars
|
||
const deprecationWarns = new Set();
|
||
|
||
/* eslint-disable jsdoc/require-description-complete-sentence */
|
||
/**
|
||
* Handsontable constructor.
|
||
*
|
||
* @core
|
||
* @class Core
|
||
* @description
|
||
*
|
||
* The `Handsontable` class (known as the `Core`) lets you modify the grid's behavior by using Handsontable's public API methods.
|
||
*
|
||
* ::: only-for react
|
||
* To use these methods, associate a Handsontable instance with your instance
|
||
* of the [`HotTable` component](@/guides/getting-started/installation.md#_4-use-the-hottable-component),
|
||
* by using React's `ref` feature (read more on the [Instance methods](@/guides/getting-started/react-methods.md) page).
|
||
* :::
|
||
*
|
||
* ## How to call a method
|
||
*
|
||
* ::: only-for javascript
|
||
* ```js
|
||
* // create a Handsontable instance
|
||
* const hot = new Handsontable(document.getElementById('example'), options);
|
||
*
|
||
* // call a method
|
||
* hot.setDataAtCell(0, 0, 'new value');
|
||
* ```
|
||
* :::
|
||
*
|
||
* ::: only-for react
|
||
* ```jsx
|
||
* import { useRef } from 'react';
|
||
*
|
||
* const hotTableComponent = useRef(null);
|
||
*
|
||
* <HotTable
|
||
* // associate your `HotTable` component with a Handsontable instance
|
||
* ref={hotTableComponent}
|
||
* settings={options}
|
||
* />
|
||
*
|
||
* // access the Handsontable instance, under the `.current.hotInstance` property
|
||
* // call a method
|
||
* hotTableComponent.current.hotInstance.setDataAtCell(0, 0, 'new value');
|
||
* ```
|
||
* :::
|
||
*
|
||
* @param {HTMLElement} rootElement The element to which the Handsontable instance is injected.
|
||
* @param {object} userSettings The user defined options.
|
||
* @param {boolean} [rootInstanceSymbol=false] Indicates if the instance is root of all later instances created.
|
||
*/
|
||
export default function Core(rootElement, userSettings) {
|
||
var _userSettings$layoutD;
|
||
let rootInstanceSymbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
||
let preventScrollingToCell = false;
|
||
let instance = this;
|
||
const eventManager = new EventManager(instance);
|
||
let datamap;
|
||
let dataSource;
|
||
let grid;
|
||
let editorManager;
|
||
let firstRun = true;
|
||
if (hasValidParameter(rootInstanceSymbol)) {
|
||
registerAsRootInstance(this);
|
||
}
|
||
|
||
// TODO: check if references to DOM elements should be move to UI layer (Walkontable)
|
||
/**
|
||
* Reference to the container element.
|
||
*
|
||
* @private
|
||
* @type {HTMLElement}
|
||
*/
|
||
this.rootElement = rootElement;
|
||
/**
|
||
* The nearest document over container.
|
||
*
|
||
* @private
|
||
* @type {Document}
|
||
*/
|
||
this.rootDocument = rootElement.ownerDocument;
|
||
/**
|
||
* Window object over container's document.
|
||
*
|
||
* @private
|
||
* @type {Window}
|
||
*/
|
||
this.rootWindow = this.rootDocument.defaultView;
|
||
/**
|
||
* A boolean to tell if the Handsontable has been fully destroyed. This is set to `true`
|
||
* after `afterDestroy` hook is called.
|
||
*
|
||
* @memberof Core#
|
||
* @member isDestroyed
|
||
* @type {boolean}
|
||
*/
|
||
this.isDestroyed = false;
|
||
/**
|
||
* The counter determines how many times the render suspending was called. It allows
|
||
* tracking the nested suspending calls. For each render suspend resuming call the
|
||
* counter is decremented. The value equal to 0 means the render suspending feature
|
||
* is disabled.
|
||
*
|
||
* @private
|
||
* @type {number}
|
||
*/
|
||
this.renderSuspendedCounter = 0;
|
||
/**
|
||
* The counter determines how many times the execution suspending was called. It allows
|
||
* tracking the nested suspending calls. For each execution suspend resuming call the
|
||
* counter is decremented. The value equal to 0 means the execution suspending feature
|
||
* is disabled.
|
||
*
|
||
* @private
|
||
* @type {number}
|
||
*/
|
||
this.executionSuspendedCounter = 0;
|
||
const layoutDirection = (_userSettings$layoutD = userSettings === null || userSettings === void 0 ? void 0 : userSettings.layoutDirection) !== null && _userSettings$layoutD !== void 0 ? _userSettings$layoutD : 'inherit';
|
||
const rootElementDirection = ['rtl', 'ltr'].includes(layoutDirection) ? layoutDirection : this.rootWindow.getComputedStyle(this.rootElement).direction;
|
||
this.rootElement.setAttribute('dir', rootElementDirection);
|
||
|
||
/**
|
||
* Checks if the grid is rendered using the right-to-left layout direction.
|
||
*
|
||
* @since 12.0.0
|
||
* @memberof Core#
|
||
* @function isRtl
|
||
* @returns {boolean} True if RTL.
|
||
*/
|
||
this.isRtl = function () {
|
||
return rootElementDirection === 'rtl';
|
||
};
|
||
|
||
/**
|
||
* Checks if the grid is rendered using the left-to-right layout direction.
|
||
*
|
||
* @since 12.0.0
|
||
* @memberof Core#
|
||
* @function isLtr
|
||
* @returns {boolean} True if LTR.
|
||
*/
|
||
this.isLtr = function () {
|
||
return !instance.isRtl();
|
||
};
|
||
|
||
/**
|
||
* Returns 1 for LTR; -1 for RTL. Useful for calculations.
|
||
*
|
||
* @since 12.0.0
|
||
* @memberof Core#
|
||
* @function getDirectionFactor
|
||
* @returns {number} Returns 1 for LTR; -1 for RTL.
|
||
*/
|
||
this.getDirectionFactor = function () {
|
||
return instance.isLtr() ? 1 : -1;
|
||
};
|
||
userSettings.language = getValidLanguageCode(userSettings.language);
|
||
const metaManager = new MetaManager(instance, userSettings, [DynamicCellMetaMod, ExtendMetaPropertiesMod]);
|
||
const tableMeta = metaManager.getTableMeta();
|
||
const globalMeta = metaManager.getGlobalMeta();
|
||
const pluginsRegistry = createUniqueMap();
|
||
this.container = this.rootDocument.createElement('div');
|
||
this.renderCall = false;
|
||
rootElement.insertBefore(this.container, rootElement.firstChild);
|
||
if (isRootInstance(this)) {
|
||
_injectProductInfo(userSettings.licenseKey, rootElement);
|
||
}
|
||
this.guid = `ht_${randomString()}`; // this is the namespace for global events
|
||
|
||
/**
|
||
* Instance of index mapper which is responsible for managing the column indexes.
|
||
*
|
||
* @memberof Core#
|
||
* @member columnIndexMapper
|
||
* @type {IndexMapper}
|
||
*/
|
||
this.columnIndexMapper = new IndexMapper();
|
||
/**
|
||
* Instance of index mapper which is responsible for managing the row indexes.
|
||
*
|
||
* @memberof Core#
|
||
* @member rowIndexMapper
|
||
* @type {IndexMapper}
|
||
*/
|
||
this.rowIndexMapper = new IndexMapper();
|
||
this.columnIndexMapper.addLocalHook('indexesSequenceChange', source => {
|
||
instance.runHooks('afterColumnSequenceChange', source);
|
||
});
|
||
this.rowIndexMapper.addLocalHook('indexesSequenceChange', source => {
|
||
instance.runHooks('afterRowSequenceChange', source);
|
||
});
|
||
dataSource = new DataSource(instance);
|
||
if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === 'ht_') {
|
||
this.rootElement.id = this.guid; // if root element does not have an id, assign a random id
|
||
}
|
||
|
||
const visualToRenderableCoords = coords => {
|
||
const {
|
||
row: visualRow,
|
||
col: visualColumn
|
||
} = coords;
|
||
return instance._createCellCoords(
|
||
// We just store indexes for rows and columns without headers.
|
||
visualRow >= 0 ? instance.rowIndexMapper.getRenderableFromVisualIndex(visualRow) : visualRow, visualColumn >= 0 ? instance.columnIndexMapper.getRenderableFromVisualIndex(visualColumn) : visualColumn);
|
||
};
|
||
const renderableToVisualCoords = coords => {
|
||
const {
|
||
row: renderableRow,
|
||
col: renderableColumn
|
||
} = coords;
|
||
return instance._createCellCoords(
|
||
// We just store indexes for rows and columns without headers.
|
||
renderableRow >= 0 ? instance.rowIndexMapper.getVisualFromRenderableIndex(renderableRow) : renderableRow, renderableColumn >= 0 ? instance.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn) : renderableColumn // eslint-disable-line max-len
|
||
);
|
||
};
|
||
|
||
let selection = new Selection(tableMeta, {
|
||
rowIndexMapper: () => instance.rowIndexMapper,
|
||
columnIndexMapper: () => instance.columnIndexMapper,
|
||
countCols: () => instance.countCols(),
|
||
countRows: () => instance.countRows(),
|
||
propToCol: prop => datamap.propToCol(prop),
|
||
isEditorOpened: () => instance.getActiveEditor() ? instance.getActiveEditor().isOpened() : false,
|
||
countColsTranslated: () => this.view.countRenderableColumns(),
|
||
countRowsTranslated: () => this.view.countRenderableRows(),
|
||
getShortcutManager: () => instance.getShortcutManager(),
|
||
createCellCoords: (row, column) => instance._createCellCoords(row, column),
|
||
createCellRange: (highlight, from, to) => instance._createCellRange(highlight, from, to),
|
||
visualToRenderableCoords,
|
||
renderableToVisualCoords,
|
||
isDisabledCellSelection: (visualRow, visualColumn) => instance.getCellMeta(visualRow, visualColumn).disableVisualSelection
|
||
});
|
||
this.selection = selection;
|
||
const onIndexMapperCacheUpdate = _ref => {
|
||
let {
|
||
hiddenIndexesChanged
|
||
} = _ref;
|
||
if (hiddenIndexesChanged) {
|
||
this.selection.refresh();
|
||
}
|
||
};
|
||
this.columnIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
|
||
this.rowIndexMapper.addLocalHook('cacheUpdated', onIndexMapperCacheUpdate);
|
||
this.selection.addLocalHook('beforeSetRangeStart', cellCoords => {
|
||
this.runHooks('beforeSetRangeStart', cellCoords);
|
||
});
|
||
this.selection.addLocalHook('beforeSetRangeStartOnly', cellCoords => {
|
||
this.runHooks('beforeSetRangeStartOnly', cellCoords);
|
||
});
|
||
this.selection.addLocalHook('beforeSetRangeEnd', cellCoords => {
|
||
this.runHooks('beforeSetRangeEnd', cellCoords);
|
||
if (cellCoords.row < 0) {
|
||
cellCoords.row = this.view._wt.wtTable.getFirstVisibleRow();
|
||
}
|
||
if (cellCoords.col < 0) {
|
||
cellCoords.col = this.view._wt.wtTable.getFirstVisibleColumn();
|
||
}
|
||
});
|
||
this.selection.addLocalHook('afterSetRangeEnd', cellCoords => {
|
||
const preventScrolling = createObjectPropListener(false);
|
||
const selectionRange = this.selection.getSelectedRange();
|
||
const {
|
||
from,
|
||
to
|
||
} = selectionRange.current();
|
||
const selectionLayerLevel = selectionRange.size() - 1;
|
||
this.runHooks('afterSelection', from.row, from.col, to.row, to.col, preventScrolling, selectionLayerLevel);
|
||
this.runHooks('afterSelectionByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), preventScrolling, selectionLayerLevel); // eslint-disable-line max-len
|
||
|
||
const isSelectedByAnyHeader = this.selection.isSelectedByAnyHeader();
|
||
const currentSelectedRange = this.selection.selectedRange.current();
|
||
let scrollToCell = true;
|
||
if (preventScrollingToCell) {
|
||
scrollToCell = false;
|
||
}
|
||
if (preventScrolling.isTouched()) {
|
||
scrollToCell = !preventScrolling.value;
|
||
}
|
||
const isSelectedByRowHeader = this.selection.isSelectedByRowHeader();
|
||
const isSelectedByColumnHeader = this.selection.isSelectedByColumnHeader();
|
||
if (scrollToCell !== false) {
|
||
if (!isSelectedByAnyHeader) {
|
||
if (currentSelectedRange && !this.selection.isMultiple()) {
|
||
this.view.scrollViewport(visualToRenderableCoords(currentSelectedRange.from));
|
||
} else {
|
||
this.view.scrollViewport(visualToRenderableCoords(cellCoords));
|
||
}
|
||
} else if (isSelectedByRowHeader) {
|
||
this.view.scrollViewportVertically(instance.rowIndexMapper.getRenderableFromVisualIndex(cellCoords.row));
|
||
} else if (isSelectedByColumnHeader) {
|
||
this.view.scrollViewportHorizontally(instance.columnIndexMapper.getRenderableFromVisualIndex(cellCoords.col));
|
||
}
|
||
}
|
||
|
||
// @TODO: These CSS classes are no longer needed anymore. They are used only as a indicator of the selected
|
||
// rows/columns in the MergedCells plugin (via border.js#L520 in the walkontable module). After fixing
|
||
// the Border class this should be removed.
|
||
if (isSelectedByRowHeader && isSelectedByColumnHeader) {
|
||
addClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
|
||
} else if (isSelectedByRowHeader) {
|
||
removeClass(this.rootElement, 'ht__selection--columns');
|
||
addClass(this.rootElement, 'ht__selection--rows');
|
||
} else if (isSelectedByColumnHeader) {
|
||
removeClass(this.rootElement, 'ht__selection--rows');
|
||
addClass(this.rootElement, 'ht__selection--columns');
|
||
} else {
|
||
removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
|
||
}
|
||
this._refreshBorders(null);
|
||
});
|
||
this.selection.addLocalHook('afterSelectionFinished', cellRanges => {
|
||
const selectionLayerLevel = cellRanges.length - 1;
|
||
const {
|
||
from,
|
||
to
|
||
} = cellRanges[selectionLayerLevel];
|
||
this.runHooks('afterSelectionEnd', from.row, from.col, to.row, to.col, selectionLayerLevel);
|
||
this.runHooks('afterSelectionEndByProp', from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), selectionLayerLevel);
|
||
});
|
||
this.selection.addLocalHook('afterIsMultipleSelection', isMultiple => {
|
||
const changedIsMultiple = this.runHooks('afterIsMultipleSelection', isMultiple.value);
|
||
if (isMultiple.value) {
|
||
isMultiple.value = changedIsMultiple;
|
||
}
|
||
});
|
||
this.selection.addLocalHook('beforeModifyTransformStart', cellCoordsDelta => {
|
||
this.runHooks('modifyTransformStart', cellCoordsDelta);
|
||
});
|
||
this.selection.addLocalHook('afterModifyTransformStart', (coords, rowTransformDir, colTransformDir) => {
|
||
this.runHooks('afterModifyTransformStart', coords, rowTransformDir, colTransformDir);
|
||
});
|
||
this.selection.addLocalHook('beforeModifyTransformEnd', cellCoordsDelta => {
|
||
this.runHooks('modifyTransformEnd', cellCoordsDelta);
|
||
});
|
||
this.selection.addLocalHook('afterModifyTransformEnd', (coords, rowTransformDir, colTransformDir) => {
|
||
this.runHooks('afterModifyTransformEnd', coords, rowTransformDir, colTransformDir);
|
||
});
|
||
this.selection.addLocalHook('afterDeselect', () => {
|
||
editorManager.destroyEditor();
|
||
this._refreshBorders();
|
||
removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
|
||
this.runHooks('afterDeselect');
|
||
});
|
||
this.selection.addLocalHook('insertRowRequire', totalRows => {
|
||
this.alter('insert_row_above', totalRows, 1, 'auto');
|
||
});
|
||
this.selection.addLocalHook('insertColRequire', totalCols => {
|
||
this.alter('insert_col_start', totalCols, 1, 'auto');
|
||
});
|
||
grid = {
|
||
/**
|
||
* Inserts or removes rows and columns.
|
||
*
|
||
* @private
|
||
* @param {string} action Possible values: "insert_row_above", "insert_row_below", "insert_col_start", "insert_col_end",
|
||
* "remove_row", "remove_col".
|
||
* @param {number|Array} index Row or column visual index which from the alter action will be triggered.
|
||
* Alter actions such as "remove_row" and "remove_col" support array indexes in the
|
||
* format `[[index, amount], [index, amount]...]` this can be used to remove
|
||
* non-consecutive columns or rows in one call.
|
||
* @param {number} [amount=1] Amount of rows or columns to remove.
|
||
* @param {string} [source] Optional. Source of hook runner.
|
||
* @param {boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows.
|
||
*/
|
||
alter(action, index) {
|
||
var _index, _index2;
|
||
let amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
|
||
let source = arguments.length > 3 ? arguments[3] : undefined;
|
||
let keepEmptyRows = arguments.length > 4 ? arguments[4] : undefined;
|
||
const normalizeIndexesGroup = indexes => {
|
||
if (indexes.length === 0) {
|
||
return [];
|
||
}
|
||
const sortedIndexes = [...indexes];
|
||
|
||
// Sort the indexes in ascending order.
|
||
sortedIndexes.sort((_ref2, _ref3) => {
|
||
let [indexA] = _ref2;
|
||
let [indexB] = _ref3;
|
||
if (indexA === indexB) {
|
||
return 0;
|
||
}
|
||
return indexA > indexB ? 1 : -1;
|
||
});
|
||
|
||
// Normalize the {index, amount} groups into bigger groups.
|
||
const normalizedIndexes = arrayReduce(sortedIndexes, (acc, _ref4) => {
|
||
let [groupIndex, groupAmount] = _ref4;
|
||
const previousItem = acc[acc.length - 1];
|
||
const [prevIndex, prevAmount] = previousItem;
|
||
const prevLastIndex = prevIndex + prevAmount;
|
||
if (groupIndex <= prevLastIndex) {
|
||
const amountToAdd = Math.max(groupAmount - (prevLastIndex - groupIndex), 0);
|
||
previousItem[1] += amountToAdd;
|
||
} else {
|
||
acc.push([groupIndex, groupAmount]);
|
||
}
|
||
return acc;
|
||
}, [sortedIndexes[0]]);
|
||
return normalizedIndexes;
|
||
};
|
||
|
||
/* eslint-disable no-case-declarations */
|
||
switch (action) {
|
||
case 'insert_row_below':
|
||
case 'insert_row_above':
|
||
const numberOfSourceRows = instance.countSourceRows();
|
||
if (tableMeta.maxRows === numberOfSourceRows) {
|
||
return;
|
||
}
|
||
|
||
// `above` is the default behavior for creating new rows
|
||
const insertRowMode = action === 'insert_row_below' ? 'below' : 'above';
|
||
|
||
// Calling the `insert_row_above` action adds a new row at the beginning of the data set.
|
||
// eslint-disable-next-line no-param-reassign
|
||
index = (_index = index) !== null && _index !== void 0 ? _index : insertRowMode === 'below' ? numberOfSourceRows : 0;
|
||
const {
|
||
delta: rowDelta,
|
||
startPhysicalIndex: startRowPhysicalIndex
|
||
} = datamap.createRow(index, amount, {
|
||
source,
|
||
mode: insertRowMode
|
||
});
|
||
if (rowDelta) {
|
||
const currentSelectedRange = selection.selectedRange.current();
|
||
const currentFromRange = currentSelectedRange === null || currentSelectedRange === void 0 ? void 0 : currentSelectedRange.from;
|
||
const currentFromRow = currentFromRange === null || currentFromRange === void 0 ? void 0 : currentFromRange.row;
|
||
const startVisualRowIndex = instance.toVisualRow(startRowPhysicalIndex);
|
||
if (selection.isSelectedByCorner()) {
|
||
instance.selectAll();
|
||
} else if (isDefined(currentFromRow) && currentFromRow >= startVisualRowIndex) {
|
||
// Moving the selection (if it exists) downward – it should be applied to the "old" row.
|
||
// TODO: The logic here should be handled by selection module.
|
||
const {
|
||
row: currentToRow,
|
||
col: currentToColumn
|
||
} = currentSelectedRange.to;
|
||
let currentFromColumn = currentFromRange.col;
|
||
|
||
// Workaround: headers are not stored inside selection.
|
||
if (selection.isSelectedByRowHeader()) {
|
||
currentFromColumn = -1;
|
||
}
|
||
|
||
// Remove from the stack the last added selection as that selection below will be
|
||
// replaced by new transformed selection.
|
||
selection.getSelectedRange().pop();
|
||
// I can't use transforms as they don't work in negative indexes.
|
||
selection.setRangeStartOnly(instance._createCellCoords(currentFromRow + rowDelta, currentFromColumn), true);
|
||
selection.setRangeEnd(instance._createCellCoords(currentToRow + rowDelta, currentToColumn)); // will call render() internally
|
||
} else {
|
||
instance._refreshBorders(); // it will call render and prepare methods
|
||
}
|
||
}
|
||
|
||
break;
|
||
case 'insert_col_start':
|
||
case 'insert_col_end':
|
||
// "start" is a default behavior for creating new columns
|
||
const insertColumnMode = action === 'insert_col_end' ? 'end' : 'start';
|
||
|
||
// Calling the `insert_col_start` action adds a new column to the left of the data set.
|
||
// eslint-disable-next-line no-param-reassign
|
||
index = (_index2 = index) !== null && _index2 !== void 0 ? _index2 : insertColumnMode === 'end' ? instance.countSourceCols() : 0;
|
||
const {
|
||
delta: colDelta,
|
||
startPhysicalIndex: startColumnPhysicalIndex
|
||
} = datamap.createCol(index, amount, {
|
||
source,
|
||
mode: insertColumnMode
|
||
});
|
||
if (colDelta) {
|
||
if (Array.isArray(tableMeta.colHeaders)) {
|
||
const spliceArray = [instance.toVisualColumn(startColumnPhysicalIndex), 0];
|
||
spliceArray.length += colDelta; // inserts empty (undefined) elements at the end of an array
|
||
Array.prototype.splice.apply(tableMeta.colHeaders, spliceArray); // inserts empty (undefined) elements into the colHeader array
|
||
}
|
||
|
||
const currentSelectedRange = selection.selectedRange.current();
|
||
const currentFromRange = currentSelectedRange === null || currentSelectedRange === void 0 ? void 0 : currentSelectedRange.from;
|
||
const currentFromColumn = currentFromRange === null || currentFromRange === void 0 ? void 0 : currentFromRange.col;
|
||
const startVisualColumnIndex = instance.toVisualColumn(startColumnPhysicalIndex);
|
||
if (selection.isSelectedByCorner()) {
|
||
instance.selectAll();
|
||
} else if (isDefined(currentFromColumn) && currentFromColumn >= startVisualColumnIndex) {
|
||
// Moving the selection (if it exists) rightward – it should be applied to the "old" column.
|
||
// TODO: The logic here should be handled by selection module.
|
||
const {
|
||
row: currentToRow,
|
||
col: currentToColumn
|
||
} = currentSelectedRange.to;
|
||
let currentFromRow = currentFromRange.row;
|
||
|
||
// Workaround: headers are not stored inside selection.
|
||
if (selection.isSelectedByColumnHeader()) {
|
||
currentFromRow = -1;
|
||
}
|
||
|
||
// Remove from the stack the last added selection as that selection below will be
|
||
// replaced by new transformed selection.
|
||
selection.getSelectedRange().pop();
|
||
|
||
// I can't use transforms as they don't work in negative indexes.
|
||
selection.setRangeStartOnly(instance._createCellCoords(currentFromRow, currentFromColumn + colDelta), true);
|
||
selection.setRangeEnd(instance._createCellCoords(currentToRow, currentToColumn + colDelta)); // will call render() internally
|
||
} else {
|
||
instance._refreshBorders(); // it will call render and prepare methods
|
||
}
|
||
}
|
||
|
||
break;
|
||
case 'remove_row':
|
||
const removeRow = indexes => {
|
||
let offset = 0;
|
||
|
||
// Normalize the {index, amount} groups into bigger groups.
|
||
arrayEach(indexes, _ref5 => {
|
||
let [groupIndex, groupAmount] = _ref5;
|
||
const calcIndex = isEmpty(groupIndex) ? instance.countRows() - 1 : Math.max(groupIndex - offset, 0);
|
||
|
||
// If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
|
||
// compatible with datamap.removeCol method.
|
||
if (Number.isInteger(groupIndex)) {
|
||
// eslint-disable-next-line no-param-reassign
|
||
groupIndex = Math.max(groupIndex - offset, 0);
|
||
}
|
||
|
||
// TODO: for datamap.removeRow index should be passed as it is (with undefined and null values). If not, the logic
|
||
// inside the datamap.removeRow breaks the removing functionality.
|
||
const wasRemoved = datamap.removeRow(groupIndex, groupAmount, source);
|
||
if (!wasRemoved) {
|
||
return;
|
||
}
|
||
const totalRows = instance.countRows();
|
||
const fixedRowsTop = tableMeta.fixedRowsTop;
|
||
if (fixedRowsTop >= calcIndex + 1) {
|
||
tableMeta.fixedRowsTop -= Math.min(groupAmount, fixedRowsTop - calcIndex);
|
||
}
|
||
const fixedRowsBottom = tableMeta.fixedRowsBottom;
|
||
if (fixedRowsBottom && calcIndex >= totalRows - fixedRowsBottom) {
|
||
tableMeta.fixedRowsBottom -= Math.min(groupAmount, fixedRowsBottom);
|
||
}
|
||
offset += groupAmount;
|
||
});
|
||
};
|
||
if (Array.isArray(index)) {
|
||
removeRow(normalizeIndexesGroup(index));
|
||
} else {
|
||
removeRow([[index, amount]]);
|
||
}
|
||
grid.adjustRowsAndCols();
|
||
instance._refreshBorders(); // it will call render and prepare methods
|
||
break;
|
||
case 'remove_col':
|
||
const removeCol = indexes => {
|
||
let offset = 0;
|
||
|
||
// Normalize the {index, amount} groups into bigger groups.
|
||
arrayEach(indexes, _ref6 => {
|
||
let [groupIndex, groupAmount] = _ref6;
|
||
const calcIndex = isEmpty(groupIndex) ? instance.countCols() - 1 : Math.max(groupIndex - offset, 0);
|
||
let physicalColumnIndex = instance.toPhysicalColumn(calcIndex);
|
||
|
||
// If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
|
||
// compatible with datamap.removeCol method.
|
||
if (Number.isInteger(groupIndex)) {
|
||
// eslint-disable-next-line no-param-reassign
|
||
groupIndex = Math.max(groupIndex - offset, 0);
|
||
}
|
||
|
||
// TODO: for datamap.removeCol index should be passed as it is (with undefined and null values). If not, the logic
|
||
// inside the datamap.removeCol breaks the removing functionality.
|
||
const wasRemoved = datamap.removeCol(groupIndex, groupAmount, source);
|
||
if (!wasRemoved) {
|
||
return;
|
||
}
|
||
const fixedColumnsStart = tableMeta.fixedColumnsStart;
|
||
if (fixedColumnsStart >= calcIndex + 1) {
|
||
tableMeta.fixedColumnsStart -= Math.min(groupAmount, fixedColumnsStart - calcIndex);
|
||
}
|
||
if (Array.isArray(tableMeta.colHeaders)) {
|
||
if (typeof physicalColumnIndex === 'undefined') {
|
||
physicalColumnIndex = -1;
|
||
}
|
||
tableMeta.colHeaders.splice(physicalColumnIndex, groupAmount);
|
||
}
|
||
offset += groupAmount;
|
||
});
|
||
};
|
||
if (Array.isArray(index)) {
|
||
removeCol(normalizeIndexesGroup(index));
|
||
} else {
|
||
removeCol([[index, amount]]);
|
||
}
|
||
grid.adjustRowsAndCols();
|
||
instance._refreshBorders(); // it will call render and prepare methods
|
||
|
||
break;
|
||
default:
|
||
throw new Error(`There is no such action "${action}"`);
|
||
}
|
||
if (!keepEmptyRows) {
|
||
grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Makes sure there are empty rows at the bottom of the table.
|
||
*
|
||
* @private
|
||
*/
|
||
adjustRowsAndCols() {
|
||
const minRows = tableMeta.minRows;
|
||
const minSpareRows = tableMeta.minSpareRows;
|
||
const minCols = tableMeta.minCols;
|
||
const minSpareCols = tableMeta.minSpareCols;
|
||
if (instance.countRows() === 0 && instance.countCols() === 0) {
|
||
selection.deselect();
|
||
}
|
||
if (minRows) {
|
||
// should I add empty rows to data source to meet minRows?
|
||
const nrOfRows = instance.countRows();
|
||
if (nrOfRows < minRows) {
|
||
// The synchronization with cell meta is not desired here. For `minRows` option,
|
||
// we don't want to touch/shift cell meta objects.
|
||
datamap.createRow(nrOfRows, minRows - nrOfRows, {
|
||
source: 'auto'
|
||
});
|
||
}
|
||
}
|
||
if (minSpareRows) {
|
||
const emptyRows = instance.countEmptyRows(true);
|
||
|
||
// should I add empty rows to meet minSpareRows?
|
||
if (emptyRows < minSpareRows) {
|
||
const emptyRowsMissing = minSpareRows - emptyRows;
|
||
const rowsToCreate = Math.min(emptyRowsMissing, tableMeta.maxRows - instance.countSourceRows());
|
||
|
||
// The synchronization with cell meta is not desired here. For `minSpareRows` option,
|
||
// we don't want to touch/shift cell meta objects.
|
||
datamap.createRow(instance.countRows(), rowsToCreate, {
|
||
source: 'auto'
|
||
});
|
||
}
|
||
}
|
||
{
|
||
let emptyCols;
|
||
|
||
// count currently empty cols
|
||
if (minCols || minSpareCols) {
|
||
emptyCols = instance.countEmptyCols(true);
|
||
}
|
||
let nrOfColumns = instance.countCols();
|
||
|
||
// should I add empty cols to meet minCols?
|
||
if (minCols && !tableMeta.columns && nrOfColumns < minCols) {
|
||
// The synchronization with cell meta is not desired here. For `minSpareRows` option,
|
||
// we don't want to touch/shift cell meta objects.
|
||
const colsToCreate = minCols - nrOfColumns;
|
||
emptyCols += colsToCreate;
|
||
datamap.createCol(nrOfColumns, colsToCreate, {
|
||
source: 'auto'
|
||
});
|
||
}
|
||
// should I add empty cols to meet minSpareCols?
|
||
if (minSpareCols && !tableMeta.columns && instance.dataType === 'array' && emptyCols < minSpareCols) {
|
||
nrOfColumns = instance.countCols();
|
||
const emptyColsMissing = minSpareCols - emptyCols;
|
||
const colsToCreate = Math.min(emptyColsMissing, tableMeta.maxCols - nrOfColumns);
|
||
|
||
// The synchronization with cell meta is not desired here. For `minSpareRows` option,
|
||
// we don't want to touch/shift cell meta objects.
|
||
datamap.createCol(nrOfColumns, colsToCreate, {
|
||
source: 'auto'
|
||
});
|
||
}
|
||
}
|
||
if (selection.isSelected()) {
|
||
const rowCount = instance.countRows();
|
||
const colCount = instance.countCols();
|
||
arrayEach(selection.selectedRange, range => {
|
||
let selectionChanged = false;
|
||
let fromRow = range.from.row;
|
||
let fromCol = range.from.col;
|
||
let toRow = range.to.row;
|
||
let toCol = range.to.col;
|
||
|
||
// if selection is outside, move selection to last row
|
||
if (fromRow > rowCount - 1) {
|
||
fromRow = rowCount - 1;
|
||
selectionChanged = true;
|
||
if (toRow > fromRow) {
|
||
toRow = fromRow;
|
||
}
|
||
} else if (toRow > rowCount - 1) {
|
||
toRow = rowCount - 1;
|
||
selectionChanged = true;
|
||
if (fromRow > toRow) {
|
||
fromRow = toRow;
|
||
}
|
||
}
|
||
// if selection is outside, move selection to last row
|
||
if (fromCol > colCount - 1) {
|
||
fromCol = colCount - 1;
|
||
selectionChanged = true;
|
||
if (toCol > fromCol) {
|
||
toCol = fromCol;
|
||
}
|
||
} else if (toCol > colCount - 1) {
|
||
toCol = colCount - 1;
|
||
selectionChanged = true;
|
||
if (fromCol > toCol) {
|
||
fromCol = toCol;
|
||
}
|
||
}
|
||
if (selectionChanged) {
|
||
instance.selectCell(fromRow, fromCol, toRow, toCol);
|
||
}
|
||
});
|
||
}
|
||
if (instance.view) {
|
||
instance.view.adjustElementsSize();
|
||
}
|
||
},
|
||
/**
|
||
* Populate the data from the provided 2d array from the given cell coordinates.
|
||
*
|
||
* @private
|
||
* @param {object} start Start selection position. Visual indexes.
|
||
* @param {Array} input 2d data array.
|
||
* @param {object} [end] End selection position (only for drag-down mode). Visual indexes.
|
||
* @param {string} [source="populateFromArray"] Source information string.
|
||
* @param {string} [method="overwrite"] Populate method. Possible options: `shift_down`, `shift_right`, `overwrite`.
|
||
* @returns {object|undefined} Ending td in pasted area (only if any cell was changed).
|
||
*/
|
||
populateFromArray(start, input, end, source, method) {
|
||
let r;
|
||
let rlen;
|
||
let c;
|
||
let clen;
|
||
const setData = [];
|
||
const current = {};
|
||
const newDataByColumns = [];
|
||
const startRow = start.row;
|
||
const startColumn = start.col;
|
||
rlen = input.length;
|
||
if (rlen === 0) {
|
||
return false;
|
||
}
|
||
let columnsPopulationEnd = 0;
|
||
let rowsPopulationEnd = 0;
|
||
if (isObject(end)) {
|
||
columnsPopulationEnd = end.col - startColumn + 1;
|
||
rowsPopulationEnd = end.row - startRow + 1;
|
||
}
|
||
|
||
// insert data with specified pasteMode method
|
||
switch (method) {
|
||
case 'shift_down':
|
||
// translate data from a list of rows to a list of columns
|
||
const populatedDataByColumns = pivot(input);
|
||
const numberOfDataColumns = populatedDataByColumns.length;
|
||
// method's argument can extend the range of data population (data would be repeated)
|
||
const numberOfColumnsToPopulate = Math.max(numberOfDataColumns, columnsPopulationEnd);
|
||
const pushedDownDataByRows = instance.getData().slice(startRow);
|
||
|
||
// translate data from a list of rows to a list of columns
|
||
const pushedDownDataByColumns = pivot(pushedDownDataByRows).slice(startColumn, startColumn + numberOfColumnsToPopulate);
|
||
for (c = 0; c < numberOfColumnsToPopulate; c += 1) {
|
||
if (c < numberOfDataColumns) {
|
||
for (r = 0, rlen = populatedDataByColumns[c].length; r < rowsPopulationEnd - rlen; r += 1) {
|
||
// repeating data for rows
|
||
populatedDataByColumns[c].push(populatedDataByColumns[c][r % rlen]);
|
||
}
|
||
if (c < pushedDownDataByColumns.length) {
|
||
newDataByColumns.push(populatedDataByColumns[c].concat(pushedDownDataByColumns[c]));
|
||
} else {
|
||
// if before data population, there was no data in the column
|
||
// we fill the required rows' newly-created cells with `null` values
|
||
newDataByColumns.push(populatedDataByColumns[c].concat(new Array(pushedDownDataByRows.length).fill(null)));
|
||
}
|
||
} else {
|
||
// Repeating data for columns.
|
||
newDataByColumns.push(populatedDataByColumns[c % numberOfDataColumns].concat(pushedDownDataByColumns[c]));
|
||
}
|
||
}
|
||
instance.populateFromArray(startRow, startColumn, pivot(newDataByColumns));
|
||
break;
|
||
case 'shift_right':
|
||
const numberOfDataRows = input.length;
|
||
// method's argument can extend the range of data population (data would be repeated)
|
||
const numberOfRowsToPopulate = Math.max(numberOfDataRows, rowsPopulationEnd);
|
||
const pushedRightDataByRows = instance.getData().slice(startRow).map(rowData => rowData.slice(startColumn));
|
||
for (r = 0; r < numberOfRowsToPopulate; r += 1) {
|
||
if (r < numberOfDataRows) {
|
||
for (c = 0, clen = input[r].length; c < columnsPopulationEnd - clen; c += 1) {
|
||
// repeating data for rows
|
||
input[r].push(input[r][c % clen]);
|
||
}
|
||
if (r < pushedRightDataByRows.length) {
|
||
for (let i = 0; i < pushedRightDataByRows[r].length; i += 1) {
|
||
input[r].push(pushedRightDataByRows[r][i]);
|
||
}
|
||
} else {
|
||
// if before data population, there was no data in the row
|
||
// we fill the required columns' newly-created cells with `null` values
|
||
input[r].push(...new Array(pushedRightDataByRows[0].length).fill(null));
|
||
}
|
||
} else {
|
||
// Repeating data for columns.
|
||
input.push(input[r % rlen].slice(0, numberOfRowsToPopulate).concat(pushedRightDataByRows[r]));
|
||
}
|
||
}
|
||
instance.populateFromArray(startRow, startColumn, input);
|
||
break;
|
||
case 'overwrite':
|
||
default:
|
||
// overwrite and other not specified options
|
||
current.row = start.row;
|
||
current.col = start.col;
|
||
let skippedRow = 0;
|
||
let skippedColumn = 0;
|
||
let pushData = true;
|
||
let cellMeta;
|
||
const getInputValue = function getInputValue(row) {
|
||
let col = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
||
const rowValue = input[row % input.length];
|
||
if (col !== null) {
|
||
return rowValue[col % rowValue.length];
|
||
}
|
||
return rowValue;
|
||
};
|
||
const rowInputLength = input.length;
|
||
const rowSelectionLength = end ? end.row - start.row + 1 : 0;
|
||
if (end) {
|
||
rlen = rowSelectionLength;
|
||
} else {
|
||
rlen = Math.max(rowInputLength, rowSelectionLength);
|
||
}
|
||
for (r = 0; r < rlen; r++) {
|
||
if (end && current.row > end.row && rowSelectionLength > rowInputLength || !tableMeta.allowInsertRow && current.row > instance.countRows() - 1 || current.row >= tableMeta.maxRows) {
|
||
break;
|
||
}
|
||
const visualRow = r - skippedRow;
|
||
const colInputLength = getInputValue(visualRow).length;
|
||
const colSelectionLength = end ? end.col - start.col + 1 : 0;
|
||
if (end) {
|
||
clen = colSelectionLength;
|
||
} else {
|
||
clen = Math.max(colInputLength, colSelectionLength);
|
||
}
|
||
current.col = start.col;
|
||
cellMeta = instance.getCellMeta(current.row, current.col);
|
||
if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipRowOnPaste) {
|
||
skippedRow += 1;
|
||
current.row += 1;
|
||
rlen += 1;
|
||
/* eslint-disable no-continue */
|
||
continue;
|
||
}
|
||
skippedColumn = 0;
|
||
for (c = 0; c < clen; c++) {
|
||
if (end && current.col > end.col && colSelectionLength > colInputLength || !tableMeta.allowInsertColumn && current.col > instance.countCols() - 1 || current.col >= tableMeta.maxCols) {
|
||
break;
|
||
}
|
||
cellMeta = instance.getCellMeta(current.row, current.col);
|
||
if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipColumnOnPaste) {
|
||
skippedColumn += 1;
|
||
current.col += 1;
|
||
clen += 1;
|
||
continue;
|
||
}
|
||
if (cellMeta.readOnly && source !== 'UndoRedo.undo') {
|
||
current.col += 1;
|
||
/* eslint-disable no-continue */
|
||
continue;
|
||
}
|
||
const visualColumn = c - skippedColumn;
|
||
let value = getInputValue(visualRow, visualColumn);
|
||
let orgValue = instance.getDataAtCell(current.row, current.col);
|
||
if (value !== null && typeof value === 'object') {
|
||
// when 'value' is array and 'orgValue' is null, set 'orgValue' to
|
||
// an empty array so that the null value can be compared to 'value'
|
||
// as an empty value for the array context
|
||
if (Array.isArray(value) && orgValue === null) orgValue = [];
|
||
if (orgValue === null || typeof orgValue !== 'object') {
|
||
pushData = false;
|
||
} else {
|
||
const orgValueSchema = duckSchema(Array.isArray(orgValue) ? orgValue : orgValue[0] || orgValue);
|
||
const valueSchema = duckSchema(Array.isArray(value) ? value : value[0] || value);
|
||
|
||
// Allow overwriting values with the same object-based schema or any array-based schema.
|
||
if (isObjectEqual(orgValueSchema, valueSchema) || Array.isArray(orgValueSchema) && Array.isArray(valueSchema)) {
|
||
value = deepClone(value);
|
||
} else {
|
||
pushData = false;
|
||
}
|
||
}
|
||
} else if (orgValue !== null && typeof orgValue === 'object') {
|
||
pushData = false;
|
||
}
|
||
if (pushData) {
|
||
setData.push([current.row, current.col, value]);
|
||
}
|
||
pushData = true;
|
||
current.col += 1;
|
||
}
|
||
current.row += 1;
|
||
}
|
||
instance.setDataAtCell(setData, null, null, source || 'populateFromArray');
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Internal function to set `language` key of settings.
|
||
*
|
||
* @private
|
||
* @param {string} languageCode Language code for specific language i.e. 'en-US', 'pt-BR', 'de-DE'.
|
||
* @fires Hooks#afterLanguageChange
|
||
*/
|
||
function setLanguage(languageCode) {
|
||
const normalizedLanguageCode = normalizeLanguageCode(languageCode);
|
||
if (hasLanguageDictionary(normalizedLanguageCode)) {
|
||
instance.runHooks('beforeLanguageChange', normalizedLanguageCode);
|
||
globalMeta.language = normalizedLanguageCode;
|
||
instance.runHooks('afterLanguageChange', normalizedLanguageCode);
|
||
} else {
|
||
warnUserAboutLanguageRegistration(languageCode);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Internal function to set `className` or `tableClassName`, depending on the key from the settings object.
|
||
*
|
||
* @private
|
||
* @param {string} className `className` or `tableClassName` from the key in the settings object.
|
||
* @param {string|string[]} classSettings String or array of strings. Contains class name(s) from settings object.
|
||
*/
|
||
function setClassName(className, classSettings) {
|
||
const element = className === 'className' ? instance.rootElement : instance.table;
|
||
if (firstRun) {
|
||
addClass(element, classSettings);
|
||
} else {
|
||
let globalMetaSettingsArray = [];
|
||
let settingsArray = [];
|
||
if (globalMeta[className]) {
|
||
globalMetaSettingsArray = Array.isArray(globalMeta[className]) ? globalMeta[className] : stringToArray(globalMeta[className]);
|
||
}
|
||
if (classSettings) {
|
||
settingsArray = Array.isArray(classSettings) ? classSettings : stringToArray(classSettings);
|
||
}
|
||
const classNameToRemove = getDifferenceOfArrays(globalMetaSettingsArray, settingsArray);
|
||
const classNameToAdd = getDifferenceOfArrays(settingsArray, globalMetaSettingsArray);
|
||
if (classNameToRemove.length) {
|
||
removeClass(element, classNameToRemove);
|
||
}
|
||
if (classNameToAdd.length) {
|
||
addClass(element, classNameToAdd);
|
||
}
|
||
}
|
||
globalMeta[className] = classSettings;
|
||
}
|
||
this.init = function () {
|
||
dataSource.setData(tableMeta.data);
|
||
instance.runHooks('beforeInit');
|
||
if (isMobileBrowser() || isIpadOS()) {
|
||
addClass(instance.rootElement, 'mobile');
|
||
}
|
||
this.updateSettings(tableMeta, true);
|
||
this.view = new TableView(this);
|
||
editorManager = EditorManager.getInstance(instance, tableMeta, selection);
|
||
instance.runHooks('init');
|
||
this.forceFullRender = true; // used when data was changed
|
||
this.view.render();
|
||
|
||
// Run the logic only if it's the table's initialization and the root element is not visible.
|
||
if (!!firstRun && instance.rootElement.offsetParent === null) {
|
||
observeVisibilityChangeOnce(instance.rootElement, () => {
|
||
// Update the spreader size cache before rendering.
|
||
instance.view._wt.wtOverlays.updateLastSpreaderSize();
|
||
instance.render();
|
||
instance.view.adjustElementsSize();
|
||
});
|
||
}
|
||
if (typeof firstRun === 'object') {
|
||
instance.runHooks('afterChange', firstRun[0], firstRun[1]);
|
||
firstRun = false;
|
||
}
|
||
instance.runHooks('afterInit');
|
||
};
|
||
|
||
/**
|
||
* @ignore
|
||
* @returns {object}
|
||
*/
|
||
function ValidatorsQueue() {
|
||
// moved this one level up so it can be used in any function here. Probably this should be moved to a separate file
|
||
let resolved = false;
|
||
return {
|
||
validatorsInQueue: 0,
|
||
valid: true,
|
||
addValidatorToQueue() {
|
||
this.validatorsInQueue += 1;
|
||
resolved = false;
|
||
},
|
||
removeValidatorFormQueue() {
|
||
this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1;
|
||
this.checkIfQueueIsEmpty();
|
||
},
|
||
onQueueEmpty() {},
|
||
checkIfQueueIsEmpty() {
|
||
if (this.validatorsInQueue === 0 && resolved === false) {
|
||
resolved = true;
|
||
this.onQueueEmpty(this.valid);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get parsed number from numeric string.
|
||
*
|
||
* @private
|
||
* @param {string} numericData Float (separated by a dot or a comma) or integer.
|
||
* @returns {number} Number if we get data in parsable format, not changed value otherwise.
|
||
*/
|
||
function getParsedNumber(numericData) {
|
||
// Unifying "float like" string. Change from value with comma determiner to value with dot determiner,
|
||
// for example from `450,65` to `450.65`.
|
||
const unifiedNumericData = numericData.replace(',', '.');
|
||
if (isNaN(parseFloat(unifiedNumericData)) === false) {
|
||
return parseFloat(unifiedNumericData);
|
||
}
|
||
return numericData;
|
||
}
|
||
|
||
/**
|
||
* @ignore
|
||
* @param {Array} changes The 2D array containing information about each of the edited cells.
|
||
* @param {string} source The string that identifies source of validation.
|
||
* @param {Function} callback The callback function fot async validation.
|
||
*/
|
||
function validateChanges(changes, source, callback) {
|
||
if (!changes.length) {
|
||
return;
|
||
}
|
||
const activeEditor = instance.getActiveEditor();
|
||
const waitingForValidator = new ValidatorsQueue();
|
||
let shouldBeCanceled = true;
|
||
waitingForValidator.onQueueEmpty = isValid => {
|
||
if (activeEditor && shouldBeCanceled) {
|
||
activeEditor.cancelChanges();
|
||
}
|
||
callback(isValid); // called when async validators are resolved and beforeChange was not async
|
||
};
|
||
|
||
for (let i = changes.length - 1; i >= 0; i--) {
|
||
const [row, prop,, newValue] = changes[i];
|
||
const col = datamap.propToCol(prop);
|
||
const cellProperties = instance.getCellMeta(row, col);
|
||
if (cellProperties.type === 'numeric' && typeof newValue === 'string' && isNumericLike(newValue)) {
|
||
changes[i][3] = getParsedNumber(newValue);
|
||
}
|
||
|
||
/* eslint-disable no-loop-func */
|
||
if (instance.getCellValidator(cellProperties)) {
|
||
waitingForValidator.addValidatorToQueue();
|
||
instance.validateCell(changes[i][3], cellProperties, function (index, cellPropertiesReference) {
|
||
return function (result) {
|
||
if (typeof result !== 'boolean') {
|
||
throw new Error('Validation error: result is not boolean');
|
||
}
|
||
if (result === false && cellPropertiesReference.allowInvalid === false) {
|
||
shouldBeCanceled = false;
|
||
changes.splice(index, 1); // cancel the change
|
||
cellPropertiesReference.valid = true; // we cancelled the change, so cell value is still valid
|
||
|
||
const cell = instance.getCell(cellPropertiesReference.visualRow, cellPropertiesReference.visualCol);
|
||
if (cell !== null) {
|
||
removeClass(cell, tableMeta.invalidCellClassName);
|
||
}
|
||
}
|
||
waitingForValidator.removeValidatorFormQueue();
|
||
};
|
||
}(i, cellProperties), source);
|
||
}
|
||
}
|
||
waitingForValidator.checkIfQueueIsEmpty();
|
||
}
|
||
|
||
/**
|
||
* Internal function to apply changes. Called after validateChanges.
|
||
*
|
||
* @private
|
||
* @param {Array} changes Array in form of [row, prop, oldValue, newValue].
|
||
* @param {string} source String that identifies how this change will be described in changes array (useful in onChange callback).
|
||
* @fires Hooks#beforeChangeRender
|
||
* @fires Hooks#afterChange
|
||
*/
|
||
function applyChanges(changes, source) {
|
||
let i = changes.length - 1;
|
||
if (i < 0) {
|
||
return;
|
||
}
|
||
for (; i >= 0; i--) {
|
||
let skipThisChange = false;
|
||
if (changes[i] === null) {
|
||
changes.splice(i, 1);
|
||
/* eslint-disable no-continue */
|
||
continue;
|
||
}
|
||
if ((changes[i][2] === null || changes[i][2] === void 0) && (changes[i][3] === null || changes[i][3] === void 0)) {
|
||
/* eslint-disable no-continue */
|
||
continue;
|
||
}
|
||
if (tableMeta.allowInsertRow) {
|
||
while (changes[i][0] > instance.countRows() - 1) {
|
||
const {
|
||
delta: numberOfCreatedRows
|
||
} = datamap.createRow(void 0, void 0, {
|
||
source
|
||
});
|
||
if (numberOfCreatedRows === 0) {
|
||
skipThisChange = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (instance.dataType === 'array' && (!tableMeta.columns || tableMeta.columns.length === 0) && tableMeta.allowInsertColumn) {
|
||
while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) {
|
||
const {
|
||
delta: numberOfCreatedColumns
|
||
} = datamap.createCol(void 0, void 0, {
|
||
source
|
||
});
|
||
if (numberOfCreatedColumns === 0) {
|
||
skipThisChange = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (skipThisChange) {
|
||
/* eslint-disable no-continue */
|
||
continue;
|
||
}
|
||
datamap.set(changes[i][0], changes[i][1], changes[i][3]);
|
||
}
|
||
instance.forceFullRender = true; // used when data was changed
|
||
grid.adjustRowsAndCols();
|
||
instance.runHooks('beforeChangeRender', changes, source);
|
||
editorManager.lockEditor();
|
||
instance._refreshBorders(null);
|
||
editorManager.unlockEditor();
|
||
instance.view.adjustElementsSize();
|
||
instance.runHooks('afterChange', changes, source || 'edit');
|
||
const activeEditor = instance.getActiveEditor();
|
||
if (activeEditor && isDefined(activeEditor.refreshValue)) {
|
||
activeEditor.refreshValue();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates and returns the CellCoords object.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function _createCellCoords
|
||
* @param {number} row The row index.
|
||
* @param {number} column The column index.
|
||
* @returns {CellCoords}
|
||
*/
|
||
this._createCellCoords = function (row, column) {
|
||
return instance.view._wt.createCellCoords(row, column);
|
||
};
|
||
|
||
/**
|
||
* Creates and returns the CellRange object.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function _createCellRange
|
||
* @param {CellCoords} highlight Defines the border around a cell where selection was started and to edit the cell
|
||
* when you press Enter. The highlight cannot point to headers (negative values).
|
||
* @param {CellCoords} from Initial coordinates.
|
||
* @param {CellCoords} to Final coordinates.
|
||
* @returns {CellRange}
|
||
*/
|
||
this._createCellRange = function (highlight, from, to) {
|
||
return instance.view._wt.createCellRange(highlight, from, to);
|
||
};
|
||
|
||
/**
|
||
* Validate a single cell.
|
||
*
|
||
* @memberof Core#
|
||
* @function validateCell
|
||
* @param {string|number} value The value to validate.
|
||
* @param {object} cellProperties The cell meta which corresponds with the value.
|
||
* @param {Function} callback The callback function.
|
||
* @param {string} source The string that identifies source of the validation.
|
||
*/
|
||
this.validateCell = function (value, cellProperties, callback, source) {
|
||
let validator = instance.getCellValidator(cellProperties);
|
||
|
||
// the `canBeValidated = false` argument suggests, that the cell passes validation by default.
|
||
/**
|
||
* @private
|
||
* @function done
|
||
* @param {boolean} valid Indicates if the validation was successful.
|
||
* @param {boolean} [canBeValidated=true] Flag which controls the validation process.
|
||
*/
|
||
function done(valid) {
|
||
let canBeValidated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||
// Fixes GH#3903
|
||
if (!canBeValidated || cellProperties.hidden === true) {
|
||
callback(valid);
|
||
return;
|
||
}
|
||
const col = cellProperties.visualCol;
|
||
const row = cellProperties.visualRow;
|
||
const td = instance.getCell(row, col, true);
|
||
if (td && td.nodeName !== 'TH') {
|
||
const renderableRow = instance.rowIndexMapper.getRenderableFromVisualIndex(row);
|
||
const renderableColumn = instance.columnIndexMapper.getRenderableFromVisualIndex(col);
|
||
instance.view._wt.getSetting('cellRenderer', renderableRow, renderableColumn, td);
|
||
}
|
||
callback(valid);
|
||
}
|
||
if (isRegExp(validator)) {
|
||
validator = function (expression) {
|
||
return function (cellValue, validatorCallback) {
|
||
validatorCallback(expression.test(cellValue));
|
||
};
|
||
}(validator);
|
||
}
|
||
if (isFunction(validator)) {
|
||
// eslint-disable-next-line no-param-reassign
|
||
value = instance.runHooks('beforeValidate', value, cellProperties.visualRow, cellProperties.prop, source);
|
||
|
||
// To provide consistent behaviour, validation should be always asynchronous
|
||
instance._registerImmediate(() => {
|
||
validator.call(cellProperties, value, valid => {
|
||
if (!instance) {
|
||
return;
|
||
}
|
||
// eslint-disable-next-line no-param-reassign
|
||
valid = instance.runHooks('afterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
|
||
cellProperties.valid = valid;
|
||
done(valid);
|
||
instance.runHooks('postAfterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
|
||
});
|
||
});
|
||
} else {
|
||
// resolve callback even if validator function was not found
|
||
instance._registerImmediate(() => {
|
||
cellProperties.valid = true;
|
||
done(cellProperties.valid, false);
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @ignore
|
||
* @param {number} row The visual row index.
|
||
* @param {string|number} propOrCol The visual prop or column index.
|
||
* @param {*} value The cell value.
|
||
* @returns {Array}
|
||
*/
|
||
function setDataInputToArray(row, propOrCol, value) {
|
||
if (Array.isArray(row)) {
|
||
// it's an array of changes
|
||
return row;
|
||
}
|
||
return [[row, propOrCol, value]];
|
||
}
|
||
|
||
/**
|
||
* Process changes prepared for applying to the dataset (unifying list of changes, closing an editor - when needed,
|
||
* calling a hook).
|
||
*
|
||
* @private
|
||
* @param {Array} changes Array of changes in format `[[row, col, value],...]`.
|
||
* @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.
|
||
* @returns {Array} List of changes finally applied to the dataset.
|
||
*/
|
||
function processChanges(changes, source) {
|
||
const activeEditor = instance.getActiveEditor();
|
||
const beforeChangeResult = instance.runHooks('beforeChange', changes, source || 'edit');
|
||
// The `beforeChange` hook could add a `null` for purpose of cancelling some dataset's change.
|
||
const filteredChanges = changes.filter(change => change !== null);
|
||
if (beforeChangeResult === false || filteredChanges.length === 0) {
|
||
if (activeEditor) {
|
||
activeEditor.cancelChanges();
|
||
}
|
||
return [];
|
||
}
|
||
return filteredChanges;
|
||
}
|
||
|
||
/**
|
||
* @description
|
||
* Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
|
||
* `[[row, col, value],...]` as the first argument.
|
||
*
|
||
* @memberof Core#
|
||
* @function setDataAtCell
|
||
* @param {number|Array} row Visual row index or array of changes in format `[[row, col, value],...]`.
|
||
* @param {number} [column] Visual column index.
|
||
* @param {string} [value] New value.
|
||
* @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.
|
||
*/
|
||
this.setDataAtCell = function (row, column, value, source) {
|
||
const input = setDataInputToArray(row, column, value);
|
||
const changes = [];
|
||
let changeSource = source;
|
||
let i;
|
||
let ilen;
|
||
let prop;
|
||
for (i = 0, ilen = input.length; i < ilen; i++) {
|
||
if (typeof input[i] !== 'object') {
|
||
throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter');
|
||
}
|
||
if (typeof input[i][1] !== 'number') {
|
||
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
|
||
}
|
||
|
||
if (input[i][1] >= this.countCols()) {
|
||
prop = input[i][1];
|
||
} else {
|
||
prop = datamap.colToProp(input[i][1]);
|
||
}
|
||
changes.push([input[i][0], prop, dataSource.getAtCell(this.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]);
|
||
}
|
||
if (!changeSource && typeof row === 'object') {
|
||
changeSource = column;
|
||
}
|
||
const processedChanges = processChanges(changes, source);
|
||
instance.runHooks('afterSetDataAtCell', processedChanges, changeSource);
|
||
validateChanges(processedChanges, changeSource, () => {
|
||
applyChanges(processedChanges, changeSource);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* @description
|
||
* Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
|
||
* `[[row, prop, value],...]` as the first argument.
|
||
*
|
||
* @memberof Core#
|
||
* @function setDataAtRowProp
|
||
* @param {number|Array} row Visual row index or array of changes in format `[[row, prop, value], ...]`.
|
||
* @param {string} prop Property name or the source string (e.g. `'first.name'` or `'0'`).
|
||
* @param {string} value Value to be set.
|
||
* @param {string} [source] String that identifies how this change will be described in changes array (useful in onChange callback).
|
||
*/
|
||
this.setDataAtRowProp = function (row, prop, value, source) {
|
||
const input = setDataInputToArray(row, prop, value);
|
||
const changes = [];
|
||
let changeSource = source;
|
||
let i;
|
||
let ilen;
|
||
for (i = 0, ilen = input.length; i < ilen; i++) {
|
||
changes.push([input[i][0], input[i][1], dataSource.getAtCell(this.toPhysicalRow(input[i][0]), input[i][1]), input[i][2]]);
|
||
}
|
||
if (!changeSource && typeof row === 'object') {
|
||
changeSource = prop;
|
||
}
|
||
const processedChanges = processChanges(changes, source);
|
||
instance.runHooks('afterSetDataAtRowProp', processedChanges, changeSource);
|
||
validateChanges(processedChanges, changeSource, () => {
|
||
applyChanges(processedChanges, changeSource);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Listen to the keyboard input on document body. This allows Handsontable to capture keyboard events and respond
|
||
* in the right way.
|
||
*
|
||
* @memberof Core#
|
||
* @function listen
|
||
* @fires Hooks#afterListen
|
||
*/
|
||
this.listen = function () {
|
||
if (instance && !instance.isListening()) {
|
||
activeGuid = instance.guid;
|
||
instance.runHooks('afterListen');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Stop listening to keyboard input on the document body. Calling this method makes the Handsontable inactive for
|
||
* any keyboard events.
|
||
*
|
||
* @memberof Core#
|
||
* @function unlisten
|
||
*/
|
||
this.unlisten = function () {
|
||
if (this.isListening()) {
|
||
activeGuid = null;
|
||
instance.runHooks('afterUnlisten');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns `true` if the current Handsontable instance is listening to keyboard input on document body.
|
||
*
|
||
* @memberof Core#
|
||
* @function isListening
|
||
* @returns {boolean} `true` if the instance is listening, `false` otherwise.
|
||
*/
|
||
this.isListening = function () {
|
||
return activeGuid === instance.guid;
|
||
};
|
||
|
||
/**
|
||
* Destroys the current editor, render the table and prepares the editor of the newly selected cell.
|
||
*
|
||
* @memberof Core#
|
||
* @function destroyEditor
|
||
* @param {boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved.
|
||
* @param {boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open.
|
||
*/
|
||
this.destroyEditor = function () {
|
||
let revertOriginal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
let prepareEditorIfNeeded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||
instance._refreshBorders(revertOriginal, prepareEditorIfNeeded);
|
||
};
|
||
|
||
/**
|
||
* Populates cells at position with 2D input array (e.g. `[[1, 2], [3, 4]]`). Use `endRow`, `endCol` when you
|
||
* want to cut input when a certain row is reached.
|
||
*
|
||
* The `populateFromArray()` method can't change [`readOnly`](@/api/options.md#readonly) cells.
|
||
*
|
||
* Optional `method` argument has the same effect as pasteMode option (see {@link Options#pasteMode}).
|
||
*
|
||
* @memberof Core#
|
||
* @function populateFromArray
|
||
* @param {number} row Start visual row index.
|
||
* @param {number} column Start visual column index.
|
||
* @param {Array} input 2d array.
|
||
* @param {number} [endRow] End visual row index (use when you want to cut input when certain row is reached).
|
||
* @param {number} [endCol] End visual column index (use when you want to cut input when certain column is reached).
|
||
* @param {string} [source=populateFromArray] Used to identify this call in the resulting events (beforeChange, afterChange).
|
||
* @param {string} [method=overwrite] Populate method, possible values: `'shift_down'`, `'shift_right'`, `'overwrite'`.
|
||
* @returns {object|undefined} Ending td in pasted area (only if any cell was changed).
|
||
*/
|
||
this.populateFromArray = function (row, column, input, endRow, endCol, source, method) {
|
||
if (!(typeof input === 'object' && typeof input[0] === 'object')) {
|
||
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
|
||
}
|
||
|
||
const c = typeof endRow === 'number' ? instance._createCellCoords(endRow, endCol) : null;
|
||
return grid.populateFromArray(instance._createCellCoords(row, column), input, c, source, method);
|
||
};
|
||
|
||
/**
|
||
* Adds/removes data from the column. This method works the same as Array.splice for arrays.
|
||
*
|
||
* @memberof Core#
|
||
* @function spliceCol
|
||
* @param {number} column Index of the column in which do you want to do splice.
|
||
* @param {number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
|
||
* @param {number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
|
||
* @param {...number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
|
||
* @returns {Array} Returns removed portion of columns.
|
||
*/
|
||
this.spliceCol = function (column, index, amount) {
|
||
for (var _len = arguments.length, elements = new Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
|
||
elements[_key - 3] = arguments[_key];
|
||
}
|
||
return datamap.spliceCol(column, index, amount, ...elements);
|
||
};
|
||
|
||
/**
|
||
* Adds/removes data from the row. This method works the same as Array.splice for arrays.
|
||
*
|
||
* @memberof Core#
|
||
* @function spliceRow
|
||
* @param {number} row Index of column in which do you want to do splice.
|
||
* @param {number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
|
||
* @param {number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
|
||
* @param {...number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
|
||
* @returns {Array} Returns removed portion of rows.
|
||
*/
|
||
this.spliceRow = function (row, index, amount) {
|
||
for (var _len2 = arguments.length, elements = new Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) {
|
||
elements[_key2 - 3] = arguments[_key2];
|
||
}
|
||
return datamap.spliceRow(row, index, amount, ...elements);
|
||
};
|
||
|
||
/**
|
||
* Returns indexes of the currently selected cells as an array of arrays `[[startRow, startCol, endRow, endCol],...]`.
|
||
*
|
||
* Start row and start column are the coordinates of the active cell (where the selection was started).
|
||
*
|
||
* The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
|
||
* Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
|
||
* you need to use `getSelectedLast` method.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSelected
|
||
* @returns {Array[]|undefined} An array of arrays of the selection's coordinates.
|
||
*/
|
||
this.getSelected = function () {
|
||
// https://github.com/handsontable/handsontable/issues/44 //cjl
|
||
if (selection.isSelected()) {
|
||
return arrayMap(selection.getSelectedRange(), _ref7 => {
|
||
let {
|
||
from,
|
||
to
|
||
} = _ref7;
|
||
return [from.row, from.col, to.row, to.col];
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns the last coordinates applied to the table as a an array `[startRow, startCol, endRow, endCol]`.
|
||
*
|
||
* @since 0.36.0
|
||
* @memberof Core#
|
||
* @function getSelectedLast
|
||
* @returns {Array|undefined} An array of the selection's coordinates.
|
||
*/
|
||
this.getSelectedLast = function () {
|
||
const selected = this.getSelected();
|
||
let result;
|
||
if (selected && selected.length > 0) {
|
||
result = selected[selected.length - 1];
|
||
}
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Returns the current selection as an array of CellRange objects.
|
||
*
|
||
* The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
|
||
* Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
|
||
* you need to use `getSelectedRangeLast` method.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSelectedRange
|
||
* @returns {CellRange[]|undefined} Selected range object or undefined if there is no selection.
|
||
*/
|
||
this.getSelectedRange = function () {
|
||
// https://github.com/handsontable/handsontable/issues/44 //cjl
|
||
if (selection.isSelected()) {
|
||
return Array.from(selection.getSelectedRange());
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns the last coordinates applied to the table as a CellRange object.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSelectedRangeLast
|
||
* @since 0.36.0
|
||
* @returns {CellRange|undefined} Selected range object or undefined` if there is no selection.
|
||
*/
|
||
this.getSelectedRangeLast = function () {
|
||
const selectedRange = this.getSelectedRange();
|
||
let result;
|
||
if (selectedRange && selectedRange.length > 0) {
|
||
result = selectedRange[selectedRange.length - 1];
|
||
}
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Erases content from cells that have been selected in the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function emptySelectedCells
|
||
* @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.
|
||
* @since 0.36.0
|
||
*/
|
||
this.emptySelectedCells = function (source) {
|
||
if (!selection.isSelected() || this.countRows() === 0 || this.countCols() === 0) {
|
||
return;
|
||
}
|
||
const changes = [];
|
||
arrayEach(selection.getSelectedRange(), cellRange => {
|
||
const topStart = cellRange.getTopStartCorner();
|
||
const bottomEnd = cellRange.getBottomEndCorner();
|
||
rangeEach(topStart.row, bottomEnd.row, row => {
|
||
rangeEach(topStart.col, bottomEnd.col, column => {
|
||
if (!this.getCellMeta(row, column).readOnly) {
|
||
changes.push([row, column, null]);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
if (changes.length > 0) {
|
||
this.setDataAtCell(changes, source);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if the table rendering process was suspended. See explanation in {@link Core#suspendRender}.
|
||
*
|
||
* @memberof Core#
|
||
* @function isRenderSuspended
|
||
* @since 8.3.0
|
||
* @returns {boolean}
|
||
*/
|
||
this.isRenderSuspended = function () {
|
||
return this.renderSuspendedCounter > 0;
|
||
};
|
||
|
||
/**
|
||
* Suspends the rendering process. It's helpful to wrap the table render
|
||
* cycles triggered by API calls or UI actions (or both) and call the "render"
|
||
* once in the end. As a result, it improves the performance of wrapped operations.
|
||
* When the table is in the suspend state, most operations will have no visual
|
||
* effect until the rendering state is resumed. Resuming the state automatically
|
||
* invokes the table rendering. To make sure that after executing all operations,
|
||
* the table will be rendered, it's highly recommended to use the {@link Core#batchRender}
|
||
* method or {@link Core#batch}, which additionally aggregates the logic execution
|
||
* that happens behind the table.
|
||
*
|
||
* The method is intended to be used by advanced users. Suspending the rendering
|
||
* process could cause visual glitches when wrongly implemented.
|
||
*
|
||
* Every [`suspendRender()`](@/api/core.md#suspendrender) call needs to correspond with one [`resumeRender()`](@/api/core.md#resumerender) call.
|
||
* 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.
|
||
*
|
||
* @memberof Core#
|
||
* @function suspendRender
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.suspendRender();
|
||
* hot.alter('insert_row_above', 5, 45);
|
||
* hot.alter('insert_col_start', 10, 40);
|
||
* hot.setDataAtCell(1, 1, 'John');
|
||
* hot.setDataAtCell(2, 2, 'Mark');
|
||
* hot.setDataAtCell(3, 3, 'Ann');
|
||
* hot.setDataAtCell(4, 4, 'Sophia');
|
||
* hot.setDataAtCell(5, 5, 'Mia');
|
||
* hot.selectCell(0, 0);
|
||
* hot.resumeRender(); // It re-renders the table internally
|
||
* ```
|
||
*/
|
||
this.suspendRender = function () {
|
||
this.renderSuspendedCounter += 1;
|
||
};
|
||
|
||
/**
|
||
* Resumes the rendering process. In combination with the {@link Core#suspendRender}
|
||
* method it allows aggregating the table render cycles triggered by API calls or UI
|
||
* actions (or both) and calls the "render" once in the end. When the table is in
|
||
* the suspend state, most operations will have no visual effect until the rendering
|
||
* state is resumed. Resuming the state automatically invokes the table rendering.
|
||
*
|
||
* The method is intended to be used by advanced users. Suspending the rendering
|
||
* process could cause visual glitches when wrongly implemented.
|
||
*
|
||
* Every [`suspendRender()`](@/api/core.md#suspendrender) call needs to correspond with one [`resumeRender()`](@/api/core.md#resumerender) call.
|
||
* 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.
|
||
*
|
||
* @memberof Core#
|
||
* @function resumeRender
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.suspendRender();
|
||
* hot.alter('insert_row_above', 5, 45);
|
||
* hot.alter('insert_col_start', 10, 40);
|
||
* hot.setDataAtCell(1, 1, 'John');
|
||
* hot.setDataAtCell(2, 2, 'Mark');
|
||
* hot.setDataAtCell(3, 3, 'Ann');
|
||
* hot.setDataAtCell(4, 4, 'Sophia');
|
||
* hot.setDataAtCell(5, 5, 'Mia');
|
||
* hot.selectCell(0, 0);
|
||
* hot.resumeRender(); // It re-renders the table internally
|
||
* ```
|
||
*/
|
||
this.resumeRender = function () {
|
||
const nextValue = this.renderSuspendedCounter - 1;
|
||
this.renderSuspendedCounter = Math.max(nextValue, 0);
|
||
if (!this.isRenderSuspended() && nextValue === this.renderSuspendedCounter) {
|
||
if (this.renderCall) {
|
||
this.render();
|
||
} else {
|
||
this._refreshBorders(null);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Rerender the table. Calling this method starts the process of recalculating, redrawing and applying the changes
|
||
* to the DOM. While rendering the table all cell renderers are recalled.
|
||
*
|
||
* Calling this method manually is not recommended. Handsontable tries to render itself by choosing the most
|
||
* optimal moments in its lifecycle.
|
||
*
|
||
* @memberof Core#
|
||
* @function render
|
||
*/
|
||
this.render = function () {
|
||
if (this.view) {
|
||
this.renderCall = true;
|
||
this.forceFullRender = true; // used when data was changed
|
||
|
||
if (!this.isRenderSuspended()) {
|
||
editorManager.lockEditor();
|
||
this._refreshBorders(null);
|
||
editorManager.unlockEditor();
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* The method aggregates multi-line API calls into a callback and postpones the
|
||
* table rendering process. After the execution of the operations, the table is
|
||
* rendered once. As a result, it improves the performance of wrapped operations.
|
||
* Without batching, a similar case could trigger multiple table render calls.
|
||
*
|
||
* @memberof Core#
|
||
* @function batchRender
|
||
* @param {Function} wrappedOperations Batched operations wrapped in a function.
|
||
* @returns {*} Returns result from the wrappedOperations callback.
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.batchRender(() => {
|
||
* hot.alter('insert_row_above', 5, 45);
|
||
* hot.alter('insert_col_start', 10, 40);
|
||
* hot.setDataAtCell(1, 1, 'John');
|
||
* hot.setDataAtCell(2, 2, 'Mark');
|
||
* hot.setDataAtCell(3, 3, 'Ann');
|
||
* hot.setDataAtCell(4, 4, 'Sophia');
|
||
* hot.setDataAtCell(5, 5, 'Mia');
|
||
* hot.selectCell(0, 0);
|
||
* // The table will be rendered once after executing the callback
|
||
* });
|
||
* ```
|
||
*/
|
||
this.batchRender = function (wrappedOperations) {
|
||
this.suspendRender();
|
||
const result = wrappedOperations();
|
||
this.resumeRender();
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Checks if the table indexes recalculation process was suspended. See explanation
|
||
* in {@link Core#suspendExecution}.
|
||
*
|
||
* @memberof Core#
|
||
* @function isExecutionSuspended
|
||
* @since 8.3.0
|
||
* @returns {boolean}
|
||
*/
|
||
this.isExecutionSuspended = function () {
|
||
return this.executionSuspendedCounter > 0;
|
||
};
|
||
|
||
/**
|
||
* Suspends the execution process. It's helpful to wrap the table logic changes
|
||
* such as index changes into one call after which the cache is updated. As a result,
|
||
* it improves the performance of wrapped operations.
|
||
*
|
||
* The method is intended to be used by advanced users. Suspending the execution
|
||
* process could cause visual glitches caused by not updated the internal table cache.
|
||
*
|
||
* @memberof Core#
|
||
* @function suspendExecution
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.suspendExecution();
|
||
* const filters = hot.getPlugin('filters');
|
||
*
|
||
* filters.addCondition(2, 'contains', ['3']);
|
||
* filters.filter();
|
||
* hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
|
||
* hot.resumeExecution(); // It updates the cache internally
|
||
* ```
|
||
*/
|
||
this.suspendExecution = function () {
|
||
this.executionSuspendedCounter += 1;
|
||
this.columnIndexMapper.suspendOperations();
|
||
this.rowIndexMapper.suspendOperations();
|
||
};
|
||
|
||
/**
|
||
* Resumes the execution process. In combination with the {@link Core#suspendExecution}
|
||
* method it allows aggregating the table logic changes after which the cache is
|
||
* updated. Resuming the state automatically invokes the table cache updating process.
|
||
*
|
||
* The method is intended to be used by advanced users. Suspending the execution
|
||
* process could cause visual glitches caused by not updated the internal table cache.
|
||
*
|
||
* @memberof Core#
|
||
* @function resumeExecution
|
||
* @param {boolean} [forceFlushChanges=false] If `true`, the table internal data cache
|
||
* is recalculated after the execution of the batched operations. For nested
|
||
* {@link Core#batchExecution} calls, it can be desire to recalculate the table
|
||
* after each batch.
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.suspendExecution();
|
||
* const filters = hot.getPlugin('filters');
|
||
*
|
||
* filters.addCondition(2, 'contains', ['3']);
|
||
* filters.filter();
|
||
* hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
|
||
* hot.resumeExecution(); // It updates the cache internally
|
||
* ```
|
||
*/
|
||
this.resumeExecution = function () {
|
||
let forceFlushChanges = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
const nextValue = this.executionSuspendedCounter - 1;
|
||
this.executionSuspendedCounter = Math.max(nextValue, 0);
|
||
if (!this.isExecutionSuspended() && nextValue === this.executionSuspendedCounter || forceFlushChanges) {
|
||
this.columnIndexMapper.resumeOperations();
|
||
this.rowIndexMapper.resumeOperations();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* The method aggregates multi-line API calls into a callback and postpones the
|
||
* table execution process. After the execution of the operations, the internal table
|
||
* cache is recalculated once. As a result, it improves the performance of wrapped
|
||
* operations. Without batching, a similar case could trigger multiple table cache rebuilds.
|
||
*
|
||
* @memberof Core#
|
||
* @function batchExecution
|
||
* @param {Function} wrappedOperations Batched operations wrapped in a function.
|
||
* @param {boolean} [forceFlushChanges=false] If `true`, the table internal data cache
|
||
* is recalculated after the execution of the batched operations. For nested calls,
|
||
* it can be a desire to recalculate the table after each batch.
|
||
* @returns {*} Returns result from the wrappedOperations callback.
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.batchExecution(() => {
|
||
* const filters = hot.getPlugin('filters');
|
||
*
|
||
* filters.addCondition(2, 'contains', ['3']);
|
||
* filters.filter();
|
||
* hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
|
||
* // The table cache will be recalculated once after executing the callback
|
||
* });
|
||
* ```
|
||
*/
|
||
this.batchExecution = function (wrappedOperations) {
|
||
let forceFlushChanges = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||
this.suspendExecution();
|
||
const result = wrappedOperations();
|
||
this.resumeExecution(forceFlushChanges);
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* It batches the rendering process and index recalculations. The method aggregates
|
||
* multi-line API calls into a callback and postpones the table rendering process
|
||
* as well aggregates the table logic changes such as index changes into one call
|
||
* after which the cache is updated. After the execution of the operations, the
|
||
* table is rendered, and the cache is updated once. As a result, it improves the
|
||
* performance of wrapped operations.
|
||
*
|
||
* @memberof Core#
|
||
* @function batch
|
||
* @param {Function} wrappedOperations Batched operations wrapped in a function.
|
||
* @returns {*} Returns result from the wrappedOperations callback.
|
||
* @since 8.3.0
|
||
* @example
|
||
* ```js
|
||
* hot.batch(() => {
|
||
* hot.alter('insert_row_above', 5, 45);
|
||
* hot.alter('insert_col_start', 10, 40);
|
||
* hot.setDataAtCell(1, 1, 'x');
|
||
* hot.setDataAtCell(2, 2, 'c');
|
||
* hot.setDataAtCell(3, 3, 'v');
|
||
* hot.setDataAtCell(4, 4, 'b');
|
||
* hot.setDataAtCell(5, 5, 'n');
|
||
* hot.selectCell(0, 0);
|
||
*
|
||
* const filters = hot.getPlugin('filters');
|
||
*
|
||
* filters.addCondition(2, 'contains', ['3']);
|
||
* filters.filter();
|
||
* hot.getPlugin('columnSorting').sort({ column: 1, sortOrder: 'desc' });
|
||
* // The table will be re-rendered and cache will be recalculated once after executing the callback
|
||
* });
|
||
* ```
|
||
*/
|
||
this.batch = function (wrappedOperations) {
|
||
this.suspendRender();
|
||
this.suspendExecution();
|
||
const result = wrappedOperations();
|
||
this.resumeExecution();
|
||
this.resumeRender();
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Updates dimensions of the table. The method compares previous dimensions with the current ones and updates accordingly.
|
||
*
|
||
* @memberof Core#
|
||
* @function refreshDimensions
|
||
* @fires Hooks#beforeRefreshDimensions
|
||
* @fires Hooks#afterRefreshDimensions
|
||
*/
|
||
this.refreshDimensions = function () {
|
||
if (!instance.view) {
|
||
return;
|
||
}
|
||
const {
|
||
width: lastWidth,
|
||
height: lastHeight
|
||
} = instance.view.getLastSize();
|
||
const {
|
||
width,
|
||
height
|
||
} = instance.rootElement.getBoundingClientRect();
|
||
const isSizeChanged = width !== lastWidth || height !== lastHeight;
|
||
const isResizeBlocked = instance.runHooks('beforeRefreshDimensions', {
|
||
width: lastWidth,
|
||
height: lastHeight
|
||
}, {
|
||
width,
|
||
height
|
||
}, isSizeChanged) === false;
|
||
if (isResizeBlocked) {
|
||
return;
|
||
}
|
||
if (isSizeChanged || instance.view._wt.wtOverlays.scrollableElement === instance.rootWindow) {
|
||
instance.view.setLastSize(width, height);
|
||
instance.render();
|
||
}
|
||
instance.runHooks('afterRefreshDimensions', {
|
||
width: lastWidth,
|
||
height: lastHeight
|
||
}, {
|
||
width,
|
||
height
|
||
}, isSizeChanged);
|
||
};
|
||
|
||
/**
|
||
* The `updateData()` method replaces Handsontable's [`data`](@/api/options.md#data) with a new dataset.
|
||
*
|
||
* The `updateData()` method:
|
||
* - Keeps cells' states (e.g. cells' [formatting](@/guides/cell-features/formatting-cells.md) and cells' [`readOnly`](@/api/options.md#readonly) states)
|
||
* - Keeps rows' states (e.g. row order)
|
||
* - Keeps columns' states (e.g. column order)
|
||
*
|
||
* To replace Handsontable's [`data`](@/api/options.md#data) and reset states, use the [`loadData()`](#loaddata) method.
|
||
*
|
||
* Read more:
|
||
* - [Binding to data](@/guides/getting-started/binding-to-data.md)
|
||
* - [Saving data](@/guides/getting-started/saving-data.md)
|
||
*
|
||
* @memberof Core#
|
||
* @function updateData
|
||
* @since 11.1.0
|
||
* @param {Array} data An [array of arrays](@/guides/getting-started/binding-to-data.md#array-of-arrays), or an [array of objects](@/guides/getting-started/binding-to-data.md#array-of-objects), that contains Handsontable's data
|
||
* @param {string} [source] The source of the `updateData()` call
|
||
* @fires Hooks#beforeUpdateData
|
||
* @fires Hooks#afterUpdateData
|
||
* @fires Hooks#afterChange
|
||
*/
|
||
this.updateData = function (data, source) {
|
||
replaceData(data, newDataMap => {
|
||
datamap = newDataMap;
|
||
}, newDataMap => {
|
||
datamap = newDataMap;
|
||
instance.columnIndexMapper.fitToLength(this.getInitialColumnCount());
|
||
instance.rowIndexMapper.fitToLength(this.countSourceRows());
|
||
grid.adjustRowsAndCols();
|
||
}, {
|
||
hotInstance: instance,
|
||
dataMap: datamap,
|
||
dataSource,
|
||
internalSource: 'updateData',
|
||
source,
|
||
metaManager,
|
||
firstRun
|
||
});
|
||
};
|
||
|
||
/**
|
||
* The `loadData()` method replaces Handsontable's [`data`](@/api/options.md#data) with a new dataset.
|
||
*
|
||
* Additionally, the `loadData()` method:
|
||
* - Resets cells' states (e.g. cells' [formatting](@/guides/cell-features/formatting-cells.md) and cells' [`readOnly`](@/api/options.md#readonly) states)
|
||
* - Resets rows' states (e.g. row order)
|
||
* - Resets columns' states (e.g. column order)
|
||
*
|
||
* To replace Handsontable's [`data`](@/api/options.md#data) without resetting states, use the [`updateData()`](#updatedata) method.
|
||
*
|
||
* Read more:
|
||
* - [Binding to data](@/guides/getting-started/binding-to-data.md)
|
||
* - [Saving data](@/guides/getting-started/saving-data.md)
|
||
*
|
||
* @memberof Core#
|
||
* @function loadData
|
||
* @param {Array} data An [array of arrays](@/guides/getting-started/binding-to-data.md#array-of-arrays), or an [array of objects](@/guides/getting-started/binding-to-data.md#array-of-objects), that contains Handsontable's data
|
||
* @param {string} [source] The source of the `loadData()` call
|
||
* @fires Hooks#beforeLoadData
|
||
* @fires Hooks#afterLoadData
|
||
* @fires Hooks#afterChange
|
||
*/
|
||
this.loadData = function (data, source) {
|
||
replaceData(data, newDataMap => {
|
||
datamap = newDataMap;
|
||
}, () => {
|
||
metaManager.clearCellsCache();
|
||
instance.initIndexMappers();
|
||
grid.adjustRowsAndCols();
|
||
if (firstRun) {
|
||
firstRun = [null, 'loadData'];
|
||
}
|
||
}, {
|
||
hotInstance: instance,
|
||
dataMap: datamap,
|
||
dataSource,
|
||
internalSource: 'loadData',
|
||
source,
|
||
metaManager,
|
||
firstRun
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Gets the initial column count, calculated based on the `columns` setting.
|
||
*
|
||
* @private
|
||
* @returns {number} The calculated number of columns.
|
||
*/
|
||
this.getInitialColumnCount = function () {
|
||
const columnsSettings = tableMeta.columns;
|
||
let finalNrOfColumns = 0;
|
||
|
||
// We will check number of columns when the `columns` property was defined as an array. Columns option may
|
||
// narrow down or expand displayed dataset in that case.
|
||
if (Array.isArray(columnsSettings)) {
|
||
finalNrOfColumns = columnsSettings.length;
|
||
} else if (isFunction(columnsSettings)) {
|
||
if (instance.dataType === 'array') {
|
||
const nrOfSourceColumns = this.countSourceCols();
|
||
for (let columnIndex = 0; columnIndex < nrOfSourceColumns; columnIndex += 1) {
|
||
if (columnsSettings(columnIndex)) {
|
||
finalNrOfColumns += 1;
|
||
}
|
||
}
|
||
|
||
// Extended dataset by the `columns` property? Moved code right from the refactored `countCols` method.
|
||
} else if (instance.dataType === 'object' || instance.dataType === 'function') {
|
||
finalNrOfColumns = datamap.colToPropCache.length;
|
||
}
|
||
|
||
// In some cases we need to check columns length from the schema, i.e. `data` may be empty.
|
||
} else if (isDefined(tableMeta.dataSchema)) {
|
||
const schema = datamap.getSchema();
|
||
|
||
// Schema may be defined as an array of objects. Each object will define column.
|
||
finalNrOfColumns = Array.isArray(schema) ? schema.length : deepObjectSize(schema);
|
||
} else {
|
||
// We init index mappers by length of source data to provide indexes also for skipped indexes.
|
||
finalNrOfColumns = this.countSourceCols();
|
||
}
|
||
return finalNrOfColumns;
|
||
};
|
||
|
||
/**
|
||
* Init index mapper which manage indexes assigned to the data.
|
||
*
|
||
* @private
|
||
*/
|
||
this.initIndexMappers = function () {
|
||
this.columnIndexMapper.initToLength(this.getInitialColumnCount());
|
||
this.rowIndexMapper.initToLength(this.countSourceRows());
|
||
};
|
||
|
||
/**
|
||
* Returns the current data object (the same one that was passed by `data` configuration option or `loadData` method,
|
||
* unless some modifications have been applied (i.e. Sequence of rows/columns was changed, some row/column was skipped).
|
||
* If that's the case - use the {@link Core#getSourceData} method.).
|
||
*
|
||
* Optionally you can provide cell range by defining `row`, `column`, `row2`, `column2` to get only a fragment of table data.
|
||
*
|
||
* @memberof Core#
|
||
* @function getData
|
||
* @param {number} [row] From visual row index.
|
||
* @param {number} [column] From visual column index.
|
||
* @param {number} [row2] To visual row index.
|
||
* @param {number} [column2] To visual column index.
|
||
* @returns {Array[]} Array with the data.
|
||
* @example
|
||
* ```js
|
||
* // Get all data (in order how it is rendered in the table).
|
||
* hot.getData();
|
||
* // Get data fragment (from top-left 0, 0 to bottom-right 3, 3).
|
||
* hot.getData(3, 3);
|
||
* // Get data fragment (from top-left 2, 1 to bottom-right 3, 3).
|
||
* hot.getData(2, 1, 3, 3);
|
||
* ```
|
||
*/
|
||
this.getData = function (row, column, row2, column2) {
|
||
if (isUndefined(row)) {
|
||
return datamap.getAll();
|
||
}
|
||
return datamap.getRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2), datamap.DESTINATION_RENDERER);
|
||
};
|
||
|
||
/**
|
||
* Returns a string value of the selected range. Each column is separated by tab, each row is separated by a new
|
||
* line character.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCopyableText
|
||
* @param {number} startRow From visual row index.
|
||
* @param {number} startCol From visual column index.
|
||
* @param {number} endRow To visual row index.
|
||
* @param {number} endCol To visual column index.
|
||
* @returns {string}
|
||
*/
|
||
this.getCopyableText = function (startRow, startCol, endRow, endCol) {
|
||
return datamap.getCopyableText(instance._createCellCoords(startRow, startCol), instance._createCellCoords(endRow, endCol));
|
||
};
|
||
|
||
/**
|
||
* Returns the data's copyable value at specified `row` and `column` index.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCopyableData
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @returns {string}
|
||
*/
|
||
this.getCopyableData = function (row, column) {
|
||
return datamap.getCopyable(row, datamap.colToProp(column));
|
||
};
|
||
|
||
/**
|
||
* Returns schema provided by constructor settings. If it doesn't exist then it returns the schema based on the data
|
||
* structure in the first row.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSchema
|
||
* @returns {object} Schema object.
|
||
*/
|
||
this.getSchema = function () {
|
||
return datamap.getSchema();
|
||
};
|
||
|
||
/**
|
||
* Use it if you need to change configuration after initialization. The `settings` argument is an object containing the changed
|
||
* settings, declared the same way as in the initial settings object.
|
||
*
|
||
* __Note__, that although the `updateSettings` method doesn't overwrite the previously declared settings, it might reset
|
||
* the settings made post-initialization. (for example - ignore changes made using the columnResize feature).
|
||
*
|
||
* Since 8.0.0 passing `columns` or `data` inside `settings` objects will result in resetting states corresponding to rows and columns
|
||
* (for example, row/column sequence, column width, row height, frozen columns etc.).
|
||
*
|
||
* Since 12.0.0 passing `data` inside `settings` objects no longer results in resetting states corresponding to rows and columns
|
||
* (for example, row/column sequence, column width, row height, frozen columns etc.).
|
||
*
|
||
* @memberof Core#
|
||
* @function updateSettings
|
||
* @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.
|
||
* @param {boolean} [init=false] Internally used for in initialization mode.
|
||
* @example
|
||
* ```js
|
||
* hot.updateSettings({
|
||
* contextMenu: true,
|
||
* colHeaders: true,
|
||
* fixedRowsTop: 2
|
||
* });
|
||
* ```
|
||
* @fires Hooks#afterCellMetaReset
|
||
* @fires Hooks#afterUpdateSettings
|
||
*/
|
||
this.updateSettings = function (settings) {
|
||
let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||
const dataUpdateFunction = (firstRun ? instance.loadData : instance.updateData).bind(this);
|
||
let columnsAsFunc = false;
|
||
let i;
|
||
let j;
|
||
if (isDefined(settings.rows)) {
|
||
throw new Error('The "rows" setting is no longer supported. Do you mean startRows, minRows or maxRows?');
|
||
}
|
||
if (isDefined(settings.cols)) {
|
||
throw new Error('The "cols" setting is no longer supported. Do you mean startCols, minCols or maxCols?');
|
||
}
|
||
if (isDefined(settings.ganttChart)) {
|
||
throw new Error('Since 8.0.0 the "ganttChart" setting is no longer supported.');
|
||
}
|
||
|
||
// eslint-disable-next-line no-restricted-syntax
|
||
for (i in settings) {
|
||
if (i === 'data') {
|
||
// Do nothing. loadData will be triggered later
|
||
} else if (i === 'language') {
|
||
setLanguage(settings.language);
|
||
} else if (i === 'className') {
|
||
setClassName('className', settings.className);
|
||
} else if (i === 'tableClassName' && instance.table) {
|
||
setClassName('tableClassName', settings.tableClassName);
|
||
instance.view._wt.wtOverlays.syncOverlayTableClassNames();
|
||
} else if (Hooks.getSingleton().isRegistered(i) || Hooks.getSingleton().isDeprecated(i)) {
|
||
if (isFunction(settings[i]) || Array.isArray(settings[i])) {
|
||
settings[i].initialHook = true;
|
||
instance.addHook(i, settings[i]);
|
||
}
|
||
} else if (!init && hasOwnProperty(settings, i)) {
|
||
// Update settings
|
||
globalMeta[i] = settings[i];
|
||
}
|
||
}
|
||
|
||
// Load data or create data map
|
||
if (settings.data === void 0 && tableMeta.data === void 0) {
|
||
dataUpdateFunction(null, 'updateSettings'); // data source created just now
|
||
} else if (settings.data !== void 0) {
|
||
dataUpdateFunction(settings.data, 'updateSettings'); // data source given as option
|
||
} else if (settings.columns !== void 0) {
|
||
datamap.createMap();
|
||
|
||
// The `column` property has changed - dataset may be expanded or narrowed down. The `loadData` do the same.
|
||
instance.initIndexMappers();
|
||
}
|
||
const clen = instance.countCols();
|
||
const columnSetting = tableMeta.columns;
|
||
|
||
// Init columns constructors configuration
|
||
if (columnSetting && isFunction(columnSetting)) {
|
||
columnsAsFunc = true;
|
||
}
|
||
|
||
// Clear cell meta cache
|
||
if (settings.cell !== void 0 || settings.cells !== void 0 || settings.columns !== void 0) {
|
||
metaManager.clearCache();
|
||
}
|
||
if (clen > 0) {
|
||
for (i = 0, j = 0; i < clen; i++) {
|
||
// Use settings provided by user
|
||
if (columnSetting) {
|
||
const column = columnsAsFunc ? columnSetting(i) : columnSetting[j];
|
||
if (column) {
|
||
metaManager.updateColumnMeta(j, column);
|
||
}
|
||
}
|
||
j += 1;
|
||
}
|
||
}
|
||
if (isDefined(settings.cell)) {
|
||
objectEach(settings.cell, cell => {
|
||
instance.setCellMetaObject(cell.row, cell.col, cell);
|
||
});
|
||
}
|
||
instance.runHooks('afterCellMetaReset');
|
||
let currentHeight = instance.rootElement.style.height;
|
||
if (currentHeight !== '') {
|
||
currentHeight = parseInt(instance.rootElement.style.height, 10);
|
||
}
|
||
let height = settings.height;
|
||
if (isFunction(height)) {
|
||
height = height();
|
||
}
|
||
if (init) {
|
||
const initialStyle = instance.rootElement.getAttribute('style');
|
||
if (initialStyle) {
|
||
instance.rootElement.setAttribute('data-initialstyle', instance.rootElement.getAttribute('style'));
|
||
}
|
||
}
|
||
if (height === null) {
|
||
const initialStyle = instance.rootElement.getAttribute('data-initialstyle');
|
||
if (initialStyle && (initialStyle.indexOf('height') > -1 || initialStyle.indexOf('overflow') > -1)) {
|
||
instance.rootElement.setAttribute('style', initialStyle);
|
||
} else {
|
||
instance.rootElement.style.height = '';
|
||
instance.rootElement.style.overflow = '';
|
||
}
|
||
} else if (height !== void 0) {
|
||
instance.rootElement.style.height = isNaN(height) ? `${height}` : `${height}px`;
|
||
instance.rootElement.style.overflow = 'hidden';
|
||
}
|
||
if (typeof settings.width !== 'undefined') {
|
||
let width = settings.width;
|
||
if (isFunction(width)) {
|
||
width = width();
|
||
}
|
||
instance.rootElement.style.width = isNaN(width) ? `${width}` : `${width}px`;
|
||
}
|
||
if (!init) {
|
||
if (instance.view) {
|
||
instance.view._wt.wtViewport.resetHasOversizedColumnHeadersMarked();
|
||
instance.view._wt.exportSettingsAsClassNames();
|
||
}
|
||
instance.runHooks('afterUpdateSettings', settings);
|
||
}
|
||
grid.adjustRowsAndCols();
|
||
if (instance.view && !firstRun) {
|
||
instance.forceFullRender = true; // used when data was changed
|
||
editorManager.lockEditor();
|
||
instance._refreshBorders(null);
|
||
instance.view._wt.wtOverlays.adjustElementsSize();
|
||
editorManager.unlockEditor();
|
||
}
|
||
if (!init && instance.view && (currentHeight === '' || height === '' || height === void 0) && currentHeight !== height) {
|
||
instance.view._wt.wtOverlays.updateMainScrollableElements();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gets the value of the currently focused cell.
|
||
*
|
||
* For column headers and row headers, returns `null`.
|
||
*
|
||
* @memberof Core#
|
||
* @function getValue
|
||
* @returns {*} The value of the focused cell.
|
||
*/
|
||
this.getValue = function () {
|
||
const sel = instance.getSelectedLast();
|
||
if (tableMeta.getValue) {
|
||
if (isFunction(tableMeta.getValue)) {
|
||
return tableMeta.getValue.call(instance);
|
||
} else if (sel) {
|
||
return instance.getData()[sel[0][0]][tableMeta.getValue];
|
||
}
|
||
} else if (sel) {
|
||
return instance.getDataAtCell(sel[0], sel[1]);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns the object settings.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSettings
|
||
* @returns {TableMeta} Object containing the current table settings.
|
||
*/
|
||
this.getSettings = function () {
|
||
return tableMeta;
|
||
};
|
||
|
||
/**
|
||
* Clears the data from the table (the table settings remain intact).
|
||
*
|
||
* @memberof Core#
|
||
* @function clear
|
||
*/
|
||
this.clear = function () {
|
||
this.selectAll();
|
||
this.emptySelectedCells();
|
||
};
|
||
|
||
/**
|
||
* The `alter()` method lets you alter the grid's structure
|
||
* by adding or removing rows and columns at specified positions.
|
||
*
|
||
* ::: tip
|
||
* The `alter()` method works only when your [`data`](@/api/options.md#data)
|
||
* is an [array of arrays](@/guides/getting-started/binding-to-data.md#array-of-arrays).
|
||
* :::
|
||
*
|
||
* ```js
|
||
* // above row 10 (by visual index), insert 1 new row
|
||
* hot.alter('insert_row_above', 10);
|
||
* ```
|
||
*
|
||
* | Action | With `index` | Without `index` |
|
||
* | -------------------- | ------------ | --------------- |
|
||
* | `'insert_row_above'` | Inserts rows above the `index` row. | Inserts rows above the first row. |
|
||
* | `'insert_row_below'` | Inserts rows below the `index` row. | Inserts rows below the last row. |
|
||
* | `'remove_row'` | Removes rows, starting from the `index` row. | Removes rows, starting from the last row. |
|
||
* | `'insert_col_start'` | Inserts columns before the `index` column. | Inserts columns before the first column. |
|
||
* | `'insert_col_end'` | Inserts columns after the `index` column. | Inserts columns after the last column. |
|
||
* | `'remove_col'` | Removes columns, starting from the `index` column. | Removes columns, starting from the last column. |
|
||
*
|
||
* Additional information about `'insert_col_start'` and `'insert_col_end'`:
|
||
* - Their behavior depends on your [`layoutDirection`](@/api/options.md#layoutdirection).
|
||
* - If the provided `index` is higher than the actual number of columns, Handsontable doesn't generate
|
||
* the columns missing in between. Instead, the new columns are inserted next to the last column.
|
||
*
|
||
* @memberof Core#
|
||
* @function alter
|
||
* @param {string} action Available operations:
|
||
* <ul>
|
||
* <li> `'insert_row_above'` </li>
|
||
* <li> `'insert_row_below'` </li>
|
||
* <li> `'remove_row'` </li> </li>
|
||
* <li> `'insert_col_start'` </li>
|
||
* <li> `'insert_col_end'` </li>
|
||
* <li> `'remove_col'` </li>
|
||
* </ul>
|
||
* @param {number|number[]} [index] A visual index of the row/column before or after which the new row/column will be
|
||
* inserted or removed. Can also be an array of arrays, in format `[[index, amount],...]`.
|
||
* @param {number} [amount] The amount of rows or columns to be inserted or removed (default: `1`).
|
||
* @param {string} [source] Source indicator.
|
||
* @param {boolean} [keepEmptyRows] If set to `true`: prevents removing empty rows.
|
||
* @example
|
||
* ```js
|
||
* // above row 10 (by visual index), insert 1 new row
|
||
* hot.alter('insert_row_above', 10);
|
||
*
|
||
* // below row 10 (by visual index), insert 3 new rows
|
||
* hot.alter('insert_row_below', 10, 3);
|
||
*
|
||
* // in the LTR layout direction: to the left of column 10 (by visual index), insert 3 new columns
|
||
* // in the RTL layout direction: to the right of column 10 (by visual index), insert 3 new columns
|
||
* hot.alter('insert_col_start', 10, 3);
|
||
*
|
||
* // in the LTR layout direction: to the right of column 10 (by visual index), insert 1 new column
|
||
* // in the RTL layout direction: to the left of column 10 (by visual index), insert 1 new column
|
||
* hot.alter('insert_col_end', 10);
|
||
*
|
||
* // remove 2 rows, starting from row 10 (by visual index)
|
||
* hot.alter('remove_row', 10, 2);
|
||
*
|
||
* // remove 3 rows, starting from row 1 (by visual index)
|
||
* // remove 2 rows, starting from row 5 (by visual index)
|
||
* hot.alter('remove_row', [[1, 3], [5, 2]]);
|
||
* ```
|
||
*/
|
||
this.alter = function (action, index, amount, source, keepEmptyRows) {
|
||
grid.alter(action, index, amount, source, keepEmptyRows);
|
||
};
|
||
|
||
/**
|
||
* Returns a TD element for the given `row` and `column` arguments, if it is rendered on screen.
|
||
* Returns `null` if the TD is not rendered on screen (probably because that part of the table is not visible).
|
||
*
|
||
* @memberof Core#
|
||
* @function getCell
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @param {boolean} [topmost=false] If set to `true`, it returns the TD element from the topmost overlay. For example,
|
||
* if the wanted cell is in the range of fixed rows, it will return a TD element from the `top` overlay.
|
||
* @returns {HTMLTableCellElement|null} The cell's TD element.
|
||
*/
|
||
this.getCell = function (row, column) {
|
||
let topmost = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
||
let renderableColumnIndex = column; // Handling also column headers.
|
||
let renderableRowIndex = row; // Handling also row headers.
|
||
|
||
if (column >= 0) {
|
||
if (this.columnIndexMapper.isHidden(this.toPhysicalColumn(column))) {
|
||
return null;
|
||
}
|
||
renderableColumnIndex = this.columnIndexMapper.getRenderableFromVisualIndex(column);
|
||
}
|
||
if (row >= 0) {
|
||
if (this.rowIndexMapper.isHidden(this.toPhysicalRow(row))) {
|
||
return null;
|
||
}
|
||
renderableRowIndex = this.rowIndexMapper.getRenderableFromVisualIndex(row);
|
||
}
|
||
if (renderableRowIndex === null || renderableColumnIndex === null) {
|
||
return null;
|
||
}
|
||
return instance.view.getCellAtCoords(instance._createCellCoords(renderableRowIndex, renderableColumnIndex), topmost);
|
||
};
|
||
|
||
/**
|
||
* Returns the coordinates of the cell, provided as a HTML table cell element.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCoords
|
||
* @param {HTMLTableCellElement} element The HTML Element representing the cell.
|
||
* @returns {CellCoords|null} Visual coordinates object.
|
||
* @example
|
||
* ```js
|
||
* hot.getCoords(hot.getCell(1, 1));
|
||
* // it returns CellCoords object instance with props row: 1 and col: 1.
|
||
* ```
|
||
*/
|
||
this.getCoords = function (element) {
|
||
const renderableCoords = this.view._wt.wtTable.getCoords(element);
|
||
if (renderableCoords === null) {
|
||
return null;
|
||
}
|
||
const {
|
||
row: renderableRow,
|
||
col: renderableColumn
|
||
} = renderableCoords;
|
||
let visualRow = renderableRow;
|
||
let visualColumn = renderableColumn;
|
||
if (renderableRow >= 0) {
|
||
visualRow = this.rowIndexMapper.getVisualFromRenderableIndex(renderableRow);
|
||
}
|
||
if (renderableColumn >= 0) {
|
||
visualColumn = this.columnIndexMapper.getVisualFromRenderableIndex(renderableColumn);
|
||
}
|
||
return instance._createCellCoords(visualRow, visualColumn);
|
||
};
|
||
|
||
/**
|
||
* Returns the property name that corresponds with the given column index.
|
||
* If the data source is an array of arrays, it returns the columns index.
|
||
*
|
||
* @memberof Core#
|
||
* @function colToProp
|
||
* @param {number} column Visual column index.
|
||
* @returns {string|number} Column property or physical column index.
|
||
*/
|
||
this.colToProp = function (column) {
|
||
return datamap.colToProp(column);
|
||
};
|
||
|
||
/**
|
||
* Returns column index that corresponds with the given property.
|
||
*
|
||
* @memberof Core#
|
||
* @function propToCol
|
||
* @param {string|number} prop Property name or physical column index.
|
||
* @returns {number} Visual column index.
|
||
*/
|
||
this.propToCol = function (prop) {
|
||
return datamap.propToCol(prop);
|
||
};
|
||
|
||
/**
|
||
* Translate physical row index into visual.
|
||
*
|
||
* This method is useful when you want to retrieve visual row index which can be reordered, moved or trimmed
|
||
* based on a physical index.
|
||
*
|
||
* @memberof Core#
|
||
* @function toVisualRow
|
||
* @param {number} row Physical row index.
|
||
* @returns {number} Returns visual row index.
|
||
*/
|
||
this.toVisualRow = row => this.rowIndexMapper.getVisualFromPhysicalIndex(row);
|
||
|
||
/**
|
||
* Translate physical column index into visual.
|
||
*
|
||
* This method is useful when you want to retrieve visual column index which can be reordered, moved or trimmed
|
||
* based on a physical index.
|
||
*
|
||
* @memberof Core#
|
||
* @function toVisualColumn
|
||
* @param {number} column Physical column index.
|
||
* @returns {number} Returns visual column index.
|
||
*/
|
||
this.toVisualColumn = column => this.columnIndexMapper.getVisualFromPhysicalIndex(column);
|
||
|
||
/**
|
||
* Translate visual row index into physical.
|
||
*
|
||
* This method is useful when you want to retrieve physical row index based on a visual index which can be
|
||
* reordered, moved or trimmed.
|
||
*
|
||
* @memberof Core#
|
||
* @function toPhysicalRow
|
||
* @param {number} row Visual row index.
|
||
* @returns {number} Returns physical row index.
|
||
*/
|
||
this.toPhysicalRow = row => this.rowIndexMapper.getPhysicalFromVisualIndex(row);
|
||
|
||
/**
|
||
* Translate visual column index into physical.
|
||
*
|
||
* This method is useful when you want to retrieve physical column index based on a visual index which can be
|
||
* reordered, moved or trimmed.
|
||
*
|
||
* @memberof Core#
|
||
* @function toPhysicalColumn
|
||
* @param {number} column Visual column index.
|
||
* @returns {number} Returns physical column index.
|
||
*/
|
||
this.toPhysicalColumn = column => this.columnIndexMapper.getPhysicalFromVisualIndex(column);
|
||
|
||
/**
|
||
* @description
|
||
* Returns the cell value at `row`, `column`.
|
||
*
|
||
* __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataAtCell
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @returns {*} Data at cell.
|
||
*/
|
||
this.getDataAtCell = function (row, column) {
|
||
return datamap.get(row, datamap.colToProp(column));
|
||
};
|
||
|
||
/**
|
||
* Returns value at visual `row` and `prop` indexes.
|
||
*
|
||
* __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataAtRowProp
|
||
* @param {number} row Visual row index.
|
||
* @param {string} prop Property name.
|
||
* @returns {*} Cell value.
|
||
*/
|
||
this.getDataAtRowProp = function (row, prop) {
|
||
return datamap.get(row, prop);
|
||
};
|
||
|
||
/**
|
||
* @description
|
||
* Returns array of column values from the data source.
|
||
*
|
||
* __Note__: If columns were reordered or sorted, the currently visible order will be used.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataAtCol
|
||
* @param {number} column Visual column index.
|
||
* @returns {Array} Array of cell values.
|
||
*/
|
||
this.getDataAtCol = function (column) {
|
||
const columnData = [];
|
||
const dataByRows = datamap.getRange(instance._createCellCoords(0, column), instance._createCellCoords(tableMeta.data.length - 1, column), datamap.DESTINATION_RENDERER);
|
||
for (let i = 0; i < dataByRows.length; i += 1) {
|
||
for (let j = 0; j < dataByRows[i].length; j += 1) {
|
||
columnData.push(dataByRows[i][j]);
|
||
}
|
||
}
|
||
return columnData;
|
||
};
|
||
|
||
/**
|
||
* Given the object property name (e.g. `'first.name'` or `'0'`), returns an array of column's values from the table data.
|
||
* You can also provide a column index as the first argument.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataAtProp
|
||
* @param {string|number} prop Property name or physical column index.
|
||
* @returns {Array} Array of cell values.
|
||
*/
|
||
// TODO: Getting data from `datamap` should work on visual indexes.
|
||
this.getDataAtProp = function (prop) {
|
||
const columnData = [];
|
||
const dataByRows = datamap.getRange(instance._createCellCoords(0, datamap.propToCol(prop)), instance._createCellCoords(tableMeta.data.length - 1, datamap.propToCol(prop)), datamap.DESTINATION_RENDERER);
|
||
for (let i = 0; i < dataByRows.length; i += 1) {
|
||
for (let j = 0; j < dataByRows[i].length; j += 1) {
|
||
columnData.push(dataByRows[i][j]);
|
||
}
|
||
}
|
||
return columnData;
|
||
};
|
||
|
||
/**
|
||
* Returns a clone of the source data object.
|
||
* Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
|
||
* fragment of the table data.
|
||
*
|
||
* __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
|
||
* sorted or trimmed only physical indexes are correct.
|
||
*
|
||
* __Note__: This method may return incorrect values for cells that contain
|
||
* [formulas](@/guides/formulas/formula-calculation.md). This is because `getSourceData()`
|
||
* operates on source data ([physical indexes](@/api/indexMapper.md)),
|
||
* whereas formulas operate on visual data (visual indexes).
|
||
*
|
||
* @memberof Core#
|
||
* @function getSourceData
|
||
* @param {number} [row] From physical row index.
|
||
* @param {number} [column] From physical column index (or visual index, if data type is an array of objects).
|
||
* @param {number} [row2] To physical row index.
|
||
* @param {number} [column2] To physical column index (or visual index, if data type is an array of objects).
|
||
* @returns {Array[]|object[]} The table data.
|
||
*/
|
||
this.getSourceData = function (row, column, row2, column2) {
|
||
let data;
|
||
if (row === void 0) {
|
||
data = dataSource.getData();
|
||
} else {
|
||
data = dataSource.getByRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2));
|
||
}
|
||
return data;
|
||
};
|
||
|
||
/**
|
||
* Returns the source data object as an arrays of arrays format even when source data was provided in another format.
|
||
* Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
|
||
* fragment of the table data.
|
||
*
|
||
* __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
|
||
* sorted or trimmed only physical indexes are correct.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSourceDataArray
|
||
* @param {number} [row] From physical row index.
|
||
* @param {number} [column] From physical column index (or visual index, if data type is an array of objects).
|
||
* @param {number} [row2] To physical row index.
|
||
* @param {number} [column2] To physical column index (or visual index, if data type is an array of objects).
|
||
* @returns {Array} An array of arrays.
|
||
*/
|
||
this.getSourceDataArray = function (row, column, row2, column2) {
|
||
let data;
|
||
if (row === void 0) {
|
||
data = dataSource.getData(true);
|
||
} else {
|
||
data = dataSource.getByRange(instance._createCellCoords(row, column), instance._createCellCoords(row2, column2), true);
|
||
}
|
||
return data;
|
||
};
|
||
|
||
/**
|
||
* Returns an array of column values from the data source.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSourceDataAtCol
|
||
* @param {number} column Visual column index.
|
||
* @returns {Array} Array of the column's cell values.
|
||
*/
|
||
// TODO: Getting data from `sourceData` should work always on physical indexes.
|
||
this.getSourceDataAtCol = function (column) {
|
||
return dataSource.getAtColumn(column);
|
||
};
|
||
|
||
/* eslint-disable jsdoc/require-param */
|
||
/**
|
||
* Set the provided value in the source data set at the provided coordinates.
|
||
*
|
||
* @memberof Core#
|
||
* @function setSourceDataAtCell
|
||
* @param {number|Array} row Physical row index or array of changes in format `[[row, prop, value], ...]`.
|
||
* @param {number|string} column Physical column index / prop name.
|
||
* @param {*} value The value to be set at the provided coordinates.
|
||
* @param {string} [source] Source of the change as a string.
|
||
*/
|
||
/* eslint-enable jsdoc/require-param */
|
||
this.setSourceDataAtCell = function (row, column, value, source) {
|
||
const input = setDataInputToArray(row, column, value);
|
||
const isThereAnySetSourceListener = this.hasHook('afterSetSourceDataAtCell');
|
||
const changesForHook = [];
|
||
if (isThereAnySetSourceListener) {
|
||
arrayEach(input, _ref8 => {
|
||
let [changeRow, changeProp, changeValue] = _ref8;
|
||
changesForHook.push([changeRow, changeProp, dataSource.getAtCell(changeRow, changeProp),
|
||
// The previous value.
|
||
changeValue]);
|
||
});
|
||
}
|
||
arrayEach(input, _ref9 => {
|
||
let [changeRow, changeProp, changeValue] = _ref9;
|
||
dataSource.setAtCell(changeRow, changeProp, changeValue);
|
||
});
|
||
if (isThereAnySetSourceListener) {
|
||
this.runHooks('afterSetSourceDataAtCell', changesForHook, source);
|
||
}
|
||
this.render();
|
||
const activeEditor = instance.getActiveEditor();
|
||
if (activeEditor && isDefined(activeEditor.refreshValue)) {
|
||
activeEditor.refreshValue();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns a single row of the data (array or object, depending on what data format you use).
|
||
*
|
||
* __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
|
||
* sorted or trimmed only physical indexes are correct.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSourceDataAtRow
|
||
* @param {number} row Physical row index.
|
||
* @returns {Array|object} Single row of data.
|
||
*/
|
||
this.getSourceDataAtRow = function (row) {
|
||
return dataSource.getAtRow(row);
|
||
};
|
||
|
||
/**
|
||
* Returns a single value from the data source.
|
||
*
|
||
* @memberof Core#
|
||
* @function getSourceDataAtCell
|
||
* @param {number} row Physical row index.
|
||
* @param {number} column Visual column index.
|
||
* @returns {*} Cell data.
|
||
*/
|
||
// TODO: Getting data from `sourceData` should work always on physical indexes.
|
||
this.getSourceDataAtCell = function (row, column) {
|
||
return dataSource.getAtCell(row, column);
|
||
};
|
||
|
||
/**
|
||
* @description
|
||
* Returns a single row of the data.
|
||
*
|
||
* __Note__: If rows were reordered, sorted or trimmed, the currently visible order will be used.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataAtRow
|
||
* @param {number} row Visual row index.
|
||
* @returns {Array} Array of row's cell data.
|
||
*/
|
||
this.getDataAtRow = function (row) {
|
||
const data = datamap.getRange(instance._createCellCoords(row, 0), instance._createCellCoords(row, this.countCols() - 1), datamap.DESTINATION_RENDERER);
|
||
return data[0] || [];
|
||
};
|
||
|
||
/**
|
||
* @description
|
||
* Returns a data type defined in the Handsontable settings under the `type` key ({@link Options#type}).
|
||
* If there are cells with different types in the selected range, it returns `'mixed'`.
|
||
*
|
||
* __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
|
||
*
|
||
* @memberof Core#
|
||
* @function getDataType
|
||
* @param {number} rowFrom From visual row index.
|
||
* @param {number} columnFrom From visual column index.
|
||
* @param {number} rowTo To visual row index.
|
||
* @param {number} columnTo To visual column index.
|
||
* @returns {string} Cell type (e.q: `'mixed'`, `'text'`, `'numeric'`, `'autocomplete'`).
|
||
*/
|
||
this.getDataType = function (rowFrom, columnFrom, rowTo, columnTo) {
|
||
const coords = rowFrom === void 0 ? [0, 0, this.countRows(), this.countCols()] : [rowFrom, columnFrom, rowTo, columnTo];
|
||
const [rowStart, columnStart] = coords;
|
||
let [,, rowEnd, columnEnd] = coords;
|
||
let previousType = null;
|
||
let currentType = null;
|
||
if (rowEnd === void 0) {
|
||
rowEnd = rowStart;
|
||
}
|
||
if (columnEnd === void 0) {
|
||
columnEnd = columnStart;
|
||
}
|
||
let type = 'mixed';
|
||
rangeEach(Math.max(Math.min(rowStart, rowEnd), 0), Math.max(rowStart, rowEnd), row => {
|
||
let isTypeEqual = true;
|
||
rangeEach(Math.max(Math.min(columnStart, columnEnd), 0), Math.max(columnStart, columnEnd), column => {
|
||
const cellType = this.getCellMeta(row, column);
|
||
currentType = cellType.type;
|
||
if (previousType) {
|
||
isTypeEqual = previousType === currentType;
|
||
} else {
|
||
previousType = currentType;
|
||
}
|
||
return isTypeEqual;
|
||
});
|
||
type = isTypeEqual ? currentType : 'mixed';
|
||
return isTypeEqual;
|
||
});
|
||
return type;
|
||
};
|
||
|
||
/**
|
||
* Remove a property defined by the `key` argument from the cell meta object for the provided `row` and `column` coordinates.
|
||
*
|
||
* @memberof Core#
|
||
* @function removeCellMeta
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @param {string} key Property name.
|
||
* @fires Hooks#beforeRemoveCellMeta
|
||
* @fires Hooks#afterRemoveCellMeta
|
||
*/
|
||
this.removeCellMeta = function (row, column, key) {
|
||
const [physicalRow, physicalColumn] = [this.toPhysicalRow(row), this.toPhysicalColumn(column)];
|
||
let cachedValue = metaManager.getCellMetaKeyValue(physicalRow, physicalColumn, key);
|
||
const hookResult = instance.runHooks('beforeRemoveCellMeta', row, column, key, cachedValue);
|
||
if (hookResult !== false) {
|
||
metaManager.removeCellMeta(physicalRow, physicalColumn, key);
|
||
instance.runHooks('afterRemoveCellMeta', row, column, key, cachedValue);
|
||
}
|
||
cachedValue = null;
|
||
};
|
||
|
||
/**
|
||
* Removes or adds one or more rows of the cell meta objects to the cell meta collections.
|
||
*
|
||
* @since 0.30.0
|
||
* @memberof Core#
|
||
* @function spliceCellsMeta
|
||
* @param {number} visualIndex A visual index that specifies at what position to add/remove items.
|
||
* @param {number} [deleteAmount=0] The number of items to be removed. If set to 0, no cell meta objects will be removed.
|
||
* @param {...object} [cellMetaRows] The new cell meta row objects to be added to the cell meta collection.
|
||
*/
|
||
this.spliceCellsMeta = function (visualIndex) {
|
||
let deleteAmount = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
||
for (var _len3 = arguments.length, cellMetaRows = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) {
|
||
cellMetaRows[_key3 - 2] = arguments[_key3];
|
||
}
|
||
if (cellMetaRows.length > 0 && !Array.isArray(cellMetaRows[0])) {
|
||
throw new Error('The 3rd argument (cellMetaRows) has to be passed as an array of cell meta objects array.');
|
||
}
|
||
if (deleteAmount > 0) {
|
||
metaManager.removeRow(this.toPhysicalRow(visualIndex), deleteAmount);
|
||
}
|
||
if (cellMetaRows.length > 0) {
|
||
arrayEach(cellMetaRows.reverse(), cellMetaRow => {
|
||
metaManager.createRow(this.toPhysicalRow(visualIndex));
|
||
arrayEach(cellMetaRow, (cellMeta, columnIndex) => this.setCellMetaObject(visualIndex, columnIndex, cellMeta));
|
||
});
|
||
}
|
||
instance.render();
|
||
};
|
||
|
||
/**
|
||
* Set cell meta data object defined by `prop` to the corresponding params `row` and `column`.
|
||
*
|
||
* @memberof Core#
|
||
* @function setCellMetaObject
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @param {object} prop Meta object.
|
||
*/
|
||
this.setCellMetaObject = function (row, column, prop) {
|
||
if (typeof prop === 'object') {
|
||
objectEach(prop, (value, key) => {
|
||
this.setCellMeta(row, column, key, value);
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sets a property defined by the `key` property to the meta object of a cell corresponding to params `row` and `column`.
|
||
*
|
||
* @memberof Core#
|
||
* @function setCellMeta
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @param {string} key Property name.
|
||
* @param {string} value Property value.
|
||
* @fires Hooks#beforeSetCellMeta
|
||
* @fires Hooks#afterSetCellMeta
|
||
*/
|
||
this.setCellMeta = function (row, column, key, value) {
|
||
const allowSetCellMeta = instance.runHooks('beforeSetCellMeta', row, column, key, value);
|
||
if (allowSetCellMeta === false) {
|
||
return;
|
||
}
|
||
let physicalRow = row;
|
||
let physicalColumn = column;
|
||
if (row < this.countRows()) {
|
||
physicalRow = this.toPhysicalRow(row);
|
||
}
|
||
if (column < this.countCols()) {
|
||
physicalColumn = this.toPhysicalColumn(column);
|
||
}
|
||
metaManager.setCellMeta(physicalRow, physicalColumn, key, value);
|
||
instance.runHooks('afterSetCellMeta', row, column, key, value);
|
||
};
|
||
|
||
/**
|
||
* Get all the cells meta settings at least once generated in the table (in order of cell initialization).
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellsMeta
|
||
* @returns {Array} Returns an array of ColumnSettings object instances.
|
||
*/
|
||
this.getCellsMeta = function () {
|
||
return metaManager.getCellsMeta();
|
||
};
|
||
|
||
/**
|
||
* Returns the cell properties object for the given `row` and `column` coordinates.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellMeta
|
||
* @param {number} row Visual row index.
|
||
* @param {number} column Visual column index.
|
||
* @returns {object} The cell properties object.
|
||
* @fires Hooks#beforeGetCellMeta
|
||
* @fires Hooks#afterGetCellMeta
|
||
*/
|
||
this.getCellMeta = function (row, column) {
|
||
let physicalRow = this.toPhysicalRow(row);
|
||
let physicalColumn = this.toPhysicalColumn(column);
|
||
if (physicalRow === null) {
|
||
physicalRow = row;
|
||
}
|
||
if (physicalColumn === null) {
|
||
physicalColumn = column;
|
||
}
|
||
return metaManager.getCellMeta(physicalRow, physicalColumn, {
|
||
visualRow: row,
|
||
visualColumn: column
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Returns an array of cell meta objects for specified physical row index.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellMetaAtRow
|
||
* @param {number} row Physical row index.
|
||
* @returns {Array}
|
||
*/
|
||
this.getCellMetaAtRow = function (row) {
|
||
return metaManager.getCellsMetaAtRow(row);
|
||
};
|
||
|
||
/**
|
||
* Checks if your [data format](@/guides/getting-started/binding-to-data.md#compatible-data-types)
|
||
* and [configuration options](@/guides/getting-started/configuration-options.md)
|
||
* allow for changing the number of columns.
|
||
*
|
||
* Returns `false` when your data is an array of objects,
|
||
* or when you use the [`columns`](@/api/options.md#columns) option.
|
||
* Otherwise, returns `true`.
|
||
*
|
||
* @memberof Core#
|
||
* @function isColumnModificationAllowed
|
||
* @returns {boolean}
|
||
*/
|
||
this.isColumnModificationAllowed = function () {
|
||
return !(instance.dataType === 'object' || tableMeta.columns);
|
||
};
|
||
|
||
/**
|
||
* Returns the cell renderer function by given `row` and `column` arguments.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellRenderer
|
||
* @param {number|object} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
|
||
* @param {number} column Visual column index.
|
||
* @returns {Function} Returns the renderer function.
|
||
* @example
|
||
* ```js
|
||
* // Get cell renderer using `row` and `column` coordinates.
|
||
* hot.getCellRenderer(1, 1);
|
||
* // Get cell renderer using cell meta object.
|
||
* hot.getCellRenderer(hot.getCellMeta(1, 1));
|
||
* ```
|
||
*/
|
||
this.getCellRenderer = function (rowOrMeta, column) {
|
||
const cellRenderer = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).renderer : rowOrMeta.renderer;
|
||
if (typeof cellRenderer === 'string') {
|
||
return getRenderer(cellRenderer);
|
||
}
|
||
return isUndefined(cellRenderer) ? getRenderer('text') : cellRenderer;
|
||
};
|
||
|
||
/**
|
||
* Returns the cell editor class by the provided `row` and `column` arguments.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellEditor
|
||
* @param {number} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
|
||
* @param {number} column Visual column index.
|
||
* @returns {Function|boolean} Returns the editor class or `false` is cell editor is disabled.
|
||
* @example
|
||
* ```js
|
||
* // Get cell editor class using `row` and `column` coordinates.
|
||
* hot.getCellEditor(1, 1);
|
||
* // Get cell editor class using cell meta object.
|
||
* hot.getCellEditor(hot.getCellMeta(1, 1));
|
||
* ```
|
||
*/
|
||
this.getCellEditor = function (rowOrMeta, column) {
|
||
const cellEditor = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).editor : rowOrMeta.editor;
|
||
if (typeof cellEditor === 'string') {
|
||
return getEditor(cellEditor);
|
||
}
|
||
return isUndefined(cellEditor) ? getEditor('text') : cellEditor;
|
||
};
|
||
|
||
/**
|
||
* Returns the cell validator by `row` and `column`.
|
||
*
|
||
* @memberof Core#
|
||
* @function getCellValidator
|
||
* @param {number|object} rowOrMeta Visual row index or cell meta object (see {@link Core#getCellMeta}).
|
||
* @param {number} column Visual column index.
|
||
* @returns {Function|RegExp|undefined} The validator function.
|
||
* @example
|
||
* ```js
|
||
* // Get cell validator using `row` and `column` coordinates.
|
||
* hot.getCellValidator(1, 1);
|
||
* // Get cell validator using cell meta object.
|
||
* hot.getCellValidator(hot.getCellMeta(1, 1));
|
||
* ```
|
||
*/
|
||
this.getCellValidator = function (rowOrMeta, column) {
|
||
const cellValidator = typeof rowOrMeta === 'number' ? instance.getCellMeta(rowOrMeta, column).validator : rowOrMeta.validator;
|
||
if (typeof cellValidator === 'string') {
|
||
return getValidator(cellValidator);
|
||
}
|
||
return cellValidator;
|
||
};
|
||
|
||
/**
|
||
* Validates every cell in the data set,
|
||
* using a [validator function](@/guides/cell-functions/cell-validator.md) configured for each cell.
|
||
*
|
||
* Doesn't validate cells that are currently [trimmed](@/guides/rows/row-trimming.md),
|
||
* [hidden](@/guides/rows/row-hiding.md), or [filtered](@/guides/columns/column-filter.md),
|
||
* as such cells are not included in the data set until you bring them back again.
|
||
*
|
||
* After the validation, the `callback` function is fired, with the `valid` argument set to:
|
||
* - `true` for valid cells
|
||
* - `false` for invalid cells
|
||
*
|
||
* @memberof Core#
|
||
* @function validateCells
|
||
* @param {Function} [callback] The callback function.
|
||
* @example
|
||
* ```js
|
||
* hot.validateCells((valid) => {
|
||
* if (valid) {
|
||
* // ... code for validated cells
|
||
* }
|
||
* })
|
||
* ```
|
||
*/
|
||
this.validateCells = function (callback) {
|
||
this._validateCells(callback);
|
||
};
|
||
|
||
/**
|
||
* Validates rows using their validator functions and calls callback when finished.
|
||
*
|
||
* If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
|
||
* would equal `true`.
|
||
*
|
||
* @memberof Core#
|
||
* @function validateRows
|
||
* @param {Array} [rows] Array of validation target visual row indexes.
|
||
* @param {Function} [callback] The callback function.
|
||
* @example
|
||
* ```js
|
||
* hot.validateRows([3, 4, 5], (valid) => {
|
||
* if (valid) {
|
||
* // ... code for validated rows
|
||
* }
|
||
* })
|
||
* ```
|
||
*/
|
||
this.validateRows = function (rows, callback) {
|
||
if (!Array.isArray(rows)) {
|
||
throw new Error('validateRows parameter `rows` must be an array');
|
||
}
|
||
this._validateCells(callback, rows);
|
||
};
|
||
|
||
/**
|
||
* Validates columns using their validator functions and calls callback when finished.
|
||
*
|
||
* If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
|
||
* would equal `true`.
|
||
*
|
||
* @memberof Core#
|
||
* @function validateColumns
|
||
* @param {Array} [columns] Array of validation target visual columns indexes.
|
||
* @param {Function} [callback] The callback function.
|
||
* @example
|
||
* ```js
|
||
* hot.validateColumns([3, 4, 5], (valid) => {
|
||
* if (valid) {
|
||
* // ... code for validated columns
|
||
* }
|
||
* })
|
||
* ```
|
||
*/
|
||
this.validateColumns = function (columns, callback) {
|
||
if (!Array.isArray(columns)) {
|
||
throw new Error('validateColumns parameter `columns` must be an array');
|
||
}
|
||
this._validateCells(callback, undefined, columns);
|
||
};
|
||
|
||
/**
|
||
* Validates all cells using their validator functions and calls callback when finished.
|
||
*
|
||
* If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it would equal `true`.
|
||
*
|
||
* Private use intended.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function _validateCells
|
||
* @param {Function} [callback] The callback function.
|
||
* @param {Array} [rows] An array of validation target visual row indexes.
|
||
* @param {Array} [columns] An array of validation target visual column indexes.
|
||
*/
|
||
this._validateCells = function (callback, rows, columns) {
|
||
const waitingForValidator = new ValidatorsQueue();
|
||
if (callback) {
|
||
waitingForValidator.onQueueEmpty = callback;
|
||
}
|
||
let i = instance.countRows() - 1;
|
||
while (i >= 0) {
|
||
if (rows !== undefined && rows.indexOf(i) === -1) {
|
||
i -= 1;
|
||
continue;
|
||
}
|
||
let j = instance.countCols() - 1;
|
||
while (j >= 0) {
|
||
if (columns !== undefined && columns.indexOf(j) === -1) {
|
||
j -= 1;
|
||
continue;
|
||
}
|
||
waitingForValidator.addValidatorToQueue();
|
||
instance.validateCell(instance.getDataAtCell(i, j), instance.getCellMeta(i, j), result => {
|
||
if (typeof result !== 'boolean') {
|
||
throw new Error('Validation error: result is not boolean');
|
||
}
|
||
if (result === false) {
|
||
waitingForValidator.valid = false;
|
||
}
|
||
waitingForValidator.removeValidatorFormQueue();
|
||
}, 'validateCells');
|
||
j -= 1;
|
||
}
|
||
i -= 1;
|
||
}
|
||
waitingForValidator.checkIfQueueIsEmpty();
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*
|
||
* @memberof Core#
|
||
* @function getRowHeader
|
||
* @param {number} [row] Visual row index.
|
||
* @fires Hooks#modifyRowHeader
|
||
* @returns {Array|string|number} Array of header values / single header value.
|
||
*/
|
||
this.getRowHeader = function (row) {
|
||
let rowHeader = tableMeta.rowHeaders;
|
||
let physicalRow = row;
|
||
if (physicalRow !== void 0) {
|
||
physicalRow = instance.runHooks('modifyRowHeader', physicalRow);
|
||
}
|
||
if (physicalRow === void 0) {
|
||
rowHeader = [];
|
||
rangeEach(instance.countRows() - 1, i => {
|
||
rowHeader.push(instance.getRowHeader(i));
|
||
});
|
||
} else if (Array.isArray(rowHeader) && rowHeader[physicalRow] !== void 0) {
|
||
rowHeader = rowHeader[physicalRow];
|
||
} else if (isFunction(rowHeader)) {
|
||
rowHeader = rowHeader(physicalRow);
|
||
} else if (rowHeader && typeof rowHeader !== 'string' && typeof rowHeader !== 'number') {
|
||
rowHeader = physicalRow + 1;
|
||
}
|
||
return rowHeader;
|
||
};
|
||
|
||
/**
|
||
* Returns information about if this table is configured to display row headers.
|
||
*
|
||
* @memberof Core#
|
||
* @function hasRowHeaders
|
||
* @returns {boolean} `true` if the instance has the row headers enabled, `false` otherwise.
|
||
*/
|
||
this.hasRowHeaders = function () {
|
||
return !!tableMeta.rowHeaders;
|
||
};
|
||
|
||
/**
|
||
* Returns information about if this table is configured to display column headers.
|
||
*
|
||
* @memberof Core#
|
||
* @function hasColHeaders
|
||
* @returns {boolean} `true` if the instance has the column headers enabled, `false` otherwise.
|
||
*/
|
||
this.hasColHeaders = function () {
|
||
if (tableMeta.colHeaders !== void 0 && tableMeta.colHeaders !== null) {
|
||
// Polymer has empty value = null
|
||
return !!tableMeta.colHeaders;
|
||
}
|
||
for (let i = 0, ilen = instance.countCols(); i < ilen; i++) {
|
||
if (instance.getColHeader(i)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Gets the values of column headers (if column headers are [enabled](@/api/options.md#colheaders)).
|
||
*
|
||
* To get an array with the values of all
|
||
* [bottom-most](@/guides/cell-features/clipboard.md#copy-with-headers) column headers,
|
||
* call `getColHeader()` with no arguments.
|
||
*
|
||
* To get the value of the bottom-most header of a specific column, use the `column` parameter.
|
||
*
|
||
* To get the value of a [specific-level](@/guides/columns/column-groups.md) header
|
||
* of a specific column, use the `column` and `headerLevel` parameters.
|
||
*
|
||
* Read more:
|
||
* - [Guides: Column groups](@/guides/columns/column-groups.md)
|
||
* - [Options: `colHeaders`](@/api/options.md#colheaders)
|
||
* - [Guides: Copy with headers](@/guides/cell-features/clipboard.md#copy-with-headers)
|
||
*
|
||
* ```js
|
||
* // get the contents of all bottom-most column headers
|
||
* hot.getColHeader();
|
||
*
|
||
* // get the contents of the bottom-most header of a specific column
|
||
* hot.getColHeader(5);
|
||
*
|
||
* // get the contents of a specific column header at a specific level
|
||
* hot.getColHeader(5, -2);
|
||
* ```
|
||
*
|
||
* @memberof Core#
|
||
* @function getColHeader
|
||
* @param {number} [column] A visual column index.
|
||
* @param {number} [headerLevel=-1] (Since 12.3.0) Header level index. Accepts positive (0 to n)
|
||
* and negative (-1 to -n) values. For positive values, 0 points to the
|
||
* topmost header. For negative values, -1 points to the bottom-most
|
||
* header (the header closest to the cells).
|
||
* @fires Hooks#modifyColHeader
|
||
* @fires Hooks#modifyColumnHeaderValue
|
||
* @returns {Array|string|number} Column header values.
|
||
*/
|
||
this.getColHeader = function (column) {
|
||
let headerLevel = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -1;
|
||
const columnIndex = instance.runHooks('modifyColHeader', column);
|
||
if (columnIndex === void 0) {
|
||
const out = [];
|
||
const ilen = instance.countCols();
|
||
for (let i = 0; i < ilen; i++) {
|
||
out.push(instance.getColHeader(i));
|
||
}
|
||
return out;
|
||
}
|
||
let result = tableMeta.colHeaders;
|
||
const translateVisualIndexToColumns = function (visualColumnIndex) {
|
||
const arr = [];
|
||
const columnsLen = instance.countCols();
|
||
let index = 0;
|
||
for (; index < columnsLen; index++) {
|
||
if (isFunction(tableMeta.columns) && tableMeta.columns(index)) {
|
||
arr.push(index);
|
||
}
|
||
}
|
||
return arr[visualColumnIndex];
|
||
};
|
||
const physicalColumn = instance.toPhysicalColumn(columnIndex);
|
||
const prop = translateVisualIndexToColumns(physicalColumn);
|
||
if (tableMeta.colHeaders === false) {
|
||
result = null;
|
||
} else if (tableMeta.columns && isFunction(tableMeta.columns) && tableMeta.columns(prop) && tableMeta.columns(prop).title) {
|
||
result = tableMeta.columns(prop).title;
|
||
} else if (tableMeta.columns && tableMeta.columns[physicalColumn] && tableMeta.columns[physicalColumn].title) {
|
||
result = tableMeta.columns[physicalColumn].title;
|
||
} else if (Array.isArray(tableMeta.colHeaders) && tableMeta.colHeaders[physicalColumn] !== void 0) {
|
||
result = tableMeta.colHeaders[physicalColumn];
|
||
} else if (isFunction(tableMeta.colHeaders)) {
|
||
result = tableMeta.colHeaders(physicalColumn);
|
||
} else if (tableMeta.colHeaders && typeof tableMeta.colHeaders !== 'string' && typeof tableMeta.colHeaders !== 'number') {
|
||
result = spreadsheetColumnLabel(columnIndex); // see #1458
|
||
}
|
||
|
||
result = instance.runHooks('modifyColumnHeaderValue', result, column, headerLevel);
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* Return column width from settings (no guessing). Private use intended.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function _getColWidthFromSettings
|
||
* @param {number} col Visual col index.
|
||
* @returns {number}
|
||
*/
|
||
this._getColWidthFromSettings = function (col) {
|
||
let width;
|
||
|
||
// We currently don't support cell meta objects for headers (negative values)
|
||
if (col >= 0) {
|
||
const cellProperties = instance.getCellMeta(0, col);
|
||
width = cellProperties.width;
|
||
}
|
||
if (width === void 0 || width === tableMeta.width) {
|
||
width = tableMeta.colWidths;
|
||
}
|
||
if (width !== void 0 && width !== null) {
|
||
switch (typeof width) {
|
||
case 'object':
|
||
// array
|
||
width = width[col];
|
||
break;
|
||
case 'function':
|
||
width = width(col);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
if (typeof width === 'string') {
|
||
width = parseInt(width, 10);
|
||
}
|
||
}
|
||
return width;
|
||
};
|
||
|
||
/**
|
||
* Returns the width of the requested column.
|
||
*
|
||
* @memberof Core#
|
||
* @function getColWidth
|
||
* @param {number} column Visual column index.
|
||
* @returns {number} Column width.
|
||
* @fires Hooks#modifyColWidth
|
||
*/
|
||
this.getColWidth = function (column) {
|
||
let width = instance._getColWidthFromSettings(column);
|
||
width = instance.runHooks('modifyColWidth', width, column);
|
||
if (width === void 0) {
|
||
width = ViewportColumnsCalculator.DEFAULT_WIDTH;
|
||
}
|
||
return width;
|
||
};
|
||
|
||
/**
|
||
* Return row height from settings (no guessing). Private use intended.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function _getRowHeightFromSettings
|
||
* @param {number} row Visual row index.
|
||
* @returns {number}
|
||
*/
|
||
this._getRowHeightFromSettings = function (row) {
|
||
// let cellProperties = instance.getCellMeta(row, 0);
|
||
// let height = cellProperties.height;
|
||
//
|
||
// if (height === void 0 || height === tableMeta.height) {
|
||
// height = cellProperties.rowHeights;
|
||
// }
|
||
let height = tableMeta.rowHeights;
|
||
if (height !== void 0 && height !== null) {
|
||
switch (typeof height) {
|
||
case 'object':
|
||
// array
|
||
height = height[row];
|
||
break;
|
||
case 'function':
|
||
height = height(row);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
if (typeof height === 'string') {
|
||
height = parseInt(height, 10);
|
||
}
|
||
}
|
||
return height;
|
||
};
|
||
|
||
/**
|
||
* Returns a row's height, as recognized by Handsontable.
|
||
*
|
||
* Depending on your configuration, the method returns (in order of priority):
|
||
* 1. The row height set by the [`ManualRowResize`](@/api/manualRowResize.md) plugin
|
||
* (if the plugin is enabled).
|
||
* 2. The row height set by the [`rowHeights`](@/api/options.md#rowheights) configuration option
|
||
* (if the option is set).
|
||
* 3. The row height as measured in the DOM by the [`AutoRowSize`](@/api/autoRowSize.md) plugin
|
||
* (if the plugin is enabled).
|
||
* 4. `undefined`, if neither [`ManualRowResize`](@/api/manualRowResize.md),
|
||
* nor [`rowHeights`](@/api/options.md#rowheights),
|
||
* nor [`AutoRowSize`](@/api/autoRowSize.md) is used.
|
||
*
|
||
* The height returned includes 1 px of the row's bottom border.
|
||
*
|
||
* Mind that this method is different from the
|
||
* [`getRowHeight()`](@/api/autoRowSize.md#getrowheight) method
|
||
* of the [`AutoRowSize`](@/api/autoRowSize.md) plugin.
|
||
*
|
||
* @memberof Core#
|
||
* @function getRowHeight
|
||
* @param {number} row A visual row index.
|
||
* @returns {number|undefined} The height of the specified row, in pixels.
|
||
* @fires Hooks#modifyRowHeight
|
||
*/
|
||
this.getRowHeight = function (row) {
|
||
let height = instance._getRowHeightFromSettings(row);
|
||
height = instance.runHooks('modifyRowHeight', height, row);
|
||
return height;
|
||
};
|
||
|
||
/**
|
||
* Returns the total number of rows in the data source.
|
||
*
|
||
* @memberof Core#
|
||
* @function countSourceRows
|
||
* @returns {number} Total number of rows.
|
||
*/
|
||
this.countSourceRows = function () {
|
||
return dataSource.countRows();
|
||
};
|
||
|
||
/**
|
||
* Returns the total number of columns in the data source.
|
||
*
|
||
* @memberof Core#
|
||
* @function countSourceCols
|
||
* @returns {number} Total number of columns.
|
||
*/
|
||
this.countSourceCols = function () {
|
||
return dataSource.countFirstRowKeys();
|
||
};
|
||
|
||
/**
|
||
* Returns the total number of visual rows in the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function countRows
|
||
* @returns {number} Total number of rows.
|
||
*/
|
||
this.countRows = function () {
|
||
return datamap.getLength();
|
||
};
|
||
|
||
/**
|
||
* Returns the total number of visible columns in the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function countCols
|
||
* @returns {number} Total number of columns.
|
||
*/
|
||
this.countCols = function () {
|
||
const maxCols = tableMeta.maxCols;
|
||
const dataLen = this.columnIndexMapper.getNotTrimmedIndexesLength();
|
||
return Math.min(maxCols, dataLen);
|
||
};
|
||
|
||
/**
|
||
* Returns the number of rendered rows including rows that are partially or fully rendered
|
||
* outside the table viewport.
|
||
*
|
||
* @memberof Core#
|
||
* @function countRenderedRows
|
||
* @returns {number} Returns -1 if table is not visible.
|
||
*/
|
||
this.countRenderedRows = function () {
|
||
return instance.view._wt.drawn ? instance.view._wt.wtTable.getRenderedRowsCount() : -1;
|
||
};
|
||
|
||
/**
|
||
* Returns the number of rendered rows that are only visible in the table viewport.
|
||
* The rows that are partially visible are not counted.
|
||
*
|
||
* @memberof Core#
|
||
* @function countVisibleRows
|
||
* @returns {number} Number of visible rows or -1.
|
||
*/
|
||
this.countVisibleRows = function () {
|
||
return instance.view._wt.drawn ? instance.view._wt.wtTable.getVisibleRowsCount() : -1;
|
||
};
|
||
|
||
/**
|
||
* Returns the number of rendered rows including columns that are partially or fully rendered
|
||
* outside the table viewport.
|
||
*
|
||
* @memberof Core#
|
||
* @function countRenderedCols
|
||
* @returns {number} Returns -1 if table is not visible.
|
||
*/
|
||
this.countRenderedCols = function () {
|
||
return instance.view._wt.drawn ? instance.view._wt.wtTable.getRenderedColumnsCount() : -1;
|
||
};
|
||
|
||
/**
|
||
* Returns the number of rendered columns that are only visible in the table viewport.
|
||
* The columns that are partially visible are not counted.
|
||
*
|
||
* @memberof Core#
|
||
* @function countVisibleCols
|
||
* @returns {number} Number of visible columns or -1.
|
||
*/
|
||
this.countVisibleCols = function () {
|
||
return instance.view._wt.drawn ? instance.view._wt.wtTable.getVisibleColumnsCount() : -1;
|
||
};
|
||
|
||
/**
|
||
* Returns the number of empty rows. If the optional ending parameter is `true`, returns the
|
||
* number of empty rows at the bottom of the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function countEmptyRows
|
||
* @param {boolean} [ending=false] If `true`, will only count empty rows at the end of the data source.
|
||
* @returns {number} Count empty rows.
|
||
*/
|
||
this.countEmptyRows = function () {
|
||
let ending = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
let emptyRows = 0;
|
||
rangeEachReverse(instance.countRows() - 1, visualIndex => {
|
||
if (instance.isEmptyRow(visualIndex)) {
|
||
emptyRows += 1;
|
||
} else if (ending === true) {
|
||
return false;
|
||
}
|
||
});
|
||
return emptyRows;
|
||
};
|
||
|
||
/**
|
||
* Returns the number of empty columns. If the optional ending parameter is `true`, returns the number of empty
|
||
* columns at right hand edge of the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function countEmptyCols
|
||
* @param {boolean} [ending=false] If `true`, will only count empty columns at the end of the data source row.
|
||
* @returns {number} Count empty cols.
|
||
*/
|
||
this.countEmptyCols = function () {
|
||
let ending = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
let emptyColumns = 0;
|
||
rangeEachReverse(instance.countCols() - 1, visualIndex => {
|
||
if (instance.isEmptyCol(visualIndex)) {
|
||
emptyColumns += 1;
|
||
} else if (ending === true) {
|
||
return false;
|
||
}
|
||
});
|
||
return emptyColumns;
|
||
};
|
||
|
||
/**
|
||
* Check if all cells in the row declared by the `row` argument are empty.
|
||
*
|
||
* @memberof Core#
|
||
* @function isEmptyRow
|
||
* @param {number} row Visual row index.
|
||
* @returns {boolean} `true` if the row at the given `row` is empty, `false` otherwise.
|
||
*/
|
||
this.isEmptyRow = function (row) {
|
||
return tableMeta.isEmptyRow.call(instance, row);
|
||
};
|
||
|
||
/**
|
||
* Check if all cells in the the column declared by the `column` argument are empty.
|
||
*
|
||
* @memberof Core#
|
||
* @function isEmptyCol
|
||
* @param {number} column Column index.
|
||
* @returns {boolean} `true` if the column at the given `col` is empty, `false` otherwise.
|
||
*/
|
||
this.isEmptyCol = function (column) {
|
||
return tableMeta.isEmptyCol.call(instance, column);
|
||
};
|
||
|
||
/**
|
||
* Select a single cell, or a single range of adjacent cells.
|
||
*
|
||
* To select a cell, pass its visual row and column indexes, for example: `selectCell(2, 4)`.
|
||
*
|
||
* To select a range, pass the visual indexes of the first and last cell in the range, for example: `selectCell(2, 4, 3, 5)`.
|
||
*
|
||
* If your columns have properties, you can pass those properties' values instead of column indexes, for example: `selectCell(2, 'first_name')`.
|
||
*
|
||
* By default, `selectCell()` also:
|
||
* - Scrolls the viewport to the newly-selected cells.
|
||
* - Switches the keyboard focus to Handsontable (by calling Handsontable's [`listen()`](#listen) method).
|
||
*
|
||
* @example
|
||
* ```js
|
||
* // select a single cell
|
||
* hot.selectCell(2, 4);
|
||
*
|
||
* // select a range of cells
|
||
* hot.selectCell(2, 4, 3, 5);
|
||
*
|
||
* // select a single cell, using a column property
|
||
* hot.selectCell(2, 'first_name');
|
||
*
|
||
* // select a range of cells, using column properties
|
||
* hot.selectCell(2, 'first_name', 3, 'last_name');
|
||
*
|
||
* // select a range of cells, without scrolling to them
|
||
* hot.selectCell(2, 4, 3, 5, false);
|
||
*
|
||
* // select a range of cells, without switching the keyboard focus to Handsontable
|
||
* hot.selectCell(2, 4, 3, 5, null, false);
|
||
* ```
|
||
*
|
||
* @memberof Core#
|
||
* @function selectCell
|
||
* @param {number} row A visual row index.
|
||
* @param {number|string} column A visual column index (`number`), or a column property's value (`string`).
|
||
* @param {number} [endRow] If selecting a range: the visual row index of the last cell in the range.
|
||
* @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.
|
||
* @param {boolean} [scrollToCell=true] `true`: scroll the viewport to the newly-selected cells. `false`: keep the previous viewport.
|
||
* @param {boolean} [changeListener=true] `true`: switch the keyboard focus to Handsontable. `false`: keep the previous keyboard focus.
|
||
* @returns {boolean} `true`: the selection was successful, `false`: the selection failed.
|
||
*/
|
||
this.selectCell = function (row, column, endRow, endColumn) {
|
||
let scrollToCell = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
|
||
let changeListener = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
|
||
if (isUndefined(row) || isUndefined(column)) {
|
||
return false;
|
||
}
|
||
return this.selectCells([[row, column, endRow, endColumn]], scrollToCell, changeListener);
|
||
};
|
||
|
||
/**
|
||
* Select multiple cells or ranges of cells, adjacent or non-adjacent.
|
||
*
|
||
* You can pass one of the below:
|
||
* - An array of arrays (which matches the output of Handsontable's [`getSelected()`](#getselected) method).
|
||
* - An array of [`CellRange`](@/api/cellRange.md) objects (which matches the output of Handsontable's [`getSelectedRange()`](#getselectedrange) method).
|
||
*
|
||
* To select multiple cells, pass the visual row and column indexes of each cell, for example: `hot.selectCells([[1, 1], [5, 5]])`.
|
||
*
|
||
* 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]])`.
|
||
*
|
||
* 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']])`.
|
||
*
|
||
* By default, `selectCell()` also:
|
||
* - Scrolls the viewport to the newly-selected cells.
|
||
* - Switches the keyboard focus to Handsontable (by calling Handsontable's [`listen()`](#listen) method).
|
||
*
|
||
* @example
|
||
* ```js
|
||
* // select non-adjacent cells
|
||
* hot.selectCells([[1, 1], [5, 5], [10, 10]]);
|
||
*
|
||
* // select non-adjacent ranges of cells
|
||
* hot.selectCells([[1, 1, 2, 2], [10, 10, 20, 20]]);
|
||
*
|
||
* // select cells and ranges of cells
|
||
* hot.selectCells([[1, 1, 2, 2], [3, 3], [6, 2, 0, 2]]);
|
||
*
|
||
* // select cells, using column properties
|
||
* hot.selectCells([[1, 'id', 2, 'first_name'], [3, 'full_name'], [6, 'last_name', 0, 'first_name']]);
|
||
*
|
||
* // select multiple ranges, using an array of `CellRange` objects
|
||
* const selected = hot.getSelectedRange();
|
||
*
|
||
* selected[0].from.row = 0;
|
||
* selected[0].from.col = 0;
|
||
* selected[0].to.row = 5;
|
||
* selected[0].to.col = 5;
|
||
*
|
||
* selected[1].from.row = 10;
|
||
* selected[1].from.col = 10;
|
||
* selected[1].to.row = 20;
|
||
* selected[1].to.col = 20;
|
||
*
|
||
* hot.selectCells(selected);
|
||
* ```
|
||
*
|
||
* @memberof Core#
|
||
* @since 0.38.0
|
||
* @function selectCells
|
||
* @param {Array[]|CellRange[]} coords Visual coordinates,
|
||
* passed either as an array of arrays (`[[rowStart, columnStart, rowEnd, columnEnd], ...]`)
|
||
* or as an array of [`CellRange`](@/api/cellRange.md) objects.
|
||
* @param {boolean} [scrollToCell=true] `true`: scroll the viewport to the newly-selected cells. `false`: keep the previous viewport.
|
||
* @param {boolean} [changeListener=true] `true`: switch the keyboard focus to Handsontable. `false`: keep the previous keyboard focus.
|
||
* @returns {boolean} `true`: the selection was successful, `false`: the selection failed.
|
||
*/
|
||
this.selectCells = function () {
|
||
let coords = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [[]];
|
||
let scrollToCell = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||
let changeListener = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
||
if (scrollToCell === false) {
|
||
preventScrollingToCell = true;
|
||
}
|
||
const wasSelected = selection.selectCells(coords);
|
||
if (wasSelected && changeListener) {
|
||
instance.listen();
|
||
}
|
||
preventScrollingToCell = false;
|
||
return wasSelected;
|
||
};
|
||
|
||
/**
|
||
* Select column specified by `startColumn` visual index, column property or a range of columns finishing at `endColumn`.
|
||
*
|
||
* @example
|
||
* ```js
|
||
* // Select column using visual index.
|
||
* hot.selectColumns(1);
|
||
* // Select column using column property.
|
||
* hot.selectColumns('id');
|
||
* // Select range of columns using visual indexes.
|
||
* hot.selectColumns(1, 4);
|
||
* // Select range of columns using column properties.
|
||
* hot.selectColumns('id', 'last_name');
|
||
* ```
|
||
*
|
||
* @memberof Core#
|
||
* @since 0.38.0
|
||
* @function selectColumns
|
||
* @param {number} startColumn The visual column index from which the selection starts.
|
||
* @param {number} [endColumn=startColumn] The visual column index to which the selection finishes. If `endColumn`
|
||
* is not defined the column defined by `startColumn` will be selected.
|
||
* @returns {boolean} `true` if selection was successful, `false` otherwise.
|
||
*/
|
||
this.selectColumns = function (startColumn) {
|
||
let endColumn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : startColumn;
|
||
return selection.selectColumns(startColumn, endColumn);
|
||
};
|
||
|
||
/**
|
||
* Select row specified by `startRow` visual index or a range of rows finishing at `endRow`.
|
||
*
|
||
* @example
|
||
* ```js
|
||
* // Select row using visual index.
|
||
* hot.selectRows(1);
|
||
* // Select range of rows using visual indexes.
|
||
* hot.selectRows(1, 4);
|
||
* ```
|
||
*
|
||
* @memberof Core#
|
||
* @since 0.38.0
|
||
* @function selectRows
|
||
* @param {number} startRow The visual row index from which the selection starts.
|
||
* @param {number} [endRow=startRow] The visual row index to which the selection finishes. If `endRow`
|
||
* is not defined the row defined by `startRow` will be selected.
|
||
* @returns {boolean} `true` if selection was successful, `false` otherwise.
|
||
*/
|
||
this.selectRows = function (startRow) {
|
||
let endRow = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : startRow;
|
||
return selection.selectRows(startRow, endRow);
|
||
};
|
||
|
||
/**
|
||
* Deselects the current cell selection on the table.
|
||
*
|
||
* @memberof Core#
|
||
* @function deselectCell
|
||
*/
|
||
this.deselectCell = function () {
|
||
selection.deselect();
|
||
};
|
||
|
||
/**
|
||
* Select the whole table.
|
||
*
|
||
* The previous selection is overwritten.
|
||
*
|
||
* ```js
|
||
* // select all cells in the table, including all headers
|
||
* hot.selectAll();
|
||
*
|
||
* // select all cells in the table, without headers
|
||
* hot.selectAll(false);
|
||
* ```
|
||
*
|
||
* @since 0.38.2
|
||
* @memberof Core#
|
||
* @function selectAll
|
||
* @param {boolean} [includeHeaders=true] `true`: include all row, column and corner headers.
|
||
* `false`: don't include any headers.
|
||
*/
|
||
this.selectAll = function () {
|
||
let includeHeaders = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
|
||
const includeRowHeaders = includeHeaders && this.hasRowHeaders();
|
||
const includeColumnHeaders = includeHeaders && this.hasColHeaders();
|
||
preventScrollingToCell = true;
|
||
selection.selectAll(includeRowHeaders, includeColumnHeaders);
|
||
preventScrollingToCell = false;
|
||
};
|
||
const getIndexToScroll = (indexMapper, visualIndex) => {
|
||
// Looking for a visual index on the right and then (when not found) on the left.
|
||
return indexMapper.getNearestNotHiddenIndex(visualIndex, 1, true);
|
||
};
|
||
|
||
/**
|
||
* Scroll viewport to coordinates specified by the `row` and `column` arguments.
|
||
*
|
||
* @memberof Core#
|
||
* @function scrollViewportTo
|
||
* @param {number} [row] Row index. If the last argument isn't defined we treat the index as a visual row index. Otherwise,
|
||
* we are using the index for numbering only this rows which may be rendered (we don't consider hidden rows).
|
||
* @param {number} [column] Column index. If the last argument isn't defined we treat the index as a visual column index.
|
||
* Otherwise, we are using the index for numbering only this columns which may be rendered (we don't consider hidden columns).
|
||
* @param {boolean} [snapToBottom=false] If `true`, the viewport is scrolled to show the cell at the bottom of the table.
|
||
* However, if the cell's height is greater than the table's viewport height, the cell is snapped to the top edge.
|
||
* @param {boolean} [snapToRight=false] If `true`, the viewport is scrolled to show the cell at the right side of the table.
|
||
* However, if the cell is wider than the table's viewport width, the cell is snapped to the left edge (or to the right edge, if the layout direction is set to `rtl`).
|
||
* @param {boolean} [considerHiddenIndexes=true] If `true`, we handle visual indexes, otherwise we handle only indexes which
|
||
* may be rendered when they are in the viewport (we don't consider hidden indexes as they aren't rendered).
|
||
* @returns {boolean} `true` if scroll was successful, `false` otherwise.
|
||
*/
|
||
this.scrollViewportTo = function (row, column) {
|
||
let snapToBottom = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
||
let snapToRight = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
|
||
let considerHiddenIndexes = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
|
||
const snapToTop = !snapToBottom;
|
||
const snapToLeft = !snapToRight;
|
||
let renderableRow = row;
|
||
let renderableColumn = column;
|
||
if (considerHiddenIndexes) {
|
||
const isRowInteger = Number.isInteger(row);
|
||
const isColumnInteger = Number.isInteger(column);
|
||
const visualRowToScroll = isRowInteger ? getIndexToScroll(this.rowIndexMapper, row) : void 0;
|
||
const visualColumnToScroll = isColumnInteger ? getIndexToScroll(this.columnIndexMapper, column) : void 0;
|
||
if (visualRowToScroll === null || visualColumnToScroll === null) {
|
||
return false;
|
||
}
|
||
renderableRow = isRowInteger ? instance.rowIndexMapper.getRenderableFromVisualIndex(visualRowToScroll) : void 0;
|
||
renderableColumn = isColumnInteger ? instance.columnIndexMapper.getRenderableFromVisualIndex(visualColumnToScroll) : void 0;
|
||
}
|
||
const isRowInteger = Number.isInteger(renderableRow);
|
||
const isColumnInteger = Number.isInteger(renderableColumn);
|
||
if (isRowInteger && isColumnInteger) {
|
||
return instance.view.scrollViewport(instance._createCellCoords(renderableRow, renderableColumn), snapToTop, snapToRight, snapToBottom, snapToLeft);
|
||
}
|
||
if (isRowInteger && isColumnInteger === false) {
|
||
return instance.view.scrollViewportVertically(renderableRow, snapToTop, snapToBottom);
|
||
}
|
||
if (isColumnInteger && isRowInteger === false) {
|
||
return instance.view.scrollViewportHorizontally(renderableColumn, snapToRight, snapToLeft);
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Removes the table from the DOM and destroys the instance of the Handsontable.
|
||
*
|
||
* @memberof Core#
|
||
* @function destroy
|
||
* @fires Hooks#afterDestroy
|
||
*/
|
||
this.destroy = function () {
|
||
instance._clearTimeouts();
|
||
instance._clearImmediates();
|
||
if (instance.view) {
|
||
// in case HT is destroyed before initialization has finished
|
||
instance.view.destroy();
|
||
}
|
||
if (dataSource) {
|
||
dataSource.destroy();
|
||
}
|
||
dataSource = null;
|
||
this.getShortcutManager().destroy();
|
||
metaManager.clearCache();
|
||
if (isRootInstance(instance)) {
|
||
const licenseInfo = this.rootDocument.querySelector('.hot-display-license-info');
|
||
if (licenseInfo) {
|
||
licenseInfo.parentNode.removeChild(licenseInfo);
|
||
}
|
||
}
|
||
empty(instance.rootElement);
|
||
eventManager.destroy();
|
||
if (editorManager) {
|
||
editorManager.destroy();
|
||
}
|
||
|
||
// The plugin's `destroy` method is called as a consequence and it should handle
|
||
// unregistration of plugin's maps. Some unregistered maps reset the cache.
|
||
instance.batchExecution(() => {
|
||
instance.rowIndexMapper.unregisterAll();
|
||
instance.columnIndexMapper.unregisterAll();
|
||
pluginsRegistry.getItems().forEach(_ref10 => {
|
||
let [, plugin] = _ref10;
|
||
plugin.destroy();
|
||
});
|
||
pluginsRegistry.clear();
|
||
instance.runHooks('afterDestroy');
|
||
}, true);
|
||
Hooks.getSingleton().destroy(instance);
|
||
objectEach(instance, (property, key, obj) => {
|
||
// replace instance methods with post mortem
|
||
if (isFunction(property)) {
|
||
obj[key] = postMortem(key);
|
||
} else if (key !== 'guid') {
|
||
// replace instance properties with null (restores memory)
|
||
// it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
|
||
obj[key] = null;
|
||
}
|
||
});
|
||
instance.isDestroyed = true;
|
||
|
||
// replace private properties with null (restores memory)
|
||
// it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
|
||
if (datamap) {
|
||
datamap.destroy();
|
||
}
|
||
datamap = null;
|
||
grid = null;
|
||
selection = null;
|
||
editorManager = null;
|
||
instance = null;
|
||
};
|
||
|
||
/**
|
||
* Replacement for all methods after the Handsontable was destroyed.
|
||
*
|
||
* @private
|
||
* @param {string} method The method name.
|
||
* @returns {Function}
|
||
*/
|
||
function postMortem(method) {
|
||
return () => {
|
||
throw new Error(`The "${method}" method cannot be called because this Handsontable instance has been destroyed`);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Returns the active editor class instance.
|
||
*
|
||
* @memberof Core#
|
||
* @function getActiveEditor
|
||
* @returns {BaseEditor} The active editor instance.
|
||
*/
|
||
this.getActiveEditor = function () {
|
||
return editorManager.getActiveEditor();
|
||
};
|
||
|
||
/**
|
||
* Returns plugin instance by provided its name.
|
||
*
|
||
* @memberof Core#
|
||
* @function getPlugin
|
||
* @param {string} pluginName The plugin name.
|
||
* @returns {BasePlugin|undefined} The plugin instance or undefined if there is no plugin.
|
||
*/
|
||
this.getPlugin = function (pluginName) {
|
||
const unifiedPluginName = toUpperCaseFirst(pluginName);
|
||
|
||
// Workaround for the UndoRedo plugin which, currently doesn't follow the plugin architecture.
|
||
if (unifiedPluginName === 'UndoRedo') {
|
||
return this.undoRedo;
|
||
}
|
||
return pluginsRegistry.getItem(unifiedPluginName);
|
||
};
|
||
|
||
/**
|
||
* Returns name of the passed plugin.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @param {BasePlugin} plugin The plugin instance.
|
||
* @returns {string}
|
||
*/
|
||
this.getPluginName = function (plugin) {
|
||
// Workaround for the UndoRedo plugin which, currently doesn't follow the plugin architecture.
|
||
if (plugin === this.undoRedo) {
|
||
return this.undoRedo.constructor.PLUGIN_KEY;
|
||
}
|
||
return pluginsRegistry.getId(plugin);
|
||
};
|
||
|
||
/**
|
||
* Returns the Handsontable instance.
|
||
*
|
||
* @memberof Core#
|
||
* @function getInstance
|
||
* @returns {Handsontable} The Handsontable instance.
|
||
*/
|
||
this.getInstance = function () {
|
||
return instance;
|
||
};
|
||
|
||
/**
|
||
* Adds listener to the specified hook name (only for this Handsontable instance).
|
||
*
|
||
* @memberof Core#
|
||
* @function addHook
|
||
* @see Hooks#add
|
||
* @param {string} key Hook name (see {@link Hooks}).
|
||
* @param {Function|Array} callback Function or array of functions.
|
||
* @example
|
||
* ```js
|
||
* hot.addHook('beforeInit', myCallback);
|
||
* ```
|
||
*/
|
||
this.addHook = function (key, callback) {
|
||
Hooks.getSingleton().add(key, callback, instance);
|
||
};
|
||
|
||
/**
|
||
* Check if for a specified hook name there are added listeners (only for this Handsontable instance). All available
|
||
* hooks you will find {@link Hooks}.
|
||
*
|
||
* @memberof Core#
|
||
* @function hasHook
|
||
* @see Hooks#has
|
||
* @param {string} key Hook name.
|
||
* @returns {boolean}
|
||
*
|
||
* @example
|
||
* ```js
|
||
* const hasBeforeInitListeners = hot.hasHook('beforeInit');
|
||
* ```
|
||
*/
|
||
this.hasHook = function (key) {
|
||
return Hooks.getSingleton().has(key, instance) || Hooks.getSingleton().has(key);
|
||
};
|
||
|
||
/**
|
||
* Adds listener to specified hook name (only for this Handsontable instance). After the listener is triggered,
|
||
* it will be automatically removed.
|
||
*
|
||
* @memberof Core#
|
||
* @function addHookOnce
|
||
* @see Hooks#once
|
||
* @param {string} key Hook name (see {@link Hooks}).
|
||
* @param {Function|Array} callback Function or array of functions.
|
||
* @example
|
||
* ```js
|
||
* hot.addHookOnce('beforeInit', myCallback);
|
||
* ```
|
||
*/
|
||
this.addHookOnce = function (key, callback) {
|
||
Hooks.getSingleton().once(key, callback, instance);
|
||
};
|
||
|
||
/**
|
||
* Removes the hook listener previously registered with {@link Core#addHook}.
|
||
*
|
||
* @memberof Core#
|
||
* @function removeHook
|
||
* @see Hooks#remove
|
||
* @param {string} key Hook name.
|
||
* @param {Function} callback Reference to the function which has been registered using {@link Core#addHook}.
|
||
*
|
||
* @example
|
||
* ```js
|
||
* hot.removeHook('beforeInit', myCallback);
|
||
* ```
|
||
*/
|
||
this.removeHook = function (key, callback) {
|
||
Hooks.getSingleton().remove(key, callback, instance);
|
||
};
|
||
|
||
/**
|
||
* Run the callbacks for the hook provided in the `key` argument using the parameters given in the other arguments.
|
||
*
|
||
* @memberof Core#
|
||
* @function runHooks
|
||
* @see Hooks#run
|
||
* @param {string} key Hook name.
|
||
* @param {*} [p1] Argument passed to the callback.
|
||
* @param {*} [p2] Argument passed to the callback.
|
||
* @param {*} [p3] Argument passed to the callback.
|
||
* @param {*} [p4] Argument passed to the callback.
|
||
* @param {*} [p5] Argument passed to the callback.
|
||
* @param {*} [p6] Argument passed to the callback.
|
||
* @returns {*}
|
||
*
|
||
* @example
|
||
* ```js
|
||
* // Run built-in hook
|
||
* hot.runHooks('beforeInit');
|
||
* // Run custom hook
|
||
* hot.runHooks('customAction', 10, 'foo');
|
||
* ```
|
||
*/
|
||
this.runHooks = function (key, p1, p2, p3, p4, p5, p6) {
|
||
return Hooks.getSingleton().run(instance, key, p1, p2, p3, p4, p5, p6);
|
||
};
|
||
|
||
/**
|
||
* Get language phrase for specified dictionary key.
|
||
*
|
||
* @memberof Core#
|
||
* @function getTranslatedPhrase
|
||
* @since 0.35.0
|
||
* @param {string} dictionaryKey Constant which is dictionary key.
|
||
* @param {*} extraArguments Arguments which will be handled by formatters.
|
||
* @returns {string}
|
||
*/
|
||
this.getTranslatedPhrase = function (dictionaryKey, extraArguments) {
|
||
return getTranslatedPhrase(tableMeta.language, dictionaryKey, extraArguments);
|
||
};
|
||
|
||
/**
|
||
* Converts instance into outerHTML of HTMLTableElement.
|
||
*
|
||
* @memberof Core#
|
||
* @function toHTML
|
||
* @since 7.1.0
|
||
* @returns {string}
|
||
*/
|
||
this.toHTML = () => instanceToHTML(this);
|
||
|
||
/**
|
||
* Converts instance into HTMLTableElement.
|
||
*
|
||
* @memberof Core#
|
||
* @function toTableElement
|
||
* @since 7.1.0
|
||
* @returns {HTMLTableElement}
|
||
*/
|
||
this.toTableElement = () => {
|
||
const tempElement = this.rootDocument.createElement('div');
|
||
tempElement.insertAdjacentHTML('afterbegin', instanceToHTML(this));
|
||
return tempElement.firstElementChild;
|
||
};
|
||
this.timeouts = [];
|
||
|
||
/**
|
||
* Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called.
|
||
*
|
||
* @param {number|Function} handle Handler returned from setTimeout or function to execute (it will be automatically wraped
|
||
* by setTimeout function).
|
||
* @param {number} [delay=0] If first argument is passed as a function this argument set delay of the execution of that function.
|
||
* @private
|
||
*/
|
||
this._registerTimeout = function (handle) {
|
||
let delay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
||
let handleFunc = handle;
|
||
if (typeof handleFunc === 'function') {
|
||
handleFunc = setTimeout(handleFunc, delay);
|
||
}
|
||
this.timeouts.push(handleFunc);
|
||
};
|
||
|
||
/**
|
||
* Clears all known timeouts.
|
||
*
|
||
* @private
|
||
*/
|
||
this._clearTimeouts = function () {
|
||
arrayEach(this.timeouts, handler => {
|
||
clearTimeout(handler);
|
||
});
|
||
};
|
||
this.immediates = [];
|
||
|
||
/**
|
||
* Execute function execution to the next event loop cycle. Purpose of this method is to clear all known timeouts when `destroy` method is called.
|
||
*
|
||
* @param {Function} callback Function to be delayed in execution.
|
||
* @private
|
||
*/
|
||
this._registerImmediate = function (callback) {
|
||
this.immediates.push(setImmediate(callback));
|
||
};
|
||
|
||
/**
|
||
* Clears all known timeouts.
|
||
*
|
||
* @private
|
||
*/
|
||
this._clearImmediates = function () {
|
||
arrayEach(this.immediates, handler => {
|
||
clearImmediate(handler);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Refresh selection borders. This is temporary method relic after selection rewrite.
|
||
*
|
||
* @private
|
||
* @param {boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved.
|
||
* @param {boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open.
|
||
*/
|
||
this._refreshBorders = function () {
|
||
let revertOriginal = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||
let prepareEditorIfNeeded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
||
editorManager.destroyEditor(revertOriginal);
|
||
instance.view.render();
|
||
if (prepareEditorIfNeeded && selection.isSelected()) {
|
||
editorManager.prepareEditor();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Check if currently it is RTL direction.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function isRtl
|
||
* @returns {boolean} True if RTL.
|
||
*/
|
||
this.isRtl = function () {
|
||
return instance.rootWindow.getComputedStyle(instance.rootElement).direction === 'rtl';
|
||
};
|
||
|
||
/**
|
||
* Check if currently it is LTR direction.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function isLtr
|
||
* @returns {boolean} True if LTR.
|
||
*/
|
||
this.isLtr = function () {
|
||
return !instance.isRtl();
|
||
};
|
||
|
||
/**
|
||
* Returns 1 for LTR; -1 for RTL. Useful for calculations.
|
||
*
|
||
* @private
|
||
* @memberof Core#
|
||
* @function getDirectionFactor
|
||
* @returns {number} Returns 1 for LTR; -1 for RTL.
|
||
*/
|
||
this.getDirectionFactor = function () {
|
||
return instance.isLtr() ? 1 : -1;
|
||
};
|
||
const shortcutManager = createShortcutManager({
|
||
handleEvent(event) {
|
||
const isListening = instance.isListening();
|
||
const isKeyboardEventWithKey = (event === null || event === void 0 ? void 0 : event.key) !== void 0;
|
||
return isListening && isKeyboardEventWithKey;
|
||
},
|
||
beforeKeyDown: event => {
|
||
return this.runHooks('beforeKeyDown', event);
|
||
},
|
||
afterKeyDown: event => {
|
||
if (this.isDestroyed) {
|
||
// Handsontable could be destroyed after performing action (executing a callback).
|
||
return;
|
||
}
|
||
instance.runHooks('afterDocumentKeyDown', event);
|
||
},
|
||
ownerWindow: this.rootWindow
|
||
});
|
||
this.addHook('beforeOnCellMouseDown', event => {
|
||
// Releasing keys as some browser/system shortcuts break events sequence (thus the `keyup` event isn't triggered).
|
||
if (event.ctrlKey === false && event.metaKey === false) {
|
||
shortcutManager.releasePressedKeys();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Returns instance of a manager responsible for handling shortcuts stored in some contexts. It run actions after
|
||
* pressing key combination in active Handsontable instance.
|
||
*
|
||
* @memberof Core#
|
||
* @since 12.0.0
|
||
* @function getShortcutManager
|
||
* @returns {ShortcutManager} Instance of {@link ShortcutManager}
|
||
*/
|
||
this.getShortcutManager = function () {
|
||
return shortcutManager;
|
||
};
|
||
const gridContext = shortcutManager.addContext('grid');
|
||
const gridConfig = {
|
||
runOnlyIf: () => {
|
||
return isDefined(instance.getSelected()) && instance.countRenderedRows() > 0 && instance.countRenderedCols() > 0;
|
||
},
|
||
group: SHORTCUTS_GROUP
|
||
};
|
||
shortcutManager.setActiveContextName('grid');
|
||
gridContext.addShortcuts([{
|
||
keys: [['Control/Meta', 'A']],
|
||
callback: () => {
|
||
instance.selectAll();
|
||
}
|
||
}, {
|
||
keys: [['Control/Meta', 'Enter']],
|
||
callback: () => {
|
||
const selectedRange = instance.getSelectedRange();
|
||
const {
|
||
row: highlightRow,
|
||
col: highlightColumn
|
||
} = selectedRange[selectedRange.length - 1].highlight;
|
||
const valueToPopulate = instance.getDataAtCell(highlightRow, highlightColumn);
|
||
const cellValues = new Map();
|
||
for (let i = 0; i < selectedRange.length; i++) {
|
||
selectedRange[i].forAll((row, column) => {
|
||
if (row >= 0 && column >= 0 && (row !== highlightRow || column !== highlightColumn)) {
|
||
const {
|
||
readOnly
|
||
} = instance.getCellMeta(row, column);
|
||
if (!readOnly) {
|
||
cellValues.set(`${row}x${column}`, [row, column, valueToPopulate]);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
instance.setDataAtCell(Array.from(cellValues.values()));
|
||
},
|
||
runOnlyIf: () => instance.getSelectedRangeLast().getCellsCount() > 1
|
||
}, {
|
||
keys: [['ArrowUp']],
|
||
callback: () => {
|
||
selection.transformStart(-1, 0);
|
||
}
|
||
}, {
|
||
keys: [['ArrowUp', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
selection.setRangeStart(instance._createCellCoords(instance.rowIndexMapper.getNearestNotHiddenIndex(0, 1), instance.getSelectedRangeLast().highlight.col));
|
||
}
|
||
}, {
|
||
keys: [['ArrowUp', 'Shift']],
|
||
callback: () => {
|
||
selection.transformEnd(-1, 0);
|
||
}
|
||
}, {
|
||
keys: [['ArrowUp', 'Shift', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const {
|
||
from,
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(0, 1);
|
||
selection.setRangeStart(from.clone());
|
||
selection.setRangeEnd(instance._createCellCoords(row, to.col));
|
||
},
|
||
runOnlyIf: () => !(instance.selection.isSelectedByCorner() || instance.selection.isSelectedByColumnHeader())
|
||
}, {
|
||
keys: [['ArrowDown']],
|
||
callback: () => {
|
||
selection.transformStart(1, 0);
|
||
}
|
||
}, {
|
||
keys: [['ArrowDown', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
selection.setRangeStart(instance._createCellCoords(instance.rowIndexMapper.getNearestNotHiddenIndex(instance.countRows() - 1, -1), instance.getSelectedRangeLast().highlight.col));
|
||
}
|
||
}, {
|
||
keys: [['ArrowDown', 'Shift']],
|
||
callback: () => {
|
||
selection.transformEnd(1, 0);
|
||
}
|
||
}, {
|
||
keys: [['ArrowDown', 'Shift', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const {
|
||
from,
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(instance.countRows() - 1, -1);
|
||
selection.setRangeStart(from.clone());
|
||
selection.setRangeEnd(instance._createCellCoords(row, to.col));
|
||
},
|
||
runOnlyIf: () => !(instance.selection.isSelectedByCorner() || instance.selection.isSelectedByColumnHeader())
|
||
}, {
|
||
keys: [['ArrowLeft']],
|
||
callback: () => {
|
||
selection.transformStart(0, -1 * instance.getDirectionFactor());
|
||
}
|
||
}, {
|
||
keys: [['ArrowLeft', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const row = instance.getSelectedRangeLast().highlight.row;
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(...(instance.isRtl() ? [instance.countCols() - 1, -1] : [0, 1]));
|
||
selection.setRangeStart(instance._createCellCoords(row, column));
|
||
}
|
||
}, {
|
||
keys: [['ArrowLeft', 'Shift']],
|
||
callback: () => {
|
||
selection.transformEnd(0, -1 * instance.getDirectionFactor());
|
||
}
|
||
}, {
|
||
keys: [['ArrowLeft', 'Shift', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const {
|
||
from,
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(...(instance.isRtl() ? [instance.countCols() - 1, -1] : [0, 1]));
|
||
selection.setRangeStart(from.clone());
|
||
selection.setRangeEnd(instance._createCellCoords(to.row, column));
|
||
},
|
||
runOnlyIf: () => !(instance.selection.isSelectedByCorner() || instance.selection.isSelectedByRowHeader())
|
||
}, {
|
||
keys: [['ArrowRight']],
|
||
callback: () => {
|
||
selection.transformStart(0, instance.getDirectionFactor());
|
||
}
|
||
}, {
|
||
keys: [['ArrowRight', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const row = instance.getSelectedRangeLast().highlight.row;
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(...(instance.isRtl() ? [0, 1] : [instance.countCols() - 1, -1]));
|
||
selection.setRangeStart(instance._createCellCoords(row, column));
|
||
}
|
||
}, {
|
||
keys: [['ArrowRight', 'Shift']],
|
||
callback: () => {
|
||
selection.transformEnd(0, instance.getDirectionFactor());
|
||
}
|
||
}, {
|
||
keys: [['ArrowRight', 'Shift', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const {
|
||
from,
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(...(instance.isRtl() ? [0, 1] : [instance.countCols() - 1, -1]));
|
||
selection.setRangeStart(from.clone());
|
||
selection.setRangeEnd(instance._createCellCoords(to.row, column));
|
||
},
|
||
runOnlyIf: () => !(instance.selection.isSelectedByCorner() || instance.selection.isSelectedByRowHeader())
|
||
}, {
|
||
keys: [['Home']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const fixedColumns = parseInt(instance.getSettings().fixedColumnsStart, 10);
|
||
const row = instance.getSelectedRangeLast().highlight.row;
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(fixedColumns, 1);
|
||
selection.setRangeStart(instance._createCellCoords(row, column));
|
||
},
|
||
runOnlyIf: () => instance.view.isMainTableNotFullyCoveredByOverlays()
|
||
}, {
|
||
keys: [['Home', 'Shift']],
|
||
callback: () => {
|
||
selection.setRangeEnd(instance._createCellCoords(selection.selectedRange.current().from.row, instance.columnIndexMapper.getNearestNotHiddenIndex(0, 1)));
|
||
}
|
||
}, {
|
||
keys: [['Home', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const fixedRows = parseInt(instance.getSettings().fixedRowsTop, 10);
|
||
const fixedColumns = parseInt(instance.getSettings().fixedColumnsStart, 10);
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(fixedRows, 1);
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(fixedColumns, 1);
|
||
selection.setRangeStart(instance._createCellCoords(row, column));
|
||
},
|
||
runOnlyIf: () => instance.view.isMainTableNotFullyCoveredByOverlays()
|
||
}, {
|
||
keys: [['End']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
selection.setRangeStart(instance._createCellCoords(instance.getSelectedRangeLast().highlight.row, instance.columnIndexMapper.getNearestNotHiddenIndex(instance.countCols() - 1, -1)));
|
||
},
|
||
runOnlyIf: () => instance.view.isMainTableNotFullyCoveredByOverlays()
|
||
}, {
|
||
keys: [['End', 'Shift']],
|
||
callback: () => {
|
||
selection.setRangeEnd(instance._createCellCoords(selection.selectedRange.current().from.row, instance.columnIndexMapper.getNearestNotHiddenIndex(instance.countCols() - 1, -1)));
|
||
}
|
||
}, {
|
||
keys: [['End', 'Control/Meta']],
|
||
captureCtrl: true,
|
||
callback: () => {
|
||
const fixedRows = parseInt(instance.getSettings().fixedRowsBottom, 10);
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(instance.countRows() - fixedRows - 1, -1);
|
||
const column = instance.columnIndexMapper.getNearestNotHiddenIndex(instance.countCols() - 1, -1);
|
||
selection.setRangeStart(instance._createCellCoords(row, column));
|
||
},
|
||
runOnlyIf: () => instance.view.isMainTableNotFullyCoveredByOverlays()
|
||
}, {
|
||
keys: [['PageUp']],
|
||
callback: () => {
|
||
selection.transformStart(-instance.countVisibleRows(), 0);
|
||
}
|
||
}, {
|
||
keys: [['PageUp', 'Shift']],
|
||
callback: () => {
|
||
const {
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const nextRowIndexToSelect = Math.max(to.row - instance.countVisibleRows(), 0);
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(nextRowIndexToSelect, 1);
|
||
if (row !== null) {
|
||
const coords = instance._createCellCoords(row, to.col);
|
||
const scrollPadding = to.row - instance.view.getFirstFullyVisibleRow();
|
||
const nextVerticalScroll = Math.max(coords.row - scrollPadding, 0);
|
||
selection.setRangeEnd(coords);
|
||
instance.scrollViewportTo(nextVerticalScroll);
|
||
}
|
||
}
|
||
}, {
|
||
keys: [['PageDown']],
|
||
callback: () => {
|
||
selection.transformStart(instance.countVisibleRows(), 0);
|
||
}
|
||
}, {
|
||
keys: [['PageDown', 'Shift']],
|
||
callback: () => {
|
||
const {
|
||
to
|
||
} = instance.getSelectedRangeLast();
|
||
const nextRowIndexToSelect = Math.min(to.row + instance.countVisibleRows(), instance.countRows() - 1);
|
||
const row = instance.rowIndexMapper.getNearestNotHiddenIndex(nextRowIndexToSelect, -1);
|
||
if (row !== null) {
|
||
const coords = instance._createCellCoords(row, to.col);
|
||
const scrollPadding = to.row - instance.view.getFirstFullyVisibleRow();
|
||
const nextVerticalScroll = Math.min(coords.row - scrollPadding, instance.countRows() - 1);
|
||
selection.setRangeEnd(coords);
|
||
instance.scrollViewportTo(nextVerticalScroll);
|
||
}
|
||
}
|
||
}, {
|
||
keys: [['Tab']],
|
||
callback: event => {
|
||
const tabMoves = typeof tableMeta.tabMoves === 'function' ? tableMeta.tabMoves(event) : tableMeta.tabMoves;
|
||
selection.transformStart(tabMoves.row, tabMoves.col, true);
|
||
}
|
||
}, {
|
||
keys: [['Shift', 'Tab']],
|
||
callback: event => {
|
||
const tabMoves = typeof tableMeta.tabMoves === 'function' ? tableMeta.tabMoves(event) : tableMeta.tabMoves;
|
||
selection.transformStart(-tabMoves.row, -tabMoves.col);
|
||
}
|
||
}], gridConfig);
|
||
getPluginsNames().forEach(pluginName => {
|
||
const PluginClass = getPlugin(pluginName);
|
||
pluginsRegistry.addItem(pluginName, new PluginClass(this));
|
||
});
|
||
Hooks.getSingleton().run(instance, 'construct');
|
||
} |