handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
221 lines (213 loc) • 10.4 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.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.some.js";
import { CellRange } from "./../3rdparty/walkontable/src/index.mjs";
import { arrayEach, arrayReduce } from "./../helpers/array.mjs";
import { isUndefined } from "./../helpers/mixed.mjs";
export const SELECTION_TYPE_UNRECOGNIZED = 0;
export const SELECTION_TYPE_EMPTY = 1;
export const SELECTION_TYPE_ARRAY = 2;
export const SELECTION_TYPE_OBJECT = 3;
export const SELECTION_TYPES = [SELECTION_TYPE_OBJECT, SELECTION_TYPE_ARRAY];
const ARRAY_TYPE_PATTERN = [['number'], ['number', 'string'], ['number', 'undefined'], ['number', 'string', 'undefined']];
const rootCall = Symbol('root');
const childCall = Symbol('child');
/**
* Detect selection schema structure.
*
* @param {*} selectionRanges The selected range or and array of selected ranges. This type of data is produced by
* `hot.getSelected()`, `hot.getSelectedLast()`, `hot.getSelectedRange()`
* and `hot.getSelectedRangeLast()` methods.
* @param {symbol} _callSymbol The symbol object which indicates source of the helper invocation.
* @returns {number} Returns a number that specifies the type of detected selection schema. If selection schema type
* is unrecognized than it returns `0`.
*/
export function detectSelectionType(selectionRanges) {
let _callSymbol = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : rootCall;
if (_callSymbol !== rootCall && _callSymbol !== childCall) {
throw new Error('The second argument is used internally only and cannot be overwritten.');
}
const isArray = Array.isArray(selectionRanges);
const isRootCall = _callSymbol === rootCall;
let result = SELECTION_TYPE_UNRECOGNIZED;
if (isArray) {
const firstItem = selectionRanges[0];
if (selectionRanges.length === 0) {
result = SELECTION_TYPE_EMPTY;
} else if (isRootCall && firstItem instanceof CellRange) {
result = SELECTION_TYPE_OBJECT;
} else if (isRootCall && Array.isArray(firstItem)) {
result = detectSelectionType(firstItem, childCall);
} else if (selectionRanges.length >= 2 && selectionRanges.length <= 4) {
const isArrayType = !selectionRanges.some((value, index) => !ARRAY_TYPE_PATTERN[index].includes(typeof value));
if (isArrayType) {
result = SELECTION_TYPE_ARRAY;
}
}
}
return result;
}
/**
* Factory function designed for normalization data schema from different data structures of the selection ranges.
*
* @param {number} type Selection type which will be processed.
* @param {object} options The normalization options.
* @param {function(number, number): CellCoords} options.createCellCoords The factory function that returns an instance of the `CellCoords` class.
* @param {function(CellCoords, CellCoords, CellCoords): CellRange} options.createCellRange The factory function that returns an instance of the `CellRange` class.
* @param {boolean} [options.keepDirection=false] If `true`, the coordinates which contain the direction of the
* selected cells won't be changed. Otherwise, the selection will be
* normalized to values starting from top-left to bottom-right.
* @param {Function} [options.propToCol] Pass the converting function (usually `datamap.propToCol`) if the column
* defined as props should be normalized to the numeric values.
* @returns {number[]} Returns normalized data about selected range as an array (`[rowStart, columnStart, rowEnd, columnEnd]`).
*/
export function normalizeSelectionFactory(type) {
let {
createCellCoords,
createCellRange,
keepDirection = false,
propToCol
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!SELECTION_TYPES.includes(type)) {
throw new Error('Unsupported selection ranges schema type was provided.');
}
return function (selection) {
const isObjectType = type === SELECTION_TYPE_OBJECT;
let rowStart = isObjectType ? selection.from.row : selection[0];
let columnStart = isObjectType ? selection.from.col : selection[1];
let rowEnd = isObjectType ? selection.to.row : selection[2];
let columnEnd = isObjectType ? selection.to.col : selection[3];
if (typeof propToCol === 'function') {
if (typeof columnStart === 'string') {
columnStart = propToCol(columnStart);
}
if (typeof columnEnd === 'string') {
columnEnd = propToCol(columnEnd);
}
}
if (isUndefined(rowEnd)) {
rowEnd = rowStart;
}
if (isUndefined(columnEnd)) {
columnEnd = columnStart;
}
if (!keepDirection) {
const origRowStart = rowStart;
const origColumnStart = columnStart;
const origRowEnd = rowEnd;
const origColumnEnd = columnEnd;
rowStart = Math.min(origRowStart, origRowEnd);
columnStart = Math.min(origColumnStart, origColumnEnd);
rowEnd = Math.max(origRowStart, origRowEnd);
columnEnd = Math.max(origColumnStart, origColumnEnd);
}
const highlight = isObjectType ? selection.highlight.clone() : createCellCoords(rowStart, columnStart);
const from = createCellCoords(rowStart, columnStart);
const to = createCellCoords(rowEnd, columnEnd);
return createCellRange(highlight, from, to);
};
}
/**
* Function transform selection ranges (produced by `hot.getSelected()` and `hot.getSelectedRange()`) to normalized
* data structure. It merges repeated ranges into consecutive coordinates. The returned structure
* contains an array of arrays. The single item contains at index 0 visual column index from the selection was
* started and at index 1 distance as a count of selected columns.
*
* @param {Core} hotInstance The Handsontable instance.
* @returns {Array[]} Returns an array of arrays with ranges defines in that schema:
* `[[visualColumnStart, distance], [visualColumnStart, distance], ...]`.
* The column distances are always created starting from the left (zero index) to the
* right (the latest column index).
*/
export function transformSelectionToColumnDistance(hotInstance) {
const selectionType = detectSelectionType(hotInstance.getSelected());
if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) {
return [];
}
const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType, {
createCellCoords: hotInstance._createCellCoords.bind(hotInstance),
createCellRange: hotInstance._createCellRange.bind(hotInstance)
});
const unorderedIndexes = new Set();
// Iterate through all ranges and collect all column indexes which are not saved yet.
arrayEach(hotInstance.getSelected(), selection => {
const {
from,
to
} = selectionSchemaNormalizer(selection);
const columnNonHeaderStart = Math.max(from.col, 0);
const amount = to.col - columnNonHeaderStart + 1;
arrayEach(Array.from(new Array(amount), (_, i) => columnNonHeaderStart + i), index => {
if (!unorderedIndexes.has(index)) {
unorderedIndexes.add(index);
}
});
});
// Sort indexes in ascending order to easily detecting non-consecutive columns.
const orderedIndexes = Array.from(unorderedIndexes).sort((a, b) => a - b);
const normalizedColumnRanges = arrayReduce(orderedIndexes, (acc, visualColumnIndex, index, array) => {
if (index !== 0 && visualColumnIndex === array[index - 1] + 1) {
acc[acc.length - 1][1] += 1;
} else {
acc.push([visualColumnIndex, 1]);
}
return acc;
}, []);
return normalizedColumnRanges;
}
/**
* Function transform selection ranges (produced by `hot.getSelected()` and `hot.getSelectedRange()`) to normalized
* data structure. It merges repeated ranges into consecutive coordinates. The returned structure
* contains an array of arrays. The single item contains at index 0 visual column index from the selection was
* started and at index 1 distance as a count of selected columns.
*
* @param {Core} hotInstance The Handsontable instance.
* @returns {Array[]} Returns an array of arrays with ranges defines in that schema:
* `[[visualColumnStart, distance], [visualColumnStart, distance], ...]`.
* The column distances are always created starting from the left (zero index) to the
* right (the latest column index).
*/
export function transformSelectionToRowDistance(hotInstance) {
const selectionType = detectSelectionType(hotInstance.getSelected());
if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) {
return [];
}
const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType, {
createCellCoords: hotInstance._createCellCoords.bind(hotInstance),
createCellRange: hotInstance._createCellRange.bind(hotInstance)
});
const unorderedIndexes = new Set();
// Iterate through all ranges and collect all column indexes which are not saved yet.
arrayEach(hotInstance.getSelected(), selection => {
const {
from,
to
} = selectionSchemaNormalizer(selection);
const rowNonHeaderStart = Math.max(from.row, 0);
const amount = to.row - rowNonHeaderStart + 1;
arrayEach(Array.from(new Array(amount), (_, i) => rowNonHeaderStart + i), index => {
if (!unorderedIndexes.has(index)) {
unorderedIndexes.add(index);
}
});
});
// Sort indexes in ascending order to easily detecting non-consecutive columns.
const orderedIndexes = Array.from(unorderedIndexes).sort((a, b) => a - b);
const normalizedRowRanges = arrayReduce(orderedIndexes, (acc, rowIndex, index, array) => {
if (index !== 0 && rowIndex === array[index - 1] + 1) {
acc[acc.length - 1][1] += 1;
} else {
acc.push([rowIndex, 1]);
}
return acc;
}, []);
return normalizedRowRanges;
}