react-glide-table 1.1.4 → 1.1.6

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/dist/core.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  useCallback as useCallback3,
32
32
  useEffect as useEffect4,
33
33
  useMemo as useMemo2,
34
- useRef as useRef3,
34
+ useRef as useRef4,
35
35
  useState as useState3
36
36
  } from "react";
37
37
 
@@ -167,7 +167,7 @@ function useCellEdit({
167
167
  }
168
168
 
169
169
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
170
- import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
170
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
171
171
 
172
172
  // src/components/ui/table/features/cell-selection/cellSelection.ts
173
173
  var INITIAL_DRAG_STATE = {
@@ -187,10 +187,36 @@ function getCellSelectionBounds(start, end) {
187
187
  endCol: Math.max(start.col, end.col)
188
188
  };
189
189
  }
190
+ function measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement) {
191
+ if (rowSpan <= 1) return void 0;
192
+ const tbody = cellElement?.closest("tbody") ?? (typeof document !== "undefined" ? document.querySelector("tbody.data-table-body") : null);
193
+ if (!tbody) return void 0;
194
+ const rows = tbody.querySelectorAll(":scope > tr");
195
+ if (rows.length < rowIndex + rowSpan) return void 0;
196
+ const heights = [];
197
+ for (let i = 0; i < rowSpan; i++) {
198
+ const row = rows[rowIndex + i];
199
+ const height = row?.getBoundingClientRect().height ?? 0;
200
+ if (height <= 0) return void 0;
201
+ heights.push(height);
202
+ }
203
+ return heights;
204
+ }
190
205
  function getRowIndexInMergedCell(clientY, cellElement, rowIndex, rowSpan) {
191
206
  if (rowSpan <= 1) return rowIndex;
192
207
  const rect = cellElement.getBoundingClientRect();
193
208
  const relativeY = clientY - rect.top;
209
+ const heights = measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement);
210
+ if (heights && heights.length === rowSpan) {
211
+ let accrued = 0;
212
+ for (let i = 0; i < rowSpan; i++) {
213
+ accrued += heights[i];
214
+ if (relativeY < accrued) {
215
+ return rowIndex + i;
216
+ }
217
+ }
218
+ return rowIndex + rowSpan - 1;
219
+ }
194
220
  const rowHeight = rect.height / rowSpan;
195
221
  const offset = Math.min(
196
222
  Math.max(Math.floor(relativeY / rowHeight), 0),
@@ -212,7 +238,35 @@ function getActiveSelectionBounds(dragState, selectionBounds) {
212
238
  var SELECTION_EDGE_WIDTH_PX = 2;
213
239
  var SELECTION_EDGE_COLOR = "var(--color-brand-primary)";
214
240
  var CELL_SELECTION_EDGES_CLASS = "cell-selection-edges";
215
- function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt) {
241
+ function rowRangeToHeightRatios(rowIndex, span, fromRow, toRowExclusive, rowHeights) {
242
+ const clampedFrom = Math.max(fromRow, rowIndex);
243
+ const clampedTo = Math.min(toRowExclusive, rowIndex + span);
244
+ if (clampedTo <= clampedFrom) {
245
+ return { offsetRatio: 0, lengthRatio: 0 };
246
+ }
247
+ if (!rowHeights || rowHeights.length !== span) {
248
+ return {
249
+ offsetRatio: (clampedFrom - rowIndex) / span,
250
+ lengthRatio: (clampedTo - clampedFrom) / span
251
+ };
252
+ }
253
+ const total = rowHeights.reduce((sum, height) => sum + height, 0) || 1;
254
+ let offsetPx = 0;
255
+ for (let i = 0; i < clampedFrom - rowIndex; i++) {
256
+ offsetPx += rowHeights[i] ?? 0;
257
+ }
258
+ let lengthPx = 0;
259
+ for (let i = clampedFrom - rowIndex; i < clampedTo - rowIndex; i++) {
260
+ lengthPx += rowHeights[i] ?? 0;
261
+ }
262
+ return {
263
+ offsetRatio: offsetPx / total,
264
+ lengthRatio: lengthPx / total,
265
+ offsetPx,
266
+ lengthPx
267
+ };
268
+ }
269
+ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt, rowHeights) {
216
270
  const cellEndRow = rowIndex + rowSpan - 1;
217
271
  const span = cellEndRow - rowIndex + 1;
218
272
  if (span <= 1) return [];
@@ -231,20 +285,30 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
231
285
  continue;
232
286
  }
233
287
  if (runStart !== null) {
234
- edges.push({
235
- side,
236
- offsetRatio: (runStart - rowIndex) / span,
237
- heightRatio: (row - runStart) / span
238
- });
288
+ const ratios = rowRangeToHeightRatios(
289
+ rowIndex,
290
+ span,
291
+ runStart,
292
+ row,
293
+ rowHeights
294
+ );
295
+ if (ratios.lengthRatio > 0) {
296
+ edges.push({ side, ...ratios });
297
+ }
239
298
  runStart = null;
240
299
  }
241
300
  }
242
301
  if (runStart !== null) {
243
- edges.push({
244
- side,
245
- offsetRatio: (runStart - rowIndex) / span,
246
- heightRatio: (toRowExclusive - runStart) / span
247
- });
302
+ const ratios = rowRangeToHeightRatios(
303
+ rowIndex,
304
+ span,
305
+ runStart,
306
+ toRowExclusive,
307
+ rowHeights
308
+ );
309
+ if (ratios.lengthRatio > 0) {
310
+ edges.push({ side, ...ratios });
311
+ }
248
312
  }
249
313
  };
250
314
  const collectSide = (side, neighborCol) => {
@@ -273,14 +337,17 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
273
337
  }
274
338
  return edges;
275
339
  }
276
- function buildPartialVerticalGradient(edge) {
340
+ function buildPartialEdgeGradient(edge) {
341
+ const usePx = edge.offsetPx != null && edge.lengthPx != null;
277
342
  const startPct = edge.offsetRatio * 100;
278
- const endPct = (edge.offsetRatio + edge.heightRatio) * 100;
279
- const overlapPx = SELECTION_EDGE_WIDTH_PX + 1;
280
- const isTopProtrusion = edge.offsetRatio === 0 && edge.heightRatio < 1;
281
- const isBottomProtrusion = edge.offsetRatio > 0;
282
- const startStop = isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
283
- const endStop = isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
343
+ const endPct = (edge.offsetRatio + edge.lengthRatio) * 100;
344
+ const startPx = edge.offsetPx ?? 0;
345
+ const endPx = (edge.offsetPx ?? 0) + (edge.lengthPx ?? 0);
346
+ const overlapPx = SELECTION_EDGE_WIDTH_PX;
347
+ const isTopProtrusion = (usePx ? startPx === 0 : edge.offsetRatio === 0) && edge.lengthRatio < 1;
348
+ const isBottomProtrusion = usePx ? startPx > 0 : edge.offsetRatio > 0;
349
+ const startStop = usePx ? isBottomProtrusion ? `${Math.max(0, startPx - overlapPx)}px` : `${startPx}px` : isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
350
+ const endStop = usePx ? isTopProtrusion ? `${endPx + overlapPx}px` : `${endPx}px` : isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
284
351
  const xPos = edge.side === "left" ? "0" : "100%";
285
352
  const layers = [
286
353
  {
@@ -290,7 +357,7 @@ function buildPartialVerticalGradient(edge) {
290
357
  }
291
358
  ];
292
359
  if (isTopProtrusion || isBottomProtrusion) {
293
- const capTop = isTopProtrusion ? `calc(${endPct}% - ${SELECTION_EDGE_WIDTH_PX}px)` : `calc(${startPct}% - ${SELECTION_EDGE_WIDTH_PX}px)`;
360
+ const capTop = usePx ? isTopProtrusion ? `${Math.max(0, endPx - SELECTION_EDGE_WIDTH_PX)}px` : `${Math.max(0, startPx - SELECTION_EDGE_WIDTH_PX)}px` : isTopProtrusion ? `calc(${endPct}% - ${SELECTION_EDGE_WIDTH_PX}px)` : `calc(${startPct}% - ${SELECTION_EDGE_WIDTH_PX}px)`;
294
361
  layers.push({
295
362
  image: `linear-gradient(${SELECTION_EDGE_COLOR}, ${SELECTION_EDGE_COLOR})`,
296
363
  size: `${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_WIDTH_PX}px`,
@@ -299,7 +366,7 @@ function buildPartialVerticalGradient(edge) {
299
366
  }
300
367
  return layers;
301
368
  }
302
- function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt) {
369
+ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt, rowHeights) {
303
370
  if (!isCellInSelection(rowIndex, colIndex, bounds, rowSpan) || !bounds)
304
371
  return void 0;
305
372
  const cellEndRow = rowIndex + rowSpan - 1;
@@ -308,39 +375,47 @@ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVi
308
375
  const isLeftEdge = colIndex === bounds.startCol;
309
376
  const isRightEdge = colIndex === bounds.endCol;
310
377
  const selectionContinuesBelow = cellEndRow < bounds.endRow;
311
- const shadows = [];
312
- if (isTopEdge) {
313
- shadows.push(
314
- `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
315
- );
316
- }
317
- if (isBottomEdge) {
318
- shadows.push(
319
- `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
320
- );
321
- }
322
- if (isLeftEdge) {
323
- shadows.push(
324
- `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
325
- );
326
- }
327
- if (isRightEdge) {
328
- shadows.push(
329
- `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
330
- );
331
- }
332
378
  const stepEdges = getMergedCellStepEdges(
333
379
  rowIndex,
334
380
  colIndex,
335
381
  bounds,
336
382
  rowSpan,
337
- isVisuallySelectedAt
383
+ isVisuallySelectedAt,
384
+ rowHeights
338
385
  );
386
+ const shadows = [];
387
+ const hasFullPerimeter = isTopEdge && isBottomEdge && isLeftEdge && isRightEdge && stepEdges.length === 0;
388
+ if (hasFullPerimeter) {
389
+ shadows.push(
390
+ `inset 0 0 0 ${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_COLOR}`
391
+ );
392
+ } else {
393
+ if (isTopEdge) {
394
+ shadows.push(
395
+ `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
396
+ );
397
+ }
398
+ if (isBottomEdge) {
399
+ shadows.push(
400
+ `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
401
+ );
402
+ }
403
+ if (isLeftEdge) {
404
+ shadows.push(
405
+ `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
406
+ );
407
+ }
408
+ if (isRightEdge) {
409
+ shadows.push(
410
+ `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
411
+ );
412
+ }
413
+ }
339
414
  const gradients = [];
340
415
  const sizes = [];
341
416
  const positions = [];
342
417
  for (const edge of stepEdges) {
343
- for (const partial of buildPartialVerticalGradient(edge)) {
418
+ for (const partial of buildPartialEdgeGradient(edge)) {
344
419
  gradients.push(partial.image);
345
420
  sizes.push(partial.size);
346
421
  positions.push(partial.position);
@@ -369,6 +444,127 @@ function hasCellSelectionEdges(style) {
369
444
  );
370
445
  }
371
446
 
447
+ // src/components/ui/table/features/cell-selection/copyData.ts
448
+ function formatCellValue(value) {
449
+ if (value === null || value === void 0) return "";
450
+ return String(value);
451
+ }
452
+ function getNestedValue(row, path) {
453
+ if (!path.includes(".")) return row[path];
454
+ return path.split(".").reduce((current, key) => {
455
+ if (current === null || current === void 0 || typeof current !== "object") {
456
+ return void 0;
457
+ }
458
+ return current[key];
459
+ }, row);
460
+ }
461
+ function readRowColumnValue(rowData, columnDef) {
462
+ if ("accessorFn" in columnDef && typeof columnDef.accessorFn === "function") {
463
+ return columnDef.accessorFn(rowData, 0);
464
+ }
465
+ if ("accessorKey" in columnDef && columnDef.accessorKey != null && columnDef.accessorKey !== "") {
466
+ return getNestedValue(rowData, String(columnDef.accessorKey));
467
+ }
468
+ return void 0;
469
+ }
470
+ function flattenSubtreeRows(row) {
471
+ const children = row.children;
472
+ if (!Array.isArray(children) || children.length === 0) return [];
473
+ const result = [];
474
+ const walk = (nodes) => {
475
+ for (const node of nodes) {
476
+ result.push(node);
477
+ const nested = node.children;
478
+ if (Array.isArray(nested) && nested.length > 0) {
479
+ walk(nested);
480
+ }
481
+ }
482
+ };
483
+ walk(children);
484
+ return result;
485
+ }
486
+ function hasSubtree(row) {
487
+ const children = row.children;
488
+ return Array.isArray(children) && children.length > 0;
489
+ }
490
+ function getOriginalRowId(original) {
491
+ return String(original.id ?? original.uniqueId ?? "");
492
+ }
493
+ function getRowDepth(original) {
494
+ return typeof original.level === "number" ? original.level : 0;
495
+ }
496
+ function collectCopyRowEntries(visibleRows, bounds, mode = "visible") {
497
+ const { startRow, endRow } = bounds;
498
+ const result = [];
499
+ const includedOriginalIds = /* @__PURE__ */ new Set();
500
+ const appendSubtree = (node, depth) => {
501
+ const children = node.children;
502
+ if (!Array.isArray(children) || children.length === 0) return;
503
+ for (const child of children) {
504
+ const childId = getOriginalRowId(child);
505
+ if (!(childId && includedOriginalIds.has(childId))) {
506
+ result.push({ row: child, depth });
507
+ if (childId) includedOriginalIds.add(childId);
508
+ }
509
+ appendSubtree(child, depth + 1);
510
+ }
511
+ };
512
+ for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) {
513
+ const row = visibleRows[rowIndex];
514
+ if (!row) continue;
515
+ const originalId = getOriginalRowId(row.original);
516
+ if (originalId && includedOriginalIds.has(originalId)) continue;
517
+ const depth = getRowDepth(row.original);
518
+ result.push({ row: row.original, depth });
519
+ if (originalId) includedOriginalIds.add(originalId);
520
+ if (mode !== "subtree" || !hasSubtree(row.original)) continue;
521
+ appendSubtree(row.original, depth + 1);
522
+ }
523
+ return result;
524
+ }
525
+ function collectCopyRows(visibleRows, bounds, mode = "visible") {
526
+ return collectCopyRowEntries(visibleRows, bounds, mode).map((entry) => entry.row);
527
+ }
528
+ function serializeCopyRowsToTSV(copyRows, visibleRows, bounds, depths) {
529
+ if (copyRows.length === 0) return "";
530
+ const { startCol, endCol } = bounds;
531
+ const columnCells = visibleRows[0]?.getVisibleCells().slice(startCol, endCol + 1) ?? [];
532
+ if (columnCells.length === 0) return "";
533
+ const resolvedDepths = depths && depths.length === copyRows.length ? depths : copyRows.map((row) => getRowDepth(row));
534
+ const minDepth = Math.min(...resolvedDepths);
535
+ return copyRows.map((rowData, index) => {
536
+ const relativeDepth = Math.max(0, (resolvedDepths[index] ?? 0) - minDepth);
537
+ const line = columnCells.map(
538
+ (cell) => formatCellValue(
539
+ readRowColumnValue(
540
+ rowData,
541
+ cell.column.columnDef
542
+ )
543
+ )
544
+ ).join(" ");
545
+ return `${" ".repeat(relativeDepth)}${line}`;
546
+ }).join("\n");
547
+ }
548
+ function serializeSelectionToTSV(visibleRows, bounds, mode = "visible") {
549
+ const entries = collectCopyRowEntries(visibleRows, bounds, mode);
550
+ return serializeCopyRowsToTSV(
551
+ entries.map((entry) => entry.row),
552
+ visibleRows,
553
+ bounds,
554
+ entries.map((entry) => entry.depth)
555
+ );
556
+ }
557
+ async function writeSelectionToClipboard(visibleRows, bounds, mode = "visible") {
558
+ const text = serializeSelectionToTSV(visibleRows, bounds, mode);
559
+ if (!text) return false;
560
+ try {
561
+ await navigator.clipboard.writeText(text);
562
+ } catch {
563
+ return false;
564
+ }
565
+ return true;
566
+ }
567
+
372
568
  // src/components/ui/table/features/cell-selection/fillData.ts
373
569
  function getColumnAccessorKey2(columnDef) {
374
570
  if ("accessorKey" in columnDef && columnDef.accessorKey) {
@@ -425,15 +621,103 @@ function hasFillExtension(sourceBounds, fillBounds) {
425
621
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
426
622
  }
427
623
 
624
+ // src/components/ui/table/features/cell-selection/pasteData.ts
625
+ function countLeadingEmptyCells(cells) {
626
+ let depth = 0;
627
+ while (depth < cells.length && cells[depth] === "") {
628
+ depth += 1;
629
+ }
630
+ return depth;
631
+ }
632
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
633
+ if (leadingEmptyCounts.length === 0) return false;
634
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
635
+ if (firstDepth !== 0) return false;
636
+ return leadingEmptyCounts.some((depth) => depth > 0);
637
+ }
638
+ function parseClipboardTSV(text) {
639
+ return parseClipboardTSVWithDepths(text).values;
640
+ }
641
+ function parseClipboardTSVWithDepths(text) {
642
+ if (!text) return { values: [], depths: [] };
643
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
644
+ const withoutTrailing = normalized.replace(/\n+$/, "");
645
+ if (!withoutTrailing) return { values: [], depths: [] };
646
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
647
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
648
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
649
+ const values = [];
650
+ const depths = [];
651
+ for (let index = 0; index < rows.length; index += 1) {
652
+ const cells = rows[index] ?? [];
653
+ const depth = leadingEmptyCounts[index] ?? 0;
654
+ if (treatAsDepth) {
655
+ values.push(cells.slice(depth));
656
+ depths.push(depth);
657
+ } else {
658
+ values.push(cells);
659
+ depths.push(0);
660
+ }
661
+ }
662
+ return { values, depths };
663
+ }
664
+ function resolvePasteColumnIds(rows, startCol, width) {
665
+ if (width <= 0) return [];
666
+ const cells = rows[0]?.getVisibleCells() ?? [];
667
+ const columnIds = [];
668
+ for (let offset = 0; offset < width; offset += 1) {
669
+ const cell = cells[startCol + offset];
670
+ if (!cell) break;
671
+ columnIds.push(cell.column.id);
672
+ }
673
+ return columnIds;
674
+ }
675
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
676
+ const { values, depths } = parseClipboardTSVWithDepths(text);
677
+ if (values.length === 0) return null;
678
+ const width = Math.max(...values.map((row) => row.length), 0);
679
+ if (width === 0) return null;
680
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
681
+ if (columnIds.length === 0) return null;
682
+ const rowIds = [];
683
+ for (let offset = 0; offset < values.length; offset += 1) {
684
+ const row = rows[startRow + offset];
685
+ if (!row) break;
686
+ rowIds.push(row.id);
687
+ }
688
+ const anchorRow = rows[endRow] ?? rows[startRow];
689
+ return {
690
+ mode,
691
+ startRow,
692
+ startCol,
693
+ endRow,
694
+ rowIds,
695
+ anchorRowId: anchorRow?.id ?? "",
696
+ columnIds,
697
+ values,
698
+ depths
699
+ };
700
+ }
701
+ function isEditablePasteTarget(target) {
702
+ if (!(target instanceof HTMLElement)) return false;
703
+ const tag = target.tagName;
704
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
705
+ return Boolean(target.isContentEditable);
706
+ }
707
+
428
708
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
429
709
  function useCellSelection({
430
710
  data,
431
711
  rows,
432
712
  enabled = true,
713
+ enableSubtreeCopy = false,
714
+ enableInsertPaste = true,
433
715
  onDataChange,
434
- onBatchChange
716
+ onBatchChange,
717
+ onRowsPaste
435
718
  }) {
436
719
  const [dragState, setDragState] = useState2(INITIAL_DRAG_STATE);
720
+ const pendingPasteModeRef = useRef2(null);
437
721
  const cellSelectionBounds = getCellSelectionBounds(dragState.start, dragState.end);
438
722
  const activeSelectionBounds = enabled ? getActiveSelectionBounds(dragState, cellSelectionBounds) : null;
439
723
  const handleCellMouseDown = useCallback2(
@@ -487,21 +771,113 @@ function useCellSelection({
487
771
  setDragState(INITIAL_DRAG_STATE);
488
772
  }
489
773
  }, [enabled]);
774
+ const copySelection = useCallback2(
775
+ async (options) => {
776
+ if (!enabled || !activeSelectionBounds) return false;
777
+ const mode = options?.includeDescendants && enableSubtreeCopy ? "subtree" : "visible";
778
+ return writeSelectionToClipboard(rows, activeSelectionBounds, mode);
779
+ },
780
+ [activeSelectionBounds, enableSubtreeCopy, enabled, rows]
781
+ );
490
782
  useEffect2(() => {
491
783
  if (!enabled) return;
492
784
  const handleKeyDown = (e) => {
493
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && activeSelectionBounds) {
494
- const { startRow, endRow, startCol, endCol } = activeSelectionBounds;
495
- const selectedData = rows.slice(startRow, endRow + 1).map((row) => {
496
- const cells = row.getVisibleCells();
497
- return cells.slice(startCol, endCol + 1).map((cell) => cell.getValue()).join(" ");
498
- }).join("\n");
499
- navigator.clipboard.writeText(selectedData);
500
- }
785
+ if (!activeSelectionBounds) return;
786
+ if (!(e.ctrlKey || e.metaKey)) return;
787
+ const isSubtreeShortcut = enableSubtreeCopy && e.shiftKey && e.key.toLowerCase() === "c";
788
+ const isVisibleCopyShortcut = !e.shiftKey && e.key.toLowerCase() === "c";
789
+ if (!isSubtreeShortcut && !isVisibleCopyShortcut) return;
790
+ e.preventDefault();
791
+ void copySelection({ includeDescendants: isSubtreeShortcut });
501
792
  };
502
793
  window.addEventListener("keydown", handleKeyDown);
503
794
  return () => window.removeEventListener("keydown", handleKeyDown);
504
- }, [activeSelectionBounds, enabled, rows]);
795
+ }, [activeSelectionBounds, copySelection, enableSubtreeCopy, enabled]);
796
+ const emitRowsPaste = useCallback2(
797
+ (text, mode) => {
798
+ if (!onRowsPaste || !activeSelectionBounds) return false;
799
+ const payload = buildRowsPastePayload(
800
+ rows,
801
+ activeSelectionBounds.startRow,
802
+ activeSelectionBounds.startCol,
803
+ text,
804
+ mode,
805
+ activeSelectionBounds.endRow
806
+ );
807
+ if (!payload) return false;
808
+ onRowsPaste(payload);
809
+ return true;
810
+ },
811
+ [activeSelectionBounds, onRowsPaste, rows]
812
+ );
813
+ useEffect2(() => {
814
+ if (!enabled || !onRowsPaste) return;
815
+ const pasteHandledRef = { current: false };
816
+ const ignoreNextPasteRef = { current: false };
817
+ const handleKeyDown = (e) => {
818
+ if (!activeSelectionBounds) return;
819
+ if (!(e.ctrlKey || e.metaKey)) return;
820
+ if (e.key.toLowerCase() !== "v") return;
821
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
822
+ return;
823
+ }
824
+ if (e.shiftKey && !enableInsertPaste) {
825
+ ignoreNextPasteRef.current = true;
826
+ pendingPasteModeRef.current = null;
827
+ return;
828
+ }
829
+ const mode = e.shiftKey ? "insert" : "overwrite";
830
+ pasteHandledRef.current = false;
831
+ ignoreNextPasteRef.current = false;
832
+ pendingPasteModeRef.current = mode;
833
+ void (async () => {
834
+ try {
835
+ const text = await navigator.clipboard.readText();
836
+ if (pasteHandledRef.current) return;
837
+ if (pendingPasteModeRef.current !== mode) return;
838
+ if (!text) return;
839
+ pasteHandledRef.current = true;
840
+ emitRowsPaste(text, mode);
841
+ pendingPasteModeRef.current = null;
842
+ } catch {
843
+ }
844
+ })();
845
+ };
846
+ const handlePaste = (e) => {
847
+ if (!activeSelectionBounds) return;
848
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
849
+ return;
850
+ }
851
+ if (ignoreNextPasteRef.current) {
852
+ ignoreNextPasteRef.current = false;
853
+ pendingPasteModeRef.current = null;
854
+ return;
855
+ }
856
+ const mode = pendingPasteModeRef.current ?? "overwrite";
857
+ if (pasteHandledRef.current) {
858
+ e.preventDefault();
859
+ return;
860
+ }
861
+ const text = e.clipboardData?.getData("text/plain");
862
+ if (text == null || text === "") return;
863
+ pasteHandledRef.current = true;
864
+ e.preventDefault();
865
+ emitRowsPaste(text, mode);
866
+ pendingPasteModeRef.current = null;
867
+ };
868
+ window.addEventListener("keydown", handleKeyDown);
869
+ window.addEventListener("paste", handlePaste);
870
+ return () => {
871
+ window.removeEventListener("keydown", handleKeyDown);
872
+ window.removeEventListener("paste", handlePaste);
873
+ };
874
+ }, [
875
+ activeSelectionBounds,
876
+ emitRowsPaste,
877
+ enableInsertPaste,
878
+ enabled,
879
+ onRowsPaste
880
+ ]);
505
881
  useEffect2(() => {
506
882
  if (!enabled) return;
507
883
  const handleMouseUp = () => {
@@ -547,12 +923,13 @@ function useCellSelection({
547
923
  activeSelectionBounds,
548
924
  handleCellMouseDown,
549
925
  handleCellMouseEnter,
550
- handleFillHandleMouseDown
926
+ handleFillHandleMouseDown,
927
+ copySelection
551
928
  };
552
929
  }
553
930
 
554
931
  // src/components/ui/table/features/row-expand/row-expand.ts
555
- import { useEffect as useEffect3, useMemo, useRef as useRef2 } from "react";
932
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
556
933
  function getFieldValue(row, key) {
557
934
  return row[key];
558
935
  }
@@ -582,8 +959,8 @@ var useConvertTreeData = ({
582
959
  expandedRows,
583
960
  onExpandedRowsChange
584
961
  }) => {
585
- const onExpandedRowsChangeRef = useRef2(onExpandedRowsChange);
586
- const hasInitializedRef = useRef2(false);
962
+ const onExpandedRowsChangeRef = useRef3(onExpandedRowsChange);
963
+ const hasInitializedRef = useRef3(false);
587
964
  useEffect3(() => {
588
965
  onExpandedRowsChangeRef.current = onExpandedRowsChange;
589
966
  }, [onExpandedRowsChange]);
@@ -629,15 +1006,16 @@ var useConvertTreeData = ({
629
1006
  children: [],
630
1007
  processed: false
631
1008
  }));
632
- const itemMap = /* @__PURE__ */ new Map();
633
- dataWithLevels.forEach((item) => {
634
- const key = getFieldValue(item, toggleField);
635
- if (typeof key !== "string" || !key) return;
636
- if (!itemMap.has(key)) {
637
- itemMap.set(key, []);
1009
+ const findNearestPrecedingParent = (index, parentKey) => {
1010
+ for (let i = index - 1; i >= 0; i -= 1) {
1011
+ const candidate = dataWithLevels[i];
1012
+ if (!candidate) continue;
1013
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1014
+ return candidate;
1015
+ }
638
1016
  }
639
- itemMap.get(key)?.push(item);
640
- });
1017
+ return void 0;
1018
+ };
641
1019
  const rootItems = [];
642
1020
  dataWithLevels.forEach((item) => {
643
1021
  if (!getFieldValue(item, childField)) {
@@ -645,29 +1023,18 @@ var useConvertTreeData = ({
645
1023
  item.processed = true;
646
1024
  }
647
1025
  });
648
- dataWithLevels.forEach((item) => {
1026
+ dataWithLevels.forEach((item, index) => {
649
1027
  const parentKey = getFieldValue(item, childField);
650
1028
  if (!parentKey || item.processed) return;
651
- const parentItems = dataWithLevels.filter(
652
- (parent) => getFieldValue(parent, toggleField) === parentKey && !getFieldValue(parent, childField)
653
- );
654
- if (parentItems.length > 0) {
655
- const parent = parentItems[0];
1029
+ const parent = findNearestPrecedingParent(index, parentKey);
1030
+ if (parent) {
656
1031
  item.level = parent.level + 1;
657
1032
  parent.children.push(item);
658
1033
  item.processed = true;
659
- } else {
660
- const otherParents = itemMap.get(String(parentKey)) || [];
661
- if (otherParents.length > 0) {
662
- const parent = otherParents[0];
663
- item.level = parent.level + 1;
664
- parent.children.push(item);
665
- item.processed = true;
666
- } else {
667
- rootItems.push(item);
668
- item.processed = true;
669
- }
1034
+ return;
670
1035
  }
1036
+ rootItems.push(item);
1037
+ item.processed = true;
671
1038
  });
672
1039
  return rootItems;
673
1040
  }, [enabled, data, toggleField, childField, flattenField]);
@@ -692,16 +1059,23 @@ var useConvertTreeData = ({
692
1059
  return result;
693
1060
  };
694
1061
  const flattenedData = flatten(processedData, [], 0);
695
- flattenedData.forEach((item) => {
696
- if (getFieldValue(item, childField)) {
697
- const parentItem = flattenedData.find(
698
- (parent) => getFieldValue(parent, toggleField) === getFieldValue(item, childField)
699
- );
700
- const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
701
- item.parentCount = parentAmount || 1;
702
- } else {
1062
+ flattenedData.forEach((item, index) => {
1063
+ const parentKey = getFieldValue(item, childField);
1064
+ if (!parentKey) {
703
1065
  item.parentCount = 1;
1066
+ return;
704
1067
  }
1068
+ let parentItem;
1069
+ for (let i = index - 1; i >= 0; i -= 1) {
1070
+ const candidate = flattenedData[i];
1071
+ if (!candidate) continue;
1072
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1073
+ parentItem = candidate;
1074
+ break;
1075
+ }
1076
+ }
1077
+ const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
1078
+ item.parentCount = parentAmount || 1;
705
1079
  });
706
1080
  return flattenedData;
707
1081
  }, [
@@ -848,6 +1222,10 @@ function useGlideTable(options) {
848
1222
  expandedRows: controlledExpandedRows,
849
1223
  onExpandedRowsChange,
850
1224
  preventExpand = false,
1225
+ enableSubtreeCopy,
1226
+ onCopyActionsReady,
1227
+ onRowsPaste,
1228
+ enableInsertPaste,
851
1229
  enableVirtualization = true,
852
1230
  estimateRowHeight = DATA_TABLE_ROW_HEIGHT,
853
1231
  virtualOverscan = DATA_TABLE_VIRTUAL_OVERSCAN
@@ -862,13 +1240,13 @@ function useGlideTable(options) {
862
1240
  };
863
1241
  }, [labelsProp, emptyText, loadingText, selectionLabel]);
864
1242
  const enableExpand = Boolean(toggleField);
1243
+ const resolvedEnableSubtreeCopy = enableSubtreeCopy ?? enableExpand;
865
1244
  const [internalRowSelection, setInternalRowSelection] = useState3({});
866
1245
  const [internalExpandedRows, setInternalExpandedRows] = useState3(
867
1246
  () => /* @__PURE__ */ new Set()
868
1247
  );
869
1248
  const [hoveredRowIndex, setHoveredRowIndex] = useState3(null);
870
- const [hoveredGroupKey, setHoveredGroupKey] = useState3(null);
871
- const scrollRef = useRef3(null);
1249
+ const scrollRef = useRef4(null);
872
1250
  const shouldVirtualize = enableVirtualization && !enableRowSpan;
873
1251
  useEffect4(() => {
874
1252
  if (enableVirtualization && enableRowSpan) {
@@ -931,6 +1309,7 @@ function useGlideTable(options) {
931
1309
  return collectRowSpanColumns(columns);
932
1310
  }, [enableRowSpan, columns]);
933
1311
  const primaryRowSpanKey = rowSpanColumnKeys[0]?.rowSpanKey;
1312
+ const primaryRowSpanColumnId = rowSpanColumnKeys[0]?.columnId;
934
1313
  const columnRowSpanMap = useMemo2(
935
1314
  () => buildColumnRowSpanMap(tableData, rowSpanColumnKeys),
936
1315
  [tableData, rowSpanColumnKeys]
@@ -949,27 +1328,29 @@ function useGlideTable(options) {
949
1328
  const totalSize = rowVirtualizer.getTotalSize();
950
1329
  const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
951
1330
  const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;
952
- const selectedGroupKeys = useMemo2(() => {
953
- if (!enableRowSpan || !primaryRowSpanKey) return /* @__PURE__ */ new Set();
954
- const keys = /* @__PURE__ */ new Set();
1331
+ const selectedRowIndices = useMemo2(() => {
1332
+ const indices = /* @__PURE__ */ new Set();
955
1333
  for (const selectedRow of selectedRows) {
956
- const value = selectedRow.original[primaryRowSpanKey];
957
- if (value !== null && value !== void 0) keys.add(String(value));
1334
+ indices.add(selectedRow.index);
958
1335
  }
959
- return keys;
960
- }, [enableRowSpan, primaryRowSpanKey, selectedRows]);
1336
+ return indices;
1337
+ }, [selectedRows]);
961
1338
  const {
962
1339
  dragState,
963
1340
  activeSelectionBounds,
964
1341
  handleCellMouseDown,
965
1342
  handleCellMouseEnter,
966
- handleFillHandleMouseDown
1343
+ handleFillHandleMouseDown,
1344
+ copySelection
967
1345
  } = useCellSelection({
968
1346
  data: tableData,
969
1347
  rows,
970
1348
  enabled: enableCellSelection,
1349
+ enableSubtreeCopy: resolvedEnableSubtreeCopy,
1350
+ enableInsertPaste: enableInsertPaste ?? true,
971
1351
  onDataChange,
972
- onBatchChange
1352
+ onBatchChange,
1353
+ onRowsPaste
973
1354
  });
974
1355
  const {
975
1356
  editingCell,
@@ -991,22 +1372,10 @@ function useGlideTable(options) {
991
1372
  );
992
1373
  const clearHover = useCallback3(() => {
993
1374
  setHoveredRowIndex(null);
994
- setHoveredGroupKey(null);
995
1375
  }, []);
996
- const handleRowHover = useCallback3(
997
- (rowIndex, rowData) => {
998
- setHoveredRowIndex(rowIndex);
999
- if (!primaryRowSpanKey) {
1000
- setHoveredGroupKey(null);
1001
- return;
1002
- }
1003
- const groupValue = rowData[primaryRowSpanKey];
1004
- setHoveredGroupKey(
1005
- groupValue === null || groupValue === void 0 ? null : String(groupValue)
1006
- );
1007
- },
1008
- [primaryRowSpanKey]
1009
- );
1376
+ const handleRowHover = useCallback3((rowIndex, _rowData) => {
1377
+ setHoveredRowIndex(rowIndex);
1378
+ }, []);
1010
1379
  const handleToggleSelect = useCallback3(
1011
1380
  (row) => {
1012
1381
  if (!row.getCanSelect()) return;
@@ -1029,10 +1398,10 @@ function useGlideTable(options) {
1029
1398
  rowSpan: {
1030
1399
  enableRowSpan,
1031
1400
  primaryRowSpanKey,
1401
+ primaryRowSpanColumnId,
1032
1402
  columnRowSpanMap,
1033
1403
  hoveredRowIndex,
1034
- hoveredGroupKey,
1035
- selectedGroupKeys,
1404
+ selectedRowIndices,
1036
1405
  onRowHover: handleRowHover
1037
1406
  },
1038
1407
  selection: {
@@ -1070,10 +1439,10 @@ function useGlideTable(options) {
1070
1439
  }, [
1071
1440
  enableRowSpan,
1072
1441
  primaryRowSpanKey,
1442
+ primaryRowSpanColumnId,
1073
1443
  columnRowSpanMap,
1074
1444
  hoveredRowIndex,
1075
- hoveredGroupKey,
1076
- selectedGroupKeys,
1445
+ selectedRowIndices,
1077
1446
  handleRowHover,
1078
1447
  rowSelectionMode,
1079
1448
  selectOnRowClick,
@@ -1099,6 +1468,14 @@ function useGlideTable(options) {
1099
1468
  labels.expandRow,
1100
1469
  labels.collapseRow
1101
1470
  ]);
1471
+ const copySelectionRef = useRef4(copySelection);
1472
+ useEffect4(() => {
1473
+ copySelectionRef.current = copySelection;
1474
+ }, [copySelection]);
1475
+ const stableCopySelection = useCallback3((options2) => copySelectionRef.current(options2), []);
1476
+ useEffect4(() => {
1477
+ onCopyActionsReady?.({ copySelection: stableCopySelection });
1478
+ }, [onCopyActionsReady, stableCopySelection]);
1102
1479
  return {
1103
1480
  table,
1104
1481
  tableData,
@@ -1118,7 +1495,8 @@ function useGlideTable(options) {
1118
1495
  paddingBottom,
1119
1496
  rowContextValue,
1120
1497
  handleToggleSelect,
1121
- clearHover
1498
+ clearHover,
1499
+ copySelection: stableCopySelection
1122
1500
  };
1123
1501
  }
1124
1502
  export {
@@ -1132,9 +1510,13 @@ export {
1132
1510
  applyFillData,
1133
1511
  applySelectionUpdater,
1134
1512
  buildColumnRowSpanMap,
1513
+ buildRowsPastePayload,
1135
1514
  canExpandRow,
1515
+ collectCopyRowEntries,
1516
+ collectCopyRows,
1136
1517
  collectFillChanges,
1137
1518
  collectRowSpanColumns,
1519
+ flattenSubtreeRows,
1138
1520
  getCellEditDraftValue,
1139
1521
  getCellSelectionEdgeStyle,
1140
1522
  getColumnEditType,
@@ -1142,13 +1524,22 @@ export {
1142
1524
  hasCellSelectionEdges,
1143
1525
  isCellInSelection,
1144
1526
  isColumnEditable,
1527
+ isEditablePasteTarget,
1528
+ measureMergedSpanRowHeights,
1145
1529
  parseCellEditValue,
1530
+ parseClipboardTSV,
1531
+ parseClipboardTSVWithDepths,
1146
1532
  resolveDataTableLabels,
1533
+ resolvePasteColumnIds,
1147
1534
  resolveRowSelection,
1148
1535
  resolveRowSpanAt,
1536
+ rowRangeToHeightRatios,
1537
+ serializeCopyRowsToTSV,
1538
+ serializeSelectionToTSV,
1149
1539
  toggleExpandedRowId,
1150
1540
  useCellEdit,
1151
1541
  useCellSelection,
1152
1542
  useConvertTreeData,
1153
- useGlideTable
1543
+ useGlideTable,
1544
+ writeSelectionToClipboard
1154
1545
  };