utils.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { CellRange } from './../3rdparty/walkontable/src';
  2. import { arrayEach, arrayReduce } from './../helpers/array';
  3. import { isUndefined } from './../helpers/mixed';
  4. export const SELECTION_TYPE_UNRECOGNIZED = 0;
  5. export const SELECTION_TYPE_EMPTY = 1;
  6. export const SELECTION_TYPE_ARRAY = 2;
  7. export const SELECTION_TYPE_OBJECT = 3;
  8. export const SELECTION_TYPES = [
  9. SELECTION_TYPE_OBJECT,
  10. SELECTION_TYPE_ARRAY,
  11. ];
  12. const ARRAY_TYPE_PATTERN = [['number'], ['number', 'string'], ['number', 'undefined'], ['number', 'string', 'undefined']];
  13. const rootCall = Symbol('root');
  14. const childCall = Symbol('child');
  15. /**
  16. * Detect selection schema structure.
  17. *
  18. * @param {*} selectionRanges The selected range or and array of selected ranges. This type of data is produced by
  19. * `hot.getSelected()`, `hot.getSelectedLast()`, `hot.getSelectedRange()`
  20. * and `hot.getSelectedRangeLast()` methods.
  21. * @returns {Number} Returns a number that specifies the type of detected selection schema. If selection schema type
  22. * is unrecognized than it returns `0`.
  23. */
  24. export function detectSelectionType(selectionRanges, _callSymbol = rootCall) {
  25. if (_callSymbol !== rootCall && _callSymbol !== childCall) {
  26. throw new Error('The second argument is used internally only and cannot be overwritten.');
  27. }
  28. const isArray = Array.isArray(selectionRanges);
  29. const isRootCall = _callSymbol === rootCall;
  30. let result = SELECTION_TYPE_UNRECOGNIZED;
  31. if (isArray) {
  32. const firstItem = selectionRanges[0];
  33. if (selectionRanges.length === 0) {
  34. result = SELECTION_TYPE_EMPTY;
  35. } else if (isRootCall && firstItem instanceof CellRange) {
  36. result = SELECTION_TYPE_OBJECT;
  37. } else if (isRootCall && Array.isArray(firstItem)) {
  38. result = detectSelectionType(firstItem, childCall);
  39. } else if (selectionRanges.length >= 2 && selectionRanges.length <= 4) {
  40. const isArrayType = !selectionRanges.some((value, index) => !ARRAY_TYPE_PATTERN[index].includes(typeof value));
  41. if (isArrayType) {
  42. result = SELECTION_TYPE_ARRAY;
  43. }
  44. }
  45. }
  46. return result;
  47. }
  48. /**
  49. * Factory function designed for normalization data schema from different data structures of the selection ranges.
  50. *
  51. * @param {String} type Selection type which will be processed.
  52. * @param {Object} [options]
  53. * @param {Boolean} [options.keepDirection=false] If `true`, the coordinates which contain the direction of the
  54. * selected cells won't be changed. Otherwise, the selection will be
  55. * normalized to values starting from top-left to bottom-right.
  56. * @param {Function} [options.propToCol] Pass the converting function (usually `datamap.propToCol`) if the column
  57. * defined as props should be normalized to the numeric values.
  58. * @returns {Number[]} Returns normalized data about selected range as an array (`[rowStart, columnStart, rowEnd, columnEnd]`).
  59. */
  60. export function normalizeSelectionFactory(type, { keepDirection = false, propToCol } = {}) {
  61. if (!SELECTION_TYPES.includes(type)) {
  62. throw new Error('Unsupported selection ranges schema type was provided.');
  63. }
  64. return function(selection) {
  65. const isObjectType = type === SELECTION_TYPE_OBJECT;
  66. let rowStart = isObjectType ? selection.from.row : selection[0];
  67. let columnStart = isObjectType ? selection.from.col : selection[1];
  68. let rowEnd = isObjectType ? selection.to.row : selection[2];
  69. let columnEnd = isObjectType ? selection.to.col : selection[3];
  70. if (typeof propToCol === 'function') {
  71. if (typeof columnStart === 'string') {
  72. columnStart = propToCol(columnStart);
  73. }
  74. if (typeof columnEnd === 'string') {
  75. columnEnd = propToCol(columnEnd);
  76. }
  77. }
  78. if (isUndefined(rowEnd)) {
  79. rowEnd = rowStart;
  80. }
  81. if (isUndefined(columnEnd)) {
  82. columnEnd = columnStart;
  83. }
  84. if (!keepDirection) {
  85. const origRowStart = rowStart;
  86. const origColumnStart = columnStart;
  87. const origRowEnd = rowEnd;
  88. const origColumnEnd = columnEnd;
  89. rowStart = Math.min(origRowStart, origRowEnd);
  90. columnStart = Math.min(origColumnStart, origColumnEnd);
  91. rowEnd = Math.max(origRowStart, origRowEnd);
  92. columnEnd = Math.max(origColumnStart, origColumnEnd);
  93. }
  94. return [rowStart, columnStart, rowEnd, columnEnd];
  95. };
  96. }
  97. /**
  98. * Function transform selection ranges (produced by `hot.getSelected()` and `hot.getSelectedRange()`) to normalized
  99. * data structure. It merges repeated ranges into consecutive coordinates. The returned structure
  100. * contains an array of arrays. The single item contains at index 0 visual column index from the selection was
  101. * started and at index 1 distance as a count of selected columns.
  102. *
  103. * @param {Array[]|CellRange[]} selectionRanges Selection ranges produced by Handsontable.
  104. * @return {Array[]} Returns an array of arrays with ranges defines in that schema:
  105. * `[[visualColumnStart, distance], [visualColumnStart, distance], ...]`.
  106. * The column distances are always created starting from the left (zero index) to the
  107. * right (the latest column index).
  108. */
  109. export function transformSelectionToColumnDistance(selectionRanges) {
  110. const selectionType = detectSelectionType(selectionRanges);
  111. if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) {
  112. return [];
  113. }
  114. const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType);
  115. const unorderedIndexes = new Set();
  116. // Iterate through all ranges and collect all column indexes which are not saved yet.
  117. arrayEach(selectionRanges, (selection) => {
  118. const [, columnStart,, columnEnd] = selectionSchemaNormalizer(selection);
  119. const amount = columnEnd - columnStart + 1;
  120. arrayEach(Array.from(new Array(amount), (_, i) => columnStart + i), (index) => {
  121. if (!unorderedIndexes.has(index)) {
  122. unorderedIndexes.add(index);
  123. }
  124. });
  125. });
  126. // Sort indexes in ascending order to easily detecting non-consecutive columns.
  127. const orderedIndexes = Array.from(unorderedIndexes).sort((a, b) => a - b);
  128. const normalizedColumnRanges = arrayReduce(orderedIndexes, (acc, visualColumnIndex, index, array) => {
  129. if (index !== 0 && visualColumnIndex === array[index - 1] + 1) {
  130. acc[acc.length - 1][1] += 1;
  131. } else {
  132. acc.push([visualColumnIndex, 1]);
  133. }
  134. return acc;
  135. }, []);
  136. return normalizedColumnRanges;
  137. }
  138. /**
  139. * Function transform selection ranges (produced by `hot.getSelected()` and `hot.getSelectedRange()`) to normalized
  140. * data structure. It merges repeated ranges into consecutive coordinates. The returned structure
  141. * contains an array of arrays. The single item contains at index 0 visual column index from the selection was
  142. * started and at index 1 distance as a count of selected columns.
  143. *
  144. * @param {Array[]|CellRange[]} selectionRanges Selection ranges produced by Handsontable.
  145. * @return {Array[]} Returns an array of arrays with ranges defines in that schema:
  146. * `[[visualColumnStart, distance], [visualColumnStart, distance], ...]`.
  147. * The column distances are always created starting from the left (zero index) to the
  148. * right (the latest column index).
  149. */
  150. export function transformSelectionToRowDistance(selectionRanges) {
  151. const selectionType = detectSelectionType(selectionRanges);
  152. if (selectionType === SELECTION_TYPE_UNRECOGNIZED || selectionType === SELECTION_TYPE_EMPTY) {
  153. return [];
  154. }
  155. const selectionSchemaNormalizer = normalizeSelectionFactory(selectionType);
  156. const unorderedIndexes = new Set();
  157. // Iterate through all ranges and collect all column indexes which are not saved yet.
  158. arrayEach(selectionRanges, (selection) => {
  159. const [rowStart,, rowEnd] = selectionSchemaNormalizer(selection);
  160. const amount = rowEnd - rowStart + 1;
  161. arrayEach(Array.from(new Array(amount), (_, i) => rowStart + i), (index) => {
  162. if (!unorderedIndexes.has(index)) {
  163. unorderedIndexes.add(index);
  164. }
  165. });
  166. });
  167. // Sort indexes in ascending order to easily detecting non-consecutive columns.
  168. const orderedIndexes = Array.from(unorderedIndexes).sort((a, b) => a - b);
  169. const normalizedRowRanges = arrayReduce(orderedIndexes, (acc, rowIndex, index, array) => {
  170. if (index !== 0 && rowIndex === array[index - 1] + 1) {
  171. acc[acc.length - 1][1] += 1;
  172. } else {
  173. acc.push([rowIndex, 1]);
  174. }
  175. return acc;
  176. }, []);
  177. return normalizedRowRanges;
  178. }
  179. /**
  180. * Check if passed value can be treated as valid cell coordinate. The second argument is
  181. * used to check if the value doesn't exceed the defined max table rows/columns count.
  182. *
  183. * @param {*} coord
  184. * @param {Number} maxTableItemsCount The value that declares the maximum coordinate that is still validatable.
  185. * @return {Boolean}
  186. */
  187. export function isValidCoord(coord, maxTableItemsCount = Infinity) {
  188. return typeof coord === 'number' && coord >= 0 && coord < maxTableItemsCount;
  189. }