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,985 @@
1
+ /*jslint browser: true, unparam: true, todo: true*/
2
+ /*globals define: true, MutationObserver: false, requestAnimationFrame: false, performance: false, btoa: false, Event: false*/
3
+ /// <reference path="./type.d.ts" />
4
+ 'use strict';
5
+
6
+ import { createHTMLString, createTextString } from '../events/util';
7
+ import {
8
+ addIntoSelections,
9
+ areSelectionsComplex,
10
+ areSelectionsNeat,
11
+ cleanupSelections,
12
+ cloneSelections,
13
+ getSelectedContiguousColumns,
14
+ getSelectedContiguousRows,
15
+ getSelectionBounds,
16
+ getSelectionStateFromCells,
17
+ isCellSelected,
18
+ isColumnSelected,
19
+ isRowSelected,
20
+ moveSelections,
21
+ normalizeSelection,
22
+ removeFromSelections,
23
+ SelectionType,
24
+ shrinkOrExpandSelections,
25
+ } from './util';
26
+
27
+ /**
28
+ * Before reading the code of this module, you must know these things:
29
+ *
30
+ * You may see words like "obsolete", "old API", "compatibility" in the code later.
31
+ * Because this grid component has used a different selection mechanism in the past.
32
+ *
33
+ * The old mechanism use a 2D array to represent the selection state.
34
+ * For example: `[undefined x 10, [0, 5]]` indicates
35
+ * there are two cells are selected in current grid. they are located in row 11.
36
+ * and one in column 1, another in column 6.
37
+ * And `[undefined x 5, [-1, 0,1,2,3,4,5,6...], undefined x 2, [1]]` indicates
38
+ * the row 6 is selected and the cell that located in row 9 column 2 is selected.
39
+ *
40
+ * But this implementation has performance issue when the grid is shipping a large dataset.
41
+ * You can just imagine about what will happen when you select all cells in a grid with 1 million rows.
42
+ *
43
+ * So we create a new way to implement the selection.
44
+ * In this way, we use a selection list to represent the selection state.
45
+ * Each element in this selection list is a selection descriptor.
46
+ * Each descriptor represents an area whose selected or unselected by the user.
47
+ * And we have four types for the descriptor: UnselectedCells, Cells, Rows and Columns
48
+ *
49
+ * Because the old selection mechanism has been in used for a long time and
50
+ * we have many public APIs about it.
51
+ * We still need to keep these APIs working in the 0.x versions in the future.
52
+ * So we create some functions to make sure these APIs and context objects work like previous version.
53
+ * This is the reason why you may see there are some functions in this file
54
+ * be marked "for compatibility" without best practice.
55
+ */
56
+ export default function (self) {
57
+ /**
58
+ * Here are explanation for the item of this array:
59
+ *
60
+ * The type of properties about the row in it is viewRowIndex (startRow, endRow).
61
+ * Here is an example of accessing the row data from the selection object:
62
+ * `const rowData = originalData[self.getBoundRowIndexFromViewRowIndex(selection.startRow)]`
63
+ *
64
+ * The type of properties about the column in it is columnOrderIndex (startColumn, endColumn)
65
+ * Here is an example of accessing thr column schema from the selection object:
66
+ * `const columnSchema = schema[self.orders.columns[selection.startColumn]]`
67
+ * @type {SelectionDescriptor[]}
68
+ */
69
+ self.selections = [];
70
+
71
+ /**
72
+ * TODO support the cache
73
+ */
74
+ self.selectionCache = [];
75
+ self.selectionCacheWidth = 0;
76
+ self.selectionCacheHeight = 0;
77
+
78
+ /**
79
+ * This method is used for adapting old API and it has performance issue for the large data set.
80
+ * We should keep this function and its related functions until version 1.x,
81
+ * because this action can cause incompatible API changes <https://semver.org/>
82
+ *
83
+ * Generate a selection 2 dimensional array from the selection list.
84
+ * Each row in this 2D array is a number array that indicates which columns are selected in this row.
85
+ * And -1 in the result means entire cells are selected in particular row/column
86
+ *
87
+ * @param {object[]} [otherSelections] The function create a 2d array based on this selection list
88
+ * if this parameter is provided. Otherwise, this function uses self.selections as the list input.
89
+ * @returns {number[][]}
90
+ */
91
+ self.getObsoleteSelectionMatrix = (otherSelections) => {
92
+ const bounds = self.getSelectionBounds(true, otherSelections);
93
+ if (!bounds) return [];
94
+ const { top, bottom, left, right } = bounds;
95
+ const height = bottom - top + 1;
96
+ const width = right - left + 1;
97
+ const states = getSelectionStateFromCells(self.selections, {
98
+ startRow: top,
99
+ endRow: bottom,
100
+ startColumn: left,
101
+ endColumn: right,
102
+ });
103
+ if (states === false) return [];
104
+
105
+ const result = [];
106
+ for (let row = top, row2 = top + height; row < row2; row++) {
107
+ if (states === true) {
108
+ result[row] = new Array(width).fill(0).map((_, i) => left + i);
109
+ continue;
110
+ }
111
+
112
+ const rowState = states[row - top];
113
+ if (!rowState) continue;
114
+ result[row] = [];
115
+ for (let col = left, col2 = left + width; col < col2; col++) {
116
+ if (rowState !== true && !rowState[col - left]) continue;
117
+ result[row].push(col);
118
+ }
119
+ }
120
+ for (let i = 0; i < self.selections.length; i++) {
121
+ const selection = self.selections[i];
122
+ if (selection.type !== SelectionType.Rows) continue;
123
+ for (let row = selection.startRow; row <= selection.endRow; row++) {
124
+ if (!result[row]) {
125
+ result[row] = [-1];
126
+ continue;
127
+ }
128
+ if (result[row][0] === -1) continue;
129
+ result[row].unshift(-1);
130
+ }
131
+ }
132
+ return result;
133
+ };
134
+
135
+ /**
136
+ * Return a tuple if the user selected contiguous columns, otherwise `null`.
137
+ * Info: Because the user may reorder the columns,
138
+ * the schemaIndex of the first item may be greater than the schemaIndex of the second item,
139
+ * but the columnIndex of the firs item must less than the columnIndex of the second item.
140
+ * @param {object[]} [schema] from `self.getSchema()`
141
+ * @returns {object[]} column schemas tuple (each schema has an additional field `schemaIndex`)
142
+ */
143
+ self.getSelectedContiguousColumns = function (schema) {
144
+ if (!schema) schema = self.getSchema();
145
+ const columnOrderIndexes = getSelectedContiguousColumns(
146
+ self.selections,
147
+ false,
148
+ );
149
+ if (!Array.isArray(columnOrderIndexes)) return;
150
+
151
+ /** @type {number[]} orders[index] => columnIndex */
152
+ const orders = self.orders.columns;
153
+ return columnOrderIndexes.map((orderIndex) => {
154
+ const schemaIndex = orders[orderIndex];
155
+ const columnSchema = schema[schemaIndex];
156
+ return Object.assign({}, columnSchema, { orderIndex });
157
+ });
158
+ };
159
+
160
+ /**
161
+ * @param {boolean} [allowOnlyOneRow]
162
+ * @returns {number[]} a viewRowIndex tuple. It can contains one row index or two row indexes.
163
+ */
164
+ self.getSelectedContiguousRows = function (allowOnlyOneRow) {
165
+ const viewRowIndexes = getSelectedContiguousRows(self.selections, false);
166
+ if (!Array.isArray(viewRowIndexes)) return;
167
+ if (!allowOnlyOneRow && viewRowIndexes[0] === viewRowIndexes[1]) return;
168
+ return viewRowIndexes;
169
+ };
170
+
171
+ /**
172
+ * Check if current selection are copyable for the system clipbaord
173
+ * @returns {boolean}
174
+ */
175
+ self.canSelectionsBeCopied = function () {
176
+ return self.selections.length > 0 && !areSelectionsComplex(self.selections);
177
+ };
178
+
179
+ /**
180
+ * Return the height of the selection area.
181
+ * This function is used for rendering the overlay for the dnd(drag and drop)
182
+ * @returns {number} The height of the selection area
183
+ */
184
+ self.getSelectedRowsHeight = function () {
185
+ const selected = getSelectedContiguousRows(self.selections, true);
186
+ if (!selected) return 0;
187
+ let height = 0;
188
+ for (let row = selected[0]; row <= selected[1]; row++)
189
+ height += self.getRowHeight(row);
190
+ return height;
191
+ };
192
+
193
+ /**
194
+ * Return the width of the selection area.
195
+ * This function is used for rendering the overlay for the dnd(drag and drop)
196
+ * @returns {number} The width of the selection area
197
+ */
198
+ self.getSelectedColumnsWidth = function () {
199
+ const selected = getSelectedContiguousColumns(self.selections, true);
200
+ if (!selected) return 0;
201
+ let width = 0;
202
+ for (let col = selected[0]; col <= selected[1]; col++)
203
+ width += self.getColumnWidth(self.orders.columns[col]);
204
+ return width;
205
+ };
206
+
207
+ /**
208
+ * @param {object} cell This method needs two properties in this parameter: `rowIndex`
209
+ * and `viewColumnIndex`
210
+ */
211
+ self.isCellSelected = (cell) => {
212
+ return isCellSelected(self.selections, cell.rowIndex, cell.viewColumnIndex);
213
+ };
214
+
215
+ /**
216
+ * Returns true if the selected columnIndex is selected on every row.
217
+ * @memberof canvasDatagrid
218
+ * @name isColumnSelected
219
+ * @method
220
+ * @param {number} columnIndex The column index to check.
221
+ */
222
+ self.isColumnSelected = (viewColumnIndex) => {
223
+ if (typeof viewColumnIndex !== 'number') return false;
224
+ return isColumnSelected(self.selections, viewColumnIndex);
225
+ };
226
+
227
+ /**
228
+ * Returns true if the selected rowIndex is selected on every column.
229
+ * @memberof canvasDatagrid
230
+ * @name isRowSelected
231
+ * @method
232
+ * @param {number} rowIndex The row index to check.
233
+ */
234
+ self.isRowSelected = (rowIndex) => {
235
+ if (typeof rowIndex !== 'number') return false;
236
+ return isRowSelected(self.selections, rowIndex);
237
+ };
238
+
239
+ /**
240
+ * Removes the selection.
241
+ * @param {boolean} triggerEvent true for trigger a event
242
+ */
243
+ self.clearSelections = (triggerEvent) => {
244
+ self.selections = [];
245
+ if (triggerEvent) self.dispatchSelectionChangedEvent();
246
+ };
247
+
248
+ /**
249
+ * Removes the selection.
250
+ * @memberof canvasDatagrid
251
+ * @name selectNone
252
+ * @param {boolean} dontDraw Suppress the draw method after the selection change.
253
+ * @method
254
+ */
255
+ self.selectNone = (dontDraw) => {
256
+ self.clearSelections(true);
257
+ if (dontDraw) return;
258
+ self.draw();
259
+ };
260
+
261
+ /**
262
+ * Selects every visible cell.
263
+ * @memberof canvasDatagrid
264
+ * @name selectAll
265
+ * @param {boolean} dontDraw Suppress the draw method after the selection change.
266
+ * @method
267
+ */
268
+ self.selectAll = function (dontDraw) {
269
+ const changed = addIntoSelections(self.selections, {
270
+ type: SelectionType.Columns,
271
+ startColumn: 0,
272
+ endColumn: self.getSchema().length - 1,
273
+ });
274
+ if (!changed || dontDraw) return;
275
+ self.draw();
276
+ };
277
+
278
+ /**
279
+ * @param {object} cell This method needs two properties in this parameter: `rowIndex`
280
+ * and `viewColumnIndex`
281
+ * @param {boolean} [suppressEvent]
282
+ */
283
+ self.unselectCell = (cell, suppressEvent) => {
284
+ const result = removeFromSelections(
285
+ self.selections,
286
+ normalizeSelection({
287
+ type: SelectionType.Cells,
288
+ startRow: cell.rowIndex,
289
+ startColumn: cell.viewColumnIndex,
290
+ }),
291
+ );
292
+ if (result && !suppressEvent) self.dispatchSelectionChangedEvent();
293
+ return result;
294
+ };
295
+
296
+ /**
297
+ * @param {number} startRow rowViewIndex
298
+ * @param {number} endRow rowViewIndex
299
+ * @param {boolean} suppressEvent When true, prevents the `selectionchanged` event from firing.
300
+ * @returns {boolean} It returns whether the selection changed
301
+ */
302
+ self.unselectRows = (startRow, endRow, suppressEvent) => {
303
+ const result = removeFromSelections(
304
+ self.selections,
305
+ normalizeSelection({
306
+ type: SelectionType.Rows,
307
+ startRow: startRow,
308
+ endRow: endRow,
309
+ }),
310
+ );
311
+ if (result && !suppressEvent) self.dispatchSelectionChangedEvent();
312
+ return result;
313
+ };
314
+
315
+ /**
316
+ * @param {object} cell This method needs two properties in this parameter: `rowIndex`
317
+ * and `viewColumnIndex`
318
+ * @param {boolean} [suppressEvent]
319
+ */
320
+ self.selectCell = (cell, suppressEvent) => {
321
+ const result = addIntoSelections(
322
+ self.selections,
323
+ normalizeSelection({
324
+ type: SelectionType.Cells,
325
+ startRow: cell.rowIndex,
326
+ startColumn: cell.viewColumnIndex,
327
+ }),
328
+ );
329
+ if (result && !suppressEvent) self.dispatchSelectionChangedEvent();
330
+ return result;
331
+ };
332
+
333
+ /**
334
+ * Moves the current selection relative to the its current position. Note: this method does not move the selected data, just the selection itself.
335
+ * @memberof canvasDatagrid
336
+ * @name moveSelection
337
+ * @method
338
+ * @param {number} offsetX The number of columns to offset the selection.
339
+ * @param {number} offsetY The number of rows to offset the selection.
340
+ */
341
+ self.moveSelection = function (offsetX, offsetY) {
342
+ moveSelections(self.selections, offsetX, offsetY);
343
+ };
344
+
345
+ /**
346
+ * Dispatch a event named `selectionchanged` with context info
347
+ */
348
+ self.dispatchSelectionChangedEvent = () => {
349
+ self.dispatchEvent('selectionchanged', self.getContextOfSelectionEvent());
350
+ };
351
+
352
+ /**
353
+ * Return the event context for event `selectionchanged`
354
+ * @returns {object} the context for the event
355
+ */
356
+ self.getContextOfSelectionEvent = () => {
357
+ const context = { selectionList: self.cloneSelections() };
358
+ //#region for API compatibility
359
+ Object.defineProperty(context, 'selections', {
360
+ get: function () {
361
+ return self.getObsoleteSelectionMatrix();
362
+ },
363
+ });
364
+ Object.defineProperty(context, 'selectedData', {
365
+ get: function () {
366
+ return self.getSelectedData();
367
+ },
368
+ });
369
+ Object.defineProperty(context, 'selectedCells', {
370
+ get: function () {
371
+ return self.getSelectedCells();
372
+ },
373
+ });
374
+ Object.defineProperty(context, 'selectionBounds', {
375
+ get: function () {
376
+ return self.getSelectionBounds();
377
+ },
378
+ });
379
+ //#endregion for API compatibility
380
+ return context;
381
+ };
382
+
383
+ /**
384
+ * Return a cloned selection list from current selection list
385
+ * @returns {object[]} A cloned selection list
386
+ */
387
+ self.cloneSelections = () => {
388
+ return cloneSelections(self.selections);
389
+ };
390
+
391
+ /**
392
+ * Gets the bounds of current selection.
393
+ * @param {boolean} [sanitized] sanitize the bound object if the value of thie paramater is `true`
394
+ * @memberof canvasDatagrid
395
+ * @name getSelectionBounds
396
+ * @method
397
+ * @returns {rect} two situations:
398
+ * 1. The result is always a bound object if `sanitized` is not `true`.
399
+ * And the result is `{top: Infinity, left: Infinity, bottom: -Infinity, right: -Infinity}` if there haven't selections.
400
+ * This is used for keeping compatibility with existing APIs.
401
+ * 2. When the parameter `sanitized` is true. The result will be null if there haven't selections.
402
+ */
403
+ self.getSelectionBounds = (sanitized, otherSelections) => {
404
+ const bounds = getSelectionBounds(otherSelections || self.selections);
405
+ if (sanitized) {
406
+ // nothing is selected
407
+ if (bounds.top > bounds.bottom || bounds.left > bounds.right) return null;
408
+ self.sanitizeSelectionBounds(bounds);
409
+ }
410
+ return bounds;
411
+ };
412
+ self.sanitizeSelectionBounds = (bounds) => {
413
+ if (!bounds) return bounds;
414
+ if (bounds.top < 0) bounds.top = 0;
415
+ if (bounds.left < 0) bounds.left = 0;
416
+
417
+ const viewDataLength = self.viewData.length;
418
+ if (bounds.bottom > viewDataLength) bounds.bottom = viewDataLength - 1;
419
+
420
+ const schemaLength = self.getSchema().length;
421
+ if (bounds.right > schemaLength) bounds.right = schemaLength - 1;
422
+ return bounds;
423
+ };
424
+
425
+ /**
426
+ * This function is migrated from the old API,
427
+ * It needs to be refactoring/deprecated to work with the large dataset.
428
+ * @param {boolean} [expandToRow]
429
+ * @returns {any[]}
430
+ */
431
+ self.getSelectedData = (expandToRow) => {
432
+ const bounds = self.getSelectionBounds(true);
433
+ if (!bounds) return [];
434
+
435
+ const selectedData = [];
436
+ const { top, bottom, left, right } = bounds;
437
+ const height = bottom - top + 1;
438
+ const width = right - left + 1;
439
+ const states = getSelectionStateFromCells(self.selections, {
440
+ startRow: top,
441
+ endRow: bottom,
442
+ startColumn: left,
443
+ endColumn: right,
444
+ });
445
+ if (states === false) return [];
446
+
447
+ const orderedSchema = getSchemaOrderByViewIndex(left, width);
448
+ for (let row = top, row2 = top + height; row < row2; row++) {
449
+ const viewData = self.viewData[row] || {};
450
+ const rowData = {};
451
+ let hasData = false;
452
+ const rowState = states === true || states[row - top];
453
+ if (!rowState) continue;
454
+
455
+ for (let col = left, col2 = left + width; col < col2; col++) {
456
+ if (states !== true && !rowState[col - left]) continue;
457
+ const header = orderedSchema[col];
458
+ if (!header || header.hidden) continue;
459
+ hasData = true;
460
+ if (expandToRow) break;
461
+ rowData[header.name] = viewData[header.name];
462
+ }
463
+ if (hasData)
464
+ selectedData[row] = expandToRow ? Object.assign({}, viewData) : rowData;
465
+ }
466
+ return selectedData;
467
+ };
468
+
469
+ /**
470
+ * This function is migrated from the old API,
471
+ * @param {boolean} [expandToRow]
472
+ * @returns {any[]}
473
+ */
474
+ self.getSelectedCells = (expandToRow) => {
475
+ const selectedCells = [];
476
+ const bounds = self.getSelectionBounds(true);
477
+ iterateSelectedCells(
478
+ (cell) => selectedCells.push(cell),
479
+ bounds,
480
+ expandToRow,
481
+ );
482
+ return selectedCells;
483
+ };
484
+
485
+ /**
486
+ * This function is migrated from the old API,
487
+ * @returns {any[]} affected cells
488
+ */
489
+ self.clearSelectedCells = () => {
490
+ const affectedCells = [];
491
+ const bounds = self.getSelectionBounds(true);
492
+ iterateSelectedCells(
493
+ (cell) => {
494
+ affectedCells.push(cell);
495
+ self.viewData[cell.boundRowIndex][cell.columnName] = '';
496
+ },
497
+ bounds,
498
+ false,
499
+ );
500
+ return affectedCells;
501
+ };
502
+
503
+ /**
504
+ * Deletes currently selected data.
505
+ * @memberof canvasDatagrid
506
+ * @name deleteSelectedData
507
+ * @method
508
+ * @param {boolean} dontDraw Suppress the draw method after the selection change.
509
+ */
510
+ self.deleteSelectedData = (dontDraw) => {
511
+ const affectedCells = self.clearSelectedCells();
512
+ const apiCompatibleCells = affectedCells.map((cell) => {
513
+ return [
514
+ cell.viewRowIndex,
515
+ cell.viewColumnIndex,
516
+ cell.boundRowIndex,
517
+ cell.boundColumnIndex,
518
+ ];
519
+ });
520
+ self.dispatchEvent('afterdelete', {
521
+ cells: apiCompatibleCells,
522
+ });
523
+ if (dontDraw) return;
524
+ requestAnimationFrame(() => self.draw());
525
+ };
526
+
527
+ /**
528
+ * Runs the defined method on each selected cell.
529
+ * @memberof canvasDatagrid
530
+ * @name forEachSelectedCell
531
+ * @method
532
+ * @param {number} fn The function to execute. The signature of the function is: (data, rowIndex, columnName).
533
+ * @param {number} expandToRow When true the data in the array is expanded to the entire row.
534
+ */
535
+ self.forEachSelectedCell = function (fn, expandToRow) {
536
+ const bounds = self.getSelectionBounds(true);
537
+ iterateSelectedCells(
538
+ (cell) => fn(self.viewData, cell.viewRowIndex, cell.columnName, cell),
539
+ bounds,
540
+ expandToRow,
541
+ );
542
+ };
543
+
544
+ /**
545
+ * @param {ClipboardInterface} clipboardData
546
+ */
547
+ self.copySelectedCellsToClipboard = (clipboardData) => {
548
+ const isNeat = areSelectionsNeat(self.selections);
549
+ const data = self.getSelectedData();
550
+ if (data.length > 0) {
551
+ // TODO: improve these two creating string method
552
+ const textString = createTextString(data, isNeat);
553
+ const htmlString = createHTMLString(data, isNeat);
554
+
555
+ const copiedData = {
556
+ 'text/plain': textString,
557
+ 'text/html': htmlString,
558
+ 'text/csv': textString,
559
+ 'application/json': JSON.stringify(data),
560
+ };
561
+ for (const [mimeType, data] of Object.entries(copiedData)) {
562
+ clipboardData.setData(mimeType, data);
563
+ }
564
+ }
565
+ };
566
+
567
+ /**
568
+ * (This function is migrated from the old API)
569
+ * Modify the width of columns that contain selected cells to fit the content.
570
+ * @param {number} [width] Custom width for each column
571
+ */
572
+ self.fitSelectedColumns = (width) => {
573
+ const bounds = self.getSelectionBounds(true);
574
+ if (!bounds) return;
575
+
576
+ const hasCustomWidth = typeof width === 'number' && width > 0;
577
+ // convert rectangle's height to only 1 row
578
+ bounds.bottom = bounds.top;
579
+ iterateSelectedCells(
580
+ (cell) => {
581
+ const { columnName, viewColumnIndex } = cell;
582
+ if (hasCustomWidth) {
583
+ self.sizes.columns[viewColumnIndex] = width;
584
+ self.dispatchEvent('resizecolumn', {
585
+ // TODO: Drop x, y from all 'resizecolumn' events and use height and width instead.
586
+ x: width,
587
+ y: self.resizingStartingHeight,
588
+ width: width,
589
+ height: self.resizingStartingHeight,
590
+ columnIndex: viewColumnIndex,
591
+ draggingItem: self.currentCell,
592
+ });
593
+ } else {
594
+ self.fitColumnToValues(columnName);
595
+ }
596
+ },
597
+ bounds,
598
+ false,
599
+ );
600
+ };
601
+
602
+ /**
603
+ * Set the height of the selected rows to the given value.
604
+ * @param {number} [height]
605
+ */
606
+ self.fitSelectedRows = (height) => {
607
+ const bounds = self.getSelectionBounds(true);
608
+ if (!bounds) return;
609
+
610
+ for (let row = bounds.top; row <= bounds.bottom; row++) {
611
+ if (!self.isRowSelected(row)) {
612
+ continue;
613
+ }
614
+ self.sizes.rows[row] = height;
615
+ self.dispatchEvent('resizerow', {
616
+ width: self.resizingStartingWidth,
617
+ height: height,
618
+ rowIndex: row,
619
+ draggingItem: self.currentCell,
620
+ });
621
+ }
622
+ };
623
+
624
+ /**
625
+ * Selects a column.
626
+ * @memberof canvasDatagrid
627
+ * @name selectColumn
628
+ * @method
629
+ * @param {number} columnIndex The column index to select.
630
+ * @param {boolean} toggleSelectMode When true, behaves as if you were holding control/command when you clicked the column.
631
+ * @param {boolean} shift When true, behaves as if you were holding shift when you clicked the column.
632
+ * @param {boolean} suppressEvent When true, prevents the selectionchanged event from firing.
633
+ */
634
+ self.selectColumn = (columnIndex, ctrl, shift, suppressEvent) => {
635
+ self.isMultiRowsSelected = false;
636
+ self.isMultiColumnsSelected = false;
637
+ function addCol(i) {
638
+ addIntoSelections(
639
+ self.selections,
640
+ normalizeSelection({
641
+ type: SelectionType.Columns,
642
+ startColumn: i,
643
+ }),
644
+ );
645
+ }
646
+ function removeCol(i) {
647
+ removeFromSelections(
648
+ self.selections,
649
+ normalizeSelection({
650
+ type: SelectionType.Columns,
651
+ startColumn: i,
652
+ }),
653
+ );
654
+ }
655
+ if (shift) {
656
+ if (!self.activeCell) {
657
+ return;
658
+ }
659
+ const s = Math.min(self.activeCell.columnIndex, columnIndex);
660
+ const e = Math.max(self.activeCell.columnIndex, columnIndex);
661
+ addIntoSelections(
662
+ self.selections,
663
+ normalizeSelection({
664
+ type: SelectionType.Columns,
665
+ startColumn: s,
666
+ endColumn: e - 1,
667
+ }),
668
+ );
669
+ if (s != e) self.isMultiColumnsSelected = true;
670
+ }
671
+ if (!ctrl && !shift) {
672
+ self.selections = [];
673
+ self.activeCell.columnIndex = columnIndex;
674
+ self.activeCell.rowIndex = self.scrollIndexTop;
675
+ }
676
+ if (ctrl && self.isColumnSelected(columnIndex)) {
677
+ removeCol(columnIndex);
678
+ } else {
679
+ addCol(columnIndex);
680
+ }
681
+ if (suppressEvent) {
682
+ return;
683
+ }
684
+ self.dispatchSelectionChangedEvent();
685
+ };
686
+
687
+ /**
688
+ * Selects a row.
689
+ * @memberof canvasDatagrid
690
+ * @name selectRow
691
+ * @method
692
+ * @param {number} rowIndex The row index to select.
693
+ * @param {boolean} ctrl When true, behaves as if you were holding control/command when you clicked the row.
694
+ * @param {boolean} shift When true, behaves as if you were holding shift when you clicked the row.
695
+ * @param {boolean} supressSelectionchangedEvent When true, prevents the selectionchanged event from firing.
696
+ */
697
+ self.selectRow = (rowIndex, ctrl, shift, suppressEvent) => {
698
+ self.isMultiRowsSelected = false;
699
+ function de() {
700
+ if (suppressEvent) {
701
+ return;
702
+ }
703
+ self.dispatchSelectionChangedEvent();
704
+ }
705
+ function addRow(ri) {
706
+ addIntoSelections(
707
+ self.selections,
708
+ normalizeSelection({
709
+ type: SelectionType.Rows,
710
+ startRow: ri,
711
+ }),
712
+ );
713
+ }
714
+ if (self.dragAddToSelection === false || self.dragObject === undefined) {
715
+ if (isRowSelected(self.selections, rowIndex) && ctrl) {
716
+ removeFromSelections(
717
+ self.selections,
718
+ normalizeSelection({
719
+ type: SelectionType.Rows,
720
+ startRow: rowIndex,
721
+ }),
722
+ );
723
+ de();
724
+ return;
725
+ }
726
+ }
727
+ if (self.dragAddToSelection === true || self.dragObject === undefined) {
728
+ if (shift && self.dragObject === undefined) {
729
+ if (!self.activeCell) {
730
+ return;
731
+ }
732
+ const st = Math.min(self.activeCell.rowIndex, rowIndex);
733
+ const en = Math.max(self.activeCell.rowIndex, rowIndex);
734
+ addIntoSelections(
735
+ self.selections,
736
+ normalizeSelection({
737
+ type: SelectionType.Rows,
738
+ startRow: st,
739
+ endRow: en,
740
+ }),
741
+ );
742
+ } else {
743
+ addRow(rowIndex);
744
+ }
745
+ }
746
+ de();
747
+ };
748
+
749
+ /**
750
+ * @param {function} iterator signature: `(cell: any) => any`
751
+ * @param {object} bounds type: `{left:number;right:number;top:number;bottom:number}`
752
+ * @param {boolean} [expandToRow]
753
+ */
754
+ function iterateSelectedCells(iterator, bounds, expandToRow) {
755
+ if (!bounds) return;
756
+ const { top, bottom, left, right } = bounds;
757
+ const height = bottom - top + 1;
758
+ const width = right - left + 1;
759
+ const states = getSelectionStateFromCells(self.selections, {
760
+ startRow: top,
761
+ endRow: bottom,
762
+ startColumn: left,
763
+ endColumn: right,
764
+ });
765
+ if (states === false) return;
766
+
767
+ const orderedSchema = getSchemaOrderByViewIndex(left, width);
768
+ for (let row = top, row2 = top + height; row < row2; row++) {
769
+ const viewData = self.viewData[row] || {};
770
+ const rowState = states === true || states[row - top];
771
+ if (!rowState) continue;
772
+ for (let col = left, col2 = left + width; col < col2; col++) {
773
+ if (rowState !== true && !rowState[col - left]) continue;
774
+
775
+ const header = orderedSchema[col];
776
+ if (!header || (header.hidden && !expandToRow)) continue;
777
+
778
+ const cell = {
779
+ value: viewData[header.name],
780
+ header,
781
+ columnName: header.name,
782
+ boundRowIndex: self.getBoundRowIndexFromViewRowIndex(row),
783
+ boundColumnIndex: self.orders.columns[col],
784
+ viewRowIndex: row,
785
+ viewColumnIndex: col,
786
+ };
787
+ iterator(cell);
788
+ }
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Selects an area of the grid.
794
+ * @memberof canvasDatagrid
795
+ * @name selectArea
796
+ * @method
797
+ * @param {rect} [bounds] A rect object representing the selected values.
798
+ * @param {boolean} [ctrl]
799
+ */
800
+ self.selectArea = function (bounds, ctrl) {
801
+ const schemaLength = self.getSchema().length;
802
+ if (bounds) {
803
+ if (bounds.right < 0) {
804
+ // patch for API compatibility
805
+ bounds.right = Math.max(schemaLength - 1, bounds.left, 0);
806
+ }
807
+ if (bounds.top > bounds.bottom || bounds.left > bounds.right)
808
+ throw new Error('Impossible selection area');
809
+ self.selectionBounds = self.sanitizeSelectionBounds(bounds);
810
+ }
811
+ let { top, bottom, left, right } = self.selectionBounds;
812
+ if (!ctrl) self.selections = [];
813
+
814
+ if (bottom >= self.viewData.length)
815
+ bottom = Math.max(self.viewData.length - 1, top);
816
+ if (top < -1 || left < -1 || right > schemaLength)
817
+ throw new Error('Impossible selection area');
818
+
819
+ /** @type {ContextForSelectionAction} */
820
+ const context = { rows: self.viewData.length, columns: schemaLength };
821
+ let changed = false;
822
+
823
+ // In original API, number -1 indicates selecting whole row or selecting while column.
824
+ if (top === -1) {
825
+ // select whole columns
826
+ const startColumn = Math.max(left, 0);
827
+ // left eq -1 means selecting all cells
828
+ const endRow =
829
+ left === -1 ? schemaLength - 1 : Math.min(right, schemaLength - 1);
830
+ changed = addIntoSelections(
831
+ self.selections,
832
+ normalizeSelection({
833
+ type: SelectionType.Columns,
834
+ startColumn: startColumn,
835
+ endColumn: endRow,
836
+ }),
837
+ context,
838
+ );
839
+ } else if (left === -1) {
840
+ // select whole rows
841
+ const startRow = Math.max(top, 0);
842
+ const endRow = Math.min(bottom, self.viewData.length - 1);
843
+ changed = addIntoSelections(
844
+ self.selections,
845
+ normalizeSelection({
846
+ type: SelectionType.Rows,
847
+ startRow: startRow,
848
+ endRow: endRow,
849
+ }),
850
+ context,
851
+ );
852
+ } else {
853
+ changed = addIntoSelections(
854
+ self.selections,
855
+ normalizeSelection({
856
+ type: SelectionType.Cells,
857
+ startRow: top,
858
+ endRow: bottom,
859
+ startColumn: left,
860
+ endColumn: right,
861
+ }),
862
+ context,
863
+ );
864
+ }
865
+ if (changed) self.dispatchSelectionChangedEvent();
866
+ };
867
+
868
+ /**
869
+ * This private method is using for make existing API about selection works.
870
+ * TODO: This method can be removed by a better way.
871
+ * @param {number} [rowIndex] 0 by default
872
+ * @returns {number[]|undefined}
873
+ */
874
+ self.getRowSelectionStates = (rowIndex) => {
875
+ if (typeof rowIndex !== 'number' || rowIndex < 0) rowIndex = 0;
876
+ const startColumn = 0;
877
+ const endRow = self.getSchema().length - 1;
878
+ const width = endRow - startColumn + 1;
879
+ const state = getSelectionStateFromCells(self.selections, {
880
+ startRow: rowIndex,
881
+ startColumn: startColumn,
882
+ endRow: rowIndex,
883
+ endColumn: endRow,
884
+ });
885
+ if (state === false) return;
886
+ if (state === true) return new Array(width).fill(true).map((_, i) => i);
887
+
888
+ const firstRow = state[0];
889
+ if (!firstRow) return;
890
+
891
+ const result = [];
892
+ for (let col = 0; col < firstRow.length; col++)
893
+ if (firstRow[col]) result.push(col);
894
+ return result;
895
+ };
896
+
897
+ /**
898
+ * Replace current selection list with new selection list
899
+ * @param {object[]} newSelections New selection list
900
+ */
901
+ self.replaceAllSelections = (newSelections) => {
902
+ cleanupSelections(newSelections);
903
+ self.selections = newSelections;
904
+ };
905
+
906
+ /**
907
+ * Select given columns and remove other existing selection area
908
+ * @param {number[]} viewIndexes A number array that represents column view indexes
909
+ */
910
+ self.selectColumnViewIndexes = (viewIndexes) => {
911
+ const selections = viewIndexes.map((col) => ({
912
+ type: SelectionType.Columns,
913
+ startColumn: col,
914
+ endRow: col,
915
+ }));
916
+ self.replaceAllSelections(selections);
917
+ };
918
+
919
+ /**
920
+ * This method will be a bottleneck in the future if this component
921
+ * need to ship large dataset. But why I wrote this function:
922
+ * 1. it is used for compatibility with existing APIs
923
+ * 2. this component still has a bottleneck on the selection even there is not this function.
924
+ * @returns {number[]}
925
+ */
926
+ self.getRowViewIndexesFromSelection = () => {
927
+ if (self.selections.length === 0) return [];
928
+ const bounds = getSelectionBounds(self.selections, true);
929
+ const result = [];
930
+ for (let rowIndex = bounds.top; rowIndex <= bounds.bottom; rowIndex++)
931
+ result.push(rowIndex);
932
+ return result;
933
+ };
934
+
935
+ /**
936
+ * @param {object} cell Signature: `{rowIndex:number;columnIndex:number}`
937
+ * @param {object} keyEvent Signature: `{key:string;shiftKey:boolean}`
938
+ * @param {boolean} [suppressEvent]
939
+ */
940
+ self.shrinkOrExpandSelections = (cell, keyEvent, suppressEvent) => {
941
+ const result = shrinkOrExpandSelections(self.selections, cell, keyEvent, {
942
+ columns: self.getSchema().length,
943
+ rows: self.viewData.length,
944
+ });
945
+ if (result && !suppressEvent) self.dispatchSelectionChangedEvent();
946
+ return result;
947
+ };
948
+
949
+ /**
950
+ * @param {number} [fromIndex]
951
+ * @param {number} [len]
952
+ */
953
+ function getSchemaOrderByViewIndex(fromIndex, len) {
954
+ const schema = self.getSchema();
955
+ const orderedSchema = [];
956
+ for (let col = fromIndex, col2 = fromIndex + len; col < col2; col++) {
957
+ const orderedIndex = self.orders.columns[col];
958
+ const header = schema[orderedIndex];
959
+ orderedSchema[col] = header;
960
+ }
961
+ return orderedSchema;
962
+ }
963
+
964
+ /**
965
+ * TODO: this function will be used for caching
966
+ * Concatenating two 2-dimensional array.
967
+ * It is simuliar with the method `concatenating` method of the Python module named `numpy`
968
+ * @template T
969
+ * @param {T[][]} a the first 2d array
970
+ * @param {T[][]} b the second 2d array
971
+ * @param {number} axis 1 implies that it is being done column-wise, otherwise row-wise
972
+ * @returns {T[][]}
973
+ */
974
+ function concatenate2DArray(a, b, axis) {
975
+ if (axis === 1) {
976
+ const result = new Array(a.length);
977
+ for (let i = 0; i < a.length; i++) {
978
+ const rowInA = a[i];
979
+ const rowInB = b[i];
980
+ result[i] = rowInA.concat(rowInB);
981
+ }
982
+ }
983
+ return a.concat(b);
984
+ }
985
+ }