@alaarab/ogrid-js 2.0.23 → 2.1.1
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 +189 -503
- package/dist/esm/OGridEventWiring.js +178 -0
- package/dist/esm/OGridRendering.js +269 -0
- package/dist/esm/components/ColumnChooser.js +26 -3
- package/dist/esm/components/InlineCellEditor.js +18 -36
- package/dist/esm/index.js +2 -0
- package/dist/esm/renderer/TableRenderer.js +102 -61
- package/dist/esm/state/ClipboardState.js +8 -54
- package/dist/esm/state/ColumnPinningState.js +1 -2
- package/dist/esm/state/ColumnReorderState.js +8 -1
- package/dist/esm/state/EventEmitter.js +3 -2
- package/dist/esm/state/FillHandleState.js +27 -41
- package/dist/esm/state/GridState.js +36 -10
- package/dist/esm/state/HeaderFilterState.js +19 -11
- package/dist/esm/state/KeyboardNavState.js +19 -132
- package/dist/esm/state/RowSelectionState.js +6 -15
- package/dist/esm/state/SideBarState.js +1 -1
- package/dist/esm/state/TableLayoutState.js +6 -4
- package/dist/esm/utils/getCellCoordinates.js +15 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/types/OGrid.d.ts +97 -9
- package/dist/types/OGridEventWiring.d.ts +60 -0
- package/dist/types/OGridRendering.d.ts +93 -0
- package/dist/types/components/ColumnChooser.d.ts +5 -0
- package/dist/types/components/InlineCellEditor.d.ts +5 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/renderer/TableRenderer.d.ts +12 -5
- package/dist/types/state/EventEmitter.d.ts +1 -1
- package/dist/types/state/FillHandleState.d.ts +1 -1
- package/dist/types/state/GridState.d.ts +7 -1
- package/dist/types/state/HeaderFilterState.d.ts +2 -0
- package/dist/types/types/gridTypes.d.ts +15 -0
- package/dist/types/utils/getCellCoordinates.d.ts +8 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +11 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { SelectionState } from './state/SelectionState';
|
|
2
|
+
import { KeyboardNavState } from './state/KeyboardNavState';
|
|
3
|
+
import { ClipboardState } from './state/ClipboardState';
|
|
4
|
+
import { UndoRedoState } from './state/UndoRedoState';
|
|
5
|
+
import { ColumnResizeState } from './state/ColumnResizeState';
|
|
6
|
+
import { FillHandleState } from './state/FillHandleState';
|
|
7
|
+
import { ColumnReorderState } from './state/ColumnReorderState';
|
|
8
|
+
import { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
|
|
9
|
+
import { InlineCellEditor } from './components/InlineCellEditor';
|
|
10
|
+
import { ContextMenu } from './components/ContextMenu';
|
|
11
|
+
import { getCellCoordinates } from './utils/getCellCoordinates';
|
|
12
|
+
export class OGridEventWiring {
|
|
13
|
+
/**
|
|
14
|
+
* Creates all interaction states, subscribes to their events, and returns
|
|
15
|
+
* the state objects so OGrid can store them.
|
|
16
|
+
*/
|
|
17
|
+
initializeInteraction(options, state, renderer, tableContainer, layoutState, rowSelectionState, pinningState, callbacks) {
|
|
18
|
+
const { editable } = options;
|
|
19
|
+
const colOffset = rowSelectionState ? 1 : 0;
|
|
20
|
+
const unsubscribes = [];
|
|
21
|
+
// Create interaction states
|
|
22
|
+
const selectionState = new SelectionState();
|
|
23
|
+
const resizeState = new ColumnResizeState();
|
|
24
|
+
const contextMenu = new ContextMenu();
|
|
25
|
+
const cellEditor = new InlineCellEditor(tableContainer);
|
|
26
|
+
// Undo/Redo (wraps onCellValueChanged if editable)
|
|
27
|
+
const onCellValueChanged = options.onCellValueChanged;
|
|
28
|
+
const undoRedoState = new UndoRedoState(onCellValueChanged);
|
|
29
|
+
// Clipboard
|
|
30
|
+
const clipboardState = new ClipboardState({
|
|
31
|
+
items: [],
|
|
32
|
+
visibleCols: [],
|
|
33
|
+
colOffset,
|
|
34
|
+
editable,
|
|
35
|
+
onCellValueChanged: undoRedoState.getWrappedCallback(),
|
|
36
|
+
}, () => selectionState.activeCell ?? null, () => selectionState.selectionRange ?? null);
|
|
37
|
+
// Fill handle
|
|
38
|
+
const fillHandleState = new FillHandleState({
|
|
39
|
+
items: [],
|
|
40
|
+
visibleCols: [],
|
|
41
|
+
editable,
|
|
42
|
+
onCellValueChanged: undoRedoState.getWrappedCallback(),
|
|
43
|
+
colOffset,
|
|
44
|
+
beginBatch: () => undoRedoState.beginBatch(),
|
|
45
|
+
endBatch: () => undoRedoState.endBatch(),
|
|
46
|
+
}, () => selectionState.selectionRange ?? null, (range) => {
|
|
47
|
+
selectionState.setSelectionRange(range);
|
|
48
|
+
callbacks.updateRendererInteractionState();
|
|
49
|
+
}, (cell) => {
|
|
50
|
+
selectionState.setActiveCell(cell);
|
|
51
|
+
});
|
|
52
|
+
// Keyboard navigation
|
|
53
|
+
const keyboardNavState = new KeyboardNavState({
|
|
54
|
+
items: [],
|
|
55
|
+
visibleCols: [],
|
|
56
|
+
colOffset,
|
|
57
|
+
getRowId: state.getRowId,
|
|
58
|
+
editable,
|
|
59
|
+
onCellValueChanged: undoRedoState.getWrappedCallback(),
|
|
60
|
+
onCopy: () => clipboardState.handleCopy(),
|
|
61
|
+
onCut: () => clipboardState.handleCut(),
|
|
62
|
+
onPaste: async () => { await clipboardState.handlePaste(); },
|
|
63
|
+
onUndo: () => undoRedoState.undo(),
|
|
64
|
+
onRedo: () => undoRedoState.redo(),
|
|
65
|
+
onContextMenu: (x, y) => callbacks.showContextMenu(x, y),
|
|
66
|
+
onStartEdit: (rowId, columnId) => callbacks.startCellEdit(rowId, columnId),
|
|
67
|
+
clearClipboardRanges: () => clipboardState.clearClipboardRanges(),
|
|
68
|
+
}, () => selectionState.activeCell ?? null, () => selectionState.selectionRange ?? null, (cell) => selectionState.setActiveCell(cell), (range) => selectionState.setSelectionRange(range));
|
|
69
|
+
// Subscribe to selection changes
|
|
70
|
+
unsubscribes.push(selectionState.onSelectionChange(() => {
|
|
71
|
+
callbacks.updateRendererInteractionState();
|
|
72
|
+
}));
|
|
73
|
+
// Subscribe to clipboard range changes
|
|
74
|
+
unsubscribes.push(clipboardState.onRangesChange(() => {
|
|
75
|
+
callbacks.updateRendererInteractionState();
|
|
76
|
+
}));
|
|
77
|
+
// Subscribe to column resize changes
|
|
78
|
+
unsubscribes.push(resizeState.onColumnWidthChange(() => {
|
|
79
|
+
callbacks.updateRendererInteractionState();
|
|
80
|
+
}));
|
|
81
|
+
// Column reorder
|
|
82
|
+
const reorderState = new ColumnReorderState();
|
|
83
|
+
unsubscribes.push(reorderState.onStateChange(({ isDragging, dropIndicatorX }) => {
|
|
84
|
+
renderer.updateDropIndicator(dropIndicatorX, isDragging);
|
|
85
|
+
}));
|
|
86
|
+
unsubscribes.push(reorderState.onReorder(({ columnOrder }) => {
|
|
87
|
+
state.setColumnOrder(columnOrder);
|
|
88
|
+
}));
|
|
89
|
+
// Attach keyboard handler to wrapper
|
|
90
|
+
const wrapper = renderer.getWrapperElement();
|
|
91
|
+
let marchingAnts = null;
|
|
92
|
+
if (wrapper) {
|
|
93
|
+
wrapper.addEventListener('keydown', keyboardNavState.handleKeyDown);
|
|
94
|
+
keyboardNavState.setWrapperRef(wrapper);
|
|
95
|
+
fillHandleState.setWrapperRef(wrapper);
|
|
96
|
+
// Initialize marching ants overlay
|
|
97
|
+
marchingAnts = new MarchingAntsOverlay(wrapper, colOffset);
|
|
98
|
+
}
|
|
99
|
+
// Attach global mouse handlers for resize and drag
|
|
100
|
+
const globalUnsubs = this.attachGlobalHandlers(selectionState, resizeState, layoutState, renderer, callbacks);
|
|
101
|
+
unsubscribes.push(...globalUnsubs);
|
|
102
|
+
return {
|
|
103
|
+
selectionState,
|
|
104
|
+
keyboardNavState,
|
|
105
|
+
clipboardState,
|
|
106
|
+
undoRedoState,
|
|
107
|
+
resizeState,
|
|
108
|
+
fillHandleState,
|
|
109
|
+
reorderState,
|
|
110
|
+
marchingAnts,
|
|
111
|
+
cellEditor,
|
|
112
|
+
contextMenu,
|
|
113
|
+
unsubscribes,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
attachGlobalHandlers(selectionState, resizeState, layoutState, renderer, callbacks) {
|
|
117
|
+
const unsubs = [];
|
|
118
|
+
let resizing = false;
|
|
119
|
+
const handleMouseMove = (e) => {
|
|
120
|
+
if (resizing && resizeState) {
|
|
121
|
+
const newWidth = resizeState.updateResize(e.clientX);
|
|
122
|
+
if (newWidth !== null && resizeState.resizingColumnId) {
|
|
123
|
+
layoutState.setColumnOverride(resizeState.resizingColumnId, newWidth);
|
|
124
|
+
callbacks.updateRendererInteractionState();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (selectionState?.isDragging) {
|
|
128
|
+
const target = e.target;
|
|
129
|
+
if (target.tagName === 'TD') {
|
|
130
|
+
const coords = getCellCoordinates(target);
|
|
131
|
+
if (coords && coords.rowIndex >= 0 && coords.colIndex >= 0) {
|
|
132
|
+
selectionState.updateDrag(coords.rowIndex, coords.colIndex, () => callbacks.updateDragAttributes());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const handleMouseUp = (e) => {
|
|
138
|
+
if (resizing && resizeState) {
|
|
139
|
+
const colId = resizeState.resizingColumnId;
|
|
140
|
+
resizeState.endResize(e.clientX);
|
|
141
|
+
if (colId) {
|
|
142
|
+
const width = resizeState.getColumnWidth(colId);
|
|
143
|
+
if (width)
|
|
144
|
+
layoutState.setColumnOverride(colId, width);
|
|
145
|
+
}
|
|
146
|
+
resizing = false;
|
|
147
|
+
document.body.style.cursor = '';
|
|
148
|
+
callbacks.updateRendererInteractionState();
|
|
149
|
+
}
|
|
150
|
+
if (selectionState?.isDragging) {
|
|
151
|
+
selectionState.endDrag();
|
|
152
|
+
callbacks.clearCachedDragCells();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const handleResizeStart = (columnId, clientX, currentWidth) => {
|
|
156
|
+
resizing = true;
|
|
157
|
+
document.body.style.cursor = 'col-resize';
|
|
158
|
+
resizeState.startResize(columnId, clientX, currentWidth);
|
|
159
|
+
};
|
|
160
|
+
document.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
161
|
+
document.addEventListener('mouseup', handleMouseUp, { passive: true });
|
|
162
|
+
unsubs.push(() => {
|
|
163
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
164
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
165
|
+
});
|
|
166
|
+
// Pass resize handler to renderer
|
|
167
|
+
renderer.setInteractionState({
|
|
168
|
+
activeCell: null,
|
|
169
|
+
selectionRange: null,
|
|
170
|
+
copyRange: null,
|
|
171
|
+
cutRange: null,
|
|
172
|
+
editingCell: null,
|
|
173
|
+
columnWidths: {},
|
|
174
|
+
onResizeStart: handleResizeStart,
|
|
175
|
+
});
|
|
176
|
+
return unsubs;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { normalizeSelectionRange, isInSelectionRange, CHECKBOX_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
2
|
+
import { getCellCoordinates } from './utils/getCellCoordinates';
|
|
3
|
+
export class OGridRendering {
|
|
4
|
+
constructor(ctx) {
|
|
5
|
+
this.layoutVersion = 0;
|
|
6
|
+
/** Cached DOM cells during drag to avoid querySelectorAll on every RAF frame. */
|
|
7
|
+
this.cachedDragCells = null;
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
}
|
|
10
|
+
/** Increment layout version (e.g., when items, columns, sizing change). */
|
|
11
|
+
incrementLayoutVersion() {
|
|
12
|
+
this.layoutVersion++;
|
|
13
|
+
}
|
|
14
|
+
/** Clear cached drag cells. */
|
|
15
|
+
clearCachedDragCells() {
|
|
16
|
+
this.cachedDragCells = null;
|
|
17
|
+
}
|
|
18
|
+
/** Get current layout version. */
|
|
19
|
+
getLayoutVersion() {
|
|
20
|
+
return this.layoutVersion;
|
|
21
|
+
}
|
|
22
|
+
updateRendererInteractionState() {
|
|
23
|
+
const { selectionState, clipboardState, resizeState, state, layoutState, pinningState, rowSelectionState, cellEditor, renderer, reorderState, marchingAnts, fillHandleState, options } = this.ctx;
|
|
24
|
+
if (!selectionState || !clipboardState || !resizeState)
|
|
25
|
+
return;
|
|
26
|
+
const { items } = state.getProcessedItems();
|
|
27
|
+
const visibleCols = state.visibleColumnDefs;
|
|
28
|
+
// Compute pinning offsets
|
|
29
|
+
const columnWidths = layoutState.getAllColumnWidths();
|
|
30
|
+
const leftOffsets = pinningState?.computeLeftOffsets(visibleCols, columnWidths, 120, !!rowSelectionState, CHECKBOX_COLUMN_WIDTH, !!options.showRowNumbers) ?? {};
|
|
31
|
+
const rightOffsets = pinningState?.computeRightOffsets(visibleCols, columnWidths, 120) ?? {};
|
|
32
|
+
renderer.setInteractionState({
|
|
33
|
+
activeCell: selectionState.activeCell,
|
|
34
|
+
selectionRange: selectionState.selectionRange,
|
|
35
|
+
copyRange: clipboardState.copyRange,
|
|
36
|
+
cutRange: clipboardState.cutRange,
|
|
37
|
+
editingCell: cellEditor?.getEditingCell() ?? null,
|
|
38
|
+
columnWidths,
|
|
39
|
+
onCellClick: (ce) => this.ctx.handleCellClick(ce.rowIndex, ce.colIndex),
|
|
40
|
+
onCellMouseDown: (ce) => { if (ce.event)
|
|
41
|
+
this.ctx.handleCellMouseDown(ce.rowIndex, ce.colIndex, ce.event); },
|
|
42
|
+
onCellDoubleClick: (ce) => { if (ce.rowId != null && ce.columnId)
|
|
43
|
+
this.ctx.startCellEdit(ce.rowId, ce.columnId); },
|
|
44
|
+
onCellContextMenu: (ce) => { if (ce.event)
|
|
45
|
+
this.ctx.handleCellContextMenu(ce.rowIndex, ce.colIndex, ce.event); },
|
|
46
|
+
onResizeStart: renderer.getOnResizeStart(),
|
|
47
|
+
// Fill handle
|
|
48
|
+
onFillHandleMouseDown: options.editable !== false ? (e) => fillHandleState?.startFillDrag(e) : undefined,
|
|
49
|
+
// Row selection
|
|
50
|
+
rowSelectionMode: rowSelectionState?.rowSelection ?? 'none',
|
|
51
|
+
selectedRowIds: rowSelectionState?.selectedRowIds,
|
|
52
|
+
onRowCheckboxChange: (rowId, checked, rowIndex, shiftKey) => {
|
|
53
|
+
rowSelectionState?.handleRowCheckboxChange(rowId, checked, rowIndex, shiftKey, items);
|
|
54
|
+
},
|
|
55
|
+
onSelectAll: (checked) => {
|
|
56
|
+
rowSelectionState?.handleSelectAll(checked, items);
|
|
57
|
+
},
|
|
58
|
+
allSelected: rowSelectionState?.isAllSelected(items),
|
|
59
|
+
someSelected: rowSelectionState?.isSomeSelected(items),
|
|
60
|
+
// Row numbers
|
|
61
|
+
showRowNumbers: options.showRowNumbers,
|
|
62
|
+
// Column pinning
|
|
63
|
+
pinnedColumns: pinningState?.pinnedColumns,
|
|
64
|
+
leftOffsets,
|
|
65
|
+
rightOffsets,
|
|
66
|
+
// Column reorder
|
|
67
|
+
onColumnReorderStart: reorderState ? (columnId, event) => {
|
|
68
|
+
const tableEl = renderer.getTableElement();
|
|
69
|
+
if (!tableEl)
|
|
70
|
+
return;
|
|
71
|
+
reorderState?.startDrag(columnId, event, visibleCols, state.columnOrder, pinningState?.pinnedColumns, tableEl);
|
|
72
|
+
} : undefined,
|
|
73
|
+
});
|
|
74
|
+
renderer.update();
|
|
75
|
+
// Update marching ants overlay
|
|
76
|
+
marchingAnts?.update(selectionState.selectionRange, clipboardState.copyRange, clipboardState.cutRange, this.layoutVersion);
|
|
77
|
+
}
|
|
78
|
+
updateDragAttributes() {
|
|
79
|
+
const wrapper = this.ctx.renderer.getWrapperElement();
|
|
80
|
+
const selectionState = this.ctx.selectionState;
|
|
81
|
+
if (!wrapper || !selectionState)
|
|
82
|
+
return;
|
|
83
|
+
const range = selectionState.getDragRange();
|
|
84
|
+
if (!range)
|
|
85
|
+
return;
|
|
86
|
+
const norm = normalizeSelectionRange(range);
|
|
87
|
+
const anchor = selectionState.dragAnchor;
|
|
88
|
+
// Cache the querySelectorAll result on first drag call; reuse for subsequent RAF frames
|
|
89
|
+
if (!this.cachedDragCells) {
|
|
90
|
+
this.cachedDragCells = wrapper.querySelectorAll('td[data-row-index][data-col-index]');
|
|
91
|
+
}
|
|
92
|
+
const cells = this.cachedDragCells;
|
|
93
|
+
for (let _i = 0; _i < cells.length; _i++) {
|
|
94
|
+
const cell = cells[_i];
|
|
95
|
+
const el = cell;
|
|
96
|
+
const coords = getCellCoordinates(el);
|
|
97
|
+
if (!coords)
|
|
98
|
+
continue;
|
|
99
|
+
const rowIndex = coords.rowIndex;
|
|
100
|
+
const colIndex = coords.colIndex;
|
|
101
|
+
if (isInSelectionRange(norm, rowIndex, colIndex)) {
|
|
102
|
+
el.setAttribute('data-drag-range', 'true');
|
|
103
|
+
// Anchor cell (white background)
|
|
104
|
+
const isAnchor = anchor && rowIndex === anchor.rowIndex && colIndex === anchor.columnIndex;
|
|
105
|
+
if (isAnchor) {
|
|
106
|
+
el.setAttribute('data-drag-anchor', '');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
el.removeAttribute('data-drag-anchor');
|
|
110
|
+
}
|
|
111
|
+
// Edge borders via CSS class instead of inline box-shadow
|
|
112
|
+
el.classList.add('ogrid-drag-target');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
el.removeAttribute('data-drag-range');
|
|
116
|
+
el.removeAttribute('data-drag-anchor');
|
|
117
|
+
el.classList.remove('ogrid-drag-target');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
renderAll() {
|
|
122
|
+
// Increment layout version to trigger marching ants re-measurement
|
|
123
|
+
this.layoutVersion++;
|
|
124
|
+
const { state, options, headerFilterState, rowSelectionState, keyboardNavState, clipboardState, undoRedoState, fillHandleState, virtualScrollState, pagination, statusBar, columnChooser, renderer } = this.ctx;
|
|
125
|
+
const colOffset = rowSelectionState ? 1 : 0;
|
|
126
|
+
// Update header filter state with current filters and options
|
|
127
|
+
headerFilterState.setFilters(state.filters);
|
|
128
|
+
headerFilterState.setFilterOptions(state.filterOptions);
|
|
129
|
+
// Update interaction states with current data
|
|
130
|
+
const { items, totalCount } = state.getProcessedItems();
|
|
131
|
+
if (keyboardNavState && clipboardState) {
|
|
132
|
+
const visibleCols = state.visibleColumnDefs;
|
|
133
|
+
keyboardNavState.updateParams({
|
|
134
|
+
items,
|
|
135
|
+
visibleCols: visibleCols,
|
|
136
|
+
colOffset,
|
|
137
|
+
getRowId: state.getRowId,
|
|
138
|
+
editable: options.editable,
|
|
139
|
+
onCellValueChanged: undoRedoState?.getWrappedCallback(),
|
|
140
|
+
onCopy: () => clipboardState?.handleCopy(),
|
|
141
|
+
onCut: () => clipboardState?.handleCut(),
|
|
142
|
+
onPaste: async () => { await clipboardState?.handlePaste(); },
|
|
143
|
+
onUndo: () => undoRedoState?.undo(),
|
|
144
|
+
onRedo: () => undoRedoState?.redo(),
|
|
145
|
+
onContextMenu: (x, y) => this.ctx.showContextMenu(x, y),
|
|
146
|
+
onStartEdit: (rowId, columnId) => this.ctx.startCellEdit(rowId, columnId),
|
|
147
|
+
clearClipboardRanges: () => clipboardState?.clearClipboardRanges(),
|
|
148
|
+
});
|
|
149
|
+
clipboardState.updateParams({
|
|
150
|
+
items,
|
|
151
|
+
visibleCols: visibleCols,
|
|
152
|
+
colOffset,
|
|
153
|
+
editable: options.editable,
|
|
154
|
+
onCellValueChanged: undoRedoState?.getWrappedCallback(),
|
|
155
|
+
});
|
|
156
|
+
// Update fill handle params
|
|
157
|
+
fillHandleState?.updateParams({
|
|
158
|
+
items,
|
|
159
|
+
visibleCols: visibleCols,
|
|
160
|
+
editable: options.editable,
|
|
161
|
+
onCellValueChanged: undoRedoState?.getWrappedCallback(),
|
|
162
|
+
colOffset,
|
|
163
|
+
beginBatch: () => undoRedoState?.beginBatch(),
|
|
164
|
+
endBatch: () => undoRedoState?.endBatch(),
|
|
165
|
+
});
|
|
166
|
+
// Update renderer interaction state before rendering
|
|
167
|
+
this.updateRendererInteractionState();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
renderer.update();
|
|
171
|
+
}
|
|
172
|
+
// Update virtual scroll with current total row count
|
|
173
|
+
virtualScrollState?.setTotalRows(totalCount);
|
|
174
|
+
pagination.render(totalCount, options.pageSizeOptions);
|
|
175
|
+
statusBar.render({ totalCount });
|
|
176
|
+
columnChooser.render();
|
|
177
|
+
this.renderSideBar();
|
|
178
|
+
this.renderLoadingOverlay();
|
|
179
|
+
}
|
|
180
|
+
renderHeaderFilterPopover() {
|
|
181
|
+
const { headerFilterState, headerFilterComponent, filterConfigs } = this.ctx;
|
|
182
|
+
const openId = headerFilterState.openColumnId;
|
|
183
|
+
if (!openId) {
|
|
184
|
+
headerFilterComponent.cleanup();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const config = filterConfigs.get(openId);
|
|
188
|
+
if (!config)
|
|
189
|
+
return;
|
|
190
|
+
headerFilterComponent.render(config);
|
|
191
|
+
// Update the popover element reference for click-outside detection
|
|
192
|
+
const popoverEl = document.querySelector('.ogrid-header-filter-popover');
|
|
193
|
+
headerFilterState.setPopoverEl(popoverEl);
|
|
194
|
+
}
|
|
195
|
+
renderSideBar() {
|
|
196
|
+
const { sideBarComponent, sideBarState, state } = this.ctx;
|
|
197
|
+
if (!sideBarComponent || !sideBarState)
|
|
198
|
+
return;
|
|
199
|
+
const columns = state.columns.map(c => ({
|
|
200
|
+
columnId: c.columnId,
|
|
201
|
+
name: c.name,
|
|
202
|
+
required: c.required === true,
|
|
203
|
+
}));
|
|
204
|
+
const filterableColumns = state.columns
|
|
205
|
+
.filter(c => c.filterable && typeof c.filterable === 'object' && c.filterable.type)
|
|
206
|
+
.map(c => ({
|
|
207
|
+
columnId: c.columnId,
|
|
208
|
+
name: c.name,
|
|
209
|
+
filterField: c.filterable.filterField ?? c.columnId,
|
|
210
|
+
filterType: c.filterable.type,
|
|
211
|
+
}));
|
|
212
|
+
sideBarComponent.setConfig({
|
|
213
|
+
columns,
|
|
214
|
+
visibleColumns: state.visibleColumns,
|
|
215
|
+
onVisibilityChange: (columnKey, visible) => {
|
|
216
|
+
const next = new Set(state.visibleColumns);
|
|
217
|
+
if (visible)
|
|
218
|
+
next.add(columnKey);
|
|
219
|
+
else
|
|
220
|
+
next.delete(columnKey);
|
|
221
|
+
state.setVisibleColumns(next);
|
|
222
|
+
},
|
|
223
|
+
onSetVisibleColumns: (cols) => state.setVisibleColumns(cols),
|
|
224
|
+
filterableColumns,
|
|
225
|
+
filters: state.filters,
|
|
226
|
+
onFilterChange: (key, value) => state.setFilter(key, value),
|
|
227
|
+
filterOptions: state.filterOptions,
|
|
228
|
+
});
|
|
229
|
+
sideBarComponent.render();
|
|
230
|
+
}
|
|
231
|
+
renderLoadingOverlay() {
|
|
232
|
+
const { state, tableContainer } = this.ctx;
|
|
233
|
+
if (state.isLoading) {
|
|
234
|
+
// Ensure the container has minimum height during loading so overlay is visible
|
|
235
|
+
const { items } = state.getProcessedItems();
|
|
236
|
+
tableContainer.style.minHeight = (!items || items.length === 0) ? '200px' : '';
|
|
237
|
+
let loadingOverlay = this.ctx.loadingOverlay;
|
|
238
|
+
if (!loadingOverlay) {
|
|
239
|
+
loadingOverlay = document.createElement('div');
|
|
240
|
+
loadingOverlay.className = 'ogrid-loading-overlay';
|
|
241
|
+
loadingOverlay.style.position = 'absolute';
|
|
242
|
+
loadingOverlay.style.top = '0';
|
|
243
|
+
loadingOverlay.style.left = '0';
|
|
244
|
+
loadingOverlay.style.right = '0';
|
|
245
|
+
loadingOverlay.style.bottom = '0';
|
|
246
|
+
loadingOverlay.style.display = 'flex';
|
|
247
|
+
loadingOverlay.style.alignItems = 'center';
|
|
248
|
+
loadingOverlay.style.justifyContent = 'center';
|
|
249
|
+
loadingOverlay.style.background = 'var(--ogrid-loading-overlay, rgba(255, 255, 255, 0.7))';
|
|
250
|
+
loadingOverlay.style.zIndex = '100';
|
|
251
|
+
const spinner = document.createElement('div');
|
|
252
|
+
spinner.className = 'ogrid-loading-spinner';
|
|
253
|
+
spinner.textContent = 'Loading...';
|
|
254
|
+
loadingOverlay.appendChild(spinner);
|
|
255
|
+
this.ctx.setLoadingOverlay(loadingOverlay);
|
|
256
|
+
}
|
|
257
|
+
if (!tableContainer.contains(loadingOverlay)) {
|
|
258
|
+
tableContainer.appendChild(loadingOverlay);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
tableContainer.style.minHeight = '';
|
|
263
|
+
const loadingOverlay = this.ctx.loadingOverlay;
|
|
264
|
+
if (loadingOverlay && tableContainer.contains(loadingOverlay)) {
|
|
265
|
+
loadingOverlay.remove();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -3,12 +3,22 @@ export class ColumnChooser {
|
|
|
3
3
|
this.el = null;
|
|
4
4
|
this.dropdown = null;
|
|
5
5
|
this.isOpen = false;
|
|
6
|
+
this.initialized = false;
|
|
6
7
|
this.container = container;
|
|
7
8
|
this.state = state;
|
|
8
9
|
}
|
|
9
10
|
render() {
|
|
10
|
-
if (this.
|
|
11
|
-
this.
|
|
11
|
+
if (!this.initialized) {
|
|
12
|
+
this.createDOM();
|
|
13
|
+
this.initialized = true;
|
|
14
|
+
}
|
|
15
|
+
// If dropdown is open, update checkbox states without destroying/recreating
|
|
16
|
+
if (this.isOpen && this.dropdown) {
|
|
17
|
+
this.updateDropdownState();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Initial DOM creation — called once. */
|
|
21
|
+
createDOM() {
|
|
12
22
|
this.el = document.createElement('div');
|
|
13
23
|
this.el.className = 'ogrid-column-chooser';
|
|
14
24
|
const btn = document.createElement('button');
|
|
@@ -18,6 +28,18 @@ export class ColumnChooser {
|
|
|
18
28
|
this.el.appendChild(btn);
|
|
19
29
|
this.container.appendChild(this.el);
|
|
20
30
|
}
|
|
31
|
+
/** Update checkbox checked states without destroying the dropdown. */
|
|
32
|
+
updateDropdownState() {
|
|
33
|
+
if (!this.dropdown)
|
|
34
|
+
return;
|
|
35
|
+
const checkboxes = this.dropdown.querySelectorAll('input[type="checkbox"]');
|
|
36
|
+
const columns = this.state.columns;
|
|
37
|
+
checkboxes.forEach((checkbox, idx) => {
|
|
38
|
+
if (idx < columns.length) {
|
|
39
|
+
checkbox.checked = this.state.visibleColumns.has(columns[idx].columnId);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
21
43
|
toggle() {
|
|
22
44
|
if (this.isOpen) {
|
|
23
45
|
this.close();
|
|
@@ -53,7 +75,7 @@ export class ColumnChooser {
|
|
|
53
75
|
label.appendChild(document.createTextNode(' ' + col.name));
|
|
54
76
|
this.dropdown.appendChild(label);
|
|
55
77
|
}
|
|
56
|
-
this.el
|
|
78
|
+
this.el?.appendChild(this.dropdown);
|
|
57
79
|
}
|
|
58
80
|
close() {
|
|
59
81
|
this.isOpen = false;
|
|
@@ -64,5 +86,6 @@ export class ColumnChooser {
|
|
|
64
86
|
this.close();
|
|
65
87
|
this.el?.remove();
|
|
66
88
|
this.el = null;
|
|
89
|
+
this.initialized = false;
|
|
67
90
|
}
|
|
68
91
|
}
|
|
@@ -70,7 +70,7 @@ export class InlineCellEditor {
|
|
|
70
70
|
// not re-render before the next click lands, so we clear it explicitly).
|
|
71
71
|
// Look up the cell by data attributes since the original element reference
|
|
72
72
|
// may have been replaced by a re-render.
|
|
73
|
-
if (this.editingCell) {
|
|
73
|
+
if (this.editingCell && this.container.isConnected) {
|
|
74
74
|
const { rowId, columnId } = this.editingCell;
|
|
75
75
|
const row = this.container.querySelector(`tr[data-row-id="${rowId}"]`);
|
|
76
76
|
if (row) {
|
|
@@ -81,8 +81,10 @@ export class InlineCellEditor {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if (this.editingCellElement) {
|
|
84
|
-
// Also reset the original element
|
|
85
|
-
this.editingCellElement.
|
|
84
|
+
// Also reset the original element if it's still connected in the DOM
|
|
85
|
+
if (this.editingCellElement.isConnected) {
|
|
86
|
+
this.editingCellElement.style.visibility = '';
|
|
87
|
+
}
|
|
86
88
|
this.editingCellElement = null;
|
|
87
89
|
}
|
|
88
90
|
if (this.editor) {
|
|
@@ -132,10 +134,14 @@ export class InlineCellEditor {
|
|
|
132
134
|
// Default: text editor
|
|
133
135
|
return this.createTextEditor(value);
|
|
134
136
|
}
|
|
135
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Shared factory for text/date input editors — both types have identical event handling,
|
|
139
|
+
* differing only in input.type and initial value formatting.
|
|
140
|
+
*/
|
|
141
|
+
createInputEditor(type, initialValue) {
|
|
136
142
|
const input = document.createElement('input');
|
|
137
|
-
input.type =
|
|
138
|
-
input.value =
|
|
143
|
+
input.type = type;
|
|
144
|
+
input.value = initialValue;
|
|
139
145
|
Object.assign(input.style, EDITOR_STYLE);
|
|
140
146
|
input.addEventListener('keydown', (e) => {
|
|
141
147
|
if (e.key === 'Enter') {
|
|
@@ -164,6 +170,9 @@ export class InlineCellEditor {
|
|
|
164
170
|
setTimeout(() => input.select(), 0);
|
|
165
171
|
return input;
|
|
166
172
|
}
|
|
173
|
+
createTextEditor(value) {
|
|
174
|
+
return this.createInputEditor('text', value != null ? String(value) : '');
|
|
175
|
+
}
|
|
167
176
|
createCheckboxEditor(value) {
|
|
168
177
|
const input = document.createElement('input');
|
|
169
178
|
input.type = 'checkbox';
|
|
@@ -188,41 +197,14 @@ export class InlineCellEditor {
|
|
|
188
197
|
return input;
|
|
189
198
|
}
|
|
190
199
|
createDateEditor(value) {
|
|
191
|
-
|
|
192
|
-
input.type = 'date';
|
|
200
|
+
let initialValue = '';
|
|
193
201
|
if (value != null) {
|
|
194
202
|
const dateStr = String(value);
|
|
195
203
|
if (dateStr.match(/^\d{4}-\d{2}-\d{2}/)) {
|
|
196
|
-
|
|
204
|
+
initialValue = dateStr.substring(0, 10);
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
|
-
|
|
200
|
-
input.addEventListener('keydown', (e) => {
|
|
201
|
-
if (e.key === 'Enter') {
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
e.stopPropagation(); // Prevent grid wrapper from re-opening the editor
|
|
204
|
-
if (this.editingCell) {
|
|
205
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
206
|
-
}
|
|
207
|
-
const afterCommit = this.onAfterCommit;
|
|
208
|
-
this.closeEditor();
|
|
209
|
-
afterCommit?.(); // Move active cell down after closing
|
|
210
|
-
}
|
|
211
|
-
else if (e.key === 'Escape') {
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
e.stopPropagation();
|
|
214
|
-
this.onCancel?.();
|
|
215
|
-
this.closeEditor();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
input.addEventListener('blur', () => {
|
|
219
|
-
if (this.editingCell) {
|
|
220
|
-
this.onCommit?.(this.editingCell.rowId, this.editingCell.columnId, input.value);
|
|
221
|
-
}
|
|
222
|
-
this.closeEditor();
|
|
223
|
-
});
|
|
224
|
-
setTimeout(() => input.select(), 0);
|
|
225
|
-
return input;
|
|
207
|
+
return this.createInputEditor('date', initialValue);
|
|
226
208
|
}
|
|
227
209
|
createSelectEditor(value, column) {
|
|
228
210
|
const values = column.cellEditorParams?.values ?? [];
|
package/dist/esm/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from '@alaarab/ogrid-core';
|
|
|
4
4
|
export { debounce } from './utils';
|
|
5
5
|
// Classes
|
|
6
6
|
export { OGrid } from './OGrid';
|
|
7
|
+
export { OGridEventWiring } from './OGridEventWiring';
|
|
8
|
+
export { OGridRendering } from './OGridRendering';
|
|
7
9
|
export { GridState } from './state/GridState';
|
|
8
10
|
export { EventEmitter } from './state/EventEmitter';
|
|
9
11
|
export { SelectionState } from './state/SelectionState';
|