@alaarab/ogrid-angular 2.1.3 → 2.1.5

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 (32) hide show
  1. package/dist/esm/index.js +4606 -30
  2. package/dist/types/components/base-datagrid-table.component.d.ts +8 -8
  3. package/package.json +4 -4
  4. package/dist/esm/components/base-column-chooser.component.js +0 -78
  5. package/dist/esm/components/base-column-header-filter.component.js +0 -281
  6. package/dist/esm/components/base-datagrid-table.component.js +0 -648
  7. package/dist/esm/components/base-inline-cell-editor.component.js +0 -253
  8. package/dist/esm/components/base-ogrid.component.js +0 -36
  9. package/dist/esm/components/base-pagination-controls.component.js +0 -72
  10. package/dist/esm/components/base-popover-cell-editor.component.js +0 -114
  11. package/dist/esm/components/empty-state.component.js +0 -58
  12. package/dist/esm/components/grid-context-menu.component.js +0 -153
  13. package/dist/esm/components/inline-cell-editor-template.js +0 -107
  14. package/dist/esm/components/marching-ants-overlay.component.js +0 -164
  15. package/dist/esm/components/ogrid-layout.component.js +0 -188
  16. package/dist/esm/components/sidebar.component.js +0 -274
  17. package/dist/esm/components/status-bar.component.js +0 -71
  18. package/dist/esm/services/column-reorder.service.js +0 -180
  19. package/dist/esm/services/datagrid-editing.service.js +0 -52
  20. package/dist/esm/services/datagrid-interaction.service.js +0 -667
  21. package/dist/esm/services/datagrid-layout.service.js +0 -151
  22. package/dist/esm/services/datagrid-state.service.js +0 -591
  23. package/dist/esm/services/ogrid.service.js +0 -746
  24. package/dist/esm/services/virtual-scroll.service.js +0 -91
  25. package/dist/esm/styles/ogrid-theme-vars.js +0 -53
  26. package/dist/esm/types/columnTypes.js +0 -1
  27. package/dist/esm/types/dataGridTypes.js +0 -1
  28. package/dist/esm/types/index.js +0 -1
  29. package/dist/esm/utils/dataGridViewModel.js +0 -6
  30. package/dist/esm/utils/debounce.js +0 -68
  31. package/dist/esm/utils/index.js +0 -8
  32. package/dist/esm/utils/latestRef.js +0 -41
@@ -1,648 +0,0 @@
1
- import { signal, computed, effect, inject } from '@angular/core';
2
- import { DataGridStateService } from '../services/datagrid-state.service';
3
- import { ColumnReorderService } from '../services/column-reorder.service';
4
- import { VirtualScrollService } from '../services/virtual-scroll.service';
5
- import { buildHeaderRows, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, measureColumnContentWidth, } from '@alaarab/ogrid-core';
6
- import { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, } from '../utils';
7
- /**
8
- * Abstract base class containing all shared TypeScript logic for DataGridTable components.
9
- * Framework-specific UI packages extend this with their templates and style overrides.
10
- *
11
- * Subclasses must:
12
- * 1. Provide a @Component decorator with template and styles
13
- * 2. Call `initBase()` in the constructor (effects require injection context)
14
- * 3. Implement abstract accessors for propsInput, wrapperRef, and tableContainerRef
15
- */
16
- export class BaseDataGridTableComponent {
17
- constructor() {
18
- this.stateService = inject(DataGridStateService);
19
- this.columnReorderService = inject(ColumnReorderService);
20
- this.virtualScrollService = inject(VirtualScrollService);
21
- this.lastMouseShift = false;
22
- this.columnSizingVersion = signal(0);
23
- /** Dirty flag — set when column layout changes, cleared after measurement. */
24
- this.measureDirty = signal(true);
25
- /** DOM-measured column widths from the last layout pass.
26
- * Used as a minWidth floor to prevent columns from shrinking
27
- * when new data loads (e.g. server-side pagination). */
28
- this.measuredColumnWidths = signal({});
29
- // Signal-backed view child elements — set from ngAfterViewInit.
30
- // @ViewChild is a plain property (not a signal), so effects/computed that read it
31
- // only evaluate once during construction when the ref is still undefined.
32
- this.wrapperElSignal = signal(null);
33
- this.tableContainerElSignal = signal(null);
34
- // --- Delegated state ---
35
- this.state = computed(() => this.stateService.getState());
36
- // Intermediate computed signals — narrow slices of state() so leaf computeds
37
- // only recompute when their specific sub-state changes.
38
- this.layoutState = computed(() => this.state().layout);
39
- this.rowSelectionState = computed(() => this.state().rowSelection);
40
- this.editingState = computed(() => this.state().editing);
41
- this.interactionState = computed(() => this.state().interaction);
42
- this.contextMenuState = computed(() => this.state().contextMenu);
43
- this.viewModelsState = computed(() => this.state().viewModels);
44
- this.pinningState = computed(() => this.state().pinning);
45
- this.tableContainerEl = computed(() => this.tableContainerElSignal());
46
- this.items = computed(() => this.getProps()?.items ?? []);
47
- this.getRowId = computed(() => this.getProps()?.getRowId ?? ((item) => item['id']));
48
- this.isLoading = computed(() => this.getProps()?.isLoading ?? false);
49
- this.loadingMessage = computed(() => 'Loading\u2026');
50
- this.layoutModeFit = computed(() => (this.getProps()?.layoutMode ?? 'fill') === 'content');
51
- this.rowHeightCssVar = computed(() => {
52
- const rh = this.getProps()?.rowHeight;
53
- return rh ? `${rh}px` : null;
54
- });
55
- this.ariaLabel = computed(() => this.getProps()?.['aria-label'] ?? 'Data grid');
56
- this.ariaLabelledBy = computed(() => this.getProps()?.['aria-labelledby']);
57
- this.stickyHeader = computed(() => this.getProps()?.stickyHeader ?? true);
58
- this.emptyState = computed(() => this.getProps()?.emptyState);
59
- this.currentPage = computed(() => this.getProps()?.currentPage ?? 1);
60
- this.pageSize = computed(() => this.getProps()?.pageSize ?? 25);
61
- this.rowNumberOffset = computed(() => this.hasRowNumbersCol() ? (this.currentPage() - 1) * this.pageSize() : 0);
62
- this.propsVisibleColumns = computed(() => this.getProps()?.visibleColumns);
63
- this.propsColumnOrder = computed(() => this.getProps()?.columnOrder);
64
- // State service outputs — read from narrow intermediate signals
65
- this.visibleCols = computed(() => this.layoutState().visibleCols);
66
- this.hasCheckboxCol = computed(() => this.layoutState().hasCheckboxCol);
67
- this.hasRowNumbersCol = computed(() => this.layoutState().hasRowNumbersCol);
68
- this.colOffset = computed(() => this.layoutState().colOffset);
69
- this.containerWidth = computed(() => this.layoutState().containerWidth);
70
- this.minTableWidth = computed(() => this.layoutState().minTableWidth);
71
- this.desiredTableWidth = computed(() => this.layoutState().desiredTableWidth);
72
- this.columnSizingOverrides = computed(() => this.layoutState().columnSizingOverrides);
73
- this.selectedRowIds = computed(() => this.rowSelectionState().selectedRowIds);
74
- this.allSelected = computed(() => this.rowSelectionState().allSelected);
75
- this.someSelected = computed(() => this.rowSelectionState().someSelected);
76
- this.editingCell = computed(() => this.editingState().editingCell);
77
- this.pendingEditorValue = computed(() => this.editingState().pendingEditorValue);
78
- this.activeCell = computed(() => this.interactionState().activeCell);
79
- this.selectionRange = computed(() => this.interactionState().selectionRange);
80
- this.hasCellSelection = computed(() => this.interactionState().hasCellSelection);
81
- this.cutRange = computed(() => this.interactionState().cutRange);
82
- this.copyRange = computed(() => this.interactionState().copyRange);
83
- this.canUndo = computed(() => this.interactionState().canUndo);
84
- this.canRedo = computed(() => this.interactionState().canRedo);
85
- this.isDragging = computed(() => this.interactionState().isDragging);
86
- this.menuPosition = computed(() => this.contextMenuState().menuPosition);
87
- this.statusBarConfig = computed(() => this.viewModelsState().statusBarConfig);
88
- this.showEmptyInGrid = computed(() => this.viewModelsState().showEmptyInGrid);
89
- this.headerFilterInput = computed(() => this.viewModelsState().headerFilterInput);
90
- this.cellDescriptorInput = computed(() => this.viewModelsState().cellDescriptorInput);
91
- // Pinning state
92
- this.pinnedColumnsMap = computed(() => this.pinningState().pinnedColumns);
93
- // Virtual scrolling
94
- this.vsEnabled = computed(() => this.virtualScrollService.isActive());
95
- this.vsVisibleRange = computed(() => this.virtualScrollService.visibleRange());
96
- this.vsTopSpacerHeight = computed(() => {
97
- if (!this.vsEnabled())
98
- return 0;
99
- return this.vsVisibleRange().offsetTop;
100
- });
101
- this.vsBottomSpacerHeight = computed(() => {
102
- if (!this.vsEnabled())
103
- return 0;
104
- return this.vsVisibleRange().offsetBottom;
105
- });
106
- this.vsVisibleItems = computed(() => {
107
- const items = this.items();
108
- if (!this.vsEnabled())
109
- return items;
110
- const range = this.vsVisibleRange();
111
- return items.slice(range.startIndex, Math.min(range.endIndex + 1, items.length));
112
- });
113
- this.vsStartIndex = computed(() => {
114
- if (!this.vsEnabled())
115
- return 0;
116
- return this.vsVisibleRange().startIndex;
117
- });
118
- // Popover editing
119
- this.popoverAnchorEl = computed(() => this.editingState().popoverAnchorEl);
120
- this.pendingEditorValueForPopover = computed(() => this.editingState().pendingEditorValue);
121
- this.allowOverflowX = computed(() => {
122
- const p = this.getProps();
123
- if (p?.suppressHorizontalScroll)
124
- return false;
125
- const cw = this.containerWidth();
126
- const mtw = this.minTableWidth();
127
- const dtw = this.desiredTableWidth();
128
- return cw > 0 && (mtw > cw || dtw > cw);
129
- });
130
- this.selectionCellCount = computed(() => {
131
- const sr = this.selectionRange();
132
- if (!sr)
133
- return undefined;
134
- return (Math.abs(sr.endRow - sr.startRow) + 1) * (Math.abs(sr.endCol - sr.startCol) + 1);
135
- });
136
- // Header rows from column definition
137
- this.headerRows = computed(() => {
138
- const p = this.getProps();
139
- if (!p)
140
- return [];
141
- return buildHeaderRows(p.columns, p.visibleColumns);
142
- });
143
- // Pre-computed column layouts
144
- this.columnLayouts = computed(() => {
145
- const cols = this.visibleCols();
146
- const props = this.getProps();
147
- const pinnedCols = props?.pinnedColumns ?? {};
148
- const measuredWidths = this.measuredColumnWidths();
149
- const sizingOverrides = this.columnSizingOverrides();
150
- return cols.map((col) => {
151
- const runtimePinned = pinnedCols[col.columnId];
152
- const pinnedLeft = runtimePinned === 'left' || col.pinned === 'left';
153
- const pinnedRight = runtimePinned === 'right' || col.pinned === 'right';
154
- const w = this.getColumnWidth(col);
155
- // Use previously-measured DOM width as a minWidth floor to prevent columns
156
- // from shrinking when new data loads (e.g. server-side pagination).
157
- const hasResizeOverride = !!sizingOverrides[col.columnId];
158
- const measuredW = measuredWidths[col.columnId];
159
- const baseMinWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
160
- const effectiveMinWidth = hasResizeOverride ? w : Math.max(baseMinWidth, measuredW ?? 0);
161
- return {
162
- col,
163
- pinnedLeft,
164
- pinnedRight,
165
- minWidth: effectiveMinWidth,
166
- width: w,
167
- };
168
- });
169
- });
170
- // Compute sticky offsets for pinned columns (single pass from both ends)
171
- this.pinningOffsets = computed(() => {
172
- const layouts = this.columnLayouts();
173
- const leftOffsets = {};
174
- const rightOffsets = {};
175
- let leftAcc = 0;
176
- if (this.hasCheckboxCol())
177
- leftAcc += CHECKBOX_COLUMN_WIDTH;
178
- if (this.hasRowNumbersCol())
179
- leftAcc += ROW_NUMBER_COLUMN_WIDTH;
180
- let rightAcc = 0;
181
- const len = layouts.length;
182
- for (let i = 0; i < len; i++) {
183
- // Left-pinned: walk forward
184
- const leftLayout = layouts[i];
185
- if (leftLayout.pinnedLeft) {
186
- leftOffsets[leftLayout.col.columnId] = leftAcc;
187
- leftAcc += leftLayout.width + CELL_PADDING;
188
- }
189
- // Right-pinned: walk backward
190
- const ri = len - 1 - i;
191
- const rightLayout = layouts[ri];
192
- if (rightLayout.pinnedRight) {
193
- rightOffsets[rightLayout.col.columnId] = rightAcc;
194
- rightAcc += rightLayout.width + CELL_PADDING;
195
- }
196
- }
197
- return { leftOffsets, rightOffsets };
198
- });
199
- /** Memoized column menu handlers — avoids recreating objects on every CD cycle */
200
- this.columnMenuHandlersMap = computed(() => {
201
- const cols = this.visibleCols();
202
- const map = new Map();
203
- for (const col of cols) {
204
- map.set(col.columnId, this.buildColumnMenuHandlers(col.columnId));
205
- }
206
- return map;
207
- });
208
- }
209
- /** Lifecycle hook — populate element signals from @ViewChild refs */
210
- ngAfterViewInit() {
211
- const wrapper = this.getWrapperRef()?.nativeElement ?? null;
212
- const tableContainer = this.getTableContainerRef()?.nativeElement ?? null;
213
- if (wrapper)
214
- this.wrapperElSignal.set(wrapper);
215
- if (tableContainer)
216
- this.tableContainerElSignal.set(tableContainer);
217
- this.measureColumnWidths();
218
- }
219
- /** Lifecycle hook — re-measure column widths only when layout changed */
220
- ngAfterViewChecked() {
221
- if (this.measureDirty()) {
222
- this.measureDirty.set(false);
223
- this.measureColumnWidths();
224
- }
225
- }
226
- /** Measure actual th widths from the DOM and update the measuredColumnWidths signal.
227
- * Only updates the signal when values actually change, to avoid render loops. */
228
- measureColumnWidths() {
229
- const wrapper = this.getWrapperRef()?.nativeElement;
230
- if (!wrapper)
231
- return;
232
- const headerCells = wrapper.querySelectorAll('th[data-column-id]');
233
- if (headerCells.length === 0)
234
- return;
235
- const measured = {};
236
- headerCells.forEach((cell) => {
237
- const colId = cell.getAttribute('data-column-id');
238
- if (colId)
239
- measured[colId] = cell.offsetWidth;
240
- });
241
- // Only update signal if values changed to avoid triggering computed re-evaluations unnecessarily
242
- const prev = this.measuredColumnWidths();
243
- let changed = Object.keys(measured).length !== Object.keys(prev).length;
244
- if (!changed) {
245
- for (const key in measured) {
246
- if (prev[key] !== measured[key]) {
247
- changed = true;
248
- break;
249
- }
250
- }
251
- }
252
- if (changed) {
253
- this.measuredColumnWidths.set(measured);
254
- }
255
- }
256
- /**
257
- * Initialize base wiring effects. Must be called from subclass constructor.
258
- *
259
- * **Timing:** Angular requires `effect()` to be created inside an injection
260
- * context (constructor or field initializer). On the first run, signals like
261
- * `wrapperElSignal()` return `null` because the DOM hasn't been created yet.
262
- * After `ngAfterViewInit` sets these signals, Angular's signal graph
263
- * automatically re-runs each effect. The null guards inside each effect body
264
- * ensure the first (null) run is a safe no-op.
265
- *
266
- * Sequence:
267
- * 1. Constructor → `initBase()` → effects created, first run (signals null → no-ops)
268
- * 2. `ngAfterViewInit` → `wrapperElSignal.set(el)` → effects re-run with real values
269
- */
270
- initBase() {
271
- // Wire props to state service
272
- effect(() => {
273
- const p = this.getProps();
274
- if (p)
275
- this.stateService.props.set(p);
276
- });
277
- // Wire wrapper element (reads from signal populated by ngAfterViewInit)
278
- effect(() => {
279
- const el = this.wrapperElSignal();
280
- if (el) {
281
- this.stateService.wrapperEl.set(el);
282
- this.columnReorderService.wrapperEl.set(el);
283
- }
284
- });
285
- // Wire column reorder service inputs
286
- effect(() => {
287
- const p = this.getProps();
288
- if (p) {
289
- const cols = this.visibleCols();
290
- this.columnReorderService.columns.set(cols);
291
- this.columnReorderService.columnOrder.set(p.columnOrder);
292
- this.columnReorderService.onColumnOrderChange.set(p.onColumnOrderChange);
293
- this.columnReorderService.enabled.set(p.columnReorder === true);
294
- }
295
- });
296
- // Mark measurement dirty when column layout changes
297
- effect(() => {
298
- // Track signals that affect column layout
299
- this.visibleCols();
300
- this.columnSizingOverrides();
301
- this.columnSizingVersion();
302
- this.measureDirty.set(true);
303
- });
304
- // Wire virtual scroll service inputs
305
- effect(() => {
306
- const p = this.getProps();
307
- if (p) {
308
- this.virtualScrollService.totalRows.set(p.items.length);
309
- if (p.virtualScroll) {
310
- this.virtualScrollService.updateConfig({
311
- enabled: p.virtualScroll.enabled,
312
- rowHeight: p.virtualScroll.rowHeight,
313
- overscan: p.virtualScroll.overscan,
314
- });
315
- }
316
- }
317
- });
318
- // Wire wrapper element to virtual scroll for scroll events + container height
319
- effect(() => {
320
- const el = this.wrapperElSignal();
321
- if (el) {
322
- this.virtualScrollService.setContainer(el);
323
- this.virtualScrollService.containerHeight.set(el.clientHeight);
324
- }
325
- });
326
- }
327
- // --- Helper methods ---
328
- /** Lookup effective min-width for a column (includes measured width floor) */
329
- getEffectiveMinWidth(col) {
330
- const layout = this.columnLayouts().find((l) => l.col.columnId === col.columnId);
331
- return layout?.minWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
332
- }
333
- /**
334
- * Returns derived cell interaction metadata (non-event attributes) for use in templates.
335
- * Mirrors React's getCellInteractionProps for the Angular view layer.
336
- * Event handlers (mousedown, click, dblclick, contextmenu) are still bound inline in templates.
337
- */
338
- getCellInteractionProps(descriptor) {
339
- return {
340
- tabIndex: descriptor.isActive ? 0 : -1,
341
- dataRowIndex: descriptor.rowIndex,
342
- dataColIndex: descriptor.globalColIndex,
343
- dataInRange: descriptor.isInRange ? 'true' : null,
344
- role: descriptor.canEditAny ? 'button' : null,
345
- };
346
- }
347
- asColumnDef(colDef) {
348
- return colDef;
349
- }
350
- visibleColIndex(col) {
351
- return this.visibleCols().indexOf(col);
352
- }
353
- getColumnWidth(col) {
354
- const overrides = this.columnSizingOverrides();
355
- const override = overrides[col.columnId];
356
- if (override)
357
- return override.widthPx;
358
- return col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
359
- }
360
- getFilterConfig(col) {
361
- return getHeaderFilterConfig(col, this.headerFilterInput());
362
- }
363
- /** Build column menu handler object for a single column */
364
- buildColumnMenuHandlers(columnId) {
365
- return {
366
- onPinLeft: () => this.onPinColumn(columnId, 'left'),
367
- onPinRight: () => this.onPinColumn(columnId, 'right'),
368
- onUnpin: () => this.onUnpinColumn(columnId),
369
- onSortAsc: () => this.onSortAsc(columnId),
370
- onSortDesc: () => this.onSortDesc(columnId),
371
- onClearSort: () => this.onClearSort(columnId),
372
- onAutosizeThis: () => this.onAutosizeColumn(columnId),
373
- onAutosizeAll: () => this.onAutosizeAllColumns(),
374
- onClose: () => { }
375
- };
376
- }
377
- /** Get memoized handlers for a column */
378
- getColumnMenuHandlersMemoized(columnId) {
379
- return this.columnMenuHandlersMap().get(columnId) ?? this.buildColumnMenuHandlers(columnId);
380
- }
381
- getCellDescriptor(item, col, rowIndex, colIdx) {
382
- return getCellRenderDescriptor(item, col, rowIndex, colIdx, this.cellDescriptorInput());
383
- }
384
- resolveCellContent(col, item, displayValue) {
385
- return resolveCellDisplayContent(col, item, displayValue);
386
- }
387
- resolveCellStyleFn(col, item) {
388
- return resolveCellStyle(col, item);
389
- }
390
- buildPopoverEditorProps(item, col, descriptor) {
391
- return buildPopoverEditorProps(item, col, descriptor, this.pendingEditorValue(), {
392
- setPendingEditorValue: (value) => this.setPendingEditorValue(value),
393
- commitCellEdit: (item, columnId, oldValue, newValue, rowIndex, globalColIndex) => this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex),
394
- cancelPopoverEdit: () => this.cancelPopoverEdit(),
395
- });
396
- }
397
- /** Check if a specific cell is the active cell (PrimeNG inline template helper). */
398
- isActiveCell(rowIndex, colIdx) {
399
- const ac = this.activeCell();
400
- if (!ac)
401
- return false;
402
- return ac.rowIndex === rowIndex && ac.columnIndex === colIdx + this.colOffset();
403
- }
404
- /** Check if a cell is within the current selection range (PrimeNG inline template helper). */
405
- isInSelectionRange(rowIndex, colIdx) {
406
- const range = this.selectionRange();
407
- if (!range)
408
- return false;
409
- const minR = Math.min(range.startRow, range.endRow);
410
- const maxR = Math.max(range.startRow, range.endRow);
411
- const minC = Math.min(range.startCol, range.endCol);
412
- const maxC = Math.max(range.startCol, range.endCol);
413
- return rowIndex >= minR && rowIndex <= maxR && colIdx >= minC && colIdx <= maxC;
414
- }
415
- /** Check if a cell is the selection end cell for fill handle display. */
416
- isSelectionEndCell(rowIndex, colIdx) {
417
- const range = this.selectionRange();
418
- if (!range || this.isDragging() || this.copyRange() || this.cutRange())
419
- return false;
420
- return rowIndex === range.endRow && colIdx === range.endCol;
421
- }
422
- /** Get cell background color based on selection state. */
423
- getCellBackground(rowIndex, colIdx) {
424
- if (this.isInSelectionRange(rowIndex, colIdx))
425
- return 'var(--ogrid-range-bg, rgba(33, 115, 70, 0.08))';
426
- return null;
427
- }
428
- /** Resolve editor type from column definition. */
429
- getEditorType(col, _item) {
430
- if (col.cellEditor === 'text' || col.cellEditor === 'select' || col.cellEditor === 'checkbox' || col.cellEditor === 'date' || col.cellEditor === 'richSelect') {
431
- return col.cellEditor;
432
- }
433
- if (col.type === 'date')
434
- return 'date';
435
- if (col.type === 'boolean')
436
- return 'checkbox';
437
- return 'text';
438
- }
439
- getSelectValues(col) {
440
- const params = col.cellEditorParams;
441
- if (params && typeof params === 'object' && 'values' in params) {
442
- return params.values.map(String);
443
- }
444
- return [];
445
- }
446
- formatDateForInput(value) {
447
- if (!value)
448
- return '';
449
- const d = new Date(String(value));
450
- if (Number.isNaN(d.getTime()))
451
- return '';
452
- return d.toISOString().split('T')[0];
453
- }
454
- getPinnedLeftOffset(columnId) {
455
- const offsets = this.pinningOffsets();
456
- return offsets.leftOffsets[columnId] ?? null;
457
- }
458
- getPinnedRightOffset(columnId) {
459
- const offsets = this.pinningOffsets();
460
- return offsets.rightOffsets[columnId] ?? null;
461
- }
462
- // --- Virtual scroll event handler ---
463
- onWrapperScroll(event) {
464
- this.virtualScrollService.onScroll(event);
465
- }
466
- // --- Popover editor helpers ---
467
- setPopoverAnchorEl(el) {
468
- this.state().editing.setPopoverAnchorEl(el);
469
- }
470
- setPendingEditorValue(value) {
471
- this.state().editing.setPendingEditorValue(value);
472
- }
473
- cancelPopoverEdit() {
474
- this.state().editing.cancelPopoverEdit();
475
- }
476
- commitPopoverEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
477
- this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
478
- }
479
- // --- Event handlers ---
480
- onWrapperMouseDown(event) {
481
- this.lastMouseShift = event.shiftKey;
482
- }
483
- onGridKeyDown(event) {
484
- this.state().interaction.handleGridKeyDown(event);
485
- }
486
- onCellMouseDown(event, rowIndex, globalColIndex) {
487
- this.state().interaction.handleCellMouseDown(event, rowIndex, globalColIndex);
488
- }
489
- onCellClick(rowIndex, globalColIndex) {
490
- this.state().interaction.setActiveCell?.({ rowIndex, columnIndex: globalColIndex });
491
- }
492
- onCellContextMenu(event) {
493
- this.state().contextMenu.handleCellContextMenu(event);
494
- }
495
- onCellDblClick(rowId, columnId) {
496
- this.state().editing.setEditingCell({ rowId, columnId });
497
- }
498
- onFillHandleMouseDown(event) {
499
- this.state().interaction.handleFillHandleMouseDown?.(event);
500
- }
501
- onResizeStart(event, col) {
502
- event.preventDefault();
503
- // Clear cell selection before resize (like React) so selection outlines don't persist during drag
504
- this.state().interaction.setActiveCell?.(null);
505
- this.state().interaction.setSelectionRange?.(null);
506
- this.getWrapperRef()?.nativeElement.focus({ preventScroll: true });
507
- const startX = event.clientX;
508
- const startWidth = this.getColumnWidth(col);
509
- const minWidth = col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
510
- const onMove = (e) => {
511
- const delta = e.clientX - startX;
512
- const newWidth = Math.max(minWidth, startWidth + delta);
513
- const overrides = { ...this.columnSizingOverrides(), [col.columnId]: { widthPx: newWidth } };
514
- this.state().layout.setColumnSizingOverrides(overrides);
515
- this.columnSizingVersion.update(v => v + 1);
516
- };
517
- const onUp = () => {
518
- window.removeEventListener('mousemove', onMove);
519
- window.removeEventListener('mouseup', onUp);
520
- const finalWidth = this.getColumnWidth(col);
521
- this.state().layout.onColumnResized?.(col.columnId, finalWidth);
522
- };
523
- window.addEventListener('mousemove', onMove);
524
- window.addEventListener('mouseup', onUp);
525
- }
526
- onSelectAllChange(event) {
527
- const checked = event.target.checked;
528
- this.state().rowSelection.handleSelectAll(!!checked);
529
- }
530
- onRowClick(event, rowId) {
531
- const p = this.getProps();
532
- if (p?.rowSelection !== 'single')
533
- return;
534
- const ids = this.selectedRowIds();
535
- this.state().rowSelection.updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
536
- }
537
- onRowCheckboxChange(rowId, event, rowIndex) {
538
- const checked = event.target.checked;
539
- this.state().rowSelection.handleRowCheckboxChange(rowId, checked, rowIndex, this.lastMouseShift);
540
- }
541
- commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex) {
542
- this.state().editing.commitCellEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
543
- }
544
- cancelEdit() {
545
- this.state().editing.setEditingCell(null);
546
- }
547
- onEditorKeydown(event, item, columnId, oldValue, rowIndex, globalColIndex) {
548
- if (event.key === 'Enter') {
549
- event.preventDefault();
550
- const newValue = event.target.value;
551
- this.commitEdit(item, columnId, oldValue, newValue, rowIndex, globalColIndex);
552
- }
553
- else if (event.key === 'Escape') {
554
- event.preventDefault();
555
- this.cancelEdit();
556
- }
557
- }
558
- closeContextMenu() {
559
- this.state().contextMenu.closeContextMenu();
560
- }
561
- handleCopy() {
562
- this.state().interaction.handleCopy();
563
- }
564
- handleCut() {
565
- this.state().interaction.handleCut();
566
- }
567
- handlePaste() {
568
- void this.state().interaction.handlePaste();
569
- }
570
- handleSelectAllCells() {
571
- this.state().interaction.handleSelectAllCells();
572
- }
573
- onUndo() {
574
- this.state().interaction.onUndo?.();
575
- }
576
- onRedo() {
577
- this.state().interaction.onRedo?.();
578
- }
579
- onHeaderMouseDown(columnId, event) {
580
- this.columnReorderService.handleHeaderMouseDown(columnId, event);
581
- }
582
- // --- Column pinning methods ---
583
- onPinColumn(columnId, side) {
584
- this.state().pinning.pinColumn(columnId, side);
585
- }
586
- onUnpinColumn(columnId) {
587
- this.state().pinning.unpinColumn(columnId);
588
- }
589
- isPinned(columnId) {
590
- return this.state().pinning.isPinned(columnId);
591
- }
592
- getPinState(columnId) {
593
- const pinned = this.isPinned(columnId);
594
- return {
595
- canPinLeft: pinned !== 'left',
596
- canPinRight: pinned !== 'right',
597
- canUnpin: !!pinned,
598
- };
599
- }
600
- // --- Column sorting methods ---
601
- onSortAsc(columnId) {
602
- const props = this.getProps();
603
- props?.onColumnSort?.(columnId, 'asc');
604
- }
605
- onSortDesc(columnId) {
606
- const props = this.getProps();
607
- props?.onColumnSort?.(columnId, 'desc');
608
- }
609
- onClearSort(columnId) {
610
- const props = this.getProps();
611
- const col = columnId ?? props?.sortBy;
612
- if (col) {
613
- props?.onColumnSort?.(col, null);
614
- }
615
- }
616
- getSortState(columnId) {
617
- const props = this.getProps();
618
- if (props?.sortBy === columnId) {
619
- return props.sortDirection ?? 'asc';
620
- }
621
- return null;
622
- }
623
- // --- Column autosize methods ---
624
- onAutosizeColumn(columnId) {
625
- const col = this.visibleCols().find((c) => c.columnId === columnId);
626
- if (!col)
627
- return;
628
- const width = measureColumnContentWidth(columnId, col.minWidth, this.tableContainerEl() ?? undefined);
629
- this.state().layout.setColumnSizingOverrides({
630
- ...this.columnSizingOverrides(),
631
- [columnId]: { widthPx: width },
632
- });
633
- (this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(columnId, width);
634
- }
635
- onAutosizeAllColumns() {
636
- const tableEl = this.tableContainerEl() ?? undefined;
637
- const overrides = {};
638
- for (const col of this.visibleCols()) {
639
- const width = measureColumnContentWidth(col.columnId, col.minWidth, tableEl);
640
- overrides[col.columnId] = { widthPx: width };
641
- (this.state().layout.onAutosizeColumn ?? this.state().layout.onColumnResized)?.(col.columnId, width);
642
- }
643
- this.state().layout.setColumnSizingOverrides({
644
- ...this.columnSizingOverrides(),
645
- ...overrides,
646
- });
647
- }
648
- }