handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
528 lines (512 loc) • 19.3 kB
JavaScript
"use strict";
exports.__esModule = true;
require("core-js/modules/es.error.cause.js");
var _base = require("../base");
var _object = require("../../helpers/object");
var _endpoints = _interopRequireDefault(require("./endpoints"));
var _templateLiteralTag = require("../../helpers/templateLiteralTag");
var _utils = require("./utils");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
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 _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"); }
const PLUGIN_KEY = exports.PLUGIN_KEY = 'columnSummary';
const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 220;
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* @plugin ColumnSummary
* @class ColumnSummary
*
* @description
* The `ColumnSummary` plugin lets you [easily summarize your columns](@/guides/columns/column-summary/column-summary.md).
*
* You can use the [built-in summary functions](@/guides/columns/column-summary/column-summary.md#built-in-summary-functions),
* or implement a [custom summary function](@/guides/columns/column-summary/column-summary.md#implement-a-custom-summary-function).
*
* For each column summary, you can set the following configuration options:
*
* | Option | Required | Type | Default | Description |
* |---|---|---|---|---|
* | `sourceColumn` | No | Number | Same as `destinationColumn` | [Selects a column to summarize](@/guides/columns/column-summary/column-summary.md#step-2-select-cells-that-you-want-to-summarize) |
* | `ranges` | No | Array | - | [Selects ranges of rows to summarize](@/guides/columns/column-summary/column-summary.md#step-2-select-cells-that-you-want-to-summarize) |
* | `type` | Yes | String | - | [Sets a summary function](@/guides/columns/column-summary/column-summary.md#step-3-calculate-your-summary) |
* | `destinationRow` | Yes | Number | - | [Sets the destination cell's row coordinate](@/guides/columns/column-summary/column-summary.md#step-4-provide-the-destination-cell-s-coordinates) |
* | `destinationColumn` | Yes | Number | - | [Sets the destination cell's column coordinate](@/guides/columns/column-summary/column-summary.md#step-4-provide-the-destination-cell-s-coordinates) |
* | `forceNumeric` | No | Boolean | `false` | [Forces the summary to treat non-numerics as numerics](@/guides/columns/column-summary/column-summary.md#force-numeric-values) |
* | `reversedRowCoords` | No | Boolean | `false` | [Reverses row coordinates](@/guides/columns/column-summary/column-summary.md#step-5-make-room-for-the-destination-cell) |
* | `suppressDataTypeErrors` | No | Boolean | `true` | [Suppresses data type errors](@/guides/columns/column-summary/column-summary.md#throw-data-type-errors) |
* | `readOnly` | No | Boolean | `true` | Makes summary cell read-only |
* | `roundFloat` | No | Number/<br>Boolean | - | [Rounds summary result](@/guides/columns/column-summary/column-summary.md#round-a-column-summary-result) |
* | `customFunction` | No | Function | - | [Lets you add a custom summary function](@/guides/columns/column-summary/column-summary.md#implement-a-custom-summary-function) |
*
* @example
* ::: only-for javascript
* ```js
* const container = document.getElementById('example');
* const hot = new Handsontable(container, {
* data: getData(),
* colHeaders: true,
* rowHeaders: true,
* columnSummary: [
* {
* type: 'min',
* destinationRow: 4,
* destinationColumn: 1,
* },
* {
* type: 'max',
* destinationRow: 0,
* destinationColumn: 3,
* reversedRowCoords: true
* },
* {
* type: 'sum',
* destinationRow: 4,
* destinationColumn: 5,
* forceNumeric: true
* }
* ]
* });
* ```
* :::
*
* ::: only-for react
* ```jsx
* <HotTable
* data={getData()}
* colHeaders={true}
* rowHeaders={true}
* columnSummary={[
* {
* type: 'min',
* destinationRow: 4,
* destinationColumn: 1,
* },
* {
* type: 'max',
* destinationRow: 0,
* destinationColumn: 3,
* reversedRowCoords: true
* },
* {
* type: 'sum',
* destinationRow: 4,
* destinationColumn: 5,
* forceNumeric: true
* }
* ]}
* />
* ```
* :::
*
* ::: only-for angular
* ```ts
* settings = {
* data: getData(),
* colHeaders: true,
* rowHeaders: true,
* columnSummary: [
* {
* type: "min",
* destinationRow: 4,
* destinationColumn: 1,
* },
* {
* type: "max",
* destinationRow: 0,
* destinationColumn: 3,
* reversedRowCoords: true,
* },
* {
* type: "sum",
* destinationRow: 4,
* destinationColumn: 5,
* forceNumeric: true,
* },
* ],
* };
* ```
*
* ```html
* <hot-table [settings]="settings"></hot-table>
* ```
* :::
*/
var _ColumnSummary_brand = /*#__PURE__*/new WeakSet();
class ColumnSummary extends _base.BasePlugin {
constructor() {
super(...arguments);
/**
* `afterInit` hook callback.
*/
_classPrivateMethodInitSpec(this, _ColumnSummary_brand);
/**
* The Endpoints class instance. Used to make all endpoint-related operations.
*
* @private
* @type {null|Endpoints}
*/
_defineProperty(this, "endpoints", null);
}
static get PLUGIN_KEY() {
return PLUGIN_KEY;
}
static get PLUGIN_PRIORITY() {
return PLUGIN_PRIORITY;
}
/**
* 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 ColumnSummary#enablePlugin} method is called.
*
* @returns {boolean}
*/
isEnabled() {
return !!this.hot.getSettings()[PLUGIN_KEY];
}
/**
* Enables the plugin functionality for this Handsontable instance.
*/
enablePlugin() {
var _this = this;
if (this.enabled) {
return;
}
this.settings = this.hot.getSettings()[PLUGIN_KEY];
this.endpoints = new _endpoints.default(this, this.settings);
this.addHook('afterInit', function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _assertClassBrand(_ColumnSummary_brand, _this, _onAfterInit).call(_this, ...args);
});
this.addHook('afterChange', function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _assertClassBrand(_ColumnSummary_brand, _this, _onAfterChange).call(_this, ...args);
});
this.addHook('afterUpdateSettings', function () {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return _assertClassBrand(_ColumnSummary_brand, _this, _onAfterUpdateSettings).call(_this, ...args);
});
this.addHook('beforeCreateRow', (index, amount, source) => this.endpoints.resetSetupBeforeStructureAlteration('insert_row', index, amount, null, source)); // eslint-disable-line max-len
this.addHook('beforeCreateCol', (index, amount, source) => this.endpoints.resetSetupBeforeStructureAlteration('insert_col', index, amount, null, source)); // eslint-disable-line max-len
this.addHook('beforeRemoveRow', function () {
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
return _this.endpoints.resetSetupBeforeStructureAlteration('remove_row', ...args);
});
this.addHook('beforeRemoveCol', function () {
for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
args[_key5] = arguments[_key5];
}
return _this.endpoints.resetSetupBeforeStructureAlteration('remove_col', ...args);
});
this.addHook('afterCreateRow', (index, amount, source) => this.endpoints.resetSetupAfterStructureAlteration('insert_row', index, amount, null, source)); // eslint-disable-line max-len
this.addHook('afterCreateCol', (index, amount, source) => this.endpoints.resetSetupAfterStructureAlteration('insert_col', index, amount, null, source)); // eslint-disable-line max-len
this.addHook('afterRemoveRow', function () {
for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
args[_key6] = arguments[_key6];
}
return _this.endpoints.resetSetupAfterStructureAlteration('remove_row', ...args);
});
this.addHook('afterRemoveCol', function () {
for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
args[_key7] = arguments[_key7];
}
return _this.endpoints.resetSetupAfterStructureAlteration('remove_col', ...args);
});
this.addHook('afterRowMove', function () {
for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
args[_key8] = arguments[_key8];
}
return _assertClassBrand(_ColumnSummary_brand, _this, _onAfterRowMove).call(_this, ...args);
});
super.enablePlugin();
}
/**
* Disables the plugin functionality for this Handsontable instance.
*/
disablePlugin() {
this.endpoints = null;
this.settings = null;
this.currentEndpoint = null;
super.disablePlugin();
}
/**
* Updates the plugin's state.
*
* This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options:
* - [`columnSummary`](@/api/options.md#columnsummary)
*/
updatePlugin() {
this.disablePlugin();
this.enablePlugin();
this.endpoints.initEndpoints();
super.updatePlugin();
}
/**
* Calculates math for a single endpoint.
*
* @private
* @param {object} endpoint Contains information about the endpoint.
*/
calculate(endpoint) {
switch (endpoint.type.toLowerCase()) {
case 'sum':
endpoint.result = this.calculateSum(endpoint);
break;
case 'min':
endpoint.result = this.calculateMinMax(endpoint, endpoint.type);
break;
case 'max':
endpoint.result = this.calculateMinMax(endpoint, endpoint.type);
break;
case 'count':
endpoint.result = this.countEntries(endpoint);
break;
case 'average':
endpoint.result = this.calculateAverage(endpoint);
break;
case 'custom':
endpoint.result = endpoint.customFunction.call(this, endpoint);
break;
default:
break;
}
}
/**
* Calculates sum of the values contained in ranges provided in the plugin config.
*
* @private
* @param {object} endpoint Contains the endpoint information.
* @returns {number} Sum for the selected range.
*/
calculateSum(endpoint) {
let sum = 0;
(0, _object.objectEach)(endpoint.ranges, range => {
sum += this.getPartialSum(range, endpoint.sourceColumn);
});
return sum;
}
/**
* Returns partial sum of values from a single row range.
*
* @private
* @param {Array} rowRange Range for the sum.
* @param {number} col Column index.
* @returns {number} The partial sum.
*/
getPartialSum(rowRange, col) {
let sum = 0;
let i = rowRange[1] || rowRange[0];
let cellValue = null;
let biggestDecimalPlacesCount = 0;
do {
cellValue = this.getCellValue(i, col);
cellValue = (0, _utils.isNullishOrNaN)(cellValue) ? null : cellValue;
if (cellValue !== null) {
const decimalPlaces = (`${cellValue}`.split('.')[1] || []).length || 1;
if (decimalPlaces > biggestDecimalPlacesCount) {
biggestDecimalPlacesCount = decimalPlaces;
}
}
sum += cellValue || 0;
i -= 1;
} while (i >= rowRange[0]);
// Workaround for e.g. 802.2 + 1.1 = 803.3000000000001
return Math.round(sum * 10 ** biggestDecimalPlacesCount) / 10 ** biggestDecimalPlacesCount;
}
/**
* Calculates the minimal value for the selected ranges.
*
* @private
* @param {object} endpoint Contains the endpoint information.
* @param {string} type `'min'` or `'max'`.
* @returns {number} Min or Max value.
*/
calculateMinMax(endpoint, type) {
let result = null;
(0, _object.objectEach)(endpoint.ranges, range => {
const partialResult = this.getPartialMinMax(range, endpoint.sourceColumn, type);
if (result === null && partialResult !== null) {
result = partialResult;
}
if (partialResult !== null) {
switch (type) {
case 'min':
result = Math.min(result, partialResult);
break;
case 'max':
result = Math.max(result, partialResult);
break;
default:
break;
}
}
});
return result === null ? 'Not enough data' : result;
}
/**
* Returns a local minimum of the provided sub-range.
*
* @private
* @param {Array} rowRange Range for the calculation.
* @param {number} col Column index.
* @param {string} type `'min'` or `'max'`.
* @returns {number|null} Min or max value.
*/
getPartialMinMax(rowRange, col, type) {
let result = null;
let i = rowRange[1] || rowRange[0];
let cellValue;
do {
cellValue = this.getCellValue(i, col);
cellValue = (0, _utils.isNullishOrNaN)(cellValue) ? null : cellValue;
if (result === null) {
result = cellValue;
} else if (cellValue !== null) {
switch (type) {
case 'min':
result = Math.min(result, cellValue);
break;
case 'max':
result = Math.max(result, cellValue);
break;
default:
break;
}
}
i -= 1;
} while (i >= rowRange[0]);
return result;
}
/**
* Counts empty cells in the provided row range.
*
* @private
* @param {Array} rowRange Row range for the calculation.
* @param {number} col Column index.
* @returns {number} Empty cells count.
*/
countEmpty(rowRange, col) {
let cellValue;
let counter = 0;
let i = rowRange[1] || rowRange[0];
do {
cellValue = this.getCellValue(i, col);
cellValue = (0, _utils.isNullishOrNaN)(cellValue) ? null : cellValue;
if (cellValue === null) {
counter += 1;
}
i -= 1;
} while (i >= rowRange[0]);
return counter;
}
/**
* Counts non-empty cells in the provided row range.
*
* @private
* @param {object} endpoint Contains the endpoint information.
* @returns {number} Entry count.
*/
countEntries(endpoint) {
let result = 0;
const ranges = endpoint.ranges;
(0, _object.objectEach)(ranges, range => {
const partial = range[1] === undefined ? 1 : range[1] - range[0] + 1;
const emptyCount = this.countEmpty(range, endpoint.sourceColumn);
result += partial;
result -= emptyCount;
});
return result;
}
/**
* Calculates the average value from the cells in the range.
*
* @private
* @param {object} endpoint Contains the endpoint information.
* @returns {number} Avarage value.
*/
calculateAverage(endpoint) {
const sum = this.calculateSum(endpoint);
const entriesCount = this.countEntries(endpoint);
return sum / entriesCount;
}
/**
* Returns a cell value, taking into consideration a basic validation.
*
* @private
* @param {number} row Row index.
* @param {number} col Column index.
* @returns {string} The cell value.
*/
getCellValue(row, col) {
const visualRowIndex = this.hot.toVisualRow(row);
const visualColumnIndex = this.hot.toVisualColumn(col);
let cellValue = this.hot.getSourceDataAtCell(row, col);
let cellClassName = '';
if (visualRowIndex !== null && visualColumnIndex !== null) {
cellClassName = this.hot.getCellMeta(visualRowIndex, visualColumnIndex).className || '';
}
if (cellClassName.indexOf('columnSummaryResult') > -1) {
return null;
}
if (this.endpoints.currentEndpoint.forceNumeric) {
if (typeof cellValue === 'string') {
cellValue = cellValue.replace(/,/, '.');
}
cellValue = parseFloat(cellValue);
}
if (isNaN(cellValue)) {
if (!this.endpoints.currentEndpoint.suppressDataTypeErrors) {
throw new Error((0, _templateLiteralTag.toSingleLine)`ColumnSummary plugin: cell at (${row}, ${col}) is not in a\x20
numeric format. Cannot do the calculation.`);
}
}
return cellValue;
}
}
exports.ColumnSummary = ColumnSummary;
function _onAfterInit() {
this.endpoints.initEndpoints();
}
/**
* Called after the settings were updated. There is a need to refresh cell metas after the settings update with
* the `columns` property as the Core resets the cell metas to their initial state.
*
* @param {object} settings The settings object.
*/
function _onAfterUpdateSettings(settings) {
if (settings.columns !== undefined) {
this.endpoints.refreshCellMetas();
}
}
/**
* `afterChange` hook callback.
*
* @param {Array} changes 2D array containing information about each of the edited cells.
* @param {string} source The string that identifies source of changes.
*/
function _onAfterChange(changes, source) {
if (changes && source !== 'ColumnSummary.reset' && source !== 'ColumnSummary.set' && source !== 'loadData') {
this.endpoints.refreshChangedEndpoints(changes);
}
}
/**
* `beforeRowMove` hook callback.
*
* @param {Array} rows Array of visual row indexes to be moved.
* @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements will be placed after the moving action.
* To check the visualization of the final index, please take a look at [documentation](@/guides/rows/row-moving/row-moving.md).
*/
function _onAfterRowMove(rows, finalIndex) {
this.endpoints.resetSetupBeforeStructureAlteration('move_row', rows[0], rows.length, rows, this.pluginName);
this.endpoints.resetSetupAfterStructureAlteration('move_row', finalIndex, rows.length, rows, this.pluginName);
}