UNPKG

handsontable

Version:

Handsontable is a JavaScript Spreadsheet Component available for React, Angular and Vue.

409 lines (349 loc) • 11.7 kB
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } import { Parser, ERROR_REF, error as isFormulaError } from 'hot-formula-parser'; import { arrayEach, arrayMap } from "../../helpers/array.mjs"; import localHooks from "../../mixins/localHooks.mjs"; import { mixin } from "../../helpers/object.mjs"; import CellValue from "./cell/value.mjs"; import CellReference from "./cell/reference.mjs"; import { isFormulaExpression, toUpperCaseFormula } from "./utils.mjs"; import Matrix from "./matrix.mjs"; import AlterManager from "./alterManager.mjs"; var STATE_UP_TO_DATE = 1; var STATE_NEED_REBUILD = 2; var STATE_NEED_FULL_REBUILD = 3; /** * Sheet component responsible for whole spreadsheet calculations. * * @class Sheet * @util */ var Sheet = /*#__PURE__*/function () { function Sheet(hot, dataProvider) { var _this = this; _classCallCheck(this, Sheet); /** * Handsontable instance. * * @type {Core} */ this.hot = hot; /** * Data provider for sheet calculations. * * @type {DataProvider} */ this.dataProvider = dataProvider; /** * Instance of {@link https://github.com/handsontable/formula-parser}. * * @type {Parser} */ this.parser = new Parser(); /** * Instance of {@link Matrix}. * * @type {Matrix} */ this.matrix = new Matrix(this.hot); /** * Instance of {@link AlterManager}. * * @type {AlterManager} */ this.alterManager = new AlterManager(this); /** * Cell object which indicates which cell is currently processing. * * @private * @type {null} */ this._processingCell = null; /** * State of the sheet. * * @type {number} * @private */ this._state = STATE_NEED_FULL_REBUILD; this.parser.on('callCellValue', function () { return _this._onCallCellValue.apply(_this, arguments); }); this.parser.on('callRangeValue', function () { return _this._onCallRangeValue.apply(_this, arguments); }); this.alterManager.addLocalHook('afterAlter', function () { return _this._onAfterAlter.apply(_this, arguments); }); } /** * Recalculate sheet. */ _createClass(Sheet, [{ key: "recalculate", value: function recalculate() { switch (this._state) { case STATE_NEED_FULL_REBUILD: this.recalculateFull(); break; case STATE_NEED_REBUILD: this.recalculateOptimized(); break; default: break; } } /** * Recalculate sheet using optimized methods (fast recalculation). */ }, { key: "recalculateOptimized", value: function recalculateOptimized() { var _this2 = this; var cells = this.matrix.getOutOfDateCells(); arrayEach(cells, function (cellValue) { var value = _this2.dataProvider.getSourceDataAtCell(cellValue.row, cellValue.column); if (isFormulaExpression(value)) { _this2.parseExpression(cellValue, value.substr(1)); } }); this._state = STATE_UP_TO_DATE; this.runLocalHooks('afterRecalculate', cells, 'optimized'); } /** * Recalculate whole table by building dependencies from scratch (slow recalculation). */ }, { key: "recalculateFull", value: function recalculateFull() { var _this3 = this; var cells = this.dataProvider.getSourceDataByRange(); this.matrix.reset(); arrayEach(cells, function (rowData, row) { arrayEach(rowData, function (value, column) { if (isFormulaExpression(value)) { _this3.parseExpression(new CellValue(row, column), value.substr(1)); } }); }); this._state = STATE_UP_TO_DATE; this.runLocalHooks('afterRecalculate', cells, 'full'); } /** * Set predefined variable name which can be visible while parsing formula expression. * * @param {string} name Variable name. * @param {*} value Variable value. */ }, { key: "setVariable", value: function setVariable(name, value) { this.parser.setVariable(name, value); } /** * Get variable name. * * @param {string} name Variable name. * @returns {*} */ }, { key: "getVariable", value: function getVariable(name) { return this.parser.getVariable(name); } /** * Apply changes to the sheet. * * @param {number} row Physical row index. * @param {number} column Physical column index. * @param {*} newValue Current cell value. */ }, { key: "applyChanges", value: function applyChanges(row, column, newValue) { // Remove formula description for old expression // TODO: Move this to recalculate() this.matrix.remove({ row: row, column: column }); // TODO: Move this to recalculate() if (isFormulaExpression(newValue)) { // ...and create new for new changed formula expression this.parseExpression(new CellValue(row, column), newValue.substr(1)); } var deps = this.getCellDependencies(this.hot.toVisualRow(row), this.hot.toVisualColumn(column)); arrayEach(deps, function (cellValue) { cellValue.setState(CellValue.STATE_OUT_OFF_DATE); }); this._state = STATE_NEED_REBUILD; } /** * Parse and evaluate formula for provided cell. * * @param {CellValue|object} cellValue Cell value object. * @param {string} formula Value to evaluate. */ }, { key: "parseExpression", value: function parseExpression(cellValue, formula) { cellValue.setState(CellValue.STATE_COMPUTING); this._processingCell = cellValue; var _this$parser$parse = this.parser.parse(toUpperCaseFormula(formula)), error = _this$parser$parse.error, result = _this$parser$parse.result; if (isFormulaExpression(result)) { this.parseExpression(cellValue, result.substr(1)); } else { cellValue.setValue(result); cellValue.setError(error); cellValue.setState(CellValue.STATE_UP_TO_DATE); } this.matrix.add(cellValue); this._processingCell = null; } /** * Get cell value object at specified physical coordinates. * * @param {number} row Physical row index. * @param {number} column Physical column index. * @returns {CellValue|undefined} */ }, { key: "getCellAt", value: function getCellAt(row, column) { return this.matrix.getCellAt(row, column); } /** * Get cell dependencies at specified physical coordinates. * * @param {number} row Physical row index. * @param {number} column Physical column index. * @returns {Array} */ }, { key: "getCellDependencies", value: function getCellDependencies(row, column) { return this.matrix.getDependencies({ row: row, column: column }); } /** * Listener for parser cell value. * * @private * @param {object} cellCoords Cell coordinates. * @param {Function} done Function to call with valid cell value. */ }, { key: "_onCallCellValue", value: function _onCallCellValue(_ref, done) { var row = _ref.row, column = _ref.column; var cell = new CellReference(row, column); if (!this.dataProvider.isInDataRange(cell.row, cell.column)) { throw Error(ERROR_REF); } this.matrix.registerCellRef(cell); this._processingCell.addPrecedent(cell); var cellValue = this.dataProvider.getRawDataAtCell(row.index, column.index); if (isFormulaError(cellValue)) { var computedCell = this.matrix.getCellAt(row.index, column.index); if (computedCell && computedCell.hasError()) { throw Error(cellValue); } } if (isFormulaExpression(cellValue)) { var _this$parser$parse2 = this.parser.parse(cellValue.substr(1)), error = _this$parser$parse2.error, result = _this$parser$parse2.result; if (error) { throw Error(error); } done(result); } else { done(cellValue); } } /** * Listener for parser cells (range) value. * * @private * @param {object} startCell Cell coordinates (top-left corner coordinate). * @param {object} endCell Cell coordinates (bottom-right corner coordinate). * @param {Function} done Function to call with valid cells values. */ }, { key: "_onCallRangeValue", value: function _onCallRangeValue(_ref2, _ref3, done) { var _this4 = this; var startRow = _ref2.row, startColumn = _ref2.column; var endRow = _ref3.row, endColumn = _ref3.column; var cellValues = this.dataProvider.getRawDataByRange(startRow.index, startColumn.index, endRow.index, endColumn.index); var mapRowData = function mapRowData(rowData, rowIndex) { return arrayMap(rowData, function (cellData, columnIndex) { var rowCellCoord = startRow.index + rowIndex; var columnCellCoord = startColumn.index + columnIndex; var cell = new CellReference(rowCellCoord, columnCellCoord); if (!_this4.dataProvider.isInDataRange(cell.row, cell.column)) { throw Error(ERROR_REF); } _this4.matrix.registerCellRef(cell); _this4._processingCell.addPrecedent(cell); var newCellData = cellData; if (isFormulaError(newCellData)) { var computedCell = _this4.matrix.getCellAt(cell.row, cell.column); if (computedCell && computedCell.hasError()) { throw Error(newCellData); } } if (isFormulaExpression(newCellData)) { var _this4$parser$parse = _this4.parser.parse(newCellData.substr(1)), error = _this4$parser$parse.error, result = _this4$parser$parse.result; if (error) { throw Error(error); } newCellData = result; } return newCellData; }); }; var calculatedCellValues = arrayMap(cellValues, function (rowData, rowIndex) { return mapRowData(rowData, rowIndex); }); done(calculatedCellValues); } /** * On after alter sheet listener. * * @private */ }, { key: "_onAfterAlter", value: function _onAfterAlter() { this.recalculateOptimized(); } /** * Destroy class. */ }, { key: "destroy", value: function destroy() { this.hot = null; this.dataProvider.destroy(); this.dataProvider = null; this.alterManager.destroy(); this.alterManager = null; this.parser = null; this.matrix.reset(); this.matrix = null; } }]); return Sheet; }(); mixin(Sheet, localHooks); export default Sheet;