react-glide-table 1.1.5 → 1.1.7

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/index.cjs CHANGED
@@ -32,10 +32,14 @@ __export(src_exports, {
32
32
  applyFillData: () => applyFillData,
33
33
  applySelectionUpdater: () => applySelectionUpdater,
34
34
  buildColumnRowSpanMap: () => buildColumnRowSpanMap,
35
+ buildRowsPastePayload: () => buildRowsPastePayload,
35
36
  canExpandRow: () => canExpandRow,
37
+ collectCopyRowEntries: () => collectCopyRowEntries,
38
+ collectCopyRows: () => collectCopyRows,
36
39
  collectFillChanges: () => collectFillChanges,
37
40
  collectRowSpanColumns: () => collectRowSpanColumns,
38
41
  createTable: () => createTable,
42
+ flattenSubtreeRows: () => flattenSubtreeRows,
39
43
  getCellEditDraftValue: () => getCellEditDraftValue,
40
44
  getCellSelectionEdgeStyle: () => getCellSelectionEdgeStyle,
41
45
  getColumnEditType: () => getColumnEditType,
@@ -43,17 +47,24 @@ __export(src_exports, {
43
47
  hasCellSelectionEdges: () => hasCellSelectionEdges,
44
48
  isCellInSelection: () => isCellInSelection,
45
49
  isColumnEditable: () => isColumnEditable,
50
+ isEditablePasteTarget: () => isEditablePasteTarget,
46
51
  measureMergedSpanRowHeights: () => measureMergedSpanRowHeights,
47
52
  parseCellEditValue: () => parseCellEditValue,
53
+ parseClipboardTSV: () => parseClipboardTSV,
54
+ parseClipboardTSVWithDepths: () => parseClipboardTSVWithDepths,
48
55
  resolveDataTableLabels: () => resolveDataTableLabels,
56
+ resolvePasteColumnIds: () => resolvePasteColumnIds,
49
57
  resolveRowSelection: () => resolveRowSelection,
50
58
  resolveRowSpanAt: () => resolveRowSpanAt,
51
59
  rowRangeToHeightRatios: () => rowRangeToHeightRatios,
60
+ serializeCopyRowsToTSV: () => serializeCopyRowsToTSV,
61
+ serializeSelectionToTSV: () => serializeSelectionToTSV,
52
62
  toggleExpandedRowId: () => toggleExpandedRowId,
53
63
  useCellEdit: () => useCellEdit,
54
64
  useCellSelection: () => useCellSelection,
55
65
  useConvertTreeData: () => useConvertTreeData,
56
- useGlideTable: () => useGlideTable
66
+ useGlideTable: () => useGlideTable,
67
+ writeSelectionToClipboard: () => writeSelectionToClipboard
57
68
  });
58
69
  module.exports = __toCommonJS(src_exports);
59
70
 
@@ -500,6 +511,127 @@ function hasCellSelectionEdges(style) {
500
511
  );
501
512
  }
502
513
 
514
+ // src/components/ui/table/features/cell-selection/copyData.ts
515
+ function formatCellValue(value) {
516
+ if (value === null || value === void 0) return "";
517
+ return String(value);
518
+ }
519
+ function getNestedValue(row, path) {
520
+ if (!path.includes(".")) return row[path];
521
+ return path.split(".").reduce((current, key) => {
522
+ if (current === null || current === void 0 || typeof current !== "object") {
523
+ return void 0;
524
+ }
525
+ return current[key];
526
+ }, row);
527
+ }
528
+ function readRowColumnValue(rowData, columnDef) {
529
+ if ("accessorFn" in columnDef && typeof columnDef.accessorFn === "function") {
530
+ return columnDef.accessorFn(rowData, 0);
531
+ }
532
+ if ("accessorKey" in columnDef && columnDef.accessorKey != null && columnDef.accessorKey !== "") {
533
+ return getNestedValue(rowData, String(columnDef.accessorKey));
534
+ }
535
+ return void 0;
536
+ }
537
+ function flattenSubtreeRows(row) {
538
+ const children = row.children;
539
+ if (!Array.isArray(children) || children.length === 0) return [];
540
+ const result = [];
541
+ const walk = (nodes) => {
542
+ for (const node of nodes) {
543
+ result.push(node);
544
+ const nested = node.children;
545
+ if (Array.isArray(nested) && nested.length > 0) {
546
+ walk(nested);
547
+ }
548
+ }
549
+ };
550
+ walk(children);
551
+ return result;
552
+ }
553
+ function hasSubtree(row) {
554
+ const children = row.children;
555
+ return Array.isArray(children) && children.length > 0;
556
+ }
557
+ function getOriginalRowId(original) {
558
+ return String(original.id ?? original.uniqueId ?? "");
559
+ }
560
+ function getRowDepth(original) {
561
+ return typeof original.level === "number" ? original.level : 0;
562
+ }
563
+ function collectCopyRowEntries(visibleRows, bounds, mode = "visible") {
564
+ const { startRow, endRow } = bounds;
565
+ const result = [];
566
+ const includedOriginalIds = /* @__PURE__ */ new Set();
567
+ const appendSubtree = (node, depth) => {
568
+ const children = node.children;
569
+ if (!Array.isArray(children) || children.length === 0) return;
570
+ for (const child of children) {
571
+ const childId = getOriginalRowId(child);
572
+ if (!(childId && includedOriginalIds.has(childId))) {
573
+ result.push({ row: child, depth });
574
+ if (childId) includedOriginalIds.add(childId);
575
+ }
576
+ appendSubtree(child, depth + 1);
577
+ }
578
+ };
579
+ for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) {
580
+ const row = visibleRows[rowIndex];
581
+ if (!row) continue;
582
+ const originalId = getOriginalRowId(row.original);
583
+ if (originalId && includedOriginalIds.has(originalId)) continue;
584
+ const depth = getRowDepth(row.original);
585
+ result.push({ row: row.original, depth });
586
+ if (originalId) includedOriginalIds.add(originalId);
587
+ if (mode !== "subtree" || !hasSubtree(row.original)) continue;
588
+ appendSubtree(row.original, depth + 1);
589
+ }
590
+ return result;
591
+ }
592
+ function collectCopyRows(visibleRows, bounds, mode = "visible") {
593
+ return collectCopyRowEntries(visibleRows, bounds, mode).map((entry) => entry.row);
594
+ }
595
+ function serializeCopyRowsToTSV(copyRows, visibleRows, bounds, depths) {
596
+ if (copyRows.length === 0) return "";
597
+ const { startCol, endCol } = bounds;
598
+ const columnCells = visibleRows[0]?.getVisibleCells().slice(startCol, endCol + 1) ?? [];
599
+ if (columnCells.length === 0) return "";
600
+ const resolvedDepths = depths && depths.length === copyRows.length ? depths : copyRows.map((row) => getRowDepth(row));
601
+ const minDepth = Math.min(...resolvedDepths);
602
+ return copyRows.map((rowData, index) => {
603
+ const relativeDepth = Math.max(0, (resolvedDepths[index] ?? 0) - minDepth);
604
+ const line = columnCells.map(
605
+ (cell) => formatCellValue(
606
+ readRowColumnValue(
607
+ rowData,
608
+ cell.column.columnDef
609
+ )
610
+ )
611
+ ).join(" ");
612
+ return `${" ".repeat(relativeDepth)}${line}`;
613
+ }).join("\n");
614
+ }
615
+ function serializeSelectionToTSV(visibleRows, bounds, mode = "visible") {
616
+ const entries = collectCopyRowEntries(visibleRows, bounds, mode);
617
+ return serializeCopyRowsToTSV(
618
+ entries.map((entry) => entry.row),
619
+ visibleRows,
620
+ bounds,
621
+ entries.map((entry) => entry.depth)
622
+ );
623
+ }
624
+ async function writeSelectionToClipboard(visibleRows, bounds, mode = "visible") {
625
+ const text = serializeSelectionToTSV(visibleRows, bounds, mode);
626
+ if (!text) return false;
627
+ try {
628
+ await navigator.clipboard.writeText(text);
629
+ } catch {
630
+ return false;
631
+ }
632
+ return true;
633
+ }
634
+
503
635
  // src/components/ui/table/features/cell-selection/fillData.ts
504
636
  function getColumnAccessorKey2(columnDef) {
505
637
  if ("accessorKey" in columnDef && columnDef.accessorKey) {
@@ -556,15 +688,103 @@ function hasFillExtension(sourceBounds, fillBounds) {
556
688
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
557
689
  }
558
690
 
691
+ // src/components/ui/table/features/cell-selection/pasteData.ts
692
+ function countLeadingEmptyCells(cells) {
693
+ let depth = 0;
694
+ while (depth < cells.length && cells[depth] === "") {
695
+ depth += 1;
696
+ }
697
+ return depth;
698
+ }
699
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
700
+ if (leadingEmptyCounts.length === 0) return false;
701
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
702
+ if (firstDepth !== 0) return false;
703
+ return leadingEmptyCounts.some((depth) => depth > 0);
704
+ }
705
+ function parseClipboardTSV(text) {
706
+ return parseClipboardTSVWithDepths(text).values;
707
+ }
708
+ function parseClipboardTSVWithDepths(text) {
709
+ if (!text) return { values: [], depths: [] };
710
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
711
+ const withoutTrailing = normalized.replace(/\n+$/, "");
712
+ if (!withoutTrailing) return { values: [], depths: [] };
713
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
714
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
715
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
716
+ const values = [];
717
+ const depths = [];
718
+ for (let index = 0; index < rows.length; index += 1) {
719
+ const cells = rows[index] ?? [];
720
+ const depth = leadingEmptyCounts[index] ?? 0;
721
+ if (treatAsDepth) {
722
+ values.push(cells.slice(depth));
723
+ depths.push(depth);
724
+ } else {
725
+ values.push(cells);
726
+ depths.push(0);
727
+ }
728
+ }
729
+ return { values, depths };
730
+ }
731
+ function resolvePasteColumnIds(rows, startCol, width) {
732
+ if (width <= 0) return [];
733
+ const cells = rows[0]?.getVisibleCells() ?? [];
734
+ const columnIds = [];
735
+ for (let offset = 0; offset < width; offset += 1) {
736
+ const cell = cells[startCol + offset];
737
+ if (!cell) break;
738
+ columnIds.push(cell.column.id);
739
+ }
740
+ return columnIds;
741
+ }
742
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
743
+ const { values, depths } = parseClipboardTSVWithDepths(text);
744
+ if (values.length === 0) return null;
745
+ const width = Math.max(...values.map((row) => row.length), 0);
746
+ if (width === 0) return null;
747
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
748
+ if (columnIds.length === 0) return null;
749
+ const rowIds = [];
750
+ for (let offset = 0; offset < values.length; offset += 1) {
751
+ const row = rows[startRow + offset];
752
+ if (!row) break;
753
+ rowIds.push(row.id);
754
+ }
755
+ const anchorRow = rows[endRow] ?? rows[startRow];
756
+ return {
757
+ mode,
758
+ startRow,
759
+ startCol,
760
+ endRow,
761
+ rowIds,
762
+ anchorRowId: anchorRow?.id ?? "",
763
+ columnIds,
764
+ values,
765
+ depths
766
+ };
767
+ }
768
+ function isEditablePasteTarget(target) {
769
+ if (!(target instanceof HTMLElement)) return false;
770
+ const tag = target.tagName;
771
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
772
+ return Boolean(target.isContentEditable);
773
+ }
774
+
559
775
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
560
776
  function useCellSelection({
561
777
  data,
562
778
  rows,
563
779
  enabled = true,
780
+ enableSubtreeCopy = false,
781
+ enableInsertPaste = true,
564
782
  onDataChange,
565
- onBatchChange
783
+ onBatchChange,
784
+ onRowsPaste
566
785
  }) {
567
786
  const [dragState, setDragState] = (0, import_react2.useState)(INITIAL_DRAG_STATE);
787
+ const pendingPasteModeRef = (0, import_react2.useRef)(null);
568
788
  const cellSelectionBounds = getCellSelectionBounds(dragState.start, dragState.end);
569
789
  const activeSelectionBounds = enabled ? getActiveSelectionBounds(dragState, cellSelectionBounds) : null;
570
790
  const handleCellMouseDown = (0, import_react2.useCallback)(
@@ -618,21 +838,113 @@ function useCellSelection({
618
838
  setDragState(INITIAL_DRAG_STATE);
619
839
  }
620
840
  }, [enabled]);
841
+ const copySelection = (0, import_react2.useCallback)(
842
+ async (options) => {
843
+ if (!enabled || !activeSelectionBounds) return false;
844
+ const mode = options?.includeDescendants && enableSubtreeCopy ? "subtree" : "visible";
845
+ return writeSelectionToClipboard(rows, activeSelectionBounds, mode);
846
+ },
847
+ [activeSelectionBounds, enableSubtreeCopy, enabled, rows]
848
+ );
621
849
  (0, import_react2.useEffect)(() => {
622
850
  if (!enabled) return;
623
851
  const handleKeyDown = (e) => {
624
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && activeSelectionBounds) {
625
- const { startRow, endRow, startCol, endCol } = activeSelectionBounds;
626
- const selectedData = rows.slice(startRow, endRow + 1).map((row) => {
627
- const cells = row.getVisibleCells();
628
- return cells.slice(startCol, endCol + 1).map((cell) => cell.getValue()).join(" ");
629
- }).join("\n");
630
- navigator.clipboard.writeText(selectedData);
631
- }
852
+ if (!activeSelectionBounds) return;
853
+ if (!(e.ctrlKey || e.metaKey)) return;
854
+ const isSubtreeShortcut = enableSubtreeCopy && e.shiftKey && e.key.toLowerCase() === "c";
855
+ const isVisibleCopyShortcut = !e.shiftKey && e.key.toLowerCase() === "c";
856
+ if (!isSubtreeShortcut && !isVisibleCopyShortcut) return;
857
+ e.preventDefault();
858
+ void copySelection({ includeDescendants: isSubtreeShortcut });
632
859
  };
633
860
  window.addEventListener("keydown", handleKeyDown);
634
861
  return () => window.removeEventListener("keydown", handleKeyDown);
635
- }, [activeSelectionBounds, enabled, rows]);
862
+ }, [activeSelectionBounds, copySelection, enableSubtreeCopy, enabled]);
863
+ const emitRowsPaste = (0, import_react2.useCallback)(
864
+ (text, mode) => {
865
+ if (!onRowsPaste || !activeSelectionBounds) return false;
866
+ const payload = buildRowsPastePayload(
867
+ rows,
868
+ activeSelectionBounds.startRow,
869
+ activeSelectionBounds.startCol,
870
+ text,
871
+ mode,
872
+ activeSelectionBounds.endRow
873
+ );
874
+ if (!payload) return false;
875
+ onRowsPaste(payload);
876
+ return true;
877
+ },
878
+ [activeSelectionBounds, onRowsPaste, rows]
879
+ );
880
+ (0, import_react2.useEffect)(() => {
881
+ if (!enabled || !onRowsPaste) return;
882
+ const pasteHandledRef = { current: false };
883
+ const ignoreNextPasteRef = { current: false };
884
+ const handleKeyDown = (e) => {
885
+ if (!activeSelectionBounds) return;
886
+ if (!(e.ctrlKey || e.metaKey)) return;
887
+ if (e.key.toLowerCase() !== "v") return;
888
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
889
+ return;
890
+ }
891
+ if (e.shiftKey && !enableInsertPaste) {
892
+ ignoreNextPasteRef.current = true;
893
+ pendingPasteModeRef.current = null;
894
+ return;
895
+ }
896
+ const mode = e.shiftKey ? "insert" : "overwrite";
897
+ pasteHandledRef.current = false;
898
+ ignoreNextPasteRef.current = false;
899
+ pendingPasteModeRef.current = mode;
900
+ void (async () => {
901
+ try {
902
+ const text = await navigator.clipboard.readText();
903
+ if (pasteHandledRef.current) return;
904
+ if (pendingPasteModeRef.current !== mode) return;
905
+ if (!text) return;
906
+ pasteHandledRef.current = true;
907
+ emitRowsPaste(text, mode);
908
+ pendingPasteModeRef.current = null;
909
+ } catch {
910
+ }
911
+ })();
912
+ };
913
+ const handlePaste = (e) => {
914
+ if (!activeSelectionBounds) return;
915
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
916
+ return;
917
+ }
918
+ if (ignoreNextPasteRef.current) {
919
+ ignoreNextPasteRef.current = false;
920
+ pendingPasteModeRef.current = null;
921
+ return;
922
+ }
923
+ const mode = pendingPasteModeRef.current ?? "overwrite";
924
+ if (pasteHandledRef.current) {
925
+ e.preventDefault();
926
+ return;
927
+ }
928
+ const text = e.clipboardData?.getData("text/plain");
929
+ if (text == null || text === "") return;
930
+ pasteHandledRef.current = true;
931
+ e.preventDefault();
932
+ emitRowsPaste(text, mode);
933
+ pendingPasteModeRef.current = null;
934
+ };
935
+ window.addEventListener("keydown", handleKeyDown);
936
+ window.addEventListener("paste", handlePaste);
937
+ return () => {
938
+ window.removeEventListener("keydown", handleKeyDown);
939
+ window.removeEventListener("paste", handlePaste);
940
+ };
941
+ }, [
942
+ activeSelectionBounds,
943
+ emitRowsPaste,
944
+ enableInsertPaste,
945
+ enabled,
946
+ onRowsPaste
947
+ ]);
636
948
  (0, import_react2.useEffect)(() => {
637
949
  if (!enabled) return;
638
950
  const handleMouseUp = () => {
@@ -678,7 +990,8 @@ function useCellSelection({
678
990
  activeSelectionBounds,
679
991
  handleCellMouseDown,
680
992
  handleCellMouseEnter,
681
- handleFillHandleMouseDown
993
+ handleFillHandleMouseDown,
994
+ copySelection
682
995
  };
683
996
  }
684
997
 
@@ -760,15 +1073,16 @@ var useConvertTreeData = ({
760
1073
  children: [],
761
1074
  processed: false
762
1075
  }));
763
- const itemMap = /* @__PURE__ */ new Map();
764
- dataWithLevels.forEach((item) => {
765
- const key = getFieldValue(item, toggleField);
766
- if (typeof key !== "string" || !key) return;
767
- if (!itemMap.has(key)) {
768
- itemMap.set(key, []);
1076
+ const findNearestPrecedingParent = (index, parentKey) => {
1077
+ for (let i = index - 1; i >= 0; i -= 1) {
1078
+ const candidate = dataWithLevels[i];
1079
+ if (!candidate) continue;
1080
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1081
+ return candidate;
1082
+ }
769
1083
  }
770
- itemMap.get(key)?.push(item);
771
- });
1084
+ return void 0;
1085
+ };
772
1086
  const rootItems = [];
773
1087
  dataWithLevels.forEach((item) => {
774
1088
  if (!getFieldValue(item, childField)) {
@@ -776,29 +1090,18 @@ var useConvertTreeData = ({
776
1090
  item.processed = true;
777
1091
  }
778
1092
  });
779
- dataWithLevels.forEach((item) => {
1093
+ dataWithLevels.forEach((item, index) => {
780
1094
  const parentKey = getFieldValue(item, childField);
781
1095
  if (!parentKey || item.processed) return;
782
- const parentItems = dataWithLevels.filter(
783
- (parent) => getFieldValue(parent, toggleField) === parentKey && !getFieldValue(parent, childField)
784
- );
785
- if (parentItems.length > 0) {
786
- const parent = parentItems[0];
1096
+ const parent = findNearestPrecedingParent(index, parentKey);
1097
+ if (parent) {
787
1098
  item.level = parent.level + 1;
788
1099
  parent.children.push(item);
789
1100
  item.processed = true;
790
- } else {
791
- const otherParents = itemMap.get(String(parentKey)) || [];
792
- if (otherParents.length > 0) {
793
- const parent = otherParents[0];
794
- item.level = parent.level + 1;
795
- parent.children.push(item);
796
- item.processed = true;
797
- } else {
798
- rootItems.push(item);
799
- item.processed = true;
800
- }
1101
+ return;
801
1102
  }
1103
+ rootItems.push(item);
1104
+ item.processed = true;
802
1105
  });
803
1106
  return rootItems;
804
1107
  }, [enabled, data, toggleField, childField, flattenField]);
@@ -823,16 +1126,23 @@ var useConvertTreeData = ({
823
1126
  return result;
824
1127
  };
825
1128
  const flattenedData = flatten(processedData, [], 0);
826
- flattenedData.forEach((item) => {
827
- if (getFieldValue(item, childField)) {
828
- const parentItem = flattenedData.find(
829
- (parent) => getFieldValue(parent, toggleField) === getFieldValue(item, childField)
830
- );
831
- const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
832
- item.parentCount = parentAmount || 1;
833
- } else {
1129
+ flattenedData.forEach((item, index) => {
1130
+ const parentKey = getFieldValue(item, childField);
1131
+ if (!parentKey) {
834
1132
  item.parentCount = 1;
1133
+ return;
835
1134
  }
1135
+ let parentItem;
1136
+ for (let i = index - 1; i >= 0; i -= 1) {
1137
+ const candidate = flattenedData[i];
1138
+ if (!candidate) continue;
1139
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1140
+ parentItem = candidate;
1141
+ break;
1142
+ }
1143
+ }
1144
+ const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
1145
+ item.parentCount = parentAmount || 1;
836
1146
  });
837
1147
  return flattenedData;
838
1148
  }, [
@@ -979,6 +1289,10 @@ function useGlideTable(options) {
979
1289
  expandedRows: controlledExpandedRows,
980
1290
  onExpandedRowsChange,
981
1291
  preventExpand = false,
1292
+ enableSubtreeCopy,
1293
+ onCopyActionsReady,
1294
+ onRowsPaste,
1295
+ enableInsertPaste,
982
1296
  enableVirtualization = true,
983
1297
  estimateRowHeight = DATA_TABLE_ROW_HEIGHT,
984
1298
  virtualOverscan = DATA_TABLE_VIRTUAL_OVERSCAN
@@ -993,12 +1307,12 @@ function useGlideTable(options) {
993
1307
  };
994
1308
  }, [labelsProp, emptyText, loadingText, selectionLabel]);
995
1309
  const enableExpand = Boolean(toggleField);
1310
+ const resolvedEnableSubtreeCopy = enableSubtreeCopy ?? enableExpand;
996
1311
  const [internalRowSelection, setInternalRowSelection] = (0, import_react4.useState)({});
997
1312
  const [internalExpandedRows, setInternalExpandedRows] = (0, import_react4.useState)(
998
1313
  () => /* @__PURE__ */ new Set()
999
1314
  );
1000
1315
  const [hoveredRowIndex, setHoveredRowIndex] = (0, import_react4.useState)(null);
1001
- const [hoveredGroupKey, setHoveredGroupKey] = (0, import_react4.useState)(null);
1002
1316
  const scrollRef = (0, import_react4.useRef)(null);
1003
1317
  const shouldVirtualize = enableVirtualization && !enableRowSpan;
1004
1318
  (0, import_react4.useEffect)(() => {
@@ -1062,6 +1376,7 @@ function useGlideTable(options) {
1062
1376
  return collectRowSpanColumns(columns);
1063
1377
  }, [enableRowSpan, columns]);
1064
1378
  const primaryRowSpanKey = rowSpanColumnKeys[0]?.rowSpanKey;
1379
+ const primaryRowSpanColumnId = rowSpanColumnKeys[0]?.columnId;
1065
1380
  const columnRowSpanMap = (0, import_react4.useMemo)(
1066
1381
  () => buildColumnRowSpanMap(tableData, rowSpanColumnKeys),
1067
1382
  [tableData, rowSpanColumnKeys]
@@ -1080,27 +1395,29 @@ function useGlideTable(options) {
1080
1395
  const totalSize = rowVirtualizer.getTotalSize();
1081
1396
  const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
1082
1397
  const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;
1083
- const selectedGroupKeys = (0, import_react4.useMemo)(() => {
1084
- if (!enableRowSpan || !primaryRowSpanKey) return /* @__PURE__ */ new Set();
1085
- const keys = /* @__PURE__ */ new Set();
1398
+ const selectedRowIndices = (0, import_react4.useMemo)(() => {
1399
+ const indices = /* @__PURE__ */ new Set();
1086
1400
  for (const selectedRow of selectedRows) {
1087
- const value = selectedRow.original[primaryRowSpanKey];
1088
- if (value !== null && value !== void 0) keys.add(String(value));
1401
+ indices.add(selectedRow.index);
1089
1402
  }
1090
- return keys;
1091
- }, [enableRowSpan, primaryRowSpanKey, selectedRows]);
1403
+ return indices;
1404
+ }, [selectedRows]);
1092
1405
  const {
1093
1406
  dragState,
1094
1407
  activeSelectionBounds,
1095
1408
  handleCellMouseDown,
1096
1409
  handleCellMouseEnter,
1097
- handleFillHandleMouseDown
1410
+ handleFillHandleMouseDown,
1411
+ copySelection
1098
1412
  } = useCellSelection({
1099
1413
  data: tableData,
1100
1414
  rows,
1101
1415
  enabled: enableCellSelection,
1416
+ enableSubtreeCopy: resolvedEnableSubtreeCopy,
1417
+ enableInsertPaste: enableInsertPaste ?? true,
1102
1418
  onDataChange,
1103
- onBatchChange
1419
+ onBatchChange,
1420
+ onRowsPaste
1104
1421
  });
1105
1422
  const {
1106
1423
  editingCell,
@@ -1122,22 +1439,10 @@ function useGlideTable(options) {
1122
1439
  );
1123
1440
  const clearHover = (0, import_react4.useCallback)(() => {
1124
1441
  setHoveredRowIndex(null);
1125
- setHoveredGroupKey(null);
1126
1442
  }, []);
1127
- const handleRowHover = (0, import_react4.useCallback)(
1128
- (rowIndex, rowData) => {
1129
- setHoveredRowIndex(rowIndex);
1130
- if (!primaryRowSpanKey) {
1131
- setHoveredGroupKey(null);
1132
- return;
1133
- }
1134
- const groupValue = rowData[primaryRowSpanKey];
1135
- setHoveredGroupKey(
1136
- groupValue === null || groupValue === void 0 ? null : String(groupValue)
1137
- );
1138
- },
1139
- [primaryRowSpanKey]
1140
- );
1443
+ const handleRowHover = (0, import_react4.useCallback)((rowIndex, _rowData) => {
1444
+ setHoveredRowIndex(rowIndex);
1445
+ }, []);
1141
1446
  const handleToggleSelect = (0, import_react4.useCallback)(
1142
1447
  (row) => {
1143
1448
  if (!row.getCanSelect()) return;
@@ -1160,10 +1465,10 @@ function useGlideTable(options) {
1160
1465
  rowSpan: {
1161
1466
  enableRowSpan,
1162
1467
  primaryRowSpanKey,
1468
+ primaryRowSpanColumnId,
1163
1469
  columnRowSpanMap,
1164
1470
  hoveredRowIndex,
1165
- hoveredGroupKey,
1166
- selectedGroupKeys,
1471
+ selectedRowIndices,
1167
1472
  onRowHover: handleRowHover
1168
1473
  },
1169
1474
  selection: {
@@ -1201,10 +1506,10 @@ function useGlideTable(options) {
1201
1506
  }, [
1202
1507
  enableRowSpan,
1203
1508
  primaryRowSpanKey,
1509
+ primaryRowSpanColumnId,
1204
1510
  columnRowSpanMap,
1205
1511
  hoveredRowIndex,
1206
- hoveredGroupKey,
1207
- selectedGroupKeys,
1512
+ selectedRowIndices,
1208
1513
  handleRowHover,
1209
1514
  rowSelectionMode,
1210
1515
  selectOnRowClick,
@@ -1230,6 +1535,14 @@ function useGlideTable(options) {
1230
1535
  labels.expandRow,
1231
1536
  labels.collapseRow
1232
1537
  ]);
1538
+ const copySelectionRef = (0, import_react4.useRef)(copySelection);
1539
+ (0, import_react4.useEffect)(() => {
1540
+ copySelectionRef.current = copySelection;
1541
+ }, [copySelection]);
1542
+ const stableCopySelection = (0, import_react4.useCallback)((options2) => copySelectionRef.current(options2), []);
1543
+ (0, import_react4.useEffect)(() => {
1544
+ onCopyActionsReady?.({ copySelection: stableCopySelection });
1545
+ }, [onCopyActionsReady, stableCopySelection]);
1233
1546
  return {
1234
1547
  table,
1235
1548
  tableData,
@@ -1249,7 +1562,8 @@ function useGlideTable(options) {
1249
1562
  paddingBottom,
1250
1563
  rowContextValue,
1251
1564
  handleToggleSelect,
1252
- clearHover
1565
+ clearHover,
1566
+ copySelection: stableCopySelection
1253
1567
  };
1254
1568
  }
1255
1569
 
@@ -1449,11 +1763,10 @@ function DataTableRow({
1449
1763
  const { classNames, rowSpan, selection, cellSelection, cellEdit, expand } = useDataTableRowContext();
1450
1764
  const {
1451
1765
  enableRowSpan,
1452
- primaryRowSpanKey,
1766
+ primaryRowSpanColumnId,
1453
1767
  columnRowSpanMap,
1454
1768
  hoveredRowIndex,
1455
- hoveredGroupKey,
1456
- selectedGroupKeys,
1769
+ selectedRowIndices,
1457
1770
  onRowHover
1458
1771
  } = rowSpan;
1459
1772
  const { rowSelectionMode, selectOnRowClick, onRowClick, getRowClassName } = selection;
@@ -1486,9 +1799,11 @@ function DataTableRow({
1486
1799
  const rowData = row.original;
1487
1800
  const isRowHovered = hoveredRowIndex === rowIndex;
1488
1801
  const isRowSelected = row.getIsSelected();
1489
- const rowGroupKey = primaryRowSpanKey !== void 0 && rowData[primaryRowSpanKey] !== null && rowData[primaryRowSpanKey] !== void 0 ? String(rowData[primaryRowSpanKey]) : null;
1490
- const isGroupHovered = enableRowSpan && hoveredGroupKey !== null && rowGroupKey === hoveredGroupKey;
1491
- const isGroupSelected = enableRowSpan && rowGroupKey !== null && selectedGroupKeys.has(rowGroupKey);
1802
+ const { startRow: primaryGroupStart, rowSpan: primaryGroupSpan } = resolveRowSpanAt(
1803
+ primaryRowSpanColumnId ? columnRowSpanMap.get(primaryRowSpanColumnId) : void 0,
1804
+ rowIndex
1805
+ );
1806
+ const isGroupHovered = enableRowSpan && hoveredRowIndex !== null && hoveredRowIndex >= primaryGroupStart && hoveredRowIndex <= primaryGroupStart + primaryGroupSpan - 1;
1492
1807
  const visibleCells = row.getVisibleCells();
1493
1808
  const columnIdsByIndex = visibleCells.map((cell) => cell.column.id);
1494
1809
  const isVisuallySelectedAt = activeSelectionBounds ? (targetRow, targetCol) => {
@@ -1565,7 +1880,16 @@ function DataTableRow({
1565
1880
  const cellRowSpan = rowSpanInfo?.rowSpan ?? 1;
1566
1881
  const isMergedCellHovered = hoveredRowIndex !== null && hoveredRowIndex >= rowIndex && hoveredRowIndex <= rowIndex + cellRowSpan - 1;
1567
1882
  const showCellHover = isRowSpanColumn ? isMergedCellHovered : isRowHovered;
1568
- const showCellSelected = isRowSpanColumn ? isGroupSelected : isRowSelected;
1883
+ let isMergedCellSelected = false;
1884
+ if (isRowSpanColumn) {
1885
+ for (let r = rowIndex; r < rowIndex + cellRowSpan; r += 1) {
1886
+ if (selectedRowIndices.has(r)) {
1887
+ isMergedCellSelected = true;
1888
+ break;
1889
+ }
1890
+ }
1891
+ }
1892
+ const showCellSelected = isRowSpanColumn ? isMergedCellSelected : isRowSelected;
1569
1893
  const isMerged = cellRowSpan > 1;
1570
1894
  const showMergedRightEdge = isMerged && (cellIndex === visibleCells.length - 1 || resolveRowSpanAt(
1571
1895
  columnRowSpanMap.get(columnIdsByIndex[cellIndex + 1]),
@@ -1925,7 +2249,12 @@ function DataTable({
1925
2249
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1926
2250
  "th",
1927
2251
  {
1928
- style: { width: header.getSize() !== 150 ? header.getSize() : void 0 },
2252
+ style: {
2253
+ width: header.getSize() !== 150 ? header.getSize() : void 0,
2254
+ // Column sizing follows TanStack's `size`, but
2255
+ // ensure the column keeps its min width when the container shrinks.
2256
+ minWidth: header.getSize() !== 150 ? header.getSize() : void 0
2257
+ },
1929
2258
  className: cn(
1930
2259
  "data-table-head-cell",
1931
2260
  CELL_ALIGN_CLASS[align],
@@ -2368,10 +2697,14 @@ var Table = Object.assign(TableRoot, {
2368
2697
  applyFillData,
2369
2698
  applySelectionUpdater,
2370
2699
  buildColumnRowSpanMap,
2700
+ buildRowsPastePayload,
2371
2701
  canExpandRow,
2702
+ collectCopyRowEntries,
2703
+ collectCopyRows,
2372
2704
  collectFillChanges,
2373
2705
  collectRowSpanColumns,
2374
2706
  createTable,
2707
+ flattenSubtreeRows,
2375
2708
  getCellEditDraftValue,
2376
2709
  getCellSelectionEdgeStyle,
2377
2710
  getColumnEditType,
@@ -2379,15 +2712,22 @@ var Table = Object.assign(TableRoot, {
2379
2712
  hasCellSelectionEdges,
2380
2713
  isCellInSelection,
2381
2714
  isColumnEditable,
2715
+ isEditablePasteTarget,
2382
2716
  measureMergedSpanRowHeights,
2383
2717
  parseCellEditValue,
2718
+ parseClipboardTSV,
2719
+ parseClipboardTSVWithDepths,
2384
2720
  resolveDataTableLabels,
2721
+ resolvePasteColumnIds,
2385
2722
  resolveRowSelection,
2386
2723
  resolveRowSpanAt,
2387
2724
  rowRangeToHeightRatios,
2725
+ serializeCopyRowsToTSV,
2726
+ serializeSelectionToTSV,
2388
2727
  toggleExpandedRowId,
2389
2728
  useCellEdit,
2390
2729
  useCellSelection,
2391
2730
  useConvertTreeData,
2392
- useGlideTable
2731
+ useGlideTable,
2732
+ writeSelectionToClipboard
2393
2733
  });