canvas-datagrid 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1319 @@
1
+ 'use strict';
2
+ //@ts-check
3
+ /// <reference path="./type.d.ts" />
4
+
5
+ // Abbreviation in this file
6
+ // - sel: selection
7
+ // - sel0, sel1: If there are two selections in the parameters, these two names are for them.
8
+
9
+ // Types of Selection
10
+ // 0. (High Priority) Unselected cells from [startRow, startColumn] to [endRow, endColumn]
11
+ // 1. Cells from [startRow, startColumn] to [endRow, endColumn]
12
+ // 2. Rows from startRow to endRow
13
+ // 3. Columns from startColumn to endColumn
14
+
15
+ const SelectionType = {
16
+ UnselectedCells: 0,
17
+ Cells: 1,
18
+ Rows: 2,
19
+ Columns: 3,
20
+ };
21
+
22
+ /**
23
+ * Swap values between two properties in an object
24
+ * @param {object} obj The object
25
+ * @param {string} prop0 First property name
26
+ * @param {string} prop1 Another property name
27
+ */
28
+ const swapProps = (obj, prop0, prop1) => {
29
+ const t = obj[prop0];
30
+ obj[prop0] = obj[prop1];
31
+ obj[prop1] = t;
32
+ };
33
+
34
+ /**
35
+ * This function transforms a selection object to normalized.
36
+ * Here is the definition of **normalized selection object**:
37
+ * - It contains these required properties: type, startRow, startColumn, endRow, endColumn
38
+ * - Property `endRow` and property `endColumn` must exist even if their value are the same with `startRow` or `startColumn`.
39
+ * - The value of `endRow` must be equals or greater than the value of `startRow`
40
+ * - The value of `endColumn` must be equals or greater than the value of `startColumn`
41
+ * @param {SelectionDescriptor} sel
42
+ * @returns {SelectionDescriptor}
43
+ */
44
+ const normalizeSelection = (sel) => {
45
+ if (!sel) return sel;
46
+ switch (sel.type) {
47
+ case SelectionType.UnselectedCells:
48
+ case SelectionType.Cells:
49
+ if (typeof sel.endRow !== 'number') sel.endRow = sel.startRow;
50
+ else if (sel.endRow < sel.startRow) swapProps(sel, 'startRow', 'endRow');
51
+
52
+ if (typeof sel.endColumn !== 'number') sel.endColumn = sel.startColumn;
53
+ else if (sel.endColumn < sel.startColumn)
54
+ swapProps(sel, 'startColumn', 'endColumn');
55
+
56
+ break;
57
+ case SelectionType.Rows:
58
+ if (typeof sel.endRow !== 'number') sel.endRow = sel.startRow;
59
+ else if (sel.endRow < sel.startRow) swapProps(sel, 'startRow', 'endRow');
60
+
61
+ break;
62
+ case SelectionType.Columns:
63
+ if (typeof sel.endColumn !== 'number') sel.endColumn = sel.startColumn;
64
+ else if (sel.endColumn < sel.startColumn)
65
+ swapProps(sel, 'startColumn', 'endColumn');
66
+
67
+ break;
68
+ }
69
+ return sel;
70
+ };
71
+
72
+ /**
73
+ * Parse a string expression to a selection object. Here are some example expressions:
74
+ * - cells:20,30-40,50
75
+ * - row:5
76
+ * - cols:5-9
77
+ * @param {string} str
78
+ * @returns {SelectionDescriptor}
79
+ */
80
+ const getSelectionFromString = (str) => {
81
+ if (typeof str !== 'string') return;
82
+
83
+ const index = str.indexOf(':');
84
+ if (index < 0) return;
85
+
86
+ const type = str.slice(0, index);
87
+ const num = str
88
+ .slice(index + 1)
89
+ .split(/[,:;-]+/)
90
+ .map((it) => parseInt(it, 10));
91
+ switch (type) {
92
+ case 'cell':
93
+ case 'cells':
94
+ case '-cell':
95
+ case '-cells':
96
+ return normalizeSelection({
97
+ type: SelectionType[type[0] === '-' ? 'UnselectedCells' : 'Cells'],
98
+ startRow: num[0],
99
+ startColumn: num[1],
100
+ endRow: num[2],
101
+ endColumn: num[3],
102
+ });
103
+ case 'row':
104
+ case 'rows':
105
+ return normalizeSelection({
106
+ type: SelectionType.Rows,
107
+ startRow: num[0],
108
+ endRow: num[1],
109
+ });
110
+ case 'col':
111
+ case 'cols':
112
+ return normalizeSelection({
113
+ type: SelectionType.Columns,
114
+ startColumn: num[0],
115
+ endColumn: num[1],
116
+ });
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Check are two cells block the same
122
+ * @param {SelectionDescriptor} block0
123
+ * @param {SelectionDescriptor} block1
124
+ * @returns {boolean}
125
+ */
126
+ const isSameCellsBlock = (block0, block1) => {
127
+ return (
128
+ block0.startRow === block1.startRow &&
129
+ block0.endRow === block1.endRow &&
130
+ block0.startColumn === block1.startColumn &&
131
+ block0.endColumn === block1.endColumn
132
+ );
133
+ };
134
+
135
+ /**
136
+ * This function is used in the function `mergeSelections`
137
+ * @see mergeSelections
138
+ * @param {SelectionDescriptor} cells
139
+ * @param {SelectionDescriptor} rowsOrColumns
140
+ * @returns {SelectionDescriptor}
141
+ */
142
+ const mergeCellsIntoRowsOrColumns = (cells, rowsOrColumns) => {
143
+ if (rowsOrColumns.type === SelectionType.Rows) {
144
+ if (
145
+ cells.startRow >= rowsOrColumns.startRow &&
146
+ cells.endRow <= rowsOrColumns.endRow
147
+ )
148
+ return rowsOrColumns;
149
+ return;
150
+ }
151
+ if (
152
+ cells.startColumn >= rowsOrColumns.startColumn &&
153
+ cells.endColumn <= rowsOrColumns.endColumn
154
+ )
155
+ return rowsOrColumns;
156
+ };
157
+
158
+ /**
159
+ * Merge two selection objects (Splicing, Containing)
160
+ * @param {SelectionDescriptor} sel0
161
+ * @param {SelectionDescriptor} sel1
162
+ * @returns {SelectionDescriptor} A concatenated selection object based on `block0`.
163
+ * Or `undefined` if they can't be concatenated
164
+ */
165
+ const mergeSelections = (sel0, sel1) => {
166
+ if (sel0.type !== sel1.type) {
167
+ if (sel1.type <= SelectionType.Cells && sel0.type > SelectionType.Cells)
168
+ return mergeCellsIntoRowsOrColumns(sel1, sel0);
169
+ if (sel0.type <= SelectionType.Cells && sel1.type > SelectionType.Cells)
170
+ return mergeCellsIntoRowsOrColumns(sel0, sel1);
171
+ return;
172
+ }
173
+
174
+ /** These two selection objects may be concatenated horizontally */
175
+ const horizontalConcat =
176
+ sel0.type === SelectionType.Columns
177
+ ? true
178
+ : sel0.startRow === sel1.startRow && sel0.endRow === sel1.endRow;
179
+
180
+ /** These two selection objects may be concatenated vertically */
181
+ const verticalConcat =
182
+ sel0.type === SelectionType.Rows
183
+ ? true
184
+ : sel0.startColumn === sel1.startColumn &&
185
+ sel0.endColumn === sel1.endColumn;
186
+
187
+ if (horizontalConcat) {
188
+ // Are they the same
189
+ if (verticalConcat) return sel0;
190
+ // Are they neighbor
191
+ if (
192
+ sel1.startColumn > sel0.endColumn + 1 ||
193
+ sel1.endColumn < sel0.startColumn - 1
194
+ )
195
+ return;
196
+ return Object.assign({}, sel0, {
197
+ startColumn: Math.min(sel0.startColumn, sel1.startColumn),
198
+ endColumn: Math.max(sel0.endColumn, sel1.endColumn),
199
+ });
200
+ }
201
+
202
+ if (verticalConcat) {
203
+ // Are they neighbor
204
+ if (sel1.startRow > sel0.endRow + 1 || sel1.endRow < sel0.startRow - 1)
205
+ return;
206
+ return Object.assign({}, sel0, {
207
+ startRow: Math.min(sel0.startRow, sel1.startRow),
208
+ endRow: Math.max(sel0.endRow, sel1.endRow),
209
+ });
210
+ }
211
+
212
+ // Does one of them contain other one
213
+ if (
214
+ sel0.type === SelectionType.Cells ||
215
+ sel0.type === SelectionType.UnselectedCells
216
+ ) {
217
+ const intersection = getIntersection(sel0, sel1);
218
+ if (intersection) {
219
+ if (isSameCellsBlock(intersection, sel0)) return sel1;
220
+ if (isSameCellsBlock(intersection, sel1)) return sel0;
221
+ }
222
+ }
223
+ };
224
+
225
+ /**
226
+ * Remove some rows from a rows selection
227
+ * @param {SelectionDescriptor} selection It must be a selection with type as `Rows`
228
+ * @param {SelectionDescriptor} remove It must be a selection with type as `Rows`
229
+ * @returns {SelectionDescriptor[]} Returning a `undefined` represents parameter `remove` doesn't intersect with parameter `selection`
230
+ */
231
+ const removePartOfRowsSelection = (selection, remove) => {
232
+ if (remove.endRow < selection.startRow) return;
233
+ if (remove.startRow > selection.endRow) return;
234
+ if (remove.startRow <= selection.startRow) {
235
+ // all rows of the selection is removed
236
+ if (remove.endRow >= selection.endRow) return [];
237
+ return [Object.assign({}, selection, { startRow: remove.endRow + 1 })];
238
+ }
239
+ if (remove.endRow >= selection.endRow)
240
+ return [Object.assign({}, selection, { endRow: remove.startRow - 1 })];
241
+ // the selection be divided into two parts
242
+ return [
243
+ Object.assign({}, selection, { endRow: remove.startRow - 1 }),
244
+ Object.assign({}, selection, { startRow: remove.endRow + 1 }),
245
+ ];
246
+ };
247
+
248
+ /**
249
+ * Remove some columns from a columns selection
250
+ * @param {object} selection It must be a selection with type as `Columns`
251
+ * @param {object} remove It must be a selection with type as `Columns`
252
+ * @returns {object[]} Returning a `undefined` represents parameter `remove` doesn't intersect with parameter `selection`
253
+ */
254
+ const removePartOfColumnsSelection = (selection, remove) => {
255
+ if (remove.endColumn < selection.startColumn) return;
256
+ if (remove.startColumn > selection.endColumn) return;
257
+ if (remove.startColumn <= selection.startColumn) {
258
+ // all cols of the selection is removed
259
+ if (remove.endColumn >= selection.endColumn) return [];
260
+ return [
261
+ Object.assign({}, selection, { startColumn: remove.endColumn + 1 }),
262
+ ];
263
+ }
264
+ if (remove.endColumn >= selection.endColumn)
265
+ return [
266
+ Object.assign({}, selection, { endColumn: remove.startColumn - 1 }),
267
+ ];
268
+ // the selection be divided into two parts
269
+ return [
270
+ Object.assign({}, selection, { endColumn: remove.startColumn - 1 }),
271
+ Object.assign({}, selection, { startColumn: remove.endColumn + 1 }),
272
+ ];
273
+ };
274
+
275
+ /**
276
+ * Remove a cells block from a cells selection
277
+ * @param {SelectionDescriptor} selection It must be a selection with type as `Cells` or `UnselectedCells`
278
+ * @param {SelectionDescriptor} remove It must be a selection
279
+ * @returns {SelectionDescriptor[]} Returning a `undefined` represents parameter `remove` doesn't intersect with parameter `selection`
280
+ */
281
+ const removePartOfCellsSelection = (selection, remove) => {
282
+ const intersect = getIntersection(selection, remove);
283
+ if (!intersect) return;
284
+ // all of cells in the selection are removed
285
+ if (isSameCellsBlock(selection, intersect)) return [];
286
+ const result = [];
287
+ let minStartRow = selection.startRow;
288
+ let maxEndRow = selection.endRow;
289
+ let maxEndColumn = selection.endColumn;
290
+ if (intersect.startRow > selection.startRow) {
291
+ // Top
292
+ result.push(
293
+ Object.assign({}, selection, { endRow: intersect.startRow - 1 }),
294
+ );
295
+ minStartRow = intersect.startRow;
296
+ }
297
+ if (intersect.endColumn < selection.endColumn) {
298
+ // Right
299
+ result.push(
300
+ Object.assign({}, selection, {
301
+ startRow: minStartRow,
302
+ startColumn: intersect.endColumn + 1,
303
+ }),
304
+ );
305
+ maxEndColumn = intersect.endColumn;
306
+ }
307
+ if (intersect.endRow < selection.endRow) {
308
+ // Bottom
309
+ result.push(
310
+ Object.assign({}, selection, {
311
+ endColumn: maxEndColumn,
312
+ startRow: intersect.endRow + 1,
313
+ }),
314
+ );
315
+ maxEndRow = intersect.endRow;
316
+ }
317
+ if (intersect.startColumn > selection.startColumn) {
318
+ // Bottom
319
+ result.push(
320
+ Object.assign({}, selection, {
321
+ startRow: minStartRow,
322
+ endRow: maxEndRow,
323
+ endColumn: intersect.startColumn - 1,
324
+ }),
325
+ );
326
+ }
327
+ return result;
328
+ };
329
+
330
+ /**
331
+ * Get intersection of two selection object
332
+ * @param {SelectionDescriptor} sel0
333
+ * @param {SelectionDescriptor} sel1
334
+ * @returns {SelectionDescriptor} a selection object or undefined
335
+ */
336
+ const getIntersection = (sel0, sel1) => {
337
+ if (sel0.type > sel1.type) return getIntersection(sel1, sel0);
338
+ if (sel0.type <= SelectionType.Cells) {
339
+ if (sel1.type <= SelectionType.Cells) {
340
+ const startColumn = Math.max(sel0.startColumn, sel1.startColumn);
341
+ const endColumn = Math.min(sel0.endColumn, sel1.endColumn);
342
+ if (startColumn > endColumn) return;
343
+
344
+ const startRow = Math.max(sel0.startRow, sel1.startRow);
345
+ const endRow = Math.min(sel0.endRow, sel1.endRow);
346
+ if (startRow > endRow) return;
347
+
348
+ return {
349
+ type: SelectionType.Cells,
350
+ startRow: startRow,
351
+ startColumn: startColumn,
352
+ endRow: endRow,
353
+ endColumn: endColumn,
354
+ };
355
+ }
356
+ if (sel1.type === SelectionType.Rows) {
357
+ const startRow = Math.max(sel0.startRow, sel1.startRow);
358
+ const endRow = Math.min(sel0.endRow, sel1.endRow);
359
+ if (startRow > endRow) return;
360
+ return {
361
+ type: SelectionType.Cells,
362
+ startRow: startRow,
363
+ startColumn: sel0.startColumn,
364
+ endRow: endRow,
365
+ endColumn: sel0.endColumn,
366
+ };
367
+ } else {
368
+ // SelectionType.Columns
369
+ const startColumn = Math.max(sel0.startColumn, sel1.startColumn);
370
+ const endColumn = Math.min(sel0.endColumn, sel1.endColumn);
371
+ if (startColumn > endColumn) return;
372
+ return {
373
+ type: SelectionType.Cells,
374
+ startColumn: startColumn,
375
+ startRow: sel0.startRow,
376
+ endColumn: endColumn,
377
+ endRow: sel0.endRow,
378
+ };
379
+ }
380
+ }
381
+ if (sel0.type === SelectionType.Rows) {
382
+ if (sel1.type === SelectionType.Rows) {
383
+ const startRow = Math.max(sel0.startRow, sel1.startRow);
384
+ const endRow = Math.min(sel0.endRow, sel1.endRow);
385
+ if (startRow > endRow) return;
386
+ return { type: SelectionType.Rows, startRow: startRow, endRow: endRow };
387
+ } else {
388
+ // SelectionType.Columns
389
+ return {
390
+ type: SelectionType.Cells,
391
+ startRow: sel0.startRow,
392
+ startColumn: sel1.startColumn,
393
+ endRow: sel0.endRow,
394
+ endColumn: sel1.endColumn,
395
+ };
396
+ }
397
+ }
398
+ // SelectionType.Columns
399
+ const startColumn = Math.max(sel0.startColumn, sel1.startColumn);
400
+ const endColumn = Math.min(sel0.endColumn, sel1.endColumn);
401
+ if (startColumn > endColumn) return;
402
+ return {
403
+ type: SelectionType.Columns,
404
+ startColumn: startColumn,
405
+ endColumn: endColumn,
406
+ };
407
+ };
408
+
409
+ /**
410
+ * Add a selection object into `selections` array. And there are two behaviours if this function:
411
+ * - It appends a new selection object into the `selections` array
412
+ * - It rewrites the selection item in the `selections` array (merge or change its type)
413
+ *
414
+ * @param {SelectionDescriptor[]} selections
415
+ * @param {SelectionDescriptor} add Supported types: Cells, Rows, Columns
416
+ * @param {ContextForSelectionAction} [context] Eg: {rows:1000, columns:1000}
417
+ * @returns {boolean} is the selections array changed
418
+ */
419
+ const addIntoSelections = (selections, add, context) => {
420
+ if (!add || typeof add.type !== 'number') return false;
421
+ if (add.type === SelectionType.Cells) {
422
+ for (let i = 0; i < selections.length; i++) {
423
+ const sel = selections[i];
424
+ if (sel.type === SelectionType.UnselectedCells) {
425
+ const parts = removePartOfCellsSelection(sel, add);
426
+ if (parts) {
427
+ selections.splice(i, 1, ...parts);
428
+ i += parts.length - 1;
429
+ }
430
+ continue;
431
+ }
432
+ if (sel.type === SelectionType.Cells) {
433
+ const parts = removePartOfCellsSelection(add, sel);
434
+ if (Array.isArray(parts)) {
435
+ let result = false;
436
+ for (let i = 0; i < parts.length; i++) {
437
+ const isChanged = addIntoSelections(selections, parts[i], context);
438
+ if (isChanged) result = true;
439
+ }
440
+ if (result) cleanupSelections(selections);
441
+ return result;
442
+ }
443
+ }
444
+ }
445
+
446
+ // has context info of the grid
447
+ if (context) {
448
+ const selectedAllRows =
449
+ add.startRow === 0 && add.endRow + 1 >= context.rows;
450
+ const selectedAllColumns =
451
+ add.startColumn === 0 && add.endColumn + 1 >= context.columns;
452
+ let isChanged0, isChanged1;
453
+ if (selectedAllRows) {
454
+ isChanged0 = addIntoSelections(
455
+ selections,
456
+ {
457
+ type: SelectionType.Columns,
458
+ startColumn: add.startColumn,
459
+ endColumn: add.endColumn,
460
+ },
461
+ context,
462
+ );
463
+ }
464
+ if (selectedAllColumns) {
465
+ isChanged1 = addIntoSelections(
466
+ selections,
467
+ {
468
+ type: SelectionType.Rows,
469
+ startRow: add.startRow,
470
+ endRow: add.endRow,
471
+ },
472
+ context,
473
+ );
474
+ }
475
+ if (selectedAllRows || selectedAllColumns)
476
+ return isChanged0 || isChanged1;
477
+ }
478
+
479
+ selections.push(add);
480
+ cleanupSelections(selections);
481
+ return true;
482
+ }
483
+ // end of cells selection
484
+
485
+ const isRowsSelection = add.type === SelectionType.Rows;
486
+ if (isRowsSelection || add.type === SelectionType.Columns) {
487
+ for (let i = 0; i < selections.length; i++) {
488
+ const sel = selections[i];
489
+ if (sel.type === SelectionType.UnselectedCells) {
490
+ const parts = removePartOfCellsSelection(
491
+ sel,
492
+ isRowsSelection
493
+ ? {
494
+ startRow: add.startRow,
495
+ endRow: add.endRow,
496
+ startColumn: 0,
497
+ endColumn: sel.endColumn,
498
+ }
499
+ : {
500
+ startColumn: add.startColumn,
501
+ endColumn: add.endColumn,
502
+ startRow: 0,
503
+ endRow: sel.endRow,
504
+ },
505
+ );
506
+ if (parts) {
507
+ selections.splice(i, 1, ...parts);
508
+ i += parts.length - 1;
509
+ }
510
+ continue;
511
+ }
512
+ if (sel.type !== add.type) continue;
513
+ // try to concat them
514
+ let merged = mergeSelections(sel, add);
515
+ if (merged) {
516
+ for (let j = i + 1; j < selections.length; j++) {
517
+ if (selections[j].type !== add.type) continue;
518
+ const newMerged = mergeSelections(selections[j], merged);
519
+ if (newMerged) {
520
+ selections.splice(j, 1);
521
+ j--;
522
+ merged = newMerged;
523
+ }
524
+ }
525
+ selections[i] = merged;
526
+ cleanupSelections(selections);
527
+ return true;
528
+ }
529
+ }
530
+ selections.push(add);
531
+ cleanupSelections(selections);
532
+ return true;
533
+ }
534
+ return false;
535
+ };
536
+
537
+ /**
538
+ * Remove a selection area from `selections` array
539
+ * @param {SelectionDescriptor[]} selections
540
+ * @param {SelectionDescriptor} remove Supported types: Cells, Rows, Columns
541
+ * @param {ContextForSelectionAction} [context] Eg: {rows:1000, columns:1000}
542
+ * @returns {boolean} is the selections array changed
543
+ */
544
+ const removeFromSelections = (selections, remove, context) => {
545
+ if (!remove || typeof remove.type !== 'number') return false;
546
+ if (remove.type === SelectionType.Cells) {
547
+ for (let i = 0; i < selections.length; i++) {
548
+ const sel = selections[i];
549
+ if (sel.type === SelectionType.UnselectedCells) {
550
+ const parts = removePartOfCellsSelection(remove, sel);
551
+ if (Array.isArray(parts)) {
552
+ let result = false;
553
+ for (let i = 0; i < parts.length; i++) {
554
+ const isChanged = removeFromSelections(
555
+ selections,
556
+ parts[i],
557
+ context,
558
+ );
559
+ if (isChanged) result = true;
560
+ }
561
+ if (result) cleanupSelections(selections);
562
+ return result;
563
+ }
564
+ }
565
+ if (sel.type === SelectionType.Cells) {
566
+ const parts = removePartOfCellsSelection(sel, remove);
567
+ if (parts) {
568
+ selections.splice(i, 1, ...parts);
569
+ i += parts.length - 1;
570
+ }
571
+ continue;
572
+ }
573
+ }
574
+ // has context info of the grid
575
+ if (context) {
576
+ const unselectedAllRows =
577
+ remove.startRow === 0 && remove.endRow + 1 >= context.rows;
578
+ const unselectedAllColumns =
579
+ remove.startColumn === 0 && remove.endColumn + 1 >= context.columns;
580
+ let isChanged0, isChanged1;
581
+ if (unselectedAllRows) {
582
+ isChanged0 = removeFromSelections(
583
+ selections,
584
+ {
585
+ type: SelectionType.Columns,
586
+ startColumn: remove.startColumn,
587
+ endColumn: remove.endColumn,
588
+ },
589
+ context,
590
+ );
591
+ }
592
+ if (unselectedAllColumns) {
593
+ isChanged1 = removeFromSelections(
594
+ selections,
595
+ {
596
+ type: SelectionType.Rows,
597
+ startRow: remove.startRow,
598
+ endRow: remove.endRow,
599
+ },
600
+ context,
601
+ );
602
+ }
603
+ if (unselectedAllRows || unselectedAllColumns)
604
+ return isChanged0 || isChanged1;
605
+ }
606
+
607
+ remove.type = SelectionType.UnselectedCells;
608
+ selections.unshift(remove);
609
+ cleanupSelections(selections);
610
+ return true;
611
+ }
612
+ // end of cells selection
613
+
614
+ const isRowsSelection = remove.type === SelectionType.Rows;
615
+ if (isRowsSelection || remove.type === SelectionType.Columns) {
616
+ let isChanged = false;
617
+ for (let i = 0; i < selections.length; i++) {
618
+ const sel = selections[i];
619
+ if (sel.type === SelectionType.Cells) {
620
+ const parts = removePartOfCellsSelection(
621
+ sel,
622
+ isRowsSelection
623
+ ? {
624
+ startRow: remove.startRow,
625
+ endRow: remove.endRow,
626
+ startColumn: 0,
627
+ endColumn: sel.endColumn,
628
+ }
629
+ : {
630
+ startColumn: remove.startColumn,
631
+ endColumn: remove.endColumn,
632
+ startRow: 0,
633
+ endRow: sel.endRow,
634
+ },
635
+ );
636
+ if (parts) {
637
+ selections.splice(i, 1, ...parts);
638
+ i += parts.length - 1;
639
+ }
640
+ continue;
641
+ }
642
+ if (sel.type === SelectionType.UnselectedCells) {
643
+ if (isRowsSelection) {
644
+ if (sel.startRow >= remove.startRow && sel.endRow <= remove.endRow) {
645
+ selections.splice(i, 1);
646
+ i--;
647
+ }
648
+ } else {
649
+ if (
650
+ sel.startColumn >= remove.startColumn &&
651
+ sel.endColumn <= remove.endColumn
652
+ ) {
653
+ selections.splice(i, 1);
654
+ i--;
655
+ }
656
+ }
657
+ continue;
658
+ }
659
+ if (sel.type === remove.type) {
660
+ const newSelection = isRowsSelection
661
+ ? removePartOfRowsSelection(sel, remove)
662
+ : removePartOfColumnsSelection(sel, remove);
663
+ if (!newSelection) continue;
664
+ isChanged = true;
665
+ selections.splice(i, 1, ...newSelection);
666
+ i += newSelection.length - 1;
667
+ } else {
668
+ isChanged = true;
669
+ /** @type {SelectionDescriptor} */
670
+ let newSelection;
671
+ if (sel.type === SelectionType.Rows) {
672
+ newSelection = {
673
+ type: SelectionType.UnselectedCells,
674
+ startColumn: remove.startColumn,
675
+ endColumn: remove.endColumn,
676
+ startRow: sel.startRow,
677
+ endRow: sel.endRow,
678
+ };
679
+ } else {
680
+ newSelection = {
681
+ type: SelectionType.UnselectedCells,
682
+ startRow: remove.startRow,
683
+ endRow: remove.endRow,
684
+ startColumn: sel.startColumn,
685
+ endColumn: sel.endColumn,
686
+ };
687
+ }
688
+ selections.unshift(newSelection);
689
+ }
690
+ }
691
+ if (isChanged) cleanupSelections(selections);
692
+ return isChanged;
693
+ }
694
+ return false;
695
+ };
696
+
697
+ /**
698
+ * Clean up a selections array.
699
+ * This function removes unnecessary selection object, and it tries to merge different selections.
700
+ * @param {SelectionDescriptor[]} selections
701
+ */
702
+ const cleanupSelections = (selections) => {
703
+ let unselect = [];
704
+ let select = [];
705
+ for (let i = 0; i < selections.length; i++) {
706
+ const sel = selections[i];
707
+ if (sel.type === SelectionType.UnselectedCells) unselect.push(sel);
708
+ else select.push(sel);
709
+ }
710
+ // clean unused unselected objects
711
+ unselect = unselect.filter((unsel) => {
712
+ for (let i = 0; i < select.length; i++)
713
+ if (getIntersection(unsel, select[i])) return true;
714
+ return false;
715
+ });
716
+ // merge neighbor cells block selections
717
+ let endMerge = false;
718
+ while (!endMerge) {
719
+ endMerge = true;
720
+ for (let i = 0; i < select.length; i++) {
721
+ const sel0 = select[i];
722
+ for (let j = i + 1; j < select.length; j++) {
723
+ const sel1 = select[j];
724
+ const newSel = mergeSelections(sel0, sel1);
725
+ if (!newSel) continue;
726
+ select[i] = newSel;
727
+ select.splice(j, 1);
728
+ endMerge = false;
729
+ break;
730
+ }
731
+ if (!endMerge) break;
732
+ }
733
+ }
734
+ // save back to `selections`
735
+ let ptr = 0;
736
+ for (let i = 0; i < unselect.length; i++) selections[ptr++] = unselect[i];
737
+ for (let i = 0; i < select.length; i++) selections[ptr++] = select[i];
738
+ selections.splice(ptr, selections.length - ptr);
739
+ };
740
+
741
+ /**
742
+ * Check if all cells in a given row selected
743
+ * @param {SelectionDescriptor[]} selections
744
+ * @param {number} rowIndex
745
+ */
746
+ const isRowSelected = (selections, rowIndex) => {
747
+ for (let i = 0; i < selections.length; i++) {
748
+ const sel = selections[i];
749
+ switch (sel.type) {
750
+ case SelectionType.UnselectedCells:
751
+ if (rowIndex >= sel.startRow && rowIndex <= sel.endRow) return false;
752
+ break;
753
+ case SelectionType.Rows:
754
+ if (rowIndex >= sel.startRow && rowIndex <= sel.endRow) return true;
755
+ }
756
+ }
757
+ return false;
758
+ };
759
+
760
+ /**
761
+ * Check if all cells in a given column selected
762
+ * @param {SelectionDescriptor[]} selections
763
+ * @param {number} columnIndex
764
+ */
765
+ const isColumnSelected = (selections, columnIndex) => {
766
+ for (let i = 0; i < selections.length; i++) {
767
+ const sel = selections[i];
768
+ switch (sel.type) {
769
+ case SelectionType.UnselectedCells:
770
+ if (columnIndex >= sel.startColumn && columnIndex <= sel.endColumn)
771
+ return false;
772
+ break;
773
+ case SelectionType.Columns:
774
+ if (columnIndex >= sel.startColumn && columnIndex <= sel.endColumn)
775
+ return true;
776
+ }
777
+ }
778
+ return false;
779
+ };
780
+
781
+ /**
782
+ * Check if a given cell selected
783
+ * @param {SelectionDescriptor[]} selections
784
+ * @param {number} rowIndex
785
+ * @param {number} columnIndex
786
+ */
787
+ const isCellSelected = (selections, rowIndex, columnIndex) => {
788
+ for (let i = 0; i < selections.length; i++) {
789
+ const sel = selections[i];
790
+ switch (sel.type) {
791
+ case SelectionType.UnselectedCells:
792
+ case SelectionType.Cells:
793
+ if (
794
+ columnIndex >= sel.startColumn &&
795
+ columnIndex <= sel.endColumn &&
796
+ rowIndex >= sel.startRow &&
797
+ rowIndex <= sel.endRow
798
+ )
799
+ return sel.type === SelectionType.Cells;
800
+ break;
801
+ case SelectionType.Rows:
802
+ if (rowIndex >= sel.startRow && rowIndex <= sel.endRow) return true;
803
+ break;
804
+ case SelectionType.Columns:
805
+ if (columnIndex >= sel.startColumn && columnIndex <= sel.endColumn)
806
+ return true;
807
+ }
808
+ }
809
+ return false;
810
+ };
811
+
812
+ /**
813
+ * Check if all given cells selected
814
+ * @param {SelectionDescriptor[]} selections
815
+ * @param {RangeDescriptor} range the range of cells block
816
+ * @returns {boolean}
817
+ */
818
+ const areAllCellsSelected = (selections, range) => {
819
+ const {
820
+ startRow: startRow,
821
+ startColumn: startColumn,
822
+ endRow: endRow,
823
+ endColumn: endColumn,
824
+ } = range;
825
+ for (let i = 0, iTo = selections.length; i < iTo; i++) {
826
+ const sel = selections[i];
827
+ if (!sel) continue;
828
+ switch (sel.type) {
829
+ case SelectionType.UnselectedCells:
830
+ case SelectionType.Cells:
831
+ {
832
+ const matched0 =
833
+ startColumn >= sel.startColumn && startRow >= sel.startRow;
834
+ const matched1 = endColumn <= sel.endColumn && endRow <= sel.endRow;
835
+ if (matched0 || matched1) {
836
+ if (matched0 && matched1) return sel.type === SelectionType.Cells;
837
+ if (
838
+ (matched0 &&
839
+ startColumn <= sel.endColumn &&
840
+ startRow <= sel.endRow) ||
841
+ (matched1 &&
842
+ endColumn >= sel.startColumn &&
843
+ endRow >= sel.startRow)
844
+ )
845
+ return false;
846
+ }
847
+ }
848
+ break;
849
+ case SelectionType.Rows:
850
+ if (startRow >= sel.startRow) {
851
+ if (endRow <= sel.endRow) return true;
852
+ if (startRow <= sel.endRow) return false;
853
+ } else if (endRow >= sel.startRow) return false;
854
+ break;
855
+ case SelectionType.Columns:
856
+ if (startColumn >= sel.startColumn) {
857
+ if (endColumn <= sel.endColumn) return true;
858
+ if (startColumn <= sel.endColumn) return false;
859
+ } else if (endColumn >= sel.startColumn) return false;
860
+ }
861
+ }
862
+ return false;
863
+ };
864
+
865
+ /**
866
+ * Get selection state from cells
867
+ * @param {SelectionDescriptor[]} selections
868
+ * @param {RangeDescriptor} range
869
+ * @returns {boolean|boolean[][]} Returning `true` means all given cells are selected,
870
+ * Returning `false` means all given cells are not selected.
871
+ * Returning a two-dimensional array means some cells are selected and some cells are not selected.
872
+ * A reference for returned value: `state[rowIndex - range.startRow][colIndex - range.startColumn]`
873
+ */
874
+ const getSelectionStateFromCells = (selections, range) => {
875
+ if (!Array.isArray(selections) || selections.length === 0) return false;
876
+ selections = selections.filter((sel) => {
877
+ if (typeof sel.startRow === 'number')
878
+ if (sel.endRow < range.startRow || sel.startRow > range.endRow)
879
+ return false;
880
+ if (typeof sel.startColumn === 'number')
881
+ if (
882
+ sel.endColumn < range.startColumn ||
883
+ sel.startColumn > range.endColumn
884
+ )
885
+ return false;
886
+ return sel;
887
+ });
888
+ if (selections.length === 0) return false;
889
+ if (areAllCellsSelected(selections, range)) return true;
890
+
891
+ const countOfColumns = range.endColumn - range.startColumn + 1;
892
+ const result = new Array(range.endRow - range.startRow + 1)
893
+ .fill(null)
894
+ .map(() => new Array(countOfColumns));
895
+
896
+ const test = Object.assign({ type: SelectionType.Cells }, range);
897
+ for (let i = selections.length - 1; i >= 0; i--) {
898
+ const sel = selections[i];
899
+ const intersection = getIntersection(test, sel);
900
+ if (!intersection) continue;
901
+ const value = sel.type !== SelectionType.UnselectedCells;
902
+ for (
903
+ let rowIndex = intersection.startRow;
904
+ rowIndex <= intersection.endRow;
905
+ rowIndex++
906
+ ) {
907
+ const row = result[rowIndex - range.startRow];
908
+ let columnOffset = intersection.startColumn - range.startColumn;
909
+ const columnOffsetEnd = intersection.endColumn - range.startColumn;
910
+ for (; columnOffset <= columnOffsetEnd; columnOffset++)
911
+ row[columnOffset] = value;
912
+ }
913
+ }
914
+ return result;
915
+ };
916
+
917
+ /**
918
+ * Get verbose selection state from cells.
919
+ * (`verbose` in here means that you can locate particular selection for selected cells)
920
+ * @param {SelectionDescriptor[]} selections
921
+ * @param {RangeDescriptor} range
922
+ * @returns {number[][]} Each item in this two-dimensional array is 0 or a positive int,
923
+ * it represents the index of matched selection plus 1 if it is a positive int.
924
+ * And if the value of item is 0, it means this cell is not selected.
925
+ * A reference for returned value: `state[rowIndex - range.startRow][colIndex - range.startColumn]`
926
+ */
927
+ const getVerboseSelectionStateFromCells = (selections, range) => {
928
+ if (!Array.isArray(selections) || selections.length === 0) return [];
929
+ selections = selections.map((sel) => {
930
+ if (typeof sel.startRow === 'number')
931
+ if (sel.endRow < range.startRow || sel.startRow > range.endRow)
932
+ return null;
933
+ if (typeof sel.startColumn === 'number')
934
+ if (
935
+ sel.endColumn < range.startColumn ||
936
+ sel.startColumn > range.endColumn
937
+ )
938
+ return null;
939
+ return sel;
940
+ });
941
+ if (selections.length === 0) return [];
942
+
943
+ const countOfColumns = range.endColumn - range.startColumn + 1;
944
+ const result = new Array(range.endRow - range.startRow + 1)
945
+ .fill(null)
946
+ .map(() => new Array(countOfColumns));
947
+
948
+ const test = Object.assign({ type: SelectionType.Cells }, range);
949
+ for (let i = selections.length - 1; i >= 0; i--) {
950
+ const sel = selections[i];
951
+ if (!sel) continue;
952
+ const intersection = getIntersection(test, sel);
953
+ if (!intersection) continue;
954
+ const value = sel.type !== SelectionType.UnselectedCells;
955
+ for (
956
+ let rowIndex = intersection.startRow;
957
+ rowIndex <= intersection.endRow;
958
+ rowIndex++
959
+ ) {
960
+ const row = result[rowIndex - range.startRow];
961
+ let columnOffset = intersection.startColumn - range.startColumn;
962
+ const columnOffsetEnd = intersection.endColumn - range.startColumn;
963
+ for (; columnOffset <= columnOffsetEnd; columnOffset++)
964
+ row[columnOffset] = value ? i + 1 : 0;
965
+ }
966
+ }
967
+ return result;
968
+ };
969
+
970
+ /**
971
+ * Check if any contiguous columns are selected.
972
+ * (This function is useful for the preconditions for actions on columns, Eg: grouping, hiding)
973
+ * @param {SelectionDescriptor[]} selections
974
+ * @param {boolean} allowImpurity This function ignores other selected rows/cells if its value is `true`
975
+ * @returns {number[]} a tuple [beginViewColumnIndex, endViewColumnIndex] or `undefined`
976
+ */
977
+ const getSelectedContiguousColumns = (selections, allowImpurity) => {
978
+ if (!selections || selections.length === 0) return;
979
+ /** A selection object */
980
+ let matched;
981
+ for (let i = selections.length - 1; i >= 0; i--) {
982
+ const selection = selections[i];
983
+ switch (selection.type) {
984
+ case SelectionType.Columns:
985
+ if (matched) {
986
+ const newMatched = mergeSelections(matched, selection);
987
+ if (newMatched) {
988
+ matched = newMatched;
989
+ break;
990
+ }
991
+ }
992
+ matched = selection;
993
+ break;
994
+ case SelectionType.Rows:
995
+ case SelectionType.Cells:
996
+ if (!allowImpurity) return;
997
+ break;
998
+ case SelectionType.UnselectedCells:
999
+ if (!matched) return;
1000
+ if (getIntersection(selection, matched)) return;
1001
+ }
1002
+ }
1003
+ if (matched) return [matched.startColumn, matched.endColumn];
1004
+ };
1005
+
1006
+ /**
1007
+ * Check if any contiguous rows are selected.
1008
+ * (This function is useful for the preconditions for actions on rows, Eg: grouping, hiding)
1009
+ * @param {SelectionDescriptor[]} selections
1010
+ * @param {boolean} allowImpurity This function ignores other selected columns/cells if its value is `true`
1011
+ * @returns {number[]} a tuple [beginRowOrderIndex, endRowOrderIndex] or `undefined`
1012
+ */
1013
+ const getSelectedContiguousRows = (selections, allowImpurity) => {
1014
+ if (!selections || selections.length === 0) return;
1015
+ /** A selection object */
1016
+ let matched;
1017
+ for (let i = selections.length - 1; i >= 0; i--) {
1018
+ const selection = selections[i];
1019
+ switch (selection.type) {
1020
+ case SelectionType.Rows:
1021
+ if (matched) {
1022
+ const newMatched = mergeSelections(matched, selection);
1023
+ if (newMatched) {
1024
+ matched = newMatched;
1025
+ break;
1026
+ }
1027
+ }
1028
+ matched = selection;
1029
+ break;
1030
+ case SelectionType.Columns:
1031
+ case SelectionType.Cells:
1032
+ if (!allowImpurity) return;
1033
+ break;
1034
+ case SelectionType.UnselectedCells:
1035
+ if (!matched) return;
1036
+ if (getIntersection(selection, matched)) return;
1037
+ }
1038
+ }
1039
+ if (matched) return [matched.startRow, matched.endRow];
1040
+ };
1041
+
1042
+ /**
1043
+ * Check if current selections are complex.
1044
+ * How we defined "complex" in here:
1045
+ * - There are any unselected cells in selected rows/columns
1046
+ * - More than one selection type in current selections
1047
+ * @param {SelectionDescriptor[]} selections
1048
+ * @returns {boolean}
1049
+ */
1050
+ const areSelectionsComplex = (selections) => {
1051
+ if (!selections || selections.length <= 1) return false;
1052
+ const baseType = selections[0].type;
1053
+ if (baseType === SelectionType.UnselectedCells) return true;
1054
+ return selections.findIndex((it) => it.type !== baseType) >= 0;
1055
+ };
1056
+
1057
+ /**
1058
+ * Check if current selections are neat.
1059
+ * This method is used for make new API are compatible with obsolete API
1060
+ * For example:
1061
+ * - Selected like <0,0-10,10> or rows<5-10> is neat
1062
+ * - Selected like <0,0-10,10>&<11,1-11,10> is untidy
1063
+ * @param {SelectionDescriptor[]} selections selections after clean up
1064
+ * @returns {boolean}
1065
+ */
1066
+ const areSelectionsNeat = (selections) => {
1067
+ if (!selections) return false;
1068
+ if (selections.length === 1) return true;
1069
+
1070
+ const base = selections[0];
1071
+ const baseType = base.type;
1072
+ if (baseType === SelectionType.UnselectedCells) return true;
1073
+
1074
+ for (let i = 0; i < selections.length; i++) {
1075
+ const sel = selections[i];
1076
+ if (baseType !== sel.type) return false;
1077
+ if (baseType === SelectionType.Cells) {
1078
+ if (
1079
+ sel.startColumn === base.startColumn &&
1080
+ sel.endColumn === base.endColumn
1081
+ )
1082
+ continue;
1083
+ if (sel.startRow === base.startRow && sel.endRow === base.endRow)
1084
+ continue;
1085
+ // they are not aligned
1086
+ return false;
1087
+ }
1088
+ }
1089
+ return true;
1090
+ };
1091
+
1092
+ /**
1093
+ * @param {SelectionDescriptor[]} selections
1094
+ * @param {number} offsetX
1095
+ * @param {number} offsetY
1096
+ */
1097
+ const moveSelections = (selections, offsetX, offsetY) => {
1098
+ for (let i = 0; i < selections.length; i++) {
1099
+ const selection = selections[i];
1100
+ switch (selection.type) {
1101
+ case SelectionType.Cells:
1102
+ case SelectionType.UnselectedCells:
1103
+ selection.startRow += offsetY;
1104
+ selection.endRow += offsetY;
1105
+ selection.startColumn += offsetX;
1106
+ selection.endColumn += offsetX;
1107
+ break;
1108
+ case SelectionType.Rows:
1109
+ selection.startRow += offsetY;
1110
+ selection.endRow += offsetY;
1111
+ break;
1112
+ case SelectionType.Columns:
1113
+ selection.startColumn += offsetX;
1114
+ selection.endColumn += offsetX;
1115
+ break;
1116
+ }
1117
+ }
1118
+ };
1119
+
1120
+ /**
1121
+ * @param {SelectionDescriptor[]} selections
1122
+ * @returns {SelectionDescriptor[]}
1123
+ */
1124
+ const cloneSelections = (selections) => {
1125
+ const clonedSelections = [];
1126
+ for (let i = 0; i < selections.length; i++) {
1127
+ const sel = selections[i];
1128
+ if (!sel) continue;
1129
+ clonedSelections.push(Object.assign({}, sel));
1130
+ }
1131
+ return clonedSelections;
1132
+ };
1133
+
1134
+ /**
1135
+ * @param {SelectionDescriptor[]} selections
1136
+ * @returns {RectangleObject}
1137
+ */
1138
+ const getSelectionBounds = (selections) => {
1139
+ /**
1140
+ * The reason why the initialize values are `Infinity` is used for making
1141
+ * the bound is compatible with the obsolete API.
1142
+ */
1143
+ let top = Infinity,
1144
+ bottom = -Infinity,
1145
+ left = Infinity,
1146
+ right = -Infinity;
1147
+ for (let i = 0; i < selections.length; i++) {
1148
+ const sel = selections[i];
1149
+ if (sel.type === SelectionType.UnselectedCells) continue;
1150
+
1151
+ if (typeof sel.startColumn !== 'number') left = 0;
1152
+ else if (sel.startColumn < left) left = sel.startColumn;
1153
+
1154
+ if (typeof sel.endColumn !== 'number') right = Infinity;
1155
+ else if (sel.endColumn > right) right = sel.endColumn;
1156
+
1157
+ if (typeof sel.startRow !== 'number') top = 0;
1158
+ else if (sel.startRow < top) top = sel.startRow;
1159
+
1160
+ if (typeof sel.endRow !== 'number') bottom = Infinity;
1161
+ else if (sel.endRow > bottom) bottom = sel.endRow;
1162
+ }
1163
+ return { top, left, bottom, right };
1164
+ };
1165
+
1166
+ /**
1167
+ * @param {SelectionDescriptor[]} selections
1168
+ * @param {object} cell Signature: `{rowIndex:number;columnIndex:number}`
1169
+ * @param {object} keyEvent Signature: `{key:string;shiftKey:boolean}`
1170
+ * @param {object} context Signature: `{columns:number;rows:number}`
1171
+ * @returns {boolean}
1172
+ */
1173
+ const shrinkOrExpandSelections = (selections, cell, keyEvent, context) => {
1174
+ const { rowIndex, columnIndex } = cell;
1175
+ if (rowIndex < 0 || columnIndex < 0) return false;
1176
+
1177
+ const keyInfo = {
1178
+ ArrowLeft: { x: 1, y: 0, isLeft: true },
1179
+ ArrowUp: { x: 0, y: 1, isUp: true },
1180
+ ArrowRight: { x: -1, y: 0, isRight: true },
1181
+ ArrowDown: { x: 0, y: -1, isDown: true },
1182
+ }[keyEvent.key];
1183
+ if (!keyInfo) return false;
1184
+
1185
+ const originalRowIndex = rowIndex + keyInfo.y;
1186
+ const originalColumnIndex = columnIndex + keyInfo.x;
1187
+
1188
+ /** @type {number[]} */
1189
+ let matchedSelections = [];
1190
+ for (let i = 0; i < selections.length; i++) {
1191
+ /** @type {SelectionDescriptor} */
1192
+ const sel = selections[i];
1193
+ switch (sel.type) {
1194
+ case SelectionType.Cells:
1195
+ case SelectionType.UnselectedCells:
1196
+ if (
1197
+ (originalRowIndex === sel.startRow ||
1198
+ originalRowIndex === sel.endRow) &&
1199
+ (originalColumnIndex === sel.startColumn ||
1200
+ originalColumnIndex === sel.endColumn)
1201
+ ) {
1202
+ // we can't shrink or expand from a unselected areas
1203
+ if (sel.type === SelectionType.UnselectedCells) return false;
1204
+ matchedSelections.push(sel);
1205
+ }
1206
+ break;
1207
+ case SelectionType.Rows:
1208
+ if (
1209
+ originalRowIndex === sel.startRow ||
1210
+ originalRowIndex === sel.endRow
1211
+ )
1212
+ matchedSelections.push(sel);
1213
+ break;
1214
+ case SelectionType.Columns:
1215
+ if (
1216
+ originalColumnIndex === sel.startColumn ||
1217
+ originalColumnIndex === sel.endColumn
1218
+ )
1219
+ matchedSelections.push(sel);
1220
+ break;
1221
+ }
1222
+ }
1223
+ // The selection is complex.
1224
+ // Or the expanding / shrinking action is not from the border cell of selected area
1225
+ if (matchedSelections.length !== 1) return false;
1226
+ const selection = matchedSelections[0];
1227
+
1228
+ const isTopBorder = selection.startRow === originalRowIndex;
1229
+ const isBottomBorder = selection.endRow === originalRowIndex;
1230
+
1231
+ const isLeftBorder = selection.startColumn === originalColumnIndex;
1232
+ const isRightBorder = selection.endColumn === originalColumnIndex;
1233
+
1234
+ const maxRow = context ? context.rows - 1 : Infinity;
1235
+ const maxColumn = context ? context.columns - 1 : Infinity;
1236
+
1237
+ const afterChange = () => {
1238
+ cleanupSelections(selections);
1239
+ return true;
1240
+ };
1241
+
1242
+ if (isTopBorder) {
1243
+ if (keyInfo.isUp) {
1244
+ // at the topmost row
1245
+ if (selection.startRow === 0) return false;
1246
+ // expand to the up
1247
+ selection.startRow--;
1248
+ return afterChange();
1249
+ } else if (keyInfo.isDown && selection.startRow < selection.endRow) {
1250
+ selection.startRow++;
1251
+ return afterChange();
1252
+ }
1253
+ }
1254
+ if (isBottomBorder) {
1255
+ if (keyInfo.isDown) {
1256
+ // at the bottomost row
1257
+ if (selection.endRow >= maxRow) return false;
1258
+ // expand to the down
1259
+ selection.endRow++;
1260
+ return afterChange();
1261
+ } else if (keyInfo.isUp && selection.startRow < selection.endRow) {
1262
+ selection.endRow--;
1263
+ return afterChange();
1264
+ }
1265
+ }
1266
+ if (isLeftBorder) {
1267
+ if (keyInfo.isLeft) {
1268
+ // at the leftmost column
1269
+ if (selection.startColumn === 0) return false;
1270
+ // expand to the left
1271
+ selection.startColumn--;
1272
+ return afterChange();
1273
+ } else if (keyInfo.isRight && selection.startColumn < selection.endColumn) {
1274
+ selection.startColumn++;
1275
+ return afterChange();
1276
+ }
1277
+ }
1278
+ if (isRightBorder) {
1279
+ if (keyInfo.isRight) {
1280
+ // at the rightmost column
1281
+ if (selection.endColumn >= maxColumn) return false;
1282
+ // expand to the down
1283
+ selection.endColumn++;
1284
+ return afterChange();
1285
+ } else if (keyInfo.isLeft && selection.startColumn < selection.endColumn) {
1286
+ selection.endColumn--;
1287
+ return afterChange();
1288
+ }
1289
+ }
1290
+ return false;
1291
+ };
1292
+
1293
+ export {
1294
+ SelectionType,
1295
+ getSelectionFromString,
1296
+ normalizeSelection,
1297
+ addIntoSelections,
1298
+ removeFromSelections,
1299
+ mergeSelections,
1300
+ removePartOfRowsSelection,
1301
+ removePartOfColumnsSelection,
1302
+ removePartOfCellsSelection,
1303
+ cleanupSelections,
1304
+ getIntersection,
1305
+ isRowSelected,
1306
+ isColumnSelected,
1307
+ isCellSelected,
1308
+ areAllCellsSelected,
1309
+ areSelectionsNeat,
1310
+ getSelectionStateFromCells,
1311
+ getVerboseSelectionStateFromCells,
1312
+ areSelectionsComplex,
1313
+ getSelectedContiguousColumns,
1314
+ getSelectedContiguousRows,
1315
+ moveSelections,
1316
+ cloneSelections,
1317
+ getSelectionBounds,
1318
+ shrinkOrExpandSelections,
1319
+ };