@alaarab/ogrid-react 2.1.3 → 2.1.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.
Files changed (72) hide show
  1. package/dist/esm/index.js +7233 -26
  2. package/package.json +7 -4
  3. package/dist/esm/components/BaseColumnHeaderMenu.js +0 -78
  4. package/dist/esm/components/BaseDropIndicator.js +0 -4
  5. package/dist/esm/components/BaseEmptyState.js +0 -4
  6. package/dist/esm/components/BaseInlineCellEditor.js +0 -167
  7. package/dist/esm/components/BaseLoadingOverlay.js +0 -4
  8. package/dist/esm/components/CellErrorBoundary.js +0 -43
  9. package/dist/esm/components/ColumnChooserProps.js +0 -6
  10. package/dist/esm/components/ColumnHeaderFilterContent.js +0 -33
  11. package/dist/esm/components/ColumnHeaderFilterRenderers.js +0 -67
  12. package/dist/esm/components/EmptyState.js +0 -19
  13. package/dist/esm/components/GridContextMenu.js +0 -35
  14. package/dist/esm/components/MarchingAntsOverlay.js +0 -90
  15. package/dist/esm/components/OGridLayout.js +0 -136
  16. package/dist/esm/components/PaginationControlsProps.js +0 -6
  17. package/dist/esm/components/SideBar.js +0 -123
  18. package/dist/esm/components/StatusBar.js +0 -6
  19. package/dist/esm/components/createOGrid.js +0 -19
  20. package/dist/esm/constants/domHelpers.js +0 -16
  21. package/dist/esm/hooks/index.js +0 -43
  22. package/dist/esm/hooks/useActiveCell.js +0 -75
  23. package/dist/esm/hooks/useCellEditing.js +0 -15
  24. package/dist/esm/hooks/useCellSelection.js +0 -389
  25. package/dist/esm/hooks/useClipboard.js +0 -106
  26. package/dist/esm/hooks/useColumnChooserState.js +0 -74
  27. package/dist/esm/hooks/useColumnHeaderFilterState.js +0 -191
  28. package/dist/esm/hooks/useColumnHeaderMenuState.js +0 -106
  29. package/dist/esm/hooks/useColumnMeta.js +0 -61
  30. package/dist/esm/hooks/useColumnPinning.js +0 -67
  31. package/dist/esm/hooks/useColumnReorder.js +0 -143
  32. package/dist/esm/hooks/useColumnResize.js +0 -127
  33. package/dist/esm/hooks/useContextMenu.js +0 -21
  34. package/dist/esm/hooks/useDataGridContextMenu.js +0 -24
  35. package/dist/esm/hooks/useDataGridEditing.js +0 -56
  36. package/dist/esm/hooks/useDataGridInteraction.js +0 -109
  37. package/dist/esm/hooks/useDataGridLayout.js +0 -172
  38. package/dist/esm/hooks/useDataGridState.js +0 -169
  39. package/dist/esm/hooks/useDataGridTableOrchestration.js +0 -199
  40. package/dist/esm/hooks/useDateFilterState.js +0 -34
  41. package/dist/esm/hooks/useDebounce.js +0 -35
  42. package/dist/esm/hooks/useFillHandle.js +0 -200
  43. package/dist/esm/hooks/useFilterOptions.js +0 -55
  44. package/dist/esm/hooks/useInlineCellEditorState.js +0 -38
  45. package/dist/esm/hooks/useKeyboardNavigation.js +0 -261
  46. package/dist/esm/hooks/useLatestRef.js +0 -11
  47. package/dist/esm/hooks/useListVirtualizer.js +0 -29
  48. package/dist/esm/hooks/useMultiSelectFilterState.js +0 -59
  49. package/dist/esm/hooks/useOGrid.js +0 -371
  50. package/dist/esm/hooks/useOGridDataFetching.js +0 -74
  51. package/dist/esm/hooks/useOGridFilters.js +0 -59
  52. package/dist/esm/hooks/useOGridPagination.js +0 -24
  53. package/dist/esm/hooks/useOGridSorting.js +0 -24
  54. package/dist/esm/hooks/usePaginationControls.js +0 -16
  55. package/dist/esm/hooks/usePeopleFilterState.js +0 -73
  56. package/dist/esm/hooks/useRichSelectState.js +0 -60
  57. package/dist/esm/hooks/useRowSelection.js +0 -69
  58. package/dist/esm/hooks/useSelectState.js +0 -62
  59. package/dist/esm/hooks/useShallowEqualMemo.js +0 -14
  60. package/dist/esm/hooks/useSideBarState.js +0 -39
  61. package/dist/esm/hooks/useTableLayout.js +0 -69
  62. package/dist/esm/hooks/useTextFilterState.js +0 -25
  63. package/dist/esm/hooks/useUndoRedo.js +0 -84
  64. package/dist/esm/hooks/useVirtualScroll.js +0 -69
  65. package/dist/esm/storybook/index.js +0 -1
  66. package/dist/esm/storybook/mockData.js +0 -73
  67. package/dist/esm/types/columnTypes.js +0 -1
  68. package/dist/esm/types/dataGridTypes.js +0 -1
  69. package/dist/esm/types/index.js +0 -1
  70. package/dist/esm/utils/dataGridViewModel.js +0 -54
  71. package/dist/esm/utils/gridRowComparator.js +0 -2
  72. package/dist/esm/utils/index.js +0 -5
@@ -1,389 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from 'react';
2
- import { normalizeSelectionRange } from '../types';
3
- import { rangesEqual, computeAutoScrollSpeed } from '../utils';
4
- import { useLatestRef } from './useLatestRef';
5
- /** DOM attribute names used for drag-range highlighting (bypasses React). */
6
- const DRAG_ATTR = 'data-drag-range';
7
- const DRAG_ANCHOR_ATTR = 'data-drag-anchor';
8
- /** Auto-scroll config */
9
- const AUTO_SCROLL_EDGE = 40; // px from wrapper edge to trigger
10
- const AUTO_SCROLL_INTERVAL = 16; // ~60fps
11
- /**
12
- * Manages cell selection range with drag-to-select and select-all support.
13
- * @param params - Row/col counts, active cell setter, and wrapper ref for auto-scroll.
14
- * @returns Selection range, setters, mouse/keyboard handlers, and drag state.
15
- */
16
- export function useCellSelection(params) {
17
- const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
18
- // Use ref for colOffset to prevent drag restart mid-drag when colOffset changes
19
- const colOffsetRef = useLatestRef(colOffset);
20
- const [selectionRange, _setSelectionRange] = useState(null);
21
- const isDraggingRef = useRef(false);
22
- const [isDragging, setIsDragging] = useState(false);
23
- /** True once a mousemove has been seen during the current drag gesture. */
24
- const dragMovedRef = useRef(false);
25
- const dragStartRef = useRef(null);
26
- const rafRef = useRef(0);
27
- /** Live drag range kept in a ref — only committed to React state on mouseup. */
28
- const liveDragRangeRef = useRef(null);
29
- /** Auto-scroll interval during drag. */
30
- const autoScrollRef = useRef(null);
31
- // Ref mirror of selectionRange — lets handleCellMouseDown read current value
32
- // without adding selectionRange to its useCallback deps (keeps it stable).
33
- const selectionRangeRef = useRef(selectionRange);
34
- selectionRangeRef.current = selectionRange;
35
- // Deduplicating setter — skips re-render when the range hasn't actually changed.
36
- const setSelectionRange = useCallback((next) => {
37
- if (rangesEqual(selectionRangeRef.current, next))
38
- return;
39
- _setSelectionRange(next);
40
- }, []);
41
- const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
42
- // Only handle primary (left) button — let middle-click scroll and right-click context menu work natively
43
- if (e.button !== 0)
44
- return;
45
- const colOff = colOffsetRef.current;
46
- if (globalColIndex < colOff)
47
- return;
48
- // Prevent native text selection during cell drag
49
- e.preventDefault();
50
- const dataColIndex = globalColIndex - colOff;
51
- const currentRange = selectionRangeRef.current;
52
- if (e.shiftKey && currentRange != null) {
53
- setSelectionRange(normalizeSelectionRange({
54
- startRow: currentRange.startRow,
55
- startCol: currentRange.startCol,
56
- endRow: rowIndex,
57
- endCol: dataColIndex,
58
- }));
59
- setActiveCell({ rowIndex, columnIndex: globalColIndex });
60
- }
61
- else {
62
- dragStartRef.current = { row: rowIndex, col: dataColIndex };
63
- dragMovedRef.current = false;
64
- const initial = {
65
- startRow: rowIndex,
66
- startCol: dataColIndex,
67
- endRow: rowIndex,
68
- endCol: dataColIndex,
69
- };
70
- setSelectionRange(initial);
71
- liveDragRangeRef.current = initial;
72
- setActiveCell({ rowIndex, columnIndex: globalColIndex });
73
- // Mark drag as "started" but don't set isDragging state yet —
74
- // setIsDragging(true) is deferred to the first mousemove to avoid
75
- // a true→false toggle on simple clicks (which causes 2 extra renders).
76
- isDraggingRef.current = true;
77
- // Apply drag attrs immediately for the initial cell so the anchor styling shows
78
- // even before the first mousemove. This ensures instant visual feedback.
79
- setTimeout(() => applyDragAttrsRef.current?.(initial), 0);
80
- }
81
- }, [setActiveCell, colOffsetRef, setSelectionRange]);
82
- const handleSelectAllCells = useCallback(() => {
83
- if (rowCount === 0 || visibleColCount === 0)
84
- return;
85
- setSelectionRange({
86
- startRow: 0,
87
- startCol: 0,
88
- endRow: rowCount - 1,
89
- endCol: visibleColCount - 1,
90
- });
91
- setActiveCell({ rowIndex: 0, columnIndex: colOffsetRef.current });
92
- }, [rowCount, visibleColCount, setActiveCell, colOffsetRef, setSelectionRange]);
93
- /** Last known mouse position during drag — used by mouseUp to flush pending RAF work. */
94
- const lastMousePosRef = useRef(null);
95
- // Ref to expose applyDragAttrs outside useEffect so it can be called from mouseDown
96
- const applyDragAttrsRef = useRef(null);
97
- // Window mouse move/up for drag selection.
98
- // Performance: during drag, we update a ref + toggle DOM attributes via rAF.
99
- // React state is only committed on mouseup (single re-render instead of 60-120/s).
100
- useEffect(() => {
101
- /** Set of currently drag-marked HTMLElements — avoids O(n) full DOM scan on each frame. */
102
- const markedCells = new Set();
103
- /** Cell lookup index built on drag start — O(1) lookups per frame instead of querySelectorAll. */
104
- let cellIndex = null;
105
- /** Build cell lookup index from a single querySelectorAll scan. */
106
- const buildCellIndex = () => {
107
- const wrapper = wrapperRef.current;
108
- if (!wrapper)
109
- return;
110
- cellIndex = new Map();
111
- const cells = wrapper.querySelectorAll('[data-row-index][data-col-index]');
112
- for (let i = 0; i < cells.length; i++) {
113
- const el = cells[i];
114
- const r = el.getAttribute('data-row-index') ?? '';
115
- const c = el.getAttribute('data-col-index') ?? '';
116
- cellIndex.set(`${r},${c}`, el);
117
- }
118
- };
119
- /** Apply styling to a single in-range cell (attrs + box-shadow). */
120
- const styleCellInRange = (el, r, c, minR, maxR, minC, maxC, anchor) => {
121
- if (!el.hasAttribute(DRAG_ATTR))
122
- el.setAttribute(DRAG_ATTR, '');
123
- const isAnchor = anchor && r === anchor.row && c === anchor.col;
124
- if (isAnchor) {
125
- if (!el.hasAttribute(DRAG_ANCHOR_ATTR))
126
- el.setAttribute(DRAG_ANCHOR_ATTR, '');
127
- }
128
- else {
129
- if (el.hasAttribute(DRAG_ANCHOR_ATTR))
130
- el.removeAttribute(DRAG_ANCHOR_ATTR);
131
- }
132
- const shadows = [];
133
- if (r === minR)
134
- shadows.push('inset 0 2px 0 0 var(--ogrid-selection, #217346)');
135
- if (r === maxR)
136
- shadows.push('inset 0 -2px 0 0 var(--ogrid-selection, #217346)');
137
- if (c === minC)
138
- shadows.push('inset 2px 0 0 0 var(--ogrid-selection, #217346)');
139
- if (c === maxC)
140
- shadows.push('inset -2px 0 0 0 var(--ogrid-selection, #217346)');
141
- el.style.boxShadow = shadows.length > 0 ? shadows.join(', ') : '';
142
- markedCells.add(el);
143
- };
144
- /** Remove drag styling from a single cell. */
145
- const unstyleCell = (el) => {
146
- el.removeAttribute(DRAG_ATTR);
147
- el.removeAttribute(DRAG_ANCHOR_ATTR);
148
- el.style.boxShadow = '';
149
- };
150
- /** Toggle DRAG_ATTR on cells to show the range highlight via CSS.
151
- * Uses a cell index Map for O(1) lookups per cell in the range instead of scanning all cells.
152
- * Also sets edge box-shadows for a green border around the selection range,
153
- * and marks the anchor cell with DRAG_ANCHOR_ATTR (white background). */
154
- const applyDragAttrs = (range) => {
155
- const wrapper = wrapperRef.current;
156
- if (!wrapper)
157
- return;
158
- const minR = Math.min(range.startRow, range.endRow);
159
- const maxR = Math.max(range.startRow, range.endRow);
160
- const minC = Math.min(range.startCol, range.endCol);
161
- const maxC = Math.max(range.startCol, range.endCol);
162
- const anchor = dragStartRef.current;
163
- const colOff = colOffsetRef.current;
164
- // 1. Un-mark cells that are no longer in the new range (iterate the small set, not all DOM)
165
- for (const el of markedCells) {
166
- const r = parseInt(el.getAttribute('data-row-index') ?? '', 10);
167
- const c = parseInt(el.getAttribute('data-col-index') ?? '', 10) - colOff;
168
- const stillInRange = r >= minR && r <= maxR && c >= minC && c <= maxC;
169
- if (!stillInRange) {
170
- unstyleCell(el);
171
- markedCells.delete(el);
172
- }
173
- }
174
- // Build index on first call if not yet initialized
175
- if (!cellIndex)
176
- buildCellIndex();
177
- // 2. Look up only the cells in the new range — O(range size) via Map lookup.
178
- for (let r = minR; r <= maxR; r++) {
179
- for (let c = minC; c <= maxC; c++) {
180
- const key = `${r},${c + colOff}`;
181
- let el = cellIndex?.get(key);
182
- // Handle virtual scroll recycling — if element is stale, rebuild index once
183
- if (el && !el.isConnected) {
184
- buildCellIndex();
185
- el = cellIndex?.get(key);
186
- }
187
- if (el) {
188
- styleCellInRange(el, r, c, minR, maxR, minC, maxC, anchor);
189
- }
190
- }
191
- }
192
- };
193
- // Expose applyDragAttrs via ref so mouseDown can access it
194
- applyDragAttrsRef.current = applyDragAttrs;
195
- /** Clear all drag styling using the tracked set — O(marked) not O(all cells). */
196
- const clearDragAttrs = () => {
197
- for (const el of markedCells) {
198
- unstyleCell(el);
199
- }
200
- markedCells.clear();
201
- cellIndex = null;
202
- };
203
- /** Resolve mouse coordinates to a cell range (shared by RAF callback and mouseUp flush). */
204
- const resolveRange = (cx, cy) => {
205
- if (!dragStartRef.current)
206
- return null;
207
- const target = document.elementFromPoint(cx, cy);
208
- const cell = target?.closest?.('[data-row-index][data-col-index]');
209
- if (!cell)
210
- return null;
211
- const r = parseInt(cell.getAttribute('data-row-index') ?? '', 10);
212
- const c = parseInt(cell.getAttribute('data-col-index') ?? '', 10);
213
- const colOff = colOffsetRef.current;
214
- if (Number.isNaN(r) || Number.isNaN(c) || c < colOff)
215
- return null;
216
- const dataCol = c - colOff;
217
- const start = dragStartRef.current;
218
- return normalizeSelectionRange({
219
- startRow: start.row,
220
- startCol: start.col,
221
- endRow: r,
222
- endCol: dataCol,
223
- });
224
- };
225
- /** Start or update auto-scroll interval based on mouse position relative to wrapper edges. */
226
- const updateAutoScroll = () => {
227
- const wrapper = wrapperRef.current;
228
- const pos = lastMousePosRef.current;
229
- if (!wrapper || !pos || !isDraggingRef.current) {
230
- stopAutoScroll();
231
- return;
232
- }
233
- const rect = wrapper.getBoundingClientRect();
234
- let dx = 0;
235
- let dy = 0;
236
- if (pos.cy < rect.top + AUTO_SCROLL_EDGE) {
237
- dy = -computeAutoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - pos.cy);
238
- }
239
- else if (pos.cy > rect.bottom - AUTO_SCROLL_EDGE) {
240
- dy = computeAutoScrollSpeed(pos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
241
- }
242
- if (pos.cx < rect.left + AUTO_SCROLL_EDGE) {
243
- dx = -computeAutoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - pos.cx);
244
- }
245
- else if (pos.cx > rect.right - AUTO_SCROLL_EDGE) {
246
- dx = computeAutoScrollSpeed(pos.cx - (rect.right - AUTO_SCROLL_EDGE));
247
- }
248
- if (dx === 0 && dy === 0) {
249
- stopAutoScroll();
250
- return;
251
- }
252
- // Start interval if not already running
253
- if (!autoScrollRef.current) {
254
- autoScrollRef.current = setInterval(() => {
255
- const w = wrapperRef.current;
256
- const p = lastMousePosRef.current;
257
- if (!w || !p || !isDraggingRef.current) {
258
- stopAutoScroll();
259
- return;
260
- }
261
- const r = w.getBoundingClientRect();
262
- let sdx = 0;
263
- let sdy = 0;
264
- if (p.cy < r.top + AUTO_SCROLL_EDGE)
265
- sdy = -computeAutoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
266
- else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
267
- sdy = computeAutoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
268
- if (p.cx < r.left + AUTO_SCROLL_EDGE)
269
- sdx = -computeAutoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
270
- else if (p.cx > r.right - AUTO_SCROLL_EDGE)
271
- sdx = computeAutoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
272
- if (sdx === 0 && sdy === 0) {
273
- stopAutoScroll();
274
- return;
275
- }
276
- w.scrollTop += sdy;
277
- w.scrollLeft += sdx;
278
- // After scrolling, re-resolve the cell under the mouse and update drag range
279
- const newRange = resolveRange(p.cx, p.cy);
280
- if (newRange) {
281
- liveDragRangeRef.current = newRange;
282
- applyDragAttrs(newRange);
283
- }
284
- }, AUTO_SCROLL_INTERVAL);
285
- }
286
- };
287
- const stopAutoScroll = () => {
288
- if (autoScrollRef.current) {
289
- clearInterval(autoScrollRef.current);
290
- autoScrollRef.current = null;
291
- }
292
- };
293
- const onMove = (e) => {
294
- if (!isDraggingRef.current || !dragStartRef.current)
295
- return;
296
- // Promote to a real drag on first mousemove (deferred from mouseDown
297
- // to avoid a true→false toggle on simple clicks).
298
- if (!dragMovedRef.current) {
299
- dragMovedRef.current = true;
300
- setIsDragging(true);
301
- // Build cell index once at drag start for O(1) lookups during drag
302
- buildCellIndex();
303
- }
304
- // Always store latest position so mouseUp can flush if RAF hasn't executed
305
- lastMousePosRef.current = { cx: e.clientX, cy: e.clientY };
306
- // Update auto-scroll based on mouse proximity to edges
307
- updateAutoScroll();
308
- // Cancel previous pending frame
309
- if (rafRef.current)
310
- cancelAnimationFrame(rafRef.current);
311
- rafRef.current = requestAnimationFrame(() => {
312
- rafRef.current = 0;
313
- const pos = lastMousePosRef.current;
314
- if (!pos)
315
- return;
316
- const newRange = resolveRange(pos.cx, pos.cy);
317
- if (!newRange)
318
- return;
319
- // Skip if range unchanged
320
- const prev = liveDragRangeRef.current;
321
- if (prev &&
322
- prev.startRow === newRange.startRow &&
323
- prev.startCol === newRange.startCol &&
324
- prev.endRow === newRange.endRow &&
325
- prev.endCol === newRange.endCol) {
326
- return;
327
- }
328
- liveDragRangeRef.current = newRange;
329
- // DOM-only highlighting — no React state update until mouseup
330
- applyDragAttrs(newRange);
331
- });
332
- };
333
- const onUp = () => {
334
- if (!isDraggingRef.current)
335
- return;
336
- stopAutoScroll();
337
- if (rafRef.current) {
338
- cancelAnimationFrame(rafRef.current);
339
- rafRef.current = 0;
340
- }
341
- isDraggingRef.current = false;
342
- const wasDrag = dragMovedRef.current;
343
- if (wasDrag) {
344
- // Flush: if the last RAF hasn't executed yet, resolve the range now from the
345
- // last known mouse position so the final committed range is always accurate.
346
- const pos = lastMousePosRef.current;
347
- if (pos) {
348
- const flushed = resolveRange(pos.cx, pos.cy);
349
- if (flushed)
350
- liveDragRangeRef.current = flushed;
351
- }
352
- // Commit final range to React state (triggers a single re-render)
353
- const finalRange = liveDragRangeRef.current;
354
- if (finalRange) {
355
- setSelectionRange(finalRange);
356
- setActiveCell({
357
- rowIndex: finalRange.endRow,
358
- columnIndex: finalRange.endCol + colOffsetRef.current,
359
- });
360
- }
361
- }
362
- // For simple clicks (no drag movement), mouseDown already set
363
- // selectionRange + activeCell — skip redundant state updates.
364
- // Clean up DOM attributes — React will apply CSS-module classes on the same paint
365
- clearDragAttrs();
366
- liveDragRangeRef.current = null;
367
- lastMousePosRef.current = null;
368
- dragStartRef.current = null;
369
- if (wasDrag)
370
- setIsDragging(false);
371
- };
372
- window.addEventListener('mousemove', onMove, true);
373
- window.addEventListener('mouseup', onUp, true);
374
- return () => {
375
- window.removeEventListener('mousemove', onMove, true);
376
- window.removeEventListener('mouseup', onUp, true);
377
- if (rafRef.current)
378
- cancelAnimationFrame(rafRef.current);
379
- stopAutoScroll();
380
- };
381
- }, [setActiveCell, colOffsetRef, setSelectionRange, wrapperRef]);
382
- return {
383
- selectionRange,
384
- setSelectionRange,
385
- handleCellMouseDown,
386
- handleSelectAllCells,
387
- isDragging,
388
- };
389
- }
@@ -1,106 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear } from '../utils';
3
- import { normalizeSelectionRange } from '../types';
4
- import { useLatestRef } from './useLatestRef';
5
- /**
6
- * Manages copy, cut, and paste operations for cell ranges with TSV clipboard format.
7
- * @param params - Items, columns, selection, editability, and value change callback.
8
- * @returns Copy/cut/paste handlers, cut/copy ranges, and range clear function.
9
- */
10
- export function useClipboard(params) {
11
- const { colOffset, beginBatch, endBatch, } = params;
12
- // Volatile values accessed via refs — keeps callbacks stable
13
- const itemsRef = useLatestRef(params.items);
14
- const visibleColsRef = useLatestRef(params.visibleCols);
15
- const selectionRangeRef = useLatestRef(params.selectionRange);
16
- const activeCellRef = useLatestRef(params.activeCell);
17
- const editableRef = useLatestRef(params.editable);
18
- const onCellValueChangedRef = useLatestRef(params.onCellValueChanged);
19
- const cutRangeRef = useRef(null);
20
- const [cutRange, setCutRange] = useState(null);
21
- const [copyRange, setCopyRange] = useState(null);
22
- /** In-page clipboard fallback when system clipboard is unavailable. */
23
- const internalClipboardRef = useRef(null);
24
- /** Guard against async clipboard reads completing after unmount. */
25
- const isMountedRef = useRef(true);
26
- useEffect(() => () => { isMountedRef.current = false; }, []);
27
- /** Resolve current effective range from selection or active cell. */
28
- const getEffectiveRange = useCallback(() => {
29
- const sel = selectionRangeRef.current;
30
- const ac = activeCellRef.current;
31
- return sel ?? (ac != null
32
- ? { startRow: ac.rowIndex, startCol: ac.columnIndex - colOffset, endRow: ac.rowIndex, endCol: ac.columnIndex - colOffset }
33
- : null);
34
- }, [colOffset, selectionRangeRef, activeCellRef]);
35
- const handleCopy = useCallback(() => {
36
- const range = getEffectiveRange();
37
- if (range == null)
38
- return;
39
- const norm = normalizeSelectionRange(range);
40
- const tsv = formatSelectionAsTsv(itemsRef.current, visibleColsRef.current, norm);
41
- internalClipboardRef.current = tsv;
42
- setCopyRange(norm);
43
- void navigator.clipboard.writeText(tsv).catch(() => { });
44
- }, [getEffectiveRange, itemsRef, visibleColsRef]);
45
- const handleCut = useCallback(() => {
46
- if (editableRef.current === false)
47
- return;
48
- const range = getEffectiveRange();
49
- if (range == null || onCellValueChangedRef.current == null)
50
- return;
51
- const norm = normalizeSelectionRange(range);
52
- cutRangeRef.current = norm;
53
- setCutRange(norm);
54
- setCopyRange(null);
55
- handleCopy();
56
- // handleCopy sets copyRange — override it back since this is a cut
57
- setCopyRange(null);
58
- }, [getEffectiveRange, handleCopy, editableRef, onCellValueChangedRef]);
59
- const handlePaste = useCallback(async () => {
60
- if (editableRef.current === false)
61
- return;
62
- const onCellValueChanged = onCellValueChangedRef.current;
63
- if (onCellValueChanged == null)
64
- return;
65
- let text;
66
- try {
67
- text = await navigator.clipboard.readText();
68
- }
69
- catch {
70
- text = '';
71
- }
72
- // Bail out if component unmounted during async clipboard read
73
- if (!isMountedRef.current)
74
- return;
75
- if (!text.trim() && internalClipboardRef.current != null) {
76
- text = internalClipboardRef.current;
77
- }
78
- if (!text.trim())
79
- return;
80
- const norm = getEffectiveRange();
81
- const anchorRow = norm ? norm.startRow : 0;
82
- const anchorCol = norm ? norm.startCol : 0;
83
- const items = itemsRef.current;
84
- const visibleCols = visibleColsRef.current;
85
- const parsedRows = parseTsvClipboard(text);
86
- beginBatch?.();
87
- const pasteEvents = applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols);
88
- for (const evt of pasteEvents)
89
- onCellValueChanged(evt);
90
- if (cutRangeRef.current) {
91
- const cutEvents = applyCutClear(cutRangeRef.current, items, visibleCols);
92
- for (const evt of cutEvents)
93
- onCellValueChanged(evt);
94
- cutRangeRef.current = null;
95
- setCutRange(null);
96
- }
97
- endBatch?.();
98
- setCopyRange(null);
99
- }, [getEffectiveRange, itemsRef, visibleColsRef, editableRef, onCellValueChangedRef, beginBatch, endBatch]);
100
- const clearClipboardRanges = useCallback(() => {
101
- setCopyRange(null);
102
- setCutRange(null);
103
- cutRangeRef.current = null;
104
- }, []);
105
- return { handleCopy, handleCut, handlePaste, cutRange, copyRange, clearClipboardRanges };
106
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Headless column chooser state and handlers for Fluent, Material, and Radix.
3
- * UI packages use this hook and render only trigger + popover (checkboxes, Select All, Clear All).
4
- */
5
- import { useState, useCallback, useEffect } from 'react';
6
- /**
7
- * Returns open/setOpen, handleToggle, handleClose (Escape handled in hook),
8
- * handleCheckboxChange(columnKey)(visible), handleSelectAll, handleClearAll,
9
- * visibleCount, totalCount. UI renders trigger + popover and wires handlers.
10
- */
11
- export function useColumnChooserState(params) {
12
- const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns } = params;
13
- const [open, setOpen] = useState(false);
14
- useEffect(() => {
15
- if (!open)
16
- return;
17
- const handleKeyDown = (event) => {
18
- if (event.key === 'Escape') {
19
- event.preventDefault();
20
- setOpen(false);
21
- }
22
- };
23
- document.addEventListener('keydown', handleKeyDown, true);
24
- return () => document.removeEventListener('keydown', handleKeyDown, true);
25
- }, [open]);
26
- const handleToggle = useCallback(() => {
27
- setOpen((prev) => !prev);
28
- }, []);
29
- const handleClose = useCallback(() => {
30
- setOpen(false);
31
- }, []);
32
- const handleCheckboxChange = useCallback((columnKey) => (visible) => {
33
- onVisibilityChange(columnKey, visible);
34
- }, [onVisibilityChange]);
35
- const handleSelectAll = useCallback(() => {
36
- if (onSetVisibleColumns) {
37
- onSetVisibleColumns(new Set(columns.map((col) => col.columnId)));
38
- }
39
- else {
40
- columns.forEach((col) => {
41
- if (!visibleColumns.has(col.columnId)) {
42
- onVisibilityChange(col.columnId, true);
43
- }
44
- });
45
- }
46
- }, [columns, visibleColumns, onVisibilityChange, onSetVisibleColumns]);
47
- const handleClearAll = useCallback(() => {
48
- if (onSetVisibleColumns) {
49
- // Keep required columns visible
50
- const required = new Set(columns.filter((col) => col.required).map((col) => col.columnId));
51
- onSetVisibleColumns(required);
52
- }
53
- else {
54
- columns.forEach((col) => {
55
- if (!col.required && visibleColumns.has(col.columnId)) {
56
- onVisibilityChange(col.columnId, false);
57
- }
58
- });
59
- }
60
- }, [columns, visibleColumns, onVisibilityChange, onSetVisibleColumns]);
61
- const visibleCount = visibleColumns.size;
62
- const totalCount = columns.length;
63
- return {
64
- open,
65
- setOpen,
66
- handleToggle,
67
- handleClose,
68
- handleCheckboxChange,
69
- handleSelectAll,
70
- handleClearAll,
71
- visibleCount,
72
- totalCount,
73
- };
74
- }