@alaarab/ogrid-js 2.0.23 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/esm/OGrid.js +189 -503
  2. package/dist/esm/OGridEventWiring.js +178 -0
  3. package/dist/esm/OGridRendering.js +269 -0
  4. package/dist/esm/components/ColumnChooser.js +26 -3
  5. package/dist/esm/components/InlineCellEditor.js +18 -36
  6. package/dist/esm/index.js +2 -0
  7. package/dist/esm/renderer/TableRenderer.js +102 -61
  8. package/dist/esm/state/ClipboardState.js +8 -54
  9. package/dist/esm/state/ColumnPinningState.js +1 -2
  10. package/dist/esm/state/ColumnReorderState.js +8 -1
  11. package/dist/esm/state/EventEmitter.js +3 -2
  12. package/dist/esm/state/FillHandleState.js +27 -41
  13. package/dist/esm/state/GridState.js +36 -10
  14. package/dist/esm/state/HeaderFilterState.js +19 -11
  15. package/dist/esm/state/KeyboardNavState.js +19 -132
  16. package/dist/esm/state/RowSelectionState.js +6 -15
  17. package/dist/esm/state/SideBarState.js +1 -1
  18. package/dist/esm/state/TableLayoutState.js +6 -4
  19. package/dist/esm/utils/getCellCoordinates.js +15 -0
  20. package/dist/esm/utils/index.js +1 -0
  21. package/dist/types/OGrid.d.ts +97 -9
  22. package/dist/types/OGridEventWiring.d.ts +60 -0
  23. package/dist/types/OGridRendering.d.ts +93 -0
  24. package/dist/types/components/ColumnChooser.d.ts +5 -0
  25. package/dist/types/components/InlineCellEditor.d.ts +5 -0
  26. package/dist/types/index.d.ts +6 -1
  27. package/dist/types/renderer/TableRenderer.d.ts +12 -5
  28. package/dist/types/state/EventEmitter.d.ts +1 -1
  29. package/dist/types/state/FillHandleState.d.ts +1 -1
  30. package/dist/types/state/GridState.d.ts +7 -1
  31. package/dist/types/state/HeaderFilterState.d.ts +2 -0
  32. package/dist/types/types/gridTypes.d.ts +15 -0
  33. package/dist/types/utils/getCellCoordinates.d.ts +8 -0
  34. package/dist/types/utils/index.d.ts +1 -0
  35. package/package.json +11 -4
package/dist/esm/OGrid.js CHANGED
@@ -7,24 +7,22 @@ import { SideBarState } from './state/SideBarState';
7
7
  import { SideBar } from './components/SideBar';
8
8
  import { HeaderFilterState } from './state/HeaderFilterState';
9
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
10
  import { TableLayoutState } from './state/TableLayoutState';
16
- import { FillHandleState } from './state/FillHandleState';
17
11
  import { RowSelectionState } from './state/RowSelectionState';
18
12
  import { ColumnPinningState } from './state/ColumnPinningState';
19
- import { ColumnReorderState } from './state/ColumnReorderState';
20
13
  import { VirtualScrollState } from './state/VirtualScrollState';
21
- import { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
22
- import { InlineCellEditor } from './components/InlineCellEditor';
23
- import { ContextMenu } from './components/ContextMenu';
24
14
  import { EventEmitter } from './state/EventEmitter';
25
- import { normalizeSelectionRange, isInSelectionRange, flattenColumns, injectGlobalStyles } from '@alaarab/ogrid-core';
26
- /** CSS variable definitions for light and dark themes (injected once per page). */
15
+ import { flattenColumns, injectGlobalStyles } from '@alaarab/ogrid-core';
16
+ import { OGridEventWiring } from './OGridEventWiring';
17
+ import { OGridRendering } from './OGridRendering';
18
+ /**
19
+ * CSS variable definitions for light and dark themes (injected once per page).
20
+ * NOTE: The dark theme variable block appears twice — once for [data-theme='dark'] and once
21
+ * for @media (prefers-color-scheme: dark). Both blocks must be kept in sync. If you change a
22
+ * dark-theme variable in one block, update the other block too.
23
+ */
27
24
  const OGRID_THEME_CSS = `
25
+ .ogrid-drag-target { box-shadow: inset 0 0 0 1px var(--ogrid-accent, #0078d4); }
28
26
  :root {
29
27
  --ogrid-bg: #ffffff;
30
28
  --ogrid-fg: rgba(0, 0, 0, 0.87);
@@ -133,10 +131,10 @@ export class OGrid {
133
131
  this.contextMenu = null;
134
132
  this.events = new EventEmitter();
135
133
  this.unsubscribes = [];
136
- this.layoutVersion = 0; // Incremented when items, columns, sizing, or order change
137
134
  this.options = options;
138
135
  this.state = new GridState(options);
139
136
  this.api = this.state.getApi();
137
+ this.eventWiringHelper = new OGridEventWiring();
140
138
  // Inject theme CSS variables (light + dark) once per page
141
139
  injectGlobalStyles('ogrid-theme-vars', OGRID_THEME_CSS);
142
140
  // Build layout
@@ -186,343 +184,165 @@ export class OGrid {
186
184
  });
187
185
  this.headerFilterComponent = new HeaderFilter(this.headerFilterState);
188
186
  this.buildFilterConfigs();
189
- // Pass filter config to renderer for filter icons in headers
190
- this.renderer.setHeaderFilterState(this.headerFilterState, this.filterConfigs);
191
- this.renderer.setOnFilterIconClick((columnId, headerEl) => {
192
- this.handleFilterIconClick(columnId, headerEl);
193
- });
194
- // Initialize sidebar if configured
195
- if (options.sideBar) {
196
- this.sideBarState = new SideBarState(options.sideBar);
197
- this.sideBarContainer = document.createElement('div');
198
- this.sideBarContainer.className = 'ogrid-sidebar-container';
199
- this.sideBarComponent = new SideBar(this.sideBarContainer, this.sideBarState);
200
- if (this.sideBarState.position === 'left') {
201
- this.bodyArea.insertBefore(this.sideBarContainer, this.tableContainer);
187
+ try {
188
+ // Pass filter config to renderer for filter icons in headers
189
+ this.renderer.setHeaderFilterState(this.headerFilterState, this.filterConfigs);
190
+ this.renderer.setOnFilterIconClick((columnId, headerEl) => {
191
+ this.handleFilterIconClick(columnId, headerEl);
192
+ });
193
+ // Initialize sidebar if configured
194
+ if (options.sideBar) {
195
+ this.sideBarState = new SideBarState(options.sideBar);
196
+ this.sideBarContainer = document.createElement('div');
197
+ this.sideBarContainer.className = 'ogrid-sidebar-container';
198
+ this.sideBarComponent = new SideBar(this.sideBarContainer, this.sideBarState);
199
+ if (this.bodyArea) {
200
+ if (this.sideBarState.position === 'left') {
201
+ this.bodyArea.insertBefore(this.sideBarContainer, this.tableContainer);
202
+ }
203
+ else {
204
+ this.bodyArea.appendChild(this.sideBarContainer);
205
+ }
206
+ }
207
+ this.unsubscribes.push(this.sideBarState.onChange(() => {
208
+ this.renderingHelper.renderSideBar();
209
+ }));
202
210
  }
203
- else {
204
- this.bodyArea.appendChild(this.sideBarContainer);
211
+ // Initialize column pinning (always active, even without interaction)
212
+ const flatCols = flattenColumns(options.columns);
213
+ this.pinningState = new ColumnPinningState(options.pinnedColumns, flatCols);
214
+ // Initialize row selection (always active if rowSelection is set)
215
+ if (options.rowSelection && options.rowSelection !== 'none') {
216
+ this.rowSelectionState = new RowSelectionState(options.rowSelection, options.getRowId);
217
+ // Wire row selection API methods
218
+ this.api.getSelectedRows = () => {
219
+ return Array.from(this.rowSelectionState?.selectedRowIds ?? []);
220
+ };
221
+ this.api.selectAll = () => {
222
+ const { items } = this.state.getProcessedItems();
223
+ this.rowSelectionState?.handleSelectAll(true, items);
224
+ };
225
+ this.api.deselectAll = () => {
226
+ const { items } = this.state.getProcessedItems();
227
+ this.rowSelectionState?.handleSelectAll(false, items);
228
+ };
229
+ this.api.setSelectedRows = (rowIds) => {
230
+ const { items } = this.state.getProcessedItems();
231
+ this.rowSelectionState?.updateSelection(new Set(rowIds), items);
232
+ };
233
+ this.unsubscribes.push(this.rowSelectionState.onRowSelectionChange(() => {
234
+ this.renderingHelper.updateRendererInteractionState();
235
+ }));
205
236
  }
206
- this.unsubscribes.push(this.sideBarState.onChange(() => {
207
- this.renderSideBar();
237
+ // Create rendering helper (uses lazy context — state objects populated after interaction init)
238
+ this.renderingHelper = this.createRenderingHelper();
239
+ // Initial render (must happen before interaction init so wrapper DOM exists)
240
+ this.renderer.render();
241
+ // Initialize interaction features if enabled (default: true for cellSelection)
242
+ const shouldEnableInteraction = options.cellSelection !== false || options.editable === true;
243
+ if (shouldEnableInteraction) {
244
+ const result = this.eventWiringHelper.initializeInteraction(options, this.state, this.renderer, this.tableContainer, this.layoutState, this.rowSelectionState, this.pinningState, {
245
+ updateRendererInteractionState: () => this.renderingHelper.updateRendererInteractionState(),
246
+ updateDragAttributes: () => this.renderingHelper.updateDragAttributes(),
247
+ clearCachedDragCells: () => this.renderingHelper.clearCachedDragCells(),
248
+ showContextMenu: (x, y) => this.showContextMenu(x, y),
249
+ startCellEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
250
+ });
251
+ // Store all created state objects
252
+ this.selectionState = result.selectionState;
253
+ this.keyboardNavState = result.keyboardNavState;
254
+ this.clipboardState = result.clipboardState;
255
+ this.undoRedoState = result.undoRedoState;
256
+ this.resizeState = result.resizeState;
257
+ this.fillHandleState = result.fillHandleState;
258
+ this.reorderState = result.reorderState;
259
+ this.marchingAnts = result.marchingAnts;
260
+ this.cellEditor = result.cellEditor;
261
+ this.contextMenu = result.contextMenu;
262
+ this.unsubscribes.push(...result.unsubscribes);
263
+ }
264
+ // Subscribe to state changes
265
+ this.unsubscribes.push(this.state.onStateChange(() => {
266
+ this.renderingHelper.renderAll();
208
267
  }));
209
- }
210
- // Initialize column pinning (always active, even without interaction)
211
- const flatCols = flattenColumns(options.columns);
212
- this.pinningState = new ColumnPinningState(options.pinnedColumns, flatCols);
213
- // Initialize row selection (always active if rowSelection is set)
214
- if (options.rowSelection && options.rowSelection !== 'none') {
215
- this.rowSelectionState = new RowSelectionState(options.rowSelection, options.getRowId);
216
- // Wire row selection API methods
217
- this.api.getSelectedRows = () => {
218
- return Array.from(this.rowSelectionState?.selectedRowIds ?? []);
219
- };
220
- this.api.selectAll = () => {
221
- const { items } = this.state.getProcessedItems();
222
- this.rowSelectionState?.handleSelectAll(true, items);
223
- };
224
- this.api.deselectAll = () => {
225
- const { items } = this.state.getProcessedItems();
226
- this.rowSelectionState?.handleSelectAll(false, items);
227
- };
228
- this.api.setSelectedRows = (rowIds) => {
229
- const { items } = this.state.getProcessedItems();
230
- this.rowSelectionState?.updateSelection(new Set(rowIds), items);
231
- };
232
- this.unsubscribes.push(this.rowSelectionState.onRowSelectionChange(() => {
233
- this.updateRendererInteractionState();
268
+ // Subscribe to pinning changes
269
+ this.unsubscribes.push(this.pinningState.onPinningChange(() => {
270
+ this.renderingHelper.updateRendererInteractionState();
234
271
  }));
235
- }
236
- // Initial render (must happen before interaction init so wrapper DOM exists)
237
- this.renderer.render();
238
- // Initialize interaction features if enabled (default: true for cellSelection)
239
- const shouldEnableInteraction = options.cellSelection !== false || options.editable === true;
240
- if (shouldEnableInteraction) {
241
- this.initializeInteraction();
242
- }
243
- // Subscribe to state changes
244
- this.unsubscribes.push(this.state.onStateChange(() => {
245
- this.renderAll();
246
- }));
247
- // Subscribe to pinning changes
248
- this.unsubscribes.push(this.pinningState.onPinningChange(() => {
249
- this.updateRendererInteractionState();
250
- }));
251
- // Subscribe to header filter state changes
252
- this.unsubscribes.push(this.headerFilterState.onChange(() => {
253
- this.renderHeaderFilterPopover();
254
- }));
255
- // Initialize virtual scrolling if configured
256
- if (options.virtualScroll?.enabled) {
257
- this.virtualScrollState = new VirtualScrollState(options.virtualScroll);
258
- this.virtualScrollState.observeContainer(this.tableContainer);
259
- this.renderer.setVirtualScrollState(this.virtualScrollState);
260
- // Wire scroll event on the table container
261
- const handleScroll = () => {
262
- this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
263
- };
264
- this.tableContainer.addEventListener('scroll', handleScroll, { passive: true });
265
- this.unsubscribes.push(() => {
266
- this.tableContainer.removeEventListener('scroll', handleScroll);
267
- });
268
- // Re-render when visible range changes
269
- this.unsubscribes.push(this.virtualScrollState.onRangeChanged(() => {
270
- this.updateRendererInteractionState();
272
+ // Subscribe to header filter state changes
273
+ this.unsubscribes.push(this.headerFilterState.onChange(() => {
274
+ this.renderingHelper.renderHeaderFilterPopover();
271
275
  }));
272
- // Wire scrollToRow API method
273
- this.api.scrollToRow = (index, opts) => {
274
- this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
275
- };
276
+ // Initialize virtual scrolling if configured
277
+ if (options.virtualScroll?.enabled) {
278
+ this.virtualScrollState = new VirtualScrollState(options.virtualScroll);
279
+ this.virtualScrollState.observeContainer(this.tableContainer);
280
+ this.renderer.setVirtualScrollState(this.virtualScrollState);
281
+ // Wire scroll event on the table container
282
+ const handleScroll = () => {
283
+ this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
284
+ };
285
+ this.tableContainer.addEventListener('scroll', handleScroll, { passive: true });
286
+ this.unsubscribes.push(() => {
287
+ this.tableContainer.removeEventListener('scroll', handleScroll);
288
+ });
289
+ // Re-render when visible range changes
290
+ this.unsubscribes.push(this.virtualScrollState.onRangeChanged(() => {
291
+ this.renderingHelper.updateRendererInteractionState();
292
+ }));
293
+ // Wire scrollToRow API method
294
+ this.api.scrollToRow = (index, opts) => {
295
+ this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
296
+ };
297
+ }
298
+ // Complete initial render (pagination, status bar, column chooser, sidebar, loading)
299
+ this.renderingHelper.renderAll();
276
300
  }
277
- // Complete initial render (pagination, status bar, column chooser, sidebar, loading)
278
- this.renderAll();
279
- }
280
- initializeInteraction() {
281
- const { editable } = this.options;
282
- const colOffset = this.rowSelectionState ? 1 : 0;
283
- // Create interaction states
284
- this.selectionState = new SelectionState();
285
- this.resizeState = new ColumnResizeState();
286
- this.contextMenu = new ContextMenu();
287
- this.cellEditor = new InlineCellEditor(this.tableContainer);
288
- // Undo/Redo (wraps onCellValueChanged if editable)
289
- const onCellValueChanged = this.options.onCellValueChanged;
290
- this.undoRedoState = new UndoRedoState(onCellValueChanged);
291
- // Clipboard
292
- this.clipboardState = new ClipboardState({
293
- items: [],
294
- visibleCols: [],
295
- colOffset,
296
- editable,
297
- onCellValueChanged: this.undoRedoState.getWrappedCallback(),
298
- }, () => this.selectionState?.activeCell ?? null, () => this.selectionState?.selectionRange ?? null);
299
- // Fill handle
300
- this.fillHandleState = new FillHandleState({
301
- items: [],
302
- visibleCols: [],
303
- editable,
304
- onCellValueChanged: this.undoRedoState.getWrappedCallback(),
305
- colOffset,
306
- beginBatch: () => this.undoRedoState?.beginBatch(),
307
- endBatch: () => this.undoRedoState?.endBatch(),
308
- }, () => this.selectionState?.selectionRange ?? null, (range) => {
309
- this.selectionState?.setSelectionRange(range);
310
- this.updateRendererInteractionState();
311
- }, (cell) => {
312
- this.selectionState?.setActiveCell(cell);
313
- });
314
- // Keyboard navigation
315
- this.keyboardNavState = new KeyboardNavState({
316
- items: [],
317
- visibleCols: [],
318
- colOffset,
319
- getRowId: this.state.getRowId,
320
- editable,
321
- onCellValueChanged: this.undoRedoState.getWrappedCallback(),
322
- onCopy: () => this.clipboardState?.handleCopy(),
323
- onCut: () => this.clipboardState?.handleCut(),
324
- onPaste: async () => { await this.clipboardState?.handlePaste(); },
325
- onUndo: () => this.undoRedoState?.undo(),
326
- onRedo: () => this.undoRedoState?.redo(),
327
- onContextMenu: (x, y) => this.showContextMenu(x, y),
328
- onStartEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
329
- clearClipboardRanges: () => this.clipboardState?.clearClipboardRanges(),
330
- }, () => this.selectionState?.activeCell ?? null, () => this.selectionState?.selectionRange ?? null, (cell) => this.selectionState?.setActiveCell(cell), (range) => this.selectionState?.setSelectionRange(range));
331
- // Subscribe to selection changes
332
- this.unsubscribes.push(this.selectionState.onSelectionChange(() => {
333
- this.updateRendererInteractionState();
334
- }));
335
- // Subscribe to clipboard range changes
336
- this.unsubscribes.push(this.clipboardState.onRangesChange(() => {
337
- this.updateRendererInteractionState();
338
- }));
339
- // Subscribe to column resize changes
340
- this.unsubscribes.push(this.resizeState.onColumnWidthChange(() => {
341
- this.updateRendererInteractionState();
342
- }));
343
- // Column reorder
344
- this.reorderState = new ColumnReorderState();
345
- this.unsubscribes.push(this.reorderState.onStateChange(({ isDragging, dropIndicatorX }) => {
346
- this.renderer.updateDropIndicator(dropIndicatorX, isDragging);
347
- }));
348
- this.unsubscribes.push(this.reorderState.onReorder(({ columnOrder }) => {
349
- this.state.setColumnOrder(columnOrder);
350
- }));
351
- // Attach keyboard handler to wrapper
352
- const wrapper = this.renderer.getWrapperElement();
353
- if (wrapper) {
354
- wrapper.addEventListener('keydown', this.keyboardNavState.handleKeyDown);
355
- this.keyboardNavState.setWrapperRef(wrapper);
356
- this.fillHandleState.setWrapperRef(wrapper);
357
- // Initialize marching ants overlay
358
- this.marchingAnts = new MarchingAntsOverlay(wrapper, colOffset);
301
+ catch (e) {
302
+ this.destroy();
303
+ throw e;
359
304
  }
360
- // Attach global mouse handlers for resize and drag
361
- this.attachGlobalHandlers();
362
- // Set initial interaction state on renderer
363
- this.updateRendererInteractionState();
364
305
  }
365
- attachGlobalHandlers() {
366
- let resizing = false;
367
- const handleMouseMove = (e) => {
368
- if (resizing && this.resizeState) {
369
- const newWidth = this.resizeState.updateResize(e.clientX);
370
- if (newWidth !== null && this.resizeState.resizingColumnId) {
371
- this.layoutState.setColumnOverride(this.resizeState.resizingColumnId, newWidth);
372
- this.updateRendererInteractionState();
373
- }
374
- }
375
- if (this.selectionState?.isDragging) {
376
- const target = e.target;
377
- if (target.tagName === 'TD') {
378
- const rowIndex = parseInt(target.getAttribute('data-row-index') ?? '-1', 10);
379
- const colIndex = parseInt(target.getAttribute('data-col-index') ?? '-1', 10);
380
- if (rowIndex >= 0 && colIndex >= 0) {
381
- this.selectionState.updateDrag(rowIndex, colIndex, () => this.updateDragAttributes());
382
- }
383
- }
384
- }
385
- };
386
- const handleMouseUp = (e) => {
387
- if (resizing && this.resizeState) {
388
- const colId = this.resizeState.resizingColumnId;
389
- this.resizeState.endResize(e.clientX);
390
- if (colId) {
391
- const width = this.resizeState.getColumnWidth(colId);
392
- if (width)
393
- this.layoutState.setColumnOverride(colId, width);
394
- }
395
- resizing = false;
396
- document.body.style.cursor = '';
397
- this.updateRendererInteractionState();
398
- }
399
- if (this.selectionState?.isDragging) {
400
- this.selectionState.endDrag();
401
- }
402
- };
403
- const handleResizeStart = (columnId, clientX, currentWidth) => {
404
- resizing = true;
405
- document.body.style.cursor = 'col-resize';
406
- this.resizeState?.startResize(columnId, clientX, currentWidth);
306
+ /** Creates the OGridRenderingContext that bridges OGrid state to the rendering helper. */
307
+ createRenderingHelper() {
308
+ const liveGetter = (getter) => ({ get: getter, enumerable: true, configurable: true });
309
+ const ctx = {
310
+ options: this.options,
311
+ state: this.state,
312
+ renderer: this.renderer,
313
+ pagination: this.pagination,
314
+ statusBar: this.statusBar,
315
+ columnChooser: this.columnChooser,
316
+ layoutState: this.layoutState,
317
+ tableContainer: this.tableContainer,
318
+ headerFilterState: this.headerFilterState,
319
+ headerFilterComponent: this.headerFilterComponent,
320
+ filterConfigs: this.filterConfigs,
321
+ setLoadingOverlay: (el) => { this.loadingOverlay = el; },
322
+ handleCellClick: (rowIndex, colIndex) => this.handleCellClick(rowIndex, colIndex),
323
+ handleCellMouseDown: (rowIndex, colIndex, e) => this.handleCellMouseDown(rowIndex, colIndex, e),
324
+ handleCellContextMenu: (rowIndex, colIndex, e) => this.handleCellContextMenu(rowIndex, colIndex, e),
325
+ startCellEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
326
+ showContextMenu: (x, y) => this.showContextMenu(x, y),
407
327
  };
408
- document.addEventListener('mousemove', handleMouseMove, { passive: true });
409
- document.addEventListener('mouseup', handleMouseUp, { passive: true });
410
- // Store references for cleanup
411
- this.unsubscribes.push(() => {
412
- document.removeEventListener('mousemove', handleMouseMove);
413
- document.removeEventListener('mouseup', handleMouseUp);
414
- });
415
- // Pass resize handler to renderer
416
- this.renderer.setInteractionState({
417
- activeCell: null,
418
- selectionRange: null,
419
- copyRange: null,
420
- cutRange: null,
421
- editingCell: null,
422
- columnWidths: {},
423
- onResizeStart: handleResizeStart,
328
+ Object.defineProperties(ctx, {
329
+ selectionState: liveGetter(() => this.selectionState),
330
+ keyboardNavState: liveGetter(() => this.keyboardNavState),
331
+ clipboardState: liveGetter(() => this.clipboardState),
332
+ undoRedoState: liveGetter(() => this.undoRedoState),
333
+ resizeState: liveGetter(() => this.resizeState),
334
+ fillHandleState: liveGetter(() => this.fillHandleState),
335
+ rowSelectionState: liveGetter(() => this.rowSelectionState),
336
+ pinningState: liveGetter(() => this.pinningState),
337
+ reorderState: liveGetter(() => this.reorderState),
338
+ virtualScrollState: liveGetter(() => this.virtualScrollState),
339
+ marchingAnts: liveGetter(() => this.marchingAnts),
340
+ cellEditor: liveGetter(() => this.cellEditor),
341
+ sideBarState: liveGetter(() => this.sideBarState),
342
+ sideBarComponent: liveGetter(() => this.sideBarComponent),
343
+ loadingOverlay: liveGetter(() => this.loadingOverlay),
424
344
  });
425
- }
426
- updateRendererInteractionState() {
427
- if (!this.selectionState || !this.clipboardState || !this.resizeState)
428
- return;
429
- const { items } = this.state.getProcessedItems();
430
- const visibleCols = this.state.visibleColumnDefs;
431
- // Compute pinning offsets
432
- const columnWidths = this.layoutState.getAllColumnWidths();
433
- const leftOffsets = this.pinningState?.computeLeftOffsets(visibleCols, columnWidths, 120, !!this.rowSelectionState, 40, !!this.options.showRowNumbers) ?? {};
434
- const rightOffsets = this.pinningState?.computeRightOffsets(visibleCols, columnWidths, 120) ?? {};
435
- this.renderer.setInteractionState({
436
- activeCell: this.selectionState.activeCell,
437
- selectionRange: this.selectionState.selectionRange,
438
- copyRange: this.clipboardState.copyRange,
439
- cutRange: this.clipboardState.cutRange,
440
- editingCell: this.cellEditor?.getEditingCell() ?? null,
441
- columnWidths,
442
- onCellClick: (rowIndex, colIndex) => this.handleCellClick(rowIndex, colIndex),
443
- onCellMouseDown: (rowIndex, colIndex, e) => this.handleCellMouseDown(rowIndex, colIndex, e),
444
- onCellDoubleClick: (rowIndex, colIndex, rowId, columnId) => this.startCellEdit(rowId, columnId),
445
- onCellContextMenu: (rowIndex, colIndex, e) => this.handleCellContextMenu(rowIndex, colIndex, e),
446
- onResizeStart: this.renderer['interactionState']?.onResizeStart,
447
- // Fill handle
448
- onFillHandleMouseDown: this.options.editable !== false ? (e) => this.fillHandleState?.startFillDrag(e) : undefined,
449
- // Row selection
450
- rowSelectionMode: this.rowSelectionState?.rowSelection ?? 'none',
451
- selectedRowIds: this.rowSelectionState?.selectedRowIds,
452
- onRowCheckboxChange: (rowId, checked, rowIndex, shiftKey) => {
453
- this.rowSelectionState?.handleRowCheckboxChange(rowId, checked, rowIndex, shiftKey, items);
454
- },
455
- onSelectAll: (checked) => {
456
- this.rowSelectionState?.handleSelectAll(checked, items);
457
- },
458
- allSelected: this.rowSelectionState?.isAllSelected(items),
459
- someSelected: this.rowSelectionState?.isSomeSelected(items),
460
- // Row numbers
461
- showRowNumbers: this.options.showRowNumbers,
462
- // Column pinning
463
- pinnedColumns: this.pinningState?.pinnedColumns,
464
- leftOffsets,
465
- rightOffsets,
466
- // Column reorder
467
- onColumnReorderStart: this.reorderState ? (columnId, event) => {
468
- const tableEl = this.renderer.getTableElement();
469
- if (!tableEl)
470
- return;
471
- this.reorderState?.startDrag(columnId, event, visibleCols, this.state.columnOrder, this.pinningState?.pinnedColumns, tableEl);
472
- } : undefined,
473
- });
474
- this.renderer.update();
475
- // Update marching ants overlay
476
- this.marchingAnts?.update(this.selectionState.selectionRange, this.clipboardState.copyRange, this.clipboardState.cutRange, this.layoutVersion);
477
- }
478
- updateDragAttributes() {
479
- const wrapper = this.renderer.getWrapperElement();
480
- if (!wrapper || !this.selectionState)
481
- return;
482
- const range = this.selectionState.getDragRange();
483
- if (!range)
484
- return;
485
- const norm = normalizeSelectionRange(range);
486
- const anchor = this.selectionState.dragAnchor;
487
- const minR = norm.startRow;
488
- const maxR = norm.endRow;
489
- const minC = norm.startCol;
490
- const maxC = norm.endCol;
491
- const cells = wrapper.querySelectorAll('td[data-row-index][data-col-index]');
492
- for (let _i = 0; _i < cells.length; _i++) {
493
- const cell = cells[_i];
494
- const el = cell;
495
- const rowIndex = parseInt(el.getAttribute('data-row-index') ?? '-1', 10);
496
- const colIndex = parseInt(el.getAttribute('data-col-index') ?? '-1', 10);
497
- if (isInSelectionRange(norm, rowIndex, colIndex)) {
498
- el.setAttribute('data-drag-range', 'true');
499
- // Anchor cell (white background)
500
- const isAnchor = anchor && rowIndex === anchor.rowIndex && colIndex === anchor.columnIndex;
501
- if (isAnchor) {
502
- el.setAttribute('data-drag-anchor', '');
503
- }
504
- else {
505
- el.removeAttribute('data-drag-anchor');
506
- }
507
- // Edge borders via inset box-shadow
508
- const shadows = [];
509
- if (rowIndex === minR)
510
- shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
511
- if (rowIndex === maxR)
512
- shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
513
- if (colIndex === minC)
514
- shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
515
- if (colIndex === maxC)
516
- shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
517
- el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
518
- }
519
- else {
520
- el.removeAttribute('data-drag-range');
521
- el.removeAttribute('data-drag-anchor');
522
- if (el.style.boxShadow)
523
- el.style.boxShadow = '';
524
- }
525
- }
345
+ return new OGridRendering(ctx);
526
346
  }
527
347
  handleCellClick(rowIndex, colIndex) {
528
348
  if (!this.selectionState)
@@ -537,7 +357,7 @@ export class OGrid {
537
357
  e.preventDefault();
538
358
  this.selectionState.startDrag(rowIndex, colIndex);
539
359
  // Apply drag attributes immediately for instant visual feedback on the initial cell
540
- setTimeout(() => this.updateDragAttributes(), 0);
360
+ setTimeout(() => this.renderingHelper.updateDragAttributes(), 0);
541
361
  }
542
362
  handleCellContextMenu(rowIndex, colIndex, e) {
543
363
  e.preventDefault();
@@ -546,7 +366,7 @@ export class OGrid {
546
366
  // Set active cell if not already set
547
367
  if (!this.selectionState.activeCell || this.selectionState.activeCell.rowIndex !== rowIndex || this.selectionState.activeCell.columnIndex !== colIndex) {
548
368
  this.selectionState.setActiveCell({ rowIndex, columnIndex: colIndex });
549
- this.updateRendererInteractionState();
369
+ this.renderingHelper.updateRendererInteractionState();
550
370
  }
551
371
  this.showContextMenu(e.clientX, e.clientY);
552
372
  }
@@ -554,24 +374,24 @@ export class OGrid {
554
374
  if (!this.contextMenu || !this.clipboardState || !this.undoRedoState || !this.keyboardNavState || !this.selectionState)
555
375
  return;
556
376
  this.contextMenu.show(x, y, {
557
- onCopy: () => this.clipboardState.handleCopy(),
558
- onCut: () => this.clipboardState.handleCut(),
559
- onPaste: () => void this.clipboardState.handlePaste(),
377
+ onCopy: () => this.clipboardState?.handleCopy(),
378
+ onCut: () => this.clipboardState?.handleCut(),
379
+ onPaste: () => void this.clipboardState?.handlePaste(),
560
380
  onSelectAll: () => {
561
381
  const { items } = this.state.getProcessedItems();
562
382
  const visibleCols = this.state.visibleColumnDefs;
563
383
  if (items.length > 0 && visibleCols.length > 0) {
564
- this.selectionState.setSelectionRange({
384
+ this.selectionState?.setSelectionRange({
565
385
  startRow: 0,
566
386
  startCol: 0,
567
387
  endRow: items.length - 1,
568
388
  endCol: visibleCols.length - 1,
569
389
  });
570
- this.updateRendererInteractionState();
390
+ this.renderingHelper.updateRendererInteractionState();
571
391
  }
572
392
  },
573
- onUndo: () => this.undoRedoState.undo(),
574
- onRedo: () => this.undoRedoState.redo(),
393
+ onUndo: () => this.undoRedoState?.undo(),
394
+ onRedo: () => this.undoRedoState?.redo(),
575
395
  }, this.undoRedoState.canUndo, this.undoRedoState.canRedo, this.selectionState.selectionRange);
576
396
  }
577
397
  startCellEdit(rowId, columnId) {
@@ -593,27 +413,30 @@ export class OGrid {
593
413
  const cell = row.querySelector(`td[data-column-id="${columnId}"]`);
594
414
  if (!cell)
595
415
  return;
596
- const onCommit = (rid, cid, value) => {
597
- const itm = items.find((i) => this.state.getRowId(i) === rid);
416
+ const rowIndex = items.indexOf(item);
417
+ const onCommit = (_rid, cid, value) => {
418
+ // Use the already-resolved item and look up the committed column
598
419
  const col = visibleCols.find((c) => c.columnId === cid);
599
- if (!itm || !col)
420
+ if (!col)
600
421
  return;
601
- const oldValue = itm[cid];
602
- itm[cid] = value;
603
- const wrapped = this.undoRedoState.getWrappedCallback();
422
+ // NOTE: Direct mutation on the item reference. This updates the in-memory data
423
+ // so subsequent renders reflect the new value before the consumer calls setRowData.
424
+ const oldValue = item[cid];
425
+ item[cid] = value;
426
+ const wrapped = this.undoRedoState?.getWrappedCallback();
604
427
  if (wrapped) {
605
428
  wrapped({
606
- item: itm,
429
+ item,
607
430
  columnId: cid,
608
431
  oldValue,
609
432
  newValue: value,
610
- rowIndex: items.indexOf(itm),
433
+ rowIndex,
611
434
  });
612
435
  }
613
- this.updateRendererInteractionState();
436
+ this.renderingHelper.updateRendererInteractionState();
614
437
  };
615
438
  const onCancel = () => {
616
- this.updateRendererInteractionState();
439
+ this.renderingHelper.updateRendererInteractionState();
617
440
  };
618
441
  const onAfterCommit = () => {
619
442
  // After Enter-commit, move the active cell down one row (Excel-style behavior)
@@ -666,150 +489,13 @@ export class OGrid {
666
489
  this.headerFilterState.setFilterOptions(this.state.filterOptions);
667
490
  this.headerFilterState.open(columnId, config, headerEl, tempPopover);
668
491
  }
669
- renderHeaderFilterPopover() {
670
- const openId = this.headerFilterState.openColumnId;
671
- if (!openId) {
672
- this.headerFilterComponent.cleanup();
673
- return;
674
- }
675
- const config = this.filterConfigs.get(openId);
676
- if (!config)
677
- return;
678
- this.headerFilterComponent.render(config);
679
- // Update the popover element reference for click-outside detection
680
- const popoverEl = document.querySelector('.ogrid-header-filter-popover');
681
- if (popoverEl) {
682
- this.headerFilterState._popoverEl = popoverEl;
683
- }
684
- }
685
- renderSideBar() {
686
- if (!this.sideBarComponent || !this.sideBarState)
687
- return;
688
- const columns = this.state.columns.map(c => ({
689
- columnId: c.columnId,
690
- name: c.name,
691
- required: c.required === true,
692
- }));
693
- const filterableColumns = this.state.columns
694
- .filter(c => c.filterable && typeof c.filterable === 'object' && c.filterable.type)
695
- .map(c => ({
696
- columnId: c.columnId,
697
- name: c.name,
698
- filterField: c.filterable.filterField ?? c.columnId,
699
- filterType: c.filterable.type,
700
- }));
701
- this.sideBarComponent.setConfig({
702
- columns,
703
- visibleColumns: this.state.visibleColumns,
704
- onVisibilityChange: (columnKey, visible) => {
705
- const next = new Set(this.state.visibleColumns);
706
- if (visible)
707
- next.add(columnKey);
708
- else
709
- next.delete(columnKey);
710
- this.state.setVisibleColumns(next);
711
- },
712
- onSetVisibleColumns: (cols) => this.state.setVisibleColumns(cols),
713
- filterableColumns,
714
- filters: this.state.filters,
715
- onFilterChange: (key, value) => this.state.setFilter(key, value),
716
- filterOptions: this.state.filterOptions,
717
- });
718
- this.sideBarComponent.render();
719
- }
720
- renderLoadingOverlay() {
721
- if (this.state.isLoading) {
722
- // Ensure the container has minimum height during loading so overlay is visible
723
- const { items } = this.state.getProcessedItems();
724
- this.tableContainer.style.minHeight = (!items || items.length === 0) ? '200px' : '';
725
- if (!this.loadingOverlay) {
726
- this.loadingOverlay = document.createElement('div');
727
- this.loadingOverlay.className = 'ogrid-loading-overlay';
728
- this.loadingOverlay.style.position = 'absolute';
729
- this.loadingOverlay.style.top = '0';
730
- this.loadingOverlay.style.left = '0';
731
- this.loadingOverlay.style.right = '0';
732
- this.loadingOverlay.style.bottom = '0';
733
- this.loadingOverlay.style.display = 'flex';
734
- this.loadingOverlay.style.alignItems = 'center';
735
- this.loadingOverlay.style.justifyContent = 'center';
736
- this.loadingOverlay.style.background = 'var(--ogrid-loading-overlay, rgba(255, 255, 255, 0.7))';
737
- this.loadingOverlay.style.zIndex = '100';
738
- const spinner = document.createElement('div');
739
- spinner.className = 'ogrid-loading-spinner';
740
- spinner.textContent = 'Loading...';
741
- this.loadingOverlay.appendChild(spinner);
742
- }
743
- if (!this.tableContainer.contains(this.loadingOverlay)) {
744
- this.tableContainer.appendChild(this.loadingOverlay);
745
- }
746
- }
747
- else {
748
- this.tableContainer.style.minHeight = '';
749
- if (this.loadingOverlay && this.tableContainer.contains(this.loadingOverlay)) {
750
- this.loadingOverlay.remove();
751
- }
752
- }
753
- }
754
- renderAll() {
755
- // Increment layout version to trigger marching ants re-measurement
756
- this.layoutVersion++;
757
- const colOffset = this.rowSelectionState ? 1 : 0;
758
- // Update header filter state with current filters and options
759
- this.headerFilterState.setFilters(this.state.filters);
760
- this.headerFilterState.setFilterOptions(this.state.filterOptions);
761
- // Update interaction states with current data
762
- if (this.keyboardNavState && this.clipboardState) {
763
- const { items } = this.state.getProcessedItems();
764
- const visibleCols = this.state.visibleColumnDefs;
765
- this.keyboardNavState.updateParams({
766
- items,
767
- visibleCols: visibleCols,
768
- colOffset,
769
- getRowId: this.state.getRowId,
770
- editable: this.options.editable,
771
- onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
772
- onCopy: () => this.clipboardState?.handleCopy(),
773
- onCut: () => this.clipboardState?.handleCut(),
774
- onPaste: async () => { await this.clipboardState?.handlePaste(); },
775
- onUndo: () => this.undoRedoState?.undo(),
776
- onRedo: () => this.undoRedoState?.redo(),
777
- onContextMenu: (x, y) => this.showContextMenu(x, y),
778
- onStartEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
779
- clearClipboardRanges: () => this.clipboardState?.clearClipboardRanges(),
780
- });
781
- this.clipboardState.updateParams({
782
- items,
783
- visibleCols: visibleCols,
784
- colOffset,
785
- editable: this.options.editable,
786
- onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
787
- });
788
- // Update fill handle params
789
- this.fillHandleState?.updateParams({
790
- items,
791
- visibleCols: visibleCols,
792
- editable: this.options.editable,
793
- onCellValueChanged: this.undoRedoState?.getWrappedCallback(),
794
- colOffset,
795
- beginBatch: () => this.undoRedoState?.beginBatch(),
796
- endBatch: () => this.undoRedoState?.endBatch(),
797
- });
798
- // Update renderer interaction state before rendering
799
- this.updateRendererInteractionState();
800
- }
801
- else {
802
- this.renderer.update();
803
- }
804
- const { totalCount } = this.state.getProcessedItems();
805
- // Update virtual scroll with current total row count
806
- this.virtualScrollState?.setTotalRows(totalCount);
807
- this.pagination.render(totalCount, this.options.pageSizeOptions);
808
- this.statusBar.render({ totalCount });
809
- this.columnChooser.render();
810
- this.renderSideBar();
811
- this.renderLoadingOverlay();
812
- }
492
+ // Rendering methods delegated to OGridRendering helper:
493
+ // - updateRendererInteractionState() -> this.renderingHelper.updateRendererInteractionState()
494
+ // - updateDragAttributes() -> this.renderingHelper.updateDragAttributes()
495
+ // - renderAll() -> this.renderingHelper.renderAll()
496
+ // - renderHeaderFilterPopover() -> this.renderingHelper.renderHeaderFilterPopover()
497
+ // - renderSideBar() -> this.renderingHelper.renderSideBar()
498
+ // - renderLoadingOverlay() -> this.renderingHelper.renderLoadingOverlay()
813
499
  /** Subscribe to grid events. */
814
500
  on(event, handler) {
815
501
  this.events.on(event, handler);