@alaarab/ogrid-react 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.
- package/README.md +55 -0
- package/dist/esm/components/BaseInlineCellEditor.js +112 -0
- package/dist/esm/components/CellErrorBoundary.js +43 -0
- package/dist/esm/components/EmptyState.js +19 -0
- package/dist/esm/components/GridContextMenu.js +35 -0
- package/dist/esm/components/MarchingAntsOverlay.js +110 -0
- package/dist/esm/components/OGridLayout.js +91 -0
- package/dist/esm/components/SideBar.js +122 -0
- package/dist/esm/components/StatusBar.js +6 -0
- package/dist/esm/hooks/index.js +25 -0
- package/dist/esm/hooks/useActiveCell.js +62 -0
- package/dist/esm/hooks/useCellEditing.js +15 -0
- package/dist/esm/hooks/useCellSelection.js +327 -0
- package/dist/esm/hooks/useClipboard.js +161 -0
- package/dist/esm/hooks/useColumnChooserState.js +62 -0
- package/dist/esm/hooks/useColumnHeaderFilterState.js +180 -0
- package/dist/esm/hooks/useColumnResize.js +92 -0
- package/dist/esm/hooks/useContextMenu.js +21 -0
- package/dist/esm/hooks/useDataGridState.js +313 -0
- package/dist/esm/hooks/useDateFilterState.js +34 -0
- package/dist/esm/hooks/useDebounce.js +35 -0
- package/dist/esm/hooks/useFillHandle.js +195 -0
- package/dist/esm/hooks/useFilterOptions.js +40 -0
- package/dist/esm/hooks/useInlineCellEditorState.js +44 -0
- package/dist/esm/hooks/useKeyboardNavigation.js +419 -0
- package/dist/esm/hooks/useLatestRef.js +11 -0
- package/dist/esm/hooks/useMultiSelectFilterState.js +59 -0
- package/dist/esm/hooks/useOGrid.js +465 -0
- package/dist/esm/hooks/usePeopleFilterState.js +68 -0
- package/dist/esm/hooks/useRichSelectState.js +58 -0
- package/dist/esm/hooks/useRowSelection.js +80 -0
- package/dist/esm/hooks/useSideBarState.js +39 -0
- package/dist/esm/hooks/useTableLayout.js +77 -0
- package/dist/esm/hooks/useTextFilterState.js +25 -0
- package/dist/esm/hooks/useUndoRedo.js +83 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/storybook/index.js +1 -0
- package/dist/esm/storybook/mockData.js +73 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/dataGridViewModel.js +220 -0
- package/dist/esm/utils/gridRowComparator.js +2 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/types/components/BaseInlineCellEditor.d.ts +33 -0
- package/dist/types/components/CellErrorBoundary.d.ts +25 -0
- package/dist/types/components/EmptyState.d.ts +26 -0
- package/dist/types/components/GridContextMenu.d.ts +18 -0
- package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
- package/dist/types/components/OGridLayout.d.ts +37 -0
- package/dist/types/components/SideBar.d.ts +30 -0
- package/dist/types/components/StatusBar.d.ts +24 -0
- package/dist/types/hooks/index.d.ts +48 -0
- package/dist/types/hooks/useActiveCell.d.ts +13 -0
- package/dist/types/hooks/useCellEditing.d.ts +16 -0
- package/dist/types/hooks/useCellSelection.d.ts +22 -0
- package/dist/types/hooks/useClipboard.d.ts +30 -0
- package/dist/types/hooks/useColumnChooserState.d.ts +27 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +73 -0
- package/dist/types/hooks/useColumnResize.d.ts +23 -0
- package/dist/types/hooks/useContextMenu.d.ts +19 -0
- package/dist/types/hooks/useDataGridState.d.ts +137 -0
- package/dist/types/hooks/useDateFilterState.d.ts +19 -0
- package/dist/types/hooks/useDebounce.d.ts +9 -0
- package/dist/types/hooks/useFillHandle.d.ts +33 -0
- package/dist/types/hooks/useFilterOptions.d.ts +16 -0
- package/dist/types/hooks/useInlineCellEditorState.d.ts +24 -0
- package/dist/types/hooks/useKeyboardNavigation.d.ts +47 -0
- package/dist/types/hooks/useLatestRef.d.ts +6 -0
- package/dist/types/hooks/useMultiSelectFilterState.d.ts +24 -0
- package/dist/types/hooks/useOGrid.d.ts +52 -0
- package/dist/types/hooks/usePeopleFilterState.d.ts +25 -0
- package/dist/types/hooks/useRichSelectState.d.ts +22 -0
- package/dist/types/hooks/useRowSelection.d.ts +22 -0
- package/dist/types/hooks/useSideBarState.d.ts +20 -0
- package/dist/types/hooks/useTableLayout.d.ts +27 -0
- package/dist/types/hooks/useTextFilterState.d.ts +16 -0
- package/dist/types/hooks/useUndoRedo.d.ts +23 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/storybook/index.d.ts +2 -0
- package/dist/types/storybook/mockData.d.ts +37 -0
- package/dist/types/types/columnTypes.d.ts +25 -0
- package/dist/types/types/dataGridTypes.d.ts +152 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +161 -0
- package/dist/types/utils/gridRowComparator.d.ts +2 -0
- package/dist/types/utils/index.d.ts +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { normalizeSelectionRange } from '../types';
|
|
3
|
+
import { getCellValue, parseValue } from '../utils';
|
|
4
|
+
/**
|
|
5
|
+
* Excel-style Ctrl+Arrow: find the target position along a 1D axis.
|
|
6
|
+
* - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
|
|
7
|
+
* - Otherwise → skip empties, land on next non-empty or edge.
|
|
8
|
+
*/
|
|
9
|
+
function findCtrlTarget(pos, edge, step, isEmpty) {
|
|
10
|
+
if (pos === edge)
|
|
11
|
+
return pos;
|
|
12
|
+
const next = pos + step;
|
|
13
|
+
if (!isEmpty(pos) && !isEmpty(next)) {
|
|
14
|
+
let p = next;
|
|
15
|
+
while (p !== edge) {
|
|
16
|
+
if (isEmpty(p + step))
|
|
17
|
+
return p;
|
|
18
|
+
p += step;
|
|
19
|
+
}
|
|
20
|
+
return edge;
|
|
21
|
+
}
|
|
22
|
+
let p = next;
|
|
23
|
+
while (p !== edge) {
|
|
24
|
+
if (!isEmpty(p))
|
|
25
|
+
return p;
|
|
26
|
+
p += step;
|
|
27
|
+
}
|
|
28
|
+
return edge;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handles all keyboard navigation, shortcuts, and cell editing triggers for the grid.
|
|
32
|
+
* @param params - Grouped data, state, handlers, and feature flags for keyboard interactions.
|
|
33
|
+
* @returns Keyboard event handler for the grid wrapper.
|
|
34
|
+
*/
|
|
35
|
+
export function useKeyboardNavigation(params) {
|
|
36
|
+
// Store latest params in a ref so handleGridKeyDown is a stable callback
|
|
37
|
+
const paramsRef = useRef(params);
|
|
38
|
+
paramsRef.current = params;
|
|
39
|
+
const handleGridKeyDown = useCallback((e) => {
|
|
40
|
+
const { data, state, handlers, features } = paramsRef.current;
|
|
41
|
+
const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId } = data;
|
|
42
|
+
const { activeCell, selectionRange, editingCell, selectedRowIds } = state;
|
|
43
|
+
const { setActiveCell, setSelectionRange, setEditingCell, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu, onUndo, onRedo, clearClipboardRanges } = handlers;
|
|
44
|
+
const { editable, onCellValueChanged, rowSelection, wrapperRef } = features;
|
|
45
|
+
const maxRowIndex = items.length - 1;
|
|
46
|
+
const maxColIndex = visibleColumnCount - 1 + colOffset;
|
|
47
|
+
if (items.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
if (activeCell === null) {
|
|
50
|
+
if ([
|
|
51
|
+
'ArrowDown',
|
|
52
|
+
'ArrowUp',
|
|
53
|
+
'ArrowLeft',
|
|
54
|
+
'ArrowRight',
|
|
55
|
+
'Tab',
|
|
56
|
+
'Enter',
|
|
57
|
+
'Home',
|
|
58
|
+
'End',
|
|
59
|
+
].includes(e.key)) {
|
|
60
|
+
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const { rowIndex, columnIndex } = activeCell;
|
|
66
|
+
const dataColIndex = columnIndex - colOffset;
|
|
67
|
+
const shift = e.shiftKey;
|
|
68
|
+
const isEmptyAt = (r, c) => {
|
|
69
|
+
if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
|
|
70
|
+
return true;
|
|
71
|
+
const v = getCellValue(items[r], visibleCols[c]);
|
|
72
|
+
return v == null || v === '';
|
|
73
|
+
};
|
|
74
|
+
switch (e.key) {
|
|
75
|
+
case 'c':
|
|
76
|
+
if (e.ctrlKey || e.metaKey) {
|
|
77
|
+
if (editingCell != null)
|
|
78
|
+
break; // let the input handle copy
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
handleCopy();
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'x':
|
|
84
|
+
if (e.ctrlKey || e.metaKey) {
|
|
85
|
+
if (editingCell != null)
|
|
86
|
+
break; // let the input handle cut
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
handleCut();
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'v':
|
|
92
|
+
if (e.ctrlKey || e.metaKey) {
|
|
93
|
+
if (editingCell != null)
|
|
94
|
+
break; // let the input handle paste
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
void handlePaste();
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'ArrowDown': {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
102
|
+
const newRow = ctrl
|
|
103
|
+
? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
104
|
+
: Math.min(rowIndex + 1, maxRowIndex);
|
|
105
|
+
if (shift) {
|
|
106
|
+
setSelectionRange(normalizeSelectionRange({
|
|
107
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
108
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
109
|
+
endRow: newRow,
|
|
110
|
+
endCol: selectionRange?.endCol ?? dataColIndex,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
setSelectionRange({
|
|
115
|
+
startRow: newRow,
|
|
116
|
+
startCol: dataColIndex,
|
|
117
|
+
endRow: newRow,
|
|
118
|
+
endCol: dataColIndex,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
setActiveCell({ rowIndex: newRow, columnIndex });
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'ArrowUp': {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
127
|
+
const newRowUp = ctrl
|
|
128
|
+
? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
129
|
+
: Math.max(rowIndex - 1, 0);
|
|
130
|
+
if (shift) {
|
|
131
|
+
setSelectionRange(normalizeSelectionRange({
|
|
132
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
133
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
134
|
+
endRow: newRowUp,
|
|
135
|
+
endCol: selectionRange?.endCol ?? dataColIndex,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
setSelectionRange({
|
|
140
|
+
startRow: newRowUp,
|
|
141
|
+
startCol: dataColIndex,
|
|
142
|
+
endRow: newRowUp,
|
|
143
|
+
endCol: dataColIndex,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
setActiveCell({ rowIndex: newRowUp, columnIndex });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'ArrowRight': {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
152
|
+
let newCol;
|
|
153
|
+
if (ctrl && dataColIndex >= 0) {
|
|
154
|
+
newCol = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
newCol = Math.min(columnIndex + 1, maxColIndex);
|
|
158
|
+
}
|
|
159
|
+
const newDataCol = newCol - colOffset;
|
|
160
|
+
if (shift) {
|
|
161
|
+
setSelectionRange(normalizeSelectionRange({
|
|
162
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
163
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
164
|
+
endRow: selectionRange?.endRow ?? rowIndex,
|
|
165
|
+
endCol: newDataCol,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
setSelectionRange({
|
|
170
|
+
startRow: rowIndex,
|
|
171
|
+
startCol: newDataCol,
|
|
172
|
+
endRow: rowIndex,
|
|
173
|
+
endCol: newDataCol,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
setActiveCell({ rowIndex, columnIndex: newCol });
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'ArrowLeft': {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
182
|
+
let newColLeft;
|
|
183
|
+
if (ctrl && dataColIndex >= 0) {
|
|
184
|
+
newColLeft = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
newColLeft = Math.max(columnIndex - 1, colOffset);
|
|
188
|
+
}
|
|
189
|
+
const newDataColLeft = newColLeft - colOffset;
|
|
190
|
+
if (shift) {
|
|
191
|
+
setSelectionRange(normalizeSelectionRange({
|
|
192
|
+
startRow: selectionRange?.startRow ?? rowIndex,
|
|
193
|
+
startCol: selectionRange?.startCol ?? dataColIndex,
|
|
194
|
+
endRow: selectionRange?.endRow ?? rowIndex,
|
|
195
|
+
endCol: newDataColLeft,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
setSelectionRange({
|
|
200
|
+
startRow: rowIndex,
|
|
201
|
+
startCol: newDataColLeft,
|
|
202
|
+
endRow: rowIndex,
|
|
203
|
+
endCol: newDataColLeft,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
setActiveCell({ rowIndex, columnIndex: newColLeft });
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'Tab': {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
let newRowTab = rowIndex;
|
|
212
|
+
let newColTab = columnIndex;
|
|
213
|
+
if (e.shiftKey) {
|
|
214
|
+
if (columnIndex > colOffset) {
|
|
215
|
+
newColTab = columnIndex - 1;
|
|
216
|
+
}
|
|
217
|
+
else if (rowIndex > 0) {
|
|
218
|
+
newRowTab = rowIndex - 1;
|
|
219
|
+
newColTab = maxColIndex;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
if (columnIndex < maxColIndex) {
|
|
224
|
+
newColTab = columnIndex + 1;
|
|
225
|
+
}
|
|
226
|
+
else if (rowIndex < maxRowIndex) {
|
|
227
|
+
newRowTab = rowIndex + 1;
|
|
228
|
+
newColTab = colOffset;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const newDataColTab = newColTab - colOffset;
|
|
232
|
+
setSelectionRange({
|
|
233
|
+
startRow: newRowTab,
|
|
234
|
+
startCol: newDataColTab,
|
|
235
|
+
endRow: newRowTab,
|
|
236
|
+
endCol: newDataColTab,
|
|
237
|
+
});
|
|
238
|
+
setActiveCell({ rowIndex: newRowTab, columnIndex: newColTab });
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'Home': {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
const newRowHome = e.ctrlKey ? 0 : rowIndex;
|
|
244
|
+
setSelectionRange({
|
|
245
|
+
startRow: newRowHome,
|
|
246
|
+
startCol: 0,
|
|
247
|
+
endRow: newRowHome,
|
|
248
|
+
endCol: 0,
|
|
249
|
+
});
|
|
250
|
+
setActiveCell({ rowIndex: newRowHome, columnIndex: colOffset });
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'End': {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
const newRowEnd = e.ctrlKey ? maxRowIndex : rowIndex;
|
|
256
|
+
setSelectionRange({
|
|
257
|
+
startRow: newRowEnd,
|
|
258
|
+
startCol: visibleColumnCount - 1,
|
|
259
|
+
endRow: newRowEnd,
|
|
260
|
+
endCol: visibleColumnCount - 1,
|
|
261
|
+
});
|
|
262
|
+
setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 'Enter':
|
|
266
|
+
case 'F2': {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
if (dataColIndex >= 0 && dataColIndex < visibleCols.length) {
|
|
269
|
+
const col = visibleCols[dataColIndex];
|
|
270
|
+
const item = items[rowIndex];
|
|
271
|
+
if (item && col) {
|
|
272
|
+
const colEditable = col.editable === true ||
|
|
273
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
274
|
+
if (editable !== false &&
|
|
275
|
+
colEditable &&
|
|
276
|
+
onCellValueChanged != null) {
|
|
277
|
+
setEditingCell({ rowId: getRowId(item), columnId: col.columnId });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'Escape':
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
if (editingCell != null) {
|
|
286
|
+
setEditingCell(null);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
clearClipboardRanges?.();
|
|
290
|
+
setActiveCell(null);
|
|
291
|
+
setSelectionRange(null);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
case ' ':
|
|
295
|
+
if (rowSelection !== 'none' &&
|
|
296
|
+
columnIndex === 0 &&
|
|
297
|
+
hasCheckboxCol) {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
const item = items[rowIndex];
|
|
300
|
+
if (item) {
|
|
301
|
+
const id = getRowId(item);
|
|
302
|
+
const isSelected = selectedRowIds.has(id);
|
|
303
|
+
handleRowCheckboxChange(id, !isSelected, rowIndex, e.shiftKey);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case 'z':
|
|
308
|
+
if (e.ctrlKey || e.metaKey) {
|
|
309
|
+
if (editingCell == null) {
|
|
310
|
+
if (e.shiftKey && onRedo) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
onRedo();
|
|
313
|
+
}
|
|
314
|
+
else if (!e.shiftKey && onUndo) {
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
onUndo();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case 'y':
|
|
322
|
+
if (e.ctrlKey || e.metaKey) {
|
|
323
|
+
if (editingCell == null && onRedo) {
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
onRedo();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case 'a':
|
|
330
|
+
if (e.ctrlKey || e.metaKey) {
|
|
331
|
+
if (editingCell != null)
|
|
332
|
+
break; // let the input handle select-all
|
|
333
|
+
e.preventDefault();
|
|
334
|
+
if (items.length > 0 && visibleColumnCount > 0) {
|
|
335
|
+
setSelectionRange({
|
|
336
|
+
startRow: 0,
|
|
337
|
+
startCol: 0,
|
|
338
|
+
endRow: items.length - 1,
|
|
339
|
+
endCol: visibleColumnCount - 1,
|
|
340
|
+
});
|
|
341
|
+
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
case 'Delete':
|
|
346
|
+
case 'Backspace': {
|
|
347
|
+
if (editingCell != null)
|
|
348
|
+
break;
|
|
349
|
+
if (editable === false)
|
|
350
|
+
break;
|
|
351
|
+
if (onCellValueChanged == null)
|
|
352
|
+
break;
|
|
353
|
+
const range = selectionRange ??
|
|
354
|
+
(activeCell != null
|
|
355
|
+
? {
|
|
356
|
+
startRow: activeCell.rowIndex,
|
|
357
|
+
startCol: activeCell.columnIndex - colOffset,
|
|
358
|
+
endRow: activeCell.rowIndex,
|
|
359
|
+
endCol: activeCell.columnIndex - colOffset,
|
|
360
|
+
}
|
|
361
|
+
: null);
|
|
362
|
+
if (range == null)
|
|
363
|
+
break;
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
const norm = normalizeSelectionRange(range);
|
|
366
|
+
for (let r = norm.startRow; r <= norm.endRow; r++) {
|
|
367
|
+
for (let c = norm.startCol; c <= norm.endCol; c++) {
|
|
368
|
+
if (r >= items.length || c >= visibleCols.length)
|
|
369
|
+
continue;
|
|
370
|
+
const item = items[r];
|
|
371
|
+
const col = visibleCols[c];
|
|
372
|
+
const colEditable = col.editable === true ||
|
|
373
|
+
(typeof col.editable === 'function' && col.editable(item));
|
|
374
|
+
if (!colEditable)
|
|
375
|
+
continue;
|
|
376
|
+
const oldValue = getCellValue(item, col);
|
|
377
|
+
const result = parseValue('', oldValue, item, col);
|
|
378
|
+
if (!result.valid)
|
|
379
|
+
continue;
|
|
380
|
+
onCellValueChanged({
|
|
381
|
+
item,
|
|
382
|
+
columnId: col.columnId,
|
|
383
|
+
oldValue,
|
|
384
|
+
newValue: result.value,
|
|
385
|
+
rowIndex: r,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'F10':
|
|
392
|
+
if (e.shiftKey) {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
if (activeCell != null && wrapperRef.current) {
|
|
395
|
+
const sel = `[data-row-index="${activeCell.rowIndex}"][data-col-index="${activeCell.columnIndex}"]`;
|
|
396
|
+
const cell = wrapperRef.current.querySelector(sel);
|
|
397
|
+
if (cell) {
|
|
398
|
+
const rect = cell.getBoundingClientRect();
|
|
399
|
+
setContextMenu({
|
|
400
|
+
x: rect.left + rect.width / 2,
|
|
401
|
+
y: rect.top + rect.height / 2,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
setContextMenu({ x: 100, y: 100 });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
setContextMenu({ x: 100, y: 100 });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
default:
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}, [] // stable — reads latest values from paramsRef
|
|
417
|
+
);
|
|
418
|
+
return { handleGridKeyDown };
|
|
419
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a ref that always holds the latest value.
|
|
4
|
+
* Useful for capturing volatile state in stable callbacks
|
|
5
|
+
* without adding the value to dependency arrays.
|
|
6
|
+
*/
|
|
7
|
+
export function useLatestRef(value) {
|
|
8
|
+
const ref = useRef(value);
|
|
9
|
+
ref.current = value;
|
|
10
|
+
return ref;
|
|
11
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-select filter state sub-hook for column header filters.
|
|
3
|
+
* Manages temporary selection set, search text, debounced search, filtered options, and select/clear handlers.
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
6
|
+
import { useDebounce } from './useDebounce';
|
|
7
|
+
const SEARCH_DEBOUNCE_MS = 150;
|
|
8
|
+
const EMPTY_OPTIONS = [];
|
|
9
|
+
export function useMultiSelectFilterState(params) {
|
|
10
|
+
const { selectedValues, onFilterChange, options, isFilterOpen } = params;
|
|
11
|
+
const safeSelectedValues = selectedValues ?? EMPTY_OPTIONS;
|
|
12
|
+
const safeOptions = options ?? EMPTY_OPTIONS;
|
|
13
|
+
const [tempSelected, setTempSelected] = useState(() => new Set(safeSelectedValues));
|
|
14
|
+
const [searchText, setSearchText] = useState('');
|
|
15
|
+
const debouncedSearchText = useDebounce(searchText, SEARCH_DEBOUNCE_MS);
|
|
16
|
+
// Sync temp state when popover opens
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isFilterOpen) {
|
|
19
|
+
setTempSelected(new Set(safeSelectedValues));
|
|
20
|
+
setSearchText('');
|
|
21
|
+
}
|
|
22
|
+
}, [isFilterOpen, safeSelectedValues]);
|
|
23
|
+
// Filtered options for multiSelect (search within options)
|
|
24
|
+
const filteredOptions = useMemo(() => {
|
|
25
|
+
if (!debouncedSearchText.trim())
|
|
26
|
+
return safeOptions;
|
|
27
|
+
const searchLower = debouncedSearchText.toLowerCase().trim();
|
|
28
|
+
return safeOptions.filter((opt) => opt.toLowerCase().includes(searchLower));
|
|
29
|
+
}, [safeOptions, debouncedSearchText]);
|
|
30
|
+
const handleCheckboxChange = useCallback((option, checked) => {
|
|
31
|
+
setTempSelected((prev) => {
|
|
32
|
+
const next = new Set(prev);
|
|
33
|
+
if (checked)
|
|
34
|
+
next.add(option);
|
|
35
|
+
else
|
|
36
|
+
next.delete(option);
|
|
37
|
+
return next;
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
const handleSelectAll = useCallback(() => {
|
|
41
|
+
setTempSelected(new Set(filteredOptions));
|
|
42
|
+
}, [filteredOptions]);
|
|
43
|
+
const handleClearSelection = useCallback(() => setTempSelected(new Set()), []);
|
|
44
|
+
const handleApplyMultiSelect = useCallback(() => {
|
|
45
|
+
onFilterChange?.(Array.from(tempSelected));
|
|
46
|
+
}, [onFilterChange, tempSelected]);
|
|
47
|
+
return {
|
|
48
|
+
tempSelected,
|
|
49
|
+
setTempSelected,
|
|
50
|
+
searchText,
|
|
51
|
+
setSearchText,
|
|
52
|
+
debouncedSearchText,
|
|
53
|
+
filteredOptions,
|
|
54
|
+
handleCheckboxChange,
|
|
55
|
+
handleSelectAll,
|
|
56
|
+
handleClearSelection,
|
|
57
|
+
handleApplyMultiSelect,
|
|
58
|
+
};
|
|
59
|
+
}
|