@alaarab/ogrid-js 2.1.2 → 2.1.4

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/index.js +6343 -32
  2. package/package.json +7 -5
  3. package/dist/esm/OGrid.js +0 -578
  4. package/dist/esm/OGridEventWiring.js +0 -178
  5. package/dist/esm/OGridRendering.js +0 -269
  6. package/dist/esm/components/ColumnChooser.js +0 -91
  7. package/dist/esm/components/ContextMenu.js +0 -125
  8. package/dist/esm/components/HeaderFilter.js +0 -281
  9. package/dist/esm/components/InlineCellEditor.js +0 -434
  10. package/dist/esm/components/MarchingAntsOverlay.js +0 -156
  11. package/dist/esm/components/PaginationControls.js +0 -85
  12. package/dist/esm/components/SideBar.js +0 -353
  13. package/dist/esm/components/StatusBar.js +0 -34
  14. package/dist/esm/renderer/TableRenderer.js +0 -846
  15. package/dist/esm/state/ClipboardState.js +0 -111
  16. package/dist/esm/state/ColumnPinningState.js +0 -82
  17. package/dist/esm/state/ColumnReorderState.js +0 -135
  18. package/dist/esm/state/ColumnResizeState.js +0 -55
  19. package/dist/esm/state/EventEmitter.js +0 -28
  20. package/dist/esm/state/FillHandleState.js +0 -206
  21. package/dist/esm/state/GridState.js +0 -324
  22. package/dist/esm/state/HeaderFilterState.js +0 -213
  23. package/dist/esm/state/KeyboardNavState.js +0 -216
  24. package/dist/esm/state/RowSelectionState.js +0 -72
  25. package/dist/esm/state/SelectionState.js +0 -109
  26. package/dist/esm/state/SideBarState.js +0 -41
  27. package/dist/esm/state/TableLayoutState.js +0 -97
  28. package/dist/esm/state/UndoRedoState.js +0 -71
  29. package/dist/esm/state/VirtualScrollState.js +0 -128
  30. package/dist/esm/types/columnTypes.js +0 -1
  31. package/dist/esm/types/gridTypes.js +0 -1
  32. package/dist/esm/types/index.js +0 -2
  33. package/dist/esm/utils/debounce.js +0 -2
  34. package/dist/esm/utils/getCellCoordinates.js +0 -15
  35. package/dist/esm/utils/index.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-js",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "OGrid vanilla JS – framework-free data grid with sorting, filtering, pagination, and spreadsheet-style editing.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,12 +9,12 @@
9
9
  ".": {
10
10
  "types": "./dist/types/index.d.ts",
11
11
  "import": "./dist/esm/index.js",
12
- "require": "./dist/esm/index.js"
12
+ "default": "./dist/esm/index.js"
13
13
  },
14
14
  "./styles": "./dist/styles/ogrid.css"
15
15
  },
16
16
  "scripts": {
17
- "build": "rimraf dist && tsc -p tsconfig.build.json && mkdir -p dist/styles && cp styles/ogrid.css dist/styles/ogrid.css",
17
+ "build": "rimraf dist && tsup && tsc -p tsconfig.build.json && mkdir -p dist/styles && cp styles/ogrid.css dist/styles/ogrid.css",
18
18
  "test": "jest"
19
19
  },
20
20
  "keywords": [
@@ -36,9 +36,11 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@alaarab/ogrid-core": "2.1.2"
39
+ "@alaarab/ogrid-core": "2.1.4"
40
40
  },
41
- "sideEffects": ["**/*.css"],
41
+ "sideEffects": [
42
+ "**/*.css"
43
+ ],
42
44
  "publishConfig": {
43
45
  "access": "public"
44
46
  },
package/dist/esm/OGrid.js DELETED
@@ -1,578 +0,0 @@
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 { TableLayoutState } from './state/TableLayoutState';
11
- import { RowSelectionState } from './state/RowSelectionState';
12
- import { ColumnPinningState } from './state/ColumnPinningState';
13
- import { VirtualScrollState } from './state/VirtualScrollState';
14
- import { EventEmitter } from './state/EventEmitter';
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
- */
24
- const OGRID_THEME_CSS = `
25
- .ogrid-drag-target { box-shadow: inset 0 0 0 1px var(--ogrid-accent, #0078d4); }
26
- :root {
27
- --ogrid-bg: #ffffff;
28
- --ogrid-fg: rgba(0, 0, 0, 0.87);
29
- --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
30
- --ogrid-fg-muted: rgba(0, 0, 0, 0.5);
31
- --ogrid-border: rgba(0, 0, 0, 0.12);
32
- --ogrid-header-bg: rgba(0, 0, 0, 0.04);
33
- --ogrid-hover-bg: rgba(0, 0, 0, 0.04);
34
- --ogrid-selected-row-bg: #e6f0fb;
35
- --ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
36
- --ogrid-range-bg: rgba(33, 115, 70, 0.12);
37
- --ogrid-accent: #0078d4;
38
- --ogrid-selection-color: #217346;
39
- --ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
40
- --ogrid-bg-subtle: #f3f2f1;
41
- --ogrid-bg-hover: rgba(0, 0, 0, 0.04);
42
- --ogrid-bg-selected: #e6f0fb;
43
- --ogrid-bg-selected-hover: #dae8f8;
44
- --ogrid-bg-range: rgba(33, 115, 70, 0.12);
45
- --ogrid-muted: rgba(0, 0, 0, 0.5);
46
- --ogrid-selection: #217346;
47
- --ogrid-primary: #217346;
48
- --ogrid-primary-fg: #fff;
49
- --ogrid-loading-bg: rgba(255, 255, 255, 0.7);
50
- --ogrid-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
51
- }
52
- [data-theme='dark'] {
53
- --ogrid-bg: #1e1e1e;
54
- --ogrid-fg: rgba(255, 255, 255, 0.87);
55
- --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
56
- --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
57
- --ogrid-border: rgba(255, 255, 255, 0.12);
58
- --ogrid-header-bg: rgba(255, 255, 255, 0.06);
59
- --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
60
- --ogrid-selected-row-bg: #1a3a5c;
61
- --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
62
- --ogrid-range-bg: rgba(46, 160, 67, 0.15);
63
- --ogrid-accent: #4da6ff;
64
- --ogrid-selection-color: #2ea043;
65
- --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
66
- --ogrid-bg-subtle: #2a2a2a;
67
- --ogrid-bg-hover: rgba(255, 255, 255, 0.08);
68
- --ogrid-bg-selected: #1a3a5c;
69
- --ogrid-bg-selected-hover: #1f426b;
70
- --ogrid-bg-range: rgba(46, 160, 67, 0.15);
71
- --ogrid-muted: rgba(255, 255, 255, 0.5);
72
- --ogrid-selection: #2ea043;
73
- --ogrid-primary: #2ea043;
74
- --ogrid-primary-fg: #fff;
75
- --ogrid-loading-bg: rgba(0, 0, 0, 0.7);
76
- --ogrid-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
77
- }
78
- @media (prefers-color-scheme: dark) {
79
- :root:not([data-theme='light']) {
80
- --ogrid-bg: #1e1e1e;
81
- --ogrid-fg: rgba(255, 255, 255, 0.87);
82
- --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
83
- --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
84
- --ogrid-border: rgba(255, 255, 255, 0.12);
85
- --ogrid-header-bg: rgba(255, 255, 255, 0.06);
86
- --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
87
- --ogrid-selected-row-bg: #1a3a5c;
88
- --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
89
- --ogrid-range-bg: rgba(46, 160, 67, 0.15);
90
- --ogrid-accent: #4da6ff;
91
- --ogrid-selection-color: #2ea043;
92
- --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
93
- --ogrid-bg-subtle: #2a2a2a;
94
- --ogrid-bg-hover: rgba(255, 255, 255, 0.08);
95
- --ogrid-bg-selected: #1a3a5c;
96
- --ogrid-bg-selected-hover: #1f426b;
97
- --ogrid-bg-range: rgba(46, 160, 67, 0.15);
98
- --ogrid-muted: rgba(255, 255, 255, 0.5);
99
- --ogrid-selection: #2ea043;
100
- --ogrid-primary: #2ea043;
101
- --ogrid-primary-fg: #fff;
102
- --ogrid-loading-bg: rgba(0, 0, 0, 0.7);
103
- --ogrid-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
104
- }
105
- }
106
- `;
107
- export class OGrid {
108
- constructor(container, options) {
109
- // Sidebar
110
- this.sideBarState = null;
111
- this.sideBarComponent = null;
112
- this.sideBarContainer = null;
113
- this.filterConfigs = new Map();
114
- // Loading overlay
115
- this.loadingOverlay = null;
116
- // Body area (holds sidebar + table)
117
- this.bodyArea = null;
118
- // Interaction states
119
- this.selectionState = null;
120
- this.keyboardNavState = null;
121
- this.clipboardState = null;
122
- this.undoRedoState = null;
123
- this.resizeState = null;
124
- this.fillHandleState = null;
125
- this.rowSelectionState = null;
126
- this.pinningState = null;
127
- this.reorderState = null;
128
- this.virtualScrollState = null;
129
- this.marchingAnts = null;
130
- this.cellEditor = null;
131
- this.contextMenu = null;
132
- this.events = new EventEmitter();
133
- this.unsubscribes = [];
134
- this.isFullScreen = false;
135
- this.fullscreenBtn = null;
136
- this.options = options;
137
- this.state = new GridState(options);
138
- this.api = this.state.getApi();
139
- this.eventWiringHelper = new OGridEventWiring();
140
- // Inject theme CSS variables (light + dark) once per page
141
- injectGlobalStyles('ogrid-theme-vars', OGRID_THEME_CSS);
142
- // Build layout
143
- this.containerEl = document.createElement('div');
144
- this.containerEl.className = 'ogrid-container';
145
- // Toolbar
146
- this.toolbarEl = document.createElement('div');
147
- this.toolbarEl.className = 'ogrid-toolbar';
148
- // Left spacer keeps column chooser on the right via justify-content: space-between
149
- const toolbarSpacer = document.createElement('div');
150
- this.toolbarEl.appendChild(toolbarSpacer);
151
- // Fullscreen toggle button
152
- if (options.fullScreen) {
153
- const toolbarRight = document.createElement('div');
154
- toolbarRight.style.display = 'flex';
155
- toolbarRight.style.alignItems = 'center';
156
- toolbarRight.style.gap = '8px';
157
- this.fullscreenBtn = document.createElement('button');
158
- this.fullscreenBtn.type = 'button';
159
- this.fullscreenBtn.className = 'ogrid-fullscreen-btn';
160
- this.fullscreenBtn.title = 'Fullscreen';
161
- this.fullscreenBtn.setAttribute('aria-label', 'Fullscreen');
162
- this.fullscreenBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="10 2 14 2 14 6"/><polyline points="6 14 2 14 2 10"/><line x1="14" y1="2" x2="10" y2="6"/><line x1="2" y1="14" x2="6" y2="10"/></svg>';
163
- this.fullscreenBtn.addEventListener('click', () => this.toggleFullScreen());
164
- toolbarRight.appendChild(this.fullscreenBtn);
165
- this.toolbarEl.appendChild(toolbarRight);
166
- // ESC key to exit fullscreen
167
- const handleEscKey = (e) => {
168
- if (e.key === 'Escape' && this.isFullScreen)
169
- this.toggleFullScreen();
170
- };
171
- document.addEventListener('keydown', handleEscKey);
172
- this.unsubscribes.push(() => document.removeEventListener('keydown', handleEscKey));
173
- }
174
- this.containerEl.appendChild(this.toolbarEl);
175
- // Body area (holds sidebar + table, side by side)
176
- this.bodyArea = document.createElement('div');
177
- this.bodyArea.className = 'ogrid-body-area';
178
- this.bodyArea.style.display = 'flex';
179
- this.bodyArea.style.flex = '1';
180
- this.bodyArea.style.overflow = 'hidden';
181
- this.containerEl.appendChild(this.bodyArea);
182
- // Table container (inside body area)
183
- this.tableContainer = document.createElement('div');
184
- this.tableContainer.className = 'ogrid-table-container';
185
- this.tableContainer.style.flex = '1';
186
- this.tableContainer.style.overflow = 'auto';
187
- this.tableContainer.style.position = 'relative';
188
- this.bodyArea.appendChild(this.tableContainer);
189
- // Status bar container
190
- this.statusBarContainer = document.createElement('div');
191
- this.statusBarContainer.className = 'ogrid-status-bar-container';
192
- this.containerEl.appendChild(this.statusBarContainer);
193
- // Pagination container
194
- this.paginationContainer = document.createElement('div');
195
- this.paginationContainer.className = 'ogrid-pagination-container';
196
- this.containerEl.appendChild(this.paginationContainer);
197
- container.appendChild(this.containerEl);
198
- // Create layout state (measures container, tracks column sizing)
199
- this.layoutState = new TableLayoutState();
200
- this.layoutState.observeContainer(this.tableContainer);
201
- // Create sub-components
202
- this.renderer = new TableRenderer(this.tableContainer, this.state);
203
- this.pagination = new PaginationControls(this.paginationContainer, this.state);
204
- this.statusBar = new StatusBar(this.statusBarContainer);
205
- this.columnChooser = new ColumnChooser(this.toolbarEl, this.state);
206
- // Initialize header filter state
207
- this.headerFilterState = new HeaderFilterState((key, value) => {
208
- this.state.setFilter(key, value);
209
- });
210
- this.headerFilterComponent = new HeaderFilter(this.headerFilterState);
211
- this.buildFilterConfigs();
212
- try {
213
- // Pass filter config to renderer for filter icons in headers
214
- this.renderer.setHeaderFilterState(this.headerFilterState, this.filterConfigs);
215
- this.renderer.setOnFilterIconClick((columnId, headerEl) => {
216
- this.handleFilterIconClick(columnId, headerEl);
217
- });
218
- // Initialize sidebar if configured
219
- if (options.sideBar) {
220
- this.sideBarState = new SideBarState(options.sideBar);
221
- this.sideBarContainer = document.createElement('div');
222
- this.sideBarContainer.className = 'ogrid-sidebar-container';
223
- this.sideBarComponent = new SideBar(this.sideBarContainer, this.sideBarState);
224
- if (this.bodyArea) {
225
- if (this.sideBarState.position === 'left') {
226
- this.bodyArea.insertBefore(this.sideBarContainer, this.tableContainer);
227
- }
228
- else {
229
- this.bodyArea.appendChild(this.sideBarContainer);
230
- }
231
- }
232
- this.unsubscribes.push(this.sideBarState.onChange(() => {
233
- this.renderingHelper.renderSideBar();
234
- }));
235
- }
236
- // Initialize column pinning (always active, even without interaction)
237
- const flatCols = flattenColumns(options.columns);
238
- this.pinningState = new ColumnPinningState(options.pinnedColumns, flatCols);
239
- // Initialize row selection (always active if rowSelection is set)
240
- if (options.rowSelection && options.rowSelection !== 'none') {
241
- this.rowSelectionState = new RowSelectionState(options.rowSelection, options.getRowId);
242
- // Wire row selection API methods
243
- this.api.getSelectedRows = () => {
244
- return Array.from(this.rowSelectionState?.selectedRowIds ?? []);
245
- };
246
- this.api.selectAll = () => {
247
- const { items } = this.state.getProcessedItems();
248
- this.rowSelectionState?.handleSelectAll(true, items);
249
- };
250
- this.api.deselectAll = () => {
251
- const { items } = this.state.getProcessedItems();
252
- this.rowSelectionState?.handleSelectAll(false, items);
253
- };
254
- this.api.setSelectedRows = (rowIds) => {
255
- const { items } = this.state.getProcessedItems();
256
- this.rowSelectionState?.updateSelection(new Set(rowIds), items);
257
- };
258
- this.unsubscribes.push(this.rowSelectionState.onRowSelectionChange(() => {
259
- this.renderingHelper.updateRendererInteractionState();
260
- }));
261
- }
262
- // Create rendering helper (uses lazy context — state objects populated after interaction init)
263
- this.renderingHelper = this.createRenderingHelper();
264
- // Initial render (must happen before interaction init so wrapper DOM exists)
265
- this.renderer.render();
266
- // Initialize interaction features if enabled (default: true for cellSelection)
267
- const shouldEnableInteraction = options.cellSelection !== false || options.editable === true;
268
- if (shouldEnableInteraction) {
269
- const result = this.eventWiringHelper.initializeInteraction(options, this.state, this.renderer, this.tableContainer, this.layoutState, this.rowSelectionState, this.pinningState, {
270
- updateRendererInteractionState: () => this.renderingHelper.updateRendererInteractionState(),
271
- updateDragAttributes: () => this.renderingHelper.updateDragAttributes(),
272
- clearCachedDragCells: () => this.renderingHelper.clearCachedDragCells(),
273
- showContextMenu: (x, y) => this.showContextMenu(x, y),
274
- startCellEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
275
- });
276
- // Store all created state objects
277
- this.selectionState = result.selectionState;
278
- this.keyboardNavState = result.keyboardNavState;
279
- this.clipboardState = result.clipboardState;
280
- this.undoRedoState = result.undoRedoState;
281
- this.resizeState = result.resizeState;
282
- this.fillHandleState = result.fillHandleState;
283
- this.reorderState = result.reorderState;
284
- this.marchingAnts = result.marchingAnts;
285
- this.cellEditor = result.cellEditor;
286
- this.contextMenu = result.contextMenu;
287
- this.unsubscribes.push(...result.unsubscribes);
288
- }
289
- // Subscribe to state changes
290
- this.unsubscribes.push(this.state.onStateChange(() => {
291
- this.renderingHelper.renderAll();
292
- }));
293
- // Subscribe to pinning changes
294
- this.unsubscribes.push(this.pinningState.onPinningChange(() => {
295
- this.renderingHelper.updateRendererInteractionState();
296
- }));
297
- // Subscribe to header filter state changes
298
- this.unsubscribes.push(this.headerFilterState.onChange(() => {
299
- this.renderingHelper.renderHeaderFilterPopover();
300
- }));
301
- // Initialize virtual scrolling if configured
302
- if (options.virtualScroll?.enabled) {
303
- this.virtualScrollState = new VirtualScrollState(options.virtualScroll);
304
- this.virtualScrollState.observeContainer(this.tableContainer);
305
- this.renderer.setVirtualScrollState(this.virtualScrollState);
306
- // Wire scroll event on the table container
307
- const handleScroll = () => {
308
- this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
309
- };
310
- this.tableContainer.addEventListener('scroll', handleScroll, { passive: true });
311
- this.unsubscribes.push(() => {
312
- this.tableContainer.removeEventListener('scroll', handleScroll);
313
- });
314
- // Re-render when visible range changes
315
- this.unsubscribes.push(this.virtualScrollState.onRangeChanged(() => {
316
- this.renderingHelper.updateRendererInteractionState();
317
- }));
318
- // Wire scrollToRow API method
319
- this.api.scrollToRow = (index, opts) => {
320
- this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
321
- };
322
- }
323
- // Complete initial render (pagination, status bar, column chooser, sidebar, loading)
324
- this.renderingHelper.renderAll();
325
- }
326
- catch (e) {
327
- this.destroy();
328
- throw e;
329
- }
330
- }
331
- /** Creates the OGridRenderingContext that bridges OGrid state to the rendering helper. */
332
- createRenderingHelper() {
333
- const liveGetter = (getter) => ({ get: getter, enumerable: true, configurable: true });
334
- const ctx = {
335
- options: this.options,
336
- state: this.state,
337
- renderer: this.renderer,
338
- pagination: this.pagination,
339
- statusBar: this.statusBar,
340
- columnChooser: this.columnChooser,
341
- layoutState: this.layoutState,
342
- tableContainer: this.tableContainer,
343
- headerFilterState: this.headerFilterState,
344
- headerFilterComponent: this.headerFilterComponent,
345
- filterConfigs: this.filterConfigs,
346
- setLoadingOverlay: (el) => { this.loadingOverlay = el; },
347
- handleCellClick: (rowIndex, colIndex) => this.handleCellClick(rowIndex, colIndex),
348
- handleCellMouseDown: (rowIndex, colIndex, e) => this.handleCellMouseDown(rowIndex, colIndex, e),
349
- handleCellContextMenu: (rowIndex, colIndex, e) => this.handleCellContextMenu(rowIndex, colIndex, e),
350
- startCellEdit: (rowId, columnId) => this.startCellEdit(rowId, columnId),
351
- showContextMenu: (x, y) => this.showContextMenu(x, y),
352
- };
353
- Object.defineProperties(ctx, {
354
- selectionState: liveGetter(() => this.selectionState),
355
- keyboardNavState: liveGetter(() => this.keyboardNavState),
356
- clipboardState: liveGetter(() => this.clipboardState),
357
- undoRedoState: liveGetter(() => this.undoRedoState),
358
- resizeState: liveGetter(() => this.resizeState),
359
- fillHandleState: liveGetter(() => this.fillHandleState),
360
- rowSelectionState: liveGetter(() => this.rowSelectionState),
361
- pinningState: liveGetter(() => this.pinningState),
362
- reorderState: liveGetter(() => this.reorderState),
363
- virtualScrollState: liveGetter(() => this.virtualScrollState),
364
- marchingAnts: liveGetter(() => this.marchingAnts),
365
- cellEditor: liveGetter(() => this.cellEditor),
366
- sideBarState: liveGetter(() => this.sideBarState),
367
- sideBarComponent: liveGetter(() => this.sideBarComponent),
368
- loadingOverlay: liveGetter(() => this.loadingOverlay),
369
- });
370
- return new OGridRendering(ctx);
371
- }
372
- handleCellClick(rowIndex, colIndex) {
373
- if (!this.selectionState)
374
- return;
375
- // setActiveCell also sets a single-cell selectionRange internally.
376
- // The selectionChange subscription handles re-rendering.
377
- this.selectionState.setActiveCell({ rowIndex, columnIndex: colIndex });
378
- }
379
- handleCellMouseDown(rowIndex, colIndex, e) {
380
- if (!this.selectionState)
381
- return;
382
- e.preventDefault();
383
- this.selectionState.startDrag(rowIndex, colIndex);
384
- // Apply drag attributes immediately for instant visual feedback on the initial cell
385
- setTimeout(() => this.renderingHelper.updateDragAttributes(), 0);
386
- }
387
- handleCellContextMenu(rowIndex, colIndex, e) {
388
- e.preventDefault();
389
- if (!this.contextMenu || !this.selectionState || !this.clipboardState || !this.undoRedoState)
390
- return;
391
- // Set active cell if not already set
392
- if (!this.selectionState.activeCell || this.selectionState.activeCell.rowIndex !== rowIndex || this.selectionState.activeCell.columnIndex !== colIndex) {
393
- this.selectionState.setActiveCell({ rowIndex, columnIndex: colIndex });
394
- this.renderingHelper.updateRendererInteractionState();
395
- }
396
- this.showContextMenu(e.clientX, e.clientY);
397
- }
398
- showContextMenu(x, y) {
399
- if (!this.contextMenu || !this.clipboardState || !this.undoRedoState || !this.keyboardNavState || !this.selectionState)
400
- return;
401
- this.contextMenu.show(x, y, {
402
- onCopy: () => this.clipboardState?.handleCopy(),
403
- onCut: () => this.clipboardState?.handleCut(),
404
- onPaste: () => void this.clipboardState?.handlePaste(),
405
- onSelectAll: () => {
406
- const { items } = this.state.getProcessedItems();
407
- const visibleCols = this.state.visibleColumnDefs;
408
- if (items.length > 0 && visibleCols.length > 0) {
409
- this.selectionState?.setSelectionRange({
410
- startRow: 0,
411
- startCol: 0,
412
- endRow: items.length - 1,
413
- endCol: visibleCols.length - 1,
414
- });
415
- this.renderingHelper.updateRendererInteractionState();
416
- }
417
- },
418
- onUndo: () => this.undoRedoState?.undo(),
419
- onRedo: () => this.undoRedoState?.redo(),
420
- }, this.undoRedoState.canUndo, this.undoRedoState.canRedo, this.selectionState.selectionRange);
421
- }
422
- startCellEdit(rowId, columnId) {
423
- if (!this.cellEditor || !this.undoRedoState)
424
- return;
425
- const { items } = this.state.getProcessedItems();
426
- const visibleCols = this.state.visibleColumnDefs;
427
- const item = items.find((it) => this.state.getRowId(it) === rowId);
428
- const column = visibleCols.find((col) => col.columnId === columnId);
429
- if (!item || !column)
430
- return;
431
- const wrapper = this.renderer.getWrapperElement();
432
- if (!wrapper)
433
- return;
434
- // Find the row first, then the cell within it
435
- const row = wrapper.querySelector(`tr[data-row-id="${rowId}"]`);
436
- if (!row)
437
- return;
438
- const cell = row.querySelector(`td[data-column-id="${columnId}"]`);
439
- if (!cell)
440
- return;
441
- const rowIndex = items.indexOf(item);
442
- const onCommit = (_rid, cid, value) => {
443
- // Use the already-resolved item and look up the committed column
444
- const col = visibleCols.find((c) => c.columnId === cid);
445
- if (!col)
446
- return;
447
- // NOTE: Direct mutation on the item reference. This updates the in-memory data
448
- // so subsequent renders reflect the new value before the consumer calls setRowData.
449
- const oldValue = item[cid];
450
- item[cid] = value;
451
- const wrapped = this.undoRedoState?.getWrappedCallback();
452
- if (wrapped) {
453
- wrapped({
454
- item,
455
- columnId: cid,
456
- oldValue,
457
- newValue: value,
458
- rowIndex,
459
- });
460
- }
461
- this.renderingHelper.updateRendererInteractionState();
462
- };
463
- const onCancel = () => {
464
- this.renderingHelper.updateRendererInteractionState();
465
- };
466
- const onAfterCommit = () => {
467
- // After Enter-commit, move the active cell down one row (Excel-style behavior)
468
- if (this.selectionState) {
469
- const ac = this.selectionState.activeCell;
470
- if (ac) {
471
- const { items: currentItems } = this.state.getProcessedItems();
472
- const newRow = Math.min(ac.rowIndex + 1, currentItems.length - 1);
473
- this.selectionState.setActiveCell({ rowIndex: newRow, columnIndex: ac.columnIndex });
474
- const colOffset = this.renderer.getColOffset();
475
- const dataCol = ac.columnIndex - colOffset;
476
- this.selectionState.setSelectionRange({
477
- startRow: newRow,
478
- startCol: dataCol,
479
- endRow: newRow,
480
- endCol: dataCol,
481
- });
482
- }
483
- }
484
- // Re-focus the grid wrapper so keyboard nav continues working
485
- const wrapper = this.renderer.getWrapperElement();
486
- wrapper?.focus();
487
- };
488
- this.cellEditor.startEdit(rowId, columnId, item, column, cell, onCommit, onCancel, onAfterCommit);
489
- }
490
- buildFilterConfigs() {
491
- const columns = flattenColumns(this.options.columns);
492
- for (const col of columns) {
493
- const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
494
- if (filterable && filterable.type) {
495
- this.filterConfigs.set(col.columnId, {
496
- columnId: col.columnId,
497
- filterField: filterable.filterField ?? col.columnId,
498
- filterType: filterable.type,
499
- });
500
- }
501
- }
502
- }
503
- handleFilterIconClick(columnId, headerEl) {
504
- const config = this.filterConfigs.get(columnId);
505
- if (!config)
506
- return;
507
- if (this.headerFilterState.openColumnId === columnId) {
508
- this.headerFilterState.close();
509
- return;
510
- }
511
- // Create a temporary popover element to pass to HeaderFilterState
512
- const tempPopover = document.createElement('div');
513
- this.headerFilterState.setFilters(this.state.filters);
514
- this.headerFilterState.setFilterOptions(this.state.filterOptions);
515
- this.headerFilterState.open(columnId, config, headerEl, tempPopover);
516
- }
517
- // Rendering methods delegated to OGridRendering helper:
518
- // - updateRendererInteractionState() -> this.renderingHelper.updateRendererInteractionState()
519
- // - updateDragAttributes() -> this.renderingHelper.updateDragAttributes()
520
- // - renderAll() -> this.renderingHelper.renderAll()
521
- // - renderHeaderFilterPopover() -> this.renderingHelper.renderHeaderFilterPopover()
522
- // - renderSideBar() -> this.renderingHelper.renderSideBar()
523
- // - renderLoadingOverlay() -> this.renderingHelper.renderLoadingOverlay()
524
- /** Subscribe to grid events. */
525
- on(event, handler) {
526
- this.events.on(event, handler);
527
- }
528
- /** Unsubscribe from grid events. */
529
- off(event, handler) {
530
- this.events.off(event, handler);
531
- }
532
- /** Toggle fullscreen mode. */
533
- toggleFullScreen() {
534
- this.isFullScreen = !this.isFullScreen;
535
- if (this.isFullScreen) {
536
- this.containerEl.classList.add('ogrid-fullscreen');
537
- }
538
- else {
539
- this.containerEl.classList.remove('ogrid-fullscreen');
540
- }
541
- // Update button icon + label
542
- if (this.fullscreenBtn) {
543
- this.fullscreenBtn.title = this.isFullScreen ? 'Exit fullscreen' : 'Fullscreen';
544
- this.fullscreenBtn.setAttribute('aria-label', this.isFullScreen ? 'Exit fullscreen' : 'Fullscreen');
545
- this.fullscreenBtn.innerHTML = this.isFullScreen
546
- ? '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 10 0 10 0 14"/><polyline points="12 6 16 6 16 2"/><line x1="0" y1="10" x2="4" y2="6"/><line x1="16" y1="6" x2="12" y2="10"/></svg>'
547
- : '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="10 2 14 2 14 6"/><polyline points="6 14 2 14 2 10"/><line x1="14" y1="2" x2="10" y2="6"/><line x1="2" y1="14" x2="6" y2="10"/></svg>';
548
- }
549
- }
550
- /** Clean up all event listeners and DOM. */
551
- destroy() {
552
- this.unsubscribes.forEach((unsub) => unsub());
553
- this.renderer.destroy();
554
- this.pagination.destroy();
555
- this.statusBar.destroy();
556
- this.columnChooser.destroy();
557
- this.sideBarState?.destroy();
558
- this.sideBarComponent?.destroy();
559
- this.headerFilterState.destroy();
560
- this.headerFilterComponent.destroy();
561
- this.state.destroy();
562
- this.selectionState?.destroy();
563
- this.clipboardState?.destroy();
564
- this.undoRedoState?.destroy();
565
- this.resizeState?.destroy();
566
- this.fillHandleState?.destroy();
567
- this.rowSelectionState?.destroy();
568
- this.pinningState?.destroy();
569
- this.reorderState?.destroy();
570
- this.virtualScrollState?.destroy();
571
- this.marchingAnts?.destroy();
572
- this.layoutState.destroy();
573
- this.cellEditor?.closeEditor();
574
- this.contextMenu?.close();
575
- this.events.removeAllListeners();
576
- this.containerEl.remove();
577
- }
578
- }