@alaarab/ogrid-angular 2.5.9 → 2.6.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 (60) hide show
  1. package/dist/esm/components/base-column-chooser.component.js +77 -0
  2. package/dist/esm/components/base-column-header-filter.component.js +267 -0
  3. package/dist/esm/components/base-column-header-menu.component.js +80 -0
  4. package/dist/esm/components/base-datagrid-table.component.js +768 -0
  5. package/dist/esm/components/base-inline-cell-editor.component.js +380 -0
  6. package/dist/esm/components/base-ogrid.component.js +36 -0
  7. package/dist/esm/components/base-pagination-controls.component.js +68 -0
  8. package/dist/esm/components/base-popover-cell-editor.component.js +122 -0
  9. package/dist/esm/components/empty-state.component.js +68 -0
  10. package/dist/esm/components/formula-bar.component.js +99 -0
  11. package/dist/esm/components/formula-ref-overlay.component.js +115 -0
  12. package/dist/esm/components/grid-context-menu.component.js +197 -0
  13. package/dist/esm/components/inline-cell-editor-template.js +134 -0
  14. package/dist/esm/components/marching-ants-overlay.component.js +177 -0
  15. package/dist/esm/components/ogrid-layout.component.js +302 -0
  16. package/dist/esm/components/sheet-tabs.component.js +83 -0
  17. package/dist/esm/components/sidebar.component.js +431 -0
  18. package/dist/esm/components/status-bar.component.js +92 -0
  19. package/dist/esm/index.js +39 -819
  20. package/dist/esm/services/column-reorder.service.js +176 -0
  21. package/dist/esm/services/datagrid-editing.service.js +59 -0
  22. package/dist/esm/services/datagrid-interaction.service.js +744 -0
  23. package/dist/esm/services/datagrid-layout.service.js +157 -0
  24. package/dist/esm/services/datagrid-state.service.js +636 -0
  25. package/dist/esm/services/formula-engine.service.js +223 -0
  26. package/dist/esm/services/ogrid.service.js +1094 -0
  27. package/dist/esm/services/virtual-scroll.service.js +114 -0
  28. package/dist/esm/styles/ogrid-theme-vars.js +112 -0
  29. package/dist/esm/types/columnTypes.js +1 -0
  30. package/dist/esm/types/dataGridTypes.js +1 -0
  31. package/dist/esm/types/index.js +1 -0
  32. package/dist/esm/utils/dataGridViewModel.js +6 -0
  33. package/dist/esm/utils/debounce.js +68 -0
  34. package/dist/esm/utils/index.js +8 -0
  35. package/dist/esm/utils/latestRef.js +41 -0
  36. package/dist/types/components/base-column-chooser.component.d.ts +3 -0
  37. package/dist/types/components/base-column-header-filter.component.d.ts +3 -0
  38. package/dist/types/components/base-column-header-menu.component.d.ts +3 -0
  39. package/dist/types/components/base-datagrid-table.component.d.ts +3 -0
  40. package/dist/types/components/base-inline-cell-editor.component.d.ts +7 -0
  41. package/dist/types/components/base-ogrid.component.d.ts +3 -0
  42. package/dist/types/components/base-pagination-controls.component.d.ts +3 -0
  43. package/dist/types/components/base-popover-cell-editor.component.d.ts +3 -0
  44. package/dist/types/components/empty-state.component.d.ts +3 -0
  45. package/dist/types/components/formula-bar.component.d.ts +3 -8
  46. package/dist/types/components/formula-ref-overlay.component.d.ts +3 -6
  47. package/dist/types/components/grid-context-menu.component.d.ts +3 -0
  48. package/dist/types/components/inline-cell-editor-template.d.ts +2 -2
  49. package/dist/types/components/marching-ants-overlay.component.d.ts +3 -0
  50. package/dist/types/components/ogrid-layout.component.d.ts +3 -0
  51. package/dist/types/components/sheet-tabs.component.d.ts +3 -8
  52. package/dist/types/components/sidebar.component.d.ts +3 -0
  53. package/dist/types/components/status-bar.component.d.ts +3 -0
  54. package/dist/types/services/column-reorder.service.d.ts +3 -0
  55. package/dist/types/services/datagrid-interaction.service.d.ts +1 -0
  56. package/dist/types/services/datagrid-state.service.d.ts +5 -0
  57. package/dist/types/services/formula-engine.service.d.ts +3 -9
  58. package/dist/types/services/ogrid.service.d.ts +8 -2
  59. package/dist/types/services/virtual-scroll.service.d.ts +3 -0
  60. package/package.json +4 -3
@@ -0,0 +1,744 @@
1
+ import { signal, computed } from '@angular/core';
2
+ import { getCellValue, normalizeSelectionRange, parseValue, UndoRedoStack, findCtrlArrowTarget, computeTabNavigation, formatSelectionAsTsv, parseTsvClipboard, rangesEqual, applyFillValues, applyCellDeletion, getScrollTopForRow, } from '@alaarab/ogrid-core';
3
+ /**
4
+ * Manages cell selection, keyboard navigation, clipboard, fill handle, and undo/redo.
5
+ * Extracted from DataGridStateService for modularity.
6
+ *
7
+ * Not @Injectable - instantiated and owned by DataGridStateService.
8
+ */
9
+ export class DataGridInteractionHelper {
10
+ constructor() {
11
+ // --- Signals ---
12
+ this.activeCellSig = signal(null, ...(ngDevMode ? [{ debugName: "activeCellSig" }] : []));
13
+ this.selectionRangeSig = signal(null, ...(ngDevMode ? [{ debugName: "selectionRangeSig" }] : []));
14
+ this.isDraggingSig = signal(false, ...(ngDevMode ? [{ debugName: "isDraggingSig" }] : []));
15
+ this.contextMenuPositionSig = signal(null, ...(ngDevMode ? [{ debugName: "contextMenuPositionSig" }] : []));
16
+ this.cutRangeSig = signal(null, ...(ngDevMode ? [{ debugName: "cutRangeSig" }] : []));
17
+ this.copyRangeSig = signal(null, ...(ngDevMode ? [{ debugName: "copyRangeSig" }] : []));
18
+ this.internalClipboard = null;
19
+ this.preferInternalClipboard = false;
20
+ // Undo/redo
21
+ this.undoRedoStack = new UndoRedoStack(100);
22
+ this.undoLengthSig = signal(0, ...(ngDevMode ? [{ debugName: "undoLengthSig" }] : []));
23
+ this.redoLengthSig = signal(0, ...(ngDevMode ? [{ debugName: "redoLengthSig" }] : []));
24
+ this.canUndo = computed(() => this.undoLengthSig() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : []));
25
+ this.canRedo = computed(() => this.redoLengthSig() > 0, ...(ngDevMode ? [{ debugName: "canRedo" }] : []));
26
+ this.hasCellSelection = computed(() => this.selectionRangeSig() != null || this.activeCellSig() != null, ...(ngDevMode ? [{ debugName: "hasCellSelection" }] : []));
27
+ // Fill handle state
28
+ this.fillDragStart = null;
29
+ this.fillRafId = 0;
30
+ this.fillMoveHandler = null;
31
+ this.fillUpHandler = null;
32
+ // Drag selection refs
33
+ this.dragStartPos = null;
34
+ this.dragMoved = false;
35
+ this.isDraggingRef = false;
36
+ this.liveDragRange = null;
37
+ this.rafId = 0;
38
+ this.lastMousePos = null;
39
+ this.autoScrollInterval = null;
40
+ }
41
+ setActiveCell(cell) {
42
+ const prev = this.activeCellSig();
43
+ if (prev === cell)
44
+ return;
45
+ if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
46
+ return;
47
+ this.activeCellSig.set(cell);
48
+ }
49
+ setSelectionRange(range) {
50
+ const prev = this.selectionRangeSig();
51
+ if (rangesEqual(prev, range))
52
+ return;
53
+ this.selectionRangeSig.set(range);
54
+ }
55
+ // --- Context menu ---
56
+ setContextMenuPosition(pos) {
57
+ this.contextMenuPositionSig.set(pos);
58
+ }
59
+ handleCellContextMenu(e) {
60
+ e.preventDefault?.();
61
+ this.contextMenuPositionSig.set({ x: e.clientX, y: e.clientY });
62
+ }
63
+ closeContextMenu() {
64
+ this.contextMenuPositionSig.set(null);
65
+ }
66
+ // --- Clipboard ---
67
+ handleCopy(items, visibleCols, colOffset) {
68
+ const range = this.getEffectiveRange(colOffset);
69
+ if (range == null)
70
+ return;
71
+ const norm = normalizeSelectionRange(range);
72
+ const tsv = formatSelectionAsTsv(items, visibleCols, norm);
73
+ this.internalClipboard = tsv;
74
+ this.preferInternalClipboard = true;
75
+ this.copyRangeSig.set(norm);
76
+ void navigator.clipboard.writeText(tsv)
77
+ .then(() => {
78
+ this.preferInternalClipboard = false;
79
+ })
80
+ .catch(() => {
81
+ this.preferInternalClipboard = true;
82
+ });
83
+ }
84
+ handleCut(items, visibleCols, colOffset, editable, wrappedOnCellValueChanged) {
85
+ if (editable === false)
86
+ return;
87
+ const range = this.getEffectiveRange(colOffset);
88
+ if (range == null || !wrappedOnCellValueChanged)
89
+ return;
90
+ const norm = normalizeSelectionRange(range);
91
+ this.cutRangeSig.set(norm);
92
+ this.copyRangeSig.set(null);
93
+ this.handleCopy(items, visibleCols, colOffset);
94
+ this.copyRangeSig.set(null);
95
+ }
96
+ async handlePaste(items, visibleCols, colOffset, editable, wrappedOnCellValueChanged) {
97
+ if (editable === false)
98
+ return;
99
+ if (!wrappedOnCellValueChanged)
100
+ return;
101
+ let text = this.preferInternalClipboard && this.internalClipboard != null
102
+ ? this.internalClipboard
103
+ : '';
104
+ if (!text.trim()) {
105
+ try {
106
+ text = await navigator.clipboard.readText();
107
+ }
108
+ catch {
109
+ text = '';
110
+ }
111
+ }
112
+ if (!text.trim() && this.internalClipboard != null) {
113
+ text = this.internalClipboard;
114
+ }
115
+ if (!text.trim())
116
+ return;
117
+ const norm = this.getEffectiveRange(colOffset);
118
+ const anchorRow = norm ? norm.startRow : 0;
119
+ const anchorCol = norm ? norm.startCol : 0;
120
+ const parsedRows = parseTsvClipboard(text);
121
+ this.beginBatch();
122
+ for (let r = 0; r < parsedRows.length; r++) {
123
+ const cells = parsedRows[r];
124
+ for (let c = 0; c < cells.length; c++) {
125
+ const targetRow = anchorRow + r;
126
+ const targetCol = anchorCol + c;
127
+ if (targetRow >= items.length || targetCol >= visibleCols.length)
128
+ continue;
129
+ const item = items[targetRow];
130
+ const col = visibleCols[targetCol];
131
+ const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
132
+ if (!colEditable)
133
+ continue;
134
+ const rawValue = cells[c] ?? '';
135
+ const oldValue = getCellValue(item, col);
136
+ const result = parseValue(rawValue, oldValue, item, col);
137
+ if (!result.valid)
138
+ continue;
139
+ wrappedOnCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: targetRow });
140
+ }
141
+ }
142
+ const cutRange = this.cutRangeSig();
143
+ if (cutRange) {
144
+ for (let r = cutRange.startRow; r <= cutRange.endRow; r++) {
145
+ for (let c = cutRange.startCol; c <= cutRange.endCol; c++) {
146
+ if (r >= items.length || c >= visibleCols.length)
147
+ continue;
148
+ const item = items[r];
149
+ const col = visibleCols[c];
150
+ const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
151
+ if (!colEditable)
152
+ continue;
153
+ const oldValue = getCellValue(item, col);
154
+ const result = parseValue('', oldValue, item, col);
155
+ if (!result.valid)
156
+ continue;
157
+ wrappedOnCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: r });
158
+ }
159
+ }
160
+ this.cutRangeSig.set(null);
161
+ }
162
+ this.endBatch();
163
+ this.copyRangeSig.set(null);
164
+ }
165
+ clearClipboardRanges() {
166
+ this.copyRangeSig.set(null);
167
+ this.cutRangeSig.set(null);
168
+ }
169
+ // --- Undo/Redo ---
170
+ beginBatch() {
171
+ this.undoRedoStack.beginBatch();
172
+ }
173
+ endBatch() {
174
+ this.undoRedoStack.endBatch();
175
+ this.undoLengthSig.set(this.undoRedoStack.historyLength);
176
+ this.redoLengthSig.set(this.undoRedoStack.redoLength);
177
+ }
178
+ undo(originalOnCellValueChanged) {
179
+ if (!originalOnCellValueChanged)
180
+ return;
181
+ const lastBatch = this.undoRedoStack.undo();
182
+ if (!lastBatch)
183
+ return;
184
+ this.undoLengthSig.set(this.undoRedoStack.historyLength);
185
+ this.redoLengthSig.set(this.undoRedoStack.redoLength);
186
+ for (let i = lastBatch.length - 1; i >= 0; i--) {
187
+ const ev = lastBatch[i];
188
+ originalOnCellValueChanged({ ...ev, oldValue: ev.newValue, newValue: ev.oldValue });
189
+ }
190
+ }
191
+ redo(originalOnCellValueChanged) {
192
+ if (!originalOnCellValueChanged)
193
+ return;
194
+ const nextBatch = this.undoRedoStack.redo();
195
+ if (!nextBatch)
196
+ return;
197
+ this.undoLengthSig.set(this.undoRedoStack.historyLength);
198
+ this.redoLengthSig.set(this.undoRedoStack.redoLength);
199
+ for (const ev of nextBatch) {
200
+ originalOnCellValueChanged(ev);
201
+ }
202
+ }
203
+ // --- Cell selection / mouse handling ---
204
+ handleCellMouseDown(e, rowIndex, globalColIndex, colOffset, wrapperEl) {
205
+ if (e.button !== 0)
206
+ return;
207
+ wrapperEl?.focus({ preventScroll: true });
208
+ this.clearClipboardRanges();
209
+ if (globalColIndex < colOffset)
210
+ return;
211
+ e.preventDefault();
212
+ const dataColIndex = globalColIndex - colOffset;
213
+ const currentRange = this.selectionRangeSig();
214
+ if (e.shiftKey && currentRange != null) {
215
+ this.setSelectionRange(normalizeSelectionRange({
216
+ startRow: currentRange.startRow,
217
+ startCol: currentRange.startCol,
218
+ endRow: rowIndex,
219
+ endCol: dataColIndex,
220
+ }));
221
+ this.setActiveCell({ rowIndex, columnIndex: globalColIndex });
222
+ }
223
+ else {
224
+ this.dragStartPos = { row: rowIndex, col: dataColIndex };
225
+ this.dragMoved = false;
226
+ const initial = {
227
+ startRow: rowIndex, startCol: dataColIndex,
228
+ endRow: rowIndex, endCol: dataColIndex,
229
+ };
230
+ this.setSelectionRange(initial);
231
+ this.liveDragRange = initial;
232
+ this.setActiveCell({ rowIndex, columnIndex: globalColIndex });
233
+ this.isDraggingRef = true;
234
+ // Apply drag attrs synchronously so the anchor cell styling (data-drag-range)
235
+ // is in place before the next browser paint. Without this, the origin cell
236
+ // briefly shows a green outline (from the active-cell CSS class) until the
237
+ // first pointermove fires and the RAF applies the drag attributes.
238
+ this.applyDragAttrs(initial, colOffset, wrapperEl);
239
+ }
240
+ }
241
+ handleSelectAllCells(rowCount, visibleColCount, colOffset) {
242
+ if (rowCount === 0 || visibleColCount === 0)
243
+ return;
244
+ this.setSelectionRange({
245
+ startRow: 0, startCol: 0,
246
+ endRow: rowCount - 1, endCol: visibleColCount - 1,
247
+ });
248
+ this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
249
+ }
250
+ // --- Fill handle ---
251
+ handleFillHandleMouseDown(e) {
252
+ e.preventDefault();
253
+ e.stopPropagation();
254
+ const range = this.selectionRangeSig();
255
+ if (!range)
256
+ return;
257
+ this.fillDragStart = { startRow: range.startRow, startCol: range.startCol };
258
+ }
259
+ // --- Keyboard navigation ---
260
+ handleGridKeyDown(e, items, getRowId, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, editable, wrappedOnCellValueChanged, originalOnCellValueChanged, rowSelection, selectedRowIds, wrapperEl, handleRowCheckboxChange, editingCell, setEditingCell, onKeyDown) {
261
+ // Consumer intercept: call consumer's handler first; skip grid default if preventDefault() was called
262
+ if (onKeyDown) {
263
+ onKeyDown(e);
264
+ if (e.defaultPrevented)
265
+ return;
266
+ }
267
+ const activeCell = this.activeCellSig();
268
+ const selectionRange = this.selectionRangeSig();
269
+ const maxRowIndex = items.length - 1;
270
+ const maxColIndex = visibleColumnCount - 1 + colOffset;
271
+ if (items.length === 0)
272
+ return;
273
+ if (activeCell === null) {
274
+ if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End', 'PageDown', 'PageUp'].includes(e.key)) {
275
+ this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
276
+ e.preventDefault();
277
+ }
278
+ return;
279
+ }
280
+ const { rowIndex, columnIndex } = activeCell;
281
+ const dataColIndex = columnIndex - colOffset;
282
+ const shift = e.shiftKey;
283
+ const ctrl = e.ctrlKey || e.metaKey;
284
+ const isEmptyAt = (r, c) => {
285
+ if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
286
+ return true;
287
+ const v = getCellValue(items[r], visibleCols[c]);
288
+ return v == null || v === '';
289
+ };
290
+ const findCtrlTarget = findCtrlArrowTarget;
291
+ switch (e.key) {
292
+ case 'c':
293
+ if (ctrl) {
294
+ if (editingCell != null)
295
+ break;
296
+ e.preventDefault();
297
+ this.handleCopy(items, visibleCols, colOffset);
298
+ }
299
+ break;
300
+ case 'x':
301
+ if (ctrl) {
302
+ if (editingCell != null)
303
+ break;
304
+ e.preventDefault();
305
+ this.handleCut(items, visibleCols, colOffset, editable, wrappedOnCellValueChanged);
306
+ }
307
+ break;
308
+ case 'v':
309
+ if (ctrl) {
310
+ if (editingCell != null)
311
+ break;
312
+ e.preventDefault();
313
+ void this.handlePaste(items, visibleCols, colOffset, editable, wrappedOnCellValueChanged);
314
+ }
315
+ break;
316
+ case 'd':
317
+ if (ctrl) {
318
+ if (editingCell != null)
319
+ break;
320
+ if (editable !== false && wrappedOnCellValueChanged != null) {
321
+ const fillRange = selectionRange ?? (activeCell != null
322
+ ? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset }
323
+ : null);
324
+ if (fillRange != null) {
325
+ e.preventDefault();
326
+ const norm = normalizeSelectionRange(fillRange);
327
+ const fillEvents = applyFillValues(norm, norm.startRow, norm.startCol, items, visibleCols);
328
+ if (fillEvents.length > 0) {
329
+ this.undoRedoStack.beginBatch();
330
+ for (const evt of fillEvents)
331
+ wrappedOnCellValueChanged(evt);
332
+ this.undoRedoStack.endBatch();
333
+ this.undoLengthSig.set(this.undoRedoStack.historyLength);
334
+ this.redoLengthSig.set(this.undoRedoStack.redoLength);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ break;
340
+ case 'ArrowDown': {
341
+ if (editingCell != null)
342
+ break;
343
+ e.preventDefault();
344
+ const newRow = ctrl
345
+ ? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
346
+ : Math.min(rowIndex + 1, maxRowIndex);
347
+ if (shift) {
348
+ this.setSelectionRange(normalizeSelectionRange({
349
+ startRow: selectionRange?.startRow ?? rowIndex,
350
+ startCol: selectionRange?.startCol ?? dataColIndex,
351
+ endRow: newRow,
352
+ endCol: selectionRange?.endCol ?? dataColIndex,
353
+ }));
354
+ }
355
+ else {
356
+ this.setSelectionRange({ startRow: newRow, startCol: dataColIndex, endRow: newRow, endCol: dataColIndex });
357
+ }
358
+ this.setActiveCell({ rowIndex: newRow, columnIndex });
359
+ break;
360
+ }
361
+ case 'ArrowUp': {
362
+ if (editingCell != null)
363
+ break;
364
+ e.preventDefault();
365
+ const newRowUp = ctrl
366
+ ? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
367
+ : Math.max(rowIndex - 1, 0);
368
+ if (shift) {
369
+ this.setSelectionRange(normalizeSelectionRange({
370
+ startRow: selectionRange?.startRow ?? rowIndex,
371
+ startCol: selectionRange?.startCol ?? dataColIndex,
372
+ endRow: newRowUp,
373
+ endCol: selectionRange?.endCol ?? dataColIndex,
374
+ }));
375
+ }
376
+ else {
377
+ this.setSelectionRange({ startRow: newRowUp, startCol: dataColIndex, endRow: newRowUp, endCol: dataColIndex });
378
+ }
379
+ this.setActiveCell({ rowIndex: newRowUp, columnIndex });
380
+ break;
381
+ }
382
+ case 'ArrowRight': {
383
+ if (editingCell != null)
384
+ break;
385
+ e.preventDefault();
386
+ let newCol;
387
+ if (ctrl && dataColIndex >= 0) {
388
+ newCol = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
389
+ }
390
+ else {
391
+ newCol = Math.min(columnIndex + 1, maxColIndex);
392
+ }
393
+ const newDataCol = newCol - colOffset;
394
+ if (shift) {
395
+ this.setSelectionRange(normalizeSelectionRange({
396
+ startRow: selectionRange?.startRow ?? rowIndex,
397
+ startCol: selectionRange?.startCol ?? dataColIndex,
398
+ endRow: selectionRange?.endRow ?? rowIndex,
399
+ endCol: newDataCol,
400
+ }));
401
+ }
402
+ else {
403
+ this.setSelectionRange({ startRow: rowIndex, startCol: newDataCol, endRow: rowIndex, endCol: newDataCol });
404
+ }
405
+ this.setActiveCell({ rowIndex, columnIndex: newCol });
406
+ break;
407
+ }
408
+ case 'ArrowLeft': {
409
+ if (editingCell != null)
410
+ break;
411
+ e.preventDefault();
412
+ let newColLeft;
413
+ if (ctrl && dataColIndex >= 0) {
414
+ newColLeft = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
415
+ }
416
+ else {
417
+ newColLeft = Math.max(columnIndex - 1, colOffset);
418
+ }
419
+ const newDataColLeft = newColLeft - colOffset;
420
+ if (shift) {
421
+ this.setSelectionRange(normalizeSelectionRange({
422
+ startRow: selectionRange?.startRow ?? rowIndex,
423
+ startCol: selectionRange?.startCol ?? dataColIndex,
424
+ endRow: selectionRange?.endRow ?? rowIndex,
425
+ endCol: newDataColLeft,
426
+ }));
427
+ }
428
+ else {
429
+ this.setSelectionRange({ startRow: rowIndex, startCol: newDataColLeft, endRow: rowIndex, endCol: newDataColLeft });
430
+ }
431
+ this.setActiveCell({ rowIndex, columnIndex: newColLeft });
432
+ break;
433
+ }
434
+ case 'Tab': {
435
+ e.preventDefault();
436
+ const tabResult = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
437
+ const newDataColTab = tabResult.columnIndex - colOffset;
438
+ this.setSelectionRange({ startRow: tabResult.rowIndex, startCol: newDataColTab, endRow: tabResult.rowIndex, endCol: newDataColTab });
439
+ this.setActiveCell({ rowIndex: tabResult.rowIndex, columnIndex: tabResult.columnIndex });
440
+ break;
441
+ }
442
+ case 'Home': {
443
+ e.preventDefault();
444
+ const newRowHome = ctrl ? 0 : rowIndex;
445
+ this.setSelectionRange({ startRow: newRowHome, startCol: 0, endRow: newRowHome, endCol: 0 });
446
+ this.setActiveCell({ rowIndex: newRowHome, columnIndex: colOffset });
447
+ break;
448
+ }
449
+ case 'End': {
450
+ e.preventDefault();
451
+ const newRowEnd = ctrl ? maxRowIndex : rowIndex;
452
+ this.setSelectionRange({ startRow: newRowEnd, startCol: visibleColumnCount - 1, endRow: newRowEnd, endCol: visibleColumnCount - 1 });
453
+ this.setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
454
+ break;
455
+ }
456
+ case 'PageDown':
457
+ case 'PageUp': {
458
+ e.preventDefault();
459
+ let pageSize = 10;
460
+ let rowHeight = 36;
461
+ if (wrapperEl) {
462
+ const firstRow = wrapperEl.querySelector('tbody tr');
463
+ if (firstRow && firstRow.offsetHeight > 0) {
464
+ rowHeight = firstRow.offsetHeight;
465
+ pageSize = Math.max(1, Math.floor(wrapperEl.clientHeight / rowHeight));
466
+ }
467
+ }
468
+ const pgDir = e.key === 'PageDown' ? 1 : -1;
469
+ const newRowPage = Math.max(0, Math.min(rowIndex + pgDir * pageSize, maxRowIndex));
470
+ if (shift) {
471
+ this.setSelectionRange(normalizeSelectionRange({
472
+ startRow: selectionRange?.startRow ?? rowIndex,
473
+ startCol: selectionRange?.startCol ?? dataColIndex,
474
+ endRow: newRowPage,
475
+ endCol: selectionRange?.endCol ?? dataColIndex,
476
+ }));
477
+ }
478
+ else {
479
+ this.setSelectionRange({ startRow: newRowPage, startCol: dataColIndex, endRow: newRowPage, endCol: dataColIndex });
480
+ }
481
+ this.setActiveCell({ rowIndex: newRowPage, columnIndex });
482
+ // Scroll the new row into view
483
+ if (wrapperEl) {
484
+ wrapperEl.scrollTop = getScrollTopForRow(newRowPage, rowHeight, wrapperEl.clientHeight, 'center');
485
+ }
486
+ break;
487
+ }
488
+ case 'Enter':
489
+ case 'F2': {
490
+ e.preventDefault();
491
+ if (dataColIndex >= 0 && dataColIndex < visibleCols.length) {
492
+ const col = visibleCols[dataColIndex];
493
+ const item = items[rowIndex];
494
+ if (item && col) {
495
+ const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
496
+ if (editable !== false && colEditable && wrappedOnCellValueChanged != null) {
497
+ setEditingCell({ rowId: getRowId(item), columnId: col.columnId });
498
+ }
499
+ }
500
+ }
501
+ break;
502
+ }
503
+ case 'Escape':
504
+ e.preventDefault();
505
+ if (editingCell != null) {
506
+ setEditingCell(null);
507
+ }
508
+ else {
509
+ this.clearClipboardRanges();
510
+ this.setActiveCell(null);
511
+ this.setSelectionRange(null);
512
+ }
513
+ break;
514
+ case ' ':
515
+ if (rowSelection !== 'none' && columnIndex === 0 && hasCheckboxCol) {
516
+ e.preventDefault();
517
+ const item = items[rowIndex];
518
+ if (item) {
519
+ const id = getRowId(item);
520
+ const isSelected = selectedRowIds.has(id);
521
+ handleRowCheckboxChange(id, !isSelected, rowIndex, e.shiftKey);
522
+ }
523
+ }
524
+ break;
525
+ case 'z':
526
+ if (ctrl) {
527
+ if (editingCell == null) {
528
+ if (e.shiftKey) {
529
+ e.preventDefault();
530
+ this.redo(originalOnCellValueChanged);
531
+ }
532
+ else {
533
+ e.preventDefault();
534
+ this.undo(originalOnCellValueChanged);
535
+ }
536
+ }
537
+ }
538
+ break;
539
+ case 'y':
540
+ if (ctrl && editingCell == null) {
541
+ e.preventDefault();
542
+ this.redo(originalOnCellValueChanged);
543
+ }
544
+ break;
545
+ case 'a':
546
+ if (ctrl) {
547
+ if (editingCell != null)
548
+ break;
549
+ e.preventDefault();
550
+ if (items.length > 0 && visibleColumnCount > 0) {
551
+ this.setSelectionRange({ startRow: 0, startCol: 0, endRow: items.length - 1, endCol: visibleColumnCount - 1 });
552
+ this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
553
+ }
554
+ }
555
+ break;
556
+ case 'Delete':
557
+ case 'Backspace': {
558
+ if (editingCell != null)
559
+ break;
560
+ if (editable === false)
561
+ break;
562
+ if (wrappedOnCellValueChanged == null)
563
+ break;
564
+ const range = selectionRange ?? (activeCell != null
565
+ ? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset }
566
+ : null);
567
+ if (range == null)
568
+ break;
569
+ e.preventDefault();
570
+ const deleteEvents = applyCellDeletion(normalizeSelectionRange(range), items, visibleCols);
571
+ for (const evt of deleteEvents)
572
+ wrappedOnCellValueChanged(evt);
573
+ break;
574
+ }
575
+ case 'F10':
576
+ if (e.shiftKey) {
577
+ e.preventDefault();
578
+ if (activeCell != null && wrapperEl) {
579
+ const sel = `[data-row-index="${activeCell.rowIndex}"][data-col-index="${activeCell.columnIndex}"]`;
580
+ const cell = wrapperEl.querySelector(sel);
581
+ if (cell) {
582
+ const rect = cell.getBoundingClientRect();
583
+ this.setContextMenuPosition({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
584
+ }
585
+ else {
586
+ this.setContextMenuPosition({ x: 100, y: 100 });
587
+ }
588
+ }
589
+ else {
590
+ this.setContextMenuPosition({ x: 100, y: 100 });
591
+ }
592
+ }
593
+ break;
594
+ default:
595
+ break;
596
+ }
597
+ }
598
+ // --- Drag helpers ---
599
+ onWindowMouseMove(e, colOffset, wrapperEl) {
600
+ if (!this.isDraggingRef || !this.dragStartPos)
601
+ return;
602
+ if (!this.dragMoved) {
603
+ this.dragMoved = true;
604
+ this.isDraggingSig.set(true);
605
+ }
606
+ this.lastMousePos = { cx: e.clientX, cy: e.clientY };
607
+ if (this.rafId)
608
+ cancelAnimationFrame(this.rafId);
609
+ this.rafId = requestAnimationFrame(() => {
610
+ this.rafId = 0;
611
+ const pos = this.lastMousePos;
612
+ if (!pos)
613
+ return;
614
+ const newRange = this.resolveRangeFromMouse(pos.cx, pos.cy, colOffset);
615
+ if (!newRange)
616
+ return;
617
+ const prev = this.liveDragRange;
618
+ if (prev && prev.startRow === newRange.startRow && prev.startCol === newRange.startCol &&
619
+ prev.endRow === newRange.endRow && prev.endCol === newRange.endCol)
620
+ return;
621
+ this.liveDragRange = newRange;
622
+ this.applyDragAttrs(newRange, colOffset, wrapperEl);
623
+ });
624
+ }
625
+ onWindowMouseUp(colOffset, wrapperEl) {
626
+ if (!this.isDraggingRef)
627
+ return;
628
+ if (this.autoScrollInterval) {
629
+ clearInterval(this.autoScrollInterval);
630
+ this.autoScrollInterval = null;
631
+ }
632
+ if (this.rafId) {
633
+ cancelAnimationFrame(this.rafId);
634
+ this.rafId = 0;
635
+ }
636
+ this.isDraggingRef = false;
637
+ const wasDrag = this.dragMoved;
638
+ if (wasDrag) {
639
+ const pos = this.lastMousePos;
640
+ if (pos) {
641
+ const flushed = this.resolveRangeFromMouse(pos.cx, pos.cy, colOffset);
642
+ if (flushed)
643
+ this.liveDragRange = flushed;
644
+ }
645
+ const finalRange = this.liveDragRange;
646
+ if (finalRange) {
647
+ this.setSelectionRange(finalRange);
648
+ // Keep the active cell at the drag anchor (start), not the endpoint.
649
+ const anchor = this.dragStartPos;
650
+ if (anchor) {
651
+ this.setActiveCell({
652
+ rowIndex: anchor.row,
653
+ columnIndex: anchor.col + colOffset,
654
+ });
655
+ }
656
+ }
657
+ }
658
+ this.clearDragAttrs(wrapperEl);
659
+ this.liveDragRange = null;
660
+ this.lastMousePos = null;
661
+ this.dragStartPos = null;
662
+ if (wasDrag)
663
+ this.isDraggingSig.set(false);
664
+ }
665
+ resolveRangeFromMouse(cx, cy, colOffset) {
666
+ if (!this.dragStartPos)
667
+ return null;
668
+ const target = document.elementFromPoint(cx, cy);
669
+ const cell = target?.closest?.('[data-row-index][data-col-index]');
670
+ if (!cell)
671
+ return null;
672
+ const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
673
+ const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
674
+ if (Number.isNaN(r) || Number.isNaN(c) || c < colOffset)
675
+ return null;
676
+ const dataCol = c - colOffset;
677
+ const start = this.dragStartPos;
678
+ return normalizeSelectionRange({
679
+ startRow: start.row, startCol: start.col,
680
+ endRow: r, endCol: dataCol,
681
+ });
682
+ }
683
+ applyDragAttrs(range, colOff, wrapper) {
684
+ if (!wrapper)
685
+ return;
686
+ const minR = Math.min(range.startRow, range.endRow);
687
+ const maxR = Math.max(range.startRow, range.endRow);
688
+ const minC = Math.min(range.startCol, range.endCol);
689
+ const maxC = Math.max(range.startCol, range.endCol);
690
+ const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
691
+ for (let i = 0; i < cells.length; i++) {
692
+ const el = cells[i];
693
+ const r = parseInt(el.getAttribute('data-row-index') ?? '', 10);
694
+ const c = parseInt(el.getAttribute('data-col-index') ?? '', 10) - colOff;
695
+ const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
696
+ if (inRange) {
697
+ if (!el.hasAttribute('data-drag-range'))
698
+ el.setAttribute('data-drag-range', '');
699
+ }
700
+ else {
701
+ if (el.hasAttribute('data-drag-range'))
702
+ el.removeAttribute('data-drag-range');
703
+ }
704
+ }
705
+ }
706
+ clearDragAttrs(wrapper) {
707
+ if (!wrapper)
708
+ return;
709
+ const marked = wrapper.querySelectorAll('[data-drag-range]');
710
+ for (let i = 0; i < marked.length; i++)
711
+ marked[i].removeAttribute('data-drag-range');
712
+ }
713
+ // --- Private helpers ---
714
+ getEffectiveRange(colOffset) {
715
+ const sel = this.selectionRangeSig();
716
+ const ac = this.activeCellSig();
717
+ return sel ?? (ac != null
718
+ ? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
719
+ : null);
720
+ }
721
+ destroy() {
722
+ if (this.rafId) {
723
+ cancelAnimationFrame(this.rafId);
724
+ this.rafId = 0;
725
+ }
726
+ if (this.fillRafId) {
727
+ cancelAnimationFrame(this.fillRafId);
728
+ this.fillRafId = 0;
729
+ }
730
+ if (this.autoScrollInterval) {
731
+ clearInterval(this.autoScrollInterval);
732
+ this.autoScrollInterval = null;
733
+ }
734
+ if (this.fillMoveHandler) {
735
+ window.removeEventListener('pointermove', this.fillMoveHandler, true);
736
+ this.fillMoveHandler = null;
737
+ }
738
+ if (this.fillUpHandler) {
739
+ window.removeEventListener('pointerup', this.fillUpHandler, true);
740
+ this.fillUpHandler = null;
741
+ }
742
+ this.undoRedoStack.clear();
743
+ }
744
+ }