handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
527 lines (511 loc) • 20.1 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/es.array.unscopables.flat-map.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.filter.js";
import "core-js/modules/esnext.iterator.flat-map.js";
import "core-js/modules/esnext.iterator.for-each.js";
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"); }
import MergedCellCoords from "./cellCoords.mjs";
import { rangeEach, clamp } from "../../helpers/number.mjs";
import { warn } from "../../helpers/console.mjs";
import { arrayEach } from "../../helpers/array.mjs";
import { toSingleLine } from "../../helpers/templateLiteralTag.mjs";
/**
* Defines a container object for the merged cells.
*
* @private
* @class MergedCellsCollection
*/
var _MergedCellsCollection_brand = /*#__PURE__*/new WeakSet();
class MergedCellsCollection {
constructor(mergeCellsPlugin) {
/**
* Gets the list of the indexes that do not intersect with other merged cells within the provided range.
*
* @param {CellRange} range The range to search within.
* @param {'row' | 'col'} axis The axis to search within.
* @param {number} scanDirection The direction to scan the range. `1` for forward, `-1` for backward.
* @returns {number[]}
*/
_classPrivateMethodInitSpec(this, _MergedCellsCollection_brand);
/**
* Reference to the Merge Cells plugin.
*
* @type {MergeCells}
*/
_defineProperty(this, "plugin", void 0);
/**
* Array of merged cells.
*
* @type {MergedCellCoords[]}
*/
_defineProperty(this, "mergedCells", []);
/**
* Matrix of cells (row, col) that points to the instances of the MergedCellCoords objects.
*
* @type {Array}
*/
_defineProperty(this, "mergedCellsMatrix", new Map());
/**
* The Handsontable instance.
*
* @type {Handsontable}
*/
_defineProperty(this, "hot", void 0);
this.plugin = mergeCellsPlugin;
this.hot = mergeCellsPlugin.hot;
}
/**
* Get a warning message for when the declared merged cell data overlaps already existing merged cells.
*
* @param {{ row: number, col: number, rowspan: number, colspan: number }} mergedCell Object containing information
* about the merged cells that was about to be added.
* @returns {string}
*/
static IS_OVERLAPPING_WARNING(_ref) {
let {
row,
col
} = _ref;
return toSingleLine`The merged cell declared at [${row}, ${col}], overlaps\x20
with the other declared merged cell. The overlapping merged cell was not added to the table, please\x20
fix your setup.`;
}
/**
* Get a merged cell from the container, based on the provided arguments. You can provide either the "starting coordinates"
* of a merged cell, or any coordinates from the body of the merged cell.
*
* @param {number} row Row index.
* @param {number} column Column index.
* @returns {MergedCellCoords|boolean} Returns a wanted merged cell on success and `false` on failure.
*/
get(row, column) {
var _this$mergedCellsMatr;
if (!this.mergedCellsMatrix.has(row)) {
return false;
}
return (_this$mergedCellsMatr = this.mergedCellsMatrix.get(row).get(column)) !== null && _this$mergedCellsMatr !== void 0 ? _this$mergedCellsMatr : false;
}
/**
* Get the first-found merged cell containing the provided range.
*
* @param {CellRange} range The range to search merged cells for.
* @returns {MergedCellCoords | false}
*/
getByRange(range) {
const {
row: rowStart,
col: columnStart
} = range.getTopStartCorner();
const {
row: rowEnd,
col: columnEnd
} = range.getBottomEndCorner();
const mergedCellsLength = this.mergedCells.length;
let result = false;
for (let i = 0; i < mergedCellsLength; i++) {
const mergedCell = this.mergedCells[i];
const {
row,
col,
rowspan,
colspan
} = mergedCell;
if (row >= rowStart && row + rowspan - 1 <= rowEnd && col >= columnStart && col + colspan - 1 <= columnEnd) {
result = mergedCell;
break;
}
}
return result;
}
/**
* Filters merge cells objects provided by users from overlapping cells.
*
* @param {{ row: number, col: number, rowspan: number, colspan: number }} mergedCellsInfo The merged cell information object.
* Has to contain `row`, `col`, `colspan` and `rowspan` properties.
* @returns {Array<{ row: number, col: number, rowspan: number, colspan: number }>}
*/
filterOverlappingMergeCells(mergedCellsInfo) {
const occupiedCells = new Set();
this.mergedCells.forEach(mergedCell => {
const {
row,
col,
colspan,
rowspan
} = mergedCell;
for (let r = row; r < row + rowspan; r++) {
for (let c = col; c < col + colspan; c++) {
occupiedCells.add(`r${r},c${c}`);
}
}
});
const filteredMergeCells = mergedCellsInfo.filter(mergedCell => {
const {
row,
col,
colspan,
rowspan
} = mergedCell;
const localOccupiedCells = new Set();
let isOverlapping = false;
for (let r = row; r < row + rowspan; r++) {
for (let c = col; c < col + colspan; c++) {
const cellId = `r${r},c${c}`;
if (occupiedCells.has(cellId)) {
warn(MergedCellsCollection.IS_OVERLAPPING_WARNING(mergedCell));
isOverlapping = true;
break;
}
localOccupiedCells.add(cellId);
}
if (isOverlapping) {
break;
}
}
if (!isOverlapping) {
occupiedCells.add(...localOccupiedCells);
}
return !isOverlapping;
});
return filteredMergeCells;
}
/**
* Get a merged cell contained in the provided range.
*
* @param {CellRange} range The range to search merged cells in.
* @param {boolean} [countPartials=false] If set to `true`, all the merged cells overlapping the range will be taken into calculation.
* @returns {MergedCellCoords[]} Array of found merged cells.
*/
getWithinRange(range) {
let countPartials = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const {
row: rowStart,
col: columnStart
} = range.getTopStartCorner();
const {
row: rowEnd,
col: columnEnd
} = range.getBottomEndCorner();
const result = [];
for (let row = rowStart; row <= rowEnd; row++) {
for (let column = columnStart; column <= columnEnd; column++) {
const mergedCell = this.get(row, column);
if (mergedCell && (countPartials || !countPartials && mergedCell.row === row && mergedCell.col === column)) {
result.push(mergedCell);
}
}
}
return result;
}
/**
* Add a merged cell to the container.
*
* @param {object} mergedCellInfo The merged cell information object. Has to contain `row`, `col`, `colspan` and `rowspan` properties.
* @param {boolean} [auto=false] `true` if called internally by the plugin (usually in batch).
* @returns {MergedCellCoords|boolean} Returns the new merged cell on success and `false` on failure.
*/
add(mergedCellInfo) {
let auto = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const row = mergedCellInfo.row;
const column = mergedCellInfo.col;
const rowspan = mergedCellInfo.rowspan;
const colspan = mergedCellInfo.colspan;
const newMergedCell = new MergedCellCoords(row, column, rowspan, colspan, this.hot._createCellCoords, this.hot._createCellRange);
const alreadyExists = this.get(row, column);
const isOverlapping = auto ? false : this.isOverlapping(newMergedCell);
if (!alreadyExists && !isOverlapping) {
if (this.hot) {
newMergedCell.normalize(this.hot);
}
this.mergedCells.push(newMergedCell);
_assertClassBrand(_MergedCellsCollection_brand, this, _addMergedCellToMatrix).call(this, newMergedCell);
return newMergedCell;
}
if (isOverlapping) {
warn(MergedCellsCollection.IS_OVERLAPPING_WARNING(newMergedCell));
}
return false;
}
/**
* Remove a merged cell from the container. You can provide either the "starting coordinates"
* of a merged cell, or any coordinates from the body of the merged cell.
*
* @param {number} row Row index.
* @param {number} column Column index.
* @returns {MergedCellCoords|boolean} Returns the removed merged cell on success and `false` on failure.
*/
remove(row, column) {
const mergedCell = this.get(row, column);
const mergedCellIndex = mergedCell ? this.mergedCells.indexOf(mergedCell) : -1;
if (mergedCell && mergedCellIndex !== -1) {
this.mergedCells.splice(mergedCellIndex, 1);
_assertClassBrand(_MergedCellsCollection_brand, this, _removeMergedCellFromMatrix).call(this, mergedCell);
return mergedCell;
}
return false;
}
/**
* Clear all the merged cells.
*/
clear() {
arrayEach(this.mergedCells, _ref2 => {
let {
row,
col,
rowspan,
colspan
} = _ref2;
rangeEach(row, row + rowspan, r => {
rangeEach(col, col + colspan, c => {
const TD = this.hot.getCell(r, c);
if (TD) {
TD.removeAttribute('rowspan');
TD.removeAttribute('colspan');
TD.style.display = '';
}
});
});
});
this.mergedCells.length = 0;
this.mergedCellsMatrix = new Map();
}
/**
* Check if the provided merged cell overlaps with the others already added.
*
* @param {MergedCellCoords} mergedCell The merged cell to check against all others in the container.
* @returns {boolean} `true` if the provided merged cell overlaps with the others, `false` otherwise.
*/
isOverlapping(mergedCell) {
const mergedCellRange = mergedCell.getRange();
for (let i = 0; i < this.mergedCells.length; i++) {
const otherMergedCell = this.mergedCells[i];
const otherMergedCellRange = otherMergedCell.getRange();
if (otherMergedCellRange.overlaps(mergedCellRange)) {
return true;
}
}
return false;
}
/**
* Check whether the provided row/col coordinates direct to a first not hidden cell within merge area.
*
* @param {number} row Visual row index.
* @param {number} column Visual column index.
* @returns {boolean}
*/
isFirstRenderableMergedCell(row, column) {
const mergeParent = this.get(row, column);
if (!mergeParent) {
return false;
}
const {
row: mergeRow,
col: mergeColumn,
rowspan,
colspan
} = mergeParent;
const overlayName = this.hot.view.getActiveOverlayName();
const firstRenderedRow = ['top', 'top_inline_start_corner'].includes(overlayName) ? 0 : this.hot.getFirstRenderedVisibleRow();
const firstRenderedColumn = ['inline_start', 'top_inline_start_corner', 'bottom_inline_start_corner'].includes(overlayName) ? 0 : this.hot.getFirstRenderedVisibleColumn();
const mergeCellsTopRow = clamp(firstRenderedRow, mergeRow, mergeRow + rowspan - 1);
const mergeCellsStartColumn = clamp(firstRenderedColumn, mergeColumn, mergeColumn + colspan - 1);
return this.hot.rowIndexMapper.getNearestNotHiddenIndex(mergeCellsTopRow, 1) === row && this.hot.columnIndexMapper.getNearestNotHiddenIndex(mergeCellsStartColumn, 1) === column;
}
/**
* Get the first renderable coords of the merged cell at the provided coordinates.
*
* @param {number} row Visual row index.
* @param {number} column Visual column index.
* @returns {CellCoords} A `CellCoords` object with the coordinates to the first renderable cell within the
* merged cell.
*/
getFirstRenderableCoords(row, column) {
const mergeParent = this.get(row, column);
if (!mergeParent || this.isFirstRenderableMergedCell(row, column)) {
return this.hot._createCellCoords(row, column);
}
const firstRenderableRow = this.hot.rowIndexMapper.getNearestNotHiddenIndex(mergeParent.row, 1);
const firstRenderableColumn = this.hot.columnIndexMapper.getNearestNotHiddenIndex(mergeParent.col, 1);
return this.hot._createCellCoords(firstRenderableRow, firstRenderableColumn);
}
/**
* Gets the start-most visual column index that do not intersect with other merged cells within the provided range.
*
* @param {CellRange} range The range to search within.
* @param {number} visualColumnIndex The visual column index to start the search from.
* @returns {number}
*/
getStartMostColumnIndex(range, visualColumnIndex) {
const indexes = _assertClassBrand(_MergedCellsCollection_brand, this, _getNonIntersectingIndexes).call(this, range, 'col', -1);
let startMostIndex = visualColumnIndex;
for (let i = 0; i < indexes.length; i++) {
if (indexes[i] <= visualColumnIndex) {
startMostIndex = indexes[i];
break;
}
}
return startMostIndex;
}
/**
* Gets the end-most visual column index that do not intersect with other merged cells within the provided range.
*
* @param {CellRange} range The range to search within.
* @param {number} visualColumnIndex The visual column index to start the search from.
* @returns {number}
*/
getEndMostColumnIndex(range, visualColumnIndex) {
const indexes = _assertClassBrand(_MergedCellsCollection_brand, this, _getNonIntersectingIndexes).call(this, range, 'col', 1);
let endMostIndex = visualColumnIndex;
for (let i = 0; i < indexes.length; i++) {
if (indexes[i] >= visualColumnIndex) {
endMostIndex = indexes[i];
break;
}
}
return endMostIndex;
}
/**
* Gets the top-most visual row index that do not intersect with other merged cells within the provided range.
*
* @param {CellRange} range The range to search within.
* @param {number} visualRowIndex The visual row index to start the search from.
* @returns {number}
*/
getTopMostRowIndex(range, visualRowIndex) {
const indexes = _assertClassBrand(_MergedCellsCollection_brand, this, _getNonIntersectingIndexes).call(this, range, 'row', -1);
let topMostIndex = visualRowIndex;
for (let i = 0; i < indexes.length; i++) {
if (indexes[i] <= visualRowIndex) {
topMostIndex = indexes[i];
break;
}
}
return topMostIndex;
}
/**
* Gets the bottom-most visual row index that do not intersect with other merged cells within the provided range.
*
* @param {CellRange} range The range to search within.
* @param {number} visualRowIndex The visual row index to start the search from.
* @returns {number}
*/
getBottomMostRowIndex(range, visualRowIndex) {
const indexes = _assertClassBrand(_MergedCellsCollection_brand, this, _getNonIntersectingIndexes).call(this, range, 'row', 1);
let bottomMostIndex = visualRowIndex;
for (let i = 0; i < indexes.length; i++) {
if (indexes[i] >= visualRowIndex) {
bottomMostIndex = indexes[i];
break;
}
}
return bottomMostIndex;
}
/**
* Shift the merged cell in the direction and by an offset defined in the arguments.
*
* @param {string} direction `right`, `left`, `up` or `down`.
* @param {number} index Index where the change, which caused the shifting took place.
* @param {number} count Number of rows/columns added/removed in the preceding action.
*/
shiftCollections(direction, index, count) {
const shiftVector = [0, 0];
switch (direction) {
case 'right':
shiftVector[0] += count;
break;
case 'left':
shiftVector[0] -= count;
break;
case 'down':
shiftVector[1] += count;
break;
case 'up':
shiftVector[1] -= count;
break;
default:
}
const removedMergedCells = [];
this.mergedCells.forEach(currentMerge => {
currentMerge.shift(shiftVector, index);
if (currentMerge.removed) {
removedMergedCells.push(currentMerge);
}
});
removedMergedCells.forEach(removedMerge => {
this.mergedCells.splice(this.mergedCells.indexOf(removedMerge), 1);
});
this.mergedCellsMatrix.clear();
this.mergedCells.forEach(currentMerge => {
_assertClassBrand(_MergedCellsCollection_brand, this, _addMergedCellToMatrix).call(this, currentMerge);
});
}
/**
* Adds a merged cell to the matrix.
*
* @param {MergedCellCoords} mergedCell The merged cell to add.
*/
}
function _getNonIntersectingIndexes(range, axis) {
let scanDirection = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
const indexes = new Map();
const from = scanDirection === 1 ? range.getTopStartCorner() : range.getBottomEndCorner();
const to = scanDirection === 1 ? range.getBottomEndCorner() : range.getTopStartCorner();
for (let row = from.row; scanDirection === 1 ? row <= to.row : row >= to.row; row += scanDirection) {
for (let column = from.col; scanDirection === 1 ? column <= to.col : column >= to.col; column += scanDirection) {
const index = axis === 'row' ? row : column;
const mergedCell = this.get(row, column);
let lastIndex = index;
if (mergedCell) {
lastIndex = scanDirection === 1 ? mergedCell[axis] + mergedCell[`${axis}span`] - 1 : mergedCell[axis];
}
if (!indexes.has(index)) {
indexes.set(index, new Set());
}
indexes.get(index).add(lastIndex);
}
}
return Array.from(new Set(Array.from(indexes.entries()).filter(_ref3 => {
let [, set] = _ref3;
return set.size === 1;
}).flatMap(_ref4 => {
let [, set] = _ref4;
return Array.from(set);
})));
}
function _addMergedCellToMatrix(mergedCell) {
for (let row = mergedCell.row; row < mergedCell.row + mergedCell.rowspan; row++) {
for (let col = mergedCell.col; col < mergedCell.col + mergedCell.colspan; col++) {
if (!this.mergedCellsMatrix.has(row)) {
this.mergedCellsMatrix.set(row, new Map());
}
this.mergedCellsMatrix.get(row).set(col, mergedCell);
}
}
}
/**
* Removes a merged cell from the matrix.
*
* @param {MergedCellCoords} mergedCell The merged cell to remove.
*/
function _removeMergedCellFromMatrix(mergedCell) {
for (let row = mergedCell.row; row < mergedCell.row + mergedCell.rowspan; row++) {
for (let col = mergedCell.col; col < mergedCell.col + mergedCell.colspan; col++) {
this.mergedCellsMatrix.get(row).delete(col);
}
}
}
export default MergedCellsCollection;