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.
- package/CHANGELOG.md +24 -0
- package/dist/canvas-datagrid.debug.js +3433 -1522
- package/dist/canvas-datagrid.debug.map +1 -1
- package/dist/canvas-datagrid.js +1 -1
- package/dist/canvas-datagrid.module.js +1 -1
- package/dist/canvas-datagrid.module.js.map +1 -1
- package/dist/types.d.ts +295 -70
- package/lib/component.js +1 -2
- package/lib/contextMenu.js +10 -102
- package/lib/defaults.js +7 -0
- package/lib/docs.js +37 -3
- package/lib/dom.js +2 -1
- package/lib/draw.js +169 -69
- package/lib/events/index.js +587 -277
- package/lib/intf.js +33 -219
- package/lib/main.js +3 -0
- package/lib/publicMethods.js +17 -386
- package/lib/selections/index.js +985 -0
- package/lib/selections/type.d.ts +38 -0
- package/lib/selections/util.js +1319 -0
- package/package.json +4 -4
|
@@ -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
|
+
};
|