@bsuite/data-grid 0.1.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.
Files changed (89) hide show
  1. package/dist/DataGrid.d.ts +6 -0
  2. package/dist/DataGrid.d.ts.map +1 -0
  3. package/dist/DataGrid.js +642 -0
  4. package/dist/DataGrid.js.map +1 -0
  5. package/dist/cn.d.ts +7 -0
  6. package/dist/cn.d.ts.map +1 -0
  7. package/dist/cn.js +9 -0
  8. package/dist/cn.js.map +1 -0
  9. package/dist/components/ColumnHeaderCell.d.ts +18 -0
  10. package/dist/components/ColumnHeaderCell.d.ts.map +1 -0
  11. package/dist/components/ColumnHeaderCell.js +30 -0
  12. package/dist/components/ColumnHeaderCell.js.map +1 -0
  13. package/dist/components/FillHandle.d.ts +11 -0
  14. package/dist/components/FillHandle.d.ts.map +1 -0
  15. package/dist/components/FillHandle.js +16 -0
  16. package/dist/components/FillHandle.js.map +1 -0
  17. package/dist/components/GridCell.d.ts +25 -0
  18. package/dist/components/GridCell.d.ts.map +1 -0
  19. package/dist/components/GridCell.js +9 -0
  20. package/dist/components/GridCell.js.map +1 -0
  21. package/dist/editors/BooleanEditor.d.ts +5 -0
  22. package/dist/editors/BooleanEditor.d.ts.map +1 -0
  23. package/dist/editors/BooleanEditor.js +23 -0
  24. package/dist/editors/BooleanEditor.js.map +1 -0
  25. package/dist/editors/DateEditor.d.ts +7 -0
  26. package/dist/editors/DateEditor.d.ts.map +1 -0
  27. package/dist/editors/DateEditor.js +26 -0
  28. package/dist/editors/DateEditor.js.map +1 -0
  29. package/dist/editors/NumberEditor.d.ts +3 -0
  30. package/dist/editors/NumberEditor.d.ts.map +1 -0
  31. package/dist/editors/NumberEditor.js +30 -0
  32. package/dist/editors/NumberEditor.js.map +1 -0
  33. package/dist/editors/SelectEditor.d.ts +3 -0
  34. package/dist/editors/SelectEditor.d.ts.map +1 -0
  35. package/dist/editors/SelectEditor.js +18 -0
  36. package/dist/editors/SelectEditor.js.map +1 -0
  37. package/dist/editors/TextEditor.d.ts +3 -0
  38. package/dist/editors/TextEditor.d.ts.map +1 -0
  39. package/dist/editors/TextEditor.js +22 -0
  40. package/dist/editors/TextEditor.js.map +1 -0
  41. package/dist/editors/index.d.ts +6 -0
  42. package/dist/editors/index.d.ts.map +1 -0
  43. package/dist/editors/index.js +6 -0
  44. package/dist/editors/index.js.map +1 -0
  45. package/dist/editors/useAutoFocusSelect.d.ts +9 -0
  46. package/dist/editors/useAutoFocusSelect.d.ts.map +1 -0
  47. package/dist/editors/useAutoFocusSelect.js +30 -0
  48. package/dist/editors/useAutoFocusSelect.js.map +1 -0
  49. package/dist/formatting.d.ts +9 -0
  50. package/dist/formatting.d.ts.map +1 -0
  51. package/dist/formatting.js +38 -0
  52. package/dist/formatting.js.map +1 -0
  53. package/dist/index.d.ts +20 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +18 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/lib/clipboard.d.ts +28 -0
  58. package/dist/lib/clipboard.d.ts.map +1 -0
  59. package/dist/lib/clipboard.js +53 -0
  60. package/dist/lib/clipboard.js.map +1 -0
  61. package/dist/lib/fill.d.ts +35 -0
  62. package/dist/lib/fill.d.ts.map +1 -0
  63. package/dist/lib/fill.js +76 -0
  64. package/dist/lib/fill.js.map +1 -0
  65. package/dist/lib/index.d.ts +7 -0
  66. package/dist/lib/index.d.ts.map +1 -0
  67. package/dist/lib/index.js +7 -0
  68. package/dist/lib/index.js.map +1 -0
  69. package/dist/lib/keyboard.d.ts +40 -0
  70. package/dist/lib/keyboard.d.ts.map +1 -0
  71. package/dist/lib/keyboard.js +85 -0
  72. package/dist/lib/keyboard.js.map +1 -0
  73. package/dist/lib/selection.d.ts +26 -0
  74. package/dist/lib/selection.d.ts.map +1 -0
  75. package/dist/lib/selection.js +57 -0
  76. package/dist/lib/selection.js.map +1 -0
  77. package/dist/lib/tsv.d.ts +31 -0
  78. package/dist/lib/tsv.d.ts.map +1 -0
  79. package/dist/lib/tsv.js +113 -0
  80. package/dist/lib/tsv.js.map +1 -0
  81. package/dist/lib/undo.d.ts +36 -0
  82. package/dist/lib/undo.d.ts.map +1 -0
  83. package/dist/lib/undo.js +57 -0
  84. package/dist/lib/undo.js.map +1 -0
  85. package/dist/types.d.ts +119 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +2 -0
  88. package/dist/types.js.map +1 -0
  89. package/package.json +66 -0
@@ -0,0 +1,6 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { DataGridHandle, DataGridProps } from './types.js';
3
+ export declare const DataGrid: <TRow>(props: DataGridProps<TRow> & {
4
+ ref?: React.Ref<DataGridHandle>;
5
+ }) => ReactElement;
6
+ //# sourceMappingURL=DataGrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DataGrid.d.ts","sourceRoot":"","sources":["../src/DataGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,EAaL,KAAK,YAAY,EAClB,MAAM,OAAO,CAAC;AAwBf,OAAO,KAAK,EAMV,cAAc,EACd,aAAa,EACd,MAAM,YAAY,CAAC;AAw0BpB,eAAO,MAAM,QAAQ,EAAgC,CAAC,IAAI,EACxD,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAC7D,YAAY,CAAC"}
@@ -0,0 +1,642 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useReducer, useRef, useState, } from 'react';
3
+ import { getCoreRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table';
4
+ import { useVirtualizer } from '@tanstack/react-virtual';
5
+ import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
6
+ import { SortableContext, arrayMove, horizontalListSortingStrategy } from '@dnd-kit/sortable';
7
+ import { cn } from './cn.js';
8
+ import { ColumnHeaderCell } from './components/ColumnHeaderCell.js';
9
+ import { FillHandle } from './components/FillHandle.js';
10
+ import { GridCell } from './components/GridCell.js';
11
+ import { BooleanEditor, DateEditor, NumberEditor, SelectEditor, TextEditor } from './editors/index.js';
12
+ import { formatCellValue, parseCellValue } from './formatting.js';
13
+ import { buildCopyText, buildPasteEdits, expandSingleCellPasteToSelection } from './lib/clipboard.js';
14
+ import { computeFillDownExtent, computeFillRange, mapFillTargetToSource } from './lib/fill.js';
15
+ import { isPrintableEditTrigger, moveEnter, moveFocus, moveTab } from './lib/keyboard.js';
16
+ import { isCellInRange, normalizeRange, rangeToCells } from './lib/selection.js';
17
+ import { UndoStack } from './lib/undo.js';
18
+ const DEFAULT_ROW_HEIGHT = 32;
19
+ const DEFAULT_HEADER_HEIGHT = 32;
20
+ const DEFAULT_COLUMN_WIDTH = 150;
21
+ const DEFAULT_MIN_COLUMN_WIDTH = 60;
22
+ const DEFAULT_UNDO_LIMIT = 200;
23
+ function overlayKey(rowIndex, columnId) {
24
+ return `${rowIndex}:${columnId}`;
25
+ }
26
+ function editorFor(column) {
27
+ if (column.renderEditor)
28
+ return column.renderEditor;
29
+ switch (column.dataType) {
30
+ case 'number':
31
+ return NumberEditor;
32
+ case 'date':
33
+ return DateEditor;
34
+ case 'boolean':
35
+ return BooleanEditor;
36
+ case 'select':
37
+ return SelectEditor;
38
+ default:
39
+ return TextEditor;
40
+ }
41
+ }
42
+ function DataGridInner(props, ref) {
43
+ const { columns, data, getRowId, onCellsEdited, onError, frozenFirstColumn = true, rowHeight = DEFAULT_ROW_HEIGHT, headerHeight = DEFAULT_HEADER_HEIGHT, height = '100%', className, undoLimit = DEFAULT_UNDO_LIMIT, emptyState, } = props;
44
+ const scrollRef = useRef(null);
45
+ const columnConfigById = useMemo(() => new Map(columns.map((c) => [c.id, c])), [columns]);
46
+ const tableColumns = useMemo(() => columns.map((col) => ({
47
+ id: col.id,
48
+ header: col.header,
49
+ accessorFn: (row) => col.accessor(row),
50
+ enableSorting: col.sortable ?? true,
51
+ size: col.width ?? DEFAULT_COLUMN_WIDTH,
52
+ minSize: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
53
+ maxSize: col.maxWidth,
54
+ })), [columns]);
55
+ const [sorting, setSorting] = useState([]);
56
+ const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.id));
57
+ const [columnSizing, setColumnSizing] = useState({});
58
+ // Keep columnOrder in sync if the `columns` prop's identity set changes
59
+ // (columns added/removed) without discarding the user's chosen order.
60
+ useEffect(() => {
61
+ setColumnOrder((prev) => {
62
+ const ids = columns.map((c) => c.id);
63
+ const idSet = new Set(ids);
64
+ const kept = prev.filter((id) => idSet.has(id));
65
+ const missing = ids.filter((id) => !kept.includes(id));
66
+ const next = [...kept, ...missing];
67
+ return next.length === prev.length && next.every((id, i) => id === prev[i]) ? prev : next;
68
+ });
69
+ }, [columns]);
70
+ const table = useReactTable({
71
+ data,
72
+ columns: tableColumns,
73
+ state: { sorting, columnOrder, columnSizing },
74
+ onSortingChange: setSorting,
75
+ onColumnOrderChange: setColumnOrder,
76
+ onColumnSizingChange: setColumnSizing,
77
+ columnResizeMode: 'onChange',
78
+ enableColumnResizing: true,
79
+ getRowId,
80
+ getCoreRowModel: getCoreRowModel(),
81
+ getSortedRowModel: getSortedRowModel(),
82
+ });
83
+ const rows = table.getRowModel().rows;
84
+ const visibleColumns = table.getVisibleLeafColumns();
85
+ const headerCells = table.getHeaderGroups()[0]?.headers ?? [];
86
+ const frozenColumn = frozenFirstColumn ? visibleColumns[0] : undefined;
87
+ const scrollableColumns = frozenFirstColumn ? visibleColumns.slice(1) : visibleColumns;
88
+ const frozenWidth = frozenColumn ? frozenColumn.getSize() : 0;
89
+ // ── Overlay: optimistic values layered over `data` until the host's own
90
+ // re-render catches up, and reverted (with onError fired first) if the
91
+ // host's onCellsEdited rejects. ──────────────────────────────────────
92
+ const [overlay, setOverlay] = useState(() => new Map());
93
+ const getEffectiveValue = useCallback((rowIndex, columnId) => {
94
+ const key = overlayKey(rowIndex, columnId);
95
+ if (overlay.has(key))
96
+ return overlay.get(key);
97
+ const column = columnConfigById.get(columnId);
98
+ const row = data[rowIndex];
99
+ return column && row ? column.accessor(row) : undefined;
100
+ }, [overlay, data, columnConfigById]);
101
+ // ── Selection: anchor (drag/shift-click origin) + focus (current cell). ─
102
+ const [selection, setSelection] = useState(rows.length > 0 && visibleColumns.length > 0 ? { anchor: { row: 0, col: 0 }, focus: { row: 0, col: 0 } } : null);
103
+ const [editingCell, setEditingCell] = useState(null);
104
+ const [editSeedChar, setEditSeedChar] = useState(undefined);
105
+ const [fillPreview, setFillPreview] = useState(null);
106
+ const isSelectingRef = useRef(false);
107
+ const isFillDraggingRef = useRef(false);
108
+ const fillSourceRef = useRef(null);
109
+ const undoStackRef = useRef(new UndoStack(undoLimit));
110
+ const [, forceRender] = useReducer((n) => n + 1, 0);
111
+ const rowVirtualizer = useVirtualizer({
112
+ count: rows.length,
113
+ getScrollElement: () => scrollRef.current,
114
+ estimateSize: () => rowHeight,
115
+ overscan: 8,
116
+ });
117
+ const columnVirtualizer = useVirtualizer({
118
+ horizontal: true,
119
+ count: scrollableColumns.length,
120
+ getScrollElement: () => scrollRef.current,
121
+ estimateSize: (index) => scrollableColumns[index]?.getSize() ?? DEFAULT_COLUMN_WIDTH,
122
+ overscan: 4,
123
+ });
124
+ useEffect(() => {
125
+ columnVirtualizer.measure();
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [columnSizing]);
128
+ const columnOffsets = useMemo(() => {
129
+ let acc = 0;
130
+ return scrollableColumns.map((col) => {
131
+ const offset = acc;
132
+ acc += col.getSize();
133
+ return offset;
134
+ });
135
+ }, [scrollableColumns]);
136
+ const totalScrollableWidth = columnOffsets.length > 0
137
+ ? columnOffsets[columnOffsets.length - 1] + scrollableColumns[columnOffsets.length - 1].getSize()
138
+ : 0;
139
+ const totalWidth = frozenWidth + totalScrollableWidth;
140
+ const totalHeight = headerHeight + rowVirtualizer.getTotalSize();
141
+ const gridBounds = { minRow: 0, maxRow: Math.max(0, rows.length - 1), minCol: 0, maxCol: Math.max(0, visibleColumns.length - 1) };
142
+ function cellRect(pos) {
143
+ const top = headerHeight + pos.row * rowHeight;
144
+ if (frozenFirstColumn && pos.col === 0) {
145
+ return { left: 0, top, width: frozenWidth, height: rowHeight };
146
+ }
147
+ const scrollIdx = pos.col - (frozenFirstColumn ? 1 : 0);
148
+ const col = scrollableColumns[scrollIdx];
149
+ const offset = columnOffsets[scrollIdx] ?? 0;
150
+ return { left: frozenWidth + offset, top, width: col?.getSize() ?? DEFAULT_COLUMN_WIDTH, height: rowHeight };
151
+ }
152
+ function getCellPositionFromPoint(clientX, clientY) {
153
+ const el = scrollRef.current;
154
+ if (!el || rows.length === 0 || visibleColumns.length === 0)
155
+ return null;
156
+ const rect = el.getBoundingClientRect();
157
+ const x = clientX - rect.left + el.scrollLeft;
158
+ const y = clientY - rect.top + el.scrollTop;
159
+ const bodyY = y - headerHeight;
160
+ if (bodyY < 0)
161
+ return null;
162
+ const rowIndex = Math.min(rows.length - 1, Math.max(0, Math.floor(bodyY / rowHeight)));
163
+ let colIndex;
164
+ if (frozenFirstColumn && x <= frozenWidth) {
165
+ colIndex = 0;
166
+ }
167
+ else {
168
+ const sx = x - frozenWidth;
169
+ let idx = scrollableColumns.length - 1;
170
+ for (let i = 0; i < columnOffsets.length; i += 1) {
171
+ if (columnOffsets[i] > sx) {
172
+ idx = i - 1;
173
+ break;
174
+ }
175
+ }
176
+ idx = Math.max(0, idx);
177
+ colIndex = idx + (frozenFirstColumn ? 1 : 0);
178
+ }
179
+ return { row: rowIndex, col: Math.min(colIndex, visibleColumns.length - 1) };
180
+ }
181
+ function scrollCellIntoView(pos) {
182
+ rowVirtualizer.scrollToIndex(pos.row);
183
+ const scrollIdx = pos.col - (frozenFirstColumn ? 1 : 0);
184
+ if (scrollIdx >= 0)
185
+ columnVirtualizer.scrollToIndex(scrollIdx);
186
+ }
187
+ // ── Mutation pipeline ---------------------------------------------------
188
+ async function commitEdits(edits, phase, opts) {
189
+ if (edits.length === 0)
190
+ return;
191
+ setOverlay((prev) => {
192
+ const next = new Map(prev);
193
+ for (const edit of edits)
194
+ next.set(overlayKey(edit.rowIndex, edit.columnId), edit.value);
195
+ return next;
196
+ });
197
+ if (!opts?.skipUndoPush) {
198
+ undoStackRef.current.push({
199
+ before: edits.map((e) => ({ ...e, value: e.previousValue, previousValue: e.value })),
200
+ after: edits,
201
+ });
202
+ forceRender();
203
+ }
204
+ try {
205
+ await onCellsEdited(edits);
206
+ }
207
+ catch (err) {
208
+ if (!opts?.skipUndoPush)
209
+ undoStackRef.current.discardLast();
210
+ // Tell the host FIRST. A grid that reverts a failed edit without
211
+ // surfacing why reproduces the "edited, nothing happened" defect
212
+ // this package exists to close (see DataGridError doc comment).
213
+ const error = {
214
+ message: err instanceof Error ? err.message : 'Failed to save the edit.',
215
+ cause: err,
216
+ phase,
217
+ edits,
218
+ };
219
+ onError(error);
220
+ setOverlay((prev) => {
221
+ const next = new Map(prev);
222
+ for (const edit of edits)
223
+ next.delete(overlayKey(edit.rowIndex, edit.columnId));
224
+ return next;
225
+ });
226
+ forceRender();
227
+ }
228
+ }
229
+ const handleUndo = useCallback(() => {
230
+ const entry = undoStackRef.current.undo();
231
+ if (!entry)
232
+ return;
233
+ forceRender();
234
+ void commitEdits(entry.before, 'undo', { skipUndoPush: true });
235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
236
+ }, [onCellsEdited, onError, overlay, data, columnConfigById]);
237
+ const handleRedo = useCallback(() => {
238
+ const entry = undoStackRef.current.redo();
239
+ if (!entry)
240
+ return;
241
+ forceRender();
242
+ void commitEdits(entry.after, 'redo', { skipUndoPush: true });
243
+ // eslint-disable-next-line react-hooks/exhaustive-deps
244
+ }, [onCellsEdited, onError, overlay, data, columnConfigById]);
245
+ const handleCopy = useCallback(async () => {
246
+ if (!selection)
247
+ return;
248
+ const range = normalizeRange(selection.anchor, selection.focus);
249
+ const text = buildCopyText(range, (pos) => {
250
+ const row = rows[pos.row];
251
+ const column = visibleColumns[pos.col];
252
+ if (!row || !column)
253
+ return '';
254
+ const config = columnConfigById.get(column.id);
255
+ const value = getEffectiveValue(row.index, column.id);
256
+ return config?.formatValue ? config.formatValue(value, data[row.index]) : formatCellValue(value, config?.dataType);
257
+ });
258
+ try {
259
+ await navigator.clipboard.writeText(text);
260
+ }
261
+ catch (err) {
262
+ onError({ message: 'Could not write to the clipboard.', cause: err, phase: 'clipboard', edits: [] });
263
+ }
264
+ // eslint-disable-next-line react-hooks/exhaustive-deps
265
+ }, [selection, rows, visibleColumns, columnConfigById, getEffectiveValue, data, onError]);
266
+ async function handleCut() {
267
+ await handleCopy();
268
+ await handleClearSelection();
269
+ }
270
+ async function handleClearSelection() {
271
+ if (!selection)
272
+ return;
273
+ const range = normalizeRange(selection.anchor, selection.focus);
274
+ const edits = [];
275
+ for (const pos of rangeToCells(range)) {
276
+ const row = rows[pos.row];
277
+ const column = visibleColumns[pos.col];
278
+ if (!row || !column)
279
+ continue;
280
+ const config = columnConfigById.get(column.id);
281
+ if (config?.editable === false)
282
+ continue;
283
+ const previousValue = getEffectiveValue(row.index, column.id);
284
+ if (previousValue == null || previousValue === '')
285
+ continue;
286
+ edits.push({ rowIndex: row.index, columnId: column.id, previousValue, value: null, row: data[row.index] });
287
+ }
288
+ if (edits.length > 0)
289
+ await commitEdits(edits, 'clear');
290
+ }
291
+ async function handlePaste() {
292
+ if (!selection)
293
+ return;
294
+ let text;
295
+ try {
296
+ text = await navigator.clipboard.readText();
297
+ }
298
+ catch (err) {
299
+ onError({ message: 'Could not read from the clipboard.', cause: err, phase: 'clipboard', edits: [] });
300
+ return;
301
+ }
302
+ const anchorPos = {
303
+ row: Math.min(selection.anchor.row, selection.focus.row),
304
+ col: Math.min(selection.anchor.col, selection.focus.col),
305
+ };
306
+ const rawEdits = buildPasteEdits(text, anchorPos, {
307
+ maxRow: gridBounds.maxRow,
308
+ maxCol: gridBounds.maxCol,
309
+ });
310
+ const currentSelection = normalizeRange(selection.anchor, selection.focus);
311
+ const expanded = expandSingleCellPasteToSelection(rawEdits, currentSelection);
312
+ const edits = [];
313
+ let maxRowSeen = anchorPos.row;
314
+ let maxColSeen = anchorPos.col;
315
+ for (const raw of expanded) {
316
+ const row = rows[raw.row];
317
+ const column = visibleColumns[raw.col];
318
+ if (!row || !column)
319
+ continue;
320
+ const config = columnConfigById.get(column.id);
321
+ if (config?.editable === false)
322
+ continue;
323
+ const parsed = config?.parseValue
324
+ ? config.parseValue(raw.value, data[row.index])
325
+ : parseCellValue(raw.value, config?.dataType);
326
+ const previousValue = getEffectiveValue(row.index, column.id);
327
+ edits.push({ rowIndex: row.index, columnId: column.id, previousValue, value: parsed, row: data[row.index] });
328
+ maxRowSeen = Math.max(maxRowSeen, raw.row);
329
+ maxColSeen = Math.max(maxColSeen, raw.col);
330
+ }
331
+ if (edits.length > 0) {
332
+ await commitEdits(edits, 'paste');
333
+ setSelection({ anchor: anchorPos, focus: { row: maxRowSeen, col: maxColSeen } });
334
+ }
335
+ }
336
+ function applyFillResult(source, result) {
337
+ if (result.direction === 'none' || result.filledCells.length === 0)
338
+ return;
339
+ const edits = [];
340
+ for (const targetPos of result.filledCells) {
341
+ const sourcePos = mapFillTargetToSource(targetPos, source, result.direction);
342
+ const targetRow = rows[targetPos.row];
343
+ const sourceRow = rows[sourcePos.row];
344
+ const targetColumn = visibleColumns[targetPos.col];
345
+ const sourceColumn = visibleColumns[sourcePos.col];
346
+ if (!targetRow || !sourceRow || !targetColumn || !sourceColumn)
347
+ continue;
348
+ const config = columnConfigById.get(targetColumn.id);
349
+ if (config?.editable === false)
350
+ continue;
351
+ const value = getEffectiveValue(sourceRow.index, sourceColumn.id);
352
+ const previousValue = getEffectiveValue(targetRow.index, targetColumn.id);
353
+ edits.push({ rowIndex: targetRow.index, columnId: targetColumn.id, previousValue, value, row: data[targetRow.index] });
354
+ }
355
+ if (edits.length > 0) {
356
+ void commitEdits(edits, 'fill');
357
+ setSelection({
358
+ anchor: { row: result.range.startRow, col: result.range.startCol },
359
+ focus: { row: result.range.endRow, col: result.range.endCol },
360
+ });
361
+ }
362
+ }
363
+ function handleFillDoubleClick() {
364
+ if (!selection)
365
+ return;
366
+ const range = normalizeRange(selection.anchor, selection.focus);
367
+ const adjacentColIndex = range.startCol > 0 ? range.startCol - 1 : range.endCol + 1;
368
+ const adjacentColumn = visibleColumns[adjacentColIndex];
369
+ if (!adjacentColumn)
370
+ return;
371
+ const extentRow = computeFillDownExtent({
372
+ startRow: range.endRow,
373
+ maxRow: gridBounds.maxRow,
374
+ adjacentColHasValue: (r) => {
375
+ const dataRow = rows[r]?.index;
376
+ if (dataRow == null)
377
+ return false;
378
+ const val = getEffectiveValue(dataRow, adjacentColumn.id);
379
+ return val != null && val !== '';
380
+ },
381
+ });
382
+ if (extentRow <= range.endRow)
383
+ return;
384
+ const result = computeFillRange(range, { row: extentRow, col: range.endCol });
385
+ applyFillResult(range, result);
386
+ }
387
+ // ── Global pointer listeners for drag-select and fill-drag. Registered
388
+ // once; always dispatches through refs holding the LATEST closures so
389
+ // there's no stale-state bug without re-subscribing on every render. ───
390
+ const latestPointerMove = useRef(() => { });
391
+ const latestPointerUp = useRef(() => { });
392
+ latestPointerMove.current = (e) => {
393
+ if (isSelectingRef.current) {
394
+ const pos = getCellPositionFromPoint(e.clientX, e.clientY);
395
+ if (pos)
396
+ setSelection((prev) => (prev ? { anchor: prev.anchor, focus: pos } : prev));
397
+ }
398
+ else if (isFillDraggingRef.current && fillSourceRef.current) {
399
+ const pos = getCellPositionFromPoint(e.clientX, e.clientY);
400
+ if (pos)
401
+ setFillPreview(computeFillRange(fillSourceRef.current, pos));
402
+ }
403
+ };
404
+ latestPointerUp.current = () => {
405
+ if (isSelectingRef.current) {
406
+ isSelectingRef.current = false;
407
+ }
408
+ if (isFillDraggingRef.current) {
409
+ isFillDraggingRef.current = false;
410
+ const source = fillSourceRef.current;
411
+ fillSourceRef.current = null;
412
+ if (source && fillPreview)
413
+ applyFillResult(source, fillPreview);
414
+ setFillPreview(null);
415
+ }
416
+ };
417
+ useEffect(() => {
418
+ const move = (e) => latestPointerMove.current(e);
419
+ const up = (e) => latestPointerUp.current(e);
420
+ window.addEventListener('pointermove', move);
421
+ window.addEventListener('pointerup', up);
422
+ return () => {
423
+ window.removeEventListener('pointermove', move);
424
+ window.removeEventListener('pointerup', up);
425
+ };
426
+ }, []);
427
+ // ── Cell interaction ------------------------------------------------
428
+ function beginEditing(pos, seedChar) {
429
+ const column = visibleColumns[pos.col];
430
+ if (!column)
431
+ return;
432
+ const config = columnConfigById.get(column.id);
433
+ if (config?.editable === false)
434
+ return;
435
+ setEditSeedChar(seedChar);
436
+ setEditingCell(pos);
437
+ }
438
+ function handleEditorCommit(nextValue) {
439
+ const pos = editingCell;
440
+ setEditingCell(null);
441
+ setEditSeedChar(undefined);
442
+ if (!pos)
443
+ return;
444
+ const row = rows[pos.row];
445
+ const column = visibleColumns[pos.col];
446
+ if (!row || !column)
447
+ return;
448
+ const previousValue = getEffectiveValue(row.index, column.id);
449
+ if (Object.is(previousValue, nextValue))
450
+ return;
451
+ void commitEdits([{ rowIndex: row.index, columnId: column.id, previousValue, value: nextValue, row: data[row.index] }], 'edit');
452
+ }
453
+ function handleEditorCancel() {
454
+ setEditingCell(null);
455
+ setEditSeedChar(undefined);
456
+ }
457
+ function handleCellMouseDown(pos, e) {
458
+ if (e.button !== 0)
459
+ return;
460
+ // The active editor owns its own uncommitted text in local state and
461
+ // commits it on blur (see TextEditor/NumberEditor/etc). A plain <div>
462
+ // cell doesn't steal focus on mousedown, so the editor's <input> would
463
+ // otherwise stay focused while the selection moves under it — blur it
464
+ // explicitly so its onBlur commit fires before we move on.
465
+ if (editingCell && (editingCell.row !== pos.row || editingCell.col !== pos.col)) {
466
+ document.activeElement?.blur();
467
+ }
468
+ if (e.shiftKey && selection) {
469
+ setSelection({ anchor: selection.anchor, focus: pos });
470
+ }
471
+ else {
472
+ setSelection({ anchor: pos, focus: pos });
473
+ isSelectingRef.current = true;
474
+ }
475
+ }
476
+ function handleCellMouseEnter(pos) {
477
+ if (isSelectingRef.current) {
478
+ setSelection((prev) => (prev ? { anchor: prev.anchor, focus: pos } : prev));
479
+ }
480
+ }
481
+ function handleKeyDown(e) {
482
+ if (editingCell)
483
+ return; // the active editor owns its own key handling
484
+ if (!selection)
485
+ return;
486
+ const focusCell = selection.focus;
487
+ const ctrlOrCmd = e.ctrlKey || e.metaKey;
488
+ const isEmptyCell = (pos) => {
489
+ const dataRow = rows[pos.row]?.index;
490
+ const column = visibleColumns[pos.col];
491
+ if (dataRow == null || !column)
492
+ return true;
493
+ const val = getEffectiveValue(dataRow, column.id);
494
+ return val == null || val === '';
495
+ };
496
+ if (ctrlOrCmd && !e.shiftKey && e.key.toLowerCase() === 'z') {
497
+ e.preventDefault();
498
+ handleUndo();
499
+ return;
500
+ }
501
+ if (ctrlOrCmd && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) {
502
+ e.preventDefault();
503
+ handleRedo();
504
+ return;
505
+ }
506
+ if (ctrlOrCmd && e.key.toLowerCase() === 'c') {
507
+ e.preventDefault();
508
+ void handleCopy();
509
+ return;
510
+ }
511
+ if (ctrlOrCmd && e.key.toLowerCase() === 'x') {
512
+ e.preventDefault();
513
+ void handleCut();
514
+ return;
515
+ }
516
+ if (ctrlOrCmd && e.key.toLowerCase() === 'v') {
517
+ e.preventDefault();
518
+ void handlePaste();
519
+ return;
520
+ }
521
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
522
+ e.preventDefault();
523
+ const next = moveFocus(focusCell, e.key, gridBounds, { ctrlOrCmd, isEmpty: isEmptyCell });
524
+ setSelection(e.shiftKey ? { anchor: selection.anchor, focus: next } : { anchor: next, focus: next });
525
+ scrollCellIntoView(next);
526
+ return;
527
+ }
528
+ if (e.key === 'Tab') {
529
+ e.preventDefault();
530
+ const next = moveTab(focusCell, gridBounds, e.shiftKey);
531
+ setSelection({ anchor: next, focus: next });
532
+ scrollCellIntoView(next);
533
+ return;
534
+ }
535
+ if (e.key === 'Enter') {
536
+ e.preventDefault();
537
+ const next = moveEnter(focusCell, gridBounds, e.shiftKey);
538
+ setSelection({ anchor: next, focus: next });
539
+ scrollCellIntoView(next);
540
+ return;
541
+ }
542
+ if (e.key === 'Delete' || e.key === 'Backspace') {
543
+ e.preventDefault();
544
+ void handleClearSelection();
545
+ return;
546
+ }
547
+ if (isPrintableEditTrigger(e)) {
548
+ // We're taking over this keystroke to seed the new editor's value
549
+ // (see TextEditor's `initialInputChar`) — prevent the default so
550
+ // nothing else (browser find-as-you-type, a testing harness's own
551
+ // input simulation) also acts on the same keypress.
552
+ e.preventDefault();
553
+ beginEditing(focusCell, e.key);
554
+ }
555
+ }
556
+ function handleColumnDragEnd(event) {
557
+ const { active, over } = event;
558
+ if (!over || active.id === over.id)
559
+ return;
560
+ setColumnOrder((prev) => {
561
+ const oldIndex = prev.indexOf(String(active.id));
562
+ const newIndex = prev.indexOf(String(over.id));
563
+ if (oldIndex <= 0 || newIndex <= 0)
564
+ return prev; // the frozen column (index 0) never moves
565
+ return arrayMove(prev, oldIndex, newIndex);
566
+ });
567
+ }
568
+ const dndSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));
569
+ useImperativeHandle(ref, () => ({
570
+ undo: handleUndo,
571
+ redo: handleRedo,
572
+ canUndo: () => undoStackRef.current.canUndo(),
573
+ canRedo: () => undoStackRef.current.canRedo(),
574
+ getSelection: () => (selection ? normalizeRange(selection.anchor, selection.focus) : null),
575
+ getFocusedCell: () => (selection ? selection.focus : null),
576
+ copySelection: handleCopy,
577
+ }), [handleUndo, handleRedo, selection, handleCopy]);
578
+ const normalizedSelection = selection ? normalizeRange(selection.anchor, selection.focus) : null;
579
+ const previewRange = fillPreview && fillPreview.direction !== 'none' ? fillPreview.range : null;
580
+ const virtualRows = rowVirtualizer.getVirtualItems();
581
+ const virtualColumns = columnVirtualizer.getVirtualItems();
582
+ const style = { height };
583
+ if (rows.length === 0 && emptyState) {
584
+ return (_jsx("div", { className: cn('flex items-center justify-center rounded-md border border-border bg-background', className), style: style, children: emptyState }));
585
+ }
586
+ return (_jsx(DndContext, { sensors: dndSensors, onDragEnd: handleColumnDragEnd, children: _jsx("div", { ref: scrollRef, role: "grid", "aria-rowcount": rows.length, "aria-colcount": visibleColumns.length, tabIndex: 0, onKeyDown: handleKeyDown, className: cn('relative overflow-auto rounded-md border border-border bg-background outline-none focus-visible:ring-2 focus-visible:ring-primary', className), style: style, children: _jsxs("div", { style: { position: 'relative', height: totalHeight, width: Math.max(totalWidth, 1) }, children: [_jsx("div", { role: "row", className: "sticky top-0 z-20 flex border-b border-border bg-card", style: { height: headerHeight, width: totalWidth }, children: _jsxs(SortableContext, { items: scrollableColumns.map((c) => c.id), strategy: horizontalListSortingStrategy, children: [frozenColumn && (() => {
587
+ const frozenHeader = headerCells.find((h) => h.column.id === frozenColumn.id);
588
+ return frozenHeader ? (_jsx(ColumnHeaderCell, { header: frozenHeader, frozen: true, style: { position: 'sticky', left: 0, zIndex: 30, width: frozenWidth, height: headerHeight } })) : null;
589
+ })(), virtualColumns.map((vc) => {
590
+ const column = scrollableColumns[vc.index];
591
+ const header = headerCells.find((h) => h.column.id === column.id);
592
+ if (!header)
593
+ return null;
594
+ return (_jsx(ColumnHeaderCell, { header: header, style: {
595
+ position: 'absolute',
596
+ left: frozenWidth + vc.start,
597
+ top: 0,
598
+ width: vc.size,
599
+ height: headerHeight,
600
+ } }, column.id));
601
+ })] }) }), virtualRows.map((vr) => {
602
+ const row = rows[vr.index];
603
+ const rowTop = headerHeight + vr.start;
604
+ return (_jsxs("div", { role: "row", className: "absolute left-0 flex", style: { top: rowTop, width: totalWidth, height: vr.size }, children: [frozenColumn && (_jsx(GridCellRenderer, { gridRow: vr.index, gridCol: 0, frozen: true, style: { position: 'sticky', left: 0, zIndex: 10, width: frozenWidth, height: vr.size }, column: columnConfigById.get(frozenColumn.id), row: row.original, rowIndex: row.index, value: getEffectiveValue(row.index, frozenColumn.id), isSelected: Boolean(normalizedSelection && isCellInRange({ row: vr.index, col: 0 }, normalizedSelection)), isActive: Boolean(selection && selection.focus.row === vr.index && selection.focus.col === 0), isEditing: Boolean(editingCell && editingCell.row === vr.index && editingCell.col === 0), editSeedChar: editSeedChar, onCommit: handleEditorCommit, onCancel: handleEditorCancel, onMouseDown: (e) => handleCellMouseDown({ row: vr.index, col: 0 }, e), onMouseEnter: () => handleCellMouseEnter({ row: vr.index, col: 0 }), onDoubleClick: () => beginEditing({ row: vr.index, col: 0 }) })), virtualColumns.map((vc) => {
605
+ const column = scrollableColumns[vc.index];
606
+ const gridCol = vc.index + (frozenFirstColumn ? 1 : 0);
607
+ const config = columnConfigById.get(column.id);
608
+ if (!config)
609
+ return null;
610
+ return (_jsx(GridCellRenderer, { gridRow: vr.index, gridCol: gridCol, style: { position: 'absolute', left: frozenWidth + vc.start, top: 0, width: vc.size, height: vr.size }, column: config, row: row.original, rowIndex: row.index, value: getEffectiveValue(row.index, column.id), isSelected: Boolean(normalizedSelection && isCellInRange({ row: vr.index, col: gridCol }, normalizedSelection)), isActive: Boolean(selection && selection.focus.row === vr.index && selection.focus.col === gridCol), isEditing: Boolean(editingCell && editingCell.row === vr.index && editingCell.col === gridCol), editSeedChar: editSeedChar, onCommit: handleEditorCommit, onCancel: handleEditorCancel, onMouseDown: (e) => handleCellMouseDown({ row: vr.index, col: gridCol }, e), onMouseEnter: () => handleCellMouseEnter({ row: vr.index, col: gridCol }), onDoubleClick: () => beginEditing({ row: vr.index, col: gridCol }) }, column.id));
611
+ })] }, row.id));
612
+ }), normalizedSelection && !editingCell && (_jsx(SelectionOverlay, { range: previewRange ?? normalizedSelection, isPreview: Boolean(previewRange), cellRect: cellRect, onFillPointerDown: (e) => {
613
+ e.preventDefault();
614
+ e.stopPropagation();
615
+ isFillDraggingRef.current = true;
616
+ fillSourceRef.current = normalizedSelection;
617
+ }, onFillDoubleClick: handleFillDoubleClick }))] }) }) }));
618
+ }
619
+ function GridCellRenderer(cellProps) {
620
+ const { column, row, rowIndex, value, ...rest } = cellProps;
621
+ const Editor = editorFor(column);
622
+ const content = column.renderCell
623
+ ? column.renderCell({ value, row, rowIndex, column })
624
+ : column.dataType === 'boolean'
625
+ ? (_jsx("input", { type: "checkbox", checked: Boolean(value), readOnly: true, disabled: true, className: "h-4 w-4 accent-primary disabled:opacity-100" }))
626
+ : column.formatValue
627
+ ? column.formatValue(value, row)
628
+ : formatCellValue(value, column.dataType);
629
+ return (_jsx(GridCell, { ...rest, column: column, row: row, rowIndex: rowIndex, value: value, content: content, Editor: Editor }));
630
+ }
631
+ function SelectionOverlay(props) {
632
+ const { range, isPreview, cellRect, onFillPointerDown, onFillDoubleClick } = props;
633
+ const topLeft = cellRect({ row: range.startRow, col: range.startCol });
634
+ const bottomRight = cellRect({ row: range.endRow, col: range.endCol });
635
+ const left = topLeft.left;
636
+ const top = topLeft.top;
637
+ const width = bottomRight.left + bottomRight.width - topLeft.left;
638
+ const height = bottomRight.top + bottomRight.height - topLeft.top;
639
+ return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": true, className: cn('pointer-events-none absolute border-2 border-primary', isPreview && 'border-dashed opacity-70'), style: { left, top, width, height, zIndex: 25 } }), !isPreview && (_jsx(FillHandle, { left: left + width, top: top + height, onPointerDown: onFillPointerDown, onDoubleClick: onFillDoubleClick }))] }));
640
+ }
641
+ export const DataGrid = forwardRef(DataGridInner);
642
+ //# sourceMappingURL=DataGrid.js.map