@alaarab/ogrid-js 2.1.3 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +6343 -32
- package/package.json +4 -4
- package/dist/esm/OGrid.js +0 -578
- package/dist/esm/OGridEventWiring.js +0 -178
- package/dist/esm/OGridRendering.js +0 -269
- package/dist/esm/components/ColumnChooser.js +0 -91
- package/dist/esm/components/ContextMenu.js +0 -125
- package/dist/esm/components/HeaderFilter.js +0 -281
- package/dist/esm/components/InlineCellEditor.js +0 -434
- package/dist/esm/components/MarchingAntsOverlay.js +0 -156
- package/dist/esm/components/PaginationControls.js +0 -85
- package/dist/esm/components/SideBar.js +0 -353
- package/dist/esm/components/StatusBar.js +0 -34
- package/dist/esm/renderer/TableRenderer.js +0 -846
- package/dist/esm/state/ClipboardState.js +0 -111
- package/dist/esm/state/ColumnPinningState.js +0 -82
- package/dist/esm/state/ColumnReorderState.js +0 -135
- package/dist/esm/state/ColumnResizeState.js +0 -55
- package/dist/esm/state/EventEmitter.js +0 -28
- package/dist/esm/state/FillHandleState.js +0 -206
- package/dist/esm/state/GridState.js +0 -324
- package/dist/esm/state/HeaderFilterState.js +0 -213
- package/dist/esm/state/KeyboardNavState.js +0 -216
- package/dist/esm/state/RowSelectionState.js +0 -72
- package/dist/esm/state/SelectionState.js +0 -109
- package/dist/esm/state/SideBarState.js +0 -41
- package/dist/esm/state/TableLayoutState.js +0 -97
- package/dist/esm/state/UndoRedoState.js +0 -71
- package/dist/esm/state/VirtualScrollState.js +0 -128
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/gridTypes.js +0 -1
- package/dist/esm/types/index.js +0 -2
- package/dist/esm/utils/debounce.js +0 -2
- package/dist/esm/utils/getCellCoordinates.js +0 -15
- package/dist/esm/utils/index.js +0 -2
|
@@ -1,178 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
export class ColumnChooser {
|
|
2
|
-
constructor(container, state) {
|
|
3
|
-
this.el = null;
|
|
4
|
-
this.dropdown = null;
|
|
5
|
-
this.isOpen = false;
|
|
6
|
-
this.initialized = false;
|
|
7
|
-
this.container = container;
|
|
8
|
-
this.state = state;
|
|
9
|
-
}
|
|
10
|
-
render() {
|
|
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() {
|
|
22
|
-
this.el = document.createElement('div');
|
|
23
|
-
this.el.className = 'ogrid-column-chooser';
|
|
24
|
-
const btn = document.createElement('button');
|
|
25
|
-
btn.className = 'ogrid-column-chooser-btn';
|
|
26
|
-
btn.textContent = 'Columns';
|
|
27
|
-
btn.addEventListener('click', () => this.toggle());
|
|
28
|
-
this.el.appendChild(btn);
|
|
29
|
-
this.container.appendChild(this.el);
|
|
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
|
-
}
|
|
43
|
-
toggle() {
|
|
44
|
-
if (this.isOpen) {
|
|
45
|
-
this.close();
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
this.open();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
open() {
|
|
52
|
-
if (this.dropdown)
|
|
53
|
-
return;
|
|
54
|
-
this.isOpen = true;
|
|
55
|
-
this.dropdown = document.createElement('div');
|
|
56
|
-
this.dropdown.className = 'ogrid-column-chooser-dropdown';
|
|
57
|
-
for (const col of this.state.columns) {
|
|
58
|
-
const label = document.createElement('label');
|
|
59
|
-
label.className = 'ogrid-column-chooser-item';
|
|
60
|
-
const checkbox = document.createElement('input');
|
|
61
|
-
checkbox.type = 'checkbox';
|
|
62
|
-
checkbox.checked = this.state.visibleColumns.has(col.columnId);
|
|
63
|
-
checkbox.disabled = !!col.required;
|
|
64
|
-
checkbox.addEventListener('change', () => {
|
|
65
|
-
const next = new Set(this.state.visibleColumns);
|
|
66
|
-
if (checkbox.checked) {
|
|
67
|
-
next.add(col.columnId);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
next.delete(col.columnId);
|
|
71
|
-
}
|
|
72
|
-
this.state.setVisibleColumns(next);
|
|
73
|
-
});
|
|
74
|
-
label.appendChild(checkbox);
|
|
75
|
-
label.appendChild(document.createTextNode(' ' + col.name));
|
|
76
|
-
this.dropdown.appendChild(label);
|
|
77
|
-
}
|
|
78
|
-
this.el?.appendChild(this.dropdown);
|
|
79
|
-
}
|
|
80
|
-
close() {
|
|
81
|
-
this.isOpen = false;
|
|
82
|
-
this.dropdown?.remove();
|
|
83
|
-
this.dropdown = null;
|
|
84
|
-
}
|
|
85
|
-
destroy() {
|
|
86
|
-
this.close();
|
|
87
|
-
this.el?.remove();
|
|
88
|
-
this.el = null;
|
|
89
|
-
this.initialized = false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { GRID_CONTEXT_MENU_ITEMS, formatShortcut } from '@alaarab/ogrid-core';
|
|
2
|
-
const MENU_STYLE = {
|
|
3
|
-
position: 'fixed',
|
|
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',
|
|
8
|
-
zIndex: '10000',
|
|
9
|
-
minWidth: '180px',
|
|
10
|
-
padding: '4px 0',
|
|
11
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
12
|
-
fontSize: '14px',
|
|
13
|
-
color: 'var(--ogrid-fg, #242424)',
|
|
14
|
-
};
|
|
15
|
-
const ITEM_STYLE = {
|
|
16
|
-
padding: '6px 12px',
|
|
17
|
-
cursor: 'pointer',
|
|
18
|
-
display: 'flex',
|
|
19
|
-
justifyContent: 'space-between',
|
|
20
|
-
alignItems: 'center',
|
|
21
|
-
};
|
|
22
|
-
const DIVIDER_STYLE = {
|
|
23
|
-
height: '1px',
|
|
24
|
-
backgroundColor: 'var(--ogrid-border, #e0e0e0)',
|
|
25
|
-
margin: '4px 0',
|
|
26
|
-
};
|
|
27
|
-
export class ContextMenu {
|
|
28
|
-
constructor() {
|
|
29
|
-
this.menu = null;
|
|
30
|
-
this.handlers = null;
|
|
31
|
-
}
|
|
32
|
-
show(x, y, handlers, canUndo, canRedo, selectionRange) {
|
|
33
|
-
this.close();
|
|
34
|
-
this.handlers = handlers;
|
|
35
|
-
this.menu = document.createElement('div');
|
|
36
|
-
Object.assign(this.menu.style, MENU_STYLE);
|
|
37
|
-
this.menu.style.left = `${x}px`;
|
|
38
|
-
this.menu.style.top = `${y}px`;
|
|
39
|
-
for (const item of GRID_CONTEXT_MENU_ITEMS) {
|
|
40
|
-
if (item.dividerBefore) {
|
|
41
|
-
const divider = document.createElement('div');
|
|
42
|
-
Object.assign(divider.style, DIVIDER_STYLE);
|
|
43
|
-
this.menu.appendChild(divider);
|
|
44
|
-
}
|
|
45
|
-
const menuItem = document.createElement('div');
|
|
46
|
-
Object.assign(menuItem.style, ITEM_STYLE);
|
|
47
|
-
const label = document.createElement('span');
|
|
48
|
-
label.textContent = item.label;
|
|
49
|
-
menuItem.appendChild(label);
|
|
50
|
-
if (item.shortcut) {
|
|
51
|
-
const shortcut = document.createElement('span');
|
|
52
|
-
shortcut.textContent = formatShortcut(item.shortcut);
|
|
53
|
-
shortcut.style.marginLeft = '20px';
|
|
54
|
-
shortcut.style.color = 'var(--ogrid-muted, #666)';
|
|
55
|
-
shortcut.style.fontSize = '12px';
|
|
56
|
-
menuItem.appendChild(shortcut);
|
|
57
|
-
}
|
|
58
|
-
const isDisabled = (item.id === 'undo' && !canUndo) ||
|
|
59
|
-
(item.id === 'redo' && !canRedo) ||
|
|
60
|
-
(item.disabledWhenNoSelection && selectionRange == null);
|
|
61
|
-
if (isDisabled) {
|
|
62
|
-
menuItem.style.color = 'var(--ogrid-fg-muted, rgba(0, 0, 0, 0.4))';
|
|
63
|
-
menuItem.style.opacity = '0.5';
|
|
64
|
-
menuItem.style.cursor = 'not-allowed';
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
menuItem.addEventListener('mouseenter', () => {
|
|
68
|
-
menuItem.style.backgroundColor = 'var(--ogrid-bg-hover, #f5f5f5)';
|
|
69
|
-
}, { passive: true });
|
|
70
|
-
menuItem.addEventListener('mouseleave', () => {
|
|
71
|
-
menuItem.style.backgroundColor = '';
|
|
72
|
-
}, { passive: true });
|
|
73
|
-
menuItem.addEventListener('click', () => {
|
|
74
|
-
this.handleItemClick(item.id);
|
|
75
|
-
}, { passive: true });
|
|
76
|
-
}
|
|
77
|
-
this.menu.appendChild(menuItem);
|
|
78
|
-
}
|
|
79
|
-
document.body.appendChild(this.menu);
|
|
80
|
-
const handleClickOutside = (e) => {
|
|
81
|
-
if (this.menu && !this.menu.contains(e.target)) {
|
|
82
|
-
this.close();
|
|
83
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
setTimeout(() => {
|
|
87
|
-
document.addEventListener('mousedown', handleClickOutside, { passive: true });
|
|
88
|
-
}, 0);
|
|
89
|
-
}
|
|
90
|
-
close() {
|
|
91
|
-
if (this.menu) {
|
|
92
|
-
this.menu.remove();
|
|
93
|
-
this.menu = null;
|
|
94
|
-
}
|
|
95
|
-
this.handlers = null;
|
|
96
|
-
}
|
|
97
|
-
handleItemClick(id) {
|
|
98
|
-
if (!this.handlers)
|
|
99
|
-
return;
|
|
100
|
-
switch (id) {
|
|
101
|
-
case 'undo':
|
|
102
|
-
this.handlers.onUndo();
|
|
103
|
-
break;
|
|
104
|
-
case 'redo':
|
|
105
|
-
this.handlers.onRedo();
|
|
106
|
-
break;
|
|
107
|
-
case 'copy':
|
|
108
|
-
this.handlers.onCopy();
|
|
109
|
-
break;
|
|
110
|
-
case 'cut':
|
|
111
|
-
this.handlers.onCut();
|
|
112
|
-
break;
|
|
113
|
-
case 'paste':
|
|
114
|
-
this.handlers.onPaste();
|
|
115
|
-
break;
|
|
116
|
-
case 'selectAll':
|
|
117
|
-
this.handlers.onSelectAll();
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
this.close();
|
|
121
|
-
}
|
|
122
|
-
destroy() {
|
|
123
|
-
this.close();
|
|
124
|
-
}
|
|
125
|
-
}
|