@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,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Manages column pinning state (left/right sticky positioning).
|
|
4
4
|
* Supports controlled and uncontrolled modes.
|
|
@@ -6,8 +6,8 @@ import { useState, useCallback, useMemo } from 'react';
|
|
|
6
6
|
*/
|
|
7
7
|
export function useColumnPinning(params) {
|
|
8
8
|
const { columns, pinnedColumns: controlledPinnedColumns, onColumnPinned } = params;
|
|
9
|
-
// Initialize internal state from column.pinned definitions
|
|
10
|
-
const
|
|
9
|
+
// Initialize internal state from column.pinned definitions (mount only)
|
|
10
|
+
const [internalPinnedColumns, setInternalPinnedColumns] = useState(() => {
|
|
11
11
|
const initial = {};
|
|
12
12
|
for (const col of columns) {
|
|
13
13
|
if (col.pinned) {
|
|
@@ -15,22 +15,21 @@ export function useColumnPinning(params) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
return initial;
|
|
18
|
-
|
|
19
|
-
}, []); // Only on mount
|
|
20
|
-
const [internalPinnedColumns, setInternalPinnedColumns] = useState(initialPinnedColumns);
|
|
18
|
+
});
|
|
21
19
|
// Use controlled state if provided, otherwise internal
|
|
22
20
|
const pinnedColumns = controlledPinnedColumns ?? internalPinnedColumns;
|
|
23
21
|
const pinColumn = useCallback((columnId, side) => {
|
|
24
22
|
const next = { ...pinnedColumns, [columnId]: side };
|
|
25
|
-
|
|
23
|
+
if (!controlledPinnedColumns)
|
|
24
|
+
setInternalPinnedColumns(next);
|
|
26
25
|
onColumnPinned?.(columnId, side);
|
|
27
|
-
}, [pinnedColumns, onColumnPinned]);
|
|
26
|
+
}, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
|
|
28
27
|
const unpinColumn = useCallback((columnId) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const { [columnId]: _, ...next } = pinnedColumns;
|
|
29
|
+
if (!controlledPinnedColumns)
|
|
30
|
+
setInternalPinnedColumns(next);
|
|
32
31
|
onColumnPinned?.(columnId, null);
|
|
33
|
-
}, [pinnedColumns, onColumnPinned]);
|
|
32
|
+
}, [pinnedColumns, controlledPinnedColumns, onColumnPinned]);
|
|
34
33
|
const isPinned = useCallback((columnId) => {
|
|
35
34
|
return pinnedColumns[columnId];
|
|
36
35
|
}, [pinnedColumns]);
|
|
@@ -98,7 +98,14 @@ export function useColumnReorder(params) {
|
|
|
98
98
|
if (!wrapper)
|
|
99
99
|
return;
|
|
100
100
|
const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
|
|
101
|
-
const result = calculateDropTarget(
|
|
101
|
+
const result = calculateDropTarget({
|
|
102
|
+
mouseX: moveEvent.clientX,
|
|
103
|
+
columnOrder: currentOrder,
|
|
104
|
+
draggedColumnId: columnId,
|
|
105
|
+
draggedPinState,
|
|
106
|
+
tableElement: wrapper,
|
|
107
|
+
pinnedColumns: pinnedShape,
|
|
108
|
+
});
|
|
102
109
|
if (result) {
|
|
103
110
|
latestDropTargetIndex = result.targetIndex;
|
|
104
111
|
setDropIndicatorX(result.indicatorX);
|
|
@@ -23,6 +23,11 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
23
23
|
const handleResizeStart = useCallback((e, col) => {
|
|
24
24
|
e.preventDefault();
|
|
25
25
|
e.stopPropagation();
|
|
26
|
+
// Clean up any in-progress drag before starting a new one
|
|
27
|
+
if (cleanupRef.current) {
|
|
28
|
+
cleanupRef.current();
|
|
29
|
+
cleanupRef.current = null;
|
|
30
|
+
}
|
|
26
31
|
const startX = e.clientX;
|
|
27
32
|
const columnId = col.columnId;
|
|
28
33
|
// Measure the actual rendered width from the DOM. With table-layout: auto,
|
|
@@ -111,8 +116,7 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
111
116
|
document.addEventListener('mousemove', onMove);
|
|
112
117
|
document.addEventListener('mouseup', onUp);
|
|
113
118
|
cleanupRef.current = cleanup;
|
|
114
|
-
|
|
115
|
-
}, [defaultWidth, minWidth, setColumnSizingOverrides]); // columnSizingOverrides read via ref
|
|
119
|
+
}, [defaultWidth, minWidth, setColumnSizingOverrides, columnSizingOverridesRef]);
|
|
116
120
|
const getColumnWidth = useCallback((col) => {
|
|
117
121
|
return columnSizingOverrides[col.columnId]?.widthPx
|
|
118
122
|
?? col.idealWidth
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useContextMenu } from './useContextMenu';
|
|
3
|
+
// Stable no-op handlers used when cellSelection is disabled
|
|
4
|
+
const NOOP = () => { };
|
|
5
|
+
const NOOP_CTX = (_e) => { };
|
|
6
|
+
/**
|
|
7
|
+
* Manages context menu position and handlers.
|
|
8
|
+
* Extracted from useDataGridState for modularity.
|
|
9
|
+
*/
|
|
10
|
+
export function useDataGridContextMenu(params) {
|
|
11
|
+
const { cellSelection } = params;
|
|
12
|
+
const { contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu } = useContextMenu();
|
|
13
|
+
const contextMenuState = useMemo(() => ({
|
|
14
|
+
menuPosition: cellSelection ? contextMenuPosition : null,
|
|
15
|
+
setMenuPosition: cellSelection ? setContextMenuPosition : NOOP,
|
|
16
|
+
handleCellContextMenu: cellSelection ? handleCellContextMenu : NOOP_CTX,
|
|
17
|
+
closeContextMenu: cellSelection ? closeContextMenu : NOOP,
|
|
18
|
+
}), [cellSelection, contextMenuPosition, setContextMenuPosition, handleCellContextMenu, closeContextMenu]);
|
|
19
|
+
return {
|
|
20
|
+
contextMenu: contextMenuState,
|
|
21
|
+
contextMenuPosition,
|
|
22
|
+
setContextMenuPosition,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMemo, useCallback, useState } from 'react';
|
|
2
|
+
import { parseValue } from '../utils';
|
|
3
|
+
import { useLatestRef } from './useLatestRef';
|
|
4
|
+
/**
|
|
5
|
+
* Manages cell editing commit/cancel logic and popover editor state.
|
|
6
|
+
* Extracted from useDataGridState for modularity.
|
|
7
|
+
*
|
|
8
|
+
* The editingCell/setEditingCell/pendingEditorValue/setPendingEditorValue are
|
|
9
|
+
* passed in from useCellEditing() (called at the orchestrator level) to avoid
|
|
10
|
+
* circular dependencies with useDataGridInteraction.
|
|
11
|
+
*/
|
|
12
|
+
export function useDataGridEditing(params) {
|
|
13
|
+
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, onCellValueChanged, setActiveCell, } = params;
|
|
14
|
+
const [popoverAnchorEl, setPopoverAnchorEl] = useState(null);
|
|
15
|
+
const visibleColsRef = useLatestRef(params.visibleCols);
|
|
16
|
+
const itemsLengthRef = useLatestRef(params.itemsLength);
|
|
17
|
+
const commitCellEdit = useCallback((item, columnId, oldValue, newValue, rowIndex, globalColIndex) => {
|
|
18
|
+
// Validate via valueParser before committing
|
|
19
|
+
const col = visibleColsRef.current.find((c) => c.columnId === columnId);
|
|
20
|
+
if (col) {
|
|
21
|
+
const result = parseValue(newValue, oldValue, item, col);
|
|
22
|
+
if (!result.valid) {
|
|
23
|
+
// Reject -- cancel the edit
|
|
24
|
+
setEditingCell(null);
|
|
25
|
+
setPopoverAnchorEl(null);
|
|
26
|
+
setPendingEditorValue(undefined);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
newValue = result.value;
|
|
30
|
+
}
|
|
31
|
+
onCellValueChanged?.({
|
|
32
|
+
item,
|
|
33
|
+
columnId,
|
|
34
|
+
oldValue,
|
|
35
|
+
newValue,
|
|
36
|
+
rowIndex,
|
|
37
|
+
});
|
|
38
|
+
setEditingCell(null);
|
|
39
|
+
setPopoverAnchorEl(null);
|
|
40
|
+
setPendingEditorValue(undefined);
|
|
41
|
+
// Advance to next row for inline editors
|
|
42
|
+
if (rowIndex < itemsLengthRef.current - 1) {
|
|
43
|
+
setActiveCell({ rowIndex: rowIndex + 1, columnIndex: globalColIndex });
|
|
44
|
+
}
|
|
45
|
+
}, [onCellValueChanged, setEditingCell, setPendingEditorValue, setActiveCell, visibleColsRef, itemsLengthRef]);
|
|
46
|
+
const cancelPopoverEdit = useCallback(() => {
|
|
47
|
+
setEditingCell(null);
|
|
48
|
+
setPopoverAnchorEl(null);
|
|
49
|
+
setPendingEditorValue(undefined);
|
|
50
|
+
}, [setEditingCell, setPendingEditorValue]);
|
|
51
|
+
const editingState = useMemo(() => ({
|
|
52
|
+
editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue,
|
|
53
|
+
commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl,
|
|
54
|
+
}), [editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl]);
|
|
55
|
+
return { editing: editingState };
|
|
56
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo, useCallback } from 'react';
|
|
2
|
+
import { useCellSelection } from './useCellSelection';
|
|
3
|
+
import { useClipboard } from './useClipboard';
|
|
4
|
+
import { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
5
|
+
import { useFillHandle } from './useFillHandle';
|
|
6
|
+
import { useUndoRedo } from './useUndoRedo';
|
|
7
|
+
// Stable no-op handlers used when cellSelection is disabled (module-scope = no re-renders)
|
|
8
|
+
const NOOP = () => { };
|
|
9
|
+
const NOOP_ASYNC = async () => { };
|
|
10
|
+
const NOOP_MOUSE = (_e, _r, _c) => { };
|
|
11
|
+
const NOOP_KEY = (_e) => { };
|
|
12
|
+
/**
|
|
13
|
+
* Manages cell selection, keyboard navigation, clipboard, fill handle, and undo/redo.
|
|
14
|
+
* Extracted from useDataGridState for modularity.
|
|
15
|
+
*
|
|
16
|
+
* activeCell/setActiveCell and editingCell/setEditingCell are passed in from the
|
|
17
|
+
* orchestrator level to avoid circular dependencies with useDataGridEditing.
|
|
18
|
+
*/
|
|
19
|
+
export function useDataGridInteraction(params) {
|
|
20
|
+
const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId, editable, onCellValueChangedProp, cellSelection, rowSelection, selectedRowIds, editingCell, setEditingCell, activeCell, setActiveCell, handleRowCheckboxChange, setContextMenuPosition, wrapperRef, } = params;
|
|
21
|
+
// Wrap onCellValueChanged with undo/redo tracking
|
|
22
|
+
const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
|
|
23
|
+
const onCellValueChanged = undoRedo.onCellValueChanged;
|
|
24
|
+
const { selectionRange, setSelectionRange, handleCellMouseDown: handleCellMouseDownBase, handleSelectAllCells, isDragging, } = useCellSelection({
|
|
25
|
+
colOffset,
|
|
26
|
+
rowCount: items.length,
|
|
27
|
+
visibleColCount: visibleCols.length,
|
|
28
|
+
setActiveCell,
|
|
29
|
+
wrapperRef,
|
|
30
|
+
});
|
|
31
|
+
const { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges } = useClipboard({
|
|
32
|
+
items,
|
|
33
|
+
visibleCols,
|
|
34
|
+
colOffset,
|
|
35
|
+
selectionRange,
|
|
36
|
+
activeCell,
|
|
37
|
+
editable,
|
|
38
|
+
onCellValueChanged,
|
|
39
|
+
beginBatch: undoRedo.beginBatch,
|
|
40
|
+
endBatch: undoRedo.endBatch,
|
|
41
|
+
});
|
|
42
|
+
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
43
|
+
if (e.button !== 0)
|
|
44
|
+
return;
|
|
45
|
+
wrapperRef.current?.focus({ preventScroll: true });
|
|
46
|
+
clearClipboardRanges();
|
|
47
|
+
handleCellMouseDownBase(e, rowIndex, globalColIndex);
|
|
48
|
+
}, [handleCellMouseDownBase, clearClipboardRanges, wrapperRef]);
|
|
49
|
+
const { handleGridKeyDown } = useKeyboardNavigation({
|
|
50
|
+
data: { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, getRowId },
|
|
51
|
+
state: { activeCell, selectionRange, editingCell, selectedRowIds },
|
|
52
|
+
handlers: { setActiveCell, setSelectionRange, setEditingCell, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu: setContextMenuPosition, onUndo: undoRedo.undo, onRedo: undoRedo.redo, clearClipboardRanges },
|
|
53
|
+
features: { editable, onCellValueChanged, rowSelection: rowSelection ?? 'none', wrapperRef },
|
|
54
|
+
});
|
|
55
|
+
const { handleFillHandleMouseDown } = useFillHandle({
|
|
56
|
+
items,
|
|
57
|
+
visibleCols,
|
|
58
|
+
editable,
|
|
59
|
+
onCellValueChanged,
|
|
60
|
+
selectionRange,
|
|
61
|
+
setSelectionRange,
|
|
62
|
+
setActiveCell,
|
|
63
|
+
colOffset,
|
|
64
|
+
wrapperRef,
|
|
65
|
+
beginBatch: undoRedo.beginBatch,
|
|
66
|
+
endBatch: undoRedo.endBatch,
|
|
67
|
+
});
|
|
68
|
+
const hasCellSelection = selectionRange != null || activeCell != null;
|
|
69
|
+
const interactionState = useMemo(() => ({
|
|
70
|
+
activeCell: cellSelection ? activeCell : null,
|
|
71
|
+
setActiveCell: cellSelection ? setActiveCell : NOOP,
|
|
72
|
+
selectionRange: cellSelection ? selectionRange : null,
|
|
73
|
+
setSelectionRange: cellSelection ? setSelectionRange : NOOP,
|
|
74
|
+
handleCellMouseDown: cellSelection ? handleCellMouseDown : NOOP_MOUSE,
|
|
75
|
+
handleSelectAllCells: cellSelection ? handleSelectAllCells : NOOP,
|
|
76
|
+
hasCellSelection: cellSelection ? hasCellSelection : false,
|
|
77
|
+
handleGridKeyDown: cellSelection ? handleGridKeyDown : NOOP_KEY,
|
|
78
|
+
handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
|
|
79
|
+
handleCopy: cellSelection ? handleCopy : NOOP,
|
|
80
|
+
handleCut: cellSelection ? handleCut : NOOP,
|
|
81
|
+
handlePaste: cellSelection ? handlePaste : NOOP_ASYNC,
|
|
82
|
+
cutRange: cellSelection ? cutRange : null,
|
|
83
|
+
copyRange: cellSelection ? copyRange : null,
|
|
84
|
+
clearClipboardRanges: cellSelection ? clearClipboardRanges : NOOP,
|
|
85
|
+
canUndo: undoRedo.canUndo,
|
|
86
|
+
canRedo: undoRedo.canRedo,
|
|
87
|
+
onUndo: undoRedo.undo,
|
|
88
|
+
onRedo: undoRedo.redo,
|
|
89
|
+
isDragging: cellSelection ? isDragging : false,
|
|
90
|
+
}), [
|
|
91
|
+
cellSelection, activeCell, setActiveCell, selectionRange, setSelectionRange,
|
|
92
|
+
handleCellMouseDown, handleSelectAllCells, hasCellSelection, handleGridKeyDown,
|
|
93
|
+
handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange,
|
|
94
|
+
clearClipboardRanges, undoRedo.canUndo, undoRedo.canRedo, undoRedo.undo, undoRedo.redo,
|
|
95
|
+
isDragging,
|
|
96
|
+
]);
|
|
97
|
+
return {
|
|
98
|
+
interaction: interactionState,
|
|
99
|
+
selectionRange,
|
|
100
|
+
setSelectionRange,
|
|
101
|
+
cutRange,
|
|
102
|
+
copyRange,
|
|
103
|
+
clearClipboardRanges,
|
|
104
|
+
isDragging,
|
|
105
|
+
onCellValueChanged,
|
|
106
|
+
canUndo: undoRedo.canUndo,
|
|
107
|
+
canRedo: undoRedo.canRedo,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useMemo, useState, useLayoutEffect, useCallback } from 'react';
|
|
2
|
+
import { flattenColumns } from '../utils';
|
|
3
|
+
import { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
|
|
4
|
+
import { useTableLayout } from './useTableLayout';
|
|
5
|
+
import { useColumnPinning } from './useColumnPinning';
|
|
6
|
+
import { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
|
|
7
|
+
import { useLatestRef } from './useLatestRef';
|
|
8
|
+
/**
|
|
9
|
+
* Manages column layout, visibility, sizing, pinning, and header menu state.
|
|
10
|
+
* Extracted from useDataGridState for modularity.
|
|
11
|
+
*/
|
|
12
|
+
export function useDataGridLayout(params) {
|
|
13
|
+
const { columns, items, getRowId, visibleColumns, columnOrder, rowSelection = 'none', showRowNumbers, initialColumnWidths, onColumnResized, onAutosizeColumn, pinnedColumns, onColumnPinned, sortBy, sortDirection, onColumnSort, wrapperRef, } = params;
|
|
14
|
+
// Cast is safe: input columns are React.IColumnDef instances; flattenColumns only extracts leaves.
|
|
15
|
+
const flatColumnsRaw = useMemo(() => flattenColumns(columns), [columns]);
|
|
16
|
+
// Apply runtime pin overrides (from applyColumnState or programmatic changes)
|
|
17
|
+
const flatColumns = useMemo(() => {
|
|
18
|
+
if (!pinnedColumns || Object.keys(pinnedColumns).length === 0)
|
|
19
|
+
return flatColumnsRaw;
|
|
20
|
+
return flatColumnsRaw.map((col) => {
|
|
21
|
+
const override = pinnedColumns[col.columnId];
|
|
22
|
+
if (override && col.pinned !== override) {
|
|
23
|
+
return { ...col, pinned: override };
|
|
24
|
+
}
|
|
25
|
+
return col;
|
|
26
|
+
});
|
|
27
|
+
}, [flatColumnsRaw, pinnedColumns]);
|
|
28
|
+
const visibleCols = useMemo(() => {
|
|
29
|
+
const filtered = visibleColumns
|
|
30
|
+
? flatColumns.filter((c) => visibleColumns.has(c.columnId))
|
|
31
|
+
: flatColumns;
|
|
32
|
+
if (!columnOrder?.length)
|
|
33
|
+
return filtered;
|
|
34
|
+
const orderMap = new Map();
|
|
35
|
+
for (let i = 0; i < columnOrder.length; i++) {
|
|
36
|
+
orderMap.set(columnOrder[i], i);
|
|
37
|
+
}
|
|
38
|
+
return [...filtered].sort((a, b) => {
|
|
39
|
+
const ia = orderMap.get(a.columnId) ?? -1;
|
|
40
|
+
const ib = orderMap.get(b.columnId) ?? -1;
|
|
41
|
+
if (ia === -1 && ib === -1)
|
|
42
|
+
return 0;
|
|
43
|
+
if (ia === -1)
|
|
44
|
+
return 1;
|
|
45
|
+
if (ib === -1)
|
|
46
|
+
return -1;
|
|
47
|
+
return ia - ib;
|
|
48
|
+
});
|
|
49
|
+
}, [flatColumns, visibleColumns, columnOrder]);
|
|
50
|
+
const visibleColumnCount = visibleCols.length;
|
|
51
|
+
const hasCheckboxCol = rowSelection === 'multiple';
|
|
52
|
+
const hasRowNumbersCol = !!showRowNumbers;
|
|
53
|
+
const specialColsCount = (hasCheckboxCol ? 1 : 0) + (hasRowNumbersCol ? 1 : 0);
|
|
54
|
+
const totalColCount = visibleColumnCount + specialColsCount;
|
|
55
|
+
const colOffset = specialColsCount;
|
|
56
|
+
const rowIndexByRowId = useMemo(() => {
|
|
57
|
+
const m = new Map();
|
|
58
|
+
items.forEach((item, idx) => m.set(getRowId(item), idx));
|
|
59
|
+
return m;
|
|
60
|
+
}, [items, getRowId]);
|
|
61
|
+
const { containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, } = useTableLayout({
|
|
62
|
+
wrapperRef,
|
|
63
|
+
visibleCols,
|
|
64
|
+
flatColumns,
|
|
65
|
+
hasCheckboxCol,
|
|
66
|
+
initialColumnWidths,
|
|
67
|
+
onColumnResized,
|
|
68
|
+
});
|
|
69
|
+
const pinningResult = useColumnPinning({
|
|
70
|
+
columns: flatColumns,
|
|
71
|
+
pinnedColumns,
|
|
72
|
+
onColumnPinned,
|
|
73
|
+
});
|
|
74
|
+
// Measure actual column widths from the DOM for accurate pinning offsets.
|
|
75
|
+
const [measuredColumnWidths, setMeasuredColumnWidths] = useState({});
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
const wrapper = wrapperRef.current;
|
|
78
|
+
if (!wrapper)
|
|
79
|
+
return;
|
|
80
|
+
const headerCells = wrapper.querySelectorAll('th[data-column-id]');
|
|
81
|
+
if (headerCells.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const measured = {};
|
|
84
|
+
headerCells.forEach((cell) => {
|
|
85
|
+
const colId = cell.getAttribute('data-column-id');
|
|
86
|
+
if (colId)
|
|
87
|
+
measured[colId] = cell.offsetWidth;
|
|
88
|
+
});
|
|
89
|
+
setMeasuredColumnWidths((prev) => {
|
|
90
|
+
for (const key in measured) {
|
|
91
|
+
if (prev[key] !== measured[key])
|
|
92
|
+
return measured;
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(prev).length !== Object.keys(measured).length)
|
|
95
|
+
return measured;
|
|
96
|
+
return prev;
|
|
97
|
+
});
|
|
98
|
+
}, [visibleCols, containerWidth, columnSizingOverrides, wrapperRef]);
|
|
99
|
+
// Build column width map for pinning offset computation
|
|
100
|
+
const columnWidthMap = useMemo(() => {
|
|
101
|
+
const map = {};
|
|
102
|
+
for (const col of visibleCols) {
|
|
103
|
+
const override = columnSizingOverrides[col.columnId];
|
|
104
|
+
map[col.columnId] = override
|
|
105
|
+
? override.widthPx
|
|
106
|
+
: (measuredColumnWidths[col.columnId] ?? col.idealWidth ?? col.defaultWidth ?? col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH);
|
|
107
|
+
}
|
|
108
|
+
return map;
|
|
109
|
+
}, [visibleCols, columnSizingOverrides, measuredColumnWidths]);
|
|
110
|
+
const leftOffsets = useMemo(() => pinningResult.computeLeftOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap, hasCheckboxCol]);
|
|
111
|
+
const rightOffsets = useMemo(() => pinningResult.computeRightOffsets(visibleCols, columnWidthMap, DEFAULT_MIN_COLUMN_WIDTH), [pinningResult, visibleCols, columnWidthMap]);
|
|
112
|
+
// Stabilize onColumnSort via ref
|
|
113
|
+
const onColumnSortRef = useLatestRef(onColumnSort);
|
|
114
|
+
const stableOnColumnSort = useCallback((columnKey, direction) => onColumnSortRef.current?.(columnKey, direction), [onColumnSortRef]);
|
|
115
|
+
// Autosize callback
|
|
116
|
+
const handleAutosizeColumn = useCallback((columnId, width) => {
|
|
117
|
+
setColumnSizingOverrides((prev) => ({ ...prev, [columnId]: { widthPx: width } }));
|
|
118
|
+
(onAutosizeColumn ?? onColumnResized)?.(columnId, width);
|
|
119
|
+
}, [setColumnSizingOverrides, onAutosizeColumn, onColumnResized]);
|
|
120
|
+
const headerMenuResult = useColumnHeaderMenuState({
|
|
121
|
+
pinnedColumns: pinningResult.pinnedColumns,
|
|
122
|
+
onPinColumn: pinningResult.pinColumn,
|
|
123
|
+
onUnpinColumn: pinningResult.unpinColumn,
|
|
124
|
+
sortBy,
|
|
125
|
+
sortDirection: sortDirection ?? 'asc',
|
|
126
|
+
onColumnSort: stableOnColumnSort,
|
|
127
|
+
onColumnResized,
|
|
128
|
+
onAutosizeColumn: handleAutosizeColumn,
|
|
129
|
+
columns: flatColumns,
|
|
130
|
+
});
|
|
131
|
+
// Memoize layout sub-object
|
|
132
|
+
const layoutState = useMemo(() => ({
|
|
133
|
+
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
134
|
+
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
135
|
+
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
136
|
+
measuredColumnWidths,
|
|
137
|
+
}), [
|
|
138
|
+
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
139
|
+
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
140
|
+
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
141
|
+
measuredColumnWidths,
|
|
142
|
+
]);
|
|
143
|
+
// Memoize pinning sub-object
|
|
144
|
+
const pinningState = useMemo(() => ({
|
|
145
|
+
pinnedColumns: pinningResult.pinnedColumns,
|
|
146
|
+
pinColumn: pinningResult.pinColumn,
|
|
147
|
+
unpinColumn: pinningResult.unpinColumn,
|
|
148
|
+
isPinned: pinningResult.isPinned,
|
|
149
|
+
leftOffsets,
|
|
150
|
+
rightOffsets,
|
|
151
|
+
headerMenu: headerMenuResult,
|
|
152
|
+
}), [
|
|
153
|
+
pinningResult.pinnedColumns, pinningResult.pinColumn, pinningResult.unpinColumn,
|
|
154
|
+
pinningResult.isPinned, leftOffsets, rightOffsets,
|
|
155
|
+
headerMenuResult,
|
|
156
|
+
]);
|
|
157
|
+
return {
|
|
158
|
+
layout: layoutState,
|
|
159
|
+
pinning: pinningState,
|
|
160
|
+
flatColumns,
|
|
161
|
+
visibleCols,
|
|
162
|
+
visibleColumnCount,
|
|
163
|
+
totalColCount,
|
|
164
|
+
colOffset,
|
|
165
|
+
hasCheckboxCol,
|
|
166
|
+
hasRowNumbersCol,
|
|
167
|
+
columnSizingOverrides,
|
|
168
|
+
setColumnSizingOverrides,
|
|
169
|
+
handleAutosizeColumn,
|
|
170
|
+
stableOnColumnSort,
|
|
171
|
+
};
|
|
172
|
+
}
|