@alaarab/ogrid-react 2.0.3 → 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 +2 -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/useDataGridState.js +41 -5
- package/dist/esm/hooks/useOGrid.js +7 -3
- package/dist/esm/index.js +1 -1
- package/dist/types/hooks/index.d.ts +4 -0
- package/dist/types/hooks/useColumnHeaderMenuState.d.ts +23 -0
- package/dist/types/hooks/useColumnPinning.d.ts +32 -0
- package/dist/types/hooks/useDataGridState.d.ts +28 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/dataGridTypes.d.ts +12 -0
- package/package.json +2 -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,6 +19,8 @@ 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';
|
|
@@ -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
|
+
}
|
|
@@ -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, columnReorder, virtualScroll, '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'
|
|
@@ -409,6 +409,9 @@ export function useOGrid(props, ref) {
|
|
|
409
409
|
rowSelection,
|
|
410
410
|
selectedRows: effectiveSelectedRows,
|
|
411
411
|
onSelectionChange: handleSelectionChange,
|
|
412
|
+
showRowNumbers,
|
|
413
|
+
currentPage: page,
|
|
414
|
+
pageSize,
|
|
412
415
|
statusBar: statusBarConfig,
|
|
413
416
|
isLoading: isLoadingResolved,
|
|
414
417
|
filters,
|
|
@@ -421,6 +424,7 @@ export function useOGrid(props, ref) {
|
|
|
421
424
|
suppressHorizontalScroll,
|
|
422
425
|
columnReorder,
|
|
423
426
|
virtualScroll,
|
|
427
|
+
density,
|
|
424
428
|
'aria-label': ariaLabel,
|
|
425
429
|
'aria-labelledby': ariaLabelledBy,
|
|
426
430
|
emptyState: {
|
|
@@ -434,10 +438,10 @@ export function useOGrid(props, ref) {
|
|
|
434
438
|
visibleColumns, columnOrder, onColumnOrderChange, handleColumnResized,
|
|
435
439
|
handleColumnPinned, pinnedOverrides, columnWidthOverrides, freezeRows, freezeCols,
|
|
436
440
|
editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo,
|
|
437
|
-
rowSelection, effectiveSelectedRows, handleSelectionChange, statusBarConfig,
|
|
441
|
+
rowSelection, effectiveSelectedRows, handleSelectionChange, showRowNumbers, page, pageSize, statusBarConfig,
|
|
438
442
|
isLoadingResolved, filters, handleFilterChange, clientFilterOptions, dataSource,
|
|
439
443
|
loadingFilterOptions, layoutMode, suppressHorizontalScroll, columnReorder, virtualScroll,
|
|
440
|
-
ariaLabel, ariaLabelledBy,
|
|
444
|
+
density, ariaLabel, ariaLabelledBy,
|
|
441
445
|
hasActiveFilters, clearAllFilters, emptyState,
|
|
442
446
|
]);
|
|
443
447
|
const pagination = useMemo(() => ({
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
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';
|
|
@@ -39,6 +39,10 @@ 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';
|
|
@@ -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;
|
|
@@ -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,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
1
|
+
export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from '@alaarab/ogrid-core';
|
|
2
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
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';
|
|
@@ -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;
|
|
@@ -72,6 +74,8 @@ interface IOGridBaseProps<T> {
|
|
|
72
74
|
columnReorder?: boolean;
|
|
73
75
|
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
74
76
|
virtualScroll?: IVirtualScrollConfig;
|
|
77
|
+
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
78
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
75
79
|
/** Fires once when the grid first renders with data (useful for restoring column state). */
|
|
76
80
|
onFirstDataRendered?: () => void;
|
|
77
81
|
/** Called when server-side fetchPage fails. */
|
|
@@ -134,6 +138,12 @@ export interface IOGridDataGridProps<T> {
|
|
|
134
138
|
rowSelection?: RowSelectionMode;
|
|
135
139
|
selectedRows?: Set<RowId>;
|
|
136
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;
|
|
137
147
|
statusBar?: IStatusBarProps;
|
|
138
148
|
/** Unified filter model (discriminated union values). */
|
|
139
149
|
filters: IFilters;
|
|
@@ -153,6 +163,8 @@ export interface IOGridDataGridProps<T> {
|
|
|
153
163
|
columnReorder?: boolean;
|
|
154
164
|
/** Virtual scrolling configuration. When provided, only visible rows are rendered for large datasets. */
|
|
155
165
|
virtualScroll?: IVirtualScrollConfig;
|
|
166
|
+
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
|
167
|
+
density?: 'compact' | 'normal' | 'comfortable';
|
|
156
168
|
/** Called when a cell renderer or custom editor throws an error. */
|
|
157
169
|
onCellError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
158
170
|
'aria-label'?: string;
|
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,7 @@
|
|
|
35
35
|
"node": ">=18"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alaarab/ogrid-core": "2.0.
|
|
38
|
+
"@alaarab/ogrid-core": "2.0.4",
|
|
39
39
|
"@tanstack/react-virtual": "^3.11.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|