@alaarab/ogrid-core 1.3.0 → 1.3.2
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.
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
|
+
/** DOM attribute name used for drag-range highlighting (bypasses React). */
|
|
4
|
+
const DRAG_ATTR = 'data-drag-range';
|
|
3
5
|
export function useCellSelection(params) {
|
|
4
|
-
const { colOffset, rowCount, visibleColCount, setActiveCell } = params;
|
|
6
|
+
const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
|
|
5
7
|
const [selectionRange, setSelectionRange] = useState(null);
|
|
6
8
|
const isDraggingRef = useRef(false);
|
|
7
9
|
const [isDragging, setIsDragging] = useState(false);
|
|
8
10
|
const dragStartRef = useRef(null);
|
|
11
|
+
const rafRef = useRef(0);
|
|
12
|
+
/** Live drag range kept in a ref — only committed to React state on mouseup. */
|
|
13
|
+
const liveDragRangeRef = useRef(null);
|
|
9
14
|
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
15
|
+
// Only handle primary (left) button — let middle-click scroll and right-click context menu work natively
|
|
16
|
+
if (e.button !== 0)
|
|
17
|
+
return;
|
|
10
18
|
if (globalColIndex < colOffset)
|
|
11
19
|
return;
|
|
12
20
|
// Prevent native text selection during cell drag
|
|
@@ -23,12 +31,14 @@ export function useCellSelection(params) {
|
|
|
23
31
|
}
|
|
24
32
|
else {
|
|
25
33
|
dragStartRef.current = { row: rowIndex, col: dataColIndex };
|
|
26
|
-
|
|
34
|
+
const initial = {
|
|
27
35
|
startRow: rowIndex,
|
|
28
36
|
startCol: dataColIndex,
|
|
29
37
|
endRow: rowIndex,
|
|
30
38
|
endCol: dataColIndex,
|
|
31
|
-
}
|
|
39
|
+
};
|
|
40
|
+
setSelectionRange(initial);
|
|
41
|
+
liveDragRangeRef.current = initial;
|
|
32
42
|
setActiveCell({ rowIndex, columnIndex: globalColIndex });
|
|
33
43
|
isDraggingRef.current = true;
|
|
34
44
|
setIsDragging(true);
|
|
@@ -45,41 +55,138 @@ export function useCellSelection(params) {
|
|
|
45
55
|
});
|
|
46
56
|
setActiveCell({ rowIndex: 0, columnIndex: colOffset });
|
|
47
57
|
}, [rowCount, visibleColCount, colOffset, setActiveCell]);
|
|
48
|
-
|
|
58
|
+
/** Last known mouse position during drag — used by mouseUp to flush pending RAF work. */
|
|
59
|
+
const lastMousePosRef = useRef(null);
|
|
60
|
+
// Window mouse move/up for drag selection.
|
|
61
|
+
// Performance: during drag, we update a ref + toggle DOM attributes via rAF.
|
|
62
|
+
// React state is only committed on mouseup (single re-render instead of 60-120/s).
|
|
49
63
|
useEffect(() => {
|
|
50
|
-
const
|
|
51
|
-
|
|
64
|
+
const colOff = colOffset; // capture for closure
|
|
65
|
+
/** Toggle DRAG_ATTR on cell-content divs to show the range highlight via CSS. */
|
|
66
|
+
const applyDragAttrs = (range) => {
|
|
67
|
+
const wrapper = wrapperRef.current;
|
|
68
|
+
if (!wrapper)
|
|
69
|
+
return;
|
|
70
|
+
const minR = Math.min(range.startRow, range.endRow);
|
|
71
|
+
const maxR = Math.max(range.startRow, range.endRow);
|
|
72
|
+
const minC = Math.min(range.startCol, range.endCol);
|
|
73
|
+
const maxC = Math.max(range.startCol, range.endCol);
|
|
74
|
+
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
75
|
+
for (let i = 0; i < cells.length; i++) {
|
|
76
|
+
const el = cells[i];
|
|
77
|
+
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
78
|
+
const c = parseInt(el.getAttribute('data-col-index'), 10) - colOff;
|
|
79
|
+
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
80
|
+
if (inRange) {
|
|
81
|
+
if (!el.hasAttribute(DRAG_ATTR))
|
|
82
|
+
el.setAttribute(DRAG_ATTR, '');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (el.hasAttribute(DRAG_ATTR))
|
|
86
|
+
el.removeAttribute(DRAG_ATTR);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const clearDragAttrs = () => {
|
|
91
|
+
const wrapper = wrapperRef.current;
|
|
92
|
+
if (!wrapper)
|
|
52
93
|
return;
|
|
53
|
-
const
|
|
94
|
+
const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
|
|
95
|
+
for (let i = 0; i < marked.length; i++)
|
|
96
|
+
marked[i].removeAttribute(DRAG_ATTR);
|
|
97
|
+
};
|
|
98
|
+
/** Resolve mouse coordinates to a cell range (shared by RAF callback and mouseUp flush). */
|
|
99
|
+
const resolveRange = (cx, cy) => {
|
|
100
|
+
if (!dragStartRef.current)
|
|
101
|
+
return null;
|
|
102
|
+
const target = document.elementFromPoint(cx, cy);
|
|
54
103
|
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
55
104
|
if (!cell)
|
|
56
|
-
return;
|
|
105
|
+
return null;
|
|
57
106
|
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
58
107
|
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
59
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c <
|
|
60
|
-
return;
|
|
61
|
-
const dataCol = c -
|
|
108
|
+
if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
|
|
109
|
+
return null;
|
|
110
|
+
const dataCol = c - colOff;
|
|
62
111
|
const start = dragStartRef.current;
|
|
63
|
-
|
|
112
|
+
return normalizeSelectionRange({
|
|
64
113
|
startRow: start.row,
|
|
65
114
|
startCol: start.col,
|
|
66
115
|
endRow: r,
|
|
67
116
|
endCol: dataCol,
|
|
68
|
-
})
|
|
69
|
-
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const onMove = (e) => {
|
|
120
|
+
if (!isDraggingRef.current || !dragStartRef.current)
|
|
121
|
+
return;
|
|
122
|
+
// Always store latest position so mouseUp can flush if RAF hasn't executed
|
|
123
|
+
lastMousePosRef.current = { cx: e.clientX, cy: e.clientY };
|
|
124
|
+
// Cancel previous pending frame
|
|
125
|
+
if (rafRef.current)
|
|
126
|
+
cancelAnimationFrame(rafRef.current);
|
|
127
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
128
|
+
rafRef.current = 0;
|
|
129
|
+
const pos = lastMousePosRef.current;
|
|
130
|
+
if (!pos)
|
|
131
|
+
return;
|
|
132
|
+
const newRange = resolveRange(pos.cx, pos.cy);
|
|
133
|
+
if (!newRange)
|
|
134
|
+
return;
|
|
135
|
+
// Skip if range unchanged
|
|
136
|
+
const prev = liveDragRangeRef.current;
|
|
137
|
+
if (prev &&
|
|
138
|
+
prev.startRow === newRange.startRow &&
|
|
139
|
+
prev.startCol === newRange.startCol &&
|
|
140
|
+
prev.endRow === newRange.endRow &&
|
|
141
|
+
prev.endCol === newRange.endCol) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
liveDragRangeRef.current = newRange;
|
|
145
|
+
// DOM-only highlighting — no React state update until mouseup
|
|
146
|
+
applyDragAttrs(newRange);
|
|
147
|
+
});
|
|
70
148
|
};
|
|
71
149
|
const onUp = () => {
|
|
150
|
+
if (!isDraggingRef.current)
|
|
151
|
+
return;
|
|
152
|
+
if (rafRef.current) {
|
|
153
|
+
cancelAnimationFrame(rafRef.current);
|
|
154
|
+
rafRef.current = 0;
|
|
155
|
+
}
|
|
72
156
|
isDraggingRef.current = false;
|
|
73
|
-
|
|
157
|
+
// Flush: if the last RAF hasn't executed yet, resolve the range now from the
|
|
158
|
+
// last known mouse position so the final committed range is always accurate.
|
|
159
|
+
const pos = lastMousePosRef.current;
|
|
160
|
+
if (pos) {
|
|
161
|
+
const flushed = resolveRange(pos.cx, pos.cy);
|
|
162
|
+
if (flushed)
|
|
163
|
+
liveDragRangeRef.current = flushed;
|
|
164
|
+
}
|
|
165
|
+
// Commit final range to React state (triggers a single re-render)
|
|
166
|
+
const finalRange = liveDragRangeRef.current;
|
|
167
|
+
if (finalRange) {
|
|
168
|
+
setSelectionRange(finalRange);
|
|
169
|
+
setActiveCell({
|
|
170
|
+
rowIndex: finalRange.endRow,
|
|
171
|
+
columnIndex: finalRange.endCol + colOff,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Clean up DOM attributes — React will apply CSS-module classes on the same paint
|
|
175
|
+
clearDragAttrs();
|
|
176
|
+
liveDragRangeRef.current = null;
|
|
177
|
+
lastMousePosRef.current = null;
|
|
74
178
|
dragStartRef.current = null;
|
|
179
|
+
setIsDragging(false);
|
|
75
180
|
};
|
|
76
181
|
window.addEventListener('mousemove', onMove, true);
|
|
77
182
|
window.addEventListener('mouseup', onUp, true);
|
|
78
183
|
return () => {
|
|
79
184
|
window.removeEventListener('mousemove', onMove, true);
|
|
80
185
|
window.removeEventListener('mouseup', onUp, true);
|
|
186
|
+
if (rafRef.current)
|
|
187
|
+
cancelAnimationFrame(rafRef.current);
|
|
81
188
|
};
|
|
82
|
-
}, [colOffset, setActiveCell]);
|
|
189
|
+
}, [colOffset, setActiveCell, wrapperRef]);
|
|
83
190
|
return {
|
|
84
191
|
selectionRange,
|
|
85
192
|
setSelectionRange,
|
|
@@ -70,6 +70,7 @@ export function useDataGridState(params) {
|
|
|
70
70
|
rowCount: items.length,
|
|
71
71
|
visibleColCount: visibleCols.length,
|
|
72
72
|
setActiveCell,
|
|
73
|
+
wrapperRef,
|
|
73
74
|
});
|
|
74
75
|
const { contextMenu, setContextMenu, handleCellContextMenu, closeContextMenu } = useContextMenu();
|
|
75
76
|
const { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges } = useClipboard({
|
|
@@ -2,35 +2,109 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
3
|
import { getCellValue } from '../utils';
|
|
4
4
|
import { parseValue } from '../utils/valueParsers';
|
|
5
|
+
/** DOM attribute name for fill-drag range highlighting (same as cell selection drag). */
|
|
6
|
+
const DRAG_ATTR = 'data-drag-range';
|
|
5
7
|
export function useFillHandle(params) {
|
|
6
8
|
const { items, visibleCols, editable, onCellValueChanged, selectionRange, setSelectionRange, setActiveCell, colOffset, wrapperRef, beginBatch, endBatch, } = params;
|
|
7
9
|
const [fillDrag, setFillDrag] = useState(null);
|
|
8
10
|
const fillDragEndRef = useRef({ endRow: 0, endCol: 0 });
|
|
11
|
+
const rafRef = useRef(0);
|
|
12
|
+
const liveFillRangeRef = useRef(null);
|
|
9
13
|
useEffect(() => {
|
|
10
14
|
if (!fillDrag || editable === false || !onCellValueChanged || !wrapperRef.current)
|
|
11
15
|
return;
|
|
12
16
|
fillDragEndRef.current = { endRow: fillDrag.startRow, endCol: fillDrag.startCol };
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
liveFillRangeRef.current = null;
|
|
18
|
+
const colOff = colOffset;
|
|
19
|
+
const applyDragAttrs = (range) => {
|
|
20
|
+
const wrapper = wrapperRef.current;
|
|
21
|
+
if (!wrapper)
|
|
22
|
+
return;
|
|
23
|
+
const minR = Math.min(range.startRow, range.endRow);
|
|
24
|
+
const maxR = Math.max(range.startRow, range.endRow);
|
|
25
|
+
const minC = Math.min(range.startCol, range.endCol);
|
|
26
|
+
const maxC = Math.max(range.startCol, range.endCol);
|
|
27
|
+
const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
|
|
28
|
+
for (let i = 0; i < cells.length; i++) {
|
|
29
|
+
const el = cells[i];
|
|
30
|
+
const r = parseInt(el.getAttribute('data-row-index'), 10);
|
|
31
|
+
const c = parseInt(el.getAttribute('data-col-index'), 10) - colOff;
|
|
32
|
+
const inRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
|
|
33
|
+
if (inRange) {
|
|
34
|
+
if (!el.hasAttribute(DRAG_ATTR))
|
|
35
|
+
el.setAttribute(DRAG_ATTR, '');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (el.hasAttribute(DRAG_ATTR))
|
|
39
|
+
el.removeAttribute(DRAG_ATTR);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const clearDragAttrs = () => {
|
|
44
|
+
const wrapper = wrapperRef.current;
|
|
45
|
+
if (!wrapper)
|
|
46
|
+
return;
|
|
47
|
+
const marked = wrapper.querySelectorAll(`[${DRAG_ATTR}]`);
|
|
48
|
+
for (let i = 0; i < marked.length; i++)
|
|
49
|
+
marked[i].removeAttribute(DRAG_ATTR);
|
|
50
|
+
};
|
|
51
|
+
let lastFillMousePos = null;
|
|
52
|
+
const resolveRange = (cx, cy) => {
|
|
53
|
+
const target = document.elementFromPoint(cx, cy);
|
|
15
54
|
const cell = target?.closest?.('[data-row-index][data-col-index]');
|
|
16
55
|
if (!cell || !wrapperRef.current?.contains(cell))
|
|
17
|
-
return;
|
|
56
|
+
return null;
|
|
18
57
|
const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
|
|
19
58
|
const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
|
|
20
|
-
if (Number.isNaN(r) || Number.isNaN(c) || c <
|
|
21
|
-
return;
|
|
22
|
-
const dataCol = c -
|
|
23
|
-
|
|
24
|
-
const norm = normalizeSelectionRange({
|
|
59
|
+
if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
|
|
60
|
+
return null;
|
|
61
|
+
const dataCol = c - colOff;
|
|
62
|
+
return normalizeSelectionRange({
|
|
25
63
|
startRow: fillDrag.startRow,
|
|
26
64
|
startCol: fillDrag.startCol,
|
|
27
65
|
endRow: r,
|
|
28
66
|
endCol: dataCol,
|
|
29
67
|
});
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
};
|
|
69
|
+
const onMove = (e) => {
|
|
70
|
+
lastFillMousePos = { cx: e.clientX, cy: e.clientY };
|
|
71
|
+
if (rafRef.current)
|
|
72
|
+
cancelAnimationFrame(rafRef.current);
|
|
73
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
74
|
+
rafRef.current = 0;
|
|
75
|
+
if (!lastFillMousePos)
|
|
76
|
+
return;
|
|
77
|
+
const newRange = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
78
|
+
if (!newRange)
|
|
79
|
+
return;
|
|
80
|
+
// Skip if unchanged
|
|
81
|
+
const prev = liveFillRangeRef.current;
|
|
82
|
+
if (prev &&
|
|
83
|
+
prev.startRow === newRange.startRow &&
|
|
84
|
+
prev.startCol === newRange.startCol &&
|
|
85
|
+
prev.endRow === newRange.endRow &&
|
|
86
|
+
prev.endCol === newRange.endCol) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
liveFillRangeRef.current = newRange;
|
|
90
|
+
fillDragEndRef.current = { endRow: newRange.endRow, endCol: newRange.endCol };
|
|
91
|
+
applyDragAttrs(newRange);
|
|
92
|
+
});
|
|
32
93
|
};
|
|
33
94
|
const onUp = () => {
|
|
95
|
+
if (rafRef.current) {
|
|
96
|
+
cancelAnimationFrame(rafRef.current);
|
|
97
|
+
rafRef.current = 0;
|
|
98
|
+
}
|
|
99
|
+
// Flush: resolve final position if RAF hasn't executed yet
|
|
100
|
+
if (lastFillMousePos) {
|
|
101
|
+
const flushed = resolveRange(lastFillMousePos.cx, lastFillMousePos.cy);
|
|
102
|
+
if (flushed) {
|
|
103
|
+
liveFillRangeRef.current = flushed;
|
|
104
|
+
fillDragEndRef.current = { endRow: flushed.endRow, endCol: flushed.endCol };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
clearDragAttrs();
|
|
34
108
|
const end = fillDragEndRef.current;
|
|
35
109
|
const norm = normalizeSelectionRange({
|
|
36
110
|
startRow: fillDrag.startRow,
|
|
@@ -38,6 +112,10 @@ export function useFillHandle(params) {
|
|
|
38
112
|
endRow: end.endRow,
|
|
39
113
|
endCol: end.endCol,
|
|
40
114
|
});
|
|
115
|
+
// Commit range to React state
|
|
116
|
+
setSelectionRange(norm);
|
|
117
|
+
setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOff });
|
|
118
|
+
// Apply fill values
|
|
41
119
|
const startItem = items[norm.startRow];
|
|
42
120
|
const startColDef = visibleCols[norm.startCol];
|
|
43
121
|
if (startItem && startColDef) {
|
|
@@ -72,12 +150,15 @@ export function useFillHandle(params) {
|
|
|
72
150
|
endBatch?.();
|
|
73
151
|
}
|
|
74
152
|
setFillDrag(null);
|
|
153
|
+
liveFillRangeRef.current = null;
|
|
75
154
|
};
|
|
76
155
|
window.addEventListener('mousemove', onMove, true);
|
|
77
156
|
window.addEventListener('mouseup', onUp, true);
|
|
78
157
|
return () => {
|
|
79
158
|
window.removeEventListener('mousemove', onMove, true);
|
|
80
159
|
window.removeEventListener('mouseup', onUp, true);
|
|
160
|
+
if (rafRef.current)
|
|
161
|
+
cancelAnimationFrame(rafRef.current);
|
|
81
162
|
};
|
|
82
163
|
}, [
|
|
83
164
|
fillDrag,
|
|
@@ -5,7 +5,7 @@ import { toDataGridFilterProps } from '../types';
|
|
|
5
5
|
import { useFilterOptions } from './useFilterOptions';
|
|
6
6
|
const DEFAULT_PAGE_SIZE = 20;
|
|
7
7
|
export function useOGrid(props, ref) {
|
|
8
|
-
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, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, emptyState, entityLabelPlural = 'items', className, title, layoutMode = '
|
|
8
|
+
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, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, emptyState, entityLabelPlural = 'items', className, title, layoutMode = 'fill', editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, onError, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
|
|
9
9
|
const columns = useMemo(() => flattenColumns(columnsProp), [columnsProp]);
|
|
10
10
|
const isServerSide = dataSource != null;
|
|
11
11
|
const isClientSide = !isServerSide;
|
|
@@ -4,6 +4,7 @@ export interface UseCellSelectionParams {
|
|
|
4
4
|
rowCount: number;
|
|
5
5
|
visibleColCount: number;
|
|
6
6
|
setActiveCell: (cell: IActiveCell | null) => void;
|
|
7
|
+
wrapperRef: React.RefObject<HTMLElement | null>;
|
|
7
8
|
}
|
|
8
9
|
export interface UseCellSelectionResult {
|
|
9
10
|
selectionRange: ISelectionRange | null;
|
package/package.json
CHANGED