@opencorestack/opengridx 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +11 -7
- package/dist/index.d.ts +55 -0
- package/dist/opengridx.es.js +6346 -6279
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +4 -0
- package/docs/architecture/datagrid-orchestration.md +4 -2
- package/docs/architecture/use-grid-virtualization.md +2 -0
- package/docs/components/pagination.md +1 -1
- package/docs/features/ai-toolkit.md +1 -0
- package/docs/features/rtl.md +1 -1
- package/docs/performance.md +19 -1
- package/docs/roadmap.md +26 -5
- package/lib/components/Cell/Cell.tsx +2 -1
- package/lib/components/DataGrid/DataGrid.tsx +19 -18
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridViewport.tsx +2 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Row/Row.tsx +6 -2
- package/lib/components/Toolbar/AggregationPanel.tsx +90 -0
- package/lib/components/Toolbar/ColumnsPanelWrapper.tsx +56 -0
- package/lib/components/Toolbar/FilterPanelWrapper.tsx +82 -0
- package/lib/components/Toolbar/GridToolbar.tsx +7 -380
- package/lib/components/Toolbar/ToolbarIcons.tsx +35 -0
- package/lib/components/Toolbar/useToolbarPanel.ts +71 -0
- package/lib/hooks/core/gridKeyDown.ts +272 -0
- package/lib/hooks/core/useDataGrid.ts +13 -5
- package/lib/hooks/core/useGridApiMethods.ts +2 -2
- package/lib/hooks/core/useGridCellRangeDrag.ts +345 -0
- package/lib/hooks/core/useGridCellSelection.ts +17 -297
- package/lib/hooks/core/useGridColumnLookup.ts +2 -1
- package/lib/hooks/core/useGridColumns.tsx +3 -2
- package/lib/hooks/core/useGridControlledState.ts +3 -2
- package/lib/hooks/core/useGridHierarchy.ts +1 -1
- package/lib/hooks/core/useGridKeyboardNavigation.ts +51 -235
- package/lib/hooks/core/useGridLiveRowSelection.ts +2 -2
- package/lib/hooks/core/useGridRowInteractions.ts +1 -1
- package/lib/hooks/core/useGridRowSelection.ts +11 -2
- package/lib/hooks/core/useGridToolbarProps.ts +3 -2
- package/lib/hooks/features/useGridAiAssistant.ts +3 -2
- package/lib/hooks/useColumnReorder.ts +2 -1
- package/lib/hooks/useTreeData.ts +4 -2
- package/lib/types/locale.ts +22 -0
- package/lib/utils/aggregation/index.ts +16 -0
- package/lib/utils/aiAssistant.ts +46 -15
- package/lib/utils/export/excelCells.ts +244 -0
- package/lib/utils/export/exportCsv.ts +179 -0
- package/lib/utils/export/exportExcelHtml.ts +220 -0
- package/lib/utils/export/exportJson.ts +179 -0
- package/lib/utils/export/exportRows.ts +85 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +22 -242
- package/lib/utils/export/index.ts +5 -695
- package/lib/utils/export/printGrid.ts +282 -0
- package/lib/utils/gridLocale.ts +30 -0
- package/lib/utils/selectionSets.ts +36 -0
- package/lib/utils/untypedColumns.ts +16 -0
- package/llms.txt +3 -1
- package/package.json +6 -2
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +0 -1
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +0 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import type { RefObject } from 'react';
|
|
8
|
+
import { isRtl } from '../../utils/direction';
|
|
9
|
+
import { isTextEntryElement } from '../../utils/focus';
|
|
10
|
+
import type { FocusedCell } from '../../utils/focus';
|
|
11
|
+
import { isRangeColumnField, isSameCoordinates } from '../../utils/cellSelection';
|
|
12
|
+
import type { CellRangeContext } from '../../utils/cellSelection';
|
|
13
|
+
import type {
|
|
14
|
+
GridCellCoordinates,
|
|
15
|
+
GridCellSelectionModel,
|
|
16
|
+
GridCellSelectionReason,
|
|
17
|
+
GridRowModel,
|
|
18
|
+
} from '../../types';
|
|
19
|
+
|
|
20
|
+
/** Root class while a pointer drag selects cells: no text selection meanwhile. Public from v3.3. */
|
|
21
|
+
export const RANGE_DRAGGING_CLASS = 'ogx--range-dragging';
|
|
22
|
+
|
|
23
|
+
/** Pointer distance (px) from a body edge at which a drag starts scrolling. */
|
|
24
|
+
import type { GridTrackedDragOptions, UseGridCellSelectionParams } from './useGridCellSelection';
|
|
25
|
+
|
|
26
|
+
// The cell range drag of useGridCellSelection: click, Shift+click and drag (with auto-scroll past
|
|
27
|
+
// the edges), and tracked drags for the fill handle. Split out of useGridCellSelection (3.10); it is
|
|
28
|
+
// called from there, so the grid's hook order is unchanged.
|
|
29
|
+
|
|
30
|
+
const AUTO_SCROLL_EDGE = 32;
|
|
31
|
+
const AUTO_SCROLL_MAX_STEP = 40;
|
|
32
|
+
|
|
33
|
+
interface DragState {
|
|
34
|
+
anchor: GridCellCoordinates;
|
|
35
|
+
head: GridCellCoordinates;
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
frame: number;
|
|
39
|
+
moved: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the viewport lays out right to left, read once per drag: `getComputedStyle` every frame
|
|
42
|
+
* forces a style recalculation, which slowed WebKit's auto-scroll enough to delay the release.
|
|
43
|
+
*/
|
|
44
|
+
rtl?: boolean;
|
|
45
|
+
/** Ends the drag; `cancelled` unless the button was released normally (tracked drags: Escape cancels). */
|
|
46
|
+
stop: (cancelled?: boolean, altKey?: boolean) => void;
|
|
47
|
+
/** A tracked drag (fill handle): told the cell under the pointer each frame instead of moving the range. */
|
|
48
|
+
onTrack?: (head: GridCellCoordinates, x: number, y: number) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The body cell of `viewport` holding `target`: its row index and field. Not cells of nested grids or detail panels. */
|
|
52
|
+
function cellFromTarget(target: EventTarget | null, viewport: HTMLElement): { rowIndex: number; field: string; element: HTMLElement } | null {
|
|
53
|
+
if (!target || (target as Node).nodeType !== 1) return null;
|
|
54
|
+
const element = (target as Element).closest<HTMLElement>('.ogx__cell[data-field]');
|
|
55
|
+
if (!element || element.closest('.ogx__viewport') !== viewport) return null;
|
|
56
|
+
const panel = element.closest('.ogx__detail-panel');
|
|
57
|
+
if (panel && viewport.contains(panel)) return null;
|
|
58
|
+
const rowElement = element.closest('[role="row"][data-rowindex]');
|
|
59
|
+
if (!rowElement) return null;
|
|
60
|
+
const rowIndex = Number(rowElement.getAttribute('data-rowindex'));
|
|
61
|
+
const field = element.dataset.field;
|
|
62
|
+
if (!Number.isInteger(rowIndex) || !field) return null;
|
|
63
|
+
return { rowIndex, field, element };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearTextSelection(doc: Document): void {
|
|
67
|
+
const selection = doc.getSelection();
|
|
68
|
+
if (selection && selection.rangeCount > 0) selection.removeAllRanges();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
|
|
72
|
+
const autoScrollStep = (over: number) => Math.min(AUTO_SCROLL_MAX_STEP, Math.max(2, Math.round(over / 2)));
|
|
73
|
+
|
|
74
|
+
/** What the drag reads from the selection hook's latest values at event time. */
|
|
75
|
+
export interface CellRangeDragLatest {
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
context: CellRangeContext | null;
|
|
78
|
+
layout: UseGridCellSelectionParams<GridRowModel>['layout'];
|
|
79
|
+
pinnedTopRowCount: number;
|
|
80
|
+
viewportRef: RefObject<HTMLDivElement | null>;
|
|
81
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface UseGridCellRangeDragParams {
|
|
85
|
+
latestRef: { readonly current: CellRangeDragLatest };
|
|
86
|
+
focusRef: { readonly current: FocusedCell | null };
|
|
87
|
+
setRange: (anchor: GridCellCoordinates, head: GridCellCoordinates, reason: GridCellSelectionReason) => void;
|
|
88
|
+
currentModel: () => GridCellSelectionModel;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useGridCellRangeDrag(params: UseGridCellRangeDragParams) {
|
|
92
|
+
const { latestRef, focusRef, setRange, currentModel } = params;
|
|
93
|
+
// ── Pointer: click, Shift+click and drag ────────────────────────────────
|
|
94
|
+
const dragRef = useRef<DragState | null>(null);
|
|
95
|
+
const suppressClickRef = useRef(false);
|
|
96
|
+
|
|
97
|
+
useEffect(() => () => dragRef.current?.stop(), []);
|
|
98
|
+
|
|
99
|
+
const headAtPoint = useCallback((viewport: HTMLElement, x: number, y: number): GridCellCoordinates | null => {
|
|
100
|
+
const ctx = latestRef.current.context;
|
|
101
|
+
if (!ctx) return null;
|
|
102
|
+
const doc = viewport.ownerDocument;
|
|
103
|
+
// jsdom has no elementFromPoint.
|
|
104
|
+
if (typeof doc.elementFromPoint !== 'function') return null;
|
|
105
|
+
const hit = cellFromTarget(doc.elementFromPoint(x, y), viewport);
|
|
106
|
+
if (!hit) return null;
|
|
107
|
+
const id = ctx.rowIds[hit.rowIndex];
|
|
108
|
+
if (id === undefined) return null;
|
|
109
|
+
// Over the system or grouping column (left of the data): the first data column.
|
|
110
|
+
const field = isRangeColumnField(hit.field) ? hit.field : ctx.fields[0];
|
|
111
|
+
return field === undefined ? null : { id, field };
|
|
112
|
+
}, [latestRef]);
|
|
113
|
+
|
|
114
|
+
/** The row whose band holds `centerY` (px from the top of the unpinned rows), with `field`. */
|
|
115
|
+
const headRowAtOffset = useCallback((centerY: number, field: string): GridCellCoordinates | null => {
|
|
116
|
+
const { context: ctx, layout, pinnedTopRowCount } = latestRef.current;
|
|
117
|
+
const heights = layout.cumulativeHeights;
|
|
118
|
+
if (!ctx || heights.length === 0 || centerY < 0) return null;
|
|
119
|
+
let lo = 0;
|
|
120
|
+
let hi = heights.length - 1;
|
|
121
|
+
while (lo < hi) {
|
|
122
|
+
const mid = (lo + hi) >> 1;
|
|
123
|
+
if (heights[mid] > centerY) hi = mid; else lo = mid + 1;
|
|
124
|
+
}
|
|
125
|
+
const id = ctx.rowIds[lo + pinnedTopRowCount];
|
|
126
|
+
return id === undefined ? null : { id, field };
|
|
127
|
+
}, [latestRef]);
|
|
128
|
+
|
|
129
|
+
// The auto-scroll loop schedules the next frame through this ref: a callback cannot name itself.
|
|
130
|
+
const runDragFrameRef = useRef<(allowScroll: boolean) => void>(() => {});
|
|
131
|
+
const runDragFrame = useCallback((allowScroll: boolean) => {
|
|
132
|
+
const drag = dragRef.current;
|
|
133
|
+
const viewport = latestRef.current.viewportRef.current;
|
|
134
|
+
if (!drag || !viewport) return;
|
|
135
|
+
drag.frame = 0;
|
|
136
|
+
const { layout } = latestRef.current;
|
|
137
|
+
const box = viewport.getBoundingClientRect();
|
|
138
|
+
const width = viewport.clientWidth;
|
|
139
|
+
const height = viewport.clientHeight;
|
|
140
|
+
const stickyTop = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-top');
|
|
141
|
+
const stickyBottom = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-bottom');
|
|
142
|
+
const headerWrap = stickyTop?.querySelector(':scope > .ogx__header-wrap');
|
|
143
|
+
const bodyTop = stickyTop ? stickyTop.getBoundingClientRect().bottom : box.top;
|
|
144
|
+
const bodyBottom = Math.min(box.top + height, stickyBottom ? stickyBottom.getBoundingClientRect().top : box.top + height);
|
|
145
|
+
const headerBottom = headerWrap ? headerWrap.getBoundingClientRect().bottom : bodyTop;
|
|
146
|
+
// Left-pinned columns (and pinned system columns) sit at the inline start: on the right in a
|
|
147
|
+
// right-to-left grid, whose vertical scrollbar is on the left.
|
|
148
|
+
if (drag.rtl === undefined) drag.rtl = isRtl(viewport);
|
|
149
|
+
const rtl = drag.rtl;
|
|
150
|
+
const contentLeft = rtl ? box.right - width : box.left;
|
|
151
|
+
const startPinnedWidth = layout.pinnedSystemColumnsWidth + layout.leftWidth;
|
|
152
|
+
const bodyLeft = contentLeft + (rtl ? layout.rightWidth : startPinnedWidth);
|
|
153
|
+
const bodyRight = contentLeft + width - (rtl ? startPinnedWidth : layout.rightWidth);
|
|
154
|
+
|
|
155
|
+
// The cell under the pointer; outside the viewport (or over the header) the nearest body cell.
|
|
156
|
+
const x = clamp(drag.x, contentLeft + 1, contentLeft + width - 2);
|
|
157
|
+
let y = drag.y;
|
|
158
|
+
if (y < box.top || y > box.top + height || y < headerBottom) y = clamp(y, bodyTop + 1, Math.max(bodyTop + 1, bodyBottom - 2));
|
|
159
|
+
// During fast auto-scroll the rows under the pointer may not be rendered yet (virtualization
|
|
160
|
+
// lags a frame or more on a slow machine), so the hit test finds no cell. Work out the row from
|
|
161
|
+
// the scroll position and the row heights instead, keeping the column the head already has.
|
|
162
|
+
const canHitTest = typeof viewport.ownerDocument.elementFromPoint === 'function';
|
|
163
|
+
const hit = headAtPoint(viewport, x, y);
|
|
164
|
+
const head = hit ?? (canHitTest && drag.moved
|
|
165
|
+
? headRowAtOffset(y - box.top - (stickyTop ? stickyTop.getBoundingClientRect().height : 0) + viewport.scrollTop, drag.head.field)
|
|
166
|
+
: null);
|
|
167
|
+
if (head && !isSameCoordinates(head, drag.head)) {
|
|
168
|
+
drag.head = head;
|
|
169
|
+
if (!drag.moved && !isSameCoordinates(head, drag.anchor)) {
|
|
170
|
+
drag.moved = true;
|
|
171
|
+
latestRef.current.containerRef.current?.classList.add(RANGE_DRAGGING_CLASS);
|
|
172
|
+
clearTextSelection(viewport.ownerDocument);
|
|
173
|
+
}
|
|
174
|
+
if (!drag.onTrack) setRange(drag.anchor, head, 'pointer');
|
|
175
|
+
}
|
|
176
|
+
// A tracked drag hears every frame: its target can change while the head stays in one cell.
|
|
177
|
+
if (drag.onTrack) drag.onTrack(drag.head, drag.x, drag.y);
|
|
178
|
+
|
|
179
|
+
if (!allowScroll) return;
|
|
180
|
+
const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < contentLeft || drag.x > contentLeft + width;
|
|
181
|
+
if (!drag.moved && !outside) return;
|
|
182
|
+
let dy = 0;
|
|
183
|
+
let dx = 0;
|
|
184
|
+
if (drag.y < bodyTop + AUTO_SCROLL_EDGE) dy = -autoScrollStep(bodyTop + AUTO_SCROLL_EDGE - drag.y);
|
|
185
|
+
else if (drag.y > bodyBottom - AUTO_SCROLL_EDGE) dy = autoScrollStep(drag.y - (bodyBottom - AUTO_SCROLL_EDGE));
|
|
186
|
+
if (drag.x < bodyLeft + AUTO_SCROLL_EDGE) dx = -autoScrollStep(bodyLeft + AUTO_SCROLL_EDGE - drag.x);
|
|
187
|
+
else if (drag.x > bodyRight - AUTO_SCROLL_EDGE) dx = autoScrollStep(drag.x - (bodyRight - AUTO_SCROLL_EDGE));
|
|
188
|
+
const canScrollY = (dy < 0 && viewport.scrollTop > 0) || (dy > 0 && viewport.scrollTop + height < viewport.scrollHeight - 1);
|
|
189
|
+
// scrollLeft runs from 0 to the maximum, or from minus the maximum to 0 right to left.
|
|
190
|
+
const maxScroll = viewport.scrollWidth - width;
|
|
191
|
+
const minLeft = rtl ? -maxScroll : 0;
|
|
192
|
+
const canScrollX = (dx < 0 && viewport.scrollLeft > minLeft) || (dx > 0 && viewport.scrollLeft < minLeft + maxScroll - 1);
|
|
193
|
+
if (!canScrollY && !canScrollX) return;
|
|
194
|
+
if (canScrollY) viewport.scrollTop += dy;
|
|
195
|
+
if (canScrollX) viewport.scrollLeft += dx;
|
|
196
|
+
// Keep going while the pointer rests in the edge zone; the next frame picks the new head.
|
|
197
|
+
drag.frame = requestAnimationFrame(() => runDragFrameRef.current(true));
|
|
198
|
+
}, [latestRef, headAtPoint, headRowAtOffset, setRange]);
|
|
199
|
+
useLayoutEffect(() => {
|
|
200
|
+
runDragFrameRef.current = runDragFrame;
|
|
201
|
+
}, [runDragFrame]);
|
|
202
|
+
|
|
203
|
+
const startDrag = useCallback((
|
|
204
|
+
anchor: GridCellCoordinates,
|
|
205
|
+
head: GridCellCoordinates,
|
|
206
|
+
x: number,
|
|
207
|
+
y: number,
|
|
208
|
+
doc: Document,
|
|
209
|
+
tracked?: Pick<GridTrackedDragOptions, 'pointerId' | 'onTrack' | 'onEnd'>,
|
|
210
|
+
) => {
|
|
211
|
+
dragRef.current?.stop();
|
|
212
|
+
// A range drag follows the mouse; a tracked drag (fill handle) follows its pointer, touch included.
|
|
213
|
+
const moveType = tracked ? 'pointermove' : 'mousemove';
|
|
214
|
+
const upType = tracked ? 'pointerup' : 'mouseup';
|
|
215
|
+
const isOwnEvent = (event: MouseEvent) => !tracked || (event as PointerEvent).pointerId === tracked.pointerId;
|
|
216
|
+
const onMove = (event: MouseEvent) => {
|
|
217
|
+
const drag = dragRef.current;
|
|
218
|
+
if (!drag || !isOwnEvent(event)) return;
|
|
219
|
+
if (event.buttons === 0) {
|
|
220
|
+
// Released outside the window.
|
|
221
|
+
drag.stop();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
drag.x = event.clientX;
|
|
225
|
+
drag.y = event.clientY;
|
|
226
|
+
// The native text selection started by the press would otherwise follow the pointer out
|
|
227
|
+
// of the grid (WebKit), and a page text selection makes Ctrl/Cmd+C leave the copy to the browser.
|
|
228
|
+
if (drag.moved) {
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
clearTextSelection(doc);
|
|
231
|
+
}
|
|
232
|
+
if (!drag.frame) drag.frame = requestAnimationFrame(() => runDragFrame(true));
|
|
233
|
+
};
|
|
234
|
+
const onUp = (event: MouseEvent) => {
|
|
235
|
+
const drag = dragRef.current;
|
|
236
|
+
if (!drag || !isOwnEvent(event)) return;
|
|
237
|
+
drag.x = event.clientX;
|
|
238
|
+
drag.y = event.clientY;
|
|
239
|
+
runDragFrame(false);
|
|
240
|
+
const moved = drag.moved;
|
|
241
|
+
drag.stop(false, event.altKey);
|
|
242
|
+
if (moved) clearTextSelection(doc);
|
|
243
|
+
if (moved) {
|
|
244
|
+
// The click that follows lands on the common ancestor of both cells (often the row):
|
|
245
|
+
// it must not select the row or move focus.
|
|
246
|
+
suppressClickRef.current = true;
|
|
247
|
+
setTimeout(() => { suppressClickRef.current = false; }, 0);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const onCancel = (event: Event) => {
|
|
251
|
+
if (isOwnEvent(event as MouseEvent)) dragRef.current?.stop();
|
|
252
|
+
};
|
|
253
|
+
// Escape abandons a tracked drag (the fill writes nothing).
|
|
254
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
255
|
+
if (event.key !== 'Escape' || !dragRef.current) return;
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
dragRef.current.stop();
|
|
259
|
+
};
|
|
260
|
+
const view = doc.defaultView;
|
|
261
|
+
const onBlur = () => dragRef.current?.stop();
|
|
262
|
+
let ended = false;
|
|
263
|
+
const drag: DragState = {
|
|
264
|
+
anchor, head, x, y, frame: 0, moved: false,
|
|
265
|
+
onTrack: tracked?.onTrack,
|
|
266
|
+
stop: (cancelled = true, altKey = false) => {
|
|
267
|
+
if (drag.frame) cancelAnimationFrame(drag.frame);
|
|
268
|
+
drag.frame = 0;
|
|
269
|
+
doc.removeEventListener(moveType, onMove, true);
|
|
270
|
+
doc.removeEventListener(upType, onUp, true);
|
|
271
|
+
if (tracked) {
|
|
272
|
+
doc.removeEventListener('pointercancel', onCancel, true);
|
|
273
|
+
doc.removeEventListener('keydown', onKeyDown, true);
|
|
274
|
+
}
|
|
275
|
+
view?.removeEventListener('blur', onBlur);
|
|
276
|
+
latestRef.current.containerRef.current?.classList.remove(RANGE_DRAGGING_CLASS);
|
|
277
|
+
if (dragRef.current === drag) dragRef.current = null;
|
|
278
|
+
if (!ended) {
|
|
279
|
+
ended = true;
|
|
280
|
+
if (tracked) tracked.onEnd({ cancelled, altKey });
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
dragRef.current = drag;
|
|
285
|
+
doc.addEventListener(moveType, onMove, true);
|
|
286
|
+
doc.addEventListener(upType, onUp, true);
|
|
287
|
+
if (tracked) {
|
|
288
|
+
doc.addEventListener('pointercancel', onCancel, true);
|
|
289
|
+
doc.addEventListener('keydown', onKeyDown, true);
|
|
290
|
+
}
|
|
291
|
+
view?.addEventListener('blur', onBlur);
|
|
292
|
+
}, [latestRef, runDragFrame]);
|
|
293
|
+
|
|
294
|
+
const startTrackedDrag = useCallback((options: GridTrackedDragOptions) => {
|
|
295
|
+
const viewport = latestRef.current.viewportRef.current;
|
|
296
|
+
if (!viewport || !latestRef.current.enabled) return;
|
|
297
|
+
const { start, x, y, pointerId, onTrack, onEnd } = options;
|
|
298
|
+
startDrag(start, start, x, y, viewport.ownerDocument, { pointerId, onTrack, onEnd });
|
|
299
|
+
}, [latestRef, startDrag]);
|
|
300
|
+
|
|
301
|
+
const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
302
|
+
if (event.button !== 0 || event.defaultPrevented) return;
|
|
303
|
+
const { context: ctx } = latestRef.current;
|
|
304
|
+
if (!ctx) return;
|
|
305
|
+
const viewport = event.currentTarget;
|
|
306
|
+
const hit = cellFromTarget(event.target, viewport);
|
|
307
|
+
if (!hit || !isRangeColumnField(hit.field)) return;
|
|
308
|
+
// The fill handle starts its own drag (useGridFillHandle).
|
|
309
|
+
if ((event.target as Element).closest?.('.ogx__cell-fill-handle')) return;
|
|
310
|
+
// Typing targets and open editors keep the pointer to themselves.
|
|
311
|
+
if (hit.element.classList.contains('ogx__cell--editing') || isTextEntryElement(event.target as Element)) return;
|
|
312
|
+
const id = ctx.rowIds[hit.rowIndex];
|
|
313
|
+
if (id === undefined) return;
|
|
314
|
+
const cell = { id, field: hit.field };
|
|
315
|
+
|
|
316
|
+
let anchor: GridCellCoordinates = cell;
|
|
317
|
+
if (event.shiftKey) {
|
|
318
|
+
const current = currentModel()[0];
|
|
319
|
+
const focus = focusRef.current;
|
|
320
|
+
if (current && ctx.rowIndexById.has(current.anchor.id) && ctx.colIndexByField.has(current.anchor.field)) anchor = current.anchor;
|
|
321
|
+
else if (focus && focus.id !== null && isRangeColumnField(focus.field)) anchor = { id: focus.id, field: focus.field };
|
|
322
|
+
// Shift+click extends the range: no text selection, no focus move and no click (row
|
|
323
|
+
// selection, onCellClick) afterwards.
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
suppressClickRef.current = true;
|
|
326
|
+
const doc = viewport.ownerDocument;
|
|
327
|
+
if (!viewport.contains(doc.activeElement)) viewport.focus({ preventScroll: true });
|
|
328
|
+
}
|
|
329
|
+
setRange(anchor, cell, 'pointer');
|
|
330
|
+
startDrag(anchor, cell, event.clientX, event.clientY, viewport.ownerDocument);
|
|
331
|
+
if (event.shiftKey) {
|
|
332
|
+
const drag = dragRef.current;
|
|
333
|
+
if (drag && !isSameCoordinates(anchor, cell)) drag.moved = true;
|
|
334
|
+
}
|
|
335
|
+
}, [focusRef, latestRef, currentModel, setRange, startDrag]);
|
|
336
|
+
|
|
337
|
+
const handleClickCapture = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
338
|
+
if (!suppressClickRef.current) return;
|
|
339
|
+
suppressClickRef.current = false;
|
|
340
|
+
event.stopPropagation();
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
344
|
+
return { startTrackedDrag, handleMouseDown, handleClickCapture };
|
|
345
|
+
}
|