UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

528 lines (507 loc) • 19 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.array.push.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.for-each.js"); require("core-js/modules/esnext.iterator.some.js"); var _array = require("../../helpers/array"); var _console = require("../../helpers/console"); var _utils = require("./utils"); 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); } /** * Class used to make all endpoint-related operations. * * @private * @class Endpoints */ class Endpoints { constructor(plugin, settings) { /** * The main plugin instance. */ _defineProperty(this, "plugin", void 0); /** * Handsontable instance. * * @type {object} */ _defineProperty(this, "hot", void 0); /** * Array of declared plugin endpoints (calculation destination points). * * @type {Array} * @default {Array} Empty array. */ _defineProperty(this, "endpoints", []); /** * The plugin settings, taken from Handsontable configuration. * * @type {object|Function} * @default null */ _defineProperty(this, "settings", void 0); /** * Settings type. Can be either 'array' or 'function'. * * @type {string} * @default {'array'} */ _defineProperty(this, "settingsType", 'array'); /** * The current endpoint (calculation destination point) in question. * * @type {object} * @default null */ _defineProperty(this, "currentEndpoint", null); /** * Array containing a list of changes to be applied. * * @private * @type {Array} * @default {[]} */ _defineProperty(this, "cellsToSetCache", []); this.plugin = plugin; this.hot = this.plugin.hot; this.settings = settings; } /** * Initialize the endpoints provided in the settings. */ initEndpoints() { this.endpoints = this.parseSettings(); this.refreshAllEndpoints(); } /** * Get a single endpoint object. * * @param {number} index Index of the endpoint. * @returns {object} */ getEndpoint(index) { if (this.settingsType === 'function') { return this.fillMissingEndpointData(this.settings)[index]; } return this.endpoints[index]; } /** * Get an array with all the endpoints. * * @returns {Array} */ getAllEndpoints() { if (this.settingsType === 'function') { return this.fillMissingEndpointData(this.settings); } return this.endpoints; } /** * Used to fill the blanks in the endpoint data provided by a settings function. * * @private * @param {Function} func Function provided in the HOT settings. * @returns {Array} An array of endpoints. */ fillMissingEndpointData(func) { return this.parseSettings(func.call(this)); } /** * Parse plugin's settings. * * @param {Array} settings The settings array. * @returns {object[]} */ parseSettings(settings) { const endpointsArray = []; let settingsArray = settings; if (!settingsArray && typeof this.settings === 'function') { this.settingsType = 'function'; return; } if (!settingsArray) { settingsArray = this.settings; } (0, _array.arrayEach)(settingsArray, val => { const newEndpoint = {}; this.assignSetting(val, newEndpoint, 'ranges', [[0, this.hot.countRows() - 1]]); this.assignSetting(val, newEndpoint, 'reversedRowCoords', false); this.assignSetting(val, newEndpoint, 'destinationRow', new Error(` You must provide a destination row for the Column Summary plugin in order to work properly! `)); this.assignSetting(val, newEndpoint, 'destinationColumn', new Error(` You must provide a destination column for the Column Summary plugin in order to work properly! `)); this.assignSetting(val, newEndpoint, 'sourceColumn', val.destinationColumn); this.assignSetting(val, newEndpoint, 'type', 'sum'); this.assignSetting(val, newEndpoint, 'forceNumeric', false); this.assignSetting(val, newEndpoint, 'suppressDataTypeErrors', true); this.assignSetting(val, newEndpoint, 'customFunction', null); this.assignSetting(val, newEndpoint, 'readOnly', true); this.assignSetting(val, newEndpoint, 'roundFloat', false); endpointsArray.push(newEndpoint); }); return endpointsArray; } /** * Setter for the internal setting objects. * * @param {object} settings Object with the settings. * @param {object} endpoint Contains information about the endpoint for the the calculation. * @param {string} name Settings name. * @param {object} defaultValue Default value for the settings. */ assignSetting(settings, endpoint, name, defaultValue) { if (name === 'ranges' && settings[name] === undefined) { endpoint[name] = defaultValue; return; } else if (name === 'ranges' && settings[name].length === 0) { return; } if (settings[name] === undefined) { if (defaultValue instanceof Error) { throw defaultValue; } endpoint[name] = defaultValue; } else { /* eslint-disable no-lonely-if */ if (name === 'destinationRow' && endpoint.reversedRowCoords) { endpoint[name] = this.hot.countRows() - settings[name] - 1; } else { endpoint[name] = settings[name]; } } } /** * Resets the endpoint setup before the structure alteration (like inserting or removing rows/columns). Used for settings provided as a function. * * @private * @param {string} action Type of the action performed. * @param {number} index Row/column index. * @param {number} number Number of rows/columns added/removed. */ resetSetupBeforeStructureAlteration(action, index, number) { if (this.settingsType !== 'function') { return; } const type = action.indexOf('row') > -1 ? 'row' : 'col'; const endpoints = this.getAllEndpoints(); (0, _array.arrayEach)(endpoints, val => { if (type === 'row' && val.destinationRow >= index) { if (action === 'insert_row') { val.alterRowOffset = number; } else if (action === 'remove_row') { val.alterRowOffset = -1 * number; } } if (type === 'col' && val.destinationColumn >= index) { if (action === 'insert_col') { val.alterColumnOffset = number; } else if (action === 'remove_col') { val.alterColumnOffset = -1 * number; } } }); this.resetAllEndpoints(endpoints, false); } /** * AfterCreateRow/afterCreateRow/afterRemoveRow/afterRemoveCol hook callback. Reset and reenables the summary functionality * after changing the table structure. * * @private * @param {string} action Type of the action performed. * @param {number} index Row/column index. * @param {number} number Number of rows/columns added/removed. * @param {Array} [logicRows] Array of the logical indexes. * @param {string} [source] Source of change. * @param {boolean} [forceRefresh] `true` of the endpoints should refresh after completing the function. */ resetSetupAfterStructureAlteration(action, index, number, logicRows, source) { let forceRefresh = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true; if (this.settingsType === 'function') { // We need to run it on a next avaiable hook, because the TrimRows' `afterCreateRow` hook triggers after this one, // and it needs to be run to properly calculate the endpoint value. const beforeViewRenderCallback = () => { this.hot.removeHook('beforeViewRender', beforeViewRenderCallback); return this.refreshAllEndpoints(); }; this.hot.addHookOnce('beforeViewRender', beforeViewRenderCallback); return; } const type = action.indexOf('row') > -1 ? 'row' : 'col'; const multiplier = action.indexOf('remove') > -1 ? -1 : 1; const endpoints = this.getAllEndpoints(); const rowMoving = action.indexOf('move_row') === 0; const placeOfAlteration = index; (0, _array.arrayEach)(endpoints, val => { if (type === 'row' && val.destinationRow >= placeOfAlteration) { val.alterRowOffset = multiplier * number; } if (type === 'col' && val.destinationColumn >= placeOfAlteration) { val.alterColumnOffset = multiplier * number; } }); this.resetAllEndpoints(endpoints, !rowMoving); if (rowMoving) { (0, _array.arrayEach)(endpoints, endpoint => { this.extendEndpointRanges(endpoint, placeOfAlteration, logicRows[0], logicRows.length); this.recreatePhysicalRanges(endpoint); this.clearOffsetInformation(endpoint); }); } else { (0, _array.arrayEach)(endpoints, endpoint => { this.shiftEndpointCoordinates(endpoint, placeOfAlteration); }); } if (forceRefresh) { this.refreshAllEndpoints(); } } /** * Clear the offset information from the endpoint object. * * @private * @param {object} endpoint And endpoint object. */ clearOffsetInformation(endpoint) { endpoint.alterRowOffset = undefined; endpoint.alterColumnOffset = undefined; } /** * Extend the row ranges for the provided endpoint. * * @private * @param {object} endpoint The endpoint object. * @param {number} placeOfAlteration Index of the row where the alteration takes place. * @param {number} previousPosition Previous endpoint result position. * @param {number} offset Offset generated by the alteration. */ extendEndpointRanges(endpoint, placeOfAlteration, previousPosition, offset) { (0, _array.arrayEach)(endpoint.ranges, range => { // is a range, not a single row if (range[1]) { if (placeOfAlteration >= range[0] && placeOfAlteration <= range[1]) { if (previousPosition > range[1]) { range[1] += offset; } else if (previousPosition < range[0]) { range[0] -= offset; } } else if (previousPosition >= range[0] && previousPosition <= range[1]) { range[1] -= offset; if (placeOfAlteration <= range[0]) { range[0] += 1; range[1] += 1; } } } }); } /** * Recreate the physical ranges for the provided endpoint. Used (for example) when a row gets moved and extends an existing range. * * @private * @param {object} endpoint An endpoint object. */ recreatePhysicalRanges(endpoint) { const ranges = endpoint.ranges; const newRanges = []; const allIndexes = []; (0, _array.arrayEach)(ranges, range => { const newRange = []; if (range[1]) { for (let i = range[0]; i <= range[1]; i++) { newRange.push(this.hot.toPhysicalRow(i)); } } else { newRange.push(this.hot.toPhysicalRow(range[0])); } allIndexes.push(newRange); }); (0, _array.arrayEach)(allIndexes, range => { let newRange = []; (0, _array.arrayEach)(range, (coord, index) => { if (index === 0) { newRange.push(coord); } else if (range[index] !== range[index - 1] + 1) { newRange.push(range[index - 1]); newRanges.push(newRange); newRange = []; newRange.push(coord); } if (index === range.length - 1) { newRange.push(coord); newRanges.push(newRange); } }); }); endpoint.ranges = newRanges; } /** * Shifts the endpoint coordinates by the defined offset. * * @private * @param {object} endpoint Endpoint object. * @param {number} offsetStartIndex Index of the performed change (if the change is located after the endpoint, nothing about the endpoint has to be changed. */ shiftEndpointCoordinates(endpoint, offsetStartIndex) { if (endpoint.alterRowOffset && endpoint.alterRowOffset !== 0) { endpoint.destinationRow += endpoint.alterRowOffset || 0; (0, _array.arrayEach)(endpoint.ranges, element => { (0, _array.arrayEach)(element, (subElement, j) => { if (subElement >= offsetStartIndex) { element[j] += endpoint.alterRowOffset || 0; } }); }); } else if (endpoint.alterColumnOffset && endpoint.alterColumnOffset !== 0) { endpoint.destinationColumn += endpoint.alterColumnOffset || 0; endpoint.sourceColumn += endpoint.alterColumnOffset || 0; } } /** * Resets (removes) the endpoints from the table. * * @param {Array} [endpoints] Array containing the endpoints. * @param {boolean} [useOffset=true] Use the cell offset value. */ resetAllEndpoints() { let endpoints = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getAllEndpoints(); let useOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; const anyEndpointOutOfRange = endpoints.some(endpoint => { const alterRowOffset = endpoint.alterRowOffset || 0; const alterColOffset = endpoint.alterColumnOffset || 0; if (endpoint.destinationRow + alterRowOffset >= this.hot.countRows() || endpoint.destinationColumn + alterColOffset >= this.hot.countCols()) { return true; } return false; }); if (anyEndpointOutOfRange) { return; } this.cellsToSetCache = []; (0, _array.arrayEach)(endpoints, endpoint => { this.resetEndpointValue(endpoint, useOffset); }); this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset'); this.cellsToSetCache = []; } /** * Calculate and refresh all defined endpoints. */ refreshAllEndpoints() { this.cellsToSetCache = []; (0, _array.arrayEach)(this.getAllEndpoints(), value => { this.currentEndpoint = value; this.plugin.calculate(value); this.setEndpointValue(value, 'init'); }); this.currentEndpoint = null; this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset'); this.cellsToSetCache = []; } /** * Calculate and refresh endpoints only in the changed columns. * * @param {Array} changes Array of changes from the `afterChange` hook. */ refreshChangedEndpoints(changes) { const needToRefresh = []; this.cellsToSetCache = []; (0, _array.arrayEach)(changes, (value, key, changesObj) => { // if nothing changed, dont update anything if (`${value[2] || ''}` === `${value[3]}`) { return; } (0, _array.arrayEach)(this.getAllEndpoints(), (endpoint, j) => { if (this.hot.propToCol(changesObj[key][1]) === endpoint.sourceColumn && needToRefresh.indexOf(j) === -1) { needToRefresh.push(j); } }); }); (0, _array.arrayEach)(needToRefresh, value => { this.refreshEndpoint(this.getEndpoint(value)); }); this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset'); this.cellsToSetCache = []; } /** * Refreshes the cell meta information for the all endpoints after the `updateSettings` method call which in some * cases (call with `columns` option) can reset the cell metas to the initial state. */ refreshCellMetas() { this.endpoints.forEach(endpoint => { const destinationVisualRow = this.hot.toVisualRow(endpoint.destinationRow); if (destinationVisualRow !== null) { const cellMeta = this.hot.getCellMeta(destinationVisualRow, endpoint.destinationColumn); cellMeta.readOnly = endpoint.readOnly; cellMeta.className = 'columnSummaryResult'; } }); } /** * Calculate and refresh a single endpoint. * * @param {object} endpoint Contains the endpoint information. */ refreshEndpoint(endpoint) { this.currentEndpoint = endpoint; this.plugin.calculate(endpoint); this.setEndpointValue(endpoint); this.currentEndpoint = null; } /** * Reset the endpoint value. * * @param {object} endpoint Contains the endpoint information. * @param {boolean} [useOffset=true] Use the cell offset value. */ resetEndpointValue(endpoint) { let useOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; const alterRowOffset = endpoint.alterRowOffset || 0; const alterColOffset = endpoint.alterColumnOffset || 0; this.cellsToSetCache.push([this.hot.toVisualRow(endpoint.destinationRow + (useOffset ? alterRowOffset : 0)), this.hot.toVisualColumn(endpoint.destinationColumn + (useOffset ? alterColOffset : 0)), '']); } /** * Set the endpoint value. * * @param {object} endpoint Contains the endpoint information. * @param {string} [source] Source of the call information. * @param {boolean} [render=false] `true` if it needs to render the table afterwards. */ setEndpointValue(endpoint, source) { let render = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; const visualEndpointRowIndex = this.hot.toVisualRow(endpoint.destinationRow); if (endpoint.destinationRow >= this.hot.countRows() || endpoint.destinationColumn >= this.hot.countCols()) { this.throwOutOfBoundsWarning(); return; } const destinationVisualRow = this.hot.toVisualRow(endpoint.destinationRow); if (destinationVisualRow !== null) { const cellMeta = this.hot.getCellMeta(destinationVisualRow, endpoint.destinationColumn); if (source === 'init' || cellMeta.readOnly !== endpoint.readOnly) { cellMeta.readOnly = endpoint.readOnly; cellMeta.className = 'columnSummaryResult'; } } endpoint.result = (0, _utils.roundFloat)(endpoint.result, endpoint.roundFloat); if (render) { this.hot.setDataAtCell(visualEndpointRowIndex, endpoint.destinationColumn, endpoint.result, 'ColumnSummary.set'); } else { this.cellsToSetCache.push([visualEndpointRowIndex, endpoint.destinationColumn, endpoint.result]); } endpoint.alterRowOffset = undefined; endpoint.alterColumnOffset = undefined; } /** * Throw an error for the calculation range being out of boundaries. * * @private */ throwOutOfBoundsWarning() { (0, _console.warn)('One of the Column Summary plugins\' destination points you provided is beyond the table boundaries!'); } } var _default = exports.default = Endpoints;