handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
528 lines (507 loc) • 19 kB
JavaScript
"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;