@alaarab/ogrid-react 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.
- package/dist/esm/components/ColumnHeaderFilterContent.js +0 -2
- package/dist/esm/components/MarchingAntsOverlay.js +2 -3
- package/dist/esm/components/SideBar.js +8 -7
- package/dist/esm/components/createOGrid.js +1 -4
- package/dist/esm/hooks/index.js +10 -0
- package/dist/esm/hooks/useActiveCell.js +2 -4
- package/dist/esm/hooks/useCellSelection.js +85 -52
- package/dist/esm/hooks/useClipboard.js +15 -54
- package/dist/esm/hooks/useColumnChooserState.js +25 -13
- package/dist/esm/hooks/useColumnHeaderFilterState.js +22 -11
- package/dist/esm/hooks/useColumnHeaderMenuState.js +1 -1
- package/dist/esm/hooks/useColumnMeta.js +61 -0
- package/dist/esm/hooks/useColumnPinning.js +11 -12
- package/dist/esm/hooks/useColumnReorder.js +8 -1
- package/dist/esm/hooks/useColumnResize.js +6 -2
- package/dist/esm/hooks/useDataGridContextMenu.js +24 -0
- package/dist/esm/hooks/useDataGridEditing.js +56 -0
- package/dist/esm/hooks/useDataGridInteraction.js +109 -0
- package/dist/esm/hooks/useDataGridLayout.js +172 -0
- package/dist/esm/hooks/useDataGridState.js +83 -318
- package/dist/esm/hooks/useDataGridTableOrchestration.js +2 -4
- package/dist/esm/hooks/useFillHandle.js +60 -55
- package/dist/esm/hooks/useFilterOptions.js +2 -4
- package/dist/esm/hooks/useInlineCellEditorState.js +7 -13
- package/dist/esm/hooks/useKeyboardNavigation.js +19 -132
- package/dist/esm/hooks/useMultiSelectFilterState.js +1 -1
- package/dist/esm/hooks/useOGrid.js +159 -301
- package/dist/esm/hooks/useOGridDataFetching.js +74 -0
- package/dist/esm/hooks/useOGridFilters.js +59 -0
- package/dist/esm/hooks/useOGridPagination.js +24 -0
- package/dist/esm/hooks/useOGridSorting.js +24 -0
- package/dist/esm/hooks/usePaginationControls.js +2 -5
- package/dist/esm/hooks/usePeopleFilterState.js +6 -1
- package/dist/esm/hooks/useRichSelectState.js +7 -5
- package/dist/esm/hooks/useRowSelection.js +6 -26
- package/dist/esm/hooks/useSelectState.js +2 -5
- package/dist/esm/hooks/useShallowEqualMemo.js +14 -0
- package/dist/esm/hooks/useTableLayout.js +3 -11
- package/dist/esm/hooks/useUndoRedo.js +16 -10
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/index.js +1 -1
- package/dist/types/components/ColumnChooserProps.d.ts +2 -0
- package/dist/types/components/ColumnHeaderFilterContent.d.ts +0 -2
- package/dist/types/hooks/index.d.ts +19 -0
- package/dist/types/hooks/useClipboard.d.ts +0 -1
- package/dist/types/hooks/useColumnChooserState.d.ts +2 -0
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +0 -2
- package/dist/types/hooks/useColumnHeaderMenuState.d.ts +0 -2
- package/dist/types/hooks/useColumnMeta.d.ts +34 -0
- package/dist/types/hooks/useDataGridContextMenu.d.ts +20 -0
- package/dist/types/hooks/useDataGridEditing.d.ts +39 -0
- package/dist/types/hooks/useDataGridInteraction.d.ts +95 -0
- package/dist/types/hooks/useDataGridLayout.d.ts +45 -0
- package/dist/types/hooks/useDataGridState.d.ts +7 -1
- package/dist/types/hooks/useDataGridTableOrchestration.d.ts +1 -2
- package/dist/types/hooks/useOGrid.d.ts +4 -2
- package/dist/types/hooks/useOGridDataFetching.d.ts +29 -0
- package/dist/types/hooks/useOGridFilters.d.ts +24 -0
- package/dist/types/hooks/useOGridPagination.d.ts +18 -0
- package/dist/types/hooks/useOGridSorting.d.ts +23 -0
- package/dist/types/hooks/usePaginationControls.d.ts +1 -1
- package/dist/types/hooks/useRichSelectState.d.ts +2 -0
- package/dist/types/hooks/useShallowEqualMemo.d.ts +7 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/utils/index.d.ts +2 -2
- package/package.json +12 -4
|
@@ -1,85 +1,51 @@
|
|
|
1
|
-
import { useMemo, useCallback
|
|
2
|
-
import {
|
|
3
|
-
import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
|
+
import { getDataGridStatusBarConfig, computeAggregations } from '../utils';
|
|
4
3
|
import { useRowSelection } from './useRowSelection';
|
|
5
4
|
import { useCellEditing } from './useCellEditing';
|
|
6
5
|
import { useActiveCell } from './useActiveCell';
|
|
7
|
-
import { useCellSelection } from './useCellSelection';
|
|
8
|
-
import { useContextMenu } from './useContextMenu';
|
|
9
|
-
import { useClipboard } from './useClipboard';
|
|
10
|
-
import { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
11
|
-
import { useFillHandle } from './useFillHandle';
|
|
12
|
-
import { useUndoRedo } from './useUndoRedo';
|
|
13
6
|
import { useLatestRef } from './useLatestRef';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const NOOP = () => { };
|
|
19
|
-
const NOOP_ASYNC = async () => { };
|
|
20
|
-
const NOOP_MOUSE = (_e, _r, _c) => { };
|
|
21
|
-
const NOOP_KEY = (_e) => { };
|
|
22
|
-
const NOOP_CTX = (_e) => { };
|
|
7
|
+
import { useDataGridLayout } from './useDataGridLayout';
|
|
8
|
+
import { useDataGridEditing } from './useDataGridEditing';
|
|
9
|
+
import { useDataGridInteraction } from './useDataGridInteraction';
|
|
10
|
+
import { useDataGridContextMenu } from './useDataGridContextMenu';
|
|
23
11
|
/**
|
|
24
12
|
* Single orchestration hook for DataGridTable. Takes grid props and wrapper ref,
|
|
25
13
|
* returns all derived state and handlers so Fluent/Material/Radix can be thin view layers.
|
|
14
|
+
*
|
|
15
|
+
* Internally delegates to focused sub-hooks:
|
|
16
|
+
* - useDataGridLayout -- column layout, sizing, pinning, header menu
|
|
17
|
+
* - useDataGridEditing -- cell editing commit/cancel, popover editor
|
|
18
|
+
* - useDataGridInteraction -- cell selection, keyboard nav, clipboard, fill handle, undo/redo
|
|
19
|
+
* - useDataGridContextMenu -- context menu state
|
|
26
20
|
*/
|
|
27
21
|
export function useDataGridState(params) {
|
|
28
22
|
const { props, wrapperRef } = params;
|
|
29
23
|
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, showRowNumbers, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, onAutosizeColumn, pinnedColumns, onColumnPinned, onCellError, } = props;
|
|
30
24
|
const cellSelection = cellSelectionProp !== false;
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Build index map for O(1) lookup instead of repeated O(n) indexOf
|
|
56
|
-
const orderMap = new Map();
|
|
57
|
-
for (let i = 0; i < columnOrder.length; i++) {
|
|
58
|
-
orderMap.set(columnOrder[i], i);
|
|
59
|
-
}
|
|
60
|
-
return [...filtered].sort((a, b) => {
|
|
61
|
-
const ia = orderMap.get(a.columnId) ?? -1;
|
|
62
|
-
const ib = orderMap.get(b.columnId) ?? -1;
|
|
63
|
-
if (ia === -1 && ib === -1)
|
|
64
|
-
return 0;
|
|
65
|
-
if (ia === -1)
|
|
66
|
-
return 1;
|
|
67
|
-
if (ib === -1)
|
|
68
|
-
return -1;
|
|
69
|
-
return ia - ib;
|
|
70
|
-
});
|
|
71
|
-
}, [flatColumns, visibleColumns, columnOrder]);
|
|
72
|
-
const visibleColumnCount = visibleCols.length;
|
|
73
|
-
const hasCheckboxCol = rowSelection === 'multiple';
|
|
74
|
-
const hasRowNumbersCol = !!showRowNumbers;
|
|
75
|
-
const specialColsCount = (hasCheckboxCol ? 1 : 0) + (hasRowNumbersCol ? 1 : 0);
|
|
76
|
-
const totalColCount = visibleColumnCount + specialColsCount;
|
|
77
|
-
const colOffset = specialColsCount;
|
|
78
|
-
const rowIndexByRowId = useMemo(() => {
|
|
79
|
-
const m = new Map();
|
|
80
|
-
items.forEach((item, idx) => m.set(getRowId(item), idx));
|
|
81
|
-
return m;
|
|
82
|
-
}, [items, getRowId]);
|
|
25
|
+
// --- Shared state hooks (called at orchestrator level to break circular deps) ---
|
|
26
|
+
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, } = useCellEditing();
|
|
27
|
+
const { activeCell, setActiveCell } = useActiveCell(wrapperRef, editingCell);
|
|
28
|
+
// --- 1. Layout, pinning, header menu ---
|
|
29
|
+
const layoutResult = useDataGridLayout({
|
|
30
|
+
columns,
|
|
31
|
+
items,
|
|
32
|
+
getRowId,
|
|
33
|
+
visibleColumns,
|
|
34
|
+
columnOrder,
|
|
35
|
+
rowSelection,
|
|
36
|
+
showRowNumbers,
|
|
37
|
+
initialColumnWidths,
|
|
38
|
+
onColumnResized,
|
|
39
|
+
onAutosizeColumn,
|
|
40
|
+
pinnedColumns,
|
|
41
|
+
onColumnPinned,
|
|
42
|
+
sortBy: props.sortBy,
|
|
43
|
+
sortDirection: props.sortDirection,
|
|
44
|
+
onColumnSort: props.onColumnSort,
|
|
45
|
+
wrapperRef,
|
|
46
|
+
});
|
|
47
|
+
const { visibleCols, visibleColumnCount, colOffset, hasCheckboxCol, } = layoutResult;
|
|
48
|
+
// --- 2. Row selection ---
|
|
83
49
|
const rowSelectionResult = useRowSelection({
|
|
84
50
|
items,
|
|
85
51
|
getRowId,
|
|
@@ -88,159 +54,52 @@ export function useDataGridState(params) {
|
|
|
88
54
|
onSelectionChange,
|
|
89
55
|
});
|
|
90
56
|
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected, } = rowSelectionResult;
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
visibleColCount: visibleCols.length,
|
|
97
|
-
setActiveCell,
|
|
98
|
-
wrapperRef,
|
|
99
|
-
});
|
|
100
|
-
const { contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu } = useContextMenu();
|
|
101
|
-
const { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges } = useClipboard({
|
|
57
|
+
// --- 3. Context menu ---
|
|
58
|
+
const contextMenuResult = useDataGridContextMenu({ cellSelection });
|
|
59
|
+
const { setContextMenuPosition } = contextMenuResult;
|
|
60
|
+
// --- 4. Interaction (selection, keyboard, clipboard, fill handle, undo/redo) ---
|
|
61
|
+
const interactionResult = useDataGridInteraction({
|
|
102
62
|
items,
|
|
103
63
|
visibleCols,
|
|
104
64
|
colOffset,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
onCellValueChanged,
|
|
109
|
-
beginBatch: undoRedo.beginBatch,
|
|
110
|
-
endBatch: undoRedo.endBatch,
|
|
111
|
-
});
|
|
112
|
-
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
113
|
-
if (e.button !== 0)
|
|
114
|
-
return;
|
|
115
|
-
wrapperRef.current?.focus({ preventScroll: true });
|
|
116
|
-
clearClipboardRanges();
|
|
117
|
-
handleCellMouseDownBase(e, rowIndex, globalColIndex);
|
|
118
|
-
},
|
|
119
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
-
[handleCellMouseDownBase, clearClipboardRanges] // wrapperRef excluded — refs are stable
|
|
121
|
-
);
|
|
122
|
-
const { handleGridKeyDown } = useKeyboardNavigation({
|
|
123
|
-
data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
|
|
124
|
-
state: { activeCell, selectionRange, editingCell, selectedRowIds },
|
|
125
|
-
handlers: { setActiveCell, setSelectionRange, setEditingCell, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu: setContextMenuPosition, onUndo: undoRedo.undo, onRedo: undoRedo.redo, clearClipboardRanges },
|
|
126
|
-
features: { editable, onCellValueChanged, rowSelection, wrapperRef },
|
|
127
|
-
});
|
|
128
|
-
const { handleFillHandleMouseDown } = useFillHandle({
|
|
129
|
-
items,
|
|
130
|
-
visibleCols,
|
|
65
|
+
hasCheckboxCol,
|
|
66
|
+
visibleColumnCount,
|
|
67
|
+
getRowId,
|
|
131
68
|
editable,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
69
|
+
onCellValueChangedProp,
|
|
70
|
+
cellSelection,
|
|
71
|
+
rowSelection,
|
|
72
|
+
selectedRowIds,
|
|
73
|
+
editingCell,
|
|
74
|
+
setEditingCell,
|
|
75
|
+
activeCell,
|
|
135
76
|
setActiveCell,
|
|
136
|
-
|
|
77
|
+
handleRowCheckboxChange,
|
|
78
|
+
setContextMenuPosition,
|
|
137
79
|
wrapperRef,
|
|
138
|
-
beginBatch: undoRedo.beginBatch,
|
|
139
|
-
endBatch: undoRedo.endBatch,
|
|
140
80
|
});
|
|
141
|
-
const {
|
|
142
|
-
|
|
81
|
+
const { selectionRange, cutRange, copyRange, isDragging, onCellValueChanged, } = interactionResult;
|
|
82
|
+
// --- 5. Editing (commit/cancel logic) ---
|
|
83
|
+
const editingResult = useDataGridEditing({
|
|
84
|
+
editingCell,
|
|
85
|
+
setEditingCell,
|
|
86
|
+
pendingEditorValue,
|
|
87
|
+
setPendingEditorValue,
|
|
143
88
|
visibleCols,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
onColumnResized,
|
|
148
|
-
});
|
|
149
|
-
const pinningResult = useColumnPinning({
|
|
150
|
-
columns: flatColumns,
|
|
151
|
-
pinnedColumns,
|
|
152
|
-
onColumnPinned,
|
|
89
|
+
itemsLength: items.length,
|
|
90
|
+
onCellValueChanged,
|
|
91
|
+
setActiveCell,
|
|
153
92
|
});
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
|
|
157
|
-
useLayoutEffect(() => {
|
|
158
|
-
const wrapper = wrapperRef.current;
|
|
159
|
-
if (!wrapper)
|
|
160
|
-
return;
|
|
161
|
-
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
162
|
-
if (headerCells.length === 0)
|
|
163
|
-
return;
|
|
164
|
-
const measured = {};
|
|
165
|
-
headerCells.forEach((cell) => {
|
|
166
|
-
const colId = cell.getAttribute('data-column-id');
|
|
167
|
-
if (colId)
|
|
168
|
-
measured[colId] = cell.offsetWidth;
|
|
169
|
-
});
|
|
170
|
-
setMeasuredColumnWidths((prev) => {
|
|
171
|
-
// Only update if widths actually changed to avoid render loops
|
|
172
|
-
for (const key in measured) {
|
|
173
|
-
if (prev[key] !== measured[key])
|
|
174
|
-
return measured;
|
|
175
|
-
}
|
|
176
|
-
if (Object.keys(prev).length !== Object.keys(measured).length)
|
|
177
|
-
return measured;
|
|
178
|
-
return prev;
|
|
179
|
-
});
|
|
180
|
-
// Re-measure when columns, container size, or resize overrides change
|
|
181
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
-
}, [visibleCols, containerWidth, columnSizingOverrides]);
|
|
183
|
-
// Build column width map for pinning offset computation
|
|
184
|
-
const columnWidthMap = useMemo(() => {
|
|
185
|
-
const map = {};
|
|
186
|
-
for (const col of visibleCols) {
|
|
187
|
-
const override = columnSizingOverrides[col.columnId];
|
|
188
|
-
map[col.columnId] = override
|
|
189
|
-
? override.widthPx
|
|
190
|
-
: (measuredColumnWidths[col.columnId] ?? col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
|
|
191
|
-
}
|
|
192
|
-
return map;
|
|
193
|
-
}, [visibleCols, columnSizingOverrides, measuredColumnWidths]);
|
|
194
|
-
const leftOffsets = useMemo(() => pinningResult.computeLeftOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap, hasCheckboxCol]);
|
|
195
|
-
const rightOffsets = useMemo(() => pinningResult.computeRightOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap]);
|
|
196
|
-
const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
|
|
197
|
-
const statusBarConfig = useMemo(() => {
|
|
198
|
-
const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
|
|
199
|
-
if (!base)
|
|
200
|
-
return null;
|
|
201
|
-
return { ...base, aggregation: aggregation ?? undefined };
|
|
202
|
-
}, [statusBar, items.length, selectedRowIds.size, aggregation]);
|
|
203
|
-
const showEmptyInGrid = items.length === 0 && !!emptyState && !props.isLoading;
|
|
204
|
-
const hasCellSelection = selectionRange != null || activeCell != null;
|
|
205
|
-
// --- View-model inputs (shared across all 3 DataGridTables) ---
|
|
206
|
-
const { sortBy, sortDirection, onColumnSort, filters, onFilterChange, filterOptions, loadingFilterOptions, peopleSearch, } = props;
|
|
207
|
-
// Stabilize callbacks via refs — headerFilterInput only re-creates when data changes,
|
|
208
|
-
// not when callback identities change (which happens on unrelated state updates).
|
|
209
|
-
const onColumnSortRef = useLatestRef(onColumnSort);
|
|
93
|
+
// --- 6. View models ---
|
|
94
|
+
const { sortBy, sortDirection, filters, onFilterChange, filterOptions, loadingFilterOptions, peopleSearch, } = props;
|
|
210
95
|
const onFilterChangeRef = useLatestRef(onFilterChange);
|
|
211
96
|
const peopleSearchRef = useLatestRef(peopleSearch);
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
215
|
-
[]);
|
|
216
|
-
// Autosize callback — updates internal column sizing state + notifies external listener
|
|
217
|
-
const handleAutosizeColumn = useCallback((columnId, width) => {
|
|
218
|
-
setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
|
|
219
|
-
(onAutosizeColumn ?? onColumnResized)?.(columnId, width);
|
|
220
|
-
}, [setColumnSizingOverrides, onAutosizeColumn, onColumnResized]);
|
|
221
|
-
const headerMenuResult = useColumnHeaderMenuState({
|
|
222
|
-
pinnedColumns: pinningResult.pinnedColumns,
|
|
223
|
-
onPinColumn: pinningResult.pinColumn,
|
|
224
|
-
onUnpinColumn: pinningResult.unpinColumn,
|
|
225
|
-
sortBy,
|
|
226
|
-
sortDirection,
|
|
227
|
-
onColumnSort: stableOnColumnSort,
|
|
228
|
-
onColumnResized,
|
|
229
|
-
onAutosizeColumn: handleAutosizeColumn,
|
|
230
|
-
columns: flatColumns,
|
|
231
|
-
data: items,
|
|
232
|
-
getRowId: getRowId,
|
|
233
|
-
});
|
|
234
|
-
const stableOnFilterChange = useCallback((...args) => onFilterChangeRef.current?.(...args),
|
|
235
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
236
|
-
[]);
|
|
237
|
-
const stablePeopleSearch = useCallback((...args) => peopleSearchRef.current?.(...args) ?? Promise.resolve([]),
|
|
238
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
|
-
[]);
|
|
97
|
+
const stableOnFilterChange = useCallback((...args) => onFilterChangeRef.current?.(...args), [onFilterChangeRef]);
|
|
98
|
+
const stablePeopleSearch = useCallback((...args) => peopleSearchRef.current?.(...args) ?? Promise.resolve([]), [peopleSearchRef]);
|
|
240
99
|
const headerFilterInput = useMemo(() => ({
|
|
241
100
|
sortBy,
|
|
242
101
|
sortDirection,
|
|
243
|
-
onColumnSort: stableOnColumnSort,
|
|
102
|
+
onColumnSort: layoutResult.stableOnColumnSort,
|
|
244
103
|
filters,
|
|
245
104
|
onFilterChange: stableOnFilterChange,
|
|
246
105
|
filterOptions,
|
|
@@ -249,7 +108,7 @@ export function useDataGridState(params) {
|
|
|
249
108
|
}), [
|
|
250
109
|
sortBy,
|
|
251
110
|
sortDirection,
|
|
252
|
-
stableOnColumnSort,
|
|
111
|
+
layoutResult.stableOnColumnSort,
|
|
253
112
|
filters,
|
|
254
113
|
stableOnFilterChange,
|
|
255
114
|
filterOptions,
|
|
@@ -282,123 +141,29 @@ export function useDataGridState(params) {
|
|
|
282
141
|
cellSelection,
|
|
283
142
|
isDragging,
|
|
284
143
|
]);
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (!result.valid) {
|
|
295
|
-
// Reject — cancel the edit
|
|
296
|
-
setEditingCell(null);
|
|
297
|
-
setPopoverAnchorEl(null);
|
|
298
|
-
setPendingEditorValue(undefined);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
newValue = result.value;
|
|
302
|
-
}
|
|
303
|
-
onCellValueChanged?.({
|
|
304
|
-
item,
|
|
305
|
-
columnId,
|
|
306
|
-
oldValue,
|
|
307
|
-
newValue,
|
|
308
|
-
rowIndex,
|
|
309
|
-
});
|
|
310
|
-
setEditingCell(null);
|
|
311
|
-
setPopoverAnchorEl(null);
|
|
312
|
-
setPendingEditorValue(undefined);
|
|
313
|
-
// Advance to next row for inline editors
|
|
314
|
-
if (rowIndex < itemsLengthRef.current - 1) {
|
|
315
|
-
setActiveCell({ rowIndex: rowIndex + 1, columnIndex: globalColIndex });
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
319
|
-
[onCellValueChanged, setEditingCell, setPendingEditorValue, setActiveCell]);
|
|
320
|
-
const cancelPopoverEdit = useCallback(() => {
|
|
321
|
-
setEditingCell(null);
|
|
322
|
-
setPopoverAnchorEl(null);
|
|
323
|
-
setPendingEditorValue(undefined);
|
|
324
|
-
}, [setEditingCell, setPendingEditorValue]);
|
|
325
|
-
// --- Memoize each sub-object so downstream consumers only re-render when their slice changes ---
|
|
326
|
-
const layoutState = useMemo(() => ({
|
|
327
|
-
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
328
|
-
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
329
|
-
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
330
|
-
measuredColumnWidths,
|
|
331
|
-
}), [
|
|
332
|
-
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
333
|
-
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
334
|
-
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
335
|
-
measuredColumnWidths,
|
|
336
|
-
]);
|
|
144
|
+
const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
|
|
145
|
+
const statusBarConfig = useMemo(() => {
|
|
146
|
+
const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
|
|
147
|
+
if (!base)
|
|
148
|
+
return null;
|
|
149
|
+
return { ...base, aggregation: aggregation ?? undefined };
|
|
150
|
+
}, [statusBar, items.length, selectedRowIds.size, aggregation]);
|
|
151
|
+
const showEmptyInGrid = items.length === 0 && !!emptyState && !props.isLoading;
|
|
152
|
+
// --- Memoize remaining sub-objects ---
|
|
337
153
|
const rowSelectionState = useMemo(() => ({
|
|
338
154
|
selectedRowIds, updateSelection, handleRowCheckboxChange,
|
|
339
155
|
handleSelectAll, allSelected, someSelected,
|
|
340
156
|
}), [selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected]);
|
|
341
|
-
const editingState = useMemo(() => ({
|
|
342
|
-
editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue,
|
|
343
|
-
commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl,
|
|
344
|
-
}), [editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl]);
|
|
345
|
-
const interactionState = useMemo(() => ({
|
|
346
|
-
activeCell: cellSelection ? activeCell : null,
|
|
347
|
-
setActiveCell: cellSelection ? setActiveCell : NOOP,
|
|
348
|
-
selectionRange: cellSelection ? selectionRange : null,
|
|
349
|
-
setSelectionRange: cellSelection ? setSelectionRange : NOOP,
|
|
350
|
-
handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
|
|
351
|
-
handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
|
|
352
|
-
hasCellSelection: cellSelection ? hasCellSelection : false,
|
|
353
|
-
handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
|
|
354
|
-
handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
|
|
355
|
-
handleCopy: cellSelection ? handleCopy : NOOP,
|
|
356
|
-
handleCut: cellSelection ? handleCut : NOOP,
|
|
357
|
-
handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
|
|
358
|
-
cutRange: cellSelection ? cutRange : null,
|
|
359
|
-
copyRange: cellSelection ? copyRange : null,
|
|
360
|
-
clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
|
|
361
|
-
canUndo: undoRedo.canUndo,
|
|
362
|
-
canRedo: undoRedo.canRedo,
|
|
363
|
-
onUndo: undoRedo.undo,
|
|
364
|
-
onRedo: undoRedo.redo,
|
|
365
|
-
isDragging: cellSelection ? isDragging : false,
|
|
366
|
-
}), [
|
|
367
|
-
cellSelection, activeCell, setActiveCell, selectionRange, setSelectionRange,
|
|
368
|
-
handleCellMouseDown, handleSelectAllCells, hasCellSelection, handleGridKeyDown,
|
|
369
|
-
handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange,
|
|
370
|
-
clearClipboardRanges, undoRedo.canUndo, undoRedo.canRedo, undoRedo.undo, undoRedo.redo,
|
|
371
|
-
isDragging,
|
|
372
|
-
]);
|
|
373
|
-
const contextMenuState = useMemo(() => ({
|
|
374
|
-
menuPosition: cellSelection ? contextMenuPosition : null,
|
|
375
|
-
setMenuPosition: cellSelection ? setContextMenuPosition : NOOP,
|
|
376
|
-
handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
|
|
377
|
-
closeContextMenu: cellSelection ? closeContextMenu : NOOP,
|
|
378
|
-
}), [cellSelection, contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu]);
|
|
379
157
|
const viewModelsState = useMemo(() => ({
|
|
380
158
|
headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError,
|
|
381
159
|
}), [headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError]);
|
|
382
|
-
const pinningState = useMemo(() => ({
|
|
383
|
-
pinnedColumns: pinningResult.pinnedColumns,
|
|
384
|
-
pinColumn: pinningResult.pinColumn,
|
|
385
|
-
unpinColumn: pinningResult.unpinColumn,
|
|
386
|
-
isPinned: pinningResult.isPinned,
|
|
387
|
-
leftOffsets,
|
|
388
|
-
rightOffsets,
|
|
389
|
-
headerMenu: headerMenuResult,
|
|
390
|
-
}), [
|
|
391
|
-
pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
|
|
392
|
-
pinningResult.isPinned, leftOffsets, rightOffsets,
|
|
393
|
-
headerMenuResult,
|
|
394
|
-
]);
|
|
395
160
|
return {
|
|
396
|
-
layout:
|
|
161
|
+
layout: layoutResult.layout,
|
|
397
162
|
rowSelection: rowSelectionState,
|
|
398
|
-
editing:
|
|
399
|
-
interaction:
|
|
400
|
-
contextMenu:
|
|
163
|
+
editing: editingResult.editing,
|
|
164
|
+
interaction: interactionResult.interaction,
|
|
165
|
+
contextMenu: contextMenuResult.contextMenu,
|
|
401
166
|
viewModels: viewModelsState,
|
|
402
|
-
pinning:
|
|
167
|
+
pinning: layoutResult.pinning,
|
|
403
168
|
};
|
|
404
169
|
}
|
|
@@ -82,16 +82,14 @@ export function useDataGridTableOrchestration(params) {
|
|
|
82
82
|
return;
|
|
83
83
|
const ids = selectedRowIdsRef.current;
|
|
84
84
|
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
85
|
-
|
|
86
|
-
}, [rowSelection, updateSelection]);
|
|
85
|
+
}, [rowSelection, updateSelection, selectedRowIdsRef]);
|
|
87
86
|
// ── Return ─────────────────────────────────────────────────────────────
|
|
88
87
|
return {
|
|
89
88
|
// Refs
|
|
90
89
|
wrapperRef,
|
|
91
90
|
tableContainerRef,
|
|
92
91
|
lastMouseShiftRef,
|
|
93
|
-
//
|
|
94
|
-
state,
|
|
92
|
+
// State sub-objects
|
|
95
93
|
layout,
|
|
96
94
|
rowSel,
|
|
97
95
|
editing,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
|
-
import {
|
|
3
|
+
import { applyFillValues } from '../utils';
|
|
4
|
+
import { useLatestRef } from './useLatestRef';
|
|
4
5
|
/** DOM attribute name for fill-drag range highlighting (same as cell selection drag). */
|
|
5
6
|
const DRAG_ATTR = 'data-drag-range';
|
|
6
7
|
/**
|
|
@@ -14,12 +15,31 @@ export function useFillHandle(params) {
|
|
|
14
15
|
const fillDragEndRef = useRef({ endRow: 0, endCol: 0 });
|
|
15
16
|
const rafRef = useRef(0);
|
|
16
17
|
const liveFillRangeRef = useRef(null);
|
|
18
|
+
const colOffsetRef = useLatestRef(colOffset);
|
|
17
19
|
useEffect(() => {
|
|
18
20
|
if (!fillDrag || editable === false || !onCellValueChanged || !wrapperRef.current)
|
|
19
21
|
return;
|
|
20
22
|
fillDragEndRef.current = { endRow: fillDrag.startRow, endCol: fillDrag.startCol };
|
|
21
23
|
liveFillRangeRef.current = null;
|
|
22
|
-
|
|
24
|
+
/** Set of currently drag-marked HTMLElements — avoids O(n) full DOM scan on clear. */
|
|
25
|
+
const markedCells = new Set();
|
|
26
|
+
/** Cell lookup index built on drag start — O(1) lookups per frame. */
|
|
27
|
+
let cellIndex = null;
|
|
28
|
+
const buildCellIndex = () => {
|
|
29
|
+
const wrapper = wrapperRef.current;
|
|
30
|
+
if (!wrapper)
|
|
31
|
+
return;
|
|
32
|
+
cellIndex = new Map();
|
|
33
|
+
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
34
|
+
for (let i = 0; i < cells.length; i++) {
|
|
35
|
+
const el = cells[i];
|
|
36
|
+
const r = el.getAttribute('data-row-index') ?? '';
|
|
37
|
+
const c = el.getAttribute('data-col-index') ?? '';
|
|
38
|
+
cellIndex.set(`${r},${c}`, el);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// Build the index once at fill drag start
|
|
42
|
+
buildCellIndex();
|
|
23
43
|
const applyDragAttrs = (range) => {
|
|
24
44
|
const wrapper = wrapperRef.current;
|
|
25
45
|
if (!wrapper)
|
|
@@ -28,29 +48,40 @@ export function useFillHandle(params) {
|
|
|
28
48
|
const maxR = Math.max(range.startRow, range.endRow);
|
|
29
49
|
const minC = Math.min(range.startCol, range.endCol);
|
|
30
50
|
const maxC = Math.max(range.startCol, range.endCol);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
35
|
-
const c = parseInt(el.getAttribute('data-col-index'), 10) - colOff;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
el.setAttribute(DRAG_ATTR, '');
|
|
51
|
+
const colOff = colOffsetRef.current;
|
|
52
|
+
// Un-mark cells no longer in range
|
|
53
|
+
for (const el of markedCells) {
|
|
54
|
+
const r = parseInt(el.getAttribute('data-row-index') ?? '', 10);
|
|
55
|
+
const c = parseInt(el.getAttribute('data-col-index') ?? '', 10) - colOff;
|
|
56
|
+
if (!(r >= minR && r <= maxR && c >= minC && c <= maxC)) {
|
|
57
|
+
el.removeAttribute(DRAG_ATTR);
|
|
58
|
+
markedCells.delete(el);
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
}
|
|
61
|
+
// Look up only cells in the new range — O(range size) via Map lookup
|
|
62
|
+
for (let r = minR; r <= maxR; r++) {
|
|
63
|
+
for (let c = minC; c <= maxC; c++) {
|
|
64
|
+
const key = `${r},${c + colOff}`;
|
|
65
|
+
let el = cellIndex?.get(key);
|
|
66
|
+
// Handle virtual scroll recycling — if element is stale, rebuild index once
|
|
67
|
+
if (el && !el.isConnected) {
|
|
68
|
+
buildCellIndex();
|
|
69
|
+
el = cellIndex?.get(key);
|
|
70
|
+
}
|
|
71
|
+
if (el) {
|
|
72
|
+
if (!el.hasAttribute(DRAG_ATTR))
|
|
73
|
+
el.setAttribute(DRAG_ATTR, '');
|
|
74
|
+
markedCells.add(el);
|
|
75
|
+
}
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
};
|
|
47
79
|
const clearDragAttrs = () => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
marked[i].removeAttribute(DRAG_ATTR);
|
|
80
|
+
for (const el of markedCells) {
|
|
81
|
+
el.removeAttribute(DRAG_ATTR);
|
|
82
|
+
}
|
|
83
|
+
markedCells.clear();
|
|
84
|
+
cellIndex = null;
|
|
54
85
|
};
|
|
55
86
|
let lastFillMousePos = null;
|
|
56
87
|
const resolveRange = (cx, cy) => {
|
|
@@ -60,9 +91,9 @@ export function useFillHandle(params) {
|
|
|
60
91
|
return null;
|
|
61
92
|
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
62
93
|
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
63
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c <
|
|
94
|
+
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffsetRef.current)
|
|
64
95
|
return null;
|
|
65
|
-
const dataCol = c -
|
|
96
|
+
const dataCol = c - colOffsetRef.current;
|
|
66
97
|
return normalizeSelectionRange({
|
|
67
98
|
startRow: fillDrag.startRow,
|
|
68
99
|
startCol: fillDrag.startCol,
|
|
@@ -118,38 +149,13 @@ export function useFillHandle(params) {
|
|
|
118
149
|
});
|
|
119
150
|
// Commit range to React state
|
|
120
151
|
setSelectionRange(norm);
|
|
121
|
-
setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol +
|
|
152
|
+
setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOffsetRef.current });
|
|
122
153
|
// Apply fill values
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
if (startItem && startColDef) {
|
|
126
|
-
const startValue = getCellValue(startItem, startColDef);
|
|
154
|
+
const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols);
|
|
155
|
+
if (fillEvents.length > 0) {
|
|
127
156
|
beginBatch?.();
|
|
128
|
-
for (
|
|
129
|
-
|
|
130
|
-
if (row === fillDrag.startRow && col === fillDrag.startCol)
|
|
131
|
-
continue;
|
|
132
|
-
if (row >= items.length || col >= visibleCols.length)
|
|
133
|
-
continue;
|
|
134
|
-
const item = items[row];
|
|
135
|
-
const colDef = visibleCols[col];
|
|
136
|
-
const colEditable = colDef.editable === true ||
|
|
137
|
-
(typeof colDef.editable === 'function' && colDef.editable(item));
|
|
138
|
-
if (!colEditable)
|
|
139
|
-
continue;
|
|
140
|
-
const oldValue = getCellValue(item, colDef);
|
|
141
|
-
const result = parseValue(startValue, oldValue, item, colDef);
|
|
142
|
-
if (!result.valid)
|
|
143
|
-
continue;
|
|
144
|
-
onCellValueChanged({
|
|
145
|
-
item,
|
|
146
|
-
columnId: colDef.columnId,
|
|
147
|
-
oldValue,
|
|
148
|
-
newValue: result.value,
|
|
149
|
-
rowIndex: row,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
157
|
+
for (const evt of fillEvents)
|
|
158
|
+
onCellValueChanged(evt);
|
|
153
159
|
endBatch?.();
|
|
154
160
|
}
|
|
155
161
|
setFillDrag(null);
|
|
@@ -163,19 +169,18 @@ export function useFillHandle(params) {
|
|
|
163
169
|
if (rafRef.current)
|
|
164
170
|
cancelAnimationFrame(rafRef.current);
|
|
165
171
|
};
|
|
166
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
172
|
}, [
|
|
168
173
|
fillDrag,
|
|
169
174
|
editable,
|
|
170
|
-
colOffset,
|
|
171
175
|
items,
|
|
172
176
|
visibleCols,
|
|
173
177
|
setSelectionRange,
|
|
174
178
|
setActiveCell,
|
|
175
179
|
onCellValueChanged,
|
|
176
|
-
// wrapperRef excluded — refs are stable across renders
|
|
177
180
|
beginBatch,
|
|
178
181
|
endBatch,
|
|
182
|
+
colOffsetRef,
|
|
183
|
+
wrapperRef,
|
|
179
184
|
]);
|
|
180
185
|
// Ref mirror — keeps handleFillHandleMouseDown stable across selection changes
|
|
181
186
|
const selectionRangeRef = useRef(selectionRange);
|