@alaarab/ogrid-js 2.0.19 → 2.0.22
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 +5 -4
- package/dist/esm/components/ContextMenu.js +4 -4
- package/dist/esm/components/InlineCellEditor.js +26 -8
- package/dist/esm/components/PaginationControls.js +2 -2
- package/dist/esm/renderer/TableRenderer.js +292 -30
- package/dist/esm/state/ColumnReorderState.js +2 -2
- package/dist/esm/state/FillHandleState.js +13 -11
- package/dist/esm/state/HeaderFilterState.js +1 -1
- package/dist/esm/state/TableLayoutState.js +1 -1
- package/dist/esm/state/UndoRedoState.js +16 -27
- package/dist/styles/ogrid.css +4 -6
- package/dist/types/components/PaginationControls.d.ts +1 -1
- package/dist/types/renderer/TableRenderer.d.ts +29 -0
- package/dist/types/state/FillHandleState.d.ts +1 -0
- package/dist/types/state/UndoRedoState.d.ts +1 -4
- package/dist/types/types/gridTypes.d.ts +41 -1
- package/package.json +2 -2
package/dist/esm/OGrid.js
CHANGED
|
@@ -405,8 +405,8 @@ export class OGrid {
|
|
|
405
405
|
document.body.style.cursor = 'col-resize';
|
|
406
406
|
this.resizeState?.startResize(columnId, clientX, currentWidth);
|
|
407
407
|
};
|
|
408
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
409
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
408
|
+
document.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
409
|
+
document.addEventListener('mouseup', handleMouseUp, { passive: true });
|
|
410
410
|
// Store references for cleanup
|
|
411
411
|
this.unsubscribes.push(() => {
|
|
412
412
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
@@ -489,7 +489,8 @@ export class OGrid {
|
|
|
489
489
|
const minC = norm.startCol;
|
|
490
490
|
const maxC = norm.endCol;
|
|
491
491
|
const cells = wrapper.querySelectorAll('td[data-row-index][data-col-index]');
|
|
492
|
-
for (
|
|
492
|
+
for (let _i = 0; _i < cells.length; _i++) {
|
|
493
|
+
const cell = cells[_i];
|
|
493
494
|
const el = cell;
|
|
494
495
|
const rowIndex = parseInt(el.getAttribute('data-row-index') ?? '-1', 10);
|
|
495
496
|
const colIndex = parseInt(el.getAttribute('data-col-index') ?? '-1', 10);
|
|
@@ -803,7 +804,7 @@ export class OGrid {
|
|
|
803
804
|
const { totalCount } = this.state.getProcessedItems();
|
|
804
805
|
// Update virtual scroll with current total row count
|
|
805
806
|
this.virtualScrollState?.setTotalRows(totalCount);
|
|
806
|
-
this.pagination.render(totalCount);
|
|
807
|
+
this.pagination.render(totalCount, this.options.pageSizeOptions);
|
|
807
808
|
this.statusBar.render({ totalCount });
|
|
808
809
|
this.columnChooser.render();
|
|
809
810
|
this.renderSideBar();
|
|
@@ -66,13 +66,13 @@ export class ContextMenu {
|
|
|
66
66
|
else {
|
|
67
67
|
menuItem.addEventListener('mouseenter', () => {
|
|
68
68
|
menuItem.style.backgroundColor = 'var(--ogrid-bg-hover, #f5f5f5)';
|
|
69
|
-
});
|
|
69
|
+
}, { passive: true });
|
|
70
70
|
menuItem.addEventListener('mouseleave', () => {
|
|
71
71
|
menuItem.style.backgroundColor = '';
|
|
72
|
-
});
|
|
72
|
+
}, { passive: true });
|
|
73
73
|
menuItem.addEventListener('click', () => {
|
|
74
74
|
this.handleItemClick(item.id);
|
|
75
|
-
});
|
|
75
|
+
}, { passive: true });
|
|
76
76
|
}
|
|
77
77
|
this.menu.appendChild(menuItem);
|
|
78
78
|
}
|
|
@@ -84,7 +84,7 @@ export class ContextMenu {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
setTimeout(() => {
|
|
87
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
87
|
+
document.addEventListener('mousedown', handleClickOutside, { passive: true });
|
|
88
88
|
}, 0);
|
|
89
89
|
}
|
|
90
90
|
close() {
|
|
@@ -267,7 +267,8 @@ export class InlineCellEditor {
|
|
|
267
267
|
dropdown.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
|
268
268
|
wrapper.appendChild(dropdown);
|
|
269
269
|
let highlightedIndex = Math.max(values.findIndex((v) => String(v) === String(value)), 0);
|
|
270
|
-
|
|
270
|
+
// Build all option elements once
|
|
271
|
+
const buildOptions = () => {
|
|
271
272
|
dropdown.innerHTML = '';
|
|
272
273
|
for (let i = 0; i < values.length; i++) {
|
|
273
274
|
const val = values[i];
|
|
@@ -291,25 +292,42 @@ export class InlineCellEditor {
|
|
|
291
292
|
dropdown.appendChild(option);
|
|
292
293
|
}
|
|
293
294
|
};
|
|
295
|
+
// Only update CSS class on old/new highlighted item — avoids rebuilding the DOM
|
|
296
|
+
const updateHighlight = (prevIndex, nextIndex) => {
|
|
297
|
+
const prev = dropdown.children[prevIndex];
|
|
298
|
+
const next = dropdown.children[nextIndex];
|
|
299
|
+
if (prev) {
|
|
300
|
+
prev.style.background = '';
|
|
301
|
+
prev.setAttribute('aria-selected', 'false');
|
|
302
|
+
}
|
|
303
|
+
if (next) {
|
|
304
|
+
next.style.background = 'var(--ogrid-bg-hover, #e8f0fe)';
|
|
305
|
+
next.setAttribute('aria-selected', 'true');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
294
308
|
const scrollHighlightedIntoView = () => {
|
|
295
309
|
const highlighted = dropdown.children[highlightedIndex];
|
|
296
310
|
highlighted?.scrollIntoView({ block: 'nearest' });
|
|
297
311
|
};
|
|
298
|
-
|
|
312
|
+
buildOptions();
|
|
299
313
|
wrapper.addEventListener('keydown', (e) => {
|
|
300
314
|
switch (e.key) {
|
|
301
|
-
case 'ArrowDown':
|
|
315
|
+
case 'ArrowDown': {
|
|
302
316
|
e.preventDefault();
|
|
317
|
+
const prevDown = highlightedIndex;
|
|
303
318
|
highlightedIndex = Math.min(highlightedIndex + 1, values.length - 1);
|
|
304
|
-
|
|
319
|
+
updateHighlight(prevDown, highlightedIndex);
|
|
305
320
|
scrollHighlightedIntoView();
|
|
306
321
|
break;
|
|
307
|
-
|
|
322
|
+
}
|
|
323
|
+
case 'ArrowUp': {
|
|
308
324
|
e.preventDefault();
|
|
325
|
+
const prevUp = highlightedIndex;
|
|
309
326
|
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
310
|
-
|
|
327
|
+
updateHighlight(prevUp, highlightedIndex);
|
|
311
328
|
scrollHighlightedIntoView();
|
|
312
329
|
break;
|
|
330
|
+
}
|
|
313
331
|
case 'Enter':
|
|
314
332
|
e.preventDefault();
|
|
315
333
|
e.stopPropagation();
|
|
@@ -386,10 +404,10 @@ export class InlineCellEditor {
|
|
|
386
404
|
});
|
|
387
405
|
option.addEventListener('mouseenter', () => {
|
|
388
406
|
option.style.backgroundColor = 'var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04))';
|
|
389
|
-
});
|
|
407
|
+
}, { passive: true });
|
|
390
408
|
option.addEventListener('mouseleave', () => {
|
|
391
409
|
option.style.backgroundColor = 'var(--ogrid-bg, #fff)';
|
|
392
|
-
});
|
|
410
|
+
}, { passive: true });
|
|
393
411
|
dropdown.appendChild(option);
|
|
394
412
|
}
|
|
395
413
|
};
|
|
@@ -5,10 +5,10 @@ export class PaginationControls {
|
|
|
5
5
|
this.container = container;
|
|
6
6
|
this.state = state;
|
|
7
7
|
}
|
|
8
|
-
render(totalCount) {
|
|
8
|
+
render(totalCount, pageSizeOptions) {
|
|
9
9
|
if (this.el)
|
|
10
10
|
this.el.remove();
|
|
11
|
-
const vm = getPaginationViewModel(this.state.page, this.state.pageSize, totalCount);
|
|
11
|
+
const vm = getPaginationViewModel(this.state.page, this.state.pageSize, totalCount, pageSizeOptions ? { pageSizeOptions } : undefined);
|
|
12
12
|
if (!vm)
|
|
13
13
|
return; // No pagination if totalCount is 0
|
|
14
14
|
this.el = document.createElement('div');
|
|
@@ -12,6 +12,20 @@ export class TableRenderer {
|
|
|
12
12
|
this.onFilterIconClick = null;
|
|
13
13
|
this.dropIndicator = null;
|
|
14
14
|
this.virtualScrollState = null;
|
|
15
|
+
// Delegated event handlers bound to tbody
|
|
16
|
+
this._tbodyClickHandler = null;
|
|
17
|
+
this._tbodyMousedownHandler = null;
|
|
18
|
+
this._tbodyDblclickHandler = null;
|
|
19
|
+
this._tbodyContextmenuHandler = null;
|
|
20
|
+
// State tracking for incremental DOM patching
|
|
21
|
+
this.lastActiveCell = null;
|
|
22
|
+
this.lastSelectionRange = null;
|
|
23
|
+
this.lastCopyRange = null;
|
|
24
|
+
this.lastCutRange = null;
|
|
25
|
+
this.lastEditingCell = null;
|
|
26
|
+
this.lastColumnWidths = {};
|
|
27
|
+
this.lastHeaderSignature = '';
|
|
28
|
+
this.lastRenderedItems = null;
|
|
15
29
|
this.container = container;
|
|
16
30
|
this.state = state;
|
|
17
31
|
}
|
|
@@ -28,6 +42,72 @@ export class TableRenderer {
|
|
|
28
42
|
setInteractionState(state) {
|
|
29
43
|
this.interactionState = state;
|
|
30
44
|
}
|
|
45
|
+
getCellFromEvent(e) {
|
|
46
|
+
const target = e.target;
|
|
47
|
+
const cell = target.closest('td[data-row-index]');
|
|
48
|
+
if (!cell)
|
|
49
|
+
return null;
|
|
50
|
+
const rowIndex = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
51
|
+
const colIndex = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
52
|
+
if (Number.isNaN(rowIndex) || Number.isNaN(colIndex))
|
|
53
|
+
return null;
|
|
54
|
+
return { el: cell, rowIndex, colIndex };
|
|
55
|
+
}
|
|
56
|
+
attachBodyDelegation() {
|
|
57
|
+
if (!this.tbody)
|
|
58
|
+
return;
|
|
59
|
+
this._tbodyClickHandler = (e) => {
|
|
60
|
+
const cell = this.getCellFromEvent(e);
|
|
61
|
+
if (!cell)
|
|
62
|
+
return;
|
|
63
|
+
this.interactionState?.onCellClick?.(cell.rowIndex, cell.colIndex, e);
|
|
64
|
+
};
|
|
65
|
+
this._tbodyMousedownHandler = (e) => {
|
|
66
|
+
const cell = this.getCellFromEvent(e);
|
|
67
|
+
if (!cell)
|
|
68
|
+
return;
|
|
69
|
+
this.interactionState?.onCellMouseDown?.(cell.rowIndex, cell.colIndex, e);
|
|
70
|
+
};
|
|
71
|
+
this._tbodyDblclickHandler = (e) => {
|
|
72
|
+
const cell = this.getCellFromEvent(e);
|
|
73
|
+
if (!cell)
|
|
74
|
+
return;
|
|
75
|
+
const columnId = cell.el.getAttribute('data-column-id') ?? '';
|
|
76
|
+
// Retrieve the typed rowId by looking up the item at the row index (avoids string/number mismatch from data-row-id)
|
|
77
|
+
const { items } = this.state.getProcessedItems();
|
|
78
|
+
const item = items[cell.rowIndex];
|
|
79
|
+
if (!item)
|
|
80
|
+
return;
|
|
81
|
+
const rowId = this.state.getRowId(item);
|
|
82
|
+
this.interactionState?.onCellDoubleClick?.(cell.rowIndex, cell.colIndex, rowId, columnId);
|
|
83
|
+
};
|
|
84
|
+
this._tbodyContextmenuHandler = (e) => {
|
|
85
|
+
const cell = this.getCellFromEvent(e);
|
|
86
|
+
if (!cell)
|
|
87
|
+
return;
|
|
88
|
+
this.interactionState?.onCellContextMenu?.(cell.rowIndex, cell.colIndex, e);
|
|
89
|
+
};
|
|
90
|
+
this.tbody.addEventListener('click', this._tbodyClickHandler, { passive: true });
|
|
91
|
+
this.tbody.addEventListener('mousedown', this._tbodyMousedownHandler);
|
|
92
|
+
this.tbody.addEventListener('dblclick', this._tbodyDblclickHandler, { passive: true });
|
|
93
|
+
this.tbody.addEventListener('contextmenu', this._tbodyContextmenuHandler);
|
|
94
|
+
}
|
|
95
|
+
detachBodyDelegation() {
|
|
96
|
+
if (!this.tbody)
|
|
97
|
+
return;
|
|
98
|
+
if (this._tbodyClickHandler)
|
|
99
|
+
this.tbody.removeEventListener('click', this._tbodyClickHandler);
|
|
100
|
+
if (this._tbodyMousedownHandler)
|
|
101
|
+
this.tbody.removeEventListener('mousedown', this._tbodyMousedownHandler);
|
|
102
|
+
if (this._tbodyDblclickHandler)
|
|
103
|
+
this.tbody.removeEventListener('dblclick', this._tbodyDblclickHandler);
|
|
104
|
+
if (this._tbodyContextmenuHandler)
|
|
105
|
+
this.tbody.removeEventListener('contextmenu', this._tbodyContextmenuHandler);
|
|
106
|
+
this._tbodyClickHandler = null;
|
|
107
|
+
this._tbodyMousedownHandler = null;
|
|
108
|
+
this._tbodyDblclickHandler = null;
|
|
109
|
+
this._tbodyContextmenuHandler = null;
|
|
110
|
+
}
|
|
31
111
|
getWrapperElement() {
|
|
32
112
|
return this.wrapperEl;
|
|
33
113
|
}
|
|
@@ -41,6 +121,10 @@ export class TableRenderer {
|
|
|
41
121
|
wrapper.setAttribute('role', 'grid');
|
|
42
122
|
wrapper.setAttribute('tabindex', '0'); // Make focusable for keyboard nav
|
|
43
123
|
wrapper.style.position = 'relative'; // For MarchingAnts absolute positioning
|
|
124
|
+
const rowHeight = this.state._options?.rowHeight;
|
|
125
|
+
if (rowHeight) {
|
|
126
|
+
wrapper.style.setProperty('--ogrid-row-height', `${rowHeight}px`);
|
|
127
|
+
}
|
|
44
128
|
const ariaLabel = this.state._ariaLabel;
|
|
45
129
|
if (ariaLabel) {
|
|
46
130
|
wrapper.setAttribute('aria-label', ariaLabel);
|
|
@@ -56,6 +140,7 @@ export class TableRenderer {
|
|
|
56
140
|
// Render body
|
|
57
141
|
this.tbody = document.createElement('tbody');
|
|
58
142
|
this.renderBody();
|
|
143
|
+
this.attachBodyDelegation();
|
|
59
144
|
this.table.appendChild(this.tbody);
|
|
60
145
|
wrapper.appendChild(this.table);
|
|
61
146
|
// Create drop indicator for column reorder (hidden by default)
|
|
@@ -64,17 +149,220 @@ export class TableRenderer {
|
|
|
64
149
|
this.dropIndicator.style.display = 'none';
|
|
65
150
|
wrapper.appendChild(this.dropIndicator);
|
|
66
151
|
this.container.appendChild(wrapper);
|
|
152
|
+
this.snapshotState();
|
|
153
|
+
}
|
|
154
|
+
/** Compute a signature string that captures header-affecting state. */
|
|
155
|
+
computeHeaderSignature() {
|
|
156
|
+
const cols = this.state.visibleColumnDefs;
|
|
157
|
+
const is = this.interactionState;
|
|
158
|
+
const parts = [];
|
|
159
|
+
for (const col of cols) {
|
|
160
|
+
parts.push(col.columnId);
|
|
161
|
+
parts.push(col.name);
|
|
162
|
+
parts.push(is?.columnWidths[col.columnId]?.toString() ?? '');
|
|
163
|
+
}
|
|
164
|
+
// Include sort state
|
|
165
|
+
const sort = this.state.sort;
|
|
166
|
+
if (sort)
|
|
167
|
+
parts.push(`sort:${sort.field}:${sort.direction}`);
|
|
168
|
+
// Include row selection mode and checkbox header state
|
|
169
|
+
parts.push(`sel:${is?.rowSelectionMode ?? ''}`);
|
|
170
|
+
parts.push(`allSel:${is?.allSelected ?? ''}`);
|
|
171
|
+
parts.push(`someSel:${is?.someSelected ?? ''}`);
|
|
172
|
+
// Include showRowNumbers
|
|
173
|
+
parts.push(`rn:${is?.showRowNumbers ?? ''}`);
|
|
174
|
+
// Include filter active states
|
|
175
|
+
for (const [colId, config] of this.filterConfigs) {
|
|
176
|
+
const hasActive = this.headerFilterState?.hasActiveFilter(config);
|
|
177
|
+
if (hasActive)
|
|
178
|
+
parts.push(`flt:${colId}`);
|
|
179
|
+
}
|
|
180
|
+
return parts.join('|');
|
|
181
|
+
}
|
|
182
|
+
/** Save current interaction state for next diff comparison. */
|
|
183
|
+
snapshotState() {
|
|
184
|
+
const is = this.interactionState;
|
|
185
|
+
this.lastActiveCell = is?.activeCell ? { ...is.activeCell } : null;
|
|
186
|
+
this.lastSelectionRange = is?.selectionRange ? { ...is.selectionRange } : null;
|
|
187
|
+
this.lastCopyRange = is?.copyRange ? { ...is.copyRange } : null;
|
|
188
|
+
this.lastCutRange = is?.cutRange ? { ...is.cutRange } : null;
|
|
189
|
+
this.lastEditingCell = is?.editingCell ? { ...is.editingCell } : null;
|
|
190
|
+
this.lastColumnWidths = is?.columnWidths ? { ...is.columnWidths } : {};
|
|
191
|
+
this.lastRowSelectionMode = is?.rowSelectionMode;
|
|
192
|
+
this.lastSelectedRowIds = is?.selectedRowIds ? new Set(is.selectedRowIds) : undefined;
|
|
193
|
+
this.lastShowRowNumbers = is?.showRowNumbers;
|
|
194
|
+
this.lastPinnedColumns = is?.pinnedColumns;
|
|
195
|
+
this.lastAllSelected = is?.allSelected;
|
|
196
|
+
this.lastSomeSelected = is?.someSelected;
|
|
197
|
+
this.lastHeaderSignature = this.computeHeaderSignature();
|
|
198
|
+
const { items } = this.state.getProcessedItems();
|
|
199
|
+
this.lastRenderedItems = items;
|
|
200
|
+
}
|
|
201
|
+
/** Check if only selection/active-cell/copy/cut ranges changed (no data or header changes). */
|
|
202
|
+
isSelectionOnlyChange() {
|
|
203
|
+
if (!this.lastRenderedItems)
|
|
204
|
+
return false;
|
|
205
|
+
const is = this.interactionState;
|
|
206
|
+
const { items } = this.state.getProcessedItems();
|
|
207
|
+
// If data items changed, need full body rebuild
|
|
208
|
+
if (items !== this.lastRenderedItems)
|
|
209
|
+
return false;
|
|
210
|
+
// If header signature changed, need header rebuild
|
|
211
|
+
const currentHeaderSig = this.computeHeaderSignature();
|
|
212
|
+
if (currentHeaderSig !== this.lastHeaderSignature)
|
|
213
|
+
return false;
|
|
214
|
+
// If editing cell changed, need body rebuild (visibility toggle on the td)
|
|
215
|
+
const curEdit = is?.editingCell;
|
|
216
|
+
const lastEdit = this.lastEditingCell;
|
|
217
|
+
if (curEdit?.rowId !== lastEdit?.rowId || curEdit?.columnId !== lastEdit?.columnId)
|
|
218
|
+
return false;
|
|
219
|
+
// If row selection changed, need body rebuild (checkbox states, row attrs)
|
|
220
|
+
if (is?.rowSelectionMode !== this.lastRowSelectionMode)
|
|
221
|
+
return false;
|
|
222
|
+
if (is?.selectedRowIds !== this.lastSelectedRowIds) {
|
|
223
|
+
// Compare sets
|
|
224
|
+
const curIds = is?.selectedRowIds;
|
|
225
|
+
const lastIds = this.lastSelectedRowIds;
|
|
226
|
+
if (!curIds && !lastIds) { /* both null, ok */ }
|
|
227
|
+
else if (!curIds || !lastIds || curIds.size !== lastIds.size)
|
|
228
|
+
return false;
|
|
229
|
+
else {
|
|
230
|
+
for (const id of curIds) {
|
|
231
|
+
if (!lastIds.has(id))
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// If pinning or row numbers changed
|
|
237
|
+
if (is?.showRowNumbers !== this.lastShowRowNumbers)
|
|
238
|
+
return false;
|
|
239
|
+
if (is?.pinnedColumns !== this.lastPinnedColumns)
|
|
240
|
+
return false;
|
|
241
|
+
// Otherwise it's just selection/active-cell/copy/cut changes
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
/** Patch only CSS classes/styles for selection, active cell, copy/cut ranges without rebuilding DOM. */
|
|
245
|
+
patchSelectionClasses() {
|
|
246
|
+
if (!this.tbody || !this.interactionState)
|
|
247
|
+
return;
|
|
248
|
+
const is = this.interactionState;
|
|
249
|
+
const { activeCell, selectionRange, copyRange, cutRange } = is;
|
|
250
|
+
const lastActive = this.lastActiveCell;
|
|
251
|
+
const lastSelection = this.lastSelectionRange;
|
|
252
|
+
const lastCopy = this.lastCopyRange;
|
|
253
|
+
const lastCut = this.lastCutRange;
|
|
254
|
+
const cells = this.tbody.querySelectorAll('td[data-row-index][data-col-index]');
|
|
255
|
+
for (let i = 0; i < cells.length; i++) {
|
|
256
|
+
const el = cells[i];
|
|
257
|
+
const rowIndex = parseInt(el.getAttribute('data-row-index'), 10);
|
|
258
|
+
const globalColIndex = parseInt(el.getAttribute('data-col-index'), 10);
|
|
259
|
+
const colOffset = this.getColOffset();
|
|
260
|
+
const colIndex = globalColIndex - colOffset;
|
|
261
|
+
// --- Active cell ---
|
|
262
|
+
const wasActive = lastActive && lastActive.rowIndex === rowIndex && lastActive.columnIndex === globalColIndex;
|
|
263
|
+
const isActive = activeCell && activeCell.rowIndex === rowIndex && activeCell.columnIndex === globalColIndex;
|
|
264
|
+
if (wasActive && !isActive) {
|
|
265
|
+
el.removeAttribute('data-active-cell');
|
|
266
|
+
el.style.outline = '';
|
|
267
|
+
}
|
|
268
|
+
else if (isActive && !wasActive) {
|
|
269
|
+
el.setAttribute('data-active-cell', 'true');
|
|
270
|
+
el.style.outline = '2px solid var(--ogrid-accent, #0078d4)';
|
|
271
|
+
}
|
|
272
|
+
// --- Selection range ---
|
|
273
|
+
const wasInRange = lastSelection && isInSelectionRange(lastSelection, rowIndex, colIndex);
|
|
274
|
+
const isInRange = selectionRange && isInSelectionRange(selectionRange, rowIndex, colIndex);
|
|
275
|
+
if (wasInRange && !isInRange) {
|
|
276
|
+
el.removeAttribute('data-in-range');
|
|
277
|
+
el.style.backgroundColor = '';
|
|
278
|
+
}
|
|
279
|
+
else if (isInRange && !wasInRange) {
|
|
280
|
+
el.setAttribute('data-in-range', 'true');
|
|
281
|
+
el.style.backgroundColor = 'var(--ogrid-range-bg, rgba(33, 115, 70, 0.12))';
|
|
282
|
+
}
|
|
283
|
+
// --- Copy range ---
|
|
284
|
+
const wasInCopy = lastCopy && isInSelectionRange(lastCopy, rowIndex, colIndex);
|
|
285
|
+
const isInCopy = copyRange && isInSelectionRange(copyRange, rowIndex, colIndex);
|
|
286
|
+
if (wasInCopy && !isInCopy) {
|
|
287
|
+
// Only clear outline if not being set by another range (active/cut)
|
|
288
|
+
if (!isActive && !(cutRange && isInSelectionRange(cutRange, rowIndex, colIndex))) {
|
|
289
|
+
el.style.outline = '';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (isInCopy && !wasInCopy) {
|
|
293
|
+
el.style.outline = '1px dashed var(--ogrid-fg-muted, rgba(0, 0, 0, 0.5))';
|
|
294
|
+
}
|
|
295
|
+
// --- Cut range ---
|
|
296
|
+
const wasInCut = lastCut && isInSelectionRange(lastCut, rowIndex, colIndex);
|
|
297
|
+
const isInCut = cutRange && isInSelectionRange(cutRange, rowIndex, colIndex);
|
|
298
|
+
if (wasInCut && !isInCut) {
|
|
299
|
+
if (!isActive && !(copyRange && isInSelectionRange(copyRange, rowIndex, colIndex))) {
|
|
300
|
+
el.style.outline = '';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (isInCut && !wasInCut) {
|
|
304
|
+
el.style.outline = '1px dashed var(--ogrid-accent, #0078d4)';
|
|
305
|
+
}
|
|
306
|
+
// --- Fill handle ---
|
|
307
|
+
// Remove old fill handle if it was on a cell no longer at the bottom-right of selection
|
|
308
|
+
const oldFill = el.querySelector('.ogrid-fill-handle');
|
|
309
|
+
const shouldHaveFill = selectionRange && is.onFillHandleMouseDown &&
|
|
310
|
+
rowIndex === Math.max(selectionRange.startRow, selectionRange.endRow) &&
|
|
311
|
+
colIndex === Math.max(selectionRange.startCol, selectionRange.endCol);
|
|
312
|
+
const hadFill = !!oldFill;
|
|
313
|
+
if (hadFill && !shouldHaveFill) {
|
|
314
|
+
oldFill.remove();
|
|
315
|
+
}
|
|
316
|
+
else if (!hadFill && shouldHaveFill) {
|
|
317
|
+
const fillHandle = document.createElement('div');
|
|
318
|
+
fillHandle.className = 'ogrid-fill-handle';
|
|
319
|
+
fillHandle.setAttribute('data-fill-handle', 'true');
|
|
320
|
+
fillHandle.style.position = 'absolute';
|
|
321
|
+
fillHandle.style.right = '-3px';
|
|
322
|
+
fillHandle.style.bottom = '-3px';
|
|
323
|
+
fillHandle.style.width = '6px';
|
|
324
|
+
fillHandle.style.height = '6px';
|
|
325
|
+
fillHandle.style.backgroundColor = 'var(--ogrid-selection, #217346)';
|
|
326
|
+
fillHandle.style.cursor = 'crosshair';
|
|
327
|
+
fillHandle.style.zIndex = '5';
|
|
328
|
+
el.style.position = el.style.position || 'relative';
|
|
329
|
+
fillHandle.addEventListener('mousedown', (e) => {
|
|
330
|
+
this.interactionState?.onFillHandleMouseDown?.(e);
|
|
331
|
+
});
|
|
332
|
+
el.appendChild(fillHandle);
|
|
333
|
+
}
|
|
334
|
+
// Restore pinned cell background if needed (selection removal may have cleared it)
|
|
335
|
+
if (!isInRange && is.pinnedColumns) {
|
|
336
|
+
const columnId = el.getAttribute('data-column-id');
|
|
337
|
+
if (columnId && is.pinnedColumns[columnId]) {
|
|
338
|
+
el.style.backgroundColor = el.style.backgroundColor || 'var(--ogrid-bg, #fff)';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.snapshotState();
|
|
67
343
|
}
|
|
68
344
|
/** Re-render body rows and header (after sort/filter/page change). */
|
|
69
345
|
update() {
|
|
70
346
|
if (!this.tbody || !this.thead) {
|
|
71
347
|
this.render();
|
|
348
|
+
this.snapshotState();
|
|
72
349
|
return;
|
|
73
350
|
}
|
|
74
|
-
|
|
75
|
-
this.
|
|
351
|
+
// Check if only selection-related state changed — if so, patch CSS only
|
|
352
|
+
if (this.isSelectionOnlyChange()) {
|
|
353
|
+
this.patchSelectionClasses();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Check if header needs rebuild
|
|
357
|
+
const currentHeaderSig = this.computeHeaderSignature();
|
|
358
|
+
if (currentHeaderSig !== this.lastHeaderSignature) {
|
|
359
|
+
this.thead.innerHTML = '';
|
|
360
|
+
this.renderHeader();
|
|
361
|
+
}
|
|
362
|
+
// Delegation listeners are on tbody itself — just clear inner HTML, keep listeners
|
|
76
363
|
this.tbody.innerHTML = '';
|
|
77
364
|
this.renderBody();
|
|
365
|
+
this.snapshotState();
|
|
78
366
|
}
|
|
79
367
|
hasCheckboxColumn() {
|
|
80
368
|
const mode = this.interactionState?.rowSelectionMode;
|
|
@@ -148,14 +436,6 @@ export class TableRenderer {
|
|
|
148
436
|
this.state.toggleSort(cell.columnDef.columnId);
|
|
149
437
|
}
|
|
150
438
|
});
|
|
151
|
-
// Sort indicator
|
|
152
|
-
const sort = this.state.sort;
|
|
153
|
-
if (sort && cell.columnDef && sort.field === cell.columnDef.columnId) {
|
|
154
|
-
const indicator = document.createElement('span');
|
|
155
|
-
indicator.className = 'ogrid-sort-indicator';
|
|
156
|
-
indicator.textContent = sort.direction === 'asc' ? ' \u25B2' : ' \u25BC';
|
|
157
|
-
th.appendChild(indicator);
|
|
158
|
-
}
|
|
159
439
|
}
|
|
160
440
|
if (!cell.isGroup && cell.columnDef) {
|
|
161
441
|
th.setAttribute('data-column-id', cell.columnDef.columnId);
|
|
@@ -211,13 +491,6 @@ export class TableRenderer {
|
|
|
211
491
|
if (col.sortable) {
|
|
212
492
|
th.classList.add('ogrid-sortable');
|
|
213
493
|
th.addEventListener('click', () => this.state.toggleSort(col.columnId));
|
|
214
|
-
const sort = this.state.sort;
|
|
215
|
-
if (sort && sort.field === col.columnId) {
|
|
216
|
-
const indicator = document.createElement('span');
|
|
217
|
-
indicator.className = 'ogrid-sort-indicator';
|
|
218
|
-
indicator.textContent = sort.direction === 'asc' ? ' \u25B2' : ' \u25BC';
|
|
219
|
-
th.appendChild(indicator);
|
|
220
|
-
}
|
|
221
494
|
}
|
|
222
495
|
if (col.type === 'numeric') {
|
|
223
496
|
th.style.textAlign = 'right';
|
|
@@ -429,19 +702,7 @@ export class TableRenderer {
|
|
|
429
702
|
if (editingCell && editingCell.rowId === rowId && editingCell.columnId === col.columnId) {
|
|
430
703
|
td.style.visibility = 'hidden';
|
|
431
704
|
}
|
|
432
|
-
// Cell interaction
|
|
433
|
-
td.addEventListener('click', (e) => {
|
|
434
|
-
this.interactionState?.onCellClick?.(rowIndex, globalColIndex, e);
|
|
435
|
-
});
|
|
436
|
-
td.addEventListener('mousedown', (e) => {
|
|
437
|
-
this.interactionState?.onCellMouseDown?.(rowIndex, globalColIndex, e);
|
|
438
|
-
});
|
|
439
|
-
td.addEventListener('dblclick', () => {
|
|
440
|
-
this.interactionState?.onCellDoubleClick?.(rowIndex, globalColIndex, rowId, col.columnId);
|
|
441
|
-
});
|
|
442
|
-
td.addEventListener('contextmenu', (e) => {
|
|
443
|
-
this.interactionState?.onCellContextMenu?.(rowIndex, globalColIndex, e);
|
|
444
|
-
});
|
|
705
|
+
// Cell interaction is handled by delegated listeners on tbody
|
|
445
706
|
}
|
|
446
707
|
// Custom DOM render
|
|
447
708
|
if (col.renderCell) {
|
|
@@ -531,6 +792,7 @@ export class TableRenderer {
|
|
|
531
792
|
this.dropIndicator.style.left = `${relativeX}px`;
|
|
532
793
|
}
|
|
533
794
|
destroy() {
|
|
795
|
+
this.detachBodyDelegation();
|
|
534
796
|
this.container.innerHTML = '';
|
|
535
797
|
this.table = null;
|
|
536
798
|
this.thead = null;
|
|
@@ -55,8 +55,8 @@ export class ColumnReorderState {
|
|
|
55
55
|
this.pinnedColumns = undefined;
|
|
56
56
|
}
|
|
57
57
|
this.draggedPinState = getPinStateForColumn(columnId, this.pinnedColumns);
|
|
58
|
-
window.addEventListener('mousemove', this.onMoveBound, true);
|
|
59
|
-
window.addEventListener('mouseup', this.onUpBound, true);
|
|
58
|
+
window.addEventListener('mousemove', this.onMoveBound, { capture: true, passive: true });
|
|
59
|
+
window.addEventListener('mouseup', this.onUpBound, { capture: true, passive: true });
|
|
60
60
|
this.emitter.emit('stateChange', { isDragging: true, dropIndicatorX: null });
|
|
61
61
|
}
|
|
62
62
|
handleMouseMove(event) {
|
|
@@ -14,6 +14,7 @@ export class FillHandleState {
|
|
|
14
14
|
this.rafHandle = 0;
|
|
15
15
|
this.liveFillRange = null;
|
|
16
16
|
this.lastMousePos = null;
|
|
17
|
+
this.cachedCells = null;
|
|
17
18
|
this.params = params;
|
|
18
19
|
this.getSelectionRange = getSelectionRange;
|
|
19
20
|
this.setSelectionRange = setSelectionRange;
|
|
@@ -46,8 +47,10 @@ export class FillHandleState {
|
|
|
46
47
|
this.fillDragStart = { startRow: range.startRow, startCol: range.startCol };
|
|
47
48
|
this.fillDragEnd = { endRow: range.startRow, endCol: range.startCol };
|
|
48
49
|
this.liveFillRange = null;
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Cache querySelectorAll result once on drag start
|
|
51
|
+
this.cachedCells = this.wrapperRef ? this.wrapperRef.querySelectorAll('[data-row-index][data-col-index]') : null;
|
|
52
|
+
window.addEventListener('mousemove', this.onMoveBound, { capture: true, passive: true });
|
|
53
|
+
window.addEventListener('mouseup', this.onUpBound, { capture: true, passive: true });
|
|
51
54
|
}
|
|
52
55
|
onMouseMove(e) {
|
|
53
56
|
if (!this._isFillDragging || !this.fillDragStart)
|
|
@@ -170,15 +173,14 @@ export class FillHandleState {
|
|
|
170
173
|
});
|
|
171
174
|
}
|
|
172
175
|
applyDragAttrs(range) {
|
|
173
|
-
const
|
|
174
|
-
if (!
|
|
176
|
+
const cells = this.cachedCells;
|
|
177
|
+
if (!cells)
|
|
175
178
|
return;
|
|
176
179
|
const colOff = this.params.colOffset;
|
|
177
180
|
const minR = Math.min(range.startRow, range.endRow);
|
|
178
181
|
const maxR = Math.max(range.startRow, range.endRow);
|
|
179
182
|
const minC = Math.min(range.startCol, range.endCol);
|
|
180
183
|
const maxC = Math.max(range.startCol, range.endCol);
|
|
181
|
-
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
182
184
|
for (let i = 0; i < cells.length; i++) {
|
|
183
185
|
const el = cells[i];
|
|
184
186
|
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
@@ -195,12 +197,12 @@ export class FillHandleState {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
clearDragAttrs() {
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
const cells = this.cachedCells;
|
|
201
|
+
if (cells) {
|
|
202
|
+
for (let i = 0; i < cells.length; i++)
|
|
203
|
+
cells[i].removeAttribute('data-drag-range');
|
|
204
|
+
}
|
|
205
|
+
this.cachedCells = null;
|
|
204
206
|
}
|
|
205
207
|
onFillRangeChange(handler) {
|
|
206
208
|
this.emitter.on('fillRangeChange', handler);
|
|
@@ -107,7 +107,7 @@ export class HeaderFilterState {
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
setTimeout(() => {
|
|
110
|
-
document.addEventListener('mousedown', this._clickOutsideHandler);
|
|
110
|
+
document.addEventListener('mousedown', this._clickOutsideHandler, { passive: true });
|
|
111
111
|
}, 0);
|
|
112
112
|
document.addEventListener('keydown', this._escapeHandler, true);
|
|
113
113
|
this.emitter.emit('change', undefined);
|
|
@@ -38,7 +38,7 @@ export class TableLayoutState {
|
|
|
38
38
|
}
|
|
39
39
|
/** Set a column width override (from resize drag). */
|
|
40
40
|
setColumnOverride(columnId, widthPx) {
|
|
41
|
-
this._columnSizingOverrides
|
|
41
|
+
this._columnSizingOverrides[columnId] = widthPx;
|
|
42
42
|
this.emitter.emit('layoutChange', { type: 'columnOverride' });
|
|
43
43
|
}
|
|
44
44
|
/** Compute minimum table width from visible columns. */
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
+
import { UndoRedoStack } from '@alaarab/ogrid-core';
|
|
1
2
|
import { EventEmitter } from './EventEmitter';
|
|
2
3
|
export class UndoRedoState {
|
|
3
4
|
constructor(onCellValueChanged, maxUndoDepth = 100) {
|
|
4
5
|
this.onCellValueChanged = onCellValueChanged;
|
|
5
6
|
this.emitter = new EventEmitter();
|
|
6
|
-
this.
|
|
7
|
-
this.redoStack = [];
|
|
8
|
-
this.batch = null;
|
|
9
|
-
this.maxUndoDepth = maxUndoDepth;
|
|
7
|
+
this.stack = new UndoRedoStack(maxUndoDepth);
|
|
10
8
|
if (onCellValueChanged) {
|
|
11
9
|
this.wrappedCallback = (event) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
this.historyStack = [...this.historyStack, [event]].slice(-this.maxUndoDepth);
|
|
17
|
-
this.redoStack = [];
|
|
10
|
+
this.stack.record(event);
|
|
11
|
+
if (!this.stack.isBatching) {
|
|
18
12
|
this.emitStackChange();
|
|
19
13
|
}
|
|
20
14
|
onCellValueChanged(event);
|
|
@@ -22,32 +16,27 @@ export class UndoRedoState {
|
|
|
22
16
|
}
|
|
23
17
|
}
|
|
24
18
|
get canUndo() {
|
|
25
|
-
return this.
|
|
19
|
+
return this.stack.canUndo;
|
|
26
20
|
}
|
|
27
21
|
get canRedo() {
|
|
28
|
-
return this.
|
|
22
|
+
return this.stack.canRedo;
|
|
29
23
|
}
|
|
30
24
|
getWrappedCallback() {
|
|
31
25
|
return this.wrappedCallback;
|
|
32
26
|
}
|
|
33
27
|
beginBatch() {
|
|
34
|
-
this.
|
|
28
|
+
this.stack.beginBatch();
|
|
35
29
|
}
|
|
36
30
|
endBatch() {
|
|
37
|
-
|
|
38
|
-
this.batch = null;
|
|
39
|
-
if (!currentBatch || currentBatch.length === 0)
|
|
40
|
-
return;
|
|
41
|
-
this.historyStack = [...this.historyStack, currentBatch].slice(-this.maxUndoDepth);
|
|
42
|
-
this.redoStack = [];
|
|
31
|
+
this.stack.endBatch();
|
|
43
32
|
this.emitStackChange();
|
|
44
33
|
}
|
|
45
34
|
undo() {
|
|
46
|
-
if (!this.onCellValueChanged
|
|
35
|
+
if (!this.onCellValueChanged)
|
|
36
|
+
return;
|
|
37
|
+
const lastBatch = this.stack.undo();
|
|
38
|
+
if (!lastBatch)
|
|
47
39
|
return;
|
|
48
|
-
const lastBatch = this.historyStack[this.historyStack.length - 1];
|
|
49
|
-
this.historyStack = this.historyStack.slice(0, -1);
|
|
50
|
-
this.redoStack = [...this.redoStack, lastBatch];
|
|
51
40
|
this.emitStackChange();
|
|
52
41
|
for (let i = lastBatch.length - 1; i >= 0; i--) {
|
|
53
42
|
const ev = lastBatch[i];
|
|
@@ -59,11 +48,11 @@ export class UndoRedoState {
|
|
|
59
48
|
}
|
|
60
49
|
}
|
|
61
50
|
redo() {
|
|
62
|
-
if (!this.onCellValueChanged
|
|
51
|
+
if (!this.onCellValueChanged)
|
|
52
|
+
return;
|
|
53
|
+
const nextBatch = this.stack.redo();
|
|
54
|
+
if (!nextBatch)
|
|
63
55
|
return;
|
|
64
|
-
const nextBatch = this.redoStack[this.redoStack.length - 1];
|
|
65
|
-
this.redoStack = this.redoStack.slice(0, -1);
|
|
66
|
-
this.historyStack = [...this.historyStack, nextBatch];
|
|
67
56
|
this.emitStackChange();
|
|
68
57
|
for (const ev of nextBatch) {
|
|
69
58
|
this.onCellValueChanged(ev);
|
package/dist/styles/ogrid.css
CHANGED
|
@@ -177,6 +177,10 @@
|
|
|
177
177
|
table-layout: auto;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
.ogrid-table tbody tr {
|
|
181
|
+
height: var(--ogrid-row-height, auto);
|
|
182
|
+
}
|
|
183
|
+
|
|
180
184
|
.ogrid-table th,
|
|
181
185
|
.ogrid-table td {
|
|
182
186
|
min-width: 80px;
|
|
@@ -220,12 +224,6 @@
|
|
|
220
224
|
user-select: none;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
|
-
.ogrid-sort-indicator {
|
|
224
|
-
margin-left: 4px;
|
|
225
|
-
font-size: 12px;
|
|
226
|
-
color: var(--ogrid-muted, #616161);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
227
|
.ogrid-group-header {
|
|
230
228
|
text-align: center;
|
|
231
229
|
font-weight: 600;
|
|
@@ -44,14 +44,43 @@ export declare class TableRenderer<T> {
|
|
|
44
44
|
private onFilterIconClick;
|
|
45
45
|
private dropIndicator;
|
|
46
46
|
private virtualScrollState;
|
|
47
|
+
private _tbodyClickHandler;
|
|
48
|
+
private _tbodyMousedownHandler;
|
|
49
|
+
private _tbodyDblclickHandler;
|
|
50
|
+
private _tbodyContextmenuHandler;
|
|
51
|
+
private lastActiveCell;
|
|
52
|
+
private lastSelectionRange;
|
|
53
|
+
private lastCopyRange;
|
|
54
|
+
private lastCutRange;
|
|
55
|
+
private lastEditingCell;
|
|
56
|
+
private lastColumnWidths;
|
|
57
|
+
private lastHeaderSignature;
|
|
58
|
+
private lastRenderedItems;
|
|
59
|
+
private lastRowSelectionMode;
|
|
60
|
+
private lastSelectedRowIds;
|
|
61
|
+
private lastShowRowNumbers;
|
|
62
|
+
private lastPinnedColumns;
|
|
63
|
+
private lastAllSelected;
|
|
64
|
+
private lastSomeSelected;
|
|
47
65
|
constructor(container: HTMLElement, state: GridState<T>);
|
|
48
66
|
setVirtualScrollState(vs: VirtualScrollState): void;
|
|
49
67
|
setHeaderFilterState(state: HeaderFilterState, configs: Map<string, HeaderFilterConfig>): void;
|
|
50
68
|
setOnFilterIconClick(handler: (columnId: string, headerEl: HTMLElement) => void): void;
|
|
51
69
|
setInteractionState(state: TableRendererInteractionState | null): void;
|
|
70
|
+
private getCellFromEvent;
|
|
71
|
+
private attachBodyDelegation;
|
|
72
|
+
private detachBodyDelegation;
|
|
52
73
|
getWrapperElement(): HTMLDivElement | null;
|
|
53
74
|
/** Full render — creates the table structure from scratch. */
|
|
54
75
|
render(): void;
|
|
76
|
+
/** Compute a signature string that captures header-affecting state. */
|
|
77
|
+
private computeHeaderSignature;
|
|
78
|
+
/** Save current interaction state for next diff comparison. */
|
|
79
|
+
private snapshotState;
|
|
80
|
+
/** Check if only selection/active-cell/copy/cut ranges changed (no data or header changes). */
|
|
81
|
+
private isSelectionOnlyChange;
|
|
82
|
+
/** Patch only CSS classes/styles for selection, active cell, copy/cut ranges without rebuilding DOM. */
|
|
83
|
+
private patchSelectionClasses;
|
|
55
84
|
/** Re-render body rows and header (after sort/filter/page change). */
|
|
56
85
|
update(): void;
|
|
57
86
|
private hasCheckboxColumn;
|
|
@@ -30,6 +30,7 @@ export declare class FillHandleState<T> {
|
|
|
30
30
|
private rafHandle;
|
|
31
31
|
private liveFillRange;
|
|
32
32
|
private lastMousePos;
|
|
33
|
+
private cachedCells;
|
|
33
34
|
private onMoveBound;
|
|
34
35
|
private onUpBound;
|
|
35
36
|
constructor(params: FillHandleParams<T>, getSelectionRange: () => ISelectionRange | null, setSelectionRange: (range: ISelectionRange | null) => void, setActiveCell: (cell: IActiveCell | null) => void);
|
|
@@ -8,10 +8,7 @@ interface UndoRedoStateEvents extends Record<string, unknown> {
|
|
|
8
8
|
export declare class UndoRedoState<T> {
|
|
9
9
|
private onCellValueChanged;
|
|
10
10
|
private emitter;
|
|
11
|
-
private
|
|
12
|
-
private redoStack;
|
|
13
|
-
private batch;
|
|
14
|
-
private maxUndoDepth;
|
|
11
|
+
private stack;
|
|
15
12
|
private wrappedCallback;
|
|
16
13
|
constructor(onCellValueChanged: ((event: ICellValueChangedEvent<T>) => void) | undefined, maxUndoDepth?: number);
|
|
17
14
|
get canUndo(): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
|
|
2
|
-
import type { RowId, IFilters, IDataSource, RowSelectionMode, IRowSelectionChangeEvent, IOGridApi, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
|
|
2
|
+
import type { RowId, IFilters, IDataSource, RowSelectionMode, IRowSelectionChangeEvent, IOGridApi, ISideBarDef, IStatusBarProps, IVirtualScrollConfig } from '@alaarab/ogrid-core';
|
|
3
3
|
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IOGridApi, } from '@alaarab/ogrid-core';
|
|
4
4
|
/** Extended API for the vanilla JS package (adds methods not in the core IOGridApi). */
|
|
5
5
|
export interface IJsOGridApi<T> extends IOGridApi<T> {
|
|
@@ -41,6 +41,10 @@ export interface OGridOptions<T> {
|
|
|
41
41
|
onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
|
|
42
42
|
/** Show row numbers column. Default: false. */
|
|
43
43
|
showRowNumbers?: boolean;
|
|
44
|
+
/** Status bar configuration or boolean to enable/disable with defaults. */
|
|
45
|
+
statusBar?: boolean | IStatusBarProps;
|
|
46
|
+
/** Plural label for the entity type (e.g. 'items'). Used in status bar and empty state. */
|
|
47
|
+
entityLabelPlural?: string;
|
|
44
48
|
rowSelection?: RowSelectionMode;
|
|
45
49
|
/** Callback fired when row selection changes. */
|
|
46
50
|
onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
|
|
@@ -55,14 +59,50 @@ export interface OGridOptions<T> {
|
|
|
55
59
|
emptyMessage?: string;
|
|
56
60
|
/** Accessible label for the grid. */
|
|
57
61
|
'aria-label'?: string;
|
|
62
|
+
/** Accessible label reference for the grid (ID of a labelling element). */
|
|
63
|
+
'aria-labelledby'?: string;
|
|
58
64
|
/** Side bar configuration (columns panel + filters panel). */
|
|
59
65
|
sideBar?: boolean | ISideBarDef;
|
|
60
66
|
/** Error callback for server-side data source failures. */
|
|
61
67
|
onError?: (error: unknown) => void;
|
|
68
|
+
/** Called when a cell editor throws an error. JS alternative: listen to the 'cellError' event. */
|
|
69
|
+
onCellError?: (error: Error, info: unknown) => void;
|
|
70
|
+
/** Called when undo is triggered. JS alternative: listen to the 'undo' event. */
|
|
71
|
+
onUndo?: () => void;
|
|
72
|
+
/** Called when redo is triggered. JS alternative: listen to the 'redo' event. */
|
|
73
|
+
onRedo?: () => void;
|
|
74
|
+
/** Whether there are undo operations available. */
|
|
75
|
+
canUndo?: boolean;
|
|
76
|
+
/** Whether there are redo operations available. */
|
|
77
|
+
canRedo?: boolean;
|
|
78
|
+
/** Called when the current page changes. JS alternative: listen to the 'pageChange' event. */
|
|
79
|
+
onPageChange?: (page: number) => void;
|
|
80
|
+
/** Called when the page size changes. JS alternative: listen to the 'pageSizeChange' event. */
|
|
81
|
+
onPageSizeChange?: (size: number) => void;
|
|
62
82
|
/** Callback fired when first data is rendered. */
|
|
63
83
|
onFirstDataRendered?: () => void;
|
|
64
84
|
/** Virtual scrolling configuration. */
|
|
65
85
|
virtualScroll?: IVirtualScrollConfig;
|
|
86
|
+
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
87
|
+
rowHeight?: number;
|
|
88
|
+
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
89
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
90
|
+
/** Enable column reordering via drag-and-drop on header cells. Default: false. */
|
|
91
|
+
columnReorder?: boolean;
|
|
92
|
+
/** Page size options shown in the pagination dropdown. Default: [10, 20, 50, 100]. */
|
|
93
|
+
pageSizeOptions?: number[];
|
|
94
|
+
/** Initial column display order (array of column ids). */
|
|
95
|
+
columnOrder?: string[];
|
|
96
|
+
/** Callback fired when column order changes. */
|
|
97
|
+
onColumnOrderChange?: (order: string[]) => void;
|
|
98
|
+
/** Callback fired when a column is resized. */
|
|
99
|
+
onColumnResized?: (columnId: string, width: number) => void;
|
|
100
|
+
/** Callback fired when a column is pinned or unpinned. */
|
|
101
|
+
onColumnPinned?: (columnId: string, pin: 'left' | 'right' | null) => void;
|
|
102
|
+
/** Where the column chooser renders. `true` or `'toolbar'` (default): toolbar. `'sidebar'`: sidebar only. `false`: hidden. */
|
|
103
|
+
columnChooser?: boolean | 'toolbar' | 'sidebar';
|
|
104
|
+
/** Secondary toolbar row rendered below the primary toolbar. */
|
|
105
|
+
toolbarBelow?: HTMLElement | null;
|
|
66
106
|
}
|
|
67
107
|
/** Events emitted by the OGrid instance. */
|
|
68
108
|
export interface OGridEvents<T> extends Record<string, unknown> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-js",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@alaarab/ogrid-core": "2.0.
|
|
39
|
+
"@alaarab/ogrid-core": "2.0.22"
|
|
40
40
|
},
|
|
41
41
|
"sideEffects": false,
|
|
42
42
|
"publishConfig": {
|