handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
1,114 lines (1,083 loc) • 55.4 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/es.json.stringify.js";
import "core-js/modules/es.set.difference.v2.js";
import "core-js/modules/es.set.intersection.v2.js";
import "core-js/modules/es.set.is-disjoint-from.v2.js";
import "core-js/modules/es.set.is-subset-of.v2.js";
import "core-js/modules/es.set.is-superset-of.v2.js";
import "core-js/modules/es.set.symmetric-difference.v2.js";
import "core-js/modules/es.set.union.v2.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.every.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.map.js";
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
import { BasePlugin } from "../base/index.mjs";
import { staticRegister } from "../../utils/staticRegister.mjs";
import { error, warn } from "../../helpers/console.mjs";
import { isNumeric } from "../../helpers/number.mjs";
import { isDefined, isUndefined } from "../../helpers/mixed.mjs";
import { getRegisteredHotInstances, setupEngine, setupSheet, unregisterEngine } from "./engine/register.mjs";
import { getDateFromExcelDate, getDateInHfFormat, getDateInHotFormat, isDate, isDateValid, isFormula, unescapeFormulaExpression } from "./utils.mjs";
import { getEngineSettingsWithOverrides, haveEngineSettingsChanged } from "./engine/settings.mjs";
import { isArrayOfArrays } from "../../helpers/data.mjs";
import { toUpperCaseFirst } from "../../helpers/string.mjs";
import { Hooks } from "../../core/hooks/index.mjs";
import IndexSyncer from "./indexSyncer/index.mjs";
export const PLUGIN_KEY = 'formulas';
export const SETTING_KEYS = ['maxRows', 'maxColumns', 'language'];
export const PLUGIN_PRIORITY = 260;
Hooks.getSingleton().register('afterNamedExpressionAdded');
Hooks.getSingleton().register('afterNamedExpressionRemoved');
Hooks.getSingleton().register('afterSheetAdded');
Hooks.getSingleton().register('afterSheetRemoved');
Hooks.getSingleton().register('afterSheetRenamed');
Hooks.getSingleton().register('afterFormulasValuesUpdate');
// This function will be used for detecting changes coming from the `UndoRedo` plugin. This kind of change won't be
// handled by whole body of listeners and therefore won't change undo/redo stack inside engine provided by HyperFormula.
// HyperFormula's `undo` and `redo` methods will do it instead. Please keep in mind that undo/redo stacks inside
// instances of Handsontable and HyperFormula should be synced (number of actions should be the same).
const isBlockedSource = source => source === 'UndoRedo.undo' || source === 'UndoRedo.redo' || source === 'auto';
/**
* This plugin allows you to perform Excel-like calculations in your business applications. It does it by an
* integration with our other product, [HyperFormula](https://github.com/handsontable/hyperformula/), which is a
* powerful calculation engine with an extensive number of features.
*
* To test out HyperFormula, see [this guide](@/guides/formulas/formula-calculation/formula-calculation.md#available-functions).
*
* @plugin Formulas
* @class Formulas
*/
var _internalOperationPending = /*#__PURE__*/new WeakMap();
var _hotWasInitializedWithEmptyData = /*#__PURE__*/new WeakMap();
var _engineListeners = /*#__PURE__*/new WeakMap();
var _Formulas_brand = /*#__PURE__*/new WeakSet();
export class Formulas extends BasePlugin {
constructor() {
var _this;
super(...arguments);
_this = this;
/**
* Update sheetName and sheetId properties.
*
* @param {string} [sheetName] The new sheet name.
*/
_classPrivateMethodInitSpec(this, _Formulas_brand);
/**
* Flag used to bypass hooks in internal operations.
*
* @private
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _internalOperationPending, false);
/**
* Flag needed to mark if Handsontable was initialized with no data.
* (Required to work around the fact, that Handsontable auto-generates sample data, when no data is provided).
*
* @type {boolean}
*/
_classPrivateFieldInitSpec(this, _hotWasInitializedWithEmptyData, false);
/**
* The list of the HyperFormula listeners.
*
* @type {Array}
*/
_classPrivateFieldInitSpec(this, _engineListeners, [['valuesUpdated', function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineValuesUpdated).call(_this, ...args);
}], ['namedExpressionAdded', function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineNamedExpressionsAdded).call(_this, ...args);
}], ['namedExpressionRemoved', function () {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineNamedExpressionsRemoved).call(_this, ...args);
}], ['sheetAdded', function () {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineSheetAdded).call(_this, ...args);
}], ['sheetRenamed', function () {
for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
args[_key5] = arguments[_key5];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineSheetRenamed).call(_this, ...args);
}], ['sheetRemoved', function () {
for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
args[_key6] = arguments[_key6];
}
return _assertClassBrand(_Formulas_brand, _this, _onEngineSheetRemoved).call(_this, ...args);
}]]);
/**
* Static register used to set up one global HyperFormula instance.
* TODO: currently used in tests, might be removed later.
*
* @private
* @type {object}
*/
_defineProperty(this, "staticRegister", staticRegister('formulas'));
/**
* The engine instance that will be used for this instance of Handsontable.
*
* @type {HyperFormula|null}
*/
_defineProperty(this, "engine", null);
/**
* HyperFormula's sheet id.
*
* @type {number|null}
*/
_defineProperty(this, "sheetId", null);
/**
* HyperFormula's sheet name.
*
* @type {string|null}
*/
_defineProperty(this, "sheetName", null);
/**
* Index synchronizer responsible for manipulating with some general options related to indexes synchronization.
*
* @type {IndexSyncer|null}
*/
_defineProperty(this, "indexSyncer", null);
/**
* Index synchronizer responsible for syncing the order of HOT and HF's data for the axis of the rows.
*
* @type {AxisSyncer|null}
*/
_defineProperty(this, "rowAxisSyncer", null);
/**
* Index synchronizer responsible for syncing the order of HOT and HF's data for the axis of the columns.
*
* @type {AxisSyncer|null}
*/
_defineProperty(this, "columnAxisSyncer", null);
}
static get PLUGIN_KEY() {
return PLUGIN_KEY;
}
static get PLUGIN_PRIORITY() {
return PLUGIN_PRIORITY;
}
static get SETTING_KEYS() {
return [PLUGIN_KEY, ...SETTING_KEYS];
}
/**
* Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
* hook and if it returns `true` then the {@link Formulas#enablePlugin} method is called.
*
* @returns {boolean}
*/
isEnabled() {
/* eslint-disable no-unneeded-ternary */
return this.hot.getSettings()[PLUGIN_KEY] ? true : false;
}
/**
* Enables the plugin functionality for this Handsontable instance.
*/
enablePlugin() {
var _setupEngine,
_this2 = this;
if (this.enabled) {
return;
}
this.engine = (_setupEngine = setupEngine(this.hot)) !== null && _setupEngine !== void 0 ? _setupEngine : this.engine;
if (!this.engine) {
warn('Missing the required `engine` key in the Formulas settings. Please fill it with either an' + ' engine class or an engine instance.');
return;
}
// Useful for disabling -> enabling the plugin using `updateSettings` or the API.
if (this.sheetName !== null && !this.engine.doesSheetExist(this.sheetName)) {
const newSheetName = this.addSheet(this.sheetName, this.hot.getSourceDataArray());
if (newSheetName !== false) {
_assertClassBrand(_Formulas_brand, this, _updateSheetNameAndSheetId).call(this, newSheetName);
}
}
this.addHook('beforeLoadData', function () {
for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
args[_key7] = arguments[_key7];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeLoadData).call(_this2, ...args);
});
this.addHook('afterLoadData', function () {
for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
args[_key8] = arguments[_key8];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterLoadData).call(_this2, ...args);
});
// The `updateData` hooks utilize the same logic as the `loadData` hooks.
this.addHook('beforeUpdateData', function () {
for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
args[_key9] = arguments[_key9];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeLoadData).call(_this2, ...args);
});
this.addHook('afterUpdateData', function () {
for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) {
args[_key0] = arguments[_key0];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterLoadData).call(_this2, ...args);
});
this.addHook('modifyData', function () {
for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) {
args[_key1] = arguments[_key1];
}
return _assertClassBrand(_Formulas_brand, _this2, _onModifyData).call(_this2, ...args);
});
this.addHook('modifySourceData', function () {
for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
args[_key10] = arguments[_key10];
}
return _assertClassBrand(_Formulas_brand, _this2, _onModifySourceData).call(_this2, ...args);
});
this.addHook('beforeValidate', function () {
for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
args[_key11] = arguments[_key11];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeValidate).call(_this2, ...args);
});
this.addHook('afterSetSourceDataAtCell', function () {
for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
args[_key12] = arguments[_key12];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterSetSourceDataAtCell).call(_this2, ...args);
});
this.addHook('afterSetDataAtCell', function () {
for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
args[_key13] = arguments[_key13];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterSetDataAtCell).call(_this2, ...args);
});
this.addHook('afterSetDataAtRowProp', function () {
for (var _len14 = arguments.length, args = new Array(_len14), _key14 = 0; _key14 < _len14; _key14++) {
args[_key14] = arguments[_key14];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterSetDataAtCell).call(_this2, ...args);
});
this.addHook('beforeCreateRow', function () {
for (var _len15 = arguments.length, args = new Array(_len15), _key15 = 0; _key15 < _len15; _key15++) {
args[_key15] = arguments[_key15];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeCreateRow).call(_this2, ...args);
});
this.addHook('beforeCreateCol', function () {
for (var _len16 = arguments.length, args = new Array(_len16), _key16 = 0; _key16 < _len16; _key16++) {
args[_key16] = arguments[_key16];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeCreateCol).call(_this2, ...args);
});
this.addHook('afterCreateRow', function () {
for (var _len17 = arguments.length, args = new Array(_len17), _key17 = 0; _key17 < _len17; _key17++) {
args[_key17] = arguments[_key17];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterCreateRow).call(_this2, ...args);
});
this.addHook('afterCreateCol', function () {
for (var _len18 = arguments.length, args = new Array(_len18), _key18 = 0; _key18 < _len18; _key18++) {
args[_key18] = arguments[_key18];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterCreateCol).call(_this2, ...args);
});
this.addHook('beforeRemoveRow', function () {
for (var _len19 = arguments.length, args = new Array(_len19), _key19 = 0; _key19 < _len19; _key19++) {
args[_key19] = arguments[_key19];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeRemoveRow).call(_this2, ...args);
});
this.addHook('beforeRemoveCol', function () {
for (var _len20 = arguments.length, args = new Array(_len20), _key20 = 0; _key20 < _len20; _key20++) {
args[_key20] = arguments[_key20];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeRemoveCol).call(_this2, ...args);
});
this.addHook('afterRemoveRow', function () {
for (var _len21 = arguments.length, args = new Array(_len21), _key21 = 0; _key21 < _len21; _key21++) {
args[_key21] = arguments[_key21];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterRemoveRow).call(_this2, ...args);
});
this.addHook('afterRemoveCol', function () {
for (var _len22 = arguments.length, args = new Array(_len22), _key22 = 0; _key22 < _len22; _key22++) {
args[_key22] = arguments[_key22];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterRemoveCol).call(_this2, ...args);
});
this.indexSyncer = new IndexSyncer(this.hot.rowIndexMapper, this.hot.columnIndexMapper, postponedAction => {
this.hot.addHookOnce('init', () => {
// Engine is initialized after executing callback to `afterLoadData` hook. Thus, some actions on indexes should
// be postponed.
postponedAction();
});
});
this.rowAxisSyncer = this.indexSyncer.getForAxis('row');
this.columnAxisSyncer = this.indexSyncer.getForAxis('column');
this.hot.addHook('afterRowSequenceChange', this.rowAxisSyncer.getIndexesChangeSyncMethod());
this.hot.addHook('afterColumnSequenceChange', this.columnAxisSyncer.getIndexesChangeSyncMethod());
this.hot.addHook('beforeRowMove', (movedRows, finalIndex, _, movePossible) => {
this.rowAxisSyncer.storeMovesInformation(movedRows, finalIndex, movePossible);
});
this.hot.addHook('beforeColumnMove', (movedColumns, finalIndex, _, movePossible) => {
this.columnAxisSyncer.storeMovesInformation(movedColumns, finalIndex, movePossible);
});
this.hot.addHook('afterRowMove', (movedRows, finalIndex, dropIndex, movePossible, orderChanged) => {
this.rowAxisSyncer.calculateAndSyncMoves(movePossible, orderChanged);
});
this.hot.addHook('afterColumnMove', (movedColumns, finalIndex, dropIndex, movePossible, orderChanged) => {
this.columnAxisSyncer.calculateAndSyncMoves(movePossible, orderChanged);
});
this.hot.addHook('beforeColumnFreeze', (column, freezePerformed) => {
this.columnAxisSyncer.storeMovesInformation([column], this.hot.getSettings().fixedColumnsStart, freezePerformed);
});
this.hot.addHook('afterColumnFreeze', (_, freezePerformed) => {
this.columnAxisSyncer.calculateAndSyncMoves(freezePerformed, freezePerformed);
});
this.hot.addHook('beforeColumnUnfreeze', (column, unfreezePerformed) => {
this.columnAxisSyncer.storeMovesInformation([column], this.hot.getSettings().fixedColumnsStart - 1, unfreezePerformed);
});
this.hot.addHook('afterColumnUnfreeze', (_, unfreezePerformed) => {
this.columnAxisSyncer.calculateAndSyncMoves(unfreezePerformed, unfreezePerformed);
});
// TODO: Actions related to overwriting dates from HOT format to HF default format are done as callback to this
// hook, because some hooks, such as `afterLoadData` doesn't have information about composed cell properties.
// Another hooks are triggered to late for setting HF's engine data needed for some actions.
this.addHook('afterCellMetaReset', function () {
for (var _len23 = arguments.length, args = new Array(_len23), _key23 = 0; _key23 < _len23; _key23++) {
args[_key23] = arguments[_key23];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterCellMetaReset).call(_this2, ...args);
});
// Handling undo actions on data just using HyperFormula's UndoRedo mechanism
this.addHook('beforeUndo', () => {
this.indexSyncer.setPerformUndo(true);
this.engine.undo();
});
// Handling redo actions on data just using HyperFormula's UndoRedo mechanism
this.addHook('beforeRedo', () => {
this.indexSyncer.setPerformRedo(true);
this.engine.redo();
});
this.addHook('afterUndo', () => {
this.indexSyncer.setPerformUndo(false);
});
this.addHook('afterUndo', () => {
this.indexSyncer.setPerformRedo(false);
});
this.addHook('afterDetachChild', function () {
for (var _len24 = arguments.length, args = new Array(_len24), _key24 = 0; _key24 < _len24; _key24++) {
args[_key24] = arguments[_key24];
}
return _assertClassBrand(_Formulas_brand, _this2, _onAfterDetachChild).call(_this2, ...args);
});
this.addHook('beforeAutofill', function () {
for (var _len25 = arguments.length, args = new Array(_len25), _key25 = 0; _key25 < _len25; _key25++) {
args[_key25] = arguments[_key25];
}
return _assertClassBrand(_Formulas_brand, _this2, _onBeforeAutofill).call(_this2, ...args);
});
_classPrivateFieldGet(_engineListeners, this).forEach(_ref => {
let [eventName, listener] = _ref;
return this.engine.on(eventName, listener);
});
super.enablePlugin();
}
/**
* Disables the plugin functionality for this Handsontable instance.
*/
disablePlugin() {
_classPrivateFieldGet(_engineListeners, this).forEach(_ref2 => {
let [eventName, listener] = _ref2;
return this.engine.off(eventName, listener);
});
unregisterEngine(this.engine, this.hot);
this.engine = null;
super.disablePlugin();
}
/**
* Triggered on `updateSettings`.
*
* @private
* @param {object} newSettings New set of settings passed to the `updateSettings` method.
*/
updatePlugin(newSettings) {
const newEngineSettings = getEngineSettingsWithOverrides(this.hot.getSettings());
if (haveEngineSettingsChanged(this.engine.getConfig(), newEngineSettings)) {
this.engine.updateConfig(newEngineSettings);
}
const pluginSettings = this.hot.getSettings()[PLUGIN_KEY];
if (isDefined(pluginSettings) && isDefined(pluginSettings.sheetName) && pluginSettings.sheetName !== this.sheetName) {
this.switchSheet(pluginSettings.sheetName);
}
// If no data was passed to the `updateSettings` method and no sheet is connected to the instance -> create a
// new sheet using the currently used data. Otherwise, it will be handled by the `afterLoadData` call.
if (!newSettings.data && this.sheetName === null) {
const sheetName = this.hot.getSettings()[PLUGIN_KEY].sheetName;
if (sheetName && this.engine.doesSheetExist(sheetName)) {
this.switchSheet(this.sheetName);
} else {
const newSheetName = this.addSheet(sheetName !== null && sheetName !== void 0 ? sheetName : undefined, this.hot.getSourceDataArray());
_assertClassBrand(_Formulas_brand, this, _updateSheetNameAndSheetId).call(this, newSheetName);
}
}
super.updatePlugin(newSettings);
}
/**
* Destroys the plugin instance.
*/
destroy() {
_classPrivateFieldGet(_engineListeners, this).forEach(_ref3 => {
var _this$engine;
let [eventName, listener] = _ref3;
return (_this$engine = this.engine) === null || _this$engine === void 0 ? void 0 : _this$engine.off(eventName, listener);
});
_classPrivateFieldSet(_engineListeners, this, null);
unregisterEngine(this.engine, this.hot);
this.engine = null;
super.destroy();
}
/**
* Add a sheet to the shared HyperFormula instance.
*
* @param {string|null} [sheetName] The new sheet name. If not provided (or a null is passed), will be
* auto-generated by HyperFormula.
* @param {Array} [sheetData] Data passed to the shared HyperFormula instance. Has to be declared as an array of
* arrays - array of objects is not supported in this scenario.
* @returns {boolean|string} `false` if the data format is unusable or it is impossible to add a new sheet to the
* engine, the created sheet name otherwise.
*/
addSheet(sheetName, sheetData) {
if (isDefined(sheetData) && !isArrayOfArrays(sheetData)) {
warn('The provided data should be an array of arrays.');
return false;
}
if (sheetName !== undefined && sheetName !== null && this.engine.doesSheetExist(sheetName)) {
warn('Sheet with the provided name already exists.');
return false;
}
try {
const actualSheetName = this.engine.addSheet(sheetName !== null && sheetName !== void 0 ? sheetName : undefined);
if (sheetData) {
this.engine.setSheetContent(this.engine.getSheetId(actualSheetName), sheetData);
}
return actualSheetName;
} catch (e) {
warn(e.message);
return false;
}
}
/**
* Switch the sheet used as data in the Handsontable instance (it loads the data from the shared HyperFormula
* instance).
*
* @param {string} sheetName Sheet name used in the shared HyperFormula instance.
*/
switchSheet(sheetName) {
if (!this.engine.doesSheetExist(sheetName)) {
error(`The sheet named \`${sheetName}\` does not exist, switch aborted.`);
return;
}
_assertClassBrand(_Formulas_brand, this, _updateSheetNameAndSheetId).call(this, sheetName);
const serialized = this.engine.getSheetSerialized(this.sheetId);
if (serialized.length > 0) {
this.hot.loadData(serialized, `${toUpperCaseFirst(PLUGIN_KEY)}.switchSheet`);
}
}
/**
* Get the cell type under specified visual coordinates.
*
* @param {number} row Visual row index.
* @param {number} column Visual column index.
* @param {number} [sheet] The target sheet id, defaults to the current sheet.
* @returns {string} Possible values: 'FORMULA' | 'VALUE' | 'ARRAYFORMULA' | 'EMPTY'.
*/
getCellType(row, column) {
let sheet = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.sheetId;
const physicalRow = this.hot.toPhysicalRow(row);
const physicalColumn = this.hot.toPhysicalColumn(column);
if (physicalRow !== null && physicalColumn !== null) {
return this.engine.getCellType({
sheet,
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(row),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(column)
});
} else {
// Should return `EMPTY` when out of bounds (according to the test cases).
return 'EMPTY';
}
}
/**
* Returns `true` if under specified visual coordinates is formula.
*
* @param {number} row Visual row index.
* @param {number} column Visual column index.
* @param {number} [sheet] The target sheet id, defaults to the current sheet.
* @returns {boolean}
*/
isFormulaCellType(row, column) {
let sheet = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.sheetId;
return this.engine.doesCellHaveFormula({
sheet,
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(row),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(column)
});
}
/**
* Renders dependent sheets (handsontable instances) based on the changes - list of the
* recalculated dependent cells.
*
* @private
* @param {object[]} dependentCells The values and location of applied changes within HF engine.
* @param {boolean} [renderSelf] `true` if it's supposed to render itself, `false` otherwise.
*/
renderDependentSheets(dependentCells) {
let renderSelf = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const affectedSheetIds = new Set();
dependentCells.forEach(change => {
var _change$address;
// For the Named expression the address is empty, hence the `sheetId` is undefined.
const sheetId = change === null || change === void 0 || (_change$address = change.address) === null || _change$address === void 0 ? void 0 : _change$address.sheet;
if (sheetId !== undefined) {
if (!affectedSheetIds.has(sheetId)) {
affectedSheetIds.add(sheetId);
}
}
});
getRegisteredHotInstances(this.engine).forEach((relatedHot, sheetId) => {
if ((renderSelf || sheetId !== this.sheetId) && affectedSheetIds.has(sheetId)) {
var _relatedHot$view;
relatedHot.render();
(_relatedHot$view = relatedHot.view) === null || _relatedHot$view === void 0 || _relatedHot$view.adjustElementsSize();
}
});
}
/**
* Validates dependent cells based on the cells that are modified by the change.
*
* @private
* @param {object[]} dependentCells The values and location of applied changes within HF engine.
* @param {object[]} [changedCells] The values and location of applied changes by developer (through API or UI).
*/
validateDependentCells(dependentCells) {
let changedCells = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
const stringifyAddress = change => {
var _change$address2;
const {
row,
col,
sheet
} = (_change$address2 = change === null || change === void 0 ? void 0 : change.address) !== null && _change$address2 !== void 0 ? _change$address2 : {};
return isDefined(sheet) ? `${sheet}:${row}x${col}` : '';
};
const changedCellsSet = new Set(changedCells.map(change => stringifyAddress(change)));
dependentCells.forEach(change => {
var _change$address3, _change$address4;
const {
row,
col
} = (_change$address3 = change.address) !== null && _change$address3 !== void 0 ? _change$address3 : {};
// Don't try to validate cells outside of the visual part of the table.
if (isDefined(row) === false || isDefined(col) === false || row >= this.hot.countRows() || col >= this.hot.countCols()) {
return;
}
// For the Named expression the address is empty, hence the `sheetId` is undefined.
const sheetId = change === null || change === void 0 || (_change$address4 = change.address) === null || _change$address4 === void 0 ? void 0 : _change$address4.sheet;
const addressId = stringifyAddress(change);
// Validate the cells that depend on the calculated formulas. Skip that cells
// where the user directly changes the values - the Core triggers those validators.
if (sheetId !== undefined && !changedCellsSet.has(addressId)) {
const boundHot = getRegisteredHotInstances(this.engine).get(sheetId);
// if `sheetId` is not bound to any Handsontable instance, skip the validation process
if (!boundHot) {
return;
}
// It will just re-render certain cell when necessary.
boundHot.validateCell(boundHot.getDataAtCell(row, col), boundHot.getCellMeta(row, col), () => {});
}
});
}
/**
* Sync a change from the change-related hooks with the engine.
*
* @private
* @param {number} row Visual row index.
* @param {number} column Visual column index.
* @param {Handsontable.CellValue} newValue New value.
* @returns {Array} Array of changes exported from the engine.
*/
syncChangeWithEngine(row, column, newValue) {
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(row),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(column),
sheet: this.sheetId
};
if (!this.engine.isItPossibleToSetCellContents(address)) {
warn(`Not possible to set cell data at ${JSON.stringify(address)}`);
return;
}
const cellMeta = this.hot.getCellMeta(row, column);
if (isDate(newValue, cellMeta.type)) {
if (isDateValid(newValue, cellMeta.dateFormat)) {
// Rewriting date in HOT format to HF format.
newValue = getDateInHfFormat(newValue, cellMeta.dateFormat);
} else if (isFormula(newValue) === false) {
// Escaping value from date parsing using "'" sign (HF feature).
newValue = `'${newValue}`;
}
}
return this.engine.setCellContents(address, newValue);
}
/**
* The hook allows to translate the formula value to calculated value before it goes to the
* validator function.
*
* @param {*} value The cell value to validate.
* @param {number} visualRow The visual row index.
* @param {number|string} prop The visual column index or property name of the column.
* @returns {*} Returns value to validate.
*/
}
function _updateSheetNameAndSheetId(sheetName) {
this.sheetName = sheetName;
this.sheetId = this.engine.getSheetId(this.sheetName);
}
function _onBeforeValidate(value, visualRow, prop) {
const visualColumn = this.hot.propToCol(prop);
if (this.isFormulaCellType(visualRow, visualColumn)) {
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn),
sheet: this.sheetId
};
const cellMeta = this.hot.getCellMeta(visualRow, visualColumn);
let cellValue = this.engine.getCellValue(address); // Date as an integer (Excel-like date).
if (cellMeta.type === 'date' && isNumeric(cellValue)) {
cellValue = getDateFromExcelDate(cellValue, cellMeta.dateFormat);
}
// If `cellValue` is an object it is expected to be an error
return typeof cellValue === 'object' && cellValue !== null ? cellValue.value : cellValue;
}
return value;
}
/**
* `onBeforeAutofill` hook callback.
*
* @param {Array[]} fillData The data that was used to fill the `targetRange`. If `beforeAutofill` was used
* and returned `[[]]`, this will be the same object that was returned from `beforeAutofill`.
* @param {CellRange} sourceRange The range values will be filled from.
* @param {CellRange} targetRange The range new values will be filled into.
* @returns {boolean|*}
*/
function _onBeforeAutofill(fillData, sourceRange, targetRange) {
const {
row: sourceTopStartRow,
col: sourceTopStartColumn
} = sourceRange.getTopStartCorner();
const {
row: sourceBottomEndRow,
col: sourceBottomEndColumn
} = sourceRange.getBottomEndCorner();
const {
row: targetTopStartRow,
col: targetTopStartColumn
} = targetRange.getTopStartCorner();
const {
row: targetBottomEndRow,
col: targetBottomEndColumn
} = targetRange.getBottomEndCorner();
const engineSourceRange = {
start: {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(sourceTopStartRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(sourceTopStartColumn),
sheet: this.sheetId
},
end: {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(sourceBottomEndRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(sourceBottomEndColumn),
sheet: this.sheetId
}
};
const engineTargetRange = {
start: {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(targetTopStartRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(targetTopStartColumn),
sheet: this.sheetId
},
end: {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(targetBottomEndRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(targetBottomEndColumn),
sheet: this.sheetId
}
};
// Blocks the autofill operation if HyperFormula says that at least one of
// the underlying cell's contents cannot be set.
if (this.engine.isItPossibleToSetCellContents(engineTargetRange) === false) {
return false;
}
const fillRangeData = this.engine.getFillRangeData(engineSourceRange, engineTargetRange);
const {
row: sourceStartRow,
col: sourceStartColumn
} = engineSourceRange.start;
const {
row: sourceEndRow,
col: sourceEndColumn
} = engineSourceRange.end;
const populationRowLength = sourceEndRow - sourceStartRow + 1;
const populationColumnLength = sourceEndColumn - sourceStartColumn + 1;
for (let populatedRowIndex = 0; populatedRowIndex < fillRangeData.length; populatedRowIndex += 1) {
for (let populatedColumnIndex = 0; populatedColumnIndex < fillRangeData[populatedRowIndex].length; populatedColumnIndex += 1) {
const populatedValue = fillRangeData[populatedRowIndex][populatedColumnIndex];
const sourceRow = sourceStartRow + populatedRowIndex % populationRowLength;
const sourceColumn = sourceStartColumn + populatedColumnIndex % populationColumnLength;
const sourceCellMeta = this.hot.getCellMeta(sourceRow, sourceColumn);
if (isDate(populatedValue, sourceCellMeta.type)) {
if (populatedValue.startsWith('\'')) {
// Populating values on HOT side without apostrophe.
fillRangeData[populatedRowIndex][populatedColumnIndex] = populatedValue.slice(1);
} else if (this.isFormulaCellType(sourceRow, sourceColumn, this.sheetId) === false) {
// Populating date in proper format, coming from the source cell.
fillRangeData[populatedRowIndex][populatedColumnIndex] = getDateInHotFormat(populatedValue, sourceCellMeta.dateFormat);
}
}
}
}
return fillRangeData;
}
/**
* `beforeLoadData` hook callback.
*
* @param {Array} sourceData Array of arrays or array of objects containing data.
* @param {boolean} initialLoad Flag that determines whether the data has been loaded during the initialization.
* @param {string} [source] Source of the call.
*/
function _onBeforeLoadData(sourceData, initialLoad) {
let source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
if (source.includes(toUpperCaseFirst(PLUGIN_KEY))) {
return;
}
// This flag needs to be defined, because not passing data to HOT results in HOT auto-generating a `null`-filled
// initial dataset.
_classPrivateFieldSet(_hotWasInitializedWithEmptyData, this, isUndefined(this.hot.getSettings().data));
}
/**
* Callback to `afterCellMetaReset` hook which is triggered after setting cell meta.
*/
function _onAfterCellMetaReset() {
if (_classPrivateFieldGet(_hotWasInitializedWithEmptyData, this)) {
this.switchSheet(this.sheetName);
return;
}
const sourceDataArray = this.hot.getSourceDataArray();
sourceDataArray.forEach((rowData, rowIndex) => {
rowData.forEach((cellValue, columnIndex) => {
const cellMeta = this.hot.getCellMeta(rowIndex, columnIndex, {
skipMetaExtension: true
});
const dateFormat = cellMeta.dateFormat;
if (isDate(cellValue, cellMeta.type)) {
if (isDateValid(cellValue, dateFormat)) {
// Rewriting date in HOT format to HF format.
sourceDataArray[rowIndex][columnIndex] = getDateInHfFormat(cellValue, dateFormat);
} else if (!cellValue.startsWith('=')) {
// Escaping value from date parsing using "'" sign (HF feature).
sourceDataArray[rowIndex][columnIndex] = `'${cellValue}`;
}
}
});
});
_classPrivateFieldSet(_internalOperationPending, this, true);
const dependentCells = this.engine.setSheetContent(this.sheetId, sourceDataArray);
this.indexSyncer.setupSyncEndpoint(this.engine, this.sheetId);
this.renderDependentSheets(dependentCells);
_classPrivateFieldSet(_internalOperationPending, this, false);
}
/**
* `afterLoadData` hook callback.
*
* @param {Array} sourceData Array of arrays or array of objects containing data.
* @param {boolean} initialLoad Flag that determines whether the data has been loaded during the initialization.
* @param {string} [source] Source of the call.
*/
function _onAfterLoadData(sourceData, initialLoad) {
let source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
if (source.includes(toUpperCaseFirst(PLUGIN_KEY))) {
return;
}
const sheetName = setupSheet(this.engine, this.hot.getSettings()[PLUGIN_KEY].sheetName);
_assertClassBrand(_Formulas_brand, this, _updateSheetNameAndSheetId).call(this, sheetName);
if (source === 'updateSettings') {
// For performance reasons, the initialization will be done in afterCellMetaReset hook
return;
}
if (!_classPrivateFieldGet(_hotWasInitializedWithEmptyData, this)) {
const sourceDataArray = this.hot.getSourceDataArray();
if (this.engine.isItPossibleToReplaceSheetContent(this.sheetId, sourceDataArray)) {
_classPrivateFieldSet(_internalOperationPending, this, true);
const dependentCells = this.engine.setSheetContent(this.sheetId, sourceDataArray);
this.indexSyncer.setupSyncEndpoint(this.engine, this.sheetId);
this.renderDependentSheets(dependentCells);
_classPrivateFieldSet(_internalOperationPending, this, false);
}
} else {
this.switchSheet(this.sheetName);
}
}
/**
* `modifyData` hook callback.
*
* @param {number} visualRow Visual row index.
* @param {number} visualColumn Visual column index.
* @param {object} valueHolder Object which contains original value which can be modified by overwriting `.value`
* property.
* @param {string} ioMode String which indicates for what operation hook is fired (`get` or `set`).
*/
function _onModifyData(visualRow, visualColumn, valueHolder, ioMode) {
if (ioMode !== 'get' || _classPrivateFieldGet(_internalOperationPending, this) || this.sheetName === null || !this.engine.doesSheetExist(this.sheetName)) {
return;
}
if (visualRow === null || visualColumn === null) {
return;
}
const cellType = this.getCellType(visualRow, visualColumn);
if (cellType === 'VALUE' || cellType === 'EMPTY') {
valueHolder.value = unescapeFormulaExpression(valueHolder.value);
return;
}
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn),
sheet: this.sheetId
};
let cellValue = this.engine.getCellValue(address); // Date as an integer (Excel like date).
const cellMeta = this.hot.getCellMeta(visualRow, visualColumn, {
skipMetaExtension: true
});
if (cellMeta.type === 'date' && isNumeric(cellValue)) {
cellValue = getDateFromExcelDate(cellValue, cellMeta.dateFormat);
}
// If `cellValue` is an object it is expected to be an error
valueHolder.value = typeof cellValue === 'object' && cellValue !== null ? cellValue.value : cellValue;
}
/**
* `modifySourceData` hook callback.
*
* @param {number} row Physical row index.
* @param {number|string} columnOrProp Physical column index or prop.
* @param {object} valueHolder Object which contains original value which can be modified by overwriting `.value`
* property.
* @param {string} ioMode String which indicates for what operation hook is fired (`get` or `set`).
*/
function _onModifySourceData(row, columnOrProp, valueHolder, ioMode) {
if (ioMode !== 'get' || _classPrivateFieldGet(_internalOperationPending, this) || this.sheetName === null || !this.engine.doesSheetExist(this.sheetName)) {
return;
}
const visualRow = this.hot.toVisualRow(row);
const visualColumn = this.hot.propToCol(columnOrProp);
if (visualRow === null || visualColumn === null) {
return;
}
const cellType = this.getCellType(visualRow, visualColumn);
if (cellType === 'VALUE' || cellType === 'EMPTY') {
return;
}
const dimensions = this.engine.getSheetDimensions(this.engine.getSheetId(this.sheetName));
// Don't actually change the source data if HyperFormula is not
// initialized yet. This is done to allow the `afterLoadData` hook to
// load the existing source data with `Handsontable#getSourceDataArray`
// properly.
if (dimensions.width === 0 && dimensions.height === 0) {
return;
}
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn),
sheet: this.sheetId
};
valueHolder.value = this.engine.getCellSerialized(address);
}
/**
* `onAfterSetDataAtCell` hook callback.
*
* @param {Array[]} changes An array of changes in format [[row, prop, oldValue, value], ...].
* @param {string} [source] String that identifies source of hook call
* ([list of all available sources]{@link https://handsontable.com/docs/javascript-data-grid/events-and-hooks/#handsontable-hooks}).
*/
function _onAfterSetDataAtCell(changes, source) {
if (isBlockedSource(source)) {
return;
}
const outOfBoundsChanges = [];
const changedCells = [];
const dependentCells = this.engine.batch(() => {
changes.forEach(_ref4 => {
let [visualRow, prop,, newValue] = _ref4;
const visualColumn = this.hot.propToCol(prop);
const physicalRow = this.hot.toPhysicalRow(visualRow);
const physicalColumn = this.hot.toPhysicalColumn(visualColumn);
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn),
sheet: this.sheetId
};
if (physicalRow !== null && physicalColumn !== null) {
this.syncChangeWithEngine(visualRow, visualColumn, newValue);
} else {
outOfBoundsChanges.push([visualRow, visualColumn, newValue]);
}
changedCells.push({
address
});
});
});
if (outOfBoundsChanges.length) {
// Workaround for rows/columns being created two times (by HOT and the engine).
// (unfortunately, this requires an extra re-render)
this.hot.addHookOnce('afterChange', () => {
const outOfBoundsDependentCells = this.engine.batch(() => {
outOfBoundsChanges.forEach(_ref5 => {
let [row, column, newValue] = _ref5;
this.syncChangeWithEngine(row, column, newValue);
});
});
this.renderDependentSheets(outOfBoundsDependentCells, true);
});
}
this.renderDependentSheets(dependentCells);
this.validateDependentCells(dependentCells, changedCells);
}
/**
* `onAfterSetSourceDataAtCell` hook callback.
*
* @param {Array[]} changes An array of changes in format [[row, column, oldValue, value], ...].
* @param {string} [source] String that identifies source of hook call
* ([list of all available sources]{@link https://handsontable.com/docs/javascript-data-grid/events-and-hooks/#handsontable-hooks}).
*/
function _onAfterSetSourceDataAtCell(changes, source) {
if (isBlockedSource(source)) {
return;
}
const dependentCells = [];
const changedCells = [];
changes.forEach(_ref6 => {
let [visualRow, prop,, newValue] = _ref6;
const visualColumn = this.hot.propToCol(prop);
if (!isNumeric(visualColumn)) {
return;
}
const address = {
row: this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow),
col: this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn),
sheet: this.sheetId
};
if (!this.engine.isItPossibleToSetCellContents(address)) {
warn(`Not possible to set source cell data at ${JSON.stringify(address)}`);
return;
}
changedCells.push({
address
});
dependentCells.push(...this.engine.setCellContents(address, newValue));
});
this.renderDependentSheets(dependentCells);
this.validateDependentCells(dependentCells, changedCells);
}
/**
* `beforeCreateRow` hook callback.
*
* @param {number} visualRow Represents the visual index of first newly created row in the data source array.
* @param {number} amount Number of newly created rows in the data source array.
* @returns {*|boolean} If false is returned the action is canceled.
*/
function _onBeforeCreateRow(visualRow, amount) {
let hfRowIndex = this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow);
if (visualRow >= this.hot.countRows()) {
hfRowIndex = visualRow; // Row beyond the table boundaries.
}
if (this.sheetId === null || !this.engine.doesSheetExist(this.sheetName) || !this.engine.isItPossibleToAddRows(this.sheetId, [hfRowIndex, amount])) {
return false;
}
}
/**
* `beforeCreateCol` hook callback.
*
* @param {number} visualColumn Represents the visual index of first newly created column in the data source.
* @param {number} amount Number of newly created columns in the data source.
* @returns {*|boolean} If false is returned the action is canceled.
*/
function _onBeforeCreateCol(visualColumn, amount) {
let hfColumnIndex = this.columnAxisSyncer.getHfIndexFromVisualIndex(visualColumn);
if (visualColumn >= this.hot.countCols()) {
hfColumnIndex = visualColumn; // Column beyond the table boundaries.
}
if (this.sheetId === null || !this.engine.doesSheetExist(this.sheetName) || !this.engine.isItPossibleToAddColumns(this.sheetId, [hfColumnIndex, amount])) {
return false;
}
}
/**
* `beforeRemoveRow` hook callback.
*
* @param {number} row Visual index of starter row.
* @param {number} amount Amount of rows to be removed.
* @param {number[]} physicalRows An array of physical rows removed from the data source.
* @returns {*|boolean} If false is returned the action is canceled.
*/
function _onBeforeRemoveRow(row, amount, physicalRows) {
const hfRows = this.rowAxisSyncer.setRemovedHfIndexes(physicalRows);
const possible = hfRows.every(hfRow => {
return this.engine.isItPossibleToRemoveRows(this.sheetId, [hfRow, 1]);
});
return possible === false ? false : undefined;
}
/**
* `beforeRemoveCol` hook callback.
*
* @param {number} col Visual index of starter column.
* @param {number} amount Amount of columns to be removed.
* @param {number[]} physicalColumns An array of physical columns removed from the data source.
* @returns {*|boolean} If false is returned the action is canceled.
*/
function _onBeforeRemoveCol(col, amount, physicalColumns) {
const hfColumns = this.columnAxisSyncer.setRemovedHfIndexes(physicalColumns);
const possible = hfColumns.every(hfColumn => {
return this.engine.isItPossibleToRemoveColumns(this.sheetId, [hfColumn, 1]);
});
return possible === false ? false : undefined;
}
/**
* `afterCreateRow` hook callback.
*
* @param {number} visualRow Represents the visual index of first newly created row in the data source array.
* @param {number} amount Number of newly created rows in the data source array.
* @param {string} [source] String that identifies source of hook call
* ([list of all available sources]{@link https://handsontable.com/docs/javascript-data-grid/events-and-hooks/#handsontable-hooks}).
*/
function _onAfterCreateRow(visualRow, amount, source) {
if (isBlockedSource(source)) {
return;
}
const changes = this.engine.addRows(this.sheetId, [this.rowAxisSyncer.getHfIndexFromVisualIndex(visualRow), amount]);
this.renderDependentSheets(changes);
}
/**
* `afterCreateCol` hook callback.
*
* @param {number} visualColumn Represents the visual index of first newly created column in the data source.
* @param {number} amount Number of newly created columns in the data source.
* @param {string} [source] String that identifies source of hook call
* ([list of all available sources]{@link https://handsontable.com/docs/javascript-data-grid/events-and-hooks/#handsontable-hooks}).
*/
function _onAfterCreateCol(visualColumn, amount, source) {
if (isBlockedSource(source))