@alaarab/ogrid-js 2.0.0-beta

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 (55) hide show
  1. package/dist/esm/OGrid.js +654 -0
  2. package/dist/esm/components/ColumnChooser.js +68 -0
  3. package/dist/esm/components/ContextMenu.js +122 -0
  4. package/dist/esm/components/HeaderFilter.js +281 -0
  5. package/dist/esm/components/InlineCellEditor.js +278 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +170 -0
  7. package/dist/esm/components/PaginationControls.js +85 -0
  8. package/dist/esm/components/SideBar.js +353 -0
  9. package/dist/esm/components/StatusBar.js +34 -0
  10. package/dist/esm/index.js +26 -0
  11. package/dist/esm/renderer/TableRenderer.js +414 -0
  12. package/dist/esm/state/ClipboardState.js +171 -0
  13. package/dist/esm/state/ColumnPinningState.js +78 -0
  14. package/dist/esm/state/ColumnResizeState.js +55 -0
  15. package/dist/esm/state/EventEmitter.js +27 -0
  16. package/dist/esm/state/FillHandleState.js +218 -0
  17. package/dist/esm/state/GridState.js +261 -0
  18. package/dist/esm/state/HeaderFilterState.js +205 -0
  19. package/dist/esm/state/KeyboardNavState.js +374 -0
  20. package/dist/esm/state/RowSelectionState.js +81 -0
  21. package/dist/esm/state/SelectionState.js +102 -0
  22. package/dist/esm/state/SideBarState.js +41 -0
  23. package/dist/esm/state/TableLayoutState.js +95 -0
  24. package/dist/esm/state/UndoRedoState.js +82 -0
  25. package/dist/esm/types/columnTypes.js +1 -0
  26. package/dist/esm/types/gridTypes.js +1 -0
  27. package/dist/esm/types/index.js +2 -0
  28. package/dist/types/OGrid.d.ts +60 -0
  29. package/dist/types/components/ColumnChooser.d.ts +14 -0
  30. package/dist/types/components/ContextMenu.d.ts +17 -0
  31. package/dist/types/components/HeaderFilter.d.ts +24 -0
  32. package/dist/types/components/InlineCellEditor.d.ts +24 -0
  33. package/dist/types/components/MarchingAntsOverlay.d.ts +25 -0
  34. package/dist/types/components/PaginationControls.d.ts +9 -0
  35. package/dist/types/components/SideBar.d.ts +35 -0
  36. package/dist/types/components/StatusBar.d.ts +8 -0
  37. package/dist/types/index.d.ts +26 -0
  38. package/dist/types/renderer/TableRenderer.d.ts +59 -0
  39. package/dist/types/state/ClipboardState.d.ts +35 -0
  40. package/dist/types/state/ColumnPinningState.d.ts +36 -0
  41. package/dist/types/state/ColumnResizeState.d.ts +23 -0
  42. package/dist/types/state/EventEmitter.d.ts +9 -0
  43. package/dist/types/state/FillHandleState.d.ts +51 -0
  44. package/dist/types/state/GridState.d.ts +68 -0
  45. package/dist/types/state/HeaderFilterState.d.ts +64 -0
  46. package/dist/types/state/KeyboardNavState.d.ts +29 -0
  47. package/dist/types/state/RowSelectionState.d.ts +23 -0
  48. package/dist/types/state/SelectionState.d.ts +37 -0
  49. package/dist/types/state/SideBarState.d.ts +19 -0
  50. package/dist/types/state/TableLayoutState.d.ts +33 -0
  51. package/dist/types/state/UndoRedoState.d.ts +28 -0
  52. package/dist/types/types/columnTypes.d.ts +28 -0
  53. package/dist/types/types/gridTypes.d.ts +69 -0
  54. package/dist/types/types/index.d.ts +2 -0
  55. package/package.json +29 -0
@@ -0,0 +1,654 @@
1
+ import { GridState } from './state/GridState';
2
+ import { TableRenderer } from './renderer/TableRenderer';
3
+ import { PaginationControls } from './components/PaginationControls';
4
+ import { StatusBar } from './components/StatusBar';
5
+ import { ColumnChooser } from './components/ColumnChooser';
6
+ import { SideBarState } from './state/SideBarState';
7
+ import { SideBar } from './components/SideBar';
8
+ import { HeaderFilterState } from './state/HeaderFilterState';
9
+ import { HeaderFilter } from './components/HeaderFilter';
10
+ import { SelectionState } from './state/SelectionState';
11
+ import { KeyboardNavState } from './state/KeyboardNavState';
12
+ import { ClipboardState } from './state/ClipboardState';
13
+ import { UndoRedoState } from './state/UndoRedoState';
14
+ import { ColumnResizeState } from './state/ColumnResizeState';
15
+ import { TableLayoutState } from './state/TableLayoutState';
16
+ import { FillHandleState } from './state/FillHandleState';
17
+ import { RowSelectionState } from './state/RowSelectionState';
18
+ import { ColumnPinningState } from './state/ColumnPinningState';
19
+ import { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
20
+ import { InlineCellEditor } from './components/InlineCellEditor';
21
+ import { ContextMenu } from './components/ContextMenu';
22
+ import { EventEmitter } from './state/EventEmitter';
23
+ import { normalizeSelectionRange, isInSelectionRange, flattenColumns } from '@alaarab/ogrid-core';
24
+ export class OGrid {
25
+ constructor(container, options) {
26
+ // Sidebar
27
+ this.sideBarState = null;
28
+ this.sideBarComponent = null;
29
+ this.sideBarContainer = null;
30
+ this.filterConfigs = new Map();
31
+ // Loading overlay
32
+ this.loadingOverlay = null;
33
+ // Body area (holds sidebar + table)
34
+ this.bodyArea = null;
35
+ // Interaction states
36
+ this.selectionState = null;
37
+ this.keyboardNavState = null;
38
+ this.clipboardState = null;
39
+ this.undoRedoState = null;
40
+ this.resizeState = null;
41
+ this.fillHandleState = null;
42
+ this.rowSelectionState = null;
43
+ this.pinningState = null;
44
+ this.marchingAnts = null;
45
+ this.cellEditor = null;
46
+ this.contextMenu = null;
47
+ this.events = new EventEmitter();
48
+ this.unsubscribes = [];
49
+ this.options = options;
50
+ this.state = new GridState(options);
51
+ this.api = this.state.getApi();
52
+ // Build layout
53
+ this.containerEl = document.createElement('div');
54
+ this.containerEl.className = 'ogrid-container';
55
+ // Toolbar
56
+ this.toolbarEl = document.createElement('div');
57
+ this.toolbarEl.className = 'ogrid-toolbar';
58
+ this.containerEl.appendChild(this.toolbarEl);
59
+ // Body area (holds sidebar + table, side by side)
60
+ this.bodyArea = document.createElement('div');
61
+ this.bodyArea.className = 'ogrid-body-area';
62
+ this.bodyArea.style.display = 'flex';
63
+ this.bodyArea.style.flex = '1';
64
+ this.bodyArea.style.overflow = 'hidden';
65
+ this.containerEl.appendChild(this.bodyArea);
66
+ // Table container (inside body area)
67
+ this.tableContainer = document.createElement('div');
68
+ this.tableContainer.className = 'ogrid-table-container';
69
+ this.tableContainer.style.flex = '1';
70
+ this.tableContainer.style.overflow = 'auto';
71
+ this.tableContainer.style.position = 'relative';
72
+ this.bodyArea.appendChild(this.tableContainer);
73
+ // Status bar container
74
+ this.statusBarContainer = document.createElement('div');
75
+ this.statusBarContainer.className = 'ogrid-status-bar-container';
76
+ this.containerEl.appendChild(this.statusBarContainer);
77
+ // Pagination container
78
+ this.paginationContainer = document.createElement('div');
79
+ this.paginationContainer.className = 'ogrid-pagination-container';
80
+ this.containerEl.appendChild(this.paginationContainer);
81
+ container.appendChild(this.containerEl);
82
+ // Create layout state (measures container, tracks column sizing)
83
+ this.layoutState = new TableLayoutState();
84
+ this.layoutState.observeContainer(this.tableContainer);
85
+ // Create sub-components
86
+ this.renderer = new TableRenderer(this.tableContainer, this.state);
87
+ this.pagination = new PaginationControls(this.paginationContainer, this.state);
88
+ this.statusBar = new StatusBar(this.statusBarContainer);
89
+ this.columnChooser = new ColumnChooser(this.toolbarEl, this.state);
90
+ // Initialize header filter state
91
+ this.headerFilterState = new HeaderFilterState((key, value) => {
92
+ this.state.setFilter(key, value);
93
+ });
94
+ this.headerFilterComponent = new HeaderFilter(this.headerFilterState);
95
+ this.buildFilterConfigs();
96
+ // Pass filter config to renderer for filter icons in headers
97
+ this.renderer.setHeaderFilterState(this.headerFilterState, this.filterConfigs);
98
+ this.renderer.setOnFilterIconClick((columnId, headerEl) => {
99
+ this.handleFilterIconClick(columnId, headerEl);
100
+ });
101
+ // Initialize sidebar if configured
102
+ if (options.sideBar) {
103
+ this.sideBarState = new SideBarState(options.sideBar);
104
+ this.sideBarContainer = document.createElement('div');
105
+ this.sideBarContainer.className = 'ogrid-sidebar-container';
106
+ this.sideBarComponent = new SideBar(this.sideBarContainer, this.sideBarState);
107
+ if (this.sideBarState.position === 'left') {
108
+ this.bodyArea.insertBefore(this.sideBarContainer, this.tableContainer);
109
+ }
110
+ else {
111
+ this.bodyArea.appendChild(this.sideBarContainer);
112
+ }
113
+ this.unsubscribes.push(this.sideBarState.onChange(() => {
114
+ this.renderSideBar();
115
+ }));
116
+ }
117
+ // Initialize column pinning (always active, even without interaction)
118
+ const flatCols = flattenColumns(options.columns);
119
+ this.pinningState = new ColumnPinningState(options.pinnedColumns, flatCols);
120
+ // Initialize row selection (always active if rowSelection is set)
121
+ if (options.rowSelection && options.rowSelection !== 'none') {
122
+ this.rowSelectionState = new RowSelectionState(options.rowSelection, options.getRowId);
123
+ // Wire row selection API methods
124
+ this.api.getSelectedRows = () => {
125
+ return Array.from(this.rowSelectionState?.selectedRowIds ?? []);
126
+ };
127
+ this.api.selectAll = () => {
128
+ const { items } = this.state.getProcessedItems();
129
+ this.rowSelectionState?.handleSelectAll(true, items);
130
+ };
131
+ this.api.deselectAll = () => {
132
+ const { items } = this.state.getProcessedItems();
133
+ this.rowSelectionState?.handleSelectAll(false, items);
134
+ };
135
+ this.api.setSelectedRows = (rowIds) => {
136
+ const { items } = this.state.getProcessedItems();
137
+ this.rowSelectionState?.updateSelection(new Set(rowIds), items);
138
+ };
139
+ this.unsubscribes.push(this.rowSelectionState.onRowSelectionChange(() => {
140
+ this.updateRendererInteractionState();
141
+ }));
142
+ }
143
+ // Initial render (must happen before interaction init so wrapper DOM exists)
144
+ this.renderer.render();
145
+ // Initialize interaction features if enabled (default: true for cellSelection)
146
+ const shouldEnableInteraction = options.cellSelection !== false || options.editable === true;
147
+ if (shouldEnableInteraction) {
148
+ this.initializeInteraction();
149
+ }
150
+ // Subscribe to state changes
151
+ this.unsubscribes.push(this.state.onStateChange(() => {
152
+ this.renderAll();
153
+ }));
154
+ // Subscribe to pinning changes
155
+ this.unsubscribes.push(this.pinningState.onPinningChange(() => {
156
+ this.updateRendererInteractionState();
157
+ }));
158
+ // Subscribe to header filter state changes
159
+ this.unsubscribes.push(this.headerFilterState.onChange(() => {
160
+ this.renderHeaderFilterPopover();
161
+ }));
162
+ // Complete initial render (pagination, status bar, column chooser, sidebar, loading)
163
+ this.renderAll();
164
+ }
165
+ initializeInteraction() {
166
+ const { editable } = this.options;
167
+ const colOffset = this.rowSelectionState ? 1 : 0;
168
+ // Create interaction states
169
+ this.selectionState = new SelectionState();
170
+ this.resizeState = new ColumnResizeState();
171
+ this.contextMenu = new ContextMenu();
172
+ this.cellEditor = new InlineCellEditor(this.tableContainer);
173
+ // Undo/Redo (wraps onCellValueChanged if editable)
174
+ const onCellValueChanged = this.options.onCellValueChanged;
175
+ this.undoRedoState = new UndoRedoState(onCellValueChanged);
176
+ // Clipboard
177
+ this.clipboardState = new ClipboardState({
178
+ items: [],
179
+ visibleCols: [],
180
+ colOffset,
181
+ editable,
182
+ onCellValueChanged: this.undoRedoState.getWrappedCallback(),
183
+ }, () => this.selectionState?.activeCell ?? null, () => this.selectionState?.selectionRange ?? null);
184
+ // Fill handle
185
+ this.fillHandleState = new FillHandleState({
186
+ items: [],
187
+ visibleCols: [],
188
+ editable,
189
+ onCellValueChanged: this.undoRedoState.getWrappedCallback(),
190
+ colOffset,
191
+ beginBatch: () => this.undoRedoState?.beginBatch(),
192
+ endBatch: () => this.undoRedoState?.endBatch(),
193
+ }, () => this.selectionState?.selectionRange ?? null, (range) => {
194
+ this.selectionState?.setSelectionRange(range);
195
+ this.updateRendererInteractionState();
196
+ }, (cell) => {
197
+ this.selectionState?.setActiveCell(cell);
198
+ });
199
+ // Keyboard navigation
200
+ this.keyboardNavState = new KeyboardNavState({
201
+ items: [],
202
+ visibleCols: [],
203
+ colOffset,
204
+ getRowId: this.state.getRowId,
205
+ editable,
206
+ onCellValueChanged: this.undoRedoState.getWrappedCallback(),
207
+ onCopy: () => this.clipboardState?.handleCopy(),
208
+ onCut: () => this.clipboardState?.handleCut(),
209
+ onPaste: async () => { await this.clipboardState?.handlePaste(); },
210
+ onUndo: () => this.undoRedoState?.undo(),
211
+ onRedo: () => this.undoRedoState?.redo(),
212
+ onContextMenu: (x, y) => this.showContextMenu(x, y),
213
+ onStartEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
214
+ clearClipboardRanges: () => this.clipboardState?.clearClipboardRanges(),
215
+ }, () => this.selectionState?.activeCell ?? null, () => this.selectionState?.selectionRange ?? null, (cell) => this.selectionState?.setActiveCell(cell), (range) => this.selectionState?.setSelectionRange(range));
216
+ // Subscribe to selection changes
217
+ this.unsubscribes.push(this.selectionState.onSelectionChange(() => {
218
+ this.updateRendererInteractionState();
219
+ }));
220
+ // Subscribe to clipboard range changes
221
+ this.unsubscribes.push(this.clipboardState.onRangesChange(() => {
222
+ this.updateRendererInteractionState();
223
+ }));
224
+ // Subscribe to column resize changes
225
+ this.unsubscribes.push(this.resizeState.onColumnWidthChange(() => {
226
+ this.updateRendererInteractionState();
227
+ }));
228
+ // Attach keyboard handler to wrapper
229
+ const wrapper = this.renderer.getWrapperElement();
230
+ if (wrapper) {
231
+ wrapper.addEventListener('keydown', this.keyboardNavState.handleKeyDown);
232
+ this.keyboardNavState.setWrapperRef(wrapper);
233
+ this.fillHandleState.setWrapperRef(wrapper);
234
+ // Initialize marching ants overlay
235
+ this.marchingAnts = new MarchingAntsOverlay(wrapper, colOffset);
236
+ }
237
+ // Attach global mouse handlers for resize and drag
238
+ this.attachGlobalHandlers();
239
+ // Set initial interaction state on renderer
240
+ this.updateRendererInteractionState();
241
+ }
242
+ attachGlobalHandlers() {
243
+ let resizing = false;
244
+ const handleMouseMove = (e) => {
245
+ if (resizing && this.resizeState) {
246
+ const newWidth = this.resizeState.updateResize(e.clientX);
247
+ if (newWidth !== null && this.resizeState.resizingColumnId) {
248
+ this.layoutState.setColumnOverride(this.resizeState.resizingColumnId, newWidth);
249
+ this.updateRendererInteractionState();
250
+ }
251
+ }
252
+ if (this.selectionState?.isDragging) {
253
+ const target = e.target;
254
+ if (target.tagName === 'TD') {
255
+ const rowIndex = parseInt(target.getAttribute('data-row-index') ?? '-1', 10);
256
+ const colIndex = parseInt(target.getAttribute('data-col-index') ?? '-1', 10);
257
+ if (rowIndex >= 0 && colIndex >= 0) {
258
+ this.selectionState.updateDrag(rowIndex, colIndex, () => this.updateDragAttributes());
259
+ }
260
+ }
261
+ }
262
+ };
263
+ const handleMouseUp = (e) => {
264
+ if (resizing && this.resizeState) {
265
+ const colId = this.resizeState.resizingColumnId;
266
+ this.resizeState.endResize(e.clientX);
267
+ if (colId) {
268
+ const width = this.resizeState.getColumnWidth(colId);
269
+ if (width)
270
+ this.layoutState.setColumnOverride(colId, width);
271
+ }
272
+ resizing = false;
273
+ document.body.style.cursor = '';
274
+ this.updateRendererInteractionState();
275
+ }
276
+ if (this.selectionState?.isDragging) {
277
+ this.selectionState.endDrag();
278
+ }
279
+ };
280
+ const handleResizeStart = (columnId, clientX, currentWidth) => {
281
+ resizing = true;
282
+ document.body.style.cursor = 'col-resize';
283
+ this.resizeState?.startResize(columnId, clientX, currentWidth);
284
+ };
285
+ document.addEventListener('mousemove', handleMouseMove);
286
+ document.addEventListener('mouseup', handleMouseUp);
287
+ // Store references for cleanup
288
+ this.unsubscribes.push(() => {
289
+ document.removeEventListener('mousemove', handleMouseMove);
290
+ document.removeEventListener('mouseup', handleMouseUp);
291
+ });
292
+ // Pass resize handler to renderer
293
+ this.renderer.setInteractionState({
294
+ activeCell: null,
295
+ selectionRange: null,
296
+ copyRange: null,
297
+ cutRange: null,
298
+ editingCell: null,
299
+ columnWidths: {},
300
+ onResizeStart: handleResizeStart,
301
+ });
302
+ }
303
+ updateRendererInteractionState() {
304
+ if (!this.selectionState || !this.clipboardState || !this.resizeState)
305
+ return;
306
+ const { items } = this.state.getProcessedItems();
307
+ const visibleCols = this.state.visibleColumnDefs;
308
+ // Compute pinning offsets
309
+ const columnWidths = this.layoutState.getAllColumnWidths();
310
+ const leftOffsets = this.pinningState?.computeLeftOffsets(visibleCols, columnWidths, 120, !!this.rowSelectionState, 40) ?? {};
311
+ const rightOffsets = this.pinningState?.computeRightOffsets(visibleCols, columnWidths, 120) ?? {};
312
+ this.renderer.setInteractionState({
313
+ activeCell: this.selectionState.activeCell,
314
+ selectionRange: this.selectionState.selectionRange,
315
+ copyRange: this.clipboardState.copyRange,
316
+ cutRange: this.clipboardState.cutRange,
317
+ editingCell: this.cellEditor?.getEditingCell() ?? null,
318
+ columnWidths,
319
+ onCellClick: (rowIndex, colIndex) => this.handleCellClick(rowIndex, colIndex),
320
+ onCellMouseDown: (rowIndex, colIndex, e) => this.handleCellMouseDown(rowIndex, colIndex, e),
321
+ onCellDoubleClick: (rowIndex, colIndex, rowId, columnId) => this.startCellEdit(rowId, columnId),
322
+ onCellContextMenu: (rowIndex, colIndex, e) => this.handleCellContextMenu(rowIndex, colIndex, e),
323
+ onResizeStart: this.renderer['interactionState']?.onResizeStart,
324
+ // Fill handle
325
+ onFillHandleMouseDown: this.options.editable !== false ? (e) => this.fillHandleState?.startFillDrag(e) : undefined,
326
+ // Row selection
327
+ rowSelectionMode: this.rowSelectionState?.rowSelection ?? 'none',
328
+ selectedRowIds: this.rowSelectionState?.selectedRowIds,
329
+ onRowCheckboxChange: (rowId, checked, rowIndex, shiftKey) => {
330
+ this.rowSelectionState?.handleRowCheckboxChange(rowId, checked, rowIndex, shiftKey, items);
331
+ },
332
+ onSelectAll: (checked) => {
333
+ this.rowSelectionState?.handleSelectAll(checked, items);
334
+ },
335
+ allSelected: this.rowSelectionState?.isAllSelected(items),
336
+ someSelected: this.rowSelectionState?.isSomeSelected(items),
337
+ // Column pinning
338
+ pinnedColumns: this.pinningState?.pinnedColumns,
339
+ leftOffsets,
340
+ rightOffsets,
341
+ });
342
+ this.renderer.update();
343
+ // Update marching ants overlay
344
+ this.marchingAnts?.update(this.selectionState.selectionRange, this.clipboardState.copyRange, this.clipboardState.cutRange);
345
+ }
346
+ updateDragAttributes() {
347
+ const wrapper = this.renderer.getWrapperElement();
348
+ if (!wrapper || !this.selectionState)
349
+ return;
350
+ const range = this.selectionState.getDragRange();
351
+ if (!range)
352
+ return;
353
+ const norm = normalizeSelectionRange(range);
354
+ const cells = wrapper.querySelectorAll('td[data-row-index][data-col-index]');
355
+ for (const cell of Array.from(cells)) {
356
+ const rowIndex = parseInt(cell.getAttribute('data-row-index') ?? '-1', 10);
357
+ const colIndex = parseInt(cell.getAttribute('data-col-index') ?? '-1', 10);
358
+ if (isInSelectionRange(norm, rowIndex, colIndex)) {
359
+ cell.setAttribute('data-drag-range', 'true');
360
+ }
361
+ else {
362
+ cell.removeAttribute('data-drag-range');
363
+ }
364
+ }
365
+ }
366
+ handleCellClick(rowIndex, colIndex) {
367
+ if (!this.selectionState)
368
+ return;
369
+ // setActiveCell also sets a single-cell selectionRange internally.
370
+ // The selectionChange subscription handles re-rendering.
371
+ this.selectionState.setActiveCell({ rowIndex, columnIndex: colIndex });
372
+ }
373
+ handleCellMouseDown(rowIndex, colIndex, e) {
374
+ if (!this.selectionState)
375
+ return;
376
+ e.preventDefault();
377
+ this.selectionState.startDrag(rowIndex, colIndex);
378
+ }
379
+ handleCellContextMenu(rowIndex, colIndex, e) {
380
+ e.preventDefault();
381
+ if (!this.contextMenu || !this.selectionState || !this.clipboardState || !this.undoRedoState)
382
+ return;
383
+ // Set active cell if not already set
384
+ if (!this.selectionState.activeCell || this.selectionState.activeCell.rowIndex !== rowIndex || this.selectionState.activeCell.columnIndex !== colIndex) {
385
+ this.selectionState.setActiveCell({ rowIndex, columnIndex: colIndex });
386
+ this.updateRendererInteractionState();
387
+ }
388
+ this.showContextMenu(e.clientX, e.clientY);
389
+ }
390
+ showContextMenu(x, y) {
391
+ if (!this.contextMenu || !this.clipboardState || !this.undoRedoState || !this.keyboardNavState || !this.selectionState)
392
+ return;
393
+ this.contextMenu.show(x, y, {
394
+ onCopy: () => this.clipboardState.handleCopy(),
395
+ onCut: () => this.clipboardState.handleCut(),
396
+ onPaste: () => void this.clipboardState.handlePaste(),
397
+ onSelectAll: () => {
398
+ const { items } = this.state.getProcessedItems();
399
+ const visibleCols = this.state.visibleColumnDefs;
400
+ if (items.length > 0 && visibleCols.length > 0) {
401
+ this.selectionState.setSelectionRange({
402
+ startRow: 0,
403
+ startCol: 0,
404
+ endRow: items.length - 1,
405
+ endCol: visibleCols.length - 1,
406
+ });
407
+ this.updateRendererInteractionState();
408
+ }
409
+ },
410
+ onUndo: () => this.undoRedoState.undo(),
411
+ onRedo: () => this.undoRedoState.redo(),
412
+ }, this.undoRedoState.canUndo, this.undoRedoState.canRedo, this.selectionState.selectionRange);
413
+ }
414
+ startCellEdit(rowId, columnId) {
415
+ if (!this.cellEditor || !this.undoRedoState)
416
+ return;
417
+ const { items } = this.state.getProcessedItems();
418
+ const visibleCols = this.state.visibleColumnDefs;
419
+ const item = items.find((it) => this.state.getRowId(it) === rowId);
420
+ const column = visibleCols.find((col) => col.columnId === columnId);
421
+ if (!item || !column)
422
+ return;
423
+ const wrapper = this.renderer.getWrapperElement();
424
+ if (!wrapper)
425
+ return;
426
+ // Find the row first, then the cell within it
427
+ const row = wrapper.querySelector(`tr[data-row-id="${rowId}"]`);
428
+ if (!row)
429
+ return;
430
+ const cell = row.querySelector(`td[data-column-id="${columnId}"]`);
431
+ if (!cell)
432
+ return;
433
+ const onCommit = (rid, cid, value) => {
434
+ const itm = items.find((i) => this.state.getRowId(i) === rid);
435
+ const col = visibleCols.find((c) => c.columnId === cid);
436
+ if (!itm || !col)
437
+ return;
438
+ const oldValue = itm[cid];
439
+ itm[cid] = value;
440
+ const wrapped = this.undoRedoState.getWrappedCallback();
441
+ if (wrapped) {
442
+ wrapped({
443
+ item: itm,
444
+ columnId: cid,
445
+ oldValue,
446
+ newValue: value,
447
+ rowIndex: items.indexOf(itm),
448
+ });
449
+ }
450
+ this.updateRendererInteractionState();
451
+ };
452
+ const onCancel = () => {
453
+ this.updateRendererInteractionState();
454
+ };
455
+ this.cellEditor.startEdit(rowId, columnId, item, column, cell, onCommit, onCancel);
456
+ }
457
+ buildFilterConfigs() {
458
+ const columns = flattenColumns(this.options.columns);
459
+ for (const col of columns) {
460
+ const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
461
+ if (filterable && filterable.type) {
462
+ this.filterConfigs.set(col.columnId, {
463
+ columnId: col.columnId,
464
+ filterField: filterable.filterField ?? col.columnId,
465
+ filterType: filterable.type,
466
+ });
467
+ }
468
+ }
469
+ }
470
+ handleFilterIconClick(columnId, headerEl) {
471
+ const config = this.filterConfigs.get(columnId);
472
+ if (!config)
473
+ return;
474
+ if (this.headerFilterState.openColumnId === columnId) {
475
+ this.headerFilterState.close();
476
+ return;
477
+ }
478
+ // Create a temporary popover element to pass to HeaderFilterState
479
+ const tempPopover = document.createElement('div');
480
+ this.headerFilterState.setFilters(this.state.filters);
481
+ this.headerFilterState.setFilterOptions(this.state.filterOptions);
482
+ this.headerFilterState.open(columnId, config, headerEl, tempPopover);
483
+ }
484
+ renderHeaderFilterPopover() {
485
+ const openId = this.headerFilterState.openColumnId;
486
+ if (!openId) {
487
+ this.headerFilterComponent.cleanup();
488
+ return;
489
+ }
490
+ const config = this.filterConfigs.get(openId);
491
+ if (!config)
492
+ return;
493
+ this.headerFilterComponent.render(config);
494
+ // Update the popover element reference for click-outside detection
495
+ const popoverEl = document.querySelector('.ogrid-header-filter-popover');
496
+ if (popoverEl) {
497
+ this.headerFilterState._popoverEl = popoverEl;
498
+ }
499
+ }
500
+ renderSideBar() {
501
+ if (!this.sideBarComponent || !this.sideBarState)
502
+ return;
503
+ const columns = this.state.columns.map(c => ({
504
+ columnId: c.columnId,
505
+ name: c.name,
506
+ required: c.required === true,
507
+ }));
508
+ const filterableColumns = this.state.columns
509
+ .filter(c => c.filterable && typeof c.filterable === 'object' && c.filterable.type)
510
+ .map(c => ({
511
+ columnId: c.columnId,
512
+ name: c.name,
513
+ filterField: c.filterable.filterField ?? c.columnId,
514
+ filterType: c.filterable.type,
515
+ }));
516
+ this.sideBarComponent.setConfig({
517
+ columns,
518
+ visibleColumns: this.state.visibleColumns,
519
+ onVisibilityChange: (columnKey, visible) => {
520
+ const next = new Set(this.state.visibleColumns);
521
+ if (visible)
522
+ next.add(columnKey);
523
+ else
524
+ next.delete(columnKey);
525
+ this.state.setVisibleColumns(next);
526
+ },
527
+ onSetVisibleColumns: (cols) => this.state.setVisibleColumns(cols),
528
+ filterableColumns,
529
+ filters: this.state.filters,
530
+ onFilterChange: (key, value) => this.state.setFilter(key, value),
531
+ filterOptions: this.state.filterOptions,
532
+ });
533
+ this.sideBarComponent.render();
534
+ }
535
+ renderLoadingOverlay() {
536
+ if (this.state.isLoading) {
537
+ if (!this.loadingOverlay) {
538
+ this.loadingOverlay = document.createElement('div');
539
+ this.loadingOverlay.className = 'ogrid-loading-overlay';
540
+ this.loadingOverlay.style.position = 'absolute';
541
+ this.loadingOverlay.style.top = '0';
542
+ this.loadingOverlay.style.left = '0';
543
+ this.loadingOverlay.style.right = '0';
544
+ this.loadingOverlay.style.bottom = '0';
545
+ this.loadingOverlay.style.display = 'flex';
546
+ this.loadingOverlay.style.alignItems = 'center';
547
+ this.loadingOverlay.style.justifyContent = 'center';
548
+ this.loadingOverlay.style.background = 'rgba(255,255,255,0.7)';
549
+ this.loadingOverlay.style.zIndex = '100';
550
+ const spinner = document.createElement('div');
551
+ spinner.className = 'ogrid-loading-spinner';
552
+ spinner.textContent = 'Loading...';
553
+ this.loadingOverlay.appendChild(spinner);
554
+ }
555
+ if (!this.tableContainer.contains(this.loadingOverlay)) {
556
+ this.tableContainer.appendChild(this.loadingOverlay);
557
+ }
558
+ }
559
+ else {
560
+ if (this.loadingOverlay && this.tableContainer.contains(this.loadingOverlay)) {
561
+ this.loadingOverlay.remove();
562
+ }
563
+ }
564
+ }
565
+ renderAll() {
566
+ const colOffset = this.rowSelectionState ? 1 : 0;
567
+ // Update header filter state with current filters and options
568
+ this.headerFilterState.setFilters(this.state.filters);
569
+ this.headerFilterState.setFilterOptions(this.state.filterOptions);
570
+ // Update interaction states with current data
571
+ if (this.keyboardNavState && this.clipboardState) {
572
+ const { items } = this.state.getProcessedItems();
573
+ const visibleCols = this.state.visibleColumnDefs;
574
+ this.keyboardNavState.updateParams({
575
+ items,
576
+ visibleCols: visibleCols,
577
+ colOffset,
578
+ getRowId: this.state.getRowId,
579
+ editable: this.options.editable,
580
+ onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
581
+ onCopy: () => this.clipboardState?.handleCopy(),
582
+ onCut: () => this.clipboardState?.handleCut(),
583
+ onPaste: async () => { await this.clipboardState?.handlePaste(); },
584
+ onUndo: () => this.undoRedoState?.undo(),
585
+ onRedo: () => this.undoRedoState?.redo(),
586
+ onContextMenu: (x, y) => this.showContextMenu(x, y),
587
+ onStartEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
588
+ clearClipboardRanges: () => this.clipboardState?.clearClipboardRanges(),
589
+ });
590
+ this.clipboardState.updateParams({
591
+ items,
592
+ visibleCols: visibleCols,
593
+ colOffset,
594
+ editable: this.options.editable,
595
+ onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
596
+ });
597
+ // Update fill handle params
598
+ this.fillHandleState?.updateParams({
599
+ items,
600
+ visibleCols: visibleCols,
601
+ editable: this.options.editable,
602
+ onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
603
+ colOffset,
604
+ beginBatch: () => this.undoRedoState?.beginBatch(),
605
+ endBatch: () => this.undoRedoState?.endBatch(),
606
+ });
607
+ // Update renderer interaction state before rendering
608
+ this.updateRendererInteractionState();
609
+ }
610
+ else {
611
+ this.renderer.update();
612
+ }
613
+ const { totalCount } = this.state.getProcessedItems();
614
+ this.pagination.render(totalCount);
615
+ this.statusBar.render({ totalCount });
616
+ this.columnChooser.render();
617
+ this.renderSideBar();
618
+ this.renderLoadingOverlay();
619
+ }
620
+ /** Subscribe to grid events. */
621
+ on(event, handler) {
622
+ this.events.on(event, handler);
623
+ }
624
+ /** Unsubscribe from grid events. */
625
+ off(event, handler) {
626
+ this.events.off(event, handler);
627
+ }
628
+ /** Clean up all event listeners and DOM. */
629
+ destroy() {
630
+ this.unsubscribes.forEach((unsub) => unsub());
631
+ this.renderer.destroy();
632
+ this.pagination.destroy();
633
+ this.statusBar.destroy();
634
+ this.columnChooser.destroy();
635
+ this.sideBarState?.destroy();
636
+ this.sideBarComponent?.destroy();
637
+ this.headerFilterState.destroy();
638
+ this.headerFilterComponent.destroy();
639
+ this.state.destroy();
640
+ this.selectionState?.destroy();
641
+ this.clipboardState?.destroy();
642
+ this.undoRedoState?.destroy();
643
+ this.resizeState?.destroy();
644
+ this.fillHandleState?.destroy();
645
+ this.rowSelectionState?.destroy();
646
+ this.pinningState?.destroy();
647
+ this.marchingAnts?.destroy();
648
+ this.layoutState.destroy();
649
+ this.cellEditor?.closeEditor();
650
+ this.contextMenu?.close();
651
+ this.events.removeAllListeners();
652
+ this.containerEl.remove();
653
+ }
654
+ }