@alaarab/ogrid-js 2.1.3 → 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.
- package/dist/esm/index.js +6343 -32
- package/package.json +4 -4
- package/dist/esm/OGrid.js +0 -578
- package/dist/esm/OGridEventWiring.js +0 -178
- package/dist/esm/OGridRendering.js +0 -269
- package/dist/esm/components/ColumnChooser.js +0 -91
- package/dist/esm/components/ContextMenu.js +0 -125
- package/dist/esm/components/HeaderFilter.js +0 -281
- package/dist/esm/components/InlineCellEditor.js +0 -434
- package/dist/esm/components/MarchingAntsOverlay.js +0 -156
- package/dist/esm/components/PaginationControls.js +0 -85
- package/dist/esm/components/SideBar.js +0 -353
- package/dist/esm/components/StatusBar.js +0 -34
- package/dist/esm/renderer/TableRenderer.js +0 -846
- package/dist/esm/state/ClipboardState.js +0 -111
- package/dist/esm/state/ColumnPinningState.js +0 -82
- package/dist/esm/state/ColumnReorderState.js +0 -135
- package/dist/esm/state/ColumnResizeState.js +0 -55
- package/dist/esm/state/EventEmitter.js +0 -28
- package/dist/esm/state/FillHandleState.js +0 -206
- package/dist/esm/state/GridState.js +0 -324
- package/dist/esm/state/HeaderFilterState.js +0 -213
- package/dist/esm/state/KeyboardNavState.js +0 -216
- package/dist/esm/state/RowSelectionState.js +0 -72
- package/dist/esm/state/SelectionState.js +0 -109
- package/dist/esm/state/SideBarState.js +0 -41
- package/dist/esm/state/TableLayoutState.js +0 -97
- package/dist/esm/state/UndoRedoState.js +0 -71
- package/dist/esm/state/VirtualScrollState.js +0 -128
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/gridTypes.js +0 -1
- package/dist/esm/types/index.js +0 -2
- package/dist/esm/utils/debounce.js +0 -2
- package/dist/esm/utils/getCellCoordinates.js +0 -15
- 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.
|
|
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
|
-
"
|
|
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,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@alaarab/ogrid-core": "2.1.
|
|
39
|
+
"@alaarab/ogrid-core": "2.1.4"
|
|
40
40
|
},
|
|
41
41
|
"sideEffects": [
|
|
42
42
|
"**/*.css"
|
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
|
-
}
|