@alaarab/ogrid-js 2.0.22 → 2.1.0

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.
Files changed (35) hide show
  1. package/dist/esm/OGrid.js +189 -503
  2. package/dist/esm/OGridEventWiring.js +178 -0
  3. package/dist/esm/OGridRendering.js +269 -0
  4. package/dist/esm/components/ColumnChooser.js +26 -3
  5. package/dist/esm/components/InlineCellEditor.js +18 -36
  6. package/dist/esm/index.js +2 -0
  7. package/dist/esm/renderer/TableRenderer.js +102 -61
  8. package/dist/esm/state/ClipboardState.js +8 -54
  9. package/dist/esm/state/ColumnPinningState.js +1 -2
  10. package/dist/esm/state/ColumnReorderState.js +8 -1
  11. package/dist/esm/state/EventEmitter.js +3 -2
  12. package/dist/esm/state/FillHandleState.js +27 -41
  13. package/dist/esm/state/GridState.js +36 -10
  14. package/dist/esm/state/HeaderFilterState.js +19 -11
  15. package/dist/esm/state/KeyboardNavState.js +19 -132
  16. package/dist/esm/state/RowSelectionState.js +6 -15
  17. package/dist/esm/state/SideBarState.js +1 -1
  18. package/dist/esm/state/TableLayoutState.js +6 -4
  19. package/dist/esm/utils/getCellCoordinates.js +15 -0
  20. package/dist/esm/utils/index.js +1 -0
  21. package/dist/types/OGrid.d.ts +97 -9
  22. package/dist/types/OGridEventWiring.d.ts +60 -0
  23. package/dist/types/OGridRendering.d.ts +93 -0
  24. package/dist/types/components/ColumnChooser.d.ts +5 -0
  25. package/dist/types/components/InlineCellEditor.d.ts +5 -0
  26. package/dist/types/index.d.ts +6 -1
  27. package/dist/types/renderer/TableRenderer.d.ts +12 -5
  28. package/dist/types/state/EventEmitter.d.ts +1 -1
  29. package/dist/types/state/FillHandleState.d.ts +1 -1
  30. package/dist/types/state/GridState.d.ts +7 -1
  31. package/dist/types/state/HeaderFilterState.d.ts +2 -0
  32. package/dist/types/types/gridTypes.d.ts +15 -0
  33. package/dist/types/utils/getCellCoordinates.d.ts +8 -0
  34. package/dist/types/utils/index.d.ts +1 -0
  35. 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.el)
11
- this.el.remove();
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.appendChild(this.dropdown);
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 in case it's still in the DOM
85
- this.editingCellElement.style.visibility = '';
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
- createTextEditor(value) {
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 = 'text';
138
- input.value = value != null ? String(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
- const input = document.createElement('input');
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
- input.value = dateStr.substring(0, 10);
204
+ initialValue = dateStr.substring(0, 10);
197
205
  }
198
206
  }
199
- Object.assign(input.style, EDITOR_STYLE);
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';