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