@alaarab/ogrid-js 2.0.0-beta

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 (55) hide show
  1. package/dist/esm/OGrid.js +654 -0
  2. package/dist/esm/components/ColumnChooser.js +68 -0
  3. package/dist/esm/components/ContextMenu.js +122 -0
  4. package/dist/esm/components/HeaderFilter.js +281 -0
  5. package/dist/esm/components/InlineCellEditor.js +278 -0
  6. package/dist/esm/components/MarchingAntsOverlay.js +170 -0
  7. package/dist/esm/components/PaginationControls.js +85 -0
  8. package/dist/esm/components/SideBar.js +353 -0
  9. package/dist/esm/components/StatusBar.js +34 -0
  10. package/dist/esm/index.js +26 -0
  11. package/dist/esm/renderer/TableRenderer.js +414 -0
  12. package/dist/esm/state/ClipboardState.js +171 -0
  13. package/dist/esm/state/ColumnPinningState.js +78 -0
  14. package/dist/esm/state/ColumnResizeState.js +55 -0
  15. package/dist/esm/state/EventEmitter.js +27 -0
  16. package/dist/esm/state/FillHandleState.js +218 -0
  17. package/dist/esm/state/GridState.js +261 -0
  18. package/dist/esm/state/HeaderFilterState.js +205 -0
  19. package/dist/esm/state/KeyboardNavState.js +374 -0
  20. package/dist/esm/state/RowSelectionState.js +81 -0
  21. package/dist/esm/state/SelectionState.js +102 -0
  22. package/dist/esm/state/SideBarState.js +41 -0
  23. package/dist/esm/state/TableLayoutState.js +95 -0
  24. package/dist/esm/state/UndoRedoState.js +82 -0
  25. package/dist/esm/types/columnTypes.js +1 -0
  26. package/dist/esm/types/gridTypes.js +1 -0
  27. package/dist/esm/types/index.js +2 -0
  28. package/dist/types/OGrid.d.ts +60 -0
  29. package/dist/types/components/ColumnChooser.d.ts +14 -0
  30. package/dist/types/components/ContextMenu.d.ts +17 -0
  31. package/dist/types/components/HeaderFilter.d.ts +24 -0
  32. package/dist/types/components/InlineCellEditor.d.ts +24 -0
  33. package/dist/types/components/MarchingAntsOverlay.d.ts +25 -0
  34. package/dist/types/components/PaginationControls.d.ts +9 -0
  35. package/dist/types/components/SideBar.d.ts +35 -0
  36. package/dist/types/components/StatusBar.d.ts +8 -0
  37. package/dist/types/index.d.ts +26 -0
  38. package/dist/types/renderer/TableRenderer.d.ts +59 -0
  39. package/dist/types/state/ClipboardState.d.ts +35 -0
  40. package/dist/types/state/ColumnPinningState.d.ts +36 -0
  41. package/dist/types/state/ColumnResizeState.d.ts +23 -0
  42. package/dist/types/state/EventEmitter.d.ts +9 -0
  43. package/dist/types/state/FillHandleState.d.ts +51 -0
  44. package/dist/types/state/GridState.d.ts +68 -0
  45. package/dist/types/state/HeaderFilterState.d.ts +64 -0
  46. package/dist/types/state/KeyboardNavState.d.ts +29 -0
  47. package/dist/types/state/RowSelectionState.d.ts +23 -0
  48. package/dist/types/state/SelectionState.d.ts +37 -0
  49. package/dist/types/state/SideBarState.d.ts +19 -0
  50. package/dist/types/state/TableLayoutState.d.ts +33 -0
  51. package/dist/types/state/UndoRedoState.d.ts +28 -0
  52. package/dist/types/types/columnTypes.d.ts +28 -0
  53. package/dist/types/types/gridTypes.d.ts +69 -0
  54. package/dist/types/types/index.d.ts +2 -0
  55. package/package.json +29 -0
@@ -0,0 +1,205 @@
1
+ import { EventEmitter } from './EventEmitter';
2
+ /**
3
+ * Manages header filter popover state for all columns.
4
+ * Equivalent of React's useColumnHeaderFilterState, but class-based.
5
+ */
6
+ export class HeaderFilterState {
7
+ constructor(onFilterChange) {
8
+ this.emitter = new EventEmitter();
9
+ // Which column's filter is currently open (null = none)
10
+ this._openColumnId = null;
11
+ // Temporary state for the currently open filter popover
12
+ this._tempTextValue = '';
13
+ this._tempSelected = new Set();
14
+ this._tempDateFrom = '';
15
+ this._tempDateTo = '';
16
+ this._searchText = '';
17
+ // Popover position
18
+ this._popoverPosition = null;
19
+ // External references
20
+ this._filters = {};
21
+ this._filterOptions = {};
22
+ // Click-outside handler
23
+ this._clickOutsideHandler = null;
24
+ this._escapeHandler = null;
25
+ this._popoverEl = null;
26
+ this._headerEl = null;
27
+ this._onFilterChange = onFilterChange;
28
+ }
29
+ get openColumnId() { return this._openColumnId; }
30
+ get tempTextValue() { return this._tempTextValue; }
31
+ get tempSelected() { return this._tempSelected; }
32
+ get tempDateFrom() { return this._tempDateFrom; }
33
+ get tempDateTo() { return this._tempDateTo; }
34
+ get searchText() { return this._searchText; }
35
+ get popoverPosition() { return this._popoverPosition; }
36
+ setFilters(filters) {
37
+ this._filters = filters;
38
+ }
39
+ setFilterOptions(options) {
40
+ this._filterOptions = options;
41
+ }
42
+ getFilterOptions(filterField) {
43
+ return this._filterOptions[filterField] ?? [];
44
+ }
45
+ getFilteredOptions(filterField) {
46
+ const options = this.getFilterOptions(filterField);
47
+ if (!this._searchText)
48
+ return options;
49
+ const lower = this._searchText.toLowerCase();
50
+ return options.filter(opt => opt.toLowerCase().includes(lower));
51
+ }
52
+ hasActiveFilter(config) {
53
+ const fv = this._filters[config.filterField];
54
+ if (!fv)
55
+ return false;
56
+ if (fv.type === 'text')
57
+ return fv.value.trim().length > 0;
58
+ if (fv.type === 'multiSelect')
59
+ return fv.value.length > 0;
60
+ if (fv.type === 'date')
61
+ return !!(fv.value.from || fv.value.to);
62
+ if (fv.type === 'people')
63
+ return !!fv.value;
64
+ return false;
65
+ }
66
+ /**
67
+ * Open a filter popover for a specific column.
68
+ */
69
+ open(columnId, config, headerEl, popoverEl) {
70
+ // Close any existing popover first
71
+ if (this._openColumnId) {
72
+ this.close();
73
+ }
74
+ this._openColumnId = columnId;
75
+ this._headerEl = headerEl;
76
+ this._popoverEl = popoverEl;
77
+ // Initialize temp state from current filter values
78
+ const fv = this._filters[config.filterField];
79
+ if (config.filterType === 'text') {
80
+ this._tempTextValue = fv?.type === 'text' ? fv.value : '';
81
+ }
82
+ else if (config.filterType === 'multiSelect') {
83
+ this._tempSelected = new Set(fv?.type === 'multiSelect' ? fv.value : []);
84
+ }
85
+ else if (config.filterType === 'date') {
86
+ const dv = fv?.type === 'date' ? fv.value : {};
87
+ this._tempDateFrom = dv.from ?? '';
88
+ this._tempDateTo = dv.to ?? '';
89
+ }
90
+ this._searchText = '';
91
+ // Compute position
92
+ const rect = headerEl.getBoundingClientRect();
93
+ this._popoverPosition = { top: rect.bottom + 4, left: rect.left };
94
+ // Set up click-outside listener
95
+ this._clickOutsideHandler = (e) => {
96
+ const target = e.target;
97
+ if (this._popoverEl && !this._popoverEl.contains(target) &&
98
+ this._headerEl && !this._headerEl.contains(target)) {
99
+ this.close();
100
+ }
101
+ };
102
+ this._escapeHandler = (e) => {
103
+ if (e.key === 'Escape') {
104
+ e.preventDefault();
105
+ e.stopPropagation();
106
+ this.close();
107
+ }
108
+ };
109
+ setTimeout(() => {
110
+ document.addEventListener('mousedown', this._clickOutsideHandler);
111
+ }, 0);
112
+ document.addEventListener('keydown', this._escapeHandler, true);
113
+ this.emitter.emit('change', undefined);
114
+ }
115
+ close() {
116
+ this._openColumnId = null;
117
+ this._popoverPosition = null;
118
+ this._popoverEl = null;
119
+ this._headerEl = null;
120
+ if (this._clickOutsideHandler) {
121
+ document.removeEventListener('mousedown', this._clickOutsideHandler);
122
+ this._clickOutsideHandler = null;
123
+ }
124
+ if (this._escapeHandler) {
125
+ document.removeEventListener('keydown', this._escapeHandler, true);
126
+ this._escapeHandler = null;
127
+ }
128
+ this.emitter.emit('change', undefined);
129
+ }
130
+ // --- Temp state setters ---
131
+ setTempTextValue(v) {
132
+ this._tempTextValue = v;
133
+ this.emitter.emit('change', undefined);
134
+ }
135
+ setSearchText(v) {
136
+ this._searchText = v;
137
+ this.emitter.emit('change', undefined);
138
+ }
139
+ setTempDateFrom(v) {
140
+ this._tempDateFrom = v;
141
+ this.emitter.emit('change', undefined);
142
+ }
143
+ setTempDateTo(v) {
144
+ this._tempDateTo = v;
145
+ this.emitter.emit('change', undefined);
146
+ }
147
+ // --- Checkbox handlers ---
148
+ handleCheckboxChange(option, checked) {
149
+ const next = new Set(this._tempSelected);
150
+ if (checked)
151
+ next.add(option);
152
+ else
153
+ next.delete(option);
154
+ this._tempSelected = next;
155
+ this.emitter.emit('change', undefined);
156
+ }
157
+ handleSelectAll(filterField) {
158
+ this._tempSelected = new Set(this.getFilterOptions(filterField));
159
+ this.emitter.emit('change', undefined);
160
+ }
161
+ handleClearSelection() {
162
+ this._tempSelected = new Set();
163
+ this.emitter.emit('change', undefined);
164
+ }
165
+ // --- Apply/Clear ---
166
+ applyTextFilter(filterField) {
167
+ const value = this._tempTextValue.trim();
168
+ this._onFilterChange(filterField, value ? { type: 'text', value } : undefined);
169
+ this.close();
170
+ }
171
+ clearTextFilter(filterField) {
172
+ this._tempTextValue = '';
173
+ this._onFilterChange(filterField, undefined);
174
+ this.close();
175
+ }
176
+ applyMultiSelectFilter(filterField) {
177
+ const arr = Array.from(this._tempSelected);
178
+ this._onFilterChange(filterField, arr.length > 0 ? { type: 'multiSelect', value: arr } : undefined);
179
+ this.close();
180
+ }
181
+ applyDateFilter(filterField) {
182
+ const from = this._tempDateFrom || undefined;
183
+ const to = this._tempDateTo || undefined;
184
+ this._onFilterChange(filterField, from || to ? { type: 'date', value: { from, to } } : undefined);
185
+ this.close();
186
+ }
187
+ clearDateFilter(filterField) {
188
+ this._tempDateFrom = '';
189
+ this._tempDateTo = '';
190
+ this._onFilterChange(filterField, undefined);
191
+ this.close();
192
+ }
193
+ clearFilter(filterField) {
194
+ this._onFilterChange(filterField, undefined);
195
+ this.close();
196
+ }
197
+ onChange(handler) {
198
+ this.emitter.on('change', handler);
199
+ return () => this.emitter.off('change', handler);
200
+ }
201
+ destroy() {
202
+ this.close();
203
+ this.emitter.removeAllListeners();
204
+ }
205
+ }
@@ -0,0 +1,374 @@
1
+ import { normalizeSelectionRange, getCellValue } from '@alaarab/ogrid-core';
2
+ import { parseValue } from '@alaarab/ogrid-core';
3
+ /**
4
+ * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
5
+ * - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
6
+ * - Otherwise → skip empties, land on next non-empty or edge.
7
+ */
8
+ function findCtrlTarget(pos, edge, step, isEmpty) {
9
+ if (pos === edge)
10
+ return pos;
11
+ const next = pos + step;
12
+ if (!isEmpty(pos) && !isEmpty(next)) {
13
+ let p = next;
14
+ while (p !== edge) {
15
+ if (isEmpty(p + step))
16
+ return p;
17
+ p += step;
18
+ }
19
+ return edge;
20
+ }
21
+ let p = next;
22
+ while (p !== edge) {
23
+ if (!isEmpty(p))
24
+ return p;
25
+ p += step;
26
+ }
27
+ return edge;
28
+ }
29
+ export class KeyboardNavState {
30
+ constructor(params, getActiveCell, getSelectionRange, setActiveCell, setSelectionRange) {
31
+ this.wrapperRef = null;
32
+ this.handleKeyDown = (e) => {
33
+ const { items, visibleCols, colOffset, editable, onCellValueChanged, onCopy, onCut, onPaste, onUndo, onRedo, onContextMenu, onStartEdit, getRowId, clearClipboardRanges } = this.params;
34
+ const activeCell = this.getActiveCell();
35
+ const selectionRange = this.getSelectionRange();
36
+ const maxRowIndex = items.length - 1;
37
+ const maxColIndex = visibleCols.length - 1 + colOffset;
38
+ if (items.length === 0)
39
+ return;
40
+ if (activeCell === null) {
41
+ if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'].includes(e.key)) {
42
+ this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
43
+ e.preventDefault();
44
+ }
45
+ return;
46
+ }
47
+ const { rowIndex, columnIndex } = activeCell;
48
+ const dataColIndex = columnIndex - colOffset;
49
+ const shift = e.shiftKey;
50
+ const isEmptyAt = (r, c) => {
51
+ if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
52
+ return true;
53
+ const v = getCellValue(items[r], visibleCols[c]);
54
+ return v == null || v === '';
55
+ };
56
+ switch (e.key) {
57
+ case 'c':
58
+ if (e.ctrlKey || e.metaKey) {
59
+ e.preventDefault();
60
+ onCopy?.();
61
+ }
62
+ break;
63
+ case 'x':
64
+ if (e.ctrlKey || e.metaKey) {
65
+ e.preventDefault();
66
+ onCut?.();
67
+ }
68
+ break;
69
+ case 'v':
70
+ if (e.ctrlKey || e.metaKey) {
71
+ e.preventDefault();
72
+ void onPaste?.();
73
+ }
74
+ break;
75
+ case 'ArrowDown': {
76
+ e.preventDefault();
77
+ const ctrl = e.ctrlKey || e.metaKey;
78
+ const newRow = ctrl
79
+ ? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
80
+ : Math.min(rowIndex + 1, maxRowIndex);
81
+ this.setActiveCell({ rowIndex: newRow, columnIndex });
82
+ if (shift) {
83
+ this.setSelectionRange(normalizeSelectionRange({
84
+ startRow: selectionRange?.startRow ?? rowIndex,
85
+ startCol: selectionRange?.startCol ?? dataColIndex,
86
+ endRow: newRow,
87
+ endCol: selectionRange?.endCol ?? dataColIndex,
88
+ }));
89
+ }
90
+ else {
91
+ this.setSelectionRange({
92
+ startRow: newRow,
93
+ startCol: dataColIndex,
94
+ endRow: newRow,
95
+ endCol: dataColIndex,
96
+ });
97
+ }
98
+ break;
99
+ }
100
+ case 'ArrowUp': {
101
+ e.preventDefault();
102
+ const ctrl = e.ctrlKey || e.metaKey;
103
+ const newRowUp = ctrl
104
+ ? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
105
+ : Math.max(rowIndex - 1, 0);
106
+ this.setActiveCell({ rowIndex: newRowUp, columnIndex });
107
+ if (shift) {
108
+ this.setSelectionRange(normalizeSelectionRange({
109
+ startRow: selectionRange?.startRow ?? rowIndex,
110
+ startCol: selectionRange?.startCol ?? dataColIndex,
111
+ endRow: newRowUp,
112
+ endCol: selectionRange?.endCol ?? dataColIndex,
113
+ }));
114
+ }
115
+ else {
116
+ this.setSelectionRange({
117
+ startRow: newRowUp,
118
+ startCol: dataColIndex,
119
+ endRow: newRowUp,
120
+ endCol: dataColIndex,
121
+ });
122
+ }
123
+ break;
124
+ }
125
+ case 'ArrowRight': {
126
+ e.preventDefault();
127
+ const ctrl = e.ctrlKey || e.metaKey;
128
+ let newCol;
129
+ if (ctrl && dataColIndex >= 0) {
130
+ newCol = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
131
+ }
132
+ else {
133
+ newCol = Math.min(columnIndex + 1, maxColIndex);
134
+ }
135
+ const newDataCol = newCol - colOffset;
136
+ this.setActiveCell({ rowIndex, columnIndex: newCol });
137
+ if (shift) {
138
+ this.setSelectionRange(normalizeSelectionRange({
139
+ startRow: selectionRange?.startRow ?? rowIndex,
140
+ startCol: selectionRange?.startCol ?? dataColIndex,
141
+ endRow: selectionRange?.endRow ?? rowIndex,
142
+ endCol: newDataCol,
143
+ }));
144
+ }
145
+ else {
146
+ this.setSelectionRange({
147
+ startRow: rowIndex,
148
+ startCol: newDataCol,
149
+ endRow: rowIndex,
150
+ endCol: newDataCol,
151
+ });
152
+ }
153
+ break;
154
+ }
155
+ case 'ArrowLeft': {
156
+ e.preventDefault();
157
+ const ctrl = e.ctrlKey || e.metaKey;
158
+ let newColLeft;
159
+ if (ctrl && dataColIndex >= 0) {
160
+ newColLeft = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
161
+ }
162
+ else {
163
+ newColLeft = Math.max(columnIndex - 1, colOffset);
164
+ }
165
+ const newDataColLeft = newColLeft - colOffset;
166
+ this.setActiveCell({ rowIndex, columnIndex: newColLeft });
167
+ if (shift) {
168
+ this.setSelectionRange(normalizeSelectionRange({
169
+ startRow: selectionRange?.startRow ?? rowIndex,
170
+ startCol: selectionRange?.startCol ?? dataColIndex,
171
+ endRow: selectionRange?.endRow ?? rowIndex,
172
+ endCol: newDataColLeft,
173
+ }));
174
+ }
175
+ else {
176
+ this.setSelectionRange({
177
+ startRow: rowIndex,
178
+ startCol: newDataColLeft,
179
+ endRow: rowIndex,
180
+ endCol: newDataColLeft,
181
+ });
182
+ }
183
+ break;
184
+ }
185
+ case 'Tab': {
186
+ e.preventDefault();
187
+ let newRowTab = rowIndex;
188
+ let newColTab = columnIndex;
189
+ if (e.shiftKey) {
190
+ if (columnIndex > colOffset) {
191
+ newColTab = columnIndex - 1;
192
+ }
193
+ else if (rowIndex > 0) {
194
+ newRowTab = rowIndex - 1;
195
+ newColTab = maxColIndex;
196
+ }
197
+ }
198
+ else {
199
+ if (columnIndex < maxColIndex) {
200
+ newColTab = columnIndex + 1;
201
+ }
202
+ else if (rowIndex < maxRowIndex) {
203
+ newRowTab = rowIndex + 1;
204
+ newColTab = colOffset;
205
+ }
206
+ }
207
+ const newDataColTab = newColTab - colOffset;
208
+ this.setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
209
+ this.setSelectionRange({
210
+ startRow: newRowTab,
211
+ startCol: newDataColTab,
212
+ endRow: newRowTab,
213
+ endCol: newDataColTab,
214
+ });
215
+ break;
216
+ }
217
+ case 'Home': {
218
+ e.preventDefault();
219
+ const newRowHome = e.ctrlKey ? 0 : rowIndex;
220
+ this.setActiveCell({ rowIndex: newRowHome, columnIndex: colOffset });
221
+ this.setSelectionRange({
222
+ startRow: newRowHome,
223
+ startCol: 0,
224
+ endRow: newRowHome,
225
+ endCol: 0,
226
+ });
227
+ break;
228
+ }
229
+ case 'End': {
230
+ e.preventDefault();
231
+ const newRowEnd = e.ctrlKey ? maxRowIndex : rowIndex;
232
+ this.setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
233
+ this.setSelectionRange({
234
+ startRow: newRowEnd,
235
+ startCol: visibleCols.length - 1,
236
+ endRow: newRowEnd,
237
+ endCol: visibleCols.length - 1,
238
+ });
239
+ break;
240
+ }
241
+ case 'Enter':
242
+ case 'F2': {
243
+ e.preventDefault();
244
+ if (dataColIndex >= 0 && dataColIndex < visibleCols.length) {
245
+ const col = visibleCols[dataColIndex];
246
+ const item = items[rowIndex];
247
+ if (item && col) {
248
+ const colEditable = col.editable === true ||
249
+ (typeof col.editable === 'function' && col.editable(item));
250
+ if (editable !== false && colEditable) {
251
+ onStartEdit?.(getRowId(item), col.columnId);
252
+ }
253
+ }
254
+ }
255
+ break;
256
+ }
257
+ case 'Escape':
258
+ e.preventDefault();
259
+ clearClipboardRanges?.();
260
+ this.setActiveCell(null);
261
+ this.setSelectionRange(null);
262
+ break;
263
+ case 'z':
264
+ if (e.ctrlKey || e.metaKey) {
265
+ if (e.shiftKey && onRedo) {
266
+ e.preventDefault();
267
+ onRedo();
268
+ }
269
+ else if (!e.shiftKey && onUndo) {
270
+ e.preventDefault();
271
+ onUndo();
272
+ }
273
+ }
274
+ break;
275
+ case 'y':
276
+ if (e.ctrlKey || e.metaKey) {
277
+ e.preventDefault();
278
+ onRedo?.();
279
+ }
280
+ break;
281
+ case 'a':
282
+ if (e.ctrlKey || e.metaKey) {
283
+ e.preventDefault();
284
+ if (items.length > 0 && visibleCols.length > 0) {
285
+ this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
286
+ this.setSelectionRange({
287
+ startRow: 0,
288
+ startCol: 0,
289
+ endRow: items.length - 1,
290
+ endCol: visibleCols.length - 1,
291
+ });
292
+ }
293
+ }
294
+ break;
295
+ case 'Delete':
296
+ case 'Backspace': {
297
+ if (editable === false)
298
+ break;
299
+ if (onCellValueChanged == null)
300
+ break;
301
+ const range = selectionRange ??
302
+ (activeCell != null
303
+ ? {
304
+ startRow: activeCell.rowIndex,
305
+ startCol: activeCell.columnIndex - colOffset,
306
+ endRow: activeCell.rowIndex,
307
+ endCol: activeCell.columnIndex - colOffset,
308
+ }
309
+ : null);
310
+ if (range == null)
311
+ break;
312
+ e.preventDefault();
313
+ const norm = normalizeSelectionRange(range);
314
+ for (let r = norm.startRow; r <= norm.endRow; r++) {
315
+ for (let c = norm.startCol; c <= norm.endCol; c++) {
316
+ if (r >= items.length || c >= visibleCols.length)
317
+ continue;
318
+ const item = items[r];
319
+ const col = visibleCols[c];
320
+ const colEditable = col.editable === true ||
321
+ (typeof col.editable === 'function' && col.editable(item));
322
+ if (!colEditable)
323
+ continue;
324
+ const oldValue = getCellValue(item, col);
325
+ const result = parseValue('', oldValue, item, col);
326
+ if (!result.valid)
327
+ continue;
328
+ onCellValueChanged({
329
+ item,
330
+ columnId: col.columnId,
331
+ oldValue,
332
+ newValue: result.value,
333
+ rowIndex: r,
334
+ });
335
+ }
336
+ }
337
+ break;
338
+ }
339
+ case 'F10':
340
+ if (e.shiftKey) {
341
+ e.preventDefault();
342
+ if (activeCell != null && this.wrapperRef) {
343
+ const sel = `[data-row-index="${activeCell.rowIndex}"][data-col-index="${activeCell.columnIndex}"]`;
344
+ const cell = this.wrapperRef.querySelector(sel);
345
+ if (cell) {
346
+ const rect = cell.getBoundingClientRect();
347
+ onContextMenu?.(rect.left + rect.width / 2, rect.top + rect.height / 2);
348
+ }
349
+ else {
350
+ onContextMenu?.(100, 100);
351
+ }
352
+ }
353
+ else {
354
+ onContextMenu?.(100, 100);
355
+ }
356
+ }
357
+ break;
358
+ default:
359
+ break;
360
+ }
361
+ };
362
+ this.params = params;
363
+ this.getActiveCell = getActiveCell;
364
+ this.getSelectionRange = getSelectionRange;
365
+ this.setActiveCell = setActiveCell;
366
+ this.setSelectionRange = setSelectionRange;
367
+ }
368
+ setWrapperRef(ref) {
369
+ this.wrapperRef = ref;
370
+ }
371
+ updateParams(params) {
372
+ this.params = params;
373
+ }
374
+ }
@@ -0,0 +1,81 @@
1
+ import { EventEmitter } from './EventEmitter';
2
+ /**
3
+ * Manages row selection state for single or multiple selection modes with shift-click range support.
4
+ * Vanilla JS equivalent of React's `useRowSelection` hook.
5
+ */
6
+ export class RowSelectionState {
7
+ constructor(rowSelection, getRowId) {
8
+ this.emitter = new EventEmitter();
9
+ this._selectedRowIds = new Set();
10
+ this._lastClickedRow = -1;
11
+ this._rowSelection = rowSelection;
12
+ this._getRowId = getRowId;
13
+ }
14
+ get selectedRowIds() {
15
+ return this._selectedRowIds;
16
+ }
17
+ get rowSelection() {
18
+ return this._rowSelection;
19
+ }
20
+ updateSelection(newSelectedIds, items) {
21
+ this._selectedRowIds = newSelectedIds;
22
+ this.emitter.emit('rowSelectionChange', {
23
+ selectedRowIds: Array.from(newSelectedIds),
24
+ selectedItems: items.filter((item) => newSelectedIds.has(this._getRowId(item))),
25
+ });
26
+ }
27
+ handleRowCheckboxChange(rowId, checked, rowIndex, shiftKey, items) {
28
+ if (this._rowSelection === 'single') {
29
+ this.updateSelection(checked ? new Set([rowId]) : new Set(), items);
30
+ this._lastClickedRow = rowIndex;
31
+ return;
32
+ }
33
+ const next = new Set(this._selectedRowIds);
34
+ if (shiftKey && this._lastClickedRow >= 0 && this._lastClickedRow !== rowIndex) {
35
+ const start = Math.min(this._lastClickedRow, rowIndex);
36
+ const end = Math.max(this._lastClickedRow, rowIndex);
37
+ for (let i = start; i <= end; i++) {
38
+ if (i < items.length) {
39
+ const id = this._getRowId(items[i]);
40
+ if (checked)
41
+ next.add(id);
42
+ else
43
+ next.delete(id);
44
+ }
45
+ }
46
+ }
47
+ else {
48
+ if (checked)
49
+ next.add(rowId);
50
+ else
51
+ next.delete(rowId);
52
+ }
53
+ this._lastClickedRow = rowIndex;
54
+ this.updateSelection(next, items);
55
+ }
56
+ handleSelectAll(checked, items) {
57
+ if (checked) {
58
+ this.updateSelection(new Set(items.map((item) => this._getRowId(item))), items);
59
+ }
60
+ else {
61
+ this.updateSelection(new Set(), items);
62
+ }
63
+ }
64
+ isAllSelected(items) {
65
+ return items.length > 0 && items.every((item) => this._selectedRowIds.has(this._getRowId(item)));
66
+ }
67
+ isSomeSelected(items) {
68
+ const allSelected = this.isAllSelected(items);
69
+ return !allSelected && items.some((item) => this._selectedRowIds.has(this._getRowId(item)));
70
+ }
71
+ getSelectedRows(items) {
72
+ return items.filter((item) => this._selectedRowIds.has(this._getRowId(item)));
73
+ }
74
+ onRowSelectionChange(handler) {
75
+ this.emitter.on('rowSelectionChange', handler);
76
+ return () => this.emitter.off('rowSelectionChange', handler);
77
+ }
78
+ destroy() {
79
+ this.emitter.removeAllListeners();
80
+ }
81
+ }