@svgrid/grid 1.0.2 → 1.1.0

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 (143) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +137 -39
  3. package/dist/GridFooter.svelte +164 -0
  4. package/dist/GridFooter.svelte.d.ts +29 -0
  5. package/dist/GridMenus.svelte +570 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +429 -0
  8. package/dist/SvGrid.controller.svelte.js +1732 -0
  9. package/dist/SvGrid.css +1709 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.svelte +344 -7043
  13. package/dist/SvGrid.svelte.d.ts +4 -357
  14. package/dist/SvGrid.types.d.ts +436 -0
  15. package/dist/SvGrid.types.js +1 -0
  16. package/dist/SvGridChart.svelte +1060 -23
  17. package/dist/SvGridChart.svelte.d.ts +17 -0
  18. package/dist/build-api.d.ts +5 -0
  19. package/dist/build-api.js +527 -0
  20. package/dist/cell-render.d.ts +15 -0
  21. package/dist/cell-render.js +246 -0
  22. package/dist/cell-values.d.ts +28 -0
  23. package/dist/cell-values.js +89 -0
  24. package/dist/chart.d.ts +370 -3
  25. package/dist/chart.js +1135 -42
  26. package/dist/clipboard.d.ts +15 -0
  27. package/dist/clipboard.js +356 -0
  28. package/dist/columns.d.ts +30 -0
  29. package/dist/columns.js +277 -0
  30. package/dist/core.d.ts +1 -1
  31. package/dist/css.d.ts +3 -0
  32. package/dist/editing.d.ts +24 -0
  33. package/dist/editing.js +343 -0
  34. package/dist/editors/cell-editors.d.ts +1 -1
  35. package/dist/facet-buckets.d.ts +13 -0
  36. package/dist/facet-buckets.js +54 -0
  37. package/dist/features.d.ts +5 -0
  38. package/dist/features.js +30 -0
  39. package/dist/filter-operators.d.ts +16 -0
  40. package/dist/filter-operators.js +69 -0
  41. package/dist/hyperformula-adapter.d.ts +82 -0
  42. package/dist/hyperformula-adapter.js +73 -0
  43. package/dist/index.d.ts +5 -2
  44. package/dist/index.js +5 -2
  45. package/dist/keyboard-handlers.d.ts +7 -0
  46. package/dist/keyboard-handlers.js +197 -0
  47. package/dist/menus.d.ts +40 -0
  48. package/dist/menus.js +389 -0
  49. package/dist/named-views.d.ts +27 -0
  50. package/dist/named-views.js +39 -0
  51. package/dist/row-resize.d.ts +43 -0
  52. package/dist/row-resize.js +158 -0
  53. package/dist/scroll-sync.d.ts +9 -0
  54. package/dist/scroll-sync.js +86 -0
  55. package/dist/selection.d.ts +26 -0
  56. package/dist/selection.js +387 -0
  57. package/dist/spreadsheet.d.ts +80 -0
  58. package/dist/spreadsheet.js +194 -0
  59. package/dist/summaries.d.ts +12 -0
  60. package/dist/summaries.js +65 -0
  61. package/dist/svgrid-wrapper.types.d.ts +12 -1
  62. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  63. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  64. package/dist/svgrid.context-menu.test.d.ts +1 -0
  65. package/dist/svgrid.context-menu.test.js +102 -0
  66. package/dist/svgrid.new-features.wrapper.test.js +30 -4
  67. package/dist/svgrid.wrapper.test.js +27 -1
  68. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  69. package/dist/virtualization/scroll-scaling.d.ts +28 -0
  70. package/dist/virtualization/scroll-scaling.js +64 -0
  71. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  72. package/dist/virtualization/scroll-scaling.test.js +86 -0
  73. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  74. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  75. package/dist/virtualization/virtualizer.d.ts +7 -0
  76. package/dist/virtualization/virtualizer.js +30 -0
  77. package/package.json +1 -1
  78. package/src/GridFooter.svelte +164 -0
  79. package/src/GridMenus.svelte +570 -0
  80. package/src/SvGrid.controller.svelte.ts +2195 -0
  81. package/src/SvGrid.css +1747 -0
  82. package/src/SvGrid.helpers.test.ts +415 -0
  83. package/src/SvGrid.helpers.ts +185 -0
  84. package/src/SvGrid.svelte +348 -7043
  85. package/src/SvGrid.types.ts +456 -0
  86. package/src/SvGridChart.svelte +1060 -23
  87. package/src/build-api.coverage.test.ts +532 -0
  88. package/src/build-api.ts +663 -0
  89. package/src/cell-render.test.ts +451 -0
  90. package/src/cell-render.ts +426 -0
  91. package/src/cell-values.ts +114 -0
  92. package/src/chart-export.test.ts +370 -0
  93. package/src/chart.coverage.test.ts +814 -0
  94. package/src/chart.ts +1352 -47
  95. package/src/clipboard.test.ts +731 -0
  96. package/src/clipboard.ts +524 -0
  97. package/src/collaboration.coverage.test.ts +220 -0
  98. package/src/columns.test.ts +702 -0
  99. package/src/columns.ts +419 -0
  100. package/src/core.ts +8 -0
  101. package/src/css.d.ts +3 -0
  102. package/src/editing.test.ts +837 -0
  103. package/src/editing.ts +513 -0
  104. package/src/editors/cell-editors.coverage.test.ts +156 -0
  105. package/src/editors/cell-editors.ts +1 -0
  106. package/src/facet-buckets.test.ts +353 -0
  107. package/src/facet-buckets.ts +67 -0
  108. package/src/features.ts +128 -0
  109. package/src/filter-operators.test.ts +174 -0
  110. package/src/filter-operators.ts +87 -0
  111. package/src/hyperformula-adapter.test.ts +256 -0
  112. package/src/hyperformula-adapter.ts +124 -0
  113. package/src/index.ts +37 -0
  114. package/src/keyboard-handlers.coverage.test.ts +560 -0
  115. package/src/keyboard-handlers.ts +353 -0
  116. package/src/keyboard.ts +97 -97
  117. package/src/menus.test.ts +620 -0
  118. package/src/menus.ts +554 -0
  119. package/src/named-views.coverage.test.ts +210 -0
  120. package/src/named-views.ts +48 -0
  121. package/src/row-resize.test.ts +369 -0
  122. package/src/row-resize.ts +171 -0
  123. package/src/scroll-sync.test.ts +330 -0
  124. package/src/scroll-sync.ts +216 -0
  125. package/src/selection.test.ts +722 -0
  126. package/src/selection.ts +545 -0
  127. package/src/server-data-source.coverage.test.ts +180 -0
  128. package/src/spreadsheet.test.ts +445 -0
  129. package/src/spreadsheet.ts +246 -0
  130. package/src/summaries.ts +204 -0
  131. package/src/sv-grid-scrollbar.ts +13 -1
  132. package/src/svgrid-wrapper.types.ts +12 -1
  133. package/src/svgrid.behavior.test.ts +22 -0
  134. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  135. package/src/svgrid.context-menu.test.ts +126 -0
  136. package/src/svgrid.interaction.test.ts +30 -0
  137. package/src/svgrid.new-features.wrapper.test.ts +65 -4
  138. package/src/svgrid.wrapper.test.ts +27 -1
  139. package/src/test-setup.ts +9 -6
  140. package/src/virtualization/scroll-scaling.test.ts +148 -0
  141. package/src/virtualization/scroll-scaling.ts +121 -0
  142. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  143. package/src/virtualization/virtualizer.ts +26 -0
@@ -0,0 +1,353 @@
1
+ // keyboard handlers extracted from the controller. Imperative event handlers
2
+ // reading/writing controller state via the `ctx` handle; the reactive core
3
+ // ($state/$derived/$effect) stays in the controller.
4
+ import {
5
+ applyExcelFilter,
6
+ normalizeForFilter,
7
+ createColumnVirtualizer,
8
+ createCoreRowModel,
9
+ createExpandedRowModel,
10
+ createFilteredRowModel,
11
+ createGroupedRowModel,
12
+ createPaginatedRowModel,
13
+ createSvelteVirtualizer,
14
+ createSortedRowModel,
15
+ createSvGrid,
16
+ filterFns,
17
+ getGridCellA11yProps,
18
+ getGridCellDomId,
19
+ getGridHeaderA11yProps,
20
+ getGridRootA11yProps,
21
+ getGridRowA11yProps,
22
+ parseEditorValue,
23
+ normalizeEditorOptions,
24
+ sortFns,
25
+ tableFeatures,
26
+ rowSortingFeature,
27
+ columnFilteringFeature,
28
+ columnGroupingFeature,
29
+ type CellContext,
30
+ type EditorContext,
31
+ type CellEditorOption,
32
+ type CellEditorType,
33
+ type CellFormatter,
34
+ type CellFormatConfig,
35
+ type Column,
36
+ type ColumnDef,
37
+ type Row,
38
+ type RowData,
39
+ type SvGridApi,
40
+ type TableFeatures,
41
+ } from "./index";
42
+ import "./sv-grid-scrollbar";
43
+ import type { Snippet } from "svelte";
44
+ import {
45
+ formatNumericWithConfig,
46
+ getDateFormatter,
47
+ resolveDatePattern,
48
+ } from "./cell-formatting";
49
+ import {
50
+ RenderSnippetConfig,
51
+ RenderComponentConfig,
52
+ } from "./render-component";
53
+ import { buildFillPattern } from "./fill-patterns";
54
+ import { buildSparkline, toSparklineValues } from "./sparkline";
55
+ import {
56
+ resolveCellFormat,
57
+ computeColumnStat,
58
+ formatsNeedingStats,
59
+ type ColumnStat,
60
+ type ResolvedCellFormat,
61
+ } from "./conditional-formatting";
62
+ import SvGridDropdown from "./SvGridDropdown.svelte";
63
+ import type {
64
+ Props,
65
+ SelectionPoint,
66
+ SelectionRange,
67
+ CellEditState,
68
+ FilterOperator,
69
+ FilterOption,
70
+ MenuPosition,
71
+ } from "./SvGrid.types";
72
+ import {
73
+ cfTextStyle,
74
+ fmtStat,
75
+ getCellKey,
76
+ resolveClassList,
77
+ toDateInputValue,
78
+ toDateTimeLocalInputValue,
79
+ getEditableInputValue,
80
+ getEditorInputType,
81
+ toValueArray,
82
+ getOptionLabel,
83
+ getOptionColor,
84
+ colorfulChipStyle,
85
+ getEditorClass,
86
+ asDate,
87
+ clampMenuX,
88
+ cssEscape,
89
+ rawToNumber,
90
+ formatFacetNumber,
91
+ formatFacetDate,
92
+ } from "./SvGrid.helpers";
93
+ import { createSummaries } from "./summaries";
94
+ import { createMenus } from "./menus";
95
+ import { createCellRender } from "./cell-render";
96
+ import { createEditing } from "./editing";
97
+ import { createSelection } from "./selection";
98
+ import { createColumns } from "./columns";
99
+ import { createGridApi } from "./build-api";
100
+ import { createClipboard } from "./clipboard";
101
+ import {
102
+ filterOperatorOptions,
103
+ fallbackOperatorOption,
104
+ TEXT_OPERATORS,
105
+ NUMBER_OPERATORS,
106
+ DATE_OPERATORS,
107
+ CHECKBOX_OPERATORS,
108
+ operatorOption,
109
+ operatorsForColumn,
110
+ defaultOperatorFor,
111
+ operatorLabelFor,
112
+ } from "./filter-operators";
113
+ import {
114
+ type FacetBucket,
115
+ isBucketableColumn,
116
+ buildBuckets,
117
+ isInBucket,
118
+ } from "./facet-buckets";
119
+ import {
120
+ getColumnBaseValue,
121
+ isGroupRow,
122
+ toolPanelHeaderLabel,
123
+ formatSummaryNumeric,
124
+ getColumnAlign,
125
+ getPinnedCellValue,
126
+ getColumnAccessorValue,
127
+ columnDefMatchesId,
128
+ } from "./cell-values";
129
+
130
+ import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
131
+
132
+ export function createKeyboard<
133
+ TFeatures extends TableFeatures = TableFeatures,
134
+ TData extends RowData = RowData,
135
+ >(ctx: any) {
136
+ function onGridKeyDown(event: KeyboardEvent) {
137
+ // Only the grid root drives navigation - keys on header buttons, menus,
138
+ // or the cell editor are handled by those controls themselves.
139
+ if (event.target !== event.currentTarget) return;
140
+ if (ctx.editingCell) return;
141
+
142
+ if ((event.ctrlKey || event.metaKey) && !event.altKey) {
143
+ const lower = event.key.toLowerCase();
144
+ if (lower === "c") {
145
+ event.preventDefault();
146
+ ctx.copySelectionToClipboard();
147
+ return;
148
+ }
149
+ if (lower === "v") {
150
+ // Secure context: read via the async Clipboard API and swallow the
151
+ // key. Insecure context (no navigator.clipboard): DON'T preventDefault
152
+ // so the browser delivers a native `paste` event to `onGridPaste`.
153
+ if (typeof navigator.clipboard?.readText === "function") {
154
+ event.preventDefault();
155
+ void ctx.pasteFromClipboard();
156
+ }
157
+ return;
158
+ }
159
+ if (lower === "x") {
160
+ event.preventDefault();
161
+ void ctx.cutSelectionToClipboard();
162
+ return;
163
+ }
164
+ // Ctrl+Z (Cmd+Z) undoes the most recent cell edit. Ctrl+Shift+Z
165
+ // and Ctrl+Y both redo. Mirrors VSCode / Sheets / Excel.
166
+ if (lower === "z" && !event.shiftKey) {
167
+ event.preventDefault()
168
+ if (ctx.historyPtr >= 0) {
169
+ const step = ctx.history[ctx.historyPtr]
170
+ if (step) {
171
+ ctx.applyHistoryStep(step, 'undo')
172
+ ctx.historyPtr -= 1
173
+ ctx.historyVersion += 1
174
+ }
175
+ }
176
+ return
177
+ }
178
+ if ((lower === "z" && event.shiftKey) || lower === "y") {
179
+ event.preventDefault()
180
+ if (ctx.historyPtr < ctx.history.length - 1) {
181
+ const step = ctx.history[ctx.historyPtr + 1]
182
+ if (step) {
183
+ ctx.applyHistoryStep(step, 'redo')
184
+ ctx.historyPtr += 1
185
+ ctx.historyVersion += 1
186
+ }
187
+ }
188
+ return
189
+ }
190
+ // Ctrl+F opens the find overlay.
191
+ if (lower === "f") {
192
+ event.preventDefault()
193
+ ctx.findOpen = true
194
+ return
195
+ }
196
+ }
197
+ // Esc closes find when nothing else owns the key.
198
+ if (event.key === "Escape" && ctx.findOpen) {
199
+ event.preventDefault()
200
+ ctx.findOpen = false
201
+ return
202
+ }
203
+
204
+ // Delete / Backspace clears every editable cell in the selection
205
+ // range. No clipboard interaction - this is the "blank the cells I
206
+ // have selected" gesture, distinct from Ctrl/Cmd+X.
207
+ if (
208
+ (event.key === "Delete" || event.key === "Backspace") &&
209
+ !event.ctrlKey && !event.metaKey && !event.altKey
210
+ ) {
211
+ if (ctx.clearSelectedCells()) {
212
+ event.preventDefault();
213
+ return;
214
+ }
215
+ }
216
+
217
+ const current = ctx.grid.getState().activeCell ?? {
218
+ rowIndex: 0,
219
+ colIndex: 0,
220
+ cellId: null,
221
+ };
222
+
223
+ if (event.key === "F2") {
224
+ event.preventDefault();
225
+ ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
226
+ return;
227
+ }
228
+
229
+ const intent = getKeyboardIntent(event);
230
+ if (intent === "noop") {
231
+ // A printable character on the active cell starts editing seeded with it.
232
+ if (
233
+ event.key.length === 1 &&
234
+ !event.ctrlKey &&
235
+ !event.metaKey &&
236
+ !event.altKey
237
+ ) {
238
+ if (
239
+ ctx.startEditingWithChar(current.rowIndex, current.colIndex, event.key)
240
+ ) {
241
+ event.preventDefault();
242
+ }
243
+ }
244
+ return;
245
+ }
246
+ event.preventDefault();
247
+
248
+ if (intent === "clearCells") {
249
+ // Excel `Delete` - clear contents of every cell in the selection
250
+ // (or the active cell if no range). Formatting is left alone, only
251
+ // the underlying value is wiped.
252
+ ctx.clearSelectedCellValues();
253
+ return;
254
+ }
255
+
256
+ if (intent === "activate") {
257
+ const row = ctx.allRows[current.rowIndex];
258
+ const column = ctx.allColumns[current.colIndex];
259
+ if (event.key === " ") {
260
+ row?.toggleSelected?.();
261
+ return;
262
+ }
263
+ if (event.ctrlKey && row?.getCanExpand?.()) {
264
+ row.toggleExpanded?.();
265
+ return;
266
+ }
267
+ if (column?.columnDef.editorType === "checkbox") {
268
+ ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
269
+ return;
270
+ }
271
+ ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
272
+ return;
273
+ }
274
+
275
+ // Excel-style page step: PageUp/PageDown jump by ~one visible
276
+ // page minus one row of overlap. Falls back to grid pagination's
277
+ // pageSize when pagination is on, or 10 as a final fallback.
278
+ function pageStep(): number {
279
+ const pageSize = ctx.grid.getState().pagination?.pageSize as number | undefined
280
+ if (pageSize && pageSize > 0) return pageSize
281
+ const clientHeight = ctx.scrollContainer?.clientHeight ?? 0
282
+ const headerHeight = ctx.headerHeight ?? 0
283
+ const rowHeight =
284
+ typeof ctx.props.rowHeight === 'function'
285
+ ? (ctx.props.rowHeight(current.rowIndex) ?? 30)
286
+ : (ctx.props.rowHeight ?? 30)
287
+ const usable = Math.max(0, clientHeight - headerHeight)
288
+ return Math.max(1, Math.floor(usable / Math.max(rowHeight, 1)) - 1)
289
+ }
290
+ const next = getNextActiveCell(current, intent, {
291
+ maxRow: Math.max(ctx.allRows.length - 1, 0),
292
+ maxCol: Math.max(ctx.allColumns.length - 1, 0),
293
+ pageSize: pageStep(),
294
+ });
295
+ ctx.setActiveCell(next.rowIndex, next.colIndex);
296
+ ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
297
+ // Shift extends the selection ONLY for arrow keys (Excel-style).
298
+ // Shift+Enter / Shift+Tab just change direction; they don't grow the
299
+ // range, otherwise hammering Shift+Tab to backspace through a row
300
+ // would paint a creeping rectangle behind the cursor.
301
+ const shouldExtend =
302
+ event.shiftKey &&
303
+ (intent === "moveLeft" ||
304
+ intent === "moveRight" ||
305
+ intent === "moveUp" ||
306
+ intent === "moveDown");
307
+ if (shouldExtend) ctx.extendSelection(next.rowIndex, next.colIndex);
308
+ else ctx.setSelection(next.rowIndex, next.colIndex);
309
+ }
310
+
311
+ function onWindowKeydown(event: KeyboardEvent) {
312
+ if (event.key === "Escape" && (ctx.columnMenuFor || ctx.operatorMenuFor)) {
313
+ ctx.closeMenus();
314
+ }
315
+ }
316
+
317
+ function onHeaderSortClick(event: MouseEvent, columnId: string) {
318
+ const column = ctx.allColumns.find((entry: any) => entry.id === columnId);
319
+ if (!column?.getCanSort?.()) return;
320
+ const clauses = ctx.grid.getState().sorting ?? [];
321
+ const current = clauses.find(
322
+ (entry: { id: string; desc: boolean }) => entry.id === columnId,
323
+ );
324
+
325
+ if (!event.shiftKey) {
326
+ // Single-sort: cycle this column's direction and clear all other
327
+ // sorts. Cycle order: none → asc → desc → none.
328
+ const nextClause = !current
329
+ ? [{ id: columnId, desc: false }]
330
+ : current.desc
331
+ ? []
332
+ : [{ id: columnId, desc: true }];
333
+ ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: nextClause }));
334
+ return;
335
+ }
336
+
337
+ // Shift-click: append/toggle as part of a multi-sort.
338
+ const nextClause = !current
339
+ ? [...clauses, { id: columnId, desc: false }]
340
+ : current.desc
341
+ ? clauses.filter((entry: { id: string }) => entry.id !== columnId)
342
+ : clauses.map((entry: { id: string; desc: boolean }) =>
343
+ entry.id === columnId ? { ...entry, desc: true } : entry,
344
+ );
345
+ ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: nextClause }));
346
+ }
347
+
348
+ return {
349
+ onGridKeyDown,
350
+ onWindowKeydown,
351
+ onHeaderSortClick,
352
+ };
353
+ }
package/src/keyboard.ts CHANGED
@@ -1,97 +1,97 @@
1
- import type { ActiveCellState } from './core'
2
-
3
- export type GridKeyboardIntent =
4
- | 'moveLeft'
5
- | 'moveRight'
6
- | 'moveUp'
7
- | 'moveDown'
8
- | 'tabNext'
9
- | 'tabPrev'
10
- | 'rowStart'
11
- | 'rowEnd'
12
- | 'gridStart'
13
- | 'gridEnd'
14
- | 'pageUp'
15
- | 'pageDown'
16
- | 'activate'
17
- | 'clearCells'
18
- | 'noop'
19
-
20
- export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
21
- // Ctrl+Home / Ctrl+End come before plain Home / End so the modifier wins.
22
- if (event.ctrlKey && event.key === 'Home') return 'gridStart'
23
- if (event.ctrlKey && event.key === 'End') return 'gridEnd'
24
-
25
- if (event.key === 'ArrowLeft') return 'moveLeft'
26
- if (event.key === 'ArrowRight') return 'moveRight'
27
- if (event.key === 'ArrowUp') return 'moveUp'
28
- if (event.key === 'ArrowDown') return 'moveDown'
29
- if (event.key === 'Home') return 'rowStart'
30
- if (event.key === 'End') return 'rowEnd'
31
- if (event.key === 'PageUp') return 'pageUp'
32
- if (event.key === 'PageDown') return 'pageDown'
33
-
34
- // Excel-style data-entry navigation:
35
- // Enter → move down (Shift+Enter → up)
36
- // Tab → move right (Shift+Tab → left), wraps at row boundaries
37
- // Delete → clear contents of the selected cells
38
- // F2 / Space → start editing the active cell
39
- if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
40
- if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
41
- if (event.key === 'F2') return 'activate'
42
- if (event.key === ' ') return 'activate'
43
- if (event.key === 'Delete') return 'clearCells'
44
-
45
- return 'noop'
46
- }
47
-
48
- export function getNextActiveCell(
49
- current: ActiveCellState,
50
- intent: GridKeyboardIntent,
51
- bounds: { maxRow: number; maxCol: number; pageSize?: number },
52
- ): ActiveCellState {
53
- const pageSize = bounds.pageSize ?? 10
54
- let rowIndex = current.rowIndex
55
- let colIndex = current.colIndex
56
-
57
- if (intent === 'moveLeft') colIndex -= 1
58
- if (intent === 'moveRight') colIndex += 1
59
- if (intent === 'moveUp') rowIndex -= 1
60
- if (intent === 'moveDown') rowIndex += 1
61
- if (intent === 'tabNext') {
62
- // Right one column; if we fall off the right edge, wrap to the
63
- // first column of the next row - Excel's "data entry" behavior.
64
- colIndex += 1
65
- if (colIndex > bounds.maxCol) {
66
- colIndex = 0
67
- rowIndex += 1
68
- }
69
- }
70
- if (intent === 'tabPrev') {
71
- colIndex -= 1
72
- if (colIndex < 0) {
73
- colIndex = bounds.maxCol
74
- rowIndex -= 1
75
- }
76
- }
77
- if (intent === 'rowStart') colIndex = 0
78
- if (intent === 'rowEnd') colIndex = bounds.maxCol
79
- if (intent === 'gridStart') {
80
- rowIndex = 0
81
- colIndex = 0
82
- }
83
- if (intent === 'gridEnd') {
84
- rowIndex = bounds.maxRow
85
- colIndex = bounds.maxCol
86
- }
87
- if (intent === 'pageUp') rowIndex -= pageSize
88
- if (intent === 'pageDown') rowIndex += pageSize
89
-
90
- rowIndex = Math.min(Math.max(rowIndex, 0), Math.max(bounds.maxRow, 0))
91
- colIndex = Math.min(Math.max(colIndex, 0), Math.max(bounds.maxCol, 0))
92
- return {
93
- rowIndex,
94
- colIndex,
95
- cellId: current.cellId,
96
- }
97
- }
1
+ import type { ActiveCellState } from './core'
2
+
3
+ export type GridKeyboardIntent =
4
+ | 'moveLeft'
5
+ | 'moveRight'
6
+ | 'moveUp'
7
+ | 'moveDown'
8
+ | 'tabNext'
9
+ | 'tabPrev'
10
+ | 'rowStart'
11
+ | 'rowEnd'
12
+ | 'gridStart'
13
+ | 'gridEnd'
14
+ | 'pageUp'
15
+ | 'pageDown'
16
+ | 'activate'
17
+ | 'clearCells'
18
+ | 'noop'
19
+
20
+ export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
21
+ // Ctrl+Home / Ctrl+End come before plain Home / End so the modifier wins.
22
+ if (event.ctrlKey && event.key === 'Home') return 'gridStart'
23
+ if (event.ctrlKey && event.key === 'End') return 'gridEnd'
24
+
25
+ if (event.key === 'ArrowLeft') return 'moveLeft'
26
+ if (event.key === 'ArrowRight') return 'moveRight'
27
+ if (event.key === 'ArrowUp') return 'moveUp'
28
+ if (event.key === 'ArrowDown') return 'moveDown'
29
+ if (event.key === 'Home') return 'rowStart'
30
+ if (event.key === 'End') return 'rowEnd'
31
+ if (event.key === 'PageUp') return 'pageUp'
32
+ if (event.key === 'PageDown') return 'pageDown'
33
+
34
+ // Excel-style data-entry navigation:
35
+ // Enter → move down (Shift+Enter → up)
36
+ // Tab → move right (Shift+Tab → left), wraps at row boundaries
37
+ // Delete → clear contents of the selected cells
38
+ // F2 / Space → start editing the active cell
39
+ if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
40
+ if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
41
+ if (event.key === 'F2') return 'activate'
42
+ if (event.key === ' ') return 'activate'
43
+ if (event.key === 'Delete') return 'clearCells'
44
+
45
+ return 'noop'
46
+ }
47
+
48
+ export function getNextActiveCell(
49
+ current: ActiveCellState,
50
+ intent: GridKeyboardIntent,
51
+ bounds: { maxRow: number; maxCol: number; pageSize?: number },
52
+ ): ActiveCellState {
53
+ const pageSize = bounds.pageSize ?? 10
54
+ let rowIndex = current.rowIndex
55
+ let colIndex = current.colIndex
56
+
57
+ if (intent === 'moveLeft') colIndex -= 1
58
+ if (intent === 'moveRight') colIndex += 1
59
+ if (intent === 'moveUp') rowIndex -= 1
60
+ if (intent === 'moveDown') rowIndex += 1
61
+ if (intent === 'tabNext') {
62
+ // Right one column; if we fall off the right edge, wrap to the
63
+ // first column of the next row - Excel's "data entry" behavior.
64
+ colIndex += 1
65
+ if (colIndex > bounds.maxCol) {
66
+ colIndex = 0
67
+ rowIndex += 1
68
+ }
69
+ }
70
+ if (intent === 'tabPrev') {
71
+ colIndex -= 1
72
+ if (colIndex < 0) {
73
+ colIndex = bounds.maxCol
74
+ rowIndex -= 1
75
+ }
76
+ }
77
+ if (intent === 'rowStart') colIndex = 0
78
+ if (intent === 'rowEnd') colIndex = bounds.maxCol
79
+ if (intent === 'gridStart') {
80
+ rowIndex = 0
81
+ colIndex = 0
82
+ }
83
+ if (intent === 'gridEnd') {
84
+ rowIndex = bounds.maxRow
85
+ colIndex = bounds.maxCol
86
+ }
87
+ if (intent === 'pageUp') rowIndex -= pageSize
88
+ if (intent === 'pageDown') rowIndex += pageSize
89
+
90
+ rowIndex = Math.min(Math.max(rowIndex, 0), Math.max(bounds.maxRow, 0))
91
+ colIndex = Math.min(Math.max(colIndex, 0), Math.max(bounds.maxCol, 0))
92
+ return {
93
+ rowIndex,
94
+ colIndex,
95
+ cellId: current.cellId,
96
+ }
97
+ }