@alaarab/ogrid-react 2.1.3 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +7233 -26
- package/package.json +7 -4
- package/dist/esm/components/BaseColumnHeaderMenu.js +0 -78
- package/dist/esm/components/BaseDropIndicator.js +0 -4
- package/dist/esm/components/BaseEmptyState.js +0 -4
- package/dist/esm/components/BaseInlineCellEditor.js +0 -167
- package/dist/esm/components/BaseLoadingOverlay.js +0 -4
- package/dist/esm/components/CellErrorBoundary.js +0 -43
- package/dist/esm/components/ColumnChooserProps.js +0 -6
- package/dist/esm/components/ColumnHeaderFilterContent.js +0 -33
- package/dist/esm/components/ColumnHeaderFilterRenderers.js +0 -67
- package/dist/esm/components/EmptyState.js +0 -19
- package/dist/esm/components/GridContextMenu.js +0 -35
- package/dist/esm/components/MarchingAntsOverlay.js +0 -90
- package/dist/esm/components/OGridLayout.js +0 -136
- package/dist/esm/components/PaginationControlsProps.js +0 -6
- package/dist/esm/components/SideBar.js +0 -123
- package/dist/esm/components/StatusBar.js +0 -6
- package/dist/esm/components/createOGrid.js +0 -19
- package/dist/esm/constants/domHelpers.js +0 -16
- package/dist/esm/hooks/index.js +0 -43
- package/dist/esm/hooks/useActiveCell.js +0 -75
- package/dist/esm/hooks/useCellEditing.js +0 -15
- package/dist/esm/hooks/useCellSelection.js +0 -389
- package/dist/esm/hooks/useClipboard.js +0 -106
- package/dist/esm/hooks/useColumnChooserState.js +0 -74
- package/dist/esm/hooks/useColumnHeaderFilterState.js +0 -191
- package/dist/esm/hooks/useColumnHeaderMenuState.js +0 -106
- package/dist/esm/hooks/useColumnMeta.js +0 -61
- package/dist/esm/hooks/useColumnPinning.js +0 -67
- package/dist/esm/hooks/useColumnReorder.js +0 -143
- package/dist/esm/hooks/useColumnResize.js +0 -127
- package/dist/esm/hooks/useContextMenu.js +0 -21
- package/dist/esm/hooks/useDataGridContextMenu.js +0 -24
- package/dist/esm/hooks/useDataGridEditing.js +0 -56
- package/dist/esm/hooks/useDataGridInteraction.js +0 -109
- package/dist/esm/hooks/useDataGridLayout.js +0 -172
- package/dist/esm/hooks/useDataGridState.js +0 -169
- package/dist/esm/hooks/useDataGridTableOrchestration.js +0 -199
- package/dist/esm/hooks/useDateFilterState.js +0 -34
- package/dist/esm/hooks/useDebounce.js +0 -35
- package/dist/esm/hooks/useFillHandle.js +0 -200
- package/dist/esm/hooks/useFilterOptions.js +0 -55
- package/dist/esm/hooks/useInlineCellEditorState.js +0 -38
- package/dist/esm/hooks/useKeyboardNavigation.js +0 -261
- package/dist/esm/hooks/useLatestRef.js +0 -11
- package/dist/esm/hooks/useListVirtualizer.js +0 -29
- package/dist/esm/hooks/useMultiSelectFilterState.js +0 -59
- package/dist/esm/hooks/useOGrid.js +0 -371
- package/dist/esm/hooks/useOGridDataFetching.js +0 -74
- package/dist/esm/hooks/useOGridFilters.js +0 -59
- package/dist/esm/hooks/useOGridPagination.js +0 -24
- package/dist/esm/hooks/useOGridSorting.js +0 -24
- package/dist/esm/hooks/usePaginationControls.js +0 -16
- package/dist/esm/hooks/usePeopleFilterState.js +0 -73
- package/dist/esm/hooks/useRichSelectState.js +0 -60
- package/dist/esm/hooks/useRowSelection.js +0 -69
- package/dist/esm/hooks/useSelectState.js +0 -62
- package/dist/esm/hooks/useShallowEqualMemo.js +0 -14
- package/dist/esm/hooks/useSideBarState.js +0 -39
- package/dist/esm/hooks/useTableLayout.js +0 -69
- package/dist/esm/hooks/useTextFilterState.js +0 -25
- package/dist/esm/hooks/useUndoRedo.js +0 -84
- package/dist/esm/hooks/useVirtualScroll.js +0 -69
- package/dist/esm/storybook/index.js +0 -1
- package/dist/esm/storybook/mockData.js +0 -73
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/dataGridTypes.js +0 -1
- package/dist/esm/types/index.js +0 -1
- package/dist/esm/utils/dataGridViewModel.js +0 -54
- package/dist/esm/utils/gridRowComparator.js +0 -2
- package/dist/esm/utils/index.js +0 -5
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useMemo } from 'react';
|
|
2
|
-
import { useDataGridState } from './useDataGridState';
|
|
3
|
-
import { useColumnResize } from './useColumnResize';
|
|
4
|
-
import { useColumnReorder } from './useColumnReorder';
|
|
5
|
-
import { useVirtualScroll } from './useVirtualScroll';
|
|
6
|
-
import { useLatestRef } from './useLatestRef';
|
|
7
|
-
import { buildHeaderRows } from '../utils';
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Hook
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
/**
|
|
12
|
-
* Shared orchestration hook for DataGridTable.
|
|
13
|
-
*
|
|
14
|
-
* Encapsulates all state management and computation that is identical across
|
|
15
|
-
* Radix, Fluent, and Material DataGridTable implementations. Each UI package
|
|
16
|
-
* calls this hook, then renders its own framework-specific JSX using the
|
|
17
|
-
* returned values.
|
|
18
|
-
*/
|
|
19
|
-
export function useDataGridTableOrchestration(params) {
|
|
20
|
-
const { props } = params;
|
|
21
|
-
// ── Refs ────────────────────────────────────────────────────────────────
|
|
22
|
-
const wrapperRef = useRef(null);
|
|
23
|
-
const tableContainerRef = useRef(null);
|
|
24
|
-
const lastMouseShiftRef = useRef(false);
|
|
25
|
-
// ── Core state ──────────────────────────────────────────────────────────
|
|
26
|
-
const state = useDataGridState({ props, wrapperRef });
|
|
27
|
-
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels, pinning } = state;
|
|
28
|
-
const { visibleCols: visibleColsTyped, totalColCount, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, measuredColumnWidths, } = layout;
|
|
29
|
-
const visibleCols = visibleColsTyped;
|
|
30
|
-
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
31
|
-
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
32
|
-
const { setActiveCell, handleCellMouseDown, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
33
|
-
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
34
|
-
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
35
|
-
const { headerMenu } = pinning;
|
|
36
|
-
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
37
|
-
// ── Props destructuring ─────────────────────────────────────────────────
|
|
38
|
-
const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', suppressHorizontalScroll, stickyHeader = true, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, columnOrder, onColumnOrderChange, columnReorder, virtualScroll, rowHeight, density = 'normal', pinnedColumns, currentPage = 1, pageSize: propPageSize = 25, } = props;
|
|
39
|
-
// ── Derived values ──────────────────────────────────────────────────────
|
|
40
|
-
const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * propPageSize : 0;
|
|
41
|
-
const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
|
|
42
|
-
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
43
|
-
const fitToContent = layoutMode === 'content';
|
|
44
|
-
// ── Column resize ──────────────────────────────────────────────────────
|
|
45
|
-
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
46
|
-
columnSizingOverrides,
|
|
47
|
-
setColumnSizingOverrides,
|
|
48
|
-
});
|
|
49
|
-
// ── Column reorder ─────────────────────────────────────────────────────
|
|
50
|
-
const { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown } = useColumnReorder({
|
|
51
|
-
columns: visibleCols,
|
|
52
|
-
columnOrder,
|
|
53
|
-
onColumnOrderChange,
|
|
54
|
-
enabled: columnReorder === true,
|
|
55
|
-
pinnedColumns,
|
|
56
|
-
wrapperRef,
|
|
57
|
-
});
|
|
58
|
-
// ── Virtual scroll ─────────────────────────────────────────────────────
|
|
59
|
-
const virtualScrollEnabled = virtualScroll?.enabled === true;
|
|
60
|
-
const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
|
|
61
|
-
const { visibleRange } = useVirtualScroll({
|
|
62
|
-
totalRows: items.length,
|
|
63
|
-
rowHeight: virtualRowHeight,
|
|
64
|
-
enabled: virtualScrollEnabled,
|
|
65
|
-
overscan: virtualScroll?.overscan,
|
|
66
|
-
containerRef: wrapperRef,
|
|
67
|
-
});
|
|
68
|
-
// ── Memoized callback groups ───────────────────────────────────────────
|
|
69
|
-
const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
|
|
70
|
-
const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
|
|
71
|
-
// ── Stable refs for volatile state ─────────────────────────────────────
|
|
72
|
-
const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
|
|
73
|
-
const pendingEditorValueRef = useLatestRef(pendingEditorValue);
|
|
74
|
-
const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
|
|
75
|
-
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
76
|
-
// ── Stable row-click handler ───────────────────────────────────────────
|
|
77
|
-
const handleSingleRowClick = useCallback((e) => {
|
|
78
|
-
if (rowSelection !== 'single')
|
|
79
|
-
return;
|
|
80
|
-
const rowId = e.currentTarget.dataset.rowId;
|
|
81
|
-
if (!rowId)
|
|
82
|
-
return;
|
|
83
|
-
const ids = selectedRowIdsRef.current;
|
|
84
|
-
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
85
|
-
}, [rowSelection, updateSelection, selectedRowIdsRef]);
|
|
86
|
-
// ── Return ─────────────────────────────────────────────────────────────
|
|
87
|
-
return {
|
|
88
|
-
// Refs
|
|
89
|
-
wrapperRef,
|
|
90
|
-
tableContainerRef,
|
|
91
|
-
lastMouseShiftRef,
|
|
92
|
-
// State sub-objects
|
|
93
|
-
layout,
|
|
94
|
-
rowSel,
|
|
95
|
-
editing,
|
|
96
|
-
interaction,
|
|
97
|
-
ctxMenu,
|
|
98
|
-
viewModels,
|
|
99
|
-
pinning,
|
|
100
|
-
// Column resize
|
|
101
|
-
handleResizeStart,
|
|
102
|
-
getColumnWidth,
|
|
103
|
-
// Column reorder
|
|
104
|
-
isReorderDragging,
|
|
105
|
-
dropIndicatorX,
|
|
106
|
-
handleHeaderMouseDown,
|
|
107
|
-
// Virtual scroll
|
|
108
|
-
virtualScrollEnabled,
|
|
109
|
-
virtualRowHeight,
|
|
110
|
-
visibleRange,
|
|
111
|
-
// Derived from props
|
|
112
|
-
items,
|
|
113
|
-
columns,
|
|
114
|
-
getRowId,
|
|
115
|
-
emptyState,
|
|
116
|
-
layoutMode,
|
|
117
|
-
rowSelection,
|
|
118
|
-
suppressHorizontalScroll,
|
|
119
|
-
stickyHeader,
|
|
120
|
-
isLoading,
|
|
121
|
-
loadingMessage,
|
|
122
|
-
ariaLabel,
|
|
123
|
-
ariaLabelledBy,
|
|
124
|
-
visibleColumns,
|
|
125
|
-
columnOrder,
|
|
126
|
-
columnReorder,
|
|
127
|
-
density,
|
|
128
|
-
rowHeight,
|
|
129
|
-
pinnedColumns,
|
|
130
|
-
currentPage,
|
|
131
|
-
propPageSize,
|
|
132
|
-
// Computed values
|
|
133
|
-
rowNumberOffset,
|
|
134
|
-
headerRows,
|
|
135
|
-
allowOverflowX,
|
|
136
|
-
fitToContent,
|
|
137
|
-
// Memoized callback groups
|
|
138
|
-
editCallbacks,
|
|
139
|
-
interactionHandlers,
|
|
140
|
-
// Stable refs for volatile state
|
|
141
|
-
cellDescriptorInputRef,
|
|
142
|
-
pendingEditorValueRef,
|
|
143
|
-
popoverAnchorElRef,
|
|
144
|
-
selectedRowIdsRef,
|
|
145
|
-
// Convenience handlers
|
|
146
|
-
handleSingleRowClick,
|
|
147
|
-
handlePasteVoid,
|
|
148
|
-
// Layout-derived references
|
|
149
|
-
visibleCols,
|
|
150
|
-
totalColCount,
|
|
151
|
-
hasCheckboxCol,
|
|
152
|
-
hasRowNumbersCol,
|
|
153
|
-
colOffset,
|
|
154
|
-
containerWidth,
|
|
155
|
-
minTableWidth,
|
|
156
|
-
desiredTableWidth,
|
|
157
|
-
columnSizingOverrides,
|
|
158
|
-
setColumnSizingOverrides,
|
|
159
|
-
measuredColumnWidths,
|
|
160
|
-
// Row selection shortcuts
|
|
161
|
-
selectedRowIds,
|
|
162
|
-
updateSelection,
|
|
163
|
-
handleRowCheckboxChange,
|
|
164
|
-
handleSelectAll,
|
|
165
|
-
allSelected,
|
|
166
|
-
someSelected,
|
|
167
|
-
// Editing shortcuts
|
|
168
|
-
editingCell,
|
|
169
|
-
setPopoverAnchorEl,
|
|
170
|
-
cancelPopoverEdit,
|
|
171
|
-
// Interaction shortcuts
|
|
172
|
-
setActiveCell,
|
|
173
|
-
selectionRange,
|
|
174
|
-
hasCellSelection,
|
|
175
|
-
handleGridKeyDown,
|
|
176
|
-
handleFillHandleMouseDown,
|
|
177
|
-
handleCopy,
|
|
178
|
-
handleCut,
|
|
179
|
-
cutRange,
|
|
180
|
-
copyRange,
|
|
181
|
-
canUndo,
|
|
182
|
-
canRedo,
|
|
183
|
-
onUndo,
|
|
184
|
-
onRedo,
|
|
185
|
-
isDragging,
|
|
186
|
-
// Context menu shortcuts
|
|
187
|
-
menuPosition,
|
|
188
|
-
handleCellContextMenu,
|
|
189
|
-
closeContextMenu,
|
|
190
|
-
// ViewModel shortcuts
|
|
191
|
-
headerFilterInput,
|
|
192
|
-
cellDescriptorInput,
|
|
193
|
-
statusBarConfig,
|
|
194
|
-
showEmptyInGrid,
|
|
195
|
-
onCellError,
|
|
196
|
-
// Pinning shortcuts
|
|
197
|
-
headerMenu,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Date filter state sub-hook for column header filters.
|
|
3
|
-
* Manages temporary date from/to values and apply/clear handlers.
|
|
4
|
-
*/
|
|
5
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
6
|
-
export function useDateFilterState(params) {
|
|
7
|
-
const { dateValue, onDateChange, isFilterOpen } = params;
|
|
8
|
-
const [tempDateFrom, setTempDateFrom] = useState(dateValue?.from ?? '');
|
|
9
|
-
const [tempDateTo, setTempDateTo] = useState(dateValue?.to ?? '');
|
|
10
|
-
// Sync temp state when popover opens
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (isFilterOpen) {
|
|
13
|
-
setTempDateFrom(dateValue?.from ?? '');
|
|
14
|
-
setTempDateTo(dateValue?.to ?? '');
|
|
15
|
-
}
|
|
16
|
-
}, [isFilterOpen, dateValue]);
|
|
17
|
-
const handleDateApply = useCallback(() => {
|
|
18
|
-
const from = tempDateFrom || undefined;
|
|
19
|
-
const to = tempDateTo || undefined;
|
|
20
|
-
onDateChange?.(from || to ? { from, to } : undefined);
|
|
21
|
-
}, [onDateChange, tempDateFrom, tempDateTo]);
|
|
22
|
-
const handleDateClear = useCallback(() => {
|
|
23
|
-
setTempDateFrom('');
|
|
24
|
-
setTempDateTo('');
|
|
25
|
-
}, []);
|
|
26
|
-
return {
|
|
27
|
-
tempDateFrom,
|
|
28
|
-
setTempDateFrom,
|
|
29
|
-
tempDateTo,
|
|
30
|
-
setTempDateTo,
|
|
31
|
-
handleDateApply,
|
|
32
|
-
handleDateClear,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a debounced value that updates after the specified delay when the source value changes.
|
|
4
|
-
*/
|
|
5
|
-
export function useDebounce(value, delayMs) {
|
|
6
|
-
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
const id = setTimeout(() => {
|
|
9
|
-
setDebouncedValue(value);
|
|
10
|
-
}, delayMs);
|
|
11
|
-
return () => clearTimeout(id);
|
|
12
|
-
}, [value, delayMs]);
|
|
13
|
-
return debouncedValue;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Returns a stable callback that invokes the given function after the specified delay.
|
|
17
|
-
* Each new call resets the timer.
|
|
18
|
-
*/
|
|
19
|
-
export function useDebouncedCallback(fn, delayMs) {
|
|
20
|
-
const timeoutRef = useRef(undefined);
|
|
21
|
-
const fnRef = useRef(fn);
|
|
22
|
-
fnRef.current = fn;
|
|
23
|
-
const debounced = useCallback(((...args) => {
|
|
24
|
-
if (timeoutRef.current)
|
|
25
|
-
clearTimeout(timeoutRef.current);
|
|
26
|
-
timeoutRef.current = setTimeout(() => {
|
|
27
|
-
fnRef.current(...args);
|
|
28
|
-
}, delayMs);
|
|
29
|
-
}), [delayMs]);
|
|
30
|
-
useEffect(() => () => {
|
|
31
|
-
if (timeoutRef.current)
|
|
32
|
-
clearTimeout(timeoutRef.current);
|
|
33
|
-
}, []);
|
|
34
|
-
return debounced;
|
|
35
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
-
import { normalizeSelectionRange } from '../types';
|
|
3
|
-
import { applyFillValues } from '../utils';
|
|
4
|
-
import { useLatestRef } from './useLatestRef';
|
|
5
|
-
/** DOM attribute name for fill-drag range highlighting (same as cell selection drag). */
|
|
6
|
-
const DRAG_ATTR = 'data-drag-range';
|
|
7
|
-
/**
|
|
8
|
-
* Manages Excel-style fill handle drag-to-fill for cell ranges.
|
|
9
|
-
* @param params - Items, columns, selection range, editability, and value change callback.
|
|
10
|
-
* @returns Fill drag state, setter, and mousedown handler for the fill handle.
|
|
11
|
-
*/
|
|
12
|
-
export function useFillHandle(params) {
|
|
13
|
-
const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, colOffset, wrapperRef, beginBatch, endBatch, } = params;
|
|
14
|
-
const [fillDrag, setFillDrag] = useState(null);
|
|
15
|
-
const fillDragEndRef = useRef({ endRow: 0, endCol: 0 });
|
|
16
|
-
const rafRef = useRef(0);
|
|
17
|
-
const liveFillRangeRef = useRef(null);
|
|
18
|
-
const colOffsetRef = useLatestRef(colOffset);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!fillDrag || editable === false || !onCellValueChanged || !wrapperRef.current)
|
|
21
|
-
return;
|
|
22
|
-
fillDragEndRef.current = { endRow: fillDrag.startRow, endCol: fillDrag.startCol };
|
|
23
|
-
liveFillRangeRef.current = null;
|
|
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();
|
|
43
|
-
const applyDragAttrs = (range) => {
|
|
44
|
-
const wrapper = wrapperRef.current;
|
|
45
|
-
if (!wrapper)
|
|
46
|
-
return;
|
|
47
|
-
const minR = Math.min(range.startRow, range.endRow);
|
|
48
|
-
const maxR = Math.max(range.startRow, range.endRow);
|
|
49
|
-
const minC = Math.min(range.startCol, range.endCol);
|
|
50
|
-
const maxC = Math.max(range.startCol, range.endCol);
|
|
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);
|
|
59
|
-
}
|
|
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
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const clearDragAttrs = () => {
|
|
80
|
-
for (const el of markedCells) {
|
|
81
|
-
el.removeAttribute(DRAG_ATTR);
|
|
82
|
-
}
|
|
83
|
-
markedCells.clear();
|
|
84
|
-
cellIndex = null;
|
|
85
|
-
};
|
|
86
|
-
let lastFillMousePos = null;
|
|
87
|
-
const resolveRange = (cx, cy) => {
|
|
88
|
-
const target = document.elementFromPoint(cx, cy);
|
|
89
|
-
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
90
|
-
if (!cell || !wrapperRef.current?.contains(cell))
|
|
91
|
-
return null;
|
|
92
|
-
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
93
|
-
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
94
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c < colOffsetRef.current)
|
|
95
|
-
return null;
|
|
96
|
-
const dataCol = c - colOffsetRef.current;
|
|
97
|
-
return normalizeSelectionRange({
|
|
98
|
-
startRow: fillDrag.startRow,
|
|
99
|
-
startCol: fillDrag.startCol,
|
|
100
|
-
endRow: r,
|
|
101
|
-
endCol: dataCol,
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
const onMove = (e) => {
|
|
105
|
-
lastFillMousePos = { cx: e.clientX, cy: e.clientY };
|
|
106
|
-
if (rafRef.current)
|
|
107
|
-
cancelAnimationFrame(rafRef.current);
|
|
108
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
109
|
-
rafRef.current = 0;
|
|
110
|
-
if (!lastFillMousePos)
|
|
111
|
-
return;
|
|
112
|
-
const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
113
|
-
if (!newRange)
|
|
114
|
-
return;
|
|
115
|
-
// Skip if unchanged
|
|
116
|
-
const prev = liveFillRangeRef.current;
|
|
117
|
-
if (prev &&
|
|
118
|
-
prev.startRow === newRange.startRow &&
|
|
119
|
-
prev.startCol === newRange.startCol &&
|
|
120
|
-
prev.endRow === newRange.endRow &&
|
|
121
|
-
prev.endCol === newRange.endCol) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
liveFillRangeRef.current = newRange;
|
|
125
|
-
fillDragEndRef.current = { endRow: newRange.endRow, endCol: newRange.endCol };
|
|
126
|
-
applyDragAttrs(newRange);
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
const onUp = () => {
|
|
130
|
-
if (rafRef.current) {
|
|
131
|
-
cancelAnimationFrame(rafRef.current);
|
|
132
|
-
rafRef.current = 0;
|
|
133
|
-
}
|
|
134
|
-
// Flush: resolve final position if RAF hasn't executed yet
|
|
135
|
-
if (lastFillMousePos) {
|
|
136
|
-
const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
137
|
-
if (flushed) {
|
|
138
|
-
liveFillRangeRef.current = flushed;
|
|
139
|
-
fillDragEndRef.current = { endRow: flushed.endRow, endCol: flushed.endCol };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
clearDragAttrs();
|
|
143
|
-
const end = fillDragEndRef.current;
|
|
144
|
-
const norm = normalizeSelectionRange({
|
|
145
|
-
startRow: fillDrag.startRow,
|
|
146
|
-
startCol: fillDrag.startCol,
|
|
147
|
-
endRow: end.endRow,
|
|
148
|
-
endCol: end.endCol,
|
|
149
|
-
});
|
|
150
|
-
// Commit range to React state
|
|
151
|
-
setSelectionRange(norm);
|
|
152
|
-
setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOffsetRef.current });
|
|
153
|
-
// Apply fill values
|
|
154
|
-
const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols);
|
|
155
|
-
if (fillEvents.length > 0) {
|
|
156
|
-
beginBatch?.();
|
|
157
|
-
for (const evt of fillEvents)
|
|
158
|
-
onCellValueChanged(evt);
|
|
159
|
-
endBatch?.();
|
|
160
|
-
}
|
|
161
|
-
setFillDrag(null);
|
|
162
|
-
liveFillRangeRef.current = null;
|
|
163
|
-
};
|
|
164
|
-
window.addEventListener('mousemove', onMove, true);
|
|
165
|
-
window.addEventListener('mouseup', onUp, true);
|
|
166
|
-
return () => {
|
|
167
|
-
window.removeEventListener('mousemove', onMove, true);
|
|
168
|
-
window.removeEventListener('mouseup', onUp, true);
|
|
169
|
-
if (rafRef.current)
|
|
170
|
-
cancelAnimationFrame(rafRef.current);
|
|
171
|
-
};
|
|
172
|
-
}, [
|
|
173
|
-
fillDrag,
|
|
174
|
-
editable,
|
|
175
|
-
items,
|
|
176
|
-
visibleCols,
|
|
177
|
-
setSelectionRange,
|
|
178
|
-
setActiveCell,
|
|
179
|
-
onCellValueChanged,
|
|
180
|
-
beginBatch,
|
|
181
|
-
endBatch,
|
|
182
|
-
colOffsetRef,
|
|
183
|
-
wrapperRef,
|
|
184
|
-
]);
|
|
185
|
-
// Ref mirror — keeps handleFillHandleMouseDown stable across selection changes
|
|
186
|
-
const selectionRangeRef = useRef(selectionRange);
|
|
187
|
-
selectionRangeRef.current = selectionRange;
|
|
188
|
-
const handleFillHandleMouseDown = useCallback((e) => {
|
|
189
|
-
e.preventDefault();
|
|
190
|
-
e.stopPropagation();
|
|
191
|
-
const range = selectionRangeRef.current;
|
|
192
|
-
if (!range)
|
|
193
|
-
return;
|
|
194
|
-
setFillDrag({
|
|
195
|
-
startRow: range.startRow,
|
|
196
|
-
startCol: range.startCol,
|
|
197
|
-
});
|
|
198
|
-
}, []);
|
|
199
|
-
return { fillDrag, setFillDrag, handleFillHandleMouseDown };
|
|
200
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
/** Shallow-compare two string arrays by value. */
|
|
3
|
-
function fieldsEqual(a, b) {
|
|
4
|
-
if (a.length !== b.length)
|
|
5
|
-
return false;
|
|
6
|
-
for (let i = 0; i < a.length; i++) {
|
|
7
|
-
if (a[i] !== b[i])
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Load filter options for the given fields from a data source.
|
|
14
|
-
*
|
|
15
|
-
* Accepts `IDataSource<T>` or a plain `{ fetchFilterOptions }` object.
|
|
16
|
-
*/
|
|
17
|
-
export function useFilterOptions(dataSource, fields) {
|
|
18
|
-
// Stabilize the fields array so inline literals (e.g. ['a','b']) don't
|
|
19
|
-
// cause infinite re-render loops via useCallback/useEffect deps.
|
|
20
|
-
const fieldsRef = useRef(fields);
|
|
21
|
-
if (!fieldsEqual(fieldsRef.current, fields)) {
|
|
22
|
-
fieldsRef.current = fields;
|
|
23
|
-
}
|
|
24
|
-
const stableFields = fieldsRef.current;
|
|
25
|
-
const [filterOptions, setFilterOptions] = useState({});
|
|
26
|
-
const [loadingOptions, setLoadingOptions] = useState({});
|
|
27
|
-
const load = useCallback(async () => {
|
|
28
|
-
const fetcher = 'fetchFilterOptions' in dataSource && typeof dataSource.fetchFilterOptions === 'function'
|
|
29
|
-
? dataSource.fetchFilterOptions.bind(dataSource)
|
|
30
|
-
: undefined;
|
|
31
|
-
if (!fetcher) {
|
|
32
|
-
setFilterOptions({});
|
|
33
|
-
setLoadingOptions({});
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const loading = {};
|
|
37
|
-
stableFields.forEach((f) => { loading[f] = true; });
|
|
38
|
-
setLoadingOptions(loading);
|
|
39
|
-
const results = {};
|
|
40
|
-
await Promise.all(stableFields.map(async (field) => {
|
|
41
|
-
try {
|
|
42
|
-
results[field] = await fetcher(field);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
results[field] = [];
|
|
46
|
-
}
|
|
47
|
-
}));
|
|
48
|
-
setFilterOptions(results);
|
|
49
|
-
setLoadingOptions({});
|
|
50
|
-
}, [dataSource, stableFields]);
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
load().catch(() => { });
|
|
53
|
-
}, [load]);
|
|
54
|
-
return { filterOptions, loadingOptions };
|
|
55
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Headless inline cell editor state for Fluent, Material, and Radix InlineCellEditor.
|
|
3
|
-
* UI packages use this hook and render only the framework input (Input, TextField, select, Checkbox).
|
|
4
|
-
*/
|
|
5
|
-
import { useState, useCallback } from 'react';
|
|
6
|
-
/**
|
|
7
|
-
* Returns localValue/setLocalValue (for text), handleKeyDown (Escape cancel, Enter commit for text),
|
|
8
|
-
* handleBlur (commit on blur for text), commit(value), cancel(). UI renders only the input.
|
|
9
|
-
*/
|
|
10
|
-
export function useInlineCellEditorState(params) {
|
|
11
|
-
const { value, editorType, onCommit, onCancel } = params;
|
|
12
|
-
const [localValue, setLocalValue] = useState(value !== null && value !== undefined ? String(value) : '');
|
|
13
|
-
const handleKeyDown = useCallback((e) => {
|
|
14
|
-
if (e.key === 'Escape') {
|
|
15
|
-
e.preventDefault();
|
|
16
|
-
e.stopPropagation(); // Don't let the grid handler clear selection on Escape
|
|
17
|
-
onCancel();
|
|
18
|
-
}
|
|
19
|
-
if (e.key === 'Enter' && editorType === 'text') {
|
|
20
|
-
e.preventDefault();
|
|
21
|
-
e.stopPropagation(); // Don't let the grid handler re-open an editor
|
|
22
|
-
onCommit(localValue);
|
|
23
|
-
}
|
|
24
|
-
}, [onCancel, onCommit, localValue, editorType]);
|
|
25
|
-
const handleBlur = useCallback(() => {
|
|
26
|
-
if (editorType === 'text') {
|
|
27
|
-
onCommit(localValue);
|
|
28
|
-
}
|
|
29
|
-
}, [editorType, localValue, onCommit]);
|
|
30
|
-
return {
|
|
31
|
-
localValue,
|
|
32
|
-
setLocalValue,
|
|
33
|
-
handleKeyDown,
|
|
34
|
-
handleBlur,
|
|
35
|
-
commit: onCommit,
|
|
36
|
-
cancel: onCancel,
|
|
37
|
-
};
|
|
38
|
-
}
|