@alaarab/ogrid-js 2.0.2 → 2.0.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/OGrid.js +105 -6
- package/dist/esm/components/ContextMenu.js +11 -8
- package/dist/esm/components/InlineCellEditor.js +53 -9
- package/dist/esm/index.js +4 -0
- package/dist/esm/renderer/TableRenderer.js +131 -5
- package/dist/esm/state/ColumnPinningState.js +7 -2
- package/dist/esm/state/ColumnReorderState.js +128 -0
- package/dist/esm/state/GridState.js +38 -5
- package/dist/esm/state/SelectionState.js +15 -0
- package/dist/esm/state/VirtualScrollState.js +128 -0
- package/dist/esm/utils/debounce.js +41 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/styles/ogrid.css +49 -4
- package/dist/types/OGrid.d.ts +2 -0
- package/dist/types/components/InlineCellEditor.d.ts +3 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/renderer/TableRenderer.d.ts +12 -1
- package/dist/types/state/ColumnPinningState.d.ts +1 -1
- package/dist/types/state/ColumnReorderState.d.ts +43 -0
- package/dist/types/state/GridState.d.ts +5 -1
- package/dist/types/state/SelectionState.d.ts +1 -0
- package/dist/types/state/VirtualScrollState.d.ts +51 -0
- package/dist/types/types/gridTypes.d.ts +14 -2
- package/dist/types/utils/debounce.d.ts +25 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +2 -2
package/dist/esm/OGrid.js
CHANGED
|
@@ -16,6 +16,8 @@ import { TableLayoutState } from './state/TableLayoutState';
|
|
|
16
16
|
import { FillHandleState } from './state/FillHandleState';
|
|
17
17
|
import { RowSelectionState } from './state/RowSelectionState';
|
|
18
18
|
import { ColumnPinningState } from './state/ColumnPinningState';
|
|
19
|
+
import { ColumnReorderState } from './state/ColumnReorderState';
|
|
20
|
+
import { VirtualScrollState } from './state/VirtualScrollState';
|
|
19
21
|
import { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
20
22
|
import { InlineCellEditor } from './components/InlineCellEditor';
|
|
21
23
|
import { ContextMenu } from './components/ContextMenu';
|
|
@@ -41,6 +43,8 @@ export class OGrid {
|
|
|
41
43
|
this.fillHandleState = null;
|
|
42
44
|
this.rowSelectionState = null;
|
|
43
45
|
this.pinningState = null;
|
|
46
|
+
this.reorderState = null;
|
|
47
|
+
this.virtualScrollState = null;
|
|
44
48
|
this.marchingAnts = null;
|
|
45
49
|
this.cellEditor = null;
|
|
46
50
|
this.contextMenu = null;
|
|
@@ -159,6 +163,28 @@ export class OGrid {
|
|
|
159
163
|
this.unsubscribes.push(this.headerFilterState.onChange(() => {
|
|
160
164
|
this.renderHeaderFilterPopover();
|
|
161
165
|
}));
|
|
166
|
+
// Initialize virtual scrolling if configured
|
|
167
|
+
if (options.virtualScroll?.enabled) {
|
|
168
|
+
this.virtualScrollState = new VirtualScrollState(options.virtualScroll);
|
|
169
|
+
this.virtualScrollState.observeContainer(this.tableContainer);
|
|
170
|
+
this.renderer.setVirtualScrollState(this.virtualScrollState);
|
|
171
|
+
// Wire scroll event on the table container
|
|
172
|
+
const handleScroll = () => {
|
|
173
|
+
this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
|
|
174
|
+
};
|
|
175
|
+
this.tableContainer.addEventListener('scroll', handleScroll, { passive: true });
|
|
176
|
+
this.unsubscribes.push(() => {
|
|
177
|
+
this.tableContainer.removeEventListener('scroll', handleScroll);
|
|
178
|
+
});
|
|
179
|
+
// Re-render when visible range changes
|
|
180
|
+
this.unsubscribes.push(this.virtualScrollState.onRangeChanged(() => {
|
|
181
|
+
this.updateRendererInteractionState();
|
|
182
|
+
}));
|
|
183
|
+
// Wire scrollToRow API method
|
|
184
|
+
this.api.scrollToRow = (index, opts) => {
|
|
185
|
+
this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
162
188
|
// Complete initial render (pagination, status bar, column chooser, sidebar, loading)
|
|
163
189
|
this.renderAll();
|
|
164
190
|
}
|
|
@@ -225,6 +251,14 @@ export class OGrid {
|
|
|
225
251
|
this.unsubscribes.push(this.resizeState.onColumnWidthChange(() => {
|
|
226
252
|
this.updateRendererInteractionState();
|
|
227
253
|
}));
|
|
254
|
+
// Column reorder
|
|
255
|
+
this.reorderState = new ColumnReorderState();
|
|
256
|
+
this.unsubscribes.push(this.reorderState.onStateChange(({ isDragging, dropIndicatorX }) => {
|
|
257
|
+
this.renderer.updateDropIndicator(dropIndicatorX, isDragging);
|
|
258
|
+
}));
|
|
259
|
+
this.unsubscribes.push(this.reorderState.onReorder(({ columnOrder }) => {
|
|
260
|
+
this.state.setColumnOrder(columnOrder);
|
|
261
|
+
}));
|
|
228
262
|
// Attach keyboard handler to wrapper
|
|
229
263
|
const wrapper = this.renderer.getWrapperElement();
|
|
230
264
|
if (wrapper) {
|
|
@@ -307,7 +341,7 @@ export class OGrid {
|
|
|
307
341
|
const visibleCols = this.state.visibleColumnDefs;
|
|
308
342
|
// Compute pinning offsets
|
|
309
343
|
const columnWidths = this.layoutState.getAllColumnWidths();
|
|
310
|
-
const leftOffsets = this.pinningState?.computeLeftOffsets(visibleCols, columnWidths, 120, !!this.rowSelectionState, 40) ?? {};
|
|
344
|
+
const leftOffsets = this.pinningState?.computeLeftOffsets(visibleCols, columnWidths, 120, !!this.rowSelectionState, 40, !!this.options.showRowNumbers) ?? {};
|
|
311
345
|
const rightOffsets = this.pinningState?.computeRightOffsets(visibleCols, columnWidths, 120) ?? {};
|
|
312
346
|
this.renderer.setInteractionState({
|
|
313
347
|
activeCell: this.selectionState.activeCell,
|
|
@@ -334,10 +368,19 @@ export class OGrid {
|
|
|
334
368
|
},
|
|
335
369
|
allSelected: this.rowSelectionState?.isAllSelected(items),
|
|
336
370
|
someSelected: this.rowSelectionState?.isSomeSelected(items),
|
|
371
|
+
// Row numbers
|
|
372
|
+
showRowNumbers: this.options.showRowNumbers,
|
|
337
373
|
// Column pinning
|
|
338
374
|
pinnedColumns: this.pinningState?.pinnedColumns,
|
|
339
375
|
leftOffsets,
|
|
340
376
|
rightOffsets,
|
|
377
|
+
// Column reorder
|
|
378
|
+
onColumnReorderStart: this.reorderState ? (columnId, event) => {
|
|
379
|
+
const tableEl = this.renderer.getTableElement();
|
|
380
|
+
if (!tableEl)
|
|
381
|
+
return;
|
|
382
|
+
this.reorderState?.startDrag(columnId, event, visibleCols, this.state.columnOrder, this.pinningState?.pinnedColumns, tableEl);
|
|
383
|
+
} : undefined,
|
|
341
384
|
});
|
|
342
385
|
this.renderer.update();
|
|
343
386
|
// Update marching ants overlay
|
|
@@ -351,15 +394,43 @@ export class OGrid {
|
|
|
351
394
|
if (!range)
|
|
352
395
|
return;
|
|
353
396
|
const norm = normalizeSelectionRange(range);
|
|
397
|
+
const anchor = this.selectionState.dragAnchor;
|
|
398
|
+
const minR = norm.startRow;
|
|
399
|
+
const maxR = norm.endRow;
|
|
400
|
+
const minC = norm.startCol;
|
|
401
|
+
const maxC = norm.endCol;
|
|
354
402
|
const cells = wrapper.querySelectorAll('td[data-row-index][data-col-index]');
|
|
355
403
|
for (const cell of Array.from(cells)) {
|
|
356
|
-
const
|
|
357
|
-
const
|
|
404
|
+
const el = cell;
|
|
405
|
+
const rowIndex = parseInt(el.getAttribute('data-row-index') ?? '-1', 10);
|
|
406
|
+
const colIndex = parseInt(el.getAttribute('data-col-index') ?? '-1', 10);
|
|
358
407
|
if (isInSelectionRange(norm, rowIndex, colIndex)) {
|
|
359
|
-
|
|
408
|
+
el.setAttribute('data-drag-range', 'true');
|
|
409
|
+
// Anchor cell (white background)
|
|
410
|
+
const isAnchor = anchor && rowIndex === anchor.rowIndex && colIndex === anchor.columnIndex;
|
|
411
|
+
if (isAnchor) {
|
|
412
|
+
el.setAttribute('data-drag-anchor', '');
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
el.removeAttribute('data-drag-anchor');
|
|
416
|
+
}
|
|
417
|
+
// Edge borders via inset box-shadow
|
|
418
|
+
const shadows = [];
|
|
419
|
+
if (rowIndex === minR)
|
|
420
|
+
shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
|
|
421
|
+
if (rowIndex === maxR)
|
|
422
|
+
shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
|
|
423
|
+
if (colIndex === minC)
|
|
424
|
+
shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
425
|
+
if (colIndex === maxC)
|
|
426
|
+
shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
427
|
+
el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
|
|
360
428
|
}
|
|
361
429
|
else {
|
|
362
|
-
|
|
430
|
+
el.removeAttribute('data-drag-range');
|
|
431
|
+
el.removeAttribute('data-drag-anchor');
|
|
432
|
+
if (el.style.boxShadow)
|
|
433
|
+
el.style.boxShadow = '';
|
|
363
434
|
}
|
|
364
435
|
}
|
|
365
436
|
}
|
|
@@ -375,6 +446,8 @@ export class OGrid {
|
|
|
375
446
|
return;
|
|
376
447
|
e.preventDefault();
|
|
377
448
|
this.selectionState.startDrag(rowIndex, colIndex);
|
|
449
|
+
// Apply drag attributes immediately for instant visual feedback on the initial cell
|
|
450
|
+
setTimeout(() => this.updateDragAttributes(), 0);
|
|
378
451
|
}
|
|
379
452
|
handleCellContextMenu(rowIndex, colIndex, e) {
|
|
380
453
|
e.preventDefault();
|
|
@@ -452,7 +525,29 @@ export class OGrid {
|
|
|
452
525
|
const onCancel = () => {
|
|
453
526
|
this.updateRendererInteractionState();
|
|
454
527
|
};
|
|
455
|
-
|
|
528
|
+
const onAfterCommit = () => {
|
|
529
|
+
// After Enter-commit, move the active cell down one row (Excel-style behavior)
|
|
530
|
+
if (this.selectionState) {
|
|
531
|
+
const ac = this.selectionState.activeCell;
|
|
532
|
+
if (ac) {
|
|
533
|
+
const { items: currentItems } = this.state.getProcessedItems();
|
|
534
|
+
const newRow = Math.min(ac.rowIndex + 1, currentItems.length - 1);
|
|
535
|
+
this.selectionState.setActiveCell({ rowIndex: newRow, columnIndex: ac.columnIndex });
|
|
536
|
+
const colOffset = this.renderer.getColOffset();
|
|
537
|
+
const dataCol = ac.columnIndex - colOffset;
|
|
538
|
+
this.selectionState.setSelectionRange({
|
|
539
|
+
startRow: newRow,
|
|
540
|
+
startCol: dataCol,
|
|
541
|
+
endRow: newRow,
|
|
542
|
+
endCol: dataCol,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Re-focus the grid wrapper so keyboard nav continues working
|
|
547
|
+
const wrapper = this.renderer.getWrapperElement();
|
|
548
|
+
wrapper?.focus();
|
|
549
|
+
};
|
|
550
|
+
this.cellEditor.startEdit(rowId, columnId, item, column, cell, onCommit, onCancel, onAfterCommit);
|
|
456
551
|
}
|
|
457
552
|
buildFilterConfigs() {
|
|
458
553
|
const columns = flattenColumns(this.options.columns);
|
|
@@ -611,6 +706,8 @@ export class OGrid {
|
|
|
611
706
|
this.renderer.update();
|
|
612
707
|
}
|
|
613
708
|
const { totalCount } = this.state.getProcessedItems();
|
|
709
|
+
// Update virtual scroll with current total row count
|
|
710
|
+
this.virtualScrollState?.setTotalRows(totalCount);
|
|
614
711
|
this.pagination.render(totalCount);
|
|
615
712
|
this.statusBar.render({ totalCount });
|
|
616
713
|
this.columnChooser.render();
|
|
@@ -644,6 +741,8 @@ export class OGrid {
|
|
|
644
741
|
this.fillHandleState?.destroy();
|
|
645
742
|
this.rowSelectionState?.destroy();
|
|
646
743
|
this.pinningState?.destroy();
|
|
744
|
+
this.reorderState?.destroy();
|
|
745
|
+
this.virtualScrollState?.destroy();
|
|
647
746
|
this.marchingAnts?.destroy();
|
|
648
747
|
this.layoutState.destroy();
|
|
649
748
|
this.cellEditor?.closeEditor();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
|
|
2
2
|
const MENU_STYLE = {
|
|
3
3
|
position: 'fixed',
|
|
4
|
-
backgroundColor: '
|
|
5
|
-
border: '1px solid #
|
|
6
|
-
boxShadow: '0
|
|
4
|
+
backgroundColor: 'var(--ogrid-bg, #fff)',
|
|
5
|
+
border: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
6
|
+
boxShadow: 'var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12))',
|
|
7
|
+
borderRadius: '6px',
|
|
7
8
|
zIndex: '10000',
|
|
8
9
|
minWidth: '180px',
|
|
9
10
|
padding: '4px 0',
|
|
10
11
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
11
12
|
fontSize: '14px',
|
|
13
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
12
14
|
};
|
|
13
15
|
const ITEM_STYLE = {
|
|
14
16
|
padding: '6px 12px',
|
|
@@ -19,7 +21,7 @@ const ITEM_STYLE = {
|
|
|
19
21
|
};
|
|
20
22
|
const DIVIDER_STYLE = {
|
|
21
23
|
height: '1px',
|
|
22
|
-
backgroundColor: '#e0e0e0',
|
|
24
|
+
backgroundColor: 'var(--ogrid-border, #e0e0e0)',
|
|
23
25
|
margin: '4px 0',
|
|
24
26
|
};
|
|
25
27
|
export class ContextMenu {
|
|
@@ -49,7 +51,7 @@ export class ContextMenu {
|
|
|
49
51
|
const shortcut = document.createElement('span');
|
|
50
52
|
shortcut.textContent = formatShortcut(item.shortcut);
|
|
51
53
|
shortcut.style.marginLeft = '20px';
|
|
52
|
-
shortcut.style.color = '#666';
|
|
54
|
+
shortcut.style.color = 'var(--ogrid-muted, #666)';
|
|
53
55
|
shortcut.style.fontSize = '12px';
|
|
54
56
|
menuItem.appendChild(shortcut);
|
|
55
57
|
}
|
|
@@ -57,15 +59,16 @@ export class ContextMenu {
|
|
|
57
59
|
(item.id === 'redo' && !canRedo) ||
|
|
58
60
|
(item.disabledWhenNoSelection && selectionRange == null);
|
|
59
61
|
if (isDisabled) {
|
|
60
|
-
menuItem.style.color = '
|
|
62
|
+
menuItem.style.color = 'var(--ogrid-fg-muted, rgba(0, 0, 0, 0.4))';
|
|
63
|
+
menuItem.style.opacity = '0.5';
|
|
61
64
|
menuItem.style.cursor = 'not-allowed';
|
|
62
65
|
}
|
|
63
66
|
else {
|
|
64
67
|
menuItem.addEventListener('mouseenter', () => {
|
|
65
|
-
menuItem.style.backgroundColor = '#
|
|
68
|
+
menuItem.style.backgroundColor = 'var(--ogrid-bg-hover, #f5f5f5)';
|
|
66
69
|
});
|
|
67
70
|
menuItem.addEventListener('mouseleave', () => {
|
|
68
|
-
menuItem.style.backgroundColor = '
|
|
71
|
+
menuItem.style.backgroundColor = '';
|
|
69
72
|
});
|
|
70
73
|
menuItem.addEventListener('click', () => {
|
|
71
74
|
this.handleItemClick(item.id);
|
|
@@ -3,7 +3,9 @@ const EDITOR_STYLE = {
|
|
|
3
3
|
position: 'absolute',
|
|
4
4
|
zIndex: '1000',
|
|
5
5
|
boxSizing: 'border-box',
|
|
6
|
-
border: '2px solid #
|
|
6
|
+
border: '2px solid var(--ogrid-selection, #217346)',
|
|
7
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
8
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
7
9
|
outline: 'none',
|
|
8
10
|
fontFamily: 'inherit',
|
|
9
11
|
fontSize: 'inherit',
|
|
@@ -12,15 +14,19 @@ export class InlineCellEditor {
|
|
|
12
14
|
constructor(container) {
|
|
13
15
|
this.editor = null;
|
|
14
16
|
this.editingCell = null;
|
|
17
|
+
this.editingCellElement = null;
|
|
15
18
|
this.onCommit = null;
|
|
16
19
|
this.onCancel = null;
|
|
20
|
+
this.onAfterCommit = null;
|
|
17
21
|
this.container = container;
|
|
18
22
|
}
|
|
19
|
-
startEdit(rowId, columnId, item, column, cell, onCommit, onCancel) {
|
|
23
|
+
startEdit(rowId, columnId, item, column, cell, onCommit, onCancel, onAfterCommit) {
|
|
20
24
|
this.closeEditor();
|
|
21
25
|
this.editingCell = { rowId, columnId };
|
|
26
|
+
this.editingCellElement = cell;
|
|
22
27
|
this.onCommit = onCommit;
|
|
23
28
|
this.onCancel = onCancel;
|
|
29
|
+
this.onAfterCommit = onAfterCommit ?? null;
|
|
24
30
|
const value = getCellValue(item, column);
|
|
25
31
|
const rect = cell.getBoundingClientRect();
|
|
26
32
|
const containerRect = this.container.getBoundingClientRect();
|
|
@@ -39,6 +45,26 @@ export class InlineCellEditor {
|
|
|
39
45
|
return this.editingCell;
|
|
40
46
|
}
|
|
41
47
|
closeEditor() {
|
|
48
|
+
// Reset visibility on the cell that was being edited (Bug 1 & 2 fix:
|
|
49
|
+
// the renderer sets visibility:hidden on the editing cell, and it may
|
|
50
|
+
// not re-render before the next click lands, so we clear it explicitly).
|
|
51
|
+
// Look up the cell by data attributes since the original element reference
|
|
52
|
+
// may have been replaced by a re-render.
|
|
53
|
+
if (this.editingCell) {
|
|
54
|
+
const { rowId, columnId } = this.editingCell;
|
|
55
|
+
const row = this.container.querySelector(`tr[data-row-id="${rowId}"]`);
|
|
56
|
+
if (row) {
|
|
57
|
+
const td = row.querySelector(`td[data-column-id="${columnId}"]`);
|
|
58
|
+
if (td) {
|
|
59
|
+
td.style.visibility = '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (this.editingCellElement) {
|
|
64
|
+
// Also reset the original element in case it's still in the DOM
|
|
65
|
+
this.editingCellElement.style.visibility = '';
|
|
66
|
+
this.editingCellElement = null;
|
|
67
|
+
}
|
|
42
68
|
if (this.editor) {
|
|
43
69
|
this.editor.remove();
|
|
44
70
|
this.editor = null;
|
|
@@ -46,6 +72,7 @@ export class InlineCellEditor {
|
|
|
46
72
|
this.editingCell = null;
|
|
47
73
|
this.onCommit = null;
|
|
48
74
|
this.onCancel = null;
|
|
75
|
+
this.onAfterCommit = null;
|
|
49
76
|
}
|
|
50
77
|
createEditor(column, item, value, cell) {
|
|
51
78
|
const editorType = column.cellEditor;
|
|
@@ -93,13 +120,17 @@ export class InlineCellEditor {
|
|
|
93
120
|
input.addEventListener('keydown', (e) => {
|
|
94
121
|
if (e.key === 'Enter') {
|
|
95
122
|
e.preventDefault();
|
|
123
|
+
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
96
124
|
if (this.editingCell) {
|
|
97
125
|
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
98
126
|
}
|
|
127
|
+
const afterCommit = this.onAfterCommit;
|
|
99
128
|
this.closeEditor();
|
|
129
|
+
afterCommit?.(); // Move active cell down after closing
|
|
100
130
|
}
|
|
101
131
|
else if (e.key === 'Escape') {
|
|
102
132
|
e.preventDefault();
|
|
133
|
+
e.stopPropagation();
|
|
103
134
|
this.onCancel?.();
|
|
104
135
|
this.closeEditor();
|
|
105
136
|
}
|
|
@@ -129,6 +160,7 @@ export class InlineCellEditor {
|
|
|
129
160
|
input.addEventListener('keydown', (e) => {
|
|
130
161
|
if (e.key === 'Escape') {
|
|
131
162
|
e.preventDefault();
|
|
163
|
+
e.stopPropagation();
|
|
132
164
|
this.onCancel?.();
|
|
133
165
|
this.closeEditor();
|
|
134
166
|
}
|
|
@@ -148,13 +180,17 @@ export class InlineCellEditor {
|
|
|
148
180
|
input.addEventListener('keydown', (e) => {
|
|
149
181
|
if (e.key === 'Enter') {
|
|
150
182
|
e.preventDefault();
|
|
183
|
+
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
151
184
|
if (this.editingCell) {
|
|
152
185
|
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
153
186
|
}
|
|
187
|
+
const afterCommit = this.onAfterCommit;
|
|
154
188
|
this.closeEditor();
|
|
189
|
+
afterCommit?.(); // Move active cell down after closing
|
|
155
190
|
}
|
|
156
191
|
else if (e.key === 'Escape') {
|
|
157
192
|
e.preventDefault();
|
|
193
|
+
e.stopPropagation();
|
|
158
194
|
this.onCancel?.();
|
|
159
195
|
this.closeEditor();
|
|
160
196
|
}
|
|
@@ -165,6 +201,7 @@ export class InlineCellEditor {
|
|
|
165
201
|
}
|
|
166
202
|
this.closeEditor();
|
|
167
203
|
});
|
|
204
|
+
setTimeout(() => input.select(), 0);
|
|
168
205
|
return input;
|
|
169
206
|
}
|
|
170
207
|
createSelectEditor(value, column) {
|
|
@@ -187,6 +224,7 @@ export class InlineCellEditor {
|
|
|
187
224
|
select.addEventListener('keydown', (e) => {
|
|
188
225
|
if (e.key === 'Escape') {
|
|
189
226
|
e.preventDefault();
|
|
227
|
+
e.stopPropagation();
|
|
190
228
|
this.onCancel?.();
|
|
191
229
|
this.closeEditor();
|
|
192
230
|
}
|
|
@@ -249,24 +287,30 @@ export class InlineCellEditor {
|
|
|
249
287
|
input.addEventListener('keydown', (e) => {
|
|
250
288
|
if (e.key === 'Enter') {
|
|
251
289
|
e.preventDefault();
|
|
290
|
+
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
252
291
|
if (this.editingCell) {
|
|
253
292
|
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
254
293
|
}
|
|
294
|
+
const afterCommit = this.onAfterCommit;
|
|
255
295
|
this.closeEditor();
|
|
296
|
+
afterCommit?.(); // Move active cell down after closing
|
|
256
297
|
}
|
|
257
298
|
else if (e.key === 'Escape') {
|
|
258
299
|
e.preventDefault();
|
|
300
|
+
e.stopPropagation();
|
|
259
301
|
this.onCancel?.();
|
|
260
302
|
this.closeEditor();
|
|
261
303
|
}
|
|
262
304
|
});
|
|
263
|
-
input.addEventListener('blur', () => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
305
|
+
input.addEventListener('blur', (e) => {
|
|
306
|
+
const related = e.relatedTarget;
|
|
307
|
+
if (related && this.editor?.contains(related)) {
|
|
308
|
+
return; // Focus moved within the editor (e.g., to dropdown), don't close
|
|
309
|
+
}
|
|
310
|
+
if (this.editingCell) {
|
|
311
|
+
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
312
|
+
}
|
|
313
|
+
this.closeEditor();
|
|
270
314
|
});
|
|
271
315
|
renderOptions('');
|
|
272
316
|
setTimeout(() => input.select(), 0);
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Re-export core types + utils
|
|
2
2
|
export * from '@alaarab/ogrid-core';
|
|
3
|
+
// Utils
|
|
4
|
+
export { debounce } from './utils';
|
|
3
5
|
// Classes
|
|
4
6
|
export { OGrid } from './OGrid';
|
|
5
7
|
export { GridState } from './state/GridState';
|
|
@@ -19,6 +21,8 @@ export { ContextMenu } from './components/ContextMenu';
|
|
|
19
21
|
export { FillHandleState } from './state/FillHandleState';
|
|
20
22
|
export { RowSelectionState } from './state/RowSelectionState';
|
|
21
23
|
export { ColumnPinningState } from './state/ColumnPinningState';
|
|
24
|
+
export { ColumnReorderState } from './state/ColumnReorderState';
|
|
25
|
+
export { VirtualScrollState } from './state/VirtualScrollState';
|
|
22
26
|
export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
23
27
|
export { SideBarState } from './state/SideBarState';
|
|
24
28
|
export { HeaderFilterState } from './state/HeaderFilterState';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCellValue, buildHeaderRows, isInSelectionRange } from '@alaarab/ogrid-core';
|
|
1
|
+
import { getCellValue, buildHeaderRows, isInSelectionRange, ROW_NUMBER_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
2
|
const CHECKBOX_COL_WIDTH = 40;
|
|
3
3
|
export class TableRenderer {
|
|
4
4
|
constructor(container, state) {
|
|
@@ -10,9 +10,14 @@ export class TableRenderer {
|
|
|
10
10
|
this.headerFilterState = null;
|
|
11
11
|
this.filterConfigs = new Map();
|
|
12
12
|
this.onFilterIconClick = null;
|
|
13
|
+
this.dropIndicator = null;
|
|
14
|
+
this.virtualScrollState = null;
|
|
13
15
|
this.container = container;
|
|
14
16
|
this.state = state;
|
|
15
17
|
}
|
|
18
|
+
setVirtualScrollState(vs) {
|
|
19
|
+
this.virtualScrollState = vs;
|
|
20
|
+
}
|
|
16
21
|
setHeaderFilterState(state, configs) {
|
|
17
22
|
this.headerFilterState = state;
|
|
18
23
|
this.filterConfigs = configs;
|
|
@@ -53,6 +58,11 @@ export class TableRenderer {
|
|
|
53
58
|
this.renderBody();
|
|
54
59
|
this.table.appendChild(this.tbody);
|
|
55
60
|
wrapper.appendChild(this.table);
|
|
61
|
+
// Create drop indicator for column reorder (hidden by default)
|
|
62
|
+
this.dropIndicator = document.createElement('div');
|
|
63
|
+
this.dropIndicator.className = 'ogrid-drop-indicator';
|
|
64
|
+
this.dropIndicator.style.display = 'none';
|
|
65
|
+
wrapper.appendChild(this.dropIndicator);
|
|
56
66
|
this.container.appendChild(wrapper);
|
|
57
67
|
}
|
|
58
68
|
/** Re-render body rows and header (after sort/filter/page change). */
|
|
@@ -70,9 +80,17 @@ export class TableRenderer {
|
|
|
70
80
|
const mode = this.interactionState?.rowSelectionMode;
|
|
71
81
|
return mode === 'single' || mode === 'multiple';
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
hasRowNumbersColumn() {
|
|
84
|
+
return !!this.interactionState?.showRowNumbers;
|
|
85
|
+
}
|
|
86
|
+
/** The column index offset for data columns (checkbox + row numbers if present). */
|
|
74
87
|
getColOffset() {
|
|
75
|
-
|
|
88
|
+
let offset = 0;
|
|
89
|
+
if (this.hasCheckboxColumn())
|
|
90
|
+
offset++;
|
|
91
|
+
if (this.hasRowNumbersColumn())
|
|
92
|
+
offset++;
|
|
93
|
+
return offset;
|
|
76
94
|
}
|
|
77
95
|
applyPinningStyles(el, columnId, isHeader) {
|
|
78
96
|
const is = this.interactionState;
|
|
@@ -140,7 +158,21 @@ export class TableRenderer {
|
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
160
|
if (!cell.isGroup && cell.columnDef) {
|
|
161
|
+
th.setAttribute('data-column-id', cell.columnDef.columnId);
|
|
143
162
|
this.applyPinningStyles(th, cell.columnDef.columnId, true);
|
|
163
|
+
// Column reorder in grouped headers
|
|
164
|
+
if (this.interactionState?.onColumnReorderStart) {
|
|
165
|
+
th.addEventListener('mousedown', (e) => {
|
|
166
|
+
const target = e.target;
|
|
167
|
+
if (target.classList.contains('ogrid-resize-handle') ||
|
|
168
|
+
target.classList.contains('ogrid-filter-icon')) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (cell.columnDef) {
|
|
172
|
+
this.interactionState?.onColumnReorderStart?.(cell.columnDef.columnId, e);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
144
176
|
}
|
|
145
177
|
tr.appendChild(th);
|
|
146
178
|
}
|
|
@@ -158,6 +190,15 @@ export class TableRenderer {
|
|
|
158
190
|
this.appendSelectAllCheckbox(th);
|
|
159
191
|
tr.appendChild(th);
|
|
160
192
|
}
|
|
193
|
+
// Row numbers header
|
|
194
|
+
if (this.hasRowNumbersColumn()) {
|
|
195
|
+
const th = document.createElement('th');
|
|
196
|
+
th.className = 'ogrid-header-cell ogrid-row-number-header';
|
|
197
|
+
th.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
|
|
198
|
+
th.style.textAlign = 'center';
|
|
199
|
+
th.textContent = '#';
|
|
200
|
+
tr.appendChild(th);
|
|
201
|
+
}
|
|
161
202
|
for (let colIdx = 0; colIdx < visibleCols.length; colIdx++) {
|
|
162
203
|
const col = visibleCols[colIdx];
|
|
163
204
|
const th = document.createElement('th');
|
|
@@ -232,6 +273,18 @@ export class TableRenderer {
|
|
|
232
273
|
});
|
|
233
274
|
th.appendChild(filterBtn);
|
|
234
275
|
}
|
|
276
|
+
// Column reorder: mousedown on header starts drag
|
|
277
|
+
if (this.interactionState?.onColumnReorderStart) {
|
|
278
|
+
th.addEventListener('mousedown', (e) => {
|
|
279
|
+
// Don't start reorder if clicking resize handle or filter button
|
|
280
|
+
const target = e.target;
|
|
281
|
+
if (target.classList.contains('ogrid-resize-handle') ||
|
|
282
|
+
target.classList.contains('ogrid-filter-icon')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
this.interactionState?.onColumnReorderStart?.(col.columnId, e);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
235
288
|
tr.appendChild(th);
|
|
236
289
|
}
|
|
237
290
|
this.thead.appendChild(tr);
|
|
@@ -258,19 +311,47 @@ export class TableRenderer {
|
|
|
258
311
|
const visibleCols = this.state.visibleColumnDefs;
|
|
259
312
|
const { items } = this.state.getProcessedItems();
|
|
260
313
|
const hasCheckbox = this.hasCheckboxColumn();
|
|
314
|
+
const hasRowNumbers = this.hasRowNumbersColumn();
|
|
261
315
|
const colOffset = this.getColOffset();
|
|
316
|
+
const totalColSpan = visibleCols.length + colOffset;
|
|
317
|
+
// Calculate row number offset for pagination
|
|
318
|
+
const rowNumberOffset = hasRowNumbers ? (this.state.page - 1) * this.state.pageSize : 0;
|
|
262
319
|
if (items.length === 0 && !this.state.isLoading) {
|
|
263
320
|
const tr = document.createElement('tr');
|
|
264
321
|
const td = document.createElement('td');
|
|
265
|
-
td.colSpan =
|
|
322
|
+
td.colSpan = totalColSpan;
|
|
266
323
|
td.className = 'ogrid-empty-state';
|
|
267
324
|
td.textContent = 'No data';
|
|
268
325
|
tr.appendChild(td);
|
|
269
326
|
this.tbody.appendChild(tr);
|
|
270
327
|
return;
|
|
271
328
|
}
|
|
272
|
-
|
|
329
|
+
// Virtual scrolling: determine which rows to render
|
|
330
|
+
const vs = this.virtualScrollState;
|
|
331
|
+
const isVirtual = vs?.enabled === true;
|
|
332
|
+
let startIndex = 0;
|
|
333
|
+
let endIndex = items.length - 1;
|
|
334
|
+
if (isVirtual) {
|
|
335
|
+
const range = vs.visibleRange;
|
|
336
|
+
startIndex = Math.max(0, range.startIndex);
|
|
337
|
+
endIndex = Math.min(items.length - 1, range.endIndex);
|
|
338
|
+
// Top spacer row
|
|
339
|
+
if (range.offsetTop > 0) {
|
|
340
|
+
const topSpacer = document.createElement('tr');
|
|
341
|
+
topSpacer.className = 'ogrid-virtual-spacer';
|
|
342
|
+
const topTd = document.createElement('td');
|
|
343
|
+
topTd.colSpan = totalColSpan;
|
|
344
|
+
topTd.style.height = `${range.offsetTop}px`;
|
|
345
|
+
topTd.style.padding = '0';
|
|
346
|
+
topTd.style.border = 'none';
|
|
347
|
+
topSpacer.appendChild(topTd);
|
|
348
|
+
this.tbody.appendChild(topSpacer);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (let rowIndex = startIndex; rowIndex <= endIndex; rowIndex++) {
|
|
273
352
|
const item = items[rowIndex];
|
|
353
|
+
if (!item)
|
|
354
|
+
continue;
|
|
274
355
|
const rowId = this.state.getRowId(item);
|
|
275
356
|
const tr = document.createElement('tr');
|
|
276
357
|
tr.className = 'ogrid-row';
|
|
@@ -298,6 +379,17 @@ export class TableRenderer {
|
|
|
298
379
|
td.appendChild(checkbox);
|
|
299
380
|
tr.appendChild(td);
|
|
300
381
|
}
|
|
382
|
+
// Row numbers column
|
|
383
|
+
if (hasRowNumbers) {
|
|
384
|
+
const td = document.createElement('td');
|
|
385
|
+
td.className = 'ogrid-cell ogrid-row-number-cell';
|
|
386
|
+
td.style.width = `${ROW_NUMBER_COLUMN_WIDTH}px`;
|
|
387
|
+
td.style.textAlign = 'center';
|
|
388
|
+
td.style.color = 'var(--ogrid-fg-muted, #666)';
|
|
389
|
+
td.style.fontSize = '0.9em';
|
|
390
|
+
td.textContent = String(rowNumberOffset + rowIndex + 1);
|
|
391
|
+
tr.appendChild(td);
|
|
392
|
+
}
|
|
301
393
|
for (let colIndex = 0; colIndex < visibleCols.length; colIndex++) {
|
|
302
394
|
const col = visibleCols[colIndex];
|
|
303
395
|
const globalColIndex = colIndex + colOffset;
|
|
@@ -404,11 +496,45 @@ export class TableRenderer {
|
|
|
404
496
|
}
|
|
405
497
|
this.tbody.appendChild(tr);
|
|
406
498
|
}
|
|
499
|
+
// Virtual scrolling: bottom spacer row
|
|
500
|
+
if (isVirtual) {
|
|
501
|
+
const range = vs.visibleRange;
|
|
502
|
+
if (range.offsetBottom > 0) {
|
|
503
|
+
const bottomSpacer = document.createElement('tr');
|
|
504
|
+
bottomSpacer.className = 'ogrid-virtual-spacer';
|
|
505
|
+
const bottomTd = document.createElement('td');
|
|
506
|
+
bottomTd.colSpan = totalColSpan;
|
|
507
|
+
bottomTd.style.height = `${range.offsetBottom}px`;
|
|
508
|
+
bottomTd.style.padding = '0';
|
|
509
|
+
bottomTd.style.border = 'none';
|
|
510
|
+
bottomSpacer.appendChild(bottomTd);
|
|
511
|
+
this.tbody.appendChild(bottomSpacer);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** Get the table element (used by ColumnReorderState for header cell queries). */
|
|
516
|
+
getTableElement() {
|
|
517
|
+
return this.table;
|
|
518
|
+
}
|
|
519
|
+
/** Update the drop indicator position during column reorder. */
|
|
520
|
+
updateDropIndicator(x, isDragging) {
|
|
521
|
+
if (!this.dropIndicator || !this.wrapperEl)
|
|
522
|
+
return;
|
|
523
|
+
if (!isDragging || x === null) {
|
|
524
|
+
this.dropIndicator.style.display = 'none';
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// Convert client X to position relative to the wrapper
|
|
528
|
+
const wrapperRect = this.wrapperEl.getBoundingClientRect();
|
|
529
|
+
const relativeX = x - wrapperRect.left + this.wrapperEl.scrollLeft;
|
|
530
|
+
this.dropIndicator.style.display = 'block';
|
|
531
|
+
this.dropIndicator.style.left = `${relativeX}px`;
|
|
407
532
|
}
|
|
408
533
|
destroy() {
|
|
409
534
|
this.container.innerHTML = '';
|
|
410
535
|
this.table = null;
|
|
411
536
|
this.thead = null;
|
|
412
537
|
this.tbody = null;
|
|
538
|
+
this.dropIndicator = null;
|
|
413
539
|
}
|
|
414
540
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROW_NUMBER_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
1
2
|
import { EventEmitter } from './EventEmitter';
|
|
2
3
|
/**
|
|
3
4
|
* Manages column pinning state — tracks which columns are pinned left/right.
|
|
@@ -40,9 +41,13 @@ export class ColumnPinningState {
|
|
|
40
41
|
* Compute sticky left offsets for left-pinned columns.
|
|
41
42
|
* Returns a map of columnId -> left offset in pixels.
|
|
42
43
|
*/
|
|
43
|
-
computeLeftOffsets(visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) {
|
|
44
|
+
computeLeftOffsets(visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth, hasRowNumbersColumn) {
|
|
44
45
|
const offsets = {};
|
|
45
|
-
let left =
|
|
46
|
+
let left = 0;
|
|
47
|
+
if (hasCheckboxColumn)
|
|
48
|
+
left += checkboxColumnWidth;
|
|
49
|
+
if (hasRowNumbersColumn)
|
|
50
|
+
left += ROW_NUMBER_COLUMN_WIDTH;
|
|
46
51
|
for (const col of visibleCols) {
|
|
47
52
|
if (this._pinnedColumns[col.columnId] === 'left') {
|
|
48
53
|
offsets[col.columnId] = left;
|