js-spread-grid 0.0.1

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.
Files changed (67) hide show
  1. package/package.json +26 -0
  2. package/src/core/render.js +400 -0
  3. package/src/core/state.js +183 -0
  4. package/src/core-utils/defaults.js +4 -0
  5. package/src/core-utils/rect.js +49 -0
  6. package/src/core-utils/rect.test.js +111 -0
  7. package/src/core-utils/roundToPixels.js +3 -0
  8. package/src/core-utils/stringifyId.js +27 -0
  9. package/src/core-utils/stringifyId.test.js +27 -0
  10. package/src/index.js +595 -0
  11. package/src/state-utils/getActive.js +17 -0
  12. package/src/state-utils/getCellSection.js +22 -0
  13. package/src/state-utils/getCellType.js +7 -0
  14. package/src/state-utils/getClipboardData.js +53 -0
  15. package/src/state-utils/getColumnIndex.js +25 -0
  16. package/src/state-utils/getCombinedCells.js +6 -0
  17. package/src/state-utils/getDataFormatting.js +46 -0
  18. package/src/state-utils/getEditableCells.js +23 -0
  19. package/src/state-utils/getEditedCellsAndFilters.js +4 -0
  20. package/src/state-utils/getEdition.js +6 -0
  21. package/src/state-utils/getFilterFormatting.js +3 -0
  22. package/src/state-utils/getFiltered.js +61 -0
  23. package/src/state-utils/getFilteringRules.js +5 -0
  24. package/src/state-utils/getFixedSize.js +8 -0
  25. package/src/state-utils/getFormatResolver.js +5 -0
  26. package/src/state-utils/getFormattingRules.js +5 -0
  27. package/src/state-utils/getHighlightedCells.js +40 -0
  28. package/src/state-utils/getHoveredCell.js +43 -0
  29. package/src/state-utils/getInputFormatting.js +3 -0
  30. package/src/state-utils/getInputPlacement.js +51 -0
  31. package/src/state-utils/getInvoked.js +5 -0
  32. package/src/state-utils/getIsTextValid.js +3 -0
  33. package/src/state-utils/getKeys.js +3 -0
  34. package/src/state-utils/getLookup.js +3 -0
  35. package/src/state-utils/getMeasureFormatting.js +3 -0
  36. package/src/state-utils/getMeasured.js +113 -0
  37. package/src/state-utils/getMousePosition.js +8 -0
  38. package/src/state-utils/getNewSortBy.js +20 -0
  39. package/src/state-utils/getPinned.js +8 -0
  40. package/src/state-utils/getPlaced.js +45 -0
  41. package/src/state-utils/getReducedCells.js +6 -0
  42. package/src/state-utils/getRenderFormatting.js +122 -0
  43. package/src/state-utils/getResizable.js +49 -0
  44. package/src/state-utils/getResolved.js +42 -0
  45. package/src/state-utils/getResolvedFilters.js +7 -0
  46. package/src/state-utils/getResolvedSortBy.js +7 -0
  47. package/src/state-utils/getRowIndex.js +25 -0
  48. package/src/state-utils/getScrollRect.js +41 -0
  49. package/src/state-utils/getSections.js +101 -0
  50. package/src/state-utils/getSelection.js +5 -0
  51. package/src/state-utils/getSorted.js +130 -0
  52. package/src/state-utils/getSortingFormatting.js +3 -0
  53. package/src/state-utils/getSortingRules.js +5 -0
  54. package/src/state-utils/getTextResolver.js +5 -0
  55. package/src/state-utils/getToggledValue.js +11 -0
  56. package/src/state-utils/getTotalSize.js +6 -0
  57. package/src/state-utils/getUnfolded.js +86 -0
  58. package/src/types/Edition.js +37 -0
  59. package/src/types/FilteringRules.js +48 -0
  60. package/src/types/FormatResolver.js +19 -0
  61. package/src/types/FormattingRules.js +120 -0
  62. package/src/types/FormattingRules.test.js +90 -0
  63. package/src/types/RulesLookup.js +118 -0
  64. package/src/types/Selection.js +25 -0
  65. package/src/types/SortingRules.js +62 -0
  66. package/src/types/TextResolver.js +60 -0
  67. package/src/types/VisibilityResolver.js +61 -0
package/src/index.js ADDED
@@ -0,0 +1,595 @@
1
+ import getEditableCells from "./state-utils/getEditableCells.js";
2
+ import stringifyId from "./core-utils/stringifyId.js";
3
+ import render from "./core/render.js";
4
+ import updateState from "./core/state.js";
5
+ import getCombinedCells from "./state-utils/getCombinedCells.js";
6
+ import getReducedCells from "./state-utils/getReducedCells.js";
7
+ import getMousePosition from "./state-utils/getMousePosition.js";
8
+ import getNewSortBy from "./state-utils/getNewSortBy.js";
9
+ import getClipboardData from "./state-utils/getClipboardData.js";
10
+ import getToggledValue from "./state-utils/getToggledValue.js";
11
+ import getCellType from "./state-utils/getCellType.js";
12
+
13
+ function initialize(element) {
14
+ if ('spread-grid-context' in element) return;
15
+
16
+ console.log('initialize');
17
+
18
+ const canvases = {
19
+ 'top-left': document.createElement('canvas'),
20
+ 'top-center': document.createElement('canvas'),
21
+ 'top-right': document.createElement('canvas'),
22
+ 'middle-left': document.createElement('canvas'),
23
+ 'middle-center': document.createElement('canvas'),
24
+ 'middle-right': document.createElement('canvas'),
25
+ 'bottom-left': document.createElement('canvas'),
26
+ 'bottom-center': document.createElement('canvas'),
27
+ 'bottom-right': document.createElement('canvas')
28
+ };
29
+ const input = document.createElement('input');
30
+
31
+ element.setAttribute('tabindex', '0');
32
+ element.setAttribute('style', 'max-width: 100vw; max-height: 100vh; overflow: auto; display: grid; position: relative; grid-template-columns: fit-content(0) fit-content(0) fit-content(0); grid-template-rows: fit-content(0) fit-content(0) fit-content(0); outline: none; user-select: none;');
33
+ element.classList.add('spread-grid');
34
+ canvases['top-left'].setAttribute('style', 'position: sticky; left: 0; top: 0; z-index: 2; grid-row: 1; grid-column: 1;');
35
+ canvases['top-center'].setAttribute('style', 'position: sticky; top: 0; z-index: 1; grid-row: 1; grid-column: 2;');
36
+ canvases['top-right'].setAttribute('style', 'position: sticky; right: 0; top: 0; z-index: 2; grid-row: 1; grid-column: 3;');
37
+ canvases['middle-left'].setAttribute('style', 'position: sticky; left: 0; z-index: 1; grid-row: 2; grid-column: 1;');
38
+ canvases['middle-center'].setAttribute('style', 'grid-row: 2; grid-column: 2; z-index: 0;');
39
+ canvases['middle-right'].setAttribute('style', 'position: sticky; right: 0; z-index: 1; grid-row: 2; grid-column: 3;');
40
+ canvases['bottom-left'].setAttribute('style', 'position: sticky; left: 0; bottom: 0; z-index: 2; grid-row: 3; grid-column: 1;');
41
+ canvases['bottom-center'].setAttribute('style', 'position: sticky; bottom: 0; z-index: 1; grid-row: 3; grid-column: 2;');
42
+ canvases['bottom-right'].setAttribute('style', 'position: sticky; right: 0; bottom: 0; z-index: 2; grid-row: 3; grid-column: 3;');
43
+ input.setAttribute('style', 'position: sticky; z-index: 3; outline: none; border: none; box-shadow: none; padding: 0 5px; font-size: 12px; font-family: Calibri; background-color: white; box-sizing: border-box; opacity: 0; pointer-events: none;');
44
+
45
+ const context = {
46
+ externalOptions: {},
47
+ state: null,
48
+ memory: {},
49
+ element: element,
50
+ canvases: canvases,
51
+ input: input,
52
+ renderRequested: false,
53
+ mousePosition: null,
54
+ isMouseDown: false,
55
+ columnWidthCache: new Map(),
56
+ rowHeightCache: new Map(),
57
+ };
58
+
59
+ context.requestNewRender = () => {
60
+ if (context.renderRequested) return;
61
+ context.renderRequested = true;
62
+ requestAnimationFrame(() => {
63
+ context.renderRequested = false;
64
+ updateState(context);
65
+ render(context);
66
+ });
67
+ };
68
+
69
+ context.addEventListener = (element, type, listener) => {
70
+ element.addEventListener(type, (event) => {
71
+ try {
72
+ listener(event);
73
+ }
74
+ catch (error) {
75
+ error.message = `[${type} event]: ${error.message}`;
76
+ context.error = error;
77
+ context.requestNewRender();
78
+ }
79
+ });
80
+ }
81
+
82
+ context.localOptions = {
83
+ data: [],
84
+ columns: [{ type: 'DATA-BLOCK' }],
85
+ rows: [{ type: 'HEADER' }, { type: 'DATA-BLOCK' }],
86
+ formatting: [],
87
+ filtering: [],
88
+ sorting: [],
89
+ dataSelector: ({ data, row, column }) => data[row.id][column.id],
90
+ pinnedTop: 0,
91
+ pinnedBottom: 0,
92
+ pinnedLeft: 0,
93
+ pinnedRight: 0,
94
+ borderWidth: 1,
95
+ focusedCell: null,
96
+ onFocusedCellChange: (focusedCell) => {
97
+ context.localOptions.focusedCell = focusedCell;
98
+ context.requestNewRender();
99
+ },
100
+ selectedCells: [],
101
+ onSelectedCellsChange: (selectedCells) => {
102
+ context.localOptions.selectedCells = selectedCells;
103
+ context.requestNewRender();
104
+ },
105
+ highlightedCells: [],
106
+ editedCells: [],
107
+ onEditedCellsChange: (editedCells) => {
108
+ // TODO: optimize by coalescing
109
+ context.localOptions.editedCells = editedCells;
110
+ context.requestNewRender();
111
+ },
112
+ filters: [],
113
+ onFiltersChange: (filters) => {
114
+ context.localOptions.filters = filters;
115
+ context.requestNewRender();
116
+ },
117
+ sortBy: [],
118
+ onSortByChange: (sortBy) => {
119
+ context.localOptions.sortBy = sortBy;
120
+ context.requestNewRender();
121
+ },
122
+ onCellClick: () => { },
123
+ onCustomCellClick: () => { },
124
+ columnWidths: [],
125
+ onColumnWidthsChange: (columnWidths) => {
126
+ context.localOptions.columnWidths = columnWidths;
127
+ context.requestNewRender();
128
+ },
129
+ rowHeights: [],
130
+ onRowHeightsChange: (rowHeights) => {
131
+ context.localOptions.rowHeights = rowHeights;
132
+ context.requestNewRender();
133
+ },
134
+ onActiveColumnsChange: () => { },
135
+ onActiveRowsChange: () => { },
136
+ };
137
+
138
+ element['spread-grid-context'] = context;
139
+
140
+ const setText = (text) => {
141
+ input.value = text;
142
+ input.dispatchEvent(new Event('input'));
143
+ }
144
+
145
+ const accept = (autoCommit) => {
146
+ const selectedCells = context.state.options.selectedCells;
147
+ const formatResolver = context.state.inputFormatResolver;
148
+ const columnLookup = context.state.columnLookup;
149
+ const rowLookup = context.state.rowLookup;
150
+ const text = context.state.text;
151
+ const isTextValid = context.state.isTextValid;
152
+ const setEditedCells = context.state.options.onEditedCellsChange;
153
+ const setFilters = context.state.options.onFiltersChange;
154
+ const addEditedCells = (cells) => setEditedCells(getCombinedCells(context.state.options.editedCells, cells));
155
+ const addFilters = (cells) => setFilters(getCombinedCells(context.state.options.filters, cells));
156
+ const editableCells = getEditableCells(selectedCells, formatResolver, columnLookup, rowLookup);
157
+
158
+ if (text === '')
159
+ return;
160
+ if (!isTextValid)
161
+ return;
162
+ if (autoCommit && !editableCells.every(cell => cell.edit.autoCommit))
163
+ return;
164
+
165
+ const dataCells = editableCells.filter(cell => cell.type === 'DATA');
166
+ const filterCells = editableCells.filter(cell => cell.type === 'FILTER');
167
+
168
+ addEditedCells(dataCells.map(cell => ({ ...cell.cell, value: cell.edit.parse({ string: text }) })));
169
+ addFilters(filterCells.map(cell => ({ ...cell.cell, expression: cell.edit.parse({ string: text }) })));
170
+
171
+ if (!autoCommit)
172
+ setText('');
173
+ };
174
+
175
+ const clear = (autoCommit) => {
176
+ const selectedCells = context.state.options.selectedCells;
177
+ const setEditedCells = context.state.options.onEditedCellsChange;
178
+ const setFilters = context.state.options.onFiltersChange;
179
+ const removeEditedCells = (cells) => setEditedCells(getReducedCells(context.state.options.editedCells, cells));
180
+ const removeFilters = (cells) => setFilters(getReducedCells(context.state.options.filters, cells));
181
+ const formatResolver = context.state.inputFormatResolver;
182
+ const columnLookup = context.state.columnLookup;
183
+ const rowLookup = context.state.rowLookup;
184
+ const editableCells = getEditableCells(selectedCells, formatResolver, columnLookup, rowLookup);
185
+
186
+ if (autoCommit && !editableCells.every(cell => cell.edit.autoCommit))
187
+ return;
188
+
189
+ removeEditedCells(selectedCells);
190
+ removeFilters(selectedCells);
191
+ }
192
+
193
+ // TODO: Move other input functions here as well
194
+
195
+ context.addEventListener(element, 'scroll', (event) => {
196
+ // TODO: only request new render if scroll position changed outside of the scope
197
+ // TODO: how to: calculate the new scrollRect here and compare it to the one in the state
198
+ // TODO: Also don't forget to check if the highlighted cell did change
199
+ // TODO: Consider forcing a new render when visible scrollRect changes (without waiting for the next render frame)
200
+ context.requestNewRender();
201
+ });
202
+
203
+ context.addEventListener(element, 'mouseenter', (event) => {
204
+ context.mousePosition = getMousePosition(event);
205
+ context.requestNewRender();
206
+ });
207
+
208
+ context.addEventListener(element, 'mousemove', (event) => {
209
+ context.mousePosition = getMousePosition(event);
210
+
211
+ if (context.resizingColumn) {
212
+ const column = context.state.columnLookup.get(stringifyId(context.resizingColumn));
213
+ const columnWidth = column.width;
214
+ const columnRight = column.right;
215
+ const mousePosition = context.mousePosition.x;
216
+ const newColumnWidth = Math.max(10, mousePosition - columnRight + columnWidth);
217
+ const previousColumnWidths = context.state.options.columnWidths;
218
+ const newColumnWidths = previousColumnWidths
219
+ .filter(columnWidth => stringifyId(columnWidth.columnId) !== column.key)
220
+ .concat([{ columnId: column.id, width: newColumnWidth }]);
221
+ context.state.options.onColumnWidthsChange(newColumnWidths);
222
+ }
223
+ if (context.resizingRow) {
224
+ const row = context.state.rowLookup.get(stringifyId(context.resizingRow));
225
+ const rowHeight = row.height;
226
+ const rowBottom = row.bottom;
227
+ const mousePosition = context.mousePosition.y;
228
+ const newRowHeight = Math.max(10, mousePosition - rowBottom + rowHeight);
229
+ const previousRowHeights = context.state.options.rowHeights;
230
+ const newRowHeights = previousRowHeights
231
+ .filter(rowHeight => stringifyId(rowHeight.rowId) !== row.key)
232
+ .concat([{ rowId: row.id, height: newRowHeight }]);
233
+ context.state.options.onRowHeightsChange(newRowHeights);
234
+ }
235
+
236
+ // TODO: only request new render if hovered cell changed
237
+ context.requestNewRender();
238
+ });
239
+
240
+ context.addEventListener(element, 'mouseleave', () => {
241
+ context.mousePosition = null;
242
+ context.requestNewRender();
243
+ });
244
+
245
+ // TODO: update mouse position on resize
246
+
247
+ context.addEventListener(element, 'mousedown', (event) => {
248
+ updateState(context);
249
+ setText('');
250
+
251
+ context.isMouseDown = true;
252
+ context.mouseDownPosition = context.mousePosition;
253
+ context.mouseDownCell = context.state.hoveredCell;
254
+
255
+ if (context.state.resizableColumn) {
256
+ context.resizingColumn = context.state.resizableColumn;
257
+ }
258
+ if (context.state.resizableRow) {
259
+ context.resizingRow = context.state.resizableRow;
260
+ }
261
+ if (!context.state.resizableColumn && !context.state.resizableRow) {
262
+ context.state.options.onFocusedCellChange(context.state.hoveredCell);
263
+ }
264
+
265
+ if (!event.ctrlKey)
266
+ context.state.options.onSelectedCellsChange([]);
267
+
268
+ context.requestNewRender();
269
+ });
270
+
271
+ context.addEventListener(element, 'mouseup', (event) => {
272
+ updateState(context);
273
+
274
+ context.isMouseDown = false;
275
+ context.resizingColumn = null;
276
+ context.resizingRow = null;
277
+
278
+ context.state.options.onSelectedCellsChange(getCombinedCells(context.state.options.selectedCells, context.state.highlightedCells));
279
+ context.requestNewRender();
280
+ });
281
+
282
+ context.addEventListener(element, 'pointerdown', (event) => {
283
+ context.element.setPointerCapture(event.pointerId);
284
+ });
285
+
286
+ context.addEventListener(element, 'pointerup', (event) => {
287
+ context.element.releasePointerCapture(event.pointerId);
288
+ });
289
+
290
+ context.addEventListener(element, 'click', (event) => {
291
+ updateState(context);
292
+
293
+ const cell = context.state.hoveredCell;
294
+ const mouseDownCell = context.mouseDownCell;
295
+
296
+ if (context.state.resizableColumn || context.state.resizableRow)
297
+ return;
298
+ if (cell === null)
299
+ return;
300
+ if (stringifyId(cell.columnId) !== stringifyId(mouseDownCell.columnId))
301
+ return;
302
+ if (stringifyId(cell.rowId) !== stringifyId(mouseDownCell.rowId))
303
+ return;
304
+
305
+ const column = context.state.columnLookup.get(stringifyId(cell.columnId));
306
+ const row = context.state.rowLookup.get(stringifyId(cell.rowId));
307
+ const sortBy = context.state.options.sortBy;
308
+ const formatResolver = context.state.inputFormatResolver;
309
+ const cellData = formatResolver.resolve(row, column);
310
+
311
+ if (cellData.edit?.toggle) {
312
+ const edition = context.state.edition;
313
+ const newValue = getToggledValue(cellData, column, row, edition);
314
+ const cellType = getCellType(column, row);
315
+ if (cellType === 'DATA')
316
+ context.state.options.onEditedCellsChange(getCombinedCells(context.state.options.editedCells, [{ ...cell, value: newValue }]));
317
+ if (cellType === 'FILTER')
318
+ context.state.options.onFiltersChange(getCombinedCells(context.state.options.filters, [{ ...cell, expression: newValue }]));
319
+ } else if (column.type === 'DATA' && row.type === 'DATA') {
320
+ context.state.options.onCellClick(context.state.hoveredCell);
321
+ } else if (column.type === 'CUSTOM' || row.type === 'CUSTOM') {
322
+ context.state.options.onCustomCellClick(context.state.hoveredCell);
323
+ } else if (column.type === 'HEADER' || row.type === 'HEADER') {
324
+ const newSortBy = getNewSortBy(sortBy, column, row, event.ctrlKey);
325
+ context.state.options.onSortByChange(newSortBy);
326
+ context.state.options.onSelectedCellsChange([]);
327
+ }
328
+ });
329
+
330
+ context.addEventListener(element, 'dblclick', (event) => {
331
+ updateState(context);
332
+
333
+ if (context.state.resizableColumn) {
334
+ const resizableColumnKey = stringifyId(context.state.resizableColumn);
335
+ const newColumnWidths = context.state.options.columnWidths.filter(columnWidth => stringifyId(columnWidth.columnId) !== resizableColumnKey);
336
+ context.state.options.onColumnWidthsChange(newColumnWidths);
337
+ context.columnWidthCache.delete(resizableColumnKey);
338
+ }
339
+ if (context.state.resizableRow) {
340
+ const resizableRowKey = stringifyId(context.state.resizableRow);
341
+ const newRowHeights = context.state.options.rowHeights.filter(rowHeight => stringifyId(rowHeight.rowId) !== resizableRowKey);
342
+ context.state.options.onRowHeightsChange(newRowHeights);
343
+ context.rowHeightCache.delete(resizableRowKey);
344
+ }
345
+
346
+ const focusedCell = context.state.focusedCell;
347
+ if (focusedCell === null)
348
+ return;
349
+
350
+ const focusedColumnKey = stringifyId(focusedCell.columnId);
351
+ const focusedRowKey = stringifyId(focusedCell.rowId);
352
+ const columnLookup = context.state.columnLookup;
353
+ const rowLookup = context.state.rowLookup;
354
+ const formatResolver = context.state.inputFormatResolver;
355
+
356
+ if (!columnLookup.has(focusedColumnKey))
357
+ return;
358
+ if (!rowLookup.has(focusedRowKey))
359
+ return;
360
+
361
+ const column = columnLookup.get(focusedColumnKey);
362
+ const row = rowLookup.get(focusedRowKey);
363
+ const cell = formatResolver.resolve(row, column);
364
+ const text = cell.text; // TODO: Make it configurable
365
+
366
+ if (!cell.edit)
367
+ return;
368
+ if (cell.edit.toggle)
369
+ return;
370
+
371
+ setText(text);
372
+ input?.select();
373
+ });
374
+
375
+ context.addEventListener(element, 'focus', () => {
376
+ if (input.parentElement)
377
+ input.focus({ preventScroll: true });
378
+ });
379
+
380
+ context.addEventListener(element, 'keydown', (event) => {
381
+ // TODO: make sure it's not invoked on keydown of the input
382
+ updateState(context);
383
+
384
+ const focusedCell = context.state.focusedCell;
385
+ const columnLookup = context.state.columnLookup;
386
+ const rowLookup = context.state.rowLookup;
387
+ const selectedCells = context.state.options.selectedCells;
388
+ const setSelectedCells = context.state.options.onSelectedCellsChange;
389
+ const addSelectedCells = (cells) => setSelectedCells(getCombinedCells(selectedCells, cells));
390
+ const setFocusedCell = context.state.options.onFocusedCellChange;
391
+ const editedCells = context.state.options.editedCells;
392
+ const setEditedCells = context.state.options.onEditedCellsChange;
393
+ const columns = context.state.columns;
394
+ const rows = context.state.rows;
395
+ const text = context.state.text;
396
+ const inputFormatResolver = context.state.inputFormatResolver;
397
+
398
+ const arrowTo = (cell, event) => {
399
+ setFocusedCell(cell);
400
+
401
+ if (event.shiftKey)
402
+ addSelectedCells([cell]);
403
+ else
404
+ setSelectedCells([cell]);
405
+ };
406
+
407
+ const arrowHorizontally = (offset, event) => {
408
+ if (!focusedCell)
409
+ return;
410
+
411
+ const focusedColumnKey = stringifyId(focusedCell.columnId);
412
+ if (!columnLookup.has(focusedColumnKey))
413
+ return;
414
+
415
+ const focusedColumnIndex = columnLookup.get(focusedColumnKey).index;
416
+ const newColumnIndex = Math.max(0, Math.min(columns.length - 1, focusedColumnIndex + offset));
417
+ if (newColumnIndex === focusedColumnIndex)
418
+ return;
419
+
420
+ const newFocusedCell = { rowId: focusedCell.rowId, columnId: columns[newColumnIndex].id };
421
+
422
+ arrowTo(newFocusedCell, event);
423
+ }
424
+
425
+ const arrowVertically = (offset, event) => {
426
+ if (!focusedCell)
427
+ return;
428
+
429
+ const focusedRowKey = stringifyId(focusedCell.rowId);
430
+ if (!rowLookup.has(focusedRowKey))
431
+ return;
432
+
433
+ const focusedRowIndex = rowLookup.get(focusedRowKey).index;
434
+ const newRowIndex = Math.max(0, Math.min(rows.length - 1, focusedRowIndex + offset));
435
+ if (newRowIndex === focusedRowIndex)
436
+ return;
437
+
438
+ const newFocusedCell = { rowId: rows[newRowIndex].id, columnId: focusedCell.columnId };
439
+
440
+ arrowTo(newFocusedCell, event);
441
+ }
442
+
443
+ const preventDefault = () => {
444
+ event.preventDefault();
445
+ event.stopPropagation();
446
+ };
447
+
448
+ const cancel = () => {
449
+ if (text !== '') {
450
+ setText('');
451
+ }
452
+ else if (selectedCells.length > 1) {
453
+ setSelectedCells([focusedCell]);
454
+ }
455
+ else if (editedCells.length > 0) {
456
+ setEditedCells([]);
457
+ }
458
+ else {
459
+ setFocusedCell(null);
460
+ setSelectedCells([]);
461
+ }
462
+ };
463
+
464
+ const copyToClipboard = (event) => {
465
+ if (!event.ctrlKey)
466
+ return;
467
+
468
+ const clipboardData = getClipboardData(selectedCells, columns, rows, inputFormatResolver);
469
+
470
+ if (navigator.clipboard) {
471
+ navigator.clipboard.writeText(clipboardData);
472
+ } else {
473
+ const textArea = document.createElement('textarea');
474
+ textArea.value = clipboardData;
475
+ document.body.appendChild(textArea);
476
+ textArea.select();
477
+ document.execCommand('copy');
478
+ document.body.removeChild(textArea);
479
+ }
480
+ }
481
+
482
+ switch (event.key) {
483
+ case 'Escape':
484
+ cancel();
485
+ break;
486
+ case 'Enter':
487
+ accept();
488
+ break;
489
+ case 'ArrowUp':
490
+ // TODO: When ctrl and shift are pressed together, select all cells between the focused cell and the new cell
491
+ // TODO: When shift is pressed, expand the current rect selection instead of moving the focused cell
492
+ preventDefault();
493
+ arrowVertically(event.ctrlKey ? -rows.length : -1, event);
494
+ break;
495
+ case 'ArrowDown':
496
+ preventDefault();
497
+ arrowVertically(event.ctrlKey ? rows.length : 1, event);
498
+ break;
499
+ case 'ArrowLeft':
500
+ preventDefault();
501
+ arrowHorizontally(event.ctrlKey ? -columns.length : -1, event);
502
+ break;
503
+ case 'ArrowRight':
504
+ preventDefault();
505
+ arrowHorizontally(event.ctrlKey ? columns.length : 1, event);
506
+ break;
507
+ case 'Delete':
508
+ case 'Backspace':
509
+ clear();
510
+ break;
511
+ case 'c':
512
+ copyToClipboard(event);
513
+ break;
514
+ default:
515
+ break;
516
+ }
517
+ });
518
+
519
+ new ResizeObserver(() => {
520
+ context.requestNewRender();
521
+ }).observe(element);
522
+
523
+ context.addEventListener(input, 'input', (event) => {
524
+ // TODO: check performance of this (if it's ok to update the state on every input event)
525
+ updateState(context);
526
+
527
+ if (event.target.value) {
528
+ accept(true);
529
+
530
+ input.style.opacity = 1;
531
+ input.style.pointerEvents = 'auto';
532
+ }
533
+ else {
534
+ if (event.isTrusted)
535
+ clear(true);
536
+
537
+ input.style.opacity = 0;
538
+ input.style.pointerEvents = 'none';
539
+ }
540
+ });
541
+
542
+ context.addEventListener(input, 'click', (event) => {
543
+ event.stopPropagation();
544
+ });
545
+
546
+ context.addEventListener(input, 'dblclick', (event) => {
547
+ event.stopPropagation();
548
+ });
549
+
550
+ context.addEventListener(input, 'mousedown', (event) => {
551
+ event.stopPropagation();
552
+ });
553
+
554
+ context.addEventListener(input, 'keydown', (event) => {
555
+ switch (event.key) {
556
+ case 'Enter':
557
+ case 'Escape':
558
+ break;
559
+ case 'Delete':
560
+ case 'Backspace':
561
+ case 'ArrowUp':
562
+ case 'ArrowDown':
563
+ case 'ArrowLeft':
564
+ case 'ArrowRight':
565
+ if (input.value !== '') {
566
+ event.stopPropagation();
567
+ context.requestNewRender();
568
+ }
569
+ break;
570
+ default:
571
+ event.stopPropagation();
572
+ // TODO: maybe only check if the new text is valid
573
+ context.requestNewRender();
574
+ break;
575
+ }
576
+ });
577
+ }
578
+
579
+ export default function createGrid(element, options) {
580
+ console.log('createGrid');
581
+
582
+ initialize(element);
583
+
584
+ const context = element['spread-grid-context'];
585
+ context.externalOptions = options;
586
+
587
+ if (context.state === null) {
588
+ updateState(context);
589
+ // TODO: only render if the state has sufficiently changed
590
+ render(context);
591
+ }
592
+ else {
593
+ context.requestNewRender();
594
+ }
595
+ }
@@ -0,0 +1,17 @@
1
+ function getActive(entries, callback) {
2
+ const ids = entries
3
+ .filter(entry => entry.type === 'DATA')
4
+ .map(entry => entry.id);
5
+
6
+ callback(ids);
7
+
8
+ return ids;
9
+ }
10
+
11
+ export function getActiveColumns(columns, callback) {
12
+ return getActive(columns, callback);
13
+ }
14
+
15
+ export function getActiveRows(rows, callback) {
16
+ return getActive(rows, callback);
17
+ }
@@ -0,0 +1,22 @@
1
+ function getVerticalSection(row) {
2
+ if (row.pinned === "BEGIN")
3
+ return "top";
4
+ if (row.pinned === "END")
5
+ return "bottom";
6
+ return "middle";
7
+ }
8
+
9
+ function getHorizontalSection(column) {
10
+ if (column.pinned === "BEGIN")
11
+ return "left";
12
+ if (column.pinned === "END")
13
+ return "right";
14
+ return "center";
15
+ }
16
+
17
+ export default function getCellSection(column, row) {
18
+ const verticalSection = getVerticalSection(row);
19
+ const horizontalSection = getHorizontalSection(column);
20
+
21
+ return `${verticalSection}-${horizontalSection}`;
22
+ }
@@ -0,0 +1,7 @@
1
+ export default function getCellType(column, row) {
2
+ if (column.type === 'FILTER' ^ row.type === 'FILTER')
3
+ return 'FILTER';
4
+ if (column.type === 'DATA' && row.type === 'DATA')
5
+ return 'DATA';
6
+ return 'OTHER';
7
+ }