@alaarab/ogrid-angular 2.0.23 → 2.1.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 (33) hide show
  1. package/dist/esm/components/base-column-header-filter.component.js +26 -11
  2. package/dist/esm/components/base-datagrid-table.component.js +64 -46
  3. package/dist/esm/components/base-ogrid.component.js +36 -0
  4. package/dist/esm/components/base-popover-cell-editor.component.js +6 -7
  5. package/dist/esm/components/empty-state.component.js +2 -2
  6. package/dist/esm/components/grid-context-menu.component.js +17 -17
  7. package/dist/esm/components/ogrid-layout.component.js +2 -49
  8. package/dist/esm/components/sidebar.component.js +2 -6
  9. package/dist/esm/components/status-bar.component.js +6 -2
  10. package/dist/esm/index.js +5 -1
  11. package/dist/esm/services/datagrid-editing.service.js +52 -0
  12. package/dist/esm/services/datagrid-interaction.service.js +667 -0
  13. package/dist/esm/services/datagrid-layout.service.js +151 -0
  14. package/dist/esm/services/datagrid-state.service.js +130 -865
  15. package/dist/esm/services/ogrid.service.js +61 -26
  16. package/dist/esm/utils/index.js +1 -1
  17. package/dist/esm/utils/latestRef.js +0 -5
  18. package/dist/types/components/base-column-header-filter.component.d.ts +2 -0
  19. package/dist/types/components/base-datagrid-table.component.d.ts +21 -5
  20. package/dist/types/components/base-ogrid.component.d.ts +10 -0
  21. package/dist/types/components/grid-context-menu.component.d.ts +5 -5
  22. package/dist/types/components/sidebar.component.d.ts +0 -2
  23. package/dist/types/components/status-bar.component.d.ts +4 -1
  24. package/dist/types/index.d.ts +5 -1
  25. package/dist/types/services/datagrid-editing.service.d.ts +31 -0
  26. package/dist/types/services/datagrid-interaction.service.d.ts +86 -0
  27. package/dist/types/services/datagrid-layout.service.d.ts +36 -0
  28. package/dist/types/services/datagrid-state.service.d.ts +20 -39
  29. package/dist/types/services/ogrid.service.d.ts +8 -3
  30. package/dist/types/types/dataGridTypes.d.ts +1 -1
  31. package/dist/types/utils/index.d.ts +1 -1
  32. package/dist/types/utils/latestRef.d.ts +0 -5
  33. package/package.json +10 -3
@@ -5,7 +5,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { Injectable, signal, computed, effect, DestroyRef, inject, NgZone } from '@angular/core';
8
- import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, UndoRedoStack, findCtrlArrowTarget, computeTabNavigation, formatSelectionAsTsv, parseTsvClipboard, rangesEqual, } from '@alaarab/ogrid-core';
8
+ import { getDataGridStatusBarConfig, parseValue, computeAggregations, getCellValue, normalizeSelectionRange, } from '@alaarab/ogrid-core';
9
+ import { DataGridLayoutHelper } from './datagrid-layout.service';
10
+ import { DataGridEditingHelper } from './datagrid-editing.service';
11
+ import { DataGridInteractionHelper } from './datagrid-interaction.service';
12
+ // Stable no-op functions to avoid allocating new closures on every getState() call
13
+ const NOOP = () => { };
14
+ const NOOP_ASYNC = async () => { };
15
+ const NOOP_MOUSE = (_e, _r, _c) => { };
16
+ const NOOP_KEY = (_e) => { };
17
+ const NOOP_CTX = (_e) => { };
9
18
  /**
10
19
  * Single orchestration service for DataGridTable. Takes grid props,
11
20
  * returns all derived state and handlers so Angular UI packages can be thin view layers.
@@ -19,130 +28,52 @@ let DataGridStateService = class DataGridStateService {
19
28
  // --- Input signals ---
20
29
  this.props = signal(null);
21
30
  this.wrapperEl = signal(null);
22
- // --- Internal state ---
23
- this.editingCellSig = signal(null);
24
- this.pendingEditorValueSig = signal(undefined);
25
- this.activeCellSig = signal(null);
26
- this.selectionRangeSig = signal(null);
27
- this.isDraggingSig = signal(false);
28
- this.contextMenuPositionSig = signal(null);
31
+ // --- Internal state (still owned by main service for backward compat) ---
29
32
  this.internalSelectedRows = signal(new Set());
30
- this.popoverAnchorElSig = signal(null);
31
- this.containerWidthSig = signal(0);
32
- this.columnSizingOverridesSig = signal({});
33
- // Clipboard state
34
- this.cutRangeSig = signal(null);
35
- this.copyRangeSig = signal(null);
36
- this.internalClipboard = null;
37
- // Undo/redo state (backed by core UndoRedoStack)
38
- this.undoRedoStack = new UndoRedoStack(100);
39
- this.undoLengthSig = signal(0);
40
- this.redoLengthSig = signal(0);
41
- // Fill handle state
42
- this.fillDragStart = null;
43
- this.fillRafId = 0;
44
- this.fillMoveHandler = null;
45
- this.fillUpHandler = null;
46
33
  // Row selection
47
34
  this.lastClickedRow = -1;
48
- // Drag selection refs
49
- this.dragStartPos = null;
50
- this.dragMoved = false;
51
- this.isDraggingRef = false;
52
- this.liveDragRange = null;
53
- this.rafId = 0;
54
- this.lastMousePos = null;
55
- this.autoScrollInterval = null;
56
- // ResizeObserver
57
- this.resizeObserver = null;
58
35
  // Header menu state (for column pinning UI)
59
36
  this.headerMenuIsOpenSig = signal(false);
60
37
  this.headerMenuOpenForColumnSig = signal(null);
61
38
  this.headerMenuAnchorElementSig = signal(null);
62
39
  // --- Derived computed ---
63
- this.propsResolved = computed(() => this.props());
40
+ this.propsResolved = computed(() => {
41
+ const p = this.props();
42
+ if (!p)
43
+ throw new Error('DataGridStateService: props must be set before use');
44
+ return p;
45
+ });
64
46
  this.cellSelection = computed(() => {
65
47
  const p = this.props();
66
48
  return p ? p.cellSelection !== false : true;
67
49
  });
68
50
  // Narrow signal extractors — prevent full props() dependency in effects/computed
69
51
  this.originalOnCellValueChanged = computed(() => this.props()?.onCellValueChanged);
70
- this.initialColumnWidthsSig = computed(() => this.props()?.initialColumnWidths);
71
52
  // Undo/redo wrapped callback — only recomputes when the actual callback reference changes
72
53
  this.wrappedOnCellValueChanged = computed(() => {
73
54
  const original = this.originalOnCellValueChanged();
74
55
  if (!original)
75
56
  return undefined;
76
57
  return (event) => {
77
- this.undoRedoStack.record(event);
78
- if (!this.undoRedoStack.isBatching) {
79
- this.undoLengthSig.set(this.undoRedoStack.historyLength);
80
- this.redoLengthSig.set(this.undoRedoStack.redoLength);
58
+ this.interactionHelper.undoRedoStack.record(event);
59
+ if (!this.interactionHelper.undoRedoStack.isBatching) {
60
+ this.interactionHelper.undoLengthSig.set(this.interactionHelper.undoRedoStack.historyLength);
61
+ this.interactionHelper.redoLengthSig.set(this.interactionHelper.undoRedoStack.redoLength);
81
62
  }
82
63
  original(event);
83
64
  };
84
65
  });
85
- this.flatColumnsRaw = computed(() => {
86
- const p = this.props();
87
- if (!p)
88
- return [];
89
- return flattenColumns(p.columns);
90
- });
91
- this.flatColumns = computed(() => {
92
- const raw = this.flatColumnsRaw();
93
- const p = this.props();
94
- const pinnedColumns = p?.pinnedColumns;
95
- if (!pinnedColumns || Object.keys(pinnedColumns).length === 0)
96
- return raw;
97
- return raw.map((col) => {
98
- const override = pinnedColumns[col.columnId];
99
- if (override && col.pinned !== override)
100
- return { ...col, pinned: override };
101
- return col;
102
- });
103
- });
104
- this.visibleCols = computed(() => {
105
- const p = this.props();
106
- if (!p)
107
- return [];
108
- const flatCols = this.flatColumns();
109
- const filtered = p.visibleColumns
110
- ? flatCols.filter((c) => p.visibleColumns.has(c.columnId))
111
- : flatCols;
112
- const order = p.columnOrder;
113
- if (!order?.length)
114
- return filtered;
115
- // Build index map for O(1) lookup instead of repeated O(n) indexOf
116
- const orderMap = new Map();
117
- for (let i = 0; i < order.length; i++) {
118
- orderMap.set(order[i], i);
119
- }
120
- return [...filtered].sort((a, b) => {
121
- const ia = orderMap.get(a.columnId) ?? -1;
122
- const ib = orderMap.get(b.columnId) ?? -1;
123
- if (ia === -1 && ib === -1)
124
- return 0;
125
- if (ia === -1)
126
- return 1;
127
- if (ib === -1)
128
- return -1;
129
- return ia - ib;
130
- });
131
- });
132
- this.visibleColumnCount = computed(() => this.visibleCols().length);
133
- this.hasCheckboxCol = computed(() => (this.props()?.rowSelection ?? 'none') === 'multiple');
134
- this.hasRowNumbersCol = computed(() => !!this.props()?.showRowNumbers);
135
- this.specialColsCount = computed(() => (this.hasCheckboxCol() ? 1 : 0) + (this.hasRowNumbersCol() ? 1 : 0));
136
- this.totalColCount = computed(() => this.visibleColumnCount() + this.specialColsCount());
137
- this.colOffset = computed(() => this.specialColsCount());
138
- this.rowIndexByRowId = computed(() => {
139
- const p = this.props();
140
- if (!p)
141
- return new Map();
142
- const m = new Map();
143
- p.items.forEach((item, idx) => m.set(p.getRowId(item), idx));
144
- return m;
145
- });
66
+ // --- Delegated computed signals from layoutHelper ---
67
+ this.flatColumnsRaw = computed(() => this.layoutHelper.flatColumnsRaw());
68
+ this.flatColumns = computed(() => this.layoutHelper.flatColumns());
69
+ this.visibleCols = computed(() => this.layoutHelper.visibleCols());
70
+ this.visibleColumnCount = computed(() => this.layoutHelper.visibleColumnCount());
71
+ this.hasCheckboxCol = computed(() => this.layoutHelper.hasCheckboxCol());
72
+ this.hasRowNumbersCol = computed(() => this.layoutHelper.hasRowNumbersCol());
73
+ this.specialColsCount = computed(() => this.layoutHelper.specialColsCount());
74
+ this.totalColCount = computed(() => this.layoutHelper.totalColCount());
75
+ this.colOffset = computed(() => this.layoutHelper.colOffset());
76
+ this.rowIndexByRowId = computed(() => this.layoutHelper.rowIndexByRowId());
146
77
  this.selectedRowIds = computed(() => {
147
78
  const p = this.props();
148
79
  if (!p)
@@ -167,30 +98,17 @@ let DataGridStateService = class DataGridStateService {
167
98
  const selected = this.selectedRowIds();
168
99
  return !this.allSelected() && p.items.some((item) => selected.has(p.getRowId(item)));
169
100
  });
170
- this.hasCellSelection = computed(() => this.selectionRangeSig() != null || this.activeCellSig() != null);
171
- this.canUndo = computed(() => this.undoLengthSig() > 0);
172
- this.canRedo = computed(() => this.redoLengthSig() > 0);
173
- // Table layout
174
- this.minTableWidth = computed(() => {
175
- const checkboxW = this.hasCheckboxCol() ? CHECKBOX_COLUMN_WIDTH : 0;
176
- return this.visibleCols().reduce((sum, c) => sum + (c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH) + CELL_PADDING, checkboxW);
177
- });
178
- this.desiredTableWidth = computed(() => {
179
- const checkboxW = this.hasCheckboxCol() ? CHECKBOX_COLUMN_WIDTH : 0;
180
- const overrides = this.columnSizingOverridesSig();
181
- return this.visibleCols().reduce((sum, c) => {
182
- const override = overrides[c.columnId];
183
- const w = override
184
- ? override.widthPx
185
- : (c.idealWidth ?? c.defaultWidth ?? c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
186
- return sum + Math.max(c.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, w) + CELL_PADDING;
187
- }, checkboxW);
188
- });
101
+ this.hasCellSelection = computed(() => this.interactionHelper.hasCellSelection());
102
+ this.canUndo = computed(() => this.interactionHelper.canUndo());
103
+ this.canRedo = computed(() => this.interactionHelper.canRedo());
104
+ // Table layout (delegated to layoutHelper)
105
+ this.minTableWidth = computed(() => this.layoutHelper.minTableWidth());
106
+ this.desiredTableWidth = computed(() => this.layoutHelper.desiredTableWidth());
189
107
  this.aggregation = computed(() => {
190
108
  const p = this.props();
191
109
  if (!p)
192
110
  return null;
193
- return computeAggregations(p.items, this.visibleCols(), this.cellSelection() ? this.selectionRangeSig() : null);
111
+ return computeAggregations(p.items, this.visibleCols(), this.cellSelection() ? this.interactionHelper.selectionRangeSig() : null);
194
112
  });
195
113
  this.statusBarConfig = computed(() => {
196
114
  const p = this.props();
@@ -207,6 +125,10 @@ let DataGridStateService = class DataGridStateService {
207
125
  return false;
208
126
  return p.items.length === 0 && !!p.emptyState && !p.isLoading;
209
127
  });
128
+ // --- Instantiate sub-helpers ---
129
+ this.layoutHelper = new DataGridLayoutHelper(this.props, this.wrapperEl, this.ngZone);
130
+ this.interactionHelper = new DataGridInteractionHelper();
131
+ this.editingHelper = new DataGridEditingHelper(() => this.visibleCols(), () => this.props()?.items ?? [], () => this.wrappedOnCellValueChanged(), (cell) => this.setActiveCell(cell));
210
132
  // Setup window event listeners for cell selection drag
211
133
  // Run outside NgZone to avoid 60Hz change detection during drag
212
134
  effect((onCleanup) => {
@@ -221,85 +143,10 @@ let DataGridStateService = class DataGridStateService {
221
143
  window.removeEventListener('mouseup', onUp, true);
222
144
  });
223
145
  });
224
- // Initialize column sizing overrides from initial widths
225
- // Only track initialColumnWidths, not all props
226
- effect(() => {
227
- const widths = this.initialColumnWidthsSig();
228
- if (widths) {
229
- const result = {};
230
- for (const [id, width] of Object.entries(widths)) {
231
- result[id] = { widthPx: width };
232
- }
233
- this.columnSizingOverridesSig.set(result);
234
- }
235
- });
236
- // Container width measurement via ResizeObserver
237
- // Run outside NgZone — signal.set() inside still triggers Angular reactivity
238
- effect(() => {
239
- const el = this.wrapperEl();
240
- if (this.resizeObserver) {
241
- this.resizeObserver.disconnect();
242
- this.resizeObserver = null;
243
- }
244
- if (!el)
245
- return;
246
- const measure = () => {
247
- const rect = el.getBoundingClientRect();
248
- const cs = window.getComputedStyle(el);
249
- const borderX = (parseFloat(cs.borderLeftWidth || '0') || 0) +
250
- (parseFloat(cs.borderRightWidth || '0') || 0);
251
- this.containerWidthSig.set(Math.max(0, rect.width - borderX));
252
- };
253
- this.ngZone.runOutsideAngular(() => {
254
- this.resizeObserver = new ResizeObserver(measure);
255
- this.resizeObserver.observe(el);
256
- });
257
- measure();
258
- });
259
146
  // Cleanup on destroy — cancel pending work and release references
260
147
  this.destroyRef.onDestroy(() => {
261
- if (this.rafId) {
262
- cancelAnimationFrame(this.rafId);
263
- this.rafId = 0;
264
- }
265
- if (this.fillRafId) {
266
- cancelAnimationFrame(this.fillRafId);
267
- this.fillRafId = 0;
268
- }
269
- if (this.autoScrollInterval) {
270
- clearInterval(this.autoScrollInterval);
271
- this.autoScrollInterval = null;
272
- }
273
- if (this.resizeObserver) {
274
- this.resizeObserver.disconnect();
275
- this.resizeObserver = null;
276
- }
277
- // Remove fill-handle window listeners if active
278
- if (this.fillMoveHandler) {
279
- window.removeEventListener('mousemove', this.fillMoveHandler, true);
280
- this.fillMoveHandler = null;
281
- }
282
- if (this.fillUpHandler) {
283
- window.removeEventListener('mouseup', this.fillUpHandler, true);
284
- this.fillUpHandler = null;
285
- }
286
- // Clear undo/redo stack to release closure references
287
- this.undoRedoStack.clear();
288
- });
289
- // Clean up column sizing overrides for removed columns
290
- effect(() => {
291
- const colIds = new Set(this.flatColumns().map((c) => c.columnId));
292
- this.columnSizingOverridesSig.update((prev) => {
293
- const next = { ...prev };
294
- let changed = false;
295
- for (const id of Object.keys(next)) {
296
- if (!colIds.has(id)) {
297
- delete next[id];
298
- changed = true;
299
- }
300
- }
301
- return changed ? next : prev;
302
- });
148
+ this.interactionHelper.destroy();
149
+ this.layoutHelper.destroy();
303
150
  });
304
151
  }
305
152
  // --- Row selection methods ---
@@ -359,556 +206,92 @@ let DataGridStateService = class DataGridStateService {
359
206
  this.updateSelection(new Set());
360
207
  }
361
208
  }
362
- // --- Cell editing ---
209
+ // --- Cell editing (delegated to editingHelper) ---
363
210
  setEditingCell(cell) {
364
- this.editingCellSig.set(cell);
211
+ this.editingHelper.setEditingCell(cell);
365
212
  }
366
213
  setPendingEditorValue(value) {
367
- this.pendingEditorValueSig.set(value);
214
+ this.editingHelper.setPendingEditorValue(value);
368
215
  }
369
216
  setActiveCell(cell) {
370
- const prev = this.activeCellSig();
371
- if (prev === cell)
372
- return;
373
- if (prev && cell && prev.rowIndex === cell.rowIndex && prev.columnIndex === cell.columnIndex)
374
- return;
375
- this.activeCellSig.set(cell);
217
+ this.interactionHelper.setActiveCell(cell);
376
218
  }
377
219
  setSelectionRange(range) {
378
- const prev = this.selectionRangeSig();
379
- if (rangesEqual(prev, range))
380
- return;
381
- this.selectionRangeSig.set(range);
220
+ this.interactionHelper.setSelectionRange(range);
382
221
  }
383
222
  commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
384
- const col = this.visibleCols().find((c) => c.columnId === columnId);
385
- if (col) {
386
- const result = parseValue(newValue, oldValue, item, col);
387
- if (!result.valid) {
388
- this.editingCellSig.set(null);
389
- this.popoverAnchorElSig.set(null);
390
- this.pendingEditorValueSig.set(undefined);
391
- return;
392
- }
393
- newValue = result.value;
394
- }
395
- const onCellValueChanged = this.wrappedOnCellValueChanged();
396
- onCellValueChanged?.({ item, columnId, oldValue, newValue, rowIndex });
397
- this.editingCellSig.set(null);
398
- this.popoverAnchorElSig.set(null);
399
- this.pendingEditorValueSig.set(undefined);
400
- const p = this.props();
401
- if (p && rowIndex < p.items.length - 1) {
402
- this.setActiveCell({ rowIndex: rowIndex + 1, columnIndex: globalColIndex });
403
- }
223
+ this.editingHelper.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
404
224
  }
405
225
  cancelPopoverEdit() {
406
- this.editingCellSig.set(null);
407
- this.popoverAnchorElSig.set(null);
408
- this.pendingEditorValueSig.set(undefined);
226
+ this.editingHelper.cancelPopoverEdit();
409
227
  }
410
- // --- Cell selection / mouse handling ---
228
+ // --- Cell selection / mouse handling (delegated to interactionHelper) ---
411
229
  handleCellMouseDown(e, rowIndex, globalColIndex) {
412
- if (e.button !== 0)
413
- return;
414
- const wrapper = this.wrapperEl();
415
- wrapper?.focus({ preventScroll: true });
416
- this.clearClipboardRanges();
417
- const colOffset = this.colOffset();
418
- if (globalColIndex < colOffset)
419
- return;
420
- e.preventDefault();
421
- const dataColIndex = globalColIndex - colOffset;
422
- const currentRange = this.selectionRangeSig();
423
- if (e.shiftKey && currentRange != null) {
424
- this.setSelectionRange(normalizeSelectionRange({
425
- startRow: currentRange.startRow,
426
- startCol: currentRange.startCol,
427
- endRow: rowIndex,
428
- endCol: dataColIndex,
429
- }));
430
- this.setActiveCell({ rowIndex, columnIndex: globalColIndex });
431
- }
432
- else {
433
- this.dragStartPos = { row: rowIndex, col: dataColIndex };
434
- this.dragMoved = false;
435
- const initial = {
436
- startRow: rowIndex, startCol: dataColIndex,
437
- endRow: rowIndex, endCol: dataColIndex,
438
- };
439
- this.setSelectionRange(initial);
440
- this.liveDragRange = initial;
441
- this.setActiveCell({ rowIndex, columnIndex: globalColIndex });
442
- this.isDraggingRef = true;
443
- }
230
+ this.interactionHelper.handleCellMouseDown(e, rowIndex, globalColIndex, this.colOffset(), this.wrapperEl());
444
231
  }
445
232
  handleSelectAllCells() {
446
233
  const p = this.props();
447
234
  if (!p)
448
235
  return;
449
- const rowCount = p.items.length;
450
- const visibleColCount = this.visibleColumnCount();
451
- if (rowCount === 0 || visibleColCount === 0)
452
- return;
453
- this.setSelectionRange({
454
- startRow: 0, startCol: 0,
455
- endRow: rowCount - 1, endCol: visibleColCount - 1,
456
- });
457
- this.setActiveCell({ rowIndex: 0, columnIndex: this.colOffset() });
236
+ this.interactionHelper.handleSelectAllCells(p.items.length, this.visibleColumnCount(), this.colOffset());
458
237
  }
459
- // --- Context menu ---
238
+ // --- Context menu (delegated to interactionHelper) ---
460
239
  setContextMenuPosition(pos) {
461
- this.contextMenuPositionSig.set(pos);
240
+ this.interactionHelper.setContextMenuPosition(pos);
462
241
  }
463
242
  handleCellContextMenu(e) {
464
- e.preventDefault?.();
465
- this.contextMenuPositionSig.set({ x: e.clientX, y: e.clientY });
243
+ this.interactionHelper.handleCellContextMenu(e);
466
244
  }
467
245
  closeContextMenu() {
468
- this.contextMenuPositionSig.set(null);
246
+ this.interactionHelper.closeContextMenu();
469
247
  }
470
- // --- Clipboard ---
248
+ // --- Clipboard (delegated to interactionHelper) ---
471
249
  handleCopy() {
472
250
  const p = this.props();
473
251
  if (!p)
474
252
  return;
475
- const range = this.getEffectiveRange();
476
- if (range == null)
477
- return;
478
- const norm = normalizeSelectionRange(range);
479
- const tsv = formatSelectionAsTsv(p.items, this.visibleCols(), norm);
480
- this.internalClipboard = tsv;
481
- this.copyRangeSig.set(norm);
482
- void navigator.clipboard.writeText(tsv).catch(() => { });
253
+ this.interactionHelper.handleCopy(p.items, this.visibleCols(), this.colOffset());
483
254
  }
484
255
  handleCut() {
485
256
  const p = this.props();
486
- if (!p || this.props()?.editable === false)
487
- return;
488
- const range = this.getEffectiveRange();
489
- if (range == null || !this.wrappedOnCellValueChanged())
257
+ if (!p)
490
258
  return;
491
- const norm = normalizeSelectionRange(range);
492
- this.cutRangeSig.set(norm);
493
- this.copyRangeSig.set(null);
494
- this.handleCopy();
495
- this.copyRangeSig.set(null);
259
+ this.interactionHelper.handleCut(p.items, this.visibleCols(), this.colOffset(), p.editable, this.wrappedOnCellValueChanged());
496
260
  }
497
261
  async handlePaste() {
498
262
  const p = this.props();
499
- if (!p || p.editable === false)
500
- return;
501
- const onCellValueChanged = this.wrappedOnCellValueChanged();
502
- if (!onCellValueChanged)
503
- return;
504
- let text;
505
- try {
506
- text = await navigator.clipboard.readText();
507
- }
508
- catch {
509
- text = '';
510
- }
511
- if (!text.trim() && this.internalClipboard != null) {
512
- text = this.internalClipboard;
513
- }
514
- if (!text.trim())
263
+ if (!p)
515
264
  return;
516
- const norm = this.getEffectiveRange();
517
- const anchorRow = norm ? norm.startRow : 0;
518
- const anchorCol = norm ? norm.startCol : 0;
519
- const visibleCols = this.visibleCols();
520
- const parsedRows = parseTsvClipboard(text);
521
- this.beginBatch();
522
- for (let r = 0; r < parsedRows.length; r++) {
523
- const cells = parsedRows[r];
524
- for (let c = 0; c < cells.length; c++) {
525
- const targetRow = anchorRow + r;
526
- const targetCol = anchorCol + c;
527
- if (targetRow >= p.items.length || targetCol >= visibleCols.length)
528
- continue;
529
- const item = p.items[targetRow];
530
- const col = visibleCols[targetCol];
531
- const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
532
- if (!colEditable)
533
- continue;
534
- const rawValue = cells[c] ?? '';
535
- const oldValue = getCellValue(item, col);
536
- const result = parseValue(rawValue, oldValue, item, col);
537
- if (!result.valid)
538
- continue;
539
- onCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: targetRow });
540
- }
541
- }
542
- const cutRange = this.cutRangeSig();
543
- if (cutRange) {
544
- for (let r = cutRange.startRow; r <= cutRange.endRow; r++) {
545
- for (let c = cutRange.startCol; c <= cutRange.endCol; c++) {
546
- if (r >= p.items.length || c >= visibleCols.length)
547
- continue;
548
- const item = p.items[r];
549
- const col = visibleCols[c];
550
- const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
551
- if (!colEditable)
552
- continue;
553
- const oldValue = getCellValue(item, col);
554
- const result = parseValue('', oldValue, item, col);
555
- if (!result.valid)
556
- continue;
557
- onCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: r });
558
- }
559
- }
560
- this.cutRangeSig.set(null);
561
- }
562
- this.endBatch();
563
- this.copyRangeSig.set(null);
265
+ await this.interactionHelper.handlePaste(p.items, this.visibleCols(), this.colOffset(), p.editable, this.wrappedOnCellValueChanged());
564
266
  }
565
267
  clearClipboardRanges() {
566
- this.copyRangeSig.set(null);
567
- this.cutRangeSig.set(null);
268
+ this.interactionHelper.clearClipboardRanges();
568
269
  }
569
- // --- Undo/Redo ---
270
+ // --- Undo/Redo (delegated to interactionHelper) ---
570
271
  beginBatch() {
571
- this.undoRedoStack.beginBatch();
272
+ this.interactionHelper.beginBatch();
572
273
  }
573
274
  endBatch() {
574
- this.undoRedoStack.endBatch();
575
- this.undoLengthSig.set(this.undoRedoStack.historyLength);
576
- this.redoLengthSig.set(this.undoRedoStack.redoLength);
275
+ this.interactionHelper.endBatch();
577
276
  }
578
277
  undo() {
579
278
  const p = this.props();
580
- const original = p?.onCellValueChanged;
581
- if (!original)
582
- return;
583
- const lastBatch = this.undoRedoStack.undo();
584
- if (!lastBatch)
585
- return;
586
- this.undoLengthSig.set(this.undoRedoStack.historyLength);
587
- this.redoLengthSig.set(this.undoRedoStack.redoLength);
588
- for (let i = lastBatch.length - 1; i >= 0; i--) {
589
- const ev = lastBatch[i];
590
- original({ ...ev, oldValue: ev.newValue, newValue: ev.oldValue });
591
- }
279
+ this.interactionHelper.undo(p?.onCellValueChanged);
592
280
  }
593
281
  redo() {
594
282
  const p = this.props();
595
- const original = p?.onCellValueChanged;
596
- if (!original)
597
- return;
598
- const nextBatch = this.undoRedoStack.redo();
599
- if (!nextBatch)
600
- return;
601
- this.undoLengthSig.set(this.undoRedoStack.historyLength);
602
- this.redoLengthSig.set(this.undoRedoStack.redoLength);
603
- for (const ev of nextBatch) {
604
- original(ev);
605
- }
283
+ this.interactionHelper.redo(p?.onCellValueChanged);
606
284
  }
607
- // --- Keyboard navigation ---
285
+ // --- Keyboard navigation (delegated to interactionHelper) ---
608
286
  handleGridKeyDown(e) {
609
287
  const p = this.props();
610
288
  if (!p)
611
289
  return;
612
- const { items, getRowId } = p;
613
- const visibleCols = this.visibleCols();
614
- const colOffset = this.colOffset();
615
- const hasCheckboxCol = this.hasCheckboxCol();
616
- const visibleColumnCount = this.visibleColumnCount();
617
- const activeCell = this.activeCellSig();
618
- const selectionRange = this.selectionRangeSig();
619
- const editingCell = this.editingCellSig();
620
- const selectedRowIds = this.selectedRowIds();
621
- const editable = p.editable;
622
- const onCellValueChanged = this.wrappedOnCellValueChanged();
623
- const rowSelection = p.rowSelection ?? 'none';
624
- const maxRowIndex = items.length - 1;
625
- const maxColIndex = visibleColumnCount - 1 + colOffset;
626
- if (items.length === 0)
627
- return;
628
- if (activeCell === null) {
629
- if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'].includes(e.key)) {
630
- this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
631
- e.preventDefault();
632
- }
633
- return;
634
- }
635
- const { rowIndex, columnIndex } = activeCell;
636
- const dataColIndex = columnIndex - colOffset;
637
- const shift = e.shiftKey;
638
- const ctrl = e.ctrlKey || e.metaKey;
639
- const isEmptyAt = (r, c) => {
640
- if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
641
- return true;
642
- const v = getCellValue(items[r], visibleCols[c]);
643
- return v == null || v === '';
644
- };
645
- const findCtrlTarget = findCtrlArrowTarget;
646
- switch (e.key) {
647
- case 'c':
648
- if (ctrl) {
649
- if (editingCell != null)
650
- break;
651
- e.preventDefault();
652
- this.handleCopy();
653
- }
654
- break;
655
- case 'x':
656
- if (ctrl) {
657
- if (editingCell != null)
658
- break;
659
- e.preventDefault();
660
- this.handleCut();
661
- }
662
- break;
663
- case 'v':
664
- if (ctrl) {
665
- if (editingCell != null)
666
- break;
667
- e.preventDefault();
668
- void this.handlePaste();
669
- }
670
- break;
671
- case 'ArrowDown': {
672
- e.preventDefault();
673
- const newRow = ctrl
674
- ? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
675
- : Math.min(rowIndex + 1, maxRowIndex);
676
- if (shift) {
677
- this.setSelectionRange(normalizeSelectionRange({
678
- startRow: selectionRange?.startRow ?? rowIndex,
679
- startCol: selectionRange?.startCol ?? dataColIndex,
680
- endRow: newRow,
681
- endCol: selectionRange?.endCol ?? dataColIndex,
682
- }));
683
- }
684
- else {
685
- this.setSelectionRange({ startRow: newRow, startCol: dataColIndex, endRow: newRow, endCol: dataColIndex });
686
- }
687
- this.setActiveCell({ rowIndex: newRow, columnIndex });
688
- break;
689
- }
690
- case 'ArrowUp': {
691
- e.preventDefault();
692
- const newRowUp = ctrl
693
- ? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
694
- : Math.max(rowIndex - 1, 0);
695
- if (shift) {
696
- this.setSelectionRange(normalizeSelectionRange({
697
- startRow: selectionRange?.startRow ?? rowIndex,
698
- startCol: selectionRange?.startCol ?? dataColIndex,
699
- endRow: newRowUp,
700
- endCol: selectionRange?.endCol ?? dataColIndex,
701
- }));
702
- }
703
- else {
704
- this.setSelectionRange({ startRow: newRowUp, startCol: dataColIndex, endRow: newRowUp, endCol: dataColIndex });
705
- }
706
- this.setActiveCell({ rowIndex: newRowUp, columnIndex });
707
- break;
708
- }
709
- case 'ArrowRight': {
710
- e.preventDefault();
711
- let newCol;
712
- if (ctrl && dataColIndex >= 0) {
713
- newCol = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
714
- }
715
- else {
716
- newCol = Math.min(columnIndex + 1, maxColIndex);
717
- }
718
- const newDataCol = newCol - colOffset;
719
- if (shift) {
720
- this.setSelectionRange(normalizeSelectionRange({
721
- startRow: selectionRange?.startRow ?? rowIndex,
722
- startCol: selectionRange?.startCol ?? dataColIndex,
723
- endRow: selectionRange?.endRow ?? rowIndex,
724
- endCol: newDataCol,
725
- }));
726
- }
727
- else {
728
- this.setSelectionRange({ startRow: rowIndex, startCol: newDataCol, endRow: rowIndex, endCol: newDataCol });
729
- }
730
- this.setActiveCell({ rowIndex, columnIndex: newCol });
731
- break;
732
- }
733
- case 'ArrowLeft': {
734
- e.preventDefault();
735
- let newColLeft;
736
- if (ctrl && dataColIndex >= 0) {
737
- newColLeft = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
738
- }
739
- else {
740
- newColLeft = Math.max(columnIndex - 1, colOffset);
741
- }
742
- const newDataColLeft = newColLeft - colOffset;
743
- if (shift) {
744
- this.setSelectionRange(normalizeSelectionRange({
745
- startRow: selectionRange?.startRow ?? rowIndex,
746
- startCol: selectionRange?.startCol ?? dataColIndex,
747
- endRow: selectionRange?.endRow ?? rowIndex,
748
- endCol: newDataColLeft,
749
- }));
750
- }
751
- else {
752
- this.setSelectionRange({ startRow: rowIndex, startCol: newDataColLeft, endRow: rowIndex, endCol: newDataColLeft });
753
- }
754
- this.setActiveCell({ rowIndex, columnIndex: newColLeft });
755
- break;
756
- }
757
- case 'Tab': {
758
- e.preventDefault();
759
- const tabResult = computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, e.shiftKey);
760
- const newDataColTab = tabResult.columnIndex - colOffset;
761
- this.setSelectionRange({ startRow: tabResult.rowIndex, startCol: newDataColTab, endRow: tabResult.rowIndex, endCol: newDataColTab });
762
- this.setActiveCell({ rowIndex: tabResult.rowIndex, columnIndex: tabResult.columnIndex });
763
- break;
764
- }
765
- case 'Home': {
766
- e.preventDefault();
767
- const newRowHome = ctrl ? 0 : rowIndex;
768
- this.setSelectionRange({ startRow: newRowHome, startCol: 0, endRow: newRowHome, endCol: 0 });
769
- this.setActiveCell({ rowIndex: newRowHome, columnIndex: colOffset });
770
- break;
771
- }
772
- case 'End': {
773
- e.preventDefault();
774
- const newRowEnd = ctrl ? maxRowIndex : rowIndex;
775
- this.setSelectionRange({ startRow: newRowEnd, startCol: visibleColumnCount - 1, endRow: newRowEnd, endCol: visibleColumnCount - 1 });
776
- this.setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
777
- break;
778
- }
779
- case 'Enter':
780
- case 'F2': {
781
- e.preventDefault();
782
- if (dataColIndex >= 0 && dataColIndex < visibleCols.length) {
783
- const col = visibleCols[dataColIndex];
784
- const item = items[rowIndex];
785
- if (item && col) {
786
- const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
787
- if (editable !== false && colEditable && onCellValueChanged != null) {
788
- this.setEditingCell({ rowId: getRowId(item), columnId: col.columnId });
789
- }
790
- }
791
- }
792
- break;
793
- }
794
- case 'Escape':
795
- e.preventDefault();
796
- if (editingCell != null) {
797
- this.setEditingCell(null);
798
- }
799
- else {
800
- this.clearClipboardRanges();
801
- this.setActiveCell(null);
802
- this.setSelectionRange(null);
803
- }
804
- break;
805
- case ' ':
806
- if (rowSelection !== 'none' && columnIndex === 0 && hasCheckboxCol) {
807
- e.preventDefault();
808
- const item = items[rowIndex];
809
- if (item) {
810
- const id = getRowId(item);
811
- const isSelected = selectedRowIds.has(id);
812
- this.handleRowCheckboxChange(id, !isSelected, rowIndex, e.shiftKey);
813
- }
814
- }
815
- break;
816
- case 'z':
817
- if (ctrl) {
818
- if (editingCell == null) {
819
- if (e.shiftKey) {
820
- e.preventDefault();
821
- this.redo();
822
- }
823
- else {
824
- e.preventDefault();
825
- this.undo();
826
- }
827
- }
828
- }
829
- break;
830
- case 'y':
831
- if (ctrl && editingCell == null) {
832
- e.preventDefault();
833
- this.redo();
834
- }
835
- break;
836
- case 'a':
837
- if (ctrl) {
838
- if (editingCell != null)
839
- break;
840
- e.preventDefault();
841
- if (items.length > 0 && visibleColumnCount > 0) {
842
- this.setSelectionRange({ startRow: 0, startCol: 0, endRow: items.length - 1, endCol: visibleColumnCount - 1 });
843
- this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
844
- }
845
- }
846
- break;
847
- case 'Delete':
848
- case 'Backspace': {
849
- if (editingCell != null)
850
- break;
851
- if (editable === false)
852
- break;
853
- if (onCellValueChanged == null)
854
- break;
855
- const range = selectionRange ?? (activeCell != null
856
- ? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset }
857
- : null);
858
- if (range == null)
859
- break;
860
- e.preventDefault();
861
- const norm = normalizeSelectionRange(range);
862
- for (let r = norm.startRow; r <= norm.endRow; r++) {
863
- for (let c = norm.startCol; c <= norm.endCol; c++) {
864
- if (r >= items.length || c >= visibleCols.length)
865
- continue;
866
- const item = items[r];
867
- const col = visibleCols[c];
868
- const colEditable = col.editable === true || (typeof col.editable === 'function' && col.editable(item));
869
- if (!colEditable)
870
- continue;
871
- const oldValue = getCellValue(item, col);
872
- const result = parseValue('', oldValue, item, col);
873
- if (!result.valid)
874
- continue;
875
- onCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: r });
876
- }
877
- }
878
- break;
879
- }
880
- case 'F10':
881
- if (e.shiftKey) {
882
- e.preventDefault();
883
- const wrapper = this.wrapperEl();
884
- if (activeCell != null && wrapper) {
885
- const sel = `[data-row-index="${activeCell.rowIndex}"][data-col-index="${activeCell.columnIndex}"]`;
886
- const cell = wrapper.querySelector(sel);
887
- if (cell) {
888
- const rect = cell.getBoundingClientRect();
889
- this.setContextMenuPosition({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
890
- }
891
- else {
892
- this.setContextMenuPosition({ x: 100, y: 100 });
893
- }
894
- }
895
- else {
896
- this.setContextMenuPosition({ x: 100, y: 100 });
897
- }
898
- }
899
- break;
900
- default:
901
- break;
902
- }
290
+ this.interactionHelper.handleGridKeyDown(e, p.items, p.getRowId, this.visibleCols(), this.colOffset(), this.hasCheckboxCol(), this.visibleColumnCount(), p.editable, this.wrappedOnCellValueChanged(), p.onCellValueChanged, p.rowSelection ?? 'none', this.selectedRowIds(), this.wrapperEl(), (rowId, checked, rowIndex, shiftKey) => this.handleRowCheckboxChange(rowId, checked, rowIndex, shiftKey), this.editingHelper.editingCellSig(), (cell) => this.setEditingCell(cell));
903
291
  }
904
- // --- Fill handle ---
292
+ // --- Fill handle (delegated to interactionHelper + setupFillHandleDrag) ---
905
293
  handleFillHandleMouseDown(e) {
906
- e.preventDefault();
907
- e.stopPropagation();
908
- const range = this.selectionRangeSig();
909
- if (!range)
910
- return;
911
- this.fillDragStart = { startRow: range.startRow, startCol: range.startCol };
294
+ this.interactionHelper.handleFillHandleMouseDown(e);
912
295
  this.setupFillHandleDrag();
913
296
  }
914
297
  // --- Column pinning ---
@@ -977,11 +360,11 @@ let DataGridStateService = class DataGridStateService {
977
360
  hasCheckboxCol: this.hasCheckboxCol(),
978
361
  hasRowNumbersCol: this.hasRowNumbersCol(),
979
362
  rowIndexByRowId: this.rowIndexByRowId(),
980
- containerWidth: this.containerWidthSig(),
363
+ containerWidth: this.layoutHelper.containerWidthSig(),
981
364
  minTableWidth: this.minTableWidth(),
982
365
  desiredTableWidth: this.desiredTableWidth(),
983
- columnSizingOverrides: this.columnSizingOverridesSig(),
984
- setColumnSizingOverrides: (overrides) => this.columnSizingOverridesSig.set(overrides),
366
+ columnSizingOverrides: this.layoutHelper.columnSizingOverridesSig(),
367
+ setColumnSizingOverrides: (overrides) => this.layoutHelper.columnSizingOverridesSig.set(overrides),
985
368
  onColumnResized: p?.onColumnResized,
986
369
  onAutosizeColumn: p?.onAutosizeColumn,
987
370
  };
@@ -994,47 +377,42 @@ let DataGridStateService = class DataGridStateService {
994
377
  someSelected: this.someSelected(),
995
378
  };
996
379
  const editing = {
997
- editingCell: this.editingCellSig(),
380
+ editingCell: this.editingHelper.editingCellSig(),
998
381
  setEditingCell: (cell) => this.setEditingCell(cell),
999
- pendingEditorValue: this.pendingEditorValueSig(),
382
+ pendingEditorValue: this.editingHelper.pendingEditorValueSig(),
1000
383
  setPendingEditorValue: (v) => this.setPendingEditorValue(v),
1001
384
  commitCellEdit: (item, colId, oldVal, newVal, rowIdx, globalColIdx) => this.commitCellEdit(item, colId, oldVal, newVal, rowIdx, globalColIdx),
1002
385
  cancelPopoverEdit: () => this.cancelPopoverEdit(),
1003
- popoverAnchorEl: this.popoverAnchorElSig(),
1004
- setPopoverAnchorEl: (el) => this.popoverAnchorElSig.set(el),
386
+ popoverAnchorEl: this.editingHelper.popoverAnchorElSig(),
387
+ setPopoverAnchorEl: (el) => this.editingHelper.popoverAnchorElSig.set(el),
1005
388
  };
1006
- const noop = () => { };
1007
- const noopAsync = async () => { };
1008
- const noopMouse = (_e, _r, _c) => { };
1009
- const noopKey = (_e) => { };
1010
- const noopCtx = (_e) => { };
1011
389
  const interaction = {
1012
- activeCell: cellSel ? this.activeCellSig() : null,
1013
- setActiveCell: cellSel ? (cell) => this.setActiveCell(cell) : noop,
1014
- selectionRange: cellSel ? this.selectionRangeSig() : null,
1015
- setSelectionRange: cellSel ? (range) => this.setSelectionRange(range) : noop,
1016
- handleCellMouseDown: cellSel ? (e, r, c) => this.handleCellMouseDown(e, r, c) : noopMouse,
1017
- handleSelectAllCells: cellSel ? () => this.handleSelectAllCells() : noop,
390
+ activeCell: cellSel ? this.interactionHelper.activeCellSig() : null,
391
+ setActiveCell: cellSel ? (cell) => this.setActiveCell(cell) : undefined,
392
+ selectionRange: cellSel ? this.interactionHelper.selectionRangeSig() : null,
393
+ setSelectionRange: cellSel ? (range) => this.setSelectionRange(range) : undefined,
394
+ handleCellMouseDown: cellSel ? (e, r, c) => this.handleCellMouseDown(e, r, c) : NOOP_MOUSE,
395
+ handleSelectAllCells: cellSel ? () => this.handleSelectAllCells() : NOOP,
1018
396
  hasCellSelection: cellSel ? this.hasCellSelection() : false,
1019
- handleGridKeyDown: cellSel ? (e) => this.handleGridKeyDown(e) : noopKey,
1020
- handleFillHandleMouseDown: cellSel ? (e) => this.handleFillHandleMouseDown(e) : noop,
1021
- handleCopy: cellSel ? () => this.handleCopy() : noop,
1022
- handleCut: cellSel ? () => this.handleCut() : noop,
1023
- handlePaste: cellSel ? () => this.handlePaste() : noopAsync,
1024
- cutRange: cellSel ? this.cutRangeSig() : null,
1025
- copyRange: cellSel ? this.copyRangeSig() : null,
1026
- clearClipboardRanges: cellSel ? () => this.clearClipboardRanges() : noop,
397
+ handleGridKeyDown: cellSel ? (e) => this.handleGridKeyDown(e) : NOOP_KEY,
398
+ handleFillHandleMouseDown: cellSel ? (e) => this.handleFillHandleMouseDown(e) : undefined,
399
+ handleCopy: cellSel ? () => this.handleCopy() : NOOP,
400
+ handleCut: cellSel ? () => this.handleCut() : NOOP,
401
+ handlePaste: cellSel ? () => this.handlePaste() : NOOP_ASYNC,
402
+ cutRange: cellSel ? this.interactionHelper.cutRangeSig() : null,
403
+ copyRange: cellSel ? this.interactionHelper.copyRangeSig() : null,
404
+ clearClipboardRanges: cellSel ? () => this.clearClipboardRanges() : NOOP,
1027
405
  canUndo: this.canUndo(),
1028
406
  canRedo: this.canRedo(),
1029
407
  onUndo: () => this.undo(),
1030
408
  onRedo: () => this.redo(),
1031
- isDragging: cellSel ? this.isDraggingSig() : false,
409
+ isDragging: cellSel ? this.interactionHelper.isDraggingSig() : false,
1032
410
  };
1033
411
  const contextMenu = {
1034
- menuPosition: cellSel ? this.contextMenuPositionSig() : null,
1035
- setMenuPosition: cellSel ? (pos) => this.setContextMenuPosition(pos) : noop,
1036
- handleCellContextMenu: cellSel ? (e) => this.handleCellContextMenu(e) : noopCtx,
1037
- closeContextMenu: cellSel ? () => this.closeContextMenu() : noop,
412
+ menuPosition: cellSel ? this.interactionHelper.contextMenuPositionSig() : null,
413
+ setMenuPosition: cellSel ? (pos) => this.setContextMenuPosition(pos) : undefined,
414
+ handleCellContextMenu: cellSel ? (e) => this.handleCellContextMenu(e) : NOOP_CTX,
415
+ closeContextMenu: cellSel ? () => this.closeContextMenu() : NOOP,
1038
416
  };
1039
417
  const viewModels = {
1040
418
  headerFilterInput: {
@@ -1048,17 +426,17 @@ let DataGridStateService = class DataGridStateService {
1048
426
  peopleSearch: p?.peopleSearch,
1049
427
  },
1050
428
  cellDescriptorInput: {
1051
- editingCell: this.editingCellSig(),
1052
- activeCell: cellSel ? this.activeCellSig() : null,
1053
- selectionRange: cellSel ? this.selectionRangeSig() : null,
1054
- cutRange: cellSel ? this.cutRangeSig() : null,
1055
- copyRange: cellSel ? this.copyRangeSig() : null,
429
+ editingCell: this.editingHelper.editingCellSig(),
430
+ activeCell: cellSel ? this.interactionHelper.activeCellSig() : null,
431
+ selectionRange: cellSel ? this.interactionHelper.selectionRangeSig() : null,
432
+ cutRange: cellSel ? this.interactionHelper.cutRangeSig() : null,
433
+ copyRange: cellSel ? this.interactionHelper.copyRangeSig() : null,
1056
434
  colOffset: this.colOffset(),
1057
435
  itemsLength: p?.items.length ?? 0,
1058
436
  getRowId: p?.getRowId ?? ((item) => item['id']),
1059
437
  editable: p?.editable,
1060
438
  onCellValueChanged: this.wrappedOnCellValueChanged(),
1061
- isDragging: cellSel ? this.isDraggingSig() : false,
439
+ isDragging: cellSel ? this.interactionHelper.isDraggingSig() : false,
1062
440
  },
1063
441
  statusBarConfig: this.statusBarConfig(),
1064
442
  showEmptyInGrid: this.showEmptyInGrid(),
@@ -1088,135 +466,20 @@ let DataGridStateService = class DataGridStateService {
1088
466
  };
1089
467
  return { layout, rowSelection, editing, interaction, contextMenu, viewModels, pinning };
1090
468
  }
1091
- // --- Private helpers ---
1092
- getEffectiveRange() {
1093
- const sel = this.selectionRangeSig();
1094
- const ac = this.activeCellSig();
1095
- const colOffset = this.colOffset();
1096
- return sel ?? (ac != null
1097
- ? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
1098
- : null);
1099
- }
469
+ // --- Private helpers (drag selection delegated to interactionHelper) ---
1100
470
  onWindowMouseMove(e) {
1101
- if (!this.isDraggingRef || !this.dragStartPos)
1102
- return;
1103
- if (!this.dragMoved) {
1104
- this.dragMoved = true;
1105
- this.isDraggingSig.set(true);
1106
- }
1107
- this.lastMousePos = { cx: e.clientX, cy: e.clientY };
1108
- if (this.rafId)
1109
- cancelAnimationFrame(this.rafId);
1110
- this.rafId = requestAnimationFrame(() => {
1111
- this.rafId = 0;
1112
- const pos = this.lastMousePos;
1113
- if (!pos)
1114
- return;
1115
- const newRange = this.resolveRangeFromMouse(pos.cx, pos.cy);
1116
- if (!newRange)
1117
- return;
1118
- const prev = this.liveDragRange;
1119
- if (prev && prev.startRow === newRange.startRow && prev.startCol === newRange.startCol &&
1120
- prev.endRow === newRange.endRow && prev.endCol === newRange.endCol)
1121
- return;
1122
- this.liveDragRange = newRange;
1123
- this.applyDragAttrs(newRange);
1124
- });
471
+ this.interactionHelper.onWindowMouseMove(e, this.colOffset(), this.wrapperEl());
1125
472
  }
1126
473
  onWindowMouseUp() {
1127
- if (!this.isDraggingRef)
1128
- return;
1129
- if (this.autoScrollInterval) {
1130
- clearInterval(this.autoScrollInterval);
1131
- this.autoScrollInterval = null;
1132
- }
1133
- if (this.rafId) {
1134
- cancelAnimationFrame(this.rafId);
1135
- this.rafId = 0;
1136
- }
1137
- this.isDraggingRef = false;
1138
- const wasDrag = this.dragMoved;
1139
- if (wasDrag) {
1140
- const pos = this.lastMousePos;
1141
- if (pos) {
1142
- const flushed = this.resolveRangeFromMouse(pos.cx, pos.cy);
1143
- if (flushed)
1144
- this.liveDragRange = flushed;
1145
- }
1146
- const finalRange = this.liveDragRange;
1147
- if (finalRange) {
1148
- this.setSelectionRange(finalRange);
1149
- this.setActiveCell({
1150
- rowIndex: finalRange.endRow,
1151
- columnIndex: finalRange.endCol + this.colOffset(),
1152
- });
1153
- }
1154
- }
1155
- this.clearDragAttrs();
1156
- this.liveDragRange = null;
1157
- this.lastMousePos = null;
1158
- this.dragStartPos = null;
1159
- if (wasDrag)
1160
- this.isDraggingSig.set(false);
1161
- }
1162
- resolveRangeFromMouse(cx, cy) {
1163
- if (!this.dragStartPos)
1164
- return null;
1165
- const target = document.elementFromPoint(cx, cy);
1166
- const cell = target?.closest?.('[data-row-index][data-col-index]');
1167
- if (!cell)
1168
- return null;
1169
- const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
1170
- const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
1171
- const colOff = this.colOffset();
1172
- if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
1173
- return null;
1174
- const dataCol = c - colOff;
1175
- const start = this.dragStartPos;
1176
- return normalizeSelectionRange({
1177
- startRow: start.row, startCol: start.col,
1178
- endRow: r, endCol: dataCol,
1179
- });
1180
- }
1181
- applyDragAttrs(range) {
1182
- const wrapper = this.wrapperEl();
1183
- if (!wrapper)
1184
- return;
1185
- const colOff = this.colOffset();
1186
- const minR = Math.min(range.startRow, range.endRow);
1187
- const maxR = Math.max(range.startRow, range.endRow);
1188
- const minC = Math.min(range.startCol, range.endCol);
1189
- const maxC = Math.max(range.startCol, range.endCol);
1190
- const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
1191
- for (let i = 0; i < cells.length; i++) {
1192
- const el = cells[i];
1193
- const r = parseInt(el.getAttribute('data-row-index'), 10);
1194
- const c = parseInt(el.getAttribute('data-col-index'), 10) - colOff;
1195
- const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
1196
- if (inRange) {
1197
- if (!el.hasAttribute('data-drag-range'))
1198
- el.setAttribute('data-drag-range', '');
1199
- }
1200
- else {
1201
- if (el.hasAttribute('data-drag-range'))
1202
- el.removeAttribute('data-drag-range');
1203
- }
1204
- }
1205
- }
1206
- clearDragAttrs() {
1207
- const wrapper = this.wrapperEl();
1208
- if (!wrapper)
1209
- return;
1210
- const marked = wrapper.querySelectorAll('[data-drag-range]');
1211
- for (let i = 0; i < marked.length; i++)
1212
- marked[i].removeAttribute('data-drag-range');
474
+ this.interactionHelper.onWindowMouseUp(this.colOffset(), this.wrapperEl());
1213
475
  }
1214
476
  setupFillHandleDrag() {
1215
477
  const p = this.props();
1216
- if (!this.fillDragStart || p?.editable === false || !this.wrappedOnCellValueChanged())
478
+ const fillDragStart = this.interactionHelper.fillDragStart;
479
+ if (!fillDragStart || p?.editable === false || !this.wrappedOnCellValueChanged())
1217
480
  return;
1218
481
  const colOff = this.colOffset();
1219
- const fillStart = this.fillDragStart;
482
+ const fillStart = fillDragStart;
1220
483
  let fillDragEnd = { endRow: fillStart.startRow, endCol: fillStart.startCol };
1221
484
  let liveFillRange = null;
1222
485
  let lastFillMousePos = null;
@@ -1238,10 +501,10 @@ let DataGridStateService = class DataGridStateService {
1238
501
  };
1239
502
  const onMove = (e) => {
1240
503
  lastFillMousePos = { cx: e.clientX, cy: e.clientY };
1241
- if (this.fillRafId)
1242
- cancelAnimationFrame(this.fillRafId);
1243
- this.fillRafId = requestAnimationFrame(() => {
1244
- this.fillRafId = 0;
504
+ if (this.interactionHelper.fillRafId)
505
+ cancelAnimationFrame(this.interactionHelper.fillRafId);
506
+ this.interactionHelper.fillRafId = requestAnimationFrame(() => {
507
+ this.interactionHelper.fillRafId = 0;
1245
508
  if (!lastFillMousePos)
1246
509
  return;
1247
510
  const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
@@ -1254,17 +517,17 @@ let DataGridStateService = class DataGridStateService {
1254
517
  return;
1255
518
  liveFillRange = newRange;
1256
519
  fillDragEnd = { endRow: newRange.endRow, endCol: newRange.endCol };
1257
- this.applyDragAttrs(newRange);
520
+ this.interactionHelper.applyDragAttrs(newRange, colOff, this.wrapperEl());
1258
521
  });
1259
522
  };
1260
523
  const onUp = () => {
1261
524
  window.removeEventListener('mousemove', onMove, true);
1262
525
  window.removeEventListener('mouseup', onUp, true);
1263
- this.fillMoveHandler = null;
1264
- this.fillUpHandler = null;
1265
- if (this.fillRafId) {
1266
- cancelAnimationFrame(this.fillRafId);
1267
- this.fillRafId = 0;
526
+ this.interactionHelper.fillMoveHandler = null;
527
+ this.interactionHelper.fillUpHandler = null;
528
+ if (this.interactionHelper.fillRafId) {
529
+ cancelAnimationFrame(this.interactionHelper.fillRafId);
530
+ this.interactionHelper.fillRafId = 0;
1268
531
  }
1269
532
  if (lastFillMousePos) {
1270
533
  const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
@@ -1273,7 +536,7 @@ let DataGridStateService = class DataGridStateService {
1273
536
  fillDragEnd = { endRow: flushed.endRow, endCol: flushed.endCol };
1274
537
  }
1275
538
  }
1276
- this.clearDragAttrs();
539
+ this.interactionHelper.clearDragAttrs(this.wrapperEl());
1277
540
  const norm = normalizeSelectionRange({
1278
541
  startRow: fillStart.startRow, startCol: fillStart.startCol,
1279
542
  endRow: fillDragEnd.endRow, endCol: fillDragEnd.endCol,
@@ -1281,12 +544,14 @@ let DataGridStateService = class DataGridStateService {
1281
544
  this.setSelectionRange(norm);
1282
545
  this.setActiveCell({ rowIndex: fillDragEnd.endRow, columnIndex: fillDragEnd.endCol + colOff });
1283
546
  // Apply fill values
547
+ if (!p)
548
+ return;
1284
549
  const items = p.items;
1285
550
  const visibleCols = this.visibleCols();
1286
551
  const startItem = items[norm.startRow];
1287
552
  const startColDef = visibleCols[norm.startCol];
1288
553
  const onCellValueChanged = this.wrappedOnCellValueChanged();
1289
- if (startItem && startColDef) {
554
+ if (startItem && startColDef && onCellValueChanged) {
1290
555
  const startValue = getCellValue(startItem, startColDef);
1291
556
  this.beginBatch();
1292
557
  for (let row = norm.startRow; row <= norm.endRow; row++) {
@@ -1309,11 +574,11 @@ let DataGridStateService = class DataGridStateService {
1309
574
  }
1310
575
  this.endBatch();
1311
576
  }
1312
- this.fillDragStart = null;
577
+ this.interactionHelper.fillDragStart = null;
1313
578
  };
1314
579
  // Track handlers for cleanup on destroy
1315
- this.fillMoveHandler = onMove;
1316
- this.fillUpHandler = onUp;
580
+ this.interactionHelper.fillMoveHandler = onMove;
581
+ this.interactionHelper.fillUpHandler = onUp;
1317
582
  this.ngZone.runOutsideAngular(() => {
1318
583
  window.addEventListener('mousemove', onMove, true);
1319
584
  window.addEventListener('mouseup', onUp, true);