@alaarab/ogrid-react 2.0.2 → 2.0.4
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/BaseInlineCellEditor.js +5 -1
- package/dist/esm/hooks/index.js +4 -0
- package/dist/esm/hooks/useCellSelection.js +44 -4
- package/dist/esm/hooks/useColumnHeaderMenuState.js +56 -0
- package/dist/esm/hooks/useColumnPinning.js +67 -0
- package/dist/esm/hooks/useColumnReorder.js +136 -0
- package/dist/esm/hooks/useDataGridState.js +41 -5
- package/dist/esm/hooks/useOGrid.js +18 -3
- package/dist/esm/hooks/useVirtualScroll.js +69 -0
- package/dist/esm/index.js +2 -2
- package/dist/types/hooks/index.d.ts +8 -0
- package/dist/types/hooks/useColumnHeaderMenuState.d.ts +23 -0
- package/dist/types/hooks/useColumnPinning.d.ts +32 -0
- package/dist/types/hooks/useColumnReorder.d.ts +22 -0
- package/dist/types/hooks/useDataGridState.d.ts +28 -0
- package/dist/types/hooks/useVirtualScroll.d.ts +33 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/types/dataGridTypes.d.ts +22 -2
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +3 -2
|
@@ -87,7 +87,11 @@ export function BaseInlineCellEditor(props) {
|
|
|
87
87
|
});
|
|
88
88
|
React.useEffect(() => {
|
|
89
89
|
const input = wrapperRef.current?.querySelector('input');
|
|
90
|
-
input
|
|
90
|
+
if (input) {
|
|
91
|
+
input.focus();
|
|
92
|
+
// Select all text for easy replacement (like Excel)
|
|
93
|
+
input.select();
|
|
94
|
+
}
|
|
91
95
|
}, []);
|
|
92
96
|
// Rich select (shared across all frameworks)
|
|
93
97
|
if (editorType === 'richSelect') {
|
package/dist/esm/hooks/index.js
CHANGED
|
@@ -19,7 +19,11 @@ export { useDateFilterState } from './useDateFilterState';
|
|
|
19
19
|
export { useColumnChooserState } from './useColumnChooserState';
|
|
20
20
|
export { useInlineCellEditorState } from './useInlineCellEditorState';
|
|
21
21
|
export { useColumnResize } from './useColumnResize';
|
|
22
|
+
export { useColumnPinning } from './useColumnPinning';
|
|
23
|
+
export { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
|
|
22
24
|
export { useRichSelectState } from './useRichSelectState';
|
|
23
25
|
export { useSideBarState } from './useSideBarState';
|
|
24
26
|
export { useTableLayout } from './useTableLayout';
|
|
27
|
+
export { useColumnReorder } from './useColumnReorder';
|
|
28
|
+
export { useVirtualScroll } from './useVirtualScroll';
|
|
25
29
|
export { useLatestRef } from './useLatestRef';
|
|
@@ -9,8 +9,9 @@ function rangesEqual(a, b) {
|
|
|
9
9
|
return a.startRow === b.startRow && a.endRow === b.endRow &&
|
|
10
10
|
a.startCol === b.startCol && a.endCol === b.endCol;
|
|
11
11
|
}
|
|
12
|
-
/** DOM attribute
|
|
12
|
+
/** DOM attribute names used for drag-range highlighting (bypasses React). */
|
|
13
13
|
const DRAG_ATTR = 'data-drag-range';
|
|
14
|
+
const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
|
|
14
15
|
/** Auto-scroll config */
|
|
15
16
|
const AUTO_SCROLL_EDGE = 40; // px from wrapper edge to trigger
|
|
16
17
|
const AUTO_SCROLL_MIN_SPEED = 2;
|
|
@@ -84,6 +85,9 @@ export function useCellSelection(params) {
|
|
|
84
85
|
// setIsDragging(true) is deferred to the first mousemove to avoid
|
|
85
86
|
// a true→false toggle on simple clicks (which causes 2 extra renders).
|
|
86
87
|
isDraggingRef.current = true;
|
|
88
|
+
// Apply drag attrs immediately for the initial cell so the anchor styling shows
|
|
89
|
+
// even before the first mousemove. This ensures instant visual feedback.
|
|
90
|
+
setTimeout(() => applyDragAttrsRef.current?.(initial), 0);
|
|
87
91
|
}
|
|
88
92
|
},
|
|
89
93
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- setSelectionRange is a stable callback
|
|
@@ -102,12 +106,16 @@ export function useCellSelection(params) {
|
|
|
102
106
|
}, [rowCount, visibleColCount, colOffset, setActiveCell]);
|
|
103
107
|
/** Last known mouse position during drag — used by mouseUp to flush pending RAF work. */
|
|
104
108
|
const lastMousePosRef = useRef(null);
|
|
109
|
+
// Ref to expose applyDragAttrs outside useEffect so it can be called from mouseDown
|
|
110
|
+
const applyDragAttrsRef = useRef(null);
|
|
105
111
|
// Window mouse move/up for drag selection.
|
|
106
112
|
// Performance: during drag, we update a ref + toggle DOM attributes via rAF.
|
|
107
113
|
// React state is only committed on mouseup (single re-render instead of 60-120/s).
|
|
108
114
|
useEffect(() => {
|
|
109
115
|
const colOff = colOffset; // capture for closure
|
|
110
|
-
/** Toggle DRAG_ATTR on
|
|
116
|
+
/** Toggle DRAG_ATTR on cells to show the range highlight via CSS.
|
|
117
|
+
* Also sets edge box-shadows for a green border around the selection range,
|
|
118
|
+
* and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
|
|
111
119
|
const applyDragAttrs = (range) => {
|
|
112
120
|
const wrapper = wrapperRef.current;
|
|
113
121
|
if (!wrapper)
|
|
@@ -116,6 +124,7 @@ export function useCellSelection(params) {
|
|
|
116
124
|
const maxR = Math.max(range.startRow, range.endRow);
|
|
117
125
|
const minC = Math.min(range.startCol, range.endCol);
|
|
118
126
|
const maxC = Math.max(range.startCol, range.endCol);
|
|
127
|
+
const anchor = dragStartRef.current;
|
|
119
128
|
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
120
129
|
for (let i = 0; i < cells.length; i++) {
|
|
121
130
|
const el = cells[i];
|
|
@@ -125,20 +134,51 @@ export function useCellSelection(params) {
|
|
|
125
134
|
if (inRange) {
|
|
126
135
|
if (!el.hasAttribute(DRAG_ATTR))
|
|
127
136
|
el.setAttribute(DRAG_ATTR, '');
|
|
137
|
+
// Anchor cell gets white background instead of green
|
|
138
|
+
const isAnchor = anchor && r === anchor.row && c === anchor.col;
|
|
139
|
+
if (isAnchor) {
|
|
140
|
+
if (!el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
141
|
+
el.setAttribute(DRAG_ANCHOR_ATTR, '');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
if (el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
145
|
+
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
146
|
+
}
|
|
147
|
+
// Edge borders via inset box-shadow (no layout shift)
|
|
148
|
+
const shadows = [];
|
|
149
|
+
if (r === minR)
|
|
150
|
+
shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
|
|
151
|
+
if (r === maxR)
|
|
152
|
+
shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
|
|
153
|
+
if (c === minC)
|
|
154
|
+
shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
155
|
+
if (c === maxC)
|
|
156
|
+
shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
|
|
157
|
+
el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
|
|
128
158
|
}
|
|
129
159
|
else {
|
|
130
160
|
if (el.hasAttribute(DRAG_ATTR))
|
|
131
161
|
el.removeAttribute(DRAG_ATTR);
|
|
162
|
+
if (el.hasAttribute(DRAG_ANCHOR_ATTR))
|
|
163
|
+
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
164
|
+
if (el.style.boxShadow)
|
|
165
|
+
el.style.boxShadow = '';
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
168
|
};
|
|
169
|
+
// Expose applyDragAttrs via ref so mouseDown can access it
|
|
170
|
+
applyDragAttrsRef.current = applyDragAttrs;
|
|
135
171
|
const clearDragAttrs = () => {
|
|
136
172
|
const wrapper = wrapperRef.current;
|
|
137
173
|
if (!wrapper)
|
|
138
174
|
return;
|
|
139
175
|
const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
|
|
140
|
-
for (let i = 0; i < marked.length; i++)
|
|
141
|
-
marked[i]
|
|
176
|
+
for (let i = 0; i < marked.length; i++) {
|
|
177
|
+
const el = marked[i];
|
|
178
|
+
el.removeAttribute(DRAG_ATTR);
|
|
179
|
+
el.removeAttribute(DRAG_ANCHOR_ATTR);
|
|
180
|
+
el.style.boxShadow = '';
|
|
181
|
+
}
|
|
142
182
|
};
|
|
143
183
|
/** Resolve mouse coordinates to a cell range (shared by RAF callback and mouseUp flush). */
|
|
144
184
|
const resolveRange = (cx, cy) => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Manages state for the column header menu (pin left/right/unpin actions).
|
|
4
|
+
* Tracks which column's menu is open, anchor element, and action handlers.
|
|
5
|
+
*/
|
|
6
|
+
export function useColumnHeaderMenuState(params) {
|
|
7
|
+
const { pinnedColumns, onPinColumn, onUnpinColumn } = params;
|
|
8
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
+
const [openForColumn, setOpenForColumn] = useState(null);
|
|
10
|
+
const [anchorElement, setAnchorElement] = useState(null);
|
|
11
|
+
const open = useCallback((columnId, anchorEl) => {
|
|
12
|
+
setOpenForColumn(columnId);
|
|
13
|
+
setAnchorElement(anchorEl);
|
|
14
|
+
setIsOpen(true);
|
|
15
|
+
}, []);
|
|
16
|
+
const close = useCallback(() => {
|
|
17
|
+
setIsOpen(false);
|
|
18
|
+
setOpenForColumn(null);
|
|
19
|
+
setAnchorElement(null);
|
|
20
|
+
}, []);
|
|
21
|
+
const currentPinState = openForColumn ? pinnedColumns[openForColumn] : undefined;
|
|
22
|
+
const canPinLeft = currentPinState !== 'left';
|
|
23
|
+
const canPinRight = currentPinState !== 'right';
|
|
24
|
+
const canUnpin = !!currentPinState;
|
|
25
|
+
const handlePinLeft = useCallback(() => {
|
|
26
|
+
if (openForColumn && canPinLeft) {
|
|
27
|
+
onPinColumn(openForColumn, 'left');
|
|
28
|
+
close();
|
|
29
|
+
}
|
|
30
|
+
}, [openForColumn, canPinLeft, onPinColumn, close]);
|
|
31
|
+
const handlePinRight = useCallback(() => {
|
|
32
|
+
if (openForColumn && canPinRight) {
|
|
33
|
+
onPinColumn(openForColumn, 'right');
|
|
34
|
+
close();
|
|
35
|
+
}
|
|
36
|
+
}, [openForColumn, canPinRight, onPinColumn, close]);
|
|
37
|
+
const handleUnpin = useCallback(() => {
|
|
38
|
+
if (openForColumn && canUnpin) {
|
|
39
|
+
onUnpinColumn(openForColumn);
|
|
40
|
+
close();
|
|
41
|
+
}
|
|
42
|
+
}, [openForColumn, canUnpin, onUnpinColumn, close]);
|
|
43
|
+
return {
|
|
44
|
+
isOpen,
|
|
45
|
+
openForColumn,
|
|
46
|
+
anchorElement,
|
|
47
|
+
open,
|
|
48
|
+
close,
|
|
49
|
+
handlePinLeft,
|
|
50
|
+
handlePinRight,
|
|
51
|
+
handleUnpin,
|
|
52
|
+
canPinLeft,
|
|
53
|
+
canPinRight,
|
|
54
|
+
canUnpin,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Manages column pinning state (left/right sticky positioning).
|
|
4
|
+
* Supports controlled and uncontrolled modes.
|
|
5
|
+
* Initializes from column.pinned definitions and pinnedColumns prop.
|
|
6
|
+
*/
|
|
7
|
+
export function useColumnPinning(params) {
|
|
8
|
+
const { columns, pinnedColumns: controlledPinnedColumns, onColumnPinned } = params;
|
|
9
|
+
// Initialize internal state from column.pinned definitions
|
|
10
|
+
const initialPinnedColumns = useMemo(() => {
|
|
11
|
+
const initial = {};
|
|
12
|
+
for (const col of columns) {
|
|
13
|
+
if (col.pinned) {
|
|
14
|
+
initial[col.columnId] = col.pinned;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return initial;
|
|
18
|
+
}, []); // Only on mount
|
|
19
|
+
const [internalPinnedColumns, setInternalPinnedColumns] = useState(initialPinnedColumns);
|
|
20
|
+
// Use controlled state if provided, otherwise internal
|
|
21
|
+
const pinnedColumns = controlledPinnedColumns ?? internalPinnedColumns;
|
|
22
|
+
const pinColumn = useCallback((columnId, side) => {
|
|
23
|
+
const next = { ...pinnedColumns, [columnId]: side };
|
|
24
|
+
setInternalPinnedColumns(next);
|
|
25
|
+
onColumnPinned?.(columnId, side);
|
|
26
|
+
}, [pinnedColumns, onColumnPinned]);
|
|
27
|
+
const unpinColumn = useCallback((columnId) => {
|
|
28
|
+
const next = { ...pinnedColumns };
|
|
29
|
+
delete next[columnId];
|
|
30
|
+
setInternalPinnedColumns(next);
|
|
31
|
+
onColumnPinned?.(columnId, null);
|
|
32
|
+
}, [pinnedColumns, onColumnPinned]);
|
|
33
|
+
const isPinned = useCallback((columnId) => {
|
|
34
|
+
return pinnedColumns[columnId];
|
|
35
|
+
}, [pinnedColumns]);
|
|
36
|
+
const computeLeftOffsets = useCallback((visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) => {
|
|
37
|
+
const offsets = {};
|
|
38
|
+
let left = hasCheckboxColumn ? checkboxColumnWidth : 0;
|
|
39
|
+
for (const col of visibleCols) {
|
|
40
|
+
if (pinnedColumns[col.columnId] === 'left') {
|
|
41
|
+
offsets[col.columnId] = left;
|
|
42
|
+
left += columnWidths[col.columnId] ?? defaultWidth;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return offsets;
|
|
46
|
+
}, [pinnedColumns]);
|
|
47
|
+
const computeRightOffsets = useCallback((visibleCols, columnWidths, defaultWidth) => {
|
|
48
|
+
const offsets = {};
|
|
49
|
+
let right = 0;
|
|
50
|
+
for (let i = visibleCols.length - 1; i >= 0; i--) {
|
|
51
|
+
const col = visibleCols[i];
|
|
52
|
+
if (pinnedColumns[col.columnId] === 'right') {
|
|
53
|
+
offsets[col.columnId] = right;
|
|
54
|
+
right += columnWidths[col.columnId] ?? defaultWidth;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return offsets;
|
|
58
|
+
}, [pinnedColumns]);
|
|
59
|
+
return {
|
|
60
|
+
pinnedColumns,
|
|
61
|
+
pinColumn,
|
|
62
|
+
unpinColumn,
|
|
63
|
+
isPinned,
|
|
64
|
+
computeLeftOffsets,
|
|
65
|
+
computeRightOffsets,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { calculateDropTarget, reorderColumnArray, getPinStateForColumn, } from '@alaarab/ogrid-core';
|
|
3
|
+
/** Width of the resize handle zone on the right edge of each header cell. */
|
|
4
|
+
const RESIZE_HANDLE_ZONE = 8;
|
|
5
|
+
/**
|
|
6
|
+
* Convert Record<string, 'left' | 'right'> to the { left?, right? } shape core expects.
|
|
7
|
+
*/
|
|
8
|
+
function toPinnedColumnsShape(pinned) {
|
|
9
|
+
if (!pinned)
|
|
10
|
+
return undefined;
|
|
11
|
+
const left = [];
|
|
12
|
+
const right = [];
|
|
13
|
+
for (const [id, side] of Object.entries(pinned)) {
|
|
14
|
+
if (side === 'left')
|
|
15
|
+
left.push(id);
|
|
16
|
+
else if (side === 'right')
|
|
17
|
+
right.push(id);
|
|
18
|
+
}
|
|
19
|
+
if (left.length === 0 && right.length === 0)
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
...(left.length > 0 ? { left } : {}),
|
|
23
|
+
...(right.length > 0 ? { right } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Manages column reorder drag interactions with RAF-throttled updates.
|
|
28
|
+
* @param params - Columns, order, change callback, enabled flag, and wrapper ref.
|
|
29
|
+
* @returns Drag state and mousedown handler for header cells.
|
|
30
|
+
*/
|
|
31
|
+
export function useColumnReorder(params) {
|
|
32
|
+
const { columns, columnOrder, onColumnOrderChange, enabled = true, pinnedColumns, wrapperRef, } = params;
|
|
33
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
34
|
+
const [dropIndicatorX, setDropIndicatorX] = useState(null);
|
|
35
|
+
const rafRef = useRef(0);
|
|
36
|
+
// Refs for latest values so the window listeners capture current state
|
|
37
|
+
const columnsRef = useRef(columns);
|
|
38
|
+
columnsRef.current = columns;
|
|
39
|
+
const columnOrderRef = useRef(columnOrder);
|
|
40
|
+
columnOrderRef.current = columnOrder;
|
|
41
|
+
const onColumnOrderChangeRef = useRef(onColumnOrderChange);
|
|
42
|
+
onColumnOrderChangeRef.current = onColumnOrderChange;
|
|
43
|
+
const pinnedColumnsRef = useRef(pinnedColumns);
|
|
44
|
+
pinnedColumnsRef.current = pinnedColumns;
|
|
45
|
+
// Track active drag state for cleanup on unmount
|
|
46
|
+
const cleanupRef = useRef(null);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
return () => {
|
|
49
|
+
if (cleanupRef.current) {
|
|
50
|
+
cleanupRef.current();
|
|
51
|
+
cleanupRef.current = null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
const handleHeaderMouseDown = useCallback((columnId, event) => {
|
|
56
|
+
if (!enabled)
|
|
57
|
+
return;
|
|
58
|
+
if (!onColumnOrderChangeRef.current)
|
|
59
|
+
return;
|
|
60
|
+
// Gate on left-click only
|
|
61
|
+
if (event.button !== 0)
|
|
62
|
+
return;
|
|
63
|
+
// Skip if in resize handle zone (right 8px of the header cell)
|
|
64
|
+
const target = event.currentTarget;
|
|
65
|
+
const rect = target.getBoundingClientRect();
|
|
66
|
+
if (event.clientX > rect.right - RESIZE_HANDLE_ZONE)
|
|
67
|
+
return;
|
|
68
|
+
// Skip column groups — only reorder leaf columns
|
|
69
|
+
const cols = columnsRef.current;
|
|
70
|
+
const colIndex = cols.findIndex((c) => c.columnId === columnId);
|
|
71
|
+
if (colIndex === -1)
|
|
72
|
+
return;
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
const startX = event.clientX;
|
|
75
|
+
let hasMoved = false;
|
|
76
|
+
let latestDropTargetIndex = null;
|
|
77
|
+
// Determine pin state of the dragged column
|
|
78
|
+
const pinnedShape = toPinnedColumnsShape(pinnedColumnsRef.current);
|
|
79
|
+
const draggedPinState = getPinStateForColumn(columnId, pinnedShape);
|
|
80
|
+
// Lock text selection and set grabbing cursor during drag
|
|
81
|
+
const prevUserSelect = document.body.style.userSelect;
|
|
82
|
+
const prevCursor = document.body.style.cursor;
|
|
83
|
+
document.body.style.userSelect = 'none';
|
|
84
|
+
document.body.style.cursor = 'grabbing';
|
|
85
|
+
const onMove = (moveEvent) => {
|
|
86
|
+
// Require a small minimum drag distance before activating
|
|
87
|
+
if (!hasMoved && Math.abs(moveEvent.clientX - startX) < 5)
|
|
88
|
+
return;
|
|
89
|
+
if (!hasMoved) {
|
|
90
|
+
hasMoved = true;
|
|
91
|
+
setIsDragging(true);
|
|
92
|
+
}
|
|
93
|
+
if (rafRef.current)
|
|
94
|
+
cancelAnimationFrame(rafRef.current);
|
|
95
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
96
|
+
rafRef.current = 0;
|
|
97
|
+
const wrapper = wrapperRef.current;
|
|
98
|
+
if (!wrapper)
|
|
99
|
+
return;
|
|
100
|
+
const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
|
|
101
|
+
const result = calculateDropTarget(moveEvent.clientX, currentOrder, columnId, draggedPinState, wrapper, pinnedShape);
|
|
102
|
+
if (result) {
|
|
103
|
+
latestDropTargetIndex = result.targetIndex;
|
|
104
|
+
setDropIndicatorX(result.indicatorX);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const cleanup = () => {
|
|
109
|
+
window.removeEventListener('mousemove', onMove, true);
|
|
110
|
+
window.removeEventListener('mouseup', onUp, true);
|
|
111
|
+
cleanupRef.current = null;
|
|
112
|
+
// Restore user-select and cursor
|
|
113
|
+
document.body.style.userSelect = prevUserSelect;
|
|
114
|
+
document.body.style.cursor = prevCursor;
|
|
115
|
+
// Cancel pending RAF
|
|
116
|
+
if (rafRef.current) {
|
|
117
|
+
cancelAnimationFrame(rafRef.current);
|
|
118
|
+
rafRef.current = 0;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const onUp = () => {
|
|
122
|
+
cleanup();
|
|
123
|
+
if (hasMoved && latestDropTargetIndex != null) {
|
|
124
|
+
const currentOrder = columnOrderRef.current ?? columnsRef.current.map((c) => c.columnId);
|
|
125
|
+
const newOrder = reorderColumnArray(currentOrder, columnId, latestDropTargetIndex);
|
|
126
|
+
onColumnOrderChangeRef.current?.(newOrder);
|
|
127
|
+
}
|
|
128
|
+
setIsDragging(false);
|
|
129
|
+
setDropIndicatorX(null);
|
|
130
|
+
};
|
|
131
|
+
window.addEventListener('mousemove', onMove, true);
|
|
132
|
+
window.addEventListener('mouseup', onUp, true);
|
|
133
|
+
cleanupRef.current = cleanup;
|
|
134
|
+
}, [enabled, wrapperRef]);
|
|
135
|
+
return { isDragging, dropIndicatorX, handleHeaderMouseDown };
|
|
136
|
+
}
|
|
@@ -11,6 +11,8 @@ import { useFillHandle } from './useFillHandle';
|
|
|
11
11
|
import { useUndoRedo } from './useUndoRedo';
|
|
12
12
|
import { useLatestRef } from './useLatestRef';
|
|
13
13
|
import { useTableLayout } from './useTableLayout';
|
|
14
|
+
import { useColumnPinning } from './useColumnPinning';
|
|
15
|
+
import { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
|
|
14
16
|
// Stable no-op handlers used when cellSelection is disabled (module-scope = no re-renders)
|
|
15
17
|
const NOOP = () => { };
|
|
16
18
|
const NOOP_ASYNC = async () => { };
|
|
@@ -23,7 +25,7 @@ const NOOP_CTX = (_e) => { };
|
|
|
23
25
|
*/
|
|
24
26
|
export function useDataGridState(params) {
|
|
25
27
|
const { props, wrapperRef } = params;
|
|
26
|
-
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, pinnedColumns, onCellError, } = props;
|
|
28
|
+
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, showRowNumbers, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, pinnedColumns, onColumnPinned, onCellError, } = props;
|
|
27
29
|
const cellSelection = cellSelectionProp !== false;
|
|
28
30
|
// Wrap onCellValueChanged with undo/redo tracking — all edits are recorded automatically
|
|
29
31
|
const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
|
|
@@ -63,8 +65,10 @@ export function useDataGridState(params) {
|
|
|
63
65
|
}, [flatColumns, visibleColumns, columnOrder]);
|
|
64
66
|
const visibleColumnCount = visibleCols.length;
|
|
65
67
|
const hasCheckboxCol = rowSelection === 'multiple';
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
+
const hasRowNumbersCol = !!showRowNumbers;
|
|
69
|
+
const specialColsCount = (hasCheckboxCol ? 1 : 0) + (hasRowNumbersCol ? 1 : 0);
|
|
70
|
+
const totalColCount = visibleColumnCount + specialColsCount;
|
|
71
|
+
const colOffset = specialColsCount;
|
|
68
72
|
const rowIndexByRowId = useMemo(() => {
|
|
69
73
|
const m = new Map();
|
|
70
74
|
items.forEach((item, idx) => m.set(getRowId(item), idx));
|
|
@@ -136,6 +140,16 @@ export function useDataGridState(params) {
|
|
|
136
140
|
initialColumnWidths,
|
|
137
141
|
onColumnResized,
|
|
138
142
|
});
|
|
143
|
+
const pinningResult = useColumnPinning({
|
|
144
|
+
columns: flatColumns,
|
|
145
|
+
pinnedColumns,
|
|
146
|
+
onColumnPinned,
|
|
147
|
+
});
|
|
148
|
+
const headerMenuResult = useColumnHeaderMenuState({
|
|
149
|
+
pinnedColumns: pinningResult.pinnedColumns,
|
|
150
|
+
onPinColumn: pinningResult.pinColumn,
|
|
151
|
+
onUnpinColumn: pinningResult.unpinColumn,
|
|
152
|
+
});
|
|
139
153
|
const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
|
|
140
154
|
const statusBarConfig = useMemo(() => {
|
|
141
155
|
const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
|
|
@@ -250,11 +264,11 @@ export function useDataGridState(params) {
|
|
|
250
264
|
// --- Memoize each sub-object so downstream consumers only re-render when their slice changes ---
|
|
251
265
|
const layoutState = useMemo(() => ({
|
|
252
266
|
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
253
|
-
hasCheckboxCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
267
|
+
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
254
268
|
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
255
269
|
}), [
|
|
256
270
|
flatColumns, visibleCols, visibleColumnCount, totalColCount, colOffset,
|
|
257
|
-
hasCheckboxCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
271
|
+
hasCheckboxCol, hasRowNumbersCol, rowIndexByRowId, containerWidth, minTableWidth,
|
|
258
272
|
desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides, onColumnResized,
|
|
259
273
|
]);
|
|
260
274
|
const rowSelectionState = useMemo(() => ({
|
|
@@ -302,6 +316,27 @@ export function useDataGridState(params) {
|
|
|
302
316
|
const viewModelsState = useMemo(() => ({
|
|
303
317
|
headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError,
|
|
304
318
|
}), [headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError]);
|
|
319
|
+
const pinningState = useMemo(() => ({
|
|
320
|
+
pinnedColumns: pinningResult.pinnedColumns,
|
|
321
|
+
pinColumn: pinningResult.pinColumn,
|
|
322
|
+
unpinColumn: pinningResult.unpinColumn,
|
|
323
|
+
isPinned: pinningResult.isPinned,
|
|
324
|
+
computeLeftOffsets: pinningResult.computeLeftOffsets,
|
|
325
|
+
computeRightOffsets: pinningResult.computeRightOffsets,
|
|
326
|
+
headerMenu: {
|
|
327
|
+
isOpen: headerMenuResult.isOpen,
|
|
328
|
+
openForColumn: headerMenuResult.openForColumn,
|
|
329
|
+
anchorElement: headerMenuResult.anchorElement,
|
|
330
|
+
open: headerMenuResult.open,
|
|
331
|
+
close: headerMenuResult.close,
|
|
332
|
+
handlePinLeft: headerMenuResult.handlePinLeft,
|
|
333
|
+
handlePinRight: headerMenuResult.handlePinRight,
|
|
334
|
+
handleUnpin: headerMenuResult.handleUnpin,
|
|
335
|
+
canPinLeft: headerMenuResult.canPinLeft,
|
|
336
|
+
canPinRight: headerMenuResult.canPinRight,
|
|
337
|
+
canUnpin: headerMenuResult.canUnpin,
|
|
338
|
+
},
|
|
339
|
+
}), [pinningResult, headerMenuResult]);
|
|
305
340
|
return {
|
|
306
341
|
layout: layoutState,
|
|
307
342
|
rowSelection: rowSelectionState,
|
|
@@ -309,5 +344,6 @@ export function useDataGridState(params) {
|
|
|
309
344
|
interaction: interactionState,
|
|
310
345
|
contextMenu: contextMenuState,
|
|
311
346
|
viewModels: viewModelsState,
|
|
347
|
+
pinning: pinningState,
|
|
312
348
|
};
|
|
313
349
|
}
|
|
@@ -11,7 +11,7 @@ const EMPTY_LOADING_OPTIONS = {};
|
|
|
11
11
|
* @returns Grouped props for DataGridTable, pagination controls, column chooser, layout, and filters.
|
|
12
12
|
*/
|
|
13
13
|
export function useOGrid(props, ref) {
|
|
14
|
-
const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
14
|
+
const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, onColumnResized, onColumnPinned, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, toolbarBelow, emptyState, entityLabelPlural = 'items', className, layoutMode = 'fill', suppressHorizontalScroll, editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, showRowNumbers, statusBar, pageSizeOptions, sideBar, onFirstDataRendered, onError, columnChooser: columnChooserProp, columnReorder, virtualScroll, density = 'normal', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
15
15
|
// Resolve column chooser placement
|
|
16
16
|
const columnChooserPlacement = columnChooserProp === false ? 'none'
|
|
17
17
|
: columnChooserProp === 'sidebar' ? 'sidebar'
|
|
@@ -265,6 +265,14 @@ export function useOGrid(props, ref) {
|
|
|
265
265
|
setRefreshCounter(refreshCounterRef.current);
|
|
266
266
|
}
|
|
267
267
|
},
|
|
268
|
+
getColumnOrder: () => columnOrder ?? columns.map((c) => c.columnId),
|
|
269
|
+
setColumnOrder: (order) => {
|
|
270
|
+
onColumnOrderChange?.(order);
|
|
271
|
+
},
|
|
272
|
+
scrollToRow: () => {
|
|
273
|
+
// No-op at orchestration level — DataGridTable components implement
|
|
274
|
+
// this via useVirtualScroll.scrollToIndex when virtual scrolling is active.
|
|
275
|
+
},
|
|
268
276
|
}), [
|
|
269
277
|
visibleColumns,
|
|
270
278
|
sort,
|
|
@@ -401,6 +409,9 @@ export function useOGrid(props, ref) {
|
|
|
401
409
|
rowSelection,
|
|
402
410
|
selectedRows: effectiveSelectedRows,
|
|
403
411
|
onSelectionChange: handleSelectionChange,
|
|
412
|
+
showRowNumbers,
|
|
413
|
+
currentPage: page,
|
|
414
|
+
pageSize,
|
|
404
415
|
statusBar: statusBarConfig,
|
|
405
416
|
isLoading: isLoadingResolved,
|
|
406
417
|
filters,
|
|
@@ -411,6 +422,9 @@ export function useOGrid(props, ref) {
|
|
|
411
422
|
getUserByEmail: dataSource?.getUserByEmail,
|
|
412
423
|
layoutMode,
|
|
413
424
|
suppressHorizontalScroll,
|
|
425
|
+
columnReorder,
|
|
426
|
+
virtualScroll,
|
|
427
|
+
density,
|
|
414
428
|
'aria-label': ariaLabel,
|
|
415
429
|
'aria-labelledby': ariaLabelledBy,
|
|
416
430
|
emptyState: {
|
|
@@ -424,9 +438,10 @@ export function useOGrid(props, ref) {
|
|
|
424
438
|
visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
|
|
425
439
|
handleColumnPinned, pinnedOverrides, columnWidthOverrides, freezeRows, freezeCols,
|
|
426
440
|
editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo,
|
|
427
|
-
rowSelection, effectiveSelectedRows, handleSelectionChange, statusBarConfig,
|
|
441
|
+
rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers, page, pageSize, statusBarConfig,
|
|
428
442
|
isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
|
|
429
|
-
loadingFilterOptions, layoutMode, suppressHorizontalScroll,
|
|
443
|
+
loadingFilterOptions, layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
|
|
444
|
+
density, ariaLabel, ariaLabelledBy,
|
|
430
445
|
hasActiveFilters, clearAllFilters, emptyState,
|
|
431
446
|
]);
|
|
432
447
|
const pagination = useMemo(() => ({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useMemo, useCallback, useRef } from 'react';
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
/** Threshold below which virtual scrolling is a no-op (all rows rendered). */
|
|
4
|
+
const PASSTHROUGH_THRESHOLD = 100;
|
|
5
|
+
/**
|
|
6
|
+
* Wraps TanStack Virtual for row virtualization.
|
|
7
|
+
* When disabled or when totalRows < threshold, returns a pass-through (all rows visible).
|
|
8
|
+
* @param params - Total rows, row height, enabled flag, overscan, and container ref.
|
|
9
|
+
* @returns Virtualizer instance, total height, visible range, and scrollToIndex helper.
|
|
10
|
+
*/
|
|
11
|
+
export function useVirtualScroll(params) {
|
|
12
|
+
const { totalRows, rowHeight, enabled, overscan = 5, containerRef, } = params;
|
|
13
|
+
const isActive = enabled && totalRows >= PASSTHROUGH_THRESHOLD;
|
|
14
|
+
const getScrollElement = useCallback(() => containerRef.current, [containerRef]);
|
|
15
|
+
const virtualizer = useVirtualizer({
|
|
16
|
+
count: isActive ? totalRows : 0,
|
|
17
|
+
getScrollElement,
|
|
18
|
+
estimateSize: () => rowHeight,
|
|
19
|
+
overscan,
|
|
20
|
+
enabled: isActive,
|
|
21
|
+
});
|
|
22
|
+
const passthroughRange = useMemo(() => ({
|
|
23
|
+
startIndex: 0,
|
|
24
|
+
endIndex: Math.max(0, totalRows - 1),
|
|
25
|
+
offsetTop: 0,
|
|
26
|
+
offsetBottom: 0,
|
|
27
|
+
}), [totalRows]);
|
|
28
|
+
const activeRange = useMemo(() => {
|
|
29
|
+
if (!isActive)
|
|
30
|
+
return passthroughRange;
|
|
31
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
32
|
+
if (virtualItems.length === 0) {
|
|
33
|
+
return { startIndex: 0, endIndex: -1, offsetTop: 0, offsetBottom: 0 };
|
|
34
|
+
}
|
|
35
|
+
const first = virtualItems[0];
|
|
36
|
+
const last = virtualItems[virtualItems.length - 1];
|
|
37
|
+
const totalSize = virtualizer.getTotalSize();
|
|
38
|
+
return {
|
|
39
|
+
startIndex: first.index,
|
|
40
|
+
endIndex: last.index,
|
|
41
|
+
offsetTop: first.start,
|
|
42
|
+
offsetBottom: Math.max(0, totalSize - last.end),
|
|
43
|
+
};
|
|
44
|
+
}, [isActive, virtualizer, passthroughRange]);
|
|
45
|
+
const totalHeight = isActive
|
|
46
|
+
? virtualizer.getTotalSize()
|
|
47
|
+
: totalRows * rowHeight;
|
|
48
|
+
const scrollToIndexRef = useRef(virtualizer);
|
|
49
|
+
scrollToIndexRef.current = virtualizer;
|
|
50
|
+
const scrollToIndex = useCallback((index) => {
|
|
51
|
+
if (isActive) {
|
|
52
|
+
scrollToIndexRef.current?.scrollToIndex(index, { align: 'auto' });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// When not virtualized, scroll the container directly
|
|
56
|
+
const container = containerRef.current;
|
|
57
|
+
if (container) {
|
|
58
|
+
const top = index * rowHeight;
|
|
59
|
+
container.scrollTo({ top, behavior: 'auto' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, [isActive, containerRef, rowHeight]);
|
|
63
|
+
return {
|
|
64
|
+
virtualizer: isActive ? virtualizer : null,
|
|
65
|
+
totalHeight,
|
|
66
|
+
visibleRange: activeRange,
|
|
67
|
+
scrollToIndex,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Constants (re-exported from core)
|
|
2
|
-
export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
2
|
+
export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
3
3
|
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
4
4
|
// Hooks
|
|
5
|
-
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useLatestRef, } from './hooks';
|
|
5
|
+
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useLatestRef, } from './hooks';
|
|
6
6
|
// Components
|
|
7
7
|
export { OGridLayout } from './components/OGridLayout';
|
|
8
8
|
export { StatusBar } from './components/StatusBar';
|
|
@@ -39,10 +39,18 @@ export { useInlineCellEditorState } from './useInlineCellEditorState';
|
|
|
39
39
|
export type { UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, } from './useInlineCellEditorState';
|
|
40
40
|
export { useColumnResize } from './useColumnResize';
|
|
41
41
|
export type { UseColumnResizeParams, UseColumnResizeResult, } from './useColumnResize';
|
|
42
|
+
export { useColumnPinning } from './useColumnPinning';
|
|
43
|
+
export type { UseColumnPinningParams, UseColumnPinningResult, } from './useColumnPinning';
|
|
44
|
+
export { useColumnHeaderMenuState } from './useColumnHeaderMenuState';
|
|
45
|
+
export type { UseColumnHeaderMenuStateParams, UseColumnHeaderMenuStateResult, } from './useColumnHeaderMenuState';
|
|
42
46
|
export { useRichSelectState } from './useRichSelectState';
|
|
43
47
|
export type { UseRichSelectStateParams, UseRichSelectStateResult } from './useRichSelectState';
|
|
44
48
|
export { useSideBarState } from './useSideBarState';
|
|
45
49
|
export type { UseSideBarStateParams, UseSideBarStateResult } from './useSideBarState';
|
|
46
50
|
export { useTableLayout } from './useTableLayout';
|
|
47
51
|
export type { UseTableLayoutParams, UseTableLayoutResult } from './useTableLayout';
|
|
52
|
+
export { useColumnReorder } from './useColumnReorder';
|
|
53
|
+
export type { UseColumnReorderParams, UseColumnReorderResult, } from './useColumnReorder';
|
|
54
|
+
export { useVirtualScroll } from './useVirtualScroll';
|
|
55
|
+
export type { IVirtualScrollConfig, UseVirtualScrollParams, UseVirtualScrollResult, } from './useVirtualScroll';
|
|
48
56
|
export { useLatestRef } from './useLatestRef';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface UseColumnHeaderMenuStateParams {
|
|
2
|
+
pinnedColumns: Record<string, 'left' | 'right'>;
|
|
3
|
+
onPinColumn: (columnId: string, side: 'left' | 'right') => void;
|
|
4
|
+
onUnpinColumn: (columnId: string) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface UseColumnHeaderMenuStateResult {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
openForColumn: string | null;
|
|
9
|
+
anchorElement: HTMLElement | null;
|
|
10
|
+
open: (columnId: string, anchorEl: HTMLElement) => void;
|
|
11
|
+
close: () => void;
|
|
12
|
+
handlePinLeft: () => void;
|
|
13
|
+
handlePinRight: () => void;
|
|
14
|
+
handleUnpin: () => void;
|
|
15
|
+
canPinLeft: boolean;
|
|
16
|
+
canPinRight: boolean;
|
|
17
|
+
canUnpin: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Manages state for the column header menu (pin left/right/unpin actions).
|
|
21
|
+
* Tracks which column's menu is open, anchor element, and action handlers.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useColumnHeaderMenuState(params: UseColumnHeaderMenuStateParams): UseColumnHeaderMenuStateResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IColumnDef } from '@alaarab/ogrid-core';
|
|
2
|
+
export interface UseColumnPinningParams<T = unknown> {
|
|
3
|
+
columns: IColumnDef<T>[];
|
|
4
|
+
/** Controlled pinned columns state. If provided, component is controlled. */
|
|
5
|
+
pinnedColumns?: Record<string, 'left' | 'right'>;
|
|
6
|
+
/** Called when user pins/unpins a column via UI. */
|
|
7
|
+
onColumnPinned?: (columnId: string, pinned: 'left' | 'right' | null) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface UseColumnPinningResult {
|
|
10
|
+
/** Current pinned columns (controlled or internal). */
|
|
11
|
+
pinnedColumns: Record<string, 'left' | 'right'>;
|
|
12
|
+
/** Pin a column to left or right. */
|
|
13
|
+
pinColumn: (columnId: string, side: 'left' | 'right') => void;
|
|
14
|
+
/** Unpin a column. */
|
|
15
|
+
unpinColumn: (columnId: string) => void;
|
|
16
|
+
/** Check if a column is pinned and which side. */
|
|
17
|
+
isPinned: (columnId: string) => 'left' | 'right' | undefined;
|
|
18
|
+
/** Compute sticky left offsets for pinned columns. */
|
|
19
|
+
computeLeftOffsets: (visibleCols: {
|
|
20
|
+
columnId: string;
|
|
21
|
+
}[], columnWidths: Record<string, number>, defaultWidth: number, hasCheckboxColumn: boolean, checkboxColumnWidth: number) => Record<string, number>;
|
|
22
|
+
/** Compute sticky right offsets for pinned columns. */
|
|
23
|
+
computeRightOffsets: (visibleCols: {
|
|
24
|
+
columnId: string;
|
|
25
|
+
}[], columnWidths: Record<string, number>, defaultWidth: number) => Record<string, number>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Manages column pinning state (left/right sticky positioning).
|
|
29
|
+
* Supports controlled and uncontrolled modes.
|
|
30
|
+
* Initializes from column.pinned definitions and pinnedColumns prop.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useColumnPinning<T = unknown>(params: UseColumnPinningParams<T>): UseColumnPinningResult;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { IColumnDef } from '../types';
|
|
3
|
+
export interface UseColumnReorderParams<T> {
|
|
4
|
+
columns: IColumnDef<T>[];
|
|
5
|
+
columnOrder?: string[];
|
|
6
|
+
onColumnOrderChange?: (order: string[]) => void;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Pinned column configuration for zone constraints. */
|
|
9
|
+
pinnedColumns?: Record<string, 'left' | 'right'>;
|
|
10
|
+
wrapperRef: RefObject<HTMLElement | null>;
|
|
11
|
+
}
|
|
12
|
+
export interface UseColumnReorderResult {
|
|
13
|
+
isDragging: boolean;
|
|
14
|
+
dropIndicatorX: number | null;
|
|
15
|
+
handleHeaderMouseDown: (columnId: string, event: React.MouseEvent) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Manages column reorder drag interactions with RAF-throttled updates.
|
|
19
|
+
* @param params - Columns, order, change callback, enabled flag, and wrapper ref.
|
|
20
|
+
* @returns Drag state and mousedown handler for header cells.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useColumnReorder<T>(params: UseColumnReorderParams<T>): UseColumnReorderResult;
|
|
@@ -13,6 +13,7 @@ export interface DataGridLayoutState<T> {
|
|
|
13
13
|
totalColCount: number;
|
|
14
14
|
colOffset: number;
|
|
15
15
|
hasCheckboxCol: boolean;
|
|
16
|
+
hasRowNumbersCol: boolean;
|
|
16
17
|
rowIndexByRowId: Map<RowId, number>;
|
|
17
18
|
containerWidth: number;
|
|
18
19
|
minTableWidth: number;
|
|
@@ -121,6 +122,32 @@ export interface DataGridViewModelState<T> {
|
|
|
121
122
|
showEmptyInGrid: boolean;
|
|
122
123
|
onCellError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
123
124
|
}
|
|
125
|
+
/** Column pinning state and column header menu. */
|
|
126
|
+
export interface DataGridPinningState {
|
|
127
|
+
pinnedColumns: Record<string, 'left' | 'right'>;
|
|
128
|
+
pinColumn: (columnId: string, side: 'left' | 'right') => void;
|
|
129
|
+
unpinColumn: (columnId: string) => void;
|
|
130
|
+
isPinned: (columnId: string) => 'left' | 'right' | undefined;
|
|
131
|
+
computeLeftOffsets: (visibleCols: {
|
|
132
|
+
columnId: string;
|
|
133
|
+
}[], columnWidths: Record<string, number>, defaultWidth: number, hasCheckboxColumn: boolean, checkboxColumnWidth: number) => Record<string, number>;
|
|
134
|
+
computeRightOffsets: (visibleCols: {
|
|
135
|
+
columnId: string;
|
|
136
|
+
}[], columnWidths: Record<string, number>, defaultWidth: number) => Record<string, number>;
|
|
137
|
+
headerMenu: {
|
|
138
|
+
isOpen: boolean;
|
|
139
|
+
openForColumn: string | null;
|
|
140
|
+
anchorElement: HTMLElement | null;
|
|
141
|
+
open: (columnId: string, anchorEl: HTMLElement) => void;
|
|
142
|
+
close: () => void;
|
|
143
|
+
handlePinLeft: () => void;
|
|
144
|
+
handlePinRight: () => void;
|
|
145
|
+
handleUnpin: () => void;
|
|
146
|
+
canPinLeft: boolean;
|
|
147
|
+
canPinRight: boolean;
|
|
148
|
+
canUnpin: boolean;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
124
151
|
/** Grouped result from useDataGridState. */
|
|
125
152
|
export interface UseDataGridStateResult<T> {
|
|
126
153
|
layout: DataGridLayoutState<T>;
|
|
@@ -129,6 +156,7 @@ export interface UseDataGridStateResult<T> {
|
|
|
129
156
|
interaction: DataGridCellInteractionState;
|
|
130
157
|
contextMenu: DataGridContextMenuState;
|
|
131
158
|
viewModels: DataGridViewModelState<T>;
|
|
159
|
+
pinning: DataGridPinningState;
|
|
132
160
|
}
|
|
133
161
|
/**
|
|
134
162
|
* Single orchestration hook for DataGridTable. Takes grid props and wrapper ref,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Virtualizer } from '@tanstack/react-virtual';
|
|
2
|
+
import type { RefObject } from 'react';
|
|
3
|
+
import type { IVisibleRange } from '@alaarab/ogrid-core';
|
|
4
|
+
export type { IVirtualScrollConfig } from '@alaarab/ogrid-core';
|
|
5
|
+
export interface UseVirtualScrollParams {
|
|
6
|
+
/** Total number of rows in the data set. */
|
|
7
|
+
totalRows: number;
|
|
8
|
+
/** Row height in pixels. */
|
|
9
|
+
rowHeight: number;
|
|
10
|
+
/** Whether virtual scrolling is enabled. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Number of extra rows to render outside the visible area. Default: 5. */
|
|
13
|
+
overscan?: number;
|
|
14
|
+
/** Ref to the scrollable container element. */
|
|
15
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
16
|
+
}
|
|
17
|
+
export interface UseVirtualScrollResult {
|
|
18
|
+
/** The TanStack virtualizer instance (null when disabled). */
|
|
19
|
+
virtualizer: Virtualizer<HTMLElement, Element> | null;
|
|
20
|
+
/** Total height of all rows in pixels. */
|
|
21
|
+
totalHeight: number;
|
|
22
|
+
/** The range of visible rows with spacer offsets. */
|
|
23
|
+
visibleRange: IVisibleRange;
|
|
24
|
+
/** Scroll to a specific row index. */
|
|
25
|
+
scrollToIndex: (index: number) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Wraps TanStack Virtual for row virtualization.
|
|
29
|
+
* When disabled or when totalRows < threshold, returns a pass-through (all rows visible).
|
|
30
|
+
* @param params - Total rows, row height, enabled flag, overscan, and container ref.
|
|
31
|
+
* @returns Virtualizer instance, total height, visible range, and scrollToIndex helper.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useVirtualScroll(params: UseVirtualScrollParams): UseVirtualScrollResult;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
2
|
-
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, RowId, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, HeaderCell, HeaderRow, SideBarPanelId, ISideBarDef, IDateFilterValue, } from './types';
|
|
1
|
+
export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
2
|
+
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, RowId, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, HeaderCell, HeaderRow, SideBarPanelId, ISideBarDef, IDateFilterValue, IVirtualScrollConfig, IColumnReorderConfig, } from './types';
|
|
3
3
|
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types';
|
|
4
|
-
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useLatestRef, } from './hooks';
|
|
5
|
-
export type { UseFilterOptionsResult, UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseActiveCellResult, UseCellEditingResult, EditingCell, UseContextMenuResult, ContextMenuPosition, UseCellSelectionResult, UseCellSelectionParams, UseClipboardResult, UseClipboardParams, UseRowSelectionResult, UseRowSelectionParams, UseKeyboardNavigationResult, UseKeyboardNavigationParams, UseUndoRedoResult, UseUndoRedoParams, UseFillHandleResult, UseFillHandleParams, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, UseColumnResizeParams, UseColumnResizeResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, UseTableLayoutParams, UseTableLayoutResult, } from './hooks';
|
|
4
|
+
export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useTextFilterState, useMultiSelectFilterState, usePeopleFilterState, useDateFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, useRichSelectState, useSideBarState, useTableLayout, useColumnReorder, useVirtualScroll, useLatestRef, } from './hooks';
|
|
5
|
+
export type { UseFilterOptionsResult, UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGridLayout, UseOGridFilters, ColumnChooserPlacement, UseActiveCellResult, UseCellEditingResult, EditingCell, UseContextMenuResult, ContextMenuPosition, UseCellSelectionResult, UseCellSelectionParams, UseClipboardResult, UseClipboardParams, UseRowSelectionResult, UseRowSelectionParams, UseKeyboardNavigationResult, UseKeyboardNavigationParams, UseUndoRedoResult, UseUndoRedoParams, UseFillHandleResult, UseFillHandleParams, UseDataGridStateParams, UseDataGridStateResult, DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseTextFilterStateParams, UseTextFilterStateResult, UseMultiSelectFilterStateParams, UseMultiSelectFilterStateResult, UsePeopleFilterStateParams, UsePeopleFilterStateResult, UseDateFilterStateParams, UseDateFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, UseColumnResizeParams, UseColumnResizeResult, UseRichSelectStateParams, UseRichSelectStateResult, UseSideBarStateParams, UseSideBarStateResult, UseTableLayoutParams, UseTableLayoutResult, UseColumnReorderParams, UseColumnReorderResult, UseVirtualScrollParams, UseVirtualScrollResult, } from './hooks';
|
|
6
6
|
export { OGridLayout } from './components/OGridLayout';
|
|
7
7
|
export type { OGridLayoutProps } from './components/OGridLayout';
|
|
8
8
|
export { StatusBar } from './components/StatusBar';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
|
|
3
|
-
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IOGridApi, } from '@alaarab/ogrid-core';
|
|
3
|
+
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IColumnReorderConfig, IOGridApi, } from '@alaarab/ogrid-core';
|
|
4
4
|
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
|
|
5
|
-
import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef } from '@alaarab/ogrid-core';
|
|
5
|
+
import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
|
|
6
6
|
/** Base props shared by both client-side and server-side OGrid modes. */
|
|
7
7
|
interface IOGridBaseProps<T> {
|
|
8
8
|
columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
|
|
@@ -43,6 +43,8 @@ interface IOGridBaseProps<T> {
|
|
|
43
43
|
rowSelection?: RowSelectionMode;
|
|
44
44
|
selectedRows?: Set<RowId>;
|
|
45
45
|
onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
|
|
46
|
+
/** Show Excel-style row numbers column at the start of the grid (1, 2, 3...). Default: false. */
|
|
47
|
+
showRowNumbers?: boolean;
|
|
46
48
|
statusBar?: boolean | IStatusBarProps;
|
|
47
49
|
defaultPageSize?: number;
|
|
48
50
|
defaultSortBy?: string;
|
|
@@ -68,6 +70,12 @@ interface IOGridBaseProps<T> {
|
|
|
68
70
|
sideBar?: boolean | ISideBarDef;
|
|
69
71
|
/** Page size options shown in the pagination dropdown. Default: [10, 20, 50, 100]. */
|
|
70
72
|
pageSizeOptions?: number[];
|
|
73
|
+
/** Enable column reordering via drag-and-drop on header cells. Default: false. */
|
|
74
|
+
columnReorder?: boolean;
|
|
75
|
+
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
76
|
+
virtualScroll?: IVirtualScrollConfig;
|
|
77
|
+
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
78
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
71
79
|
/** Fires once when the grid first renders with data (useful for restoring column state). */
|
|
72
80
|
onFirstDataRendered?: () => void;
|
|
73
81
|
/** Called when server-side fetchPage fails. */
|
|
@@ -130,6 +138,12 @@ export interface IOGridDataGridProps<T> {
|
|
|
130
138
|
rowSelection?: RowSelectionMode;
|
|
131
139
|
selectedRows?: Set<RowId>;
|
|
132
140
|
onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
|
|
141
|
+
/** Show Excel-style row numbers column. */
|
|
142
|
+
showRowNumbers?: boolean;
|
|
143
|
+
/** Current page number (1-based) for row number calculation. */
|
|
144
|
+
currentPage?: number;
|
|
145
|
+
/** Page size for row number calculation. */
|
|
146
|
+
pageSize?: number;
|
|
133
147
|
statusBar?: IStatusBarProps;
|
|
134
148
|
/** Unified filter model (discriminated union values). */
|
|
135
149
|
filters: IFilters;
|
|
@@ -145,6 +159,12 @@ export interface IOGridDataGridProps<T> {
|
|
|
145
159
|
message?: ReactNode;
|
|
146
160
|
render?: () => ReactNode;
|
|
147
161
|
};
|
|
162
|
+
/** Enable column reordering via drag-and-drop on header cells. Default: false. */
|
|
163
|
+
columnReorder?: boolean;
|
|
164
|
+
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
165
|
+
virtualScroll?: IVirtualScrollConfig;
|
|
166
|
+
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
167
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
148
168
|
/** Called when a cell renderer or custom editor throws an error. */
|
|
149
169
|
onCellError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
150
170
|
'aria-label'?: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
|
|
2
|
-
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, } from './dataGridTypes';
|
|
2
|
+
export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IColumnReorderConfig, } from './dataGridTypes';
|
|
3
3
|
export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-react",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "OGrid React – React hooks, headless components, and utilities for OGrid data grids.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
38
|
+
"@alaarab/ogrid-core": "2.0.4",
|
|
39
|
+
"@tanstack/react-virtual": "^3.11.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|