@opencorestack/opengridx 3.9.1 → 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 +20 -0
- 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/features/ai-toolkit.md +1 -0
- package/docs/performance.md +1 -1
- package/docs/roadmap.md +5 -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 +4 -2
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
2
9
|
import type { RefObject } from 'react';
|
|
3
10
|
import { getRowIndex } from '../../utils/rowIndex';
|
|
4
11
|
import { scrollColumnIntoView, scrollRowIntoView } from '../../utils/scroll';
|
|
5
|
-
import { isRtl } from '../../utils/direction';
|
|
6
12
|
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
7
|
-
import { isTextEntryElement } from '../../utils/focus';
|
|
8
13
|
import type { FocusedCell } from '../../utils/focus';
|
|
9
14
|
import { getCellValue } from '../../utils/values';
|
|
10
15
|
import { AGGREGATION_FUNCTIONS } from '../../utils/aggregation';
|
|
@@ -18,7 +23,12 @@ import {
|
|
|
18
23
|
rectCellCount,
|
|
19
24
|
resolveCellRange,
|
|
20
25
|
} from '../../utils/cellSelection';
|
|
21
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
CellRangeContext,
|
|
28
|
+
CellRangeRect,
|
|
29
|
+
CellRangeSpanLookup,
|
|
30
|
+
ResolvedCellRange,
|
|
31
|
+
} from '../../utils/cellSelection';
|
|
22
32
|
import { buildRangeTsv } from '../features/useGridClipboard';
|
|
23
33
|
import type { GridSpanningResult } from '../features/useGridSpanning';
|
|
24
34
|
import type { LayoutResult } from './useLayout';
|
|
@@ -34,13 +44,12 @@ import type {
|
|
|
34
44
|
GridRowModel,
|
|
35
45
|
GridSelectedCell,
|
|
36
46
|
} from '../../types';
|
|
47
|
+
import { useGridCellRangeDrag } from './useGridCellRangeDrag';
|
|
37
48
|
|
|
38
49
|
/** Root class while a pointer drag selects cells: no text selection meanwhile. Public from v3.3. */
|
|
39
50
|
export const RANGE_DRAGGING_CLASS = 'ogx--range-dragging';
|
|
40
51
|
|
|
41
52
|
/** Pointer distance (px) from a body edge at which a drag starts scrolling. */
|
|
42
|
-
const AUTO_SCROLL_EDGE = 32;
|
|
43
|
-
const AUTO_SCROLL_MAX_STEP = 40;
|
|
44
53
|
const ANNOUNCEMENT_DELAY_MS = 300;
|
|
45
54
|
|
|
46
55
|
/** The rectangle in the coordinates rows render with: row indices of the renderable rows, data column indices. */
|
|
@@ -127,24 +136,6 @@ export interface UseGridCellSelectionReturn {
|
|
|
127
136
|
startTrackedDrag: (options: GridTrackedDragOptions) => void;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
interface DragState {
|
|
131
|
-
anchor: GridCellCoordinates;
|
|
132
|
-
head: GridCellCoordinates;
|
|
133
|
-
x: number;
|
|
134
|
-
y: number;
|
|
135
|
-
frame: number;
|
|
136
|
-
moved: boolean;
|
|
137
|
-
/**
|
|
138
|
-
* Whether the viewport lays out right to left, read once per drag: `getComputedStyle` every frame
|
|
139
|
-
* forces a style recalculation, which slowed WebKit's auto-scroll enough to delay the release.
|
|
140
|
-
*/
|
|
141
|
-
rtl?: boolean;
|
|
142
|
-
/** Ends the drag; `cancelled` unless the button was released normally (tracked drags: Escape cancels). */
|
|
143
|
-
stop: (cancelled?: boolean, altKey?: boolean) => void;
|
|
144
|
-
/** A tracked drag (fill handle): told the cell under the pointer each frame instead of moving the range. */
|
|
145
|
-
onTrack?: (head: GridCellCoordinates, x: number, y: number) => void;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
/** A drag that reuses the range drag's hit testing and auto-scroll without selecting (the fill handle, v3.5). */
|
|
149
140
|
export interface GridTrackedDragOptions {
|
|
150
141
|
/** The cell the drag starts on. */
|
|
@@ -159,29 +150,6 @@ export interface GridTrackedDragOptions {
|
|
|
159
150
|
onEnd: (details: { cancelled: boolean; altKey: boolean }) => void;
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
/** The body cell of `viewport` holding `target`: its row index and field. Not cells of nested grids or detail panels. */
|
|
163
|
-
function cellFromTarget(target: EventTarget | null, viewport: HTMLElement): { rowIndex: number; field: string; element: HTMLElement } | null {
|
|
164
|
-
if (!target || (target as Node).nodeType !== 1) return null;
|
|
165
|
-
const element = (target as Element).closest<HTMLElement>('.ogx__cell[data-field]');
|
|
166
|
-
if (!element || element.closest('.ogx__viewport') !== viewport) return null;
|
|
167
|
-
const panel = element.closest('.ogx__detail-panel');
|
|
168
|
-
if (panel && viewport.contains(panel)) return null;
|
|
169
|
-
const rowElement = element.closest('[role="row"][data-rowindex]');
|
|
170
|
-
if (!rowElement) return null;
|
|
171
|
-
const rowIndex = Number(rowElement.getAttribute('data-rowindex'));
|
|
172
|
-
const field = element.dataset.field;
|
|
173
|
-
if (!Number.isInteger(rowIndex) || !field) return null;
|
|
174
|
-
return { rowIndex, field, element };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function clearTextSelection(doc: Document): void {
|
|
178
|
-
const selection = doc.getSelection();
|
|
179
|
-
if (selection && selection.rangeCount > 0) selection.removeAllRanges();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
|
|
183
|
-
const autoScrollStep = (over: number) => Math.min(AUTO_SCROLL_MAX_STEP, Math.max(2, Math.round(over / 2)));
|
|
184
|
-
|
|
185
153
|
function warnLargeCopy(cellCount: number): void {
|
|
186
154
|
if (process.env.NODE_ENV === 'production') return;
|
|
187
155
|
console.warn(
|
|
@@ -400,256 +368,8 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
400
368
|
return cells;
|
|
401
369
|
}, [currentRange]);
|
|
402
370
|
|
|
403
|
-
// ── Pointer: click, Shift+click and drag
|
|
404
|
-
const
|
|
405
|
-
const suppressClickRef = useRef(false);
|
|
406
|
-
|
|
407
|
-
useEffect(() => () => dragRef.current?.stop(), []);
|
|
408
|
-
|
|
409
|
-
const headAtPoint = useCallback((viewport: HTMLElement, x: number, y: number): GridCellCoordinates | null => {
|
|
410
|
-
const ctx = latestRef.current.context;
|
|
411
|
-
if (!ctx) return null;
|
|
412
|
-
const doc = viewport.ownerDocument;
|
|
413
|
-
// jsdom has no elementFromPoint.
|
|
414
|
-
if (typeof doc.elementFromPoint !== 'function') return null;
|
|
415
|
-
const hit = cellFromTarget(doc.elementFromPoint(x, y), viewport);
|
|
416
|
-
if (!hit) return null;
|
|
417
|
-
const id = ctx.rowIds[hit.rowIndex];
|
|
418
|
-
if (id === undefined) return null;
|
|
419
|
-
// Over the system or grouping column (left of the data): the first data column.
|
|
420
|
-
const field = isRangeColumnField(hit.field) ? hit.field : ctx.fields[0];
|
|
421
|
-
return field === undefined ? null : { id, field };
|
|
422
|
-
}, []);
|
|
423
|
-
|
|
424
|
-
/** The row whose band holds `centerY` (px from the top of the unpinned rows), with `field`. */
|
|
425
|
-
const headRowAtOffset = useCallback((centerY: number, field: string): GridCellCoordinates | null => {
|
|
426
|
-
const { context: ctx, layout, pinnedTopRowCount } = latestRef.current;
|
|
427
|
-
const heights = layout.cumulativeHeights;
|
|
428
|
-
if (!ctx || heights.length === 0 || centerY < 0) return null;
|
|
429
|
-
let lo = 0;
|
|
430
|
-
let hi = heights.length - 1;
|
|
431
|
-
while (lo < hi) {
|
|
432
|
-
const mid = (lo + hi) >> 1;
|
|
433
|
-
if (heights[mid] > centerY) hi = mid; else lo = mid + 1;
|
|
434
|
-
}
|
|
435
|
-
const id = ctx.rowIds[lo + pinnedTopRowCount];
|
|
436
|
-
return id === undefined ? null : { id, field };
|
|
437
|
-
}, []);
|
|
438
|
-
|
|
439
|
-
// The auto-scroll loop schedules the next frame through this ref: a callback cannot name itself.
|
|
440
|
-
const runDragFrameRef = useRef<(allowScroll: boolean) => void>(() => {});
|
|
441
|
-
const runDragFrame = useCallback((allowScroll: boolean) => {
|
|
442
|
-
const drag = dragRef.current;
|
|
443
|
-
const viewport = latestRef.current.viewportRef.current;
|
|
444
|
-
if (!drag || !viewport) return;
|
|
445
|
-
drag.frame = 0;
|
|
446
|
-
const { layout } = latestRef.current;
|
|
447
|
-
const box = viewport.getBoundingClientRect();
|
|
448
|
-
const width = viewport.clientWidth;
|
|
449
|
-
const height = viewport.clientHeight;
|
|
450
|
-
const stickyTop = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-top');
|
|
451
|
-
const stickyBottom = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-bottom');
|
|
452
|
-
const headerWrap = stickyTop?.querySelector(':scope > .ogx__header-wrap');
|
|
453
|
-
const bodyTop = stickyTop ? stickyTop.getBoundingClientRect().bottom : box.top;
|
|
454
|
-
const bodyBottom = Math.min(box.top + height, stickyBottom ? stickyBottom.getBoundingClientRect().top : box.top + height);
|
|
455
|
-
const headerBottom = headerWrap ? headerWrap.getBoundingClientRect().bottom : bodyTop;
|
|
456
|
-
// Left-pinned columns (and pinned system columns) sit at the inline start: on the right in a
|
|
457
|
-
// right-to-left grid, whose vertical scrollbar is on the left.
|
|
458
|
-
if (drag.rtl === undefined) drag.rtl = isRtl(viewport);
|
|
459
|
-
const rtl = drag.rtl;
|
|
460
|
-
const contentLeft = rtl ? box.right - width : box.left;
|
|
461
|
-
const startPinnedWidth = layout.pinnedSystemColumnsWidth + layout.leftWidth;
|
|
462
|
-
const bodyLeft = contentLeft + (rtl ? layout.rightWidth : startPinnedWidth);
|
|
463
|
-
const bodyRight = contentLeft + width - (rtl ? startPinnedWidth : layout.rightWidth);
|
|
464
|
-
|
|
465
|
-
// The cell under the pointer; outside the viewport (or over the header) the nearest body cell.
|
|
466
|
-
const x = clamp(drag.x, contentLeft + 1, contentLeft + width - 2);
|
|
467
|
-
let y = drag.y;
|
|
468
|
-
if (y < box.top || y > box.top + height || y < headerBottom) y = clamp(y, bodyTop + 1, Math.max(bodyTop + 1, bodyBottom - 2));
|
|
469
|
-
// During fast auto-scroll the rows under the pointer may not be rendered yet (virtualization
|
|
470
|
-
// lags a frame or more on a slow machine), so the hit test finds no cell. Work out the row from
|
|
471
|
-
// the scroll position and the row heights instead, keeping the column the head already has.
|
|
472
|
-
const canHitTest = typeof viewport.ownerDocument.elementFromPoint === 'function';
|
|
473
|
-
const hit = headAtPoint(viewport, x, y);
|
|
474
|
-
const head = hit ?? (canHitTest && drag.moved
|
|
475
|
-
? headRowAtOffset(y - box.top - (stickyTop ? stickyTop.getBoundingClientRect().height : 0) + viewport.scrollTop, drag.head.field)
|
|
476
|
-
: null);
|
|
477
|
-
if (head && !isSameCoordinates(head, drag.head)) {
|
|
478
|
-
drag.head = head;
|
|
479
|
-
if (!drag.moved && !isSameCoordinates(head, drag.anchor)) {
|
|
480
|
-
drag.moved = true;
|
|
481
|
-
latestRef.current.containerRef.current?.classList.add(RANGE_DRAGGING_CLASS);
|
|
482
|
-
clearTextSelection(viewport.ownerDocument);
|
|
483
|
-
}
|
|
484
|
-
if (!drag.onTrack) setRange(drag.anchor, head, 'pointer');
|
|
485
|
-
}
|
|
486
|
-
// A tracked drag hears every frame: its target can change while the head stays in one cell.
|
|
487
|
-
if (drag.onTrack) drag.onTrack(drag.head, drag.x, drag.y);
|
|
488
|
-
|
|
489
|
-
if (!allowScroll) return;
|
|
490
|
-
const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < contentLeft || drag.x > contentLeft + width;
|
|
491
|
-
if (!drag.moved && !outside) return;
|
|
492
|
-
let dy = 0;
|
|
493
|
-
let dx = 0;
|
|
494
|
-
if (drag.y < bodyTop + AUTO_SCROLL_EDGE) dy = -autoScrollStep(bodyTop + AUTO_SCROLL_EDGE - drag.y);
|
|
495
|
-
else if (drag.y > bodyBottom - AUTO_SCROLL_EDGE) dy = autoScrollStep(drag.y - (bodyBottom - AUTO_SCROLL_EDGE));
|
|
496
|
-
if (drag.x < bodyLeft + AUTO_SCROLL_EDGE) dx = -autoScrollStep(bodyLeft + AUTO_SCROLL_EDGE - drag.x);
|
|
497
|
-
else if (drag.x > bodyRight - AUTO_SCROLL_EDGE) dx = autoScrollStep(drag.x - (bodyRight - AUTO_SCROLL_EDGE));
|
|
498
|
-
const canScrollY = (dy < 0 && viewport.scrollTop > 0) || (dy > 0 && viewport.scrollTop + height < viewport.scrollHeight - 1);
|
|
499
|
-
// scrollLeft runs from 0 to the maximum, or from minus the maximum to 0 right to left.
|
|
500
|
-
const maxScroll = viewport.scrollWidth - width;
|
|
501
|
-
const minLeft = rtl ? -maxScroll : 0;
|
|
502
|
-
const canScrollX = (dx < 0 && viewport.scrollLeft > minLeft) || (dx > 0 && viewport.scrollLeft < minLeft + maxScroll - 1);
|
|
503
|
-
if (!canScrollY && !canScrollX) return;
|
|
504
|
-
if (canScrollY) viewport.scrollTop += dy;
|
|
505
|
-
if (canScrollX) viewport.scrollLeft += dx;
|
|
506
|
-
// Keep going while the pointer rests in the edge zone; the next frame picks the new head.
|
|
507
|
-
drag.frame = requestAnimationFrame(() => runDragFrameRef.current(true));
|
|
508
|
-
}, [headAtPoint, headRowAtOffset, setRange]);
|
|
509
|
-
useLayoutEffect(() => {
|
|
510
|
-
runDragFrameRef.current = runDragFrame;
|
|
511
|
-
}, [runDragFrame]);
|
|
512
|
-
|
|
513
|
-
const startDrag = useCallback((
|
|
514
|
-
anchor: GridCellCoordinates,
|
|
515
|
-
head: GridCellCoordinates,
|
|
516
|
-
x: number,
|
|
517
|
-
y: number,
|
|
518
|
-
doc: Document,
|
|
519
|
-
tracked?: Pick<GridTrackedDragOptions, 'pointerId' | 'onTrack' | 'onEnd'>,
|
|
520
|
-
) => {
|
|
521
|
-
dragRef.current?.stop();
|
|
522
|
-
// A range drag follows the mouse; a tracked drag (fill handle) follows its pointer, touch included.
|
|
523
|
-
const moveType = tracked ? 'pointermove' : 'mousemove';
|
|
524
|
-
const upType = tracked ? 'pointerup' : 'mouseup';
|
|
525
|
-
const isOwnEvent = (event: MouseEvent) => !tracked || (event as PointerEvent).pointerId === tracked.pointerId;
|
|
526
|
-
const onMove = (event: MouseEvent) => {
|
|
527
|
-
const drag = dragRef.current;
|
|
528
|
-
if (!drag || !isOwnEvent(event)) return;
|
|
529
|
-
if (event.buttons === 0) {
|
|
530
|
-
// Released outside the window.
|
|
531
|
-
drag.stop();
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
drag.x = event.clientX;
|
|
535
|
-
drag.y = event.clientY;
|
|
536
|
-
// The native text selection started by the press would otherwise follow the pointer out
|
|
537
|
-
// of the grid (WebKit), and a page text selection makes Ctrl/Cmd+C leave the copy to the browser.
|
|
538
|
-
if (drag.moved) {
|
|
539
|
-
event.preventDefault();
|
|
540
|
-
clearTextSelection(doc);
|
|
541
|
-
}
|
|
542
|
-
if (!drag.frame) drag.frame = requestAnimationFrame(() => runDragFrame(true));
|
|
543
|
-
};
|
|
544
|
-
const onUp = (event: MouseEvent) => {
|
|
545
|
-
const drag = dragRef.current;
|
|
546
|
-
if (!drag || !isOwnEvent(event)) return;
|
|
547
|
-
drag.x = event.clientX;
|
|
548
|
-
drag.y = event.clientY;
|
|
549
|
-
runDragFrame(false);
|
|
550
|
-
const moved = drag.moved;
|
|
551
|
-
drag.stop(false, event.altKey);
|
|
552
|
-
if (moved) clearTextSelection(doc);
|
|
553
|
-
if (moved) {
|
|
554
|
-
// The click that follows lands on the common ancestor of both cells (often the row):
|
|
555
|
-
// it must not select the row or move focus.
|
|
556
|
-
suppressClickRef.current = true;
|
|
557
|
-
setTimeout(() => { suppressClickRef.current = false; }, 0);
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
const onCancel = (event: Event) => {
|
|
561
|
-
if (isOwnEvent(event as MouseEvent)) dragRef.current?.stop();
|
|
562
|
-
};
|
|
563
|
-
// Escape abandons a tracked drag (the fill writes nothing).
|
|
564
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
565
|
-
if (event.key !== 'Escape' || !dragRef.current) return;
|
|
566
|
-
event.preventDefault();
|
|
567
|
-
event.stopPropagation();
|
|
568
|
-
dragRef.current.stop();
|
|
569
|
-
};
|
|
570
|
-
const view = doc.defaultView;
|
|
571
|
-
const onBlur = () => dragRef.current?.stop();
|
|
572
|
-
let ended = false;
|
|
573
|
-
const drag: DragState = {
|
|
574
|
-
anchor, head, x, y, frame: 0, moved: false,
|
|
575
|
-
onTrack: tracked?.onTrack,
|
|
576
|
-
stop: (cancelled = true, altKey = false) => {
|
|
577
|
-
if (drag.frame) cancelAnimationFrame(drag.frame);
|
|
578
|
-
drag.frame = 0;
|
|
579
|
-
doc.removeEventListener(moveType, onMove, true);
|
|
580
|
-
doc.removeEventListener(upType, onUp, true);
|
|
581
|
-
if (tracked) {
|
|
582
|
-
doc.removeEventListener('pointercancel', onCancel, true);
|
|
583
|
-
doc.removeEventListener('keydown', onKeyDown, true);
|
|
584
|
-
}
|
|
585
|
-
view?.removeEventListener('blur', onBlur);
|
|
586
|
-
latestRef.current.containerRef.current?.classList.remove(RANGE_DRAGGING_CLASS);
|
|
587
|
-
if (dragRef.current === drag) dragRef.current = null;
|
|
588
|
-
if (!ended) {
|
|
589
|
-
ended = true;
|
|
590
|
-
if (tracked) tracked.onEnd({ cancelled, altKey });
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
};
|
|
594
|
-
dragRef.current = drag;
|
|
595
|
-
doc.addEventListener(moveType, onMove, true);
|
|
596
|
-
doc.addEventListener(upType, onUp, true);
|
|
597
|
-
if (tracked) {
|
|
598
|
-
doc.addEventListener('pointercancel', onCancel, true);
|
|
599
|
-
doc.addEventListener('keydown', onKeyDown, true);
|
|
600
|
-
}
|
|
601
|
-
view?.addEventListener('blur', onBlur);
|
|
602
|
-
}, [runDragFrame]);
|
|
603
|
-
|
|
604
|
-
const startTrackedDrag = useCallback((options: GridTrackedDragOptions) => {
|
|
605
|
-
const viewport = latestRef.current.viewportRef.current;
|
|
606
|
-
if (!viewport || !latestRef.current.enabled) return;
|
|
607
|
-
const { start, x, y, pointerId, onTrack, onEnd } = options;
|
|
608
|
-
startDrag(start, start, x, y, viewport.ownerDocument, { pointerId, onTrack, onEnd });
|
|
609
|
-
}, [startDrag]);
|
|
610
|
-
|
|
611
|
-
const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
612
|
-
if (event.button !== 0 || event.defaultPrevented) return;
|
|
613
|
-
const { context: ctx } = latestRef.current;
|
|
614
|
-
if (!ctx) return;
|
|
615
|
-
const viewport = event.currentTarget;
|
|
616
|
-
const hit = cellFromTarget(event.target, viewport);
|
|
617
|
-
if (!hit || !isRangeColumnField(hit.field)) return;
|
|
618
|
-
// The fill handle starts its own drag (useGridFillHandle).
|
|
619
|
-
if ((event.target as Element).closest?.('.ogx__cell-fill-handle')) return;
|
|
620
|
-
// Typing targets and open editors keep the pointer to themselves.
|
|
621
|
-
if (hit.element.classList.contains('ogx__cell--editing') || isTextEntryElement(event.target as Element)) return;
|
|
622
|
-
const id = ctx.rowIds[hit.rowIndex];
|
|
623
|
-
if (id === undefined) return;
|
|
624
|
-
const cell = { id, field: hit.field };
|
|
625
|
-
|
|
626
|
-
let anchor: GridCellCoordinates = cell;
|
|
627
|
-
if (event.shiftKey) {
|
|
628
|
-
const current = currentModel()[0];
|
|
629
|
-
const focus = focusRef.current;
|
|
630
|
-
if (current && ctx.rowIndexById.has(current.anchor.id) && ctx.colIndexByField.has(current.anchor.field)) anchor = current.anchor;
|
|
631
|
-
else if (focus && focus.id !== null && isRangeColumnField(focus.field)) anchor = { id: focus.id, field: focus.field };
|
|
632
|
-
// Shift+click extends the range: no text selection, no focus move and no click (row
|
|
633
|
-
// selection, onCellClick) afterwards.
|
|
634
|
-
event.preventDefault();
|
|
635
|
-
suppressClickRef.current = true;
|
|
636
|
-
const doc = viewport.ownerDocument;
|
|
637
|
-
if (!viewport.contains(doc.activeElement)) viewport.focus({ preventScroll: true });
|
|
638
|
-
}
|
|
639
|
-
setRange(anchor, cell, 'pointer');
|
|
640
|
-
startDrag(anchor, cell, event.clientX, event.clientY, viewport.ownerDocument);
|
|
641
|
-
if (event.shiftKey) {
|
|
642
|
-
const drag = dragRef.current;
|
|
643
|
-
if (drag && !isSameCoordinates(anchor, cell)) drag.moved = true;
|
|
644
|
-
}
|
|
645
|
-
}, [currentModel, setRange, startDrag]);
|
|
646
|
-
|
|
647
|
-
const handleClickCapture = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
648
|
-
if (!suppressClickRef.current) return;
|
|
649
|
-
suppressClickRef.current = false;
|
|
650
|
-
event.stopPropagation();
|
|
651
|
-
event.preventDefault();
|
|
652
|
-
}, []);
|
|
371
|
+
// ── Pointer: click, Shift+click and drag (useGridCellRangeDrag) ──────────────
|
|
372
|
+
const { startTrackedDrag, handleMouseDown, handleClickCapture } = useGridCellRangeDrag({ latestRef, focusRef, setRange, currentModel });
|
|
653
373
|
|
|
654
374
|
// ── Live region and status bar ──────────────────────────────────────────
|
|
655
375
|
const columnCount = resolved ? resolved.right - resolved.left + 1 : 0;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { GridColDef, GridRowModel } from '../../types';
|
|
3
3
|
import { buildColumnLookup, type GridColumnLookup } from '../../utils/columnLookup';
|
|
4
|
+
import { untypedColumns } from '../../utils/untypedColumns';
|
|
4
5
|
|
|
5
6
|
const SEPARATOR = '\u0000';
|
|
6
7
|
|
|
@@ -25,6 +26,6 @@ export function useGridColumnLookup<R extends GridRowModel>(
|
|
|
25
26
|
if (hiddenFieldsKey) {
|
|
26
27
|
for (const field of hiddenFieldsKey.split(SEPARATOR)) hidden[field] = false;
|
|
27
28
|
}
|
|
28
|
-
return buildColumnLookup(columns
|
|
29
|
+
return buildColumnLookup(untypedColumns(columns), hidden);
|
|
29
30
|
}, [columns, hiddenFieldsKey]);
|
|
30
31
|
}
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
GridAggregationModel,
|
|
16
16
|
} from '../../types';
|
|
17
17
|
import type { GridInitialState } from '../../state/types';
|
|
18
|
+
import { untypedColumn, untypedColumns } from '../../utils/untypedColumns';
|
|
18
19
|
|
|
19
20
|
interface HierarchyHandlers {
|
|
20
21
|
toggleExpansion: (id: GridRowId) => void;
|
|
@@ -334,7 +335,7 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
334
335
|
}, []);
|
|
335
336
|
|
|
336
337
|
useEffect(() => {
|
|
337
|
-
setColumns(activeColumns
|
|
338
|
+
setColumns(untypedColumns(activeColumns));
|
|
338
339
|
}, [activeColumns, setColumns]);
|
|
339
340
|
|
|
340
341
|
// ── Ordered / visible columns ─────────────────────────────────────────────
|
|
@@ -370,7 +371,7 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
370
371
|
const [movedField] = newOrder.splice(oldIndex, 1);
|
|
371
372
|
newOrder.splice(targetIndex, 0, movedField);
|
|
372
373
|
commitColumnOrder(newOrder, {
|
|
373
|
-
column: orderedColumns[oldIndex]
|
|
374
|
+
column: untypedColumn(orderedColumns[oldIndex]),
|
|
374
375
|
oldIndex,
|
|
375
376
|
targetIndex,
|
|
376
377
|
});
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from '../../types';
|
|
15
15
|
import type { GridInitialState, GridDensityState } from '../../state/types';
|
|
16
16
|
import { EMPTY_ROW_GROUPING_MODEL } from '../../utils/gridModes';
|
|
17
|
+
import { selectionSetOf } from '../../utils/selectionSets';
|
|
17
18
|
|
|
18
19
|
export type GridDensity = GridDensityState['density'];
|
|
19
20
|
|
|
@@ -96,7 +97,7 @@ export interface UseGridControlledStateReturn {
|
|
|
96
97
|
|
|
97
98
|
// selection
|
|
98
99
|
rowSelectionModel: GridRowId[];
|
|
99
|
-
selectedRowIds:
|
|
100
|
+
selectedRowIds: ReadonlySet<GridRowId>;
|
|
100
101
|
isSelectionControlled: boolean;
|
|
101
102
|
setInternalRowSelectionModel: React.Dispatch<React.SetStateAction<GridRowId[]>>;
|
|
102
103
|
/** Sets the internal model when uncontrolled and fires onRowSelectionModelChange. */
|
|
@@ -267,7 +268,7 @@ export function useGridControlledState(params: UseGridControlledStateParams): Us
|
|
|
267
268
|
const isSelectionControlled = propRowSelectionModel !== undefined;
|
|
268
269
|
const [internalRowSelectionModel, setInternalRowSelectionModel] = useState<GridRowId[]>([]);
|
|
269
270
|
const rowSelectionModel = isSelectionControlled ? propRowSelectionModel! : internalRowSelectionModel;
|
|
270
|
-
const selectedRowIds = useMemo(() =>
|
|
271
|
+
const selectedRowIds = useMemo(() => selectionSetOf(rowSelectionModel), [rowSelectionModel]);
|
|
271
272
|
|
|
272
273
|
// Cleared on every commit: the rendered model is current again (a controlled parent may
|
|
273
274
|
// also have rejected the change).
|