import { CellRange } from './../3rdparty/walkontable/src'; import { arrayEach, arrayReduce } from './../helpers/array'; import { isUndefined } from './../helpers/mixed'; 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. * @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, _callSymbol = 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 {String} type Selection type which will be processed. * @param {Object} [options] * @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, { keepDirection = false, propToCol } = {}) { 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); } return [rowStart, columnStart, rowEnd, columnEnd]; }; } /** * 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 {Array[]|CellRange[]} selectionRanges Selection ranges produced by Handsontable. * @return {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(selectionRanges) { const selectionType = detectSelectionType(selectionRanges); if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) { return []; } const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType); const unorderedIndexes = new Set(); // Iterate through all ranges and collect all column indexes which are not saved yet. arrayEach(selectionRanges, (selection) => { const [, columnStart,, columnEnd] = selectionSchemaNormalizer(selection); const amount = columnEnd - columnStart + 1; arrayEach(Array.from(new Array(amount), (_, i) => columnStart + 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 {Array[]|CellRange[]} selectionRanges Selection ranges produced by Handsontable. * @return {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(selectionRanges) { const selectionType = detectSelectionType(selectionRanges); if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) { return []; } const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType); const unorderedIndexes = new Set(); // Iterate through all ranges and collect all column indexes which are not saved yet. arrayEach(selectionRanges, (selection) => { const [rowStart,, rowEnd] = selectionSchemaNormalizer(selection); const amount = rowEnd - rowStart + 1; arrayEach(Array.from(new Array(amount), (_, i) => rowStart + 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; } /** * Check if passed value can be treated as valid cell coordinate. The second argument is * used to check if the value doesn't exceed the defined max table rows/columns count. * * @param {*} coord * @param {Number} maxTableItemsCount The value that declares the maximum coordinate that is still validatable. * @return {Boolean} */ export function isValidCoord(coord, maxTableItemsCount = Infinity) { return typeof coord === 'number' && coord >= 0 && coord < maxTableItemsCount; }