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.cjs CHANGED
@@ -30,9 +30,13 @@ __export(core_exports, {
30
30
  applyFillData: () => applyFillData,
31
31
  applySelectionUpdater: () => applySelectionUpdater,
32
32
  buildColumnRowSpanMap: () => buildColumnRowSpanMap,
33
+ buildRowsPastePayload: () => buildRowsPastePayload,
33
34
  canExpandRow: () => canExpandRow,
35
+ collectCopyRowEntries: () => collectCopyRowEntries,
36
+ collectCopyRows: () => collectCopyRows,
34
37
  collectFillChanges: () => collectFillChanges,
35
38
  collectRowSpanColumns: () => collectRowSpanColumns,
39
+ flattenSubtreeRows: () => flattenSubtreeRows,
36
40
  getCellEditDraftValue: () => getCellEditDraftValue,
37
41
  getCellSelectionEdgeStyle: () => getCellSelectionEdgeStyle,
38
42
  getColumnEditType: () => getColumnEditType,
@@ -40,15 +44,24 @@ __export(core_exports, {
40
44
  hasCellSelectionEdges: () => hasCellSelectionEdges,
41
45
  isCellInSelection: () => isCellInSelection,
42
46
  isColumnEditable: () => isColumnEditable,
47
+ isEditablePasteTarget: () => isEditablePasteTarget,
48
+ measureMergedSpanRowHeights: () => measureMergedSpanRowHeights,
43
49
  parseCellEditValue: () => parseCellEditValue,
50
+ parseClipboardTSV: () => parseClipboardTSV,
51
+ parseClipboardTSVWithDepths: () => parseClipboardTSVWithDepths,
44
52
  resolveDataTableLabels: () => resolveDataTableLabels,
53
+ resolvePasteColumnIds: () => resolvePasteColumnIds,
45
54
  resolveRowSelection: () => resolveRowSelection,
46
55
  resolveRowSpanAt: () => resolveRowSpanAt,
56
+ rowRangeToHeightRatios: () => rowRangeToHeightRatios,
57
+ serializeCopyRowsToTSV: () => serializeCopyRowsToTSV,
58
+ serializeSelectionToTSV: () => serializeSelectionToTSV,
47
59
  toggleExpandedRowId: () => toggleExpandedRowId,
48
60
  useCellEdit: () => useCellEdit,
49
61
  useCellSelection: () => useCellSelection,
50
62
  useConvertTreeData: () => useConvertTreeData,
51
- useGlideTable: () => useGlideTable
63
+ useGlideTable: () => useGlideTable,
64
+ writeSelectionToClipboard: () => writeSelectionToClipboard
52
65
  });
53
66
  module.exports = __toCommonJS(core_exports);
54
67
 
@@ -230,10 +243,36 @@ function getCellSelectionBounds(start, end) {
230
243
  endCol: Math.max(start.col, end.col)
231
244
  };
232
245
  }
246
+ function measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement) {
247
+ if (rowSpan <= 1) return void 0;
248
+ const tbody = cellElement?.closest("tbody") ?? (typeof document !== "undefined" ? document.querySelector("tbody.data-table-body") : null);
249
+ if (!tbody) return void 0;
250
+ const rows = tbody.querySelectorAll(":scope > tr");
251
+ if (rows.length < rowIndex + rowSpan) return void 0;
252
+ const heights = [];
253
+ for (let i = 0; i < rowSpan; i++) {
254
+ const row = rows[rowIndex + i];
255
+ const height = row?.getBoundingClientRect().height ?? 0;
256
+ if (height <= 0) return void 0;
257
+ heights.push(height);
258
+ }
259
+ return heights;
260
+ }
233
261
  function getRowIndexInMergedCell(clientY, cellElement, rowIndex, rowSpan) {
234
262
  if (rowSpan <= 1) return rowIndex;
235
263
  const rect = cellElement.getBoundingClientRect();
236
264
  const relativeY = clientY - rect.top;
265
+ const heights = measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement);
266
+ if (heights && heights.length === rowSpan) {
267
+ let accrued = 0;
268
+ for (let i = 0; i < rowSpan; i++) {
269
+ accrued += heights[i];
270
+ if (relativeY < accrued) {
271
+ return rowIndex + i;
272
+ }
273
+ }
274
+ return rowIndex + rowSpan - 1;
275
+ }
237
276
  const rowHeight = rect.height / rowSpan;
238
277
  const offset = Math.min(
239
278
  Math.max(Math.floor(relativeY / rowHeight), 0),
@@ -255,7 +294,35 @@ function getActiveSelectionBounds(dragState, selectionBounds) {
255
294
  var SELECTION_EDGE_WIDTH_PX = 2;
256
295
  var SELECTION_EDGE_COLOR = "var(--color-brand-primary)";
257
296
  var CELL_SELECTION_EDGES_CLASS = "cell-selection-edges";
258
- function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt) {
297
+ function rowRangeToHeightRatios(rowIndex, span, fromRow, toRowExclusive, rowHeights) {
298
+ const clampedFrom = Math.max(fromRow, rowIndex);
299
+ const clampedTo = Math.min(toRowExclusive, rowIndex + span);
300
+ if (clampedTo <= clampedFrom) {
301
+ return { offsetRatio: 0, lengthRatio: 0 };
302
+ }
303
+ if (!rowHeights || rowHeights.length !== span) {
304
+ return {
305
+ offsetRatio: (clampedFrom - rowIndex) / span,
306
+ lengthRatio: (clampedTo - clampedFrom) / span
307
+ };
308
+ }
309
+ const total = rowHeights.reduce((sum, height) => sum + height, 0) || 1;
310
+ let offsetPx = 0;
311
+ for (let i = 0; i < clampedFrom - rowIndex; i++) {
312
+ offsetPx += rowHeights[i] ?? 0;
313
+ }
314
+ let lengthPx = 0;
315
+ for (let i = clampedFrom - rowIndex; i < clampedTo - rowIndex; i++) {
316
+ lengthPx += rowHeights[i] ?? 0;
317
+ }
318
+ return {
319
+ offsetRatio: offsetPx / total,
320
+ lengthRatio: lengthPx / total,
321
+ offsetPx,
322
+ lengthPx
323
+ };
324
+ }
325
+ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt, rowHeights) {
259
326
  const cellEndRow = rowIndex + rowSpan - 1;
260
327
  const span = cellEndRow - rowIndex + 1;
261
328
  if (span <= 1) return [];
@@ -274,20 +341,30 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
274
341
  continue;
275
342
  }
276
343
  if (runStart !== null) {
277
- edges.push({
278
- side,
279
- offsetRatio: (runStart - rowIndex) / span,
280
- heightRatio: (row - runStart) / span
281
- });
344
+ const ratios = rowRangeToHeightRatios(
345
+ rowIndex,
346
+ span,
347
+ runStart,
348
+ row,
349
+ rowHeights
350
+ );
351
+ if (ratios.lengthRatio > 0) {
352
+ edges.push({ side, ...ratios });
353
+ }
282
354
  runStart = null;
283
355
  }
284
356
  }
285
357
  if (runStart !== null) {
286
- edges.push({
287
- side,
288
- offsetRatio: (runStart - rowIndex) / span,
289
- heightRatio: (toRowExclusive - runStart) / span
290
- });
358
+ const ratios = rowRangeToHeightRatios(
359
+ rowIndex,
360
+ span,
361
+ runStart,
362
+ toRowExclusive,
363
+ rowHeights
364
+ );
365
+ if (ratios.lengthRatio > 0) {
366
+ edges.push({ side, ...ratios });
367
+ }
291
368
  }
292
369
  };
293
370
  const collectSide = (side, neighborCol) => {
@@ -316,14 +393,17 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
316
393
  }
317
394
  return edges;
318
395
  }
319
- function buildPartialVerticalGradient(edge) {
396
+ function buildPartialEdgeGradient(edge) {
397
+ const usePx = edge.offsetPx != null && edge.lengthPx != null;
320
398
  const startPct = edge.offsetRatio * 100;
321
- const endPct = (edge.offsetRatio + edge.heightRatio) * 100;
322
- const overlapPx = SELECTION_EDGE_WIDTH_PX + 1;
323
- const isTopProtrusion = edge.offsetRatio === 0 && edge.heightRatio < 1;
324
- const isBottomProtrusion = edge.offsetRatio > 0;
325
- const startStop = isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
326
- const endStop = isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
399
+ const endPct = (edge.offsetRatio + edge.lengthRatio) * 100;
400
+ const startPx = edge.offsetPx ?? 0;
401
+ const endPx = (edge.offsetPx ?? 0) + (edge.lengthPx ?? 0);
402
+ const overlapPx = SELECTION_EDGE_WIDTH_PX;
403
+ const isTopProtrusion = (usePx ? startPx === 0 : edge.offsetRatio === 0) && edge.lengthRatio < 1;
404
+ const isBottomProtrusion = usePx ? startPx > 0 : edge.offsetRatio > 0;
405
+ const startStop = usePx ? isBottomProtrusion ? `${Math.max(0, startPx - overlapPx)}px` : `${startPx}px` : isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
406
+ const endStop = usePx ? isTopProtrusion ? `${endPx + overlapPx}px` : `${endPx}px` : isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
327
407
  const xPos = edge.side === "left" ? "0" : "100%";
328
408
  const layers = [
329
409
  {
@@ -333,7 +413,7 @@ function buildPartialVerticalGradient(edge) {
333
413
  }
334
414
  ];
335
415
  if (isTopProtrusion || isBottomProtrusion) {
336
- const capTop = isTopProtrusion ? `calc(${endPct}% - ${SELECTION_EDGE_WIDTH_PX}px)` : `calc(${startPct}% - ${SELECTION_EDGE_WIDTH_PX}px)`;
416
+ 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)`;
337
417
  layers.push({
338
418
  image: `linear-gradient(${SELECTION_EDGE_COLOR}, ${SELECTION_EDGE_COLOR})`,
339
419
  size: `${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_WIDTH_PX}px`,
@@ -342,7 +422,7 @@ function buildPartialVerticalGradient(edge) {
342
422
  }
343
423
  return layers;
344
424
  }
345
- function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt) {
425
+ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt, rowHeights) {
346
426
  if (!isCellInSelection(rowIndex, colIndex, bounds, rowSpan) || !bounds)
347
427
  return void 0;
348
428
  const cellEndRow = rowIndex + rowSpan - 1;
@@ -351,39 +431,47 @@ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVi
351
431
  const isLeftEdge = colIndex === bounds.startCol;
352
432
  const isRightEdge = colIndex === bounds.endCol;
353
433
  const selectionContinuesBelow = cellEndRow < bounds.endRow;
354
- const shadows = [];
355
- if (isTopEdge) {
356
- shadows.push(
357
- `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
358
- );
359
- }
360
- if (isBottomEdge) {
361
- shadows.push(
362
- `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
363
- );
364
- }
365
- if (isLeftEdge) {
366
- shadows.push(
367
- `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
368
- );
369
- }
370
- if (isRightEdge) {
371
- shadows.push(
372
- `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
373
- );
374
- }
375
434
  const stepEdges = getMergedCellStepEdges(
376
435
  rowIndex,
377
436
  colIndex,
378
437
  bounds,
379
438
  rowSpan,
380
- isVisuallySelectedAt
439
+ isVisuallySelectedAt,
440
+ rowHeights
381
441
  );
442
+ const shadows = [];
443
+ const hasFullPerimeter = isTopEdge && isBottomEdge && isLeftEdge && isRightEdge && stepEdges.length === 0;
444
+ if (hasFullPerimeter) {
445
+ shadows.push(
446
+ `inset 0 0 0 ${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_COLOR}`
447
+ );
448
+ } else {
449
+ if (isTopEdge) {
450
+ shadows.push(
451
+ `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
452
+ );
453
+ }
454
+ if (isBottomEdge) {
455
+ shadows.push(
456
+ `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
457
+ );
458
+ }
459
+ if (isLeftEdge) {
460
+ shadows.push(
461
+ `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
462
+ );
463
+ }
464
+ if (isRightEdge) {
465
+ shadows.push(
466
+ `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
467
+ );
468
+ }
469
+ }
382
470
  const gradients = [];
383
471
  const sizes = [];
384
472
  const positions = [];
385
473
  for (const edge of stepEdges) {
386
- for (const partial of buildPartialVerticalGradient(edge)) {
474
+ for (const partial of buildPartialEdgeGradient(edge)) {
387
475
  gradients.push(partial.image);
388
476
  sizes.push(partial.size);
389
477
  positions.push(partial.position);
@@ -412,6 +500,127 @@ function hasCellSelectionEdges(style) {
412
500
  );
413
501
  }
414
502
 
503
+ // src/components/ui/table/features/cell-selection/copyData.ts
504
+ function formatCellValue(value) {
505
+ if (value === null || value === void 0) return "";
506
+ return String(value);
507
+ }
508
+ function getNestedValue(row, path) {
509
+ if (!path.includes(".")) return row[path];
510
+ return path.split(".").reduce((current, key) => {
511
+ if (current === null || current === void 0 || typeof current !== "object") {
512
+ return void 0;
513
+ }
514
+ return current[key];
515
+ }, row);
516
+ }
517
+ function readRowColumnValue(rowData, columnDef) {
518
+ if ("accessorFn" in columnDef && typeof columnDef.accessorFn === "function") {
519
+ return columnDef.accessorFn(rowData, 0);
520
+ }
521
+ if ("accessorKey" in columnDef && columnDef.accessorKey != null && columnDef.accessorKey !== "") {
522
+ return getNestedValue(rowData, String(columnDef.accessorKey));
523
+ }
524
+ return void 0;
525
+ }
526
+ function flattenSubtreeRows(row) {
527
+ const children = row.children;
528
+ if (!Array.isArray(children) || children.length === 0) return [];
529
+ const result = [];
530
+ const walk = (nodes) => {
531
+ for (const node of nodes) {
532
+ result.push(node);
533
+ const nested = node.children;
534
+ if (Array.isArray(nested) && nested.length > 0) {
535
+ walk(nested);
536
+ }
537
+ }
538
+ };
539
+ walk(children);
540
+ return result;
541
+ }
542
+ function hasSubtree(row) {
543
+ const children = row.children;
544
+ return Array.isArray(children) && children.length > 0;
545
+ }
546
+ function getOriginalRowId(original) {
547
+ return String(original.id ?? original.uniqueId ?? "");
548
+ }
549
+ function getRowDepth(original) {
550
+ return typeof original.level === "number" ? original.level : 0;
551
+ }
552
+ function collectCopyRowEntries(visibleRows, bounds, mode = "visible") {
553
+ const { startRow, endRow } = bounds;
554
+ const result = [];
555
+ const includedOriginalIds = /* @__PURE__ */ new Set();
556
+ const appendSubtree = (node, depth) => {
557
+ const children = node.children;
558
+ if (!Array.isArray(children) || children.length === 0) return;
559
+ for (const child of children) {
560
+ const childId = getOriginalRowId(child);
561
+ if (!(childId && includedOriginalIds.has(childId))) {
562
+ result.push({ row: child, depth });
563
+ if (childId) includedOriginalIds.add(childId);
564
+ }
565
+ appendSubtree(child, depth + 1);
566
+ }
567
+ };
568
+ for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) {
569
+ const row = visibleRows[rowIndex];
570
+ if (!row) continue;
571
+ const originalId = getOriginalRowId(row.original);
572
+ if (originalId && includedOriginalIds.has(originalId)) continue;
573
+ const depth = getRowDepth(row.original);
574
+ result.push({ row: row.original, depth });
575
+ if (originalId) includedOriginalIds.add(originalId);
576
+ if (mode !== "subtree" || !hasSubtree(row.original)) continue;
577
+ appendSubtree(row.original, depth + 1);
578
+ }
579
+ return result;
580
+ }
581
+ function collectCopyRows(visibleRows, bounds, mode = "visible") {
582
+ return collectCopyRowEntries(visibleRows, bounds, mode).map((entry) => entry.row);
583
+ }
584
+ function serializeCopyRowsToTSV(copyRows, visibleRows, bounds, depths) {
585
+ if (copyRows.length === 0) return "";
586
+ const { startCol, endCol } = bounds;
587
+ const columnCells = visibleRows[0]?.getVisibleCells().slice(startCol, endCol + 1) ?? [];
588
+ if (columnCells.length === 0) return "";
589
+ const resolvedDepths = depths && depths.length === copyRows.length ? depths : copyRows.map((row) => getRowDepth(row));
590
+ const minDepth = Math.min(...resolvedDepths);
591
+ return copyRows.map((rowData, index) => {
592
+ const relativeDepth = Math.max(0, (resolvedDepths[index] ?? 0) - minDepth);
593
+ const line = columnCells.map(
594
+ (cell) => formatCellValue(
595
+ readRowColumnValue(
596
+ rowData,
597
+ cell.column.columnDef
598
+ )
599
+ )
600
+ ).join(" ");
601
+ return `${" ".repeat(relativeDepth)}${line}`;
602
+ }).join("\n");
603
+ }
604
+ function serializeSelectionToTSV(visibleRows, bounds, mode = "visible") {
605
+ const entries = collectCopyRowEntries(visibleRows, bounds, mode);
606
+ return serializeCopyRowsToTSV(
607
+ entries.map((entry) => entry.row),
608
+ visibleRows,
609
+ bounds,
610
+ entries.map((entry) => entry.depth)
611
+ );
612
+ }
613
+ async function writeSelectionToClipboard(visibleRows, bounds, mode = "visible") {
614
+ const text = serializeSelectionToTSV(visibleRows, bounds, mode);
615
+ if (!text) return false;
616
+ try {
617
+ await navigator.clipboard.writeText(text);
618
+ } catch {
619
+ return false;
620
+ }
621
+ return true;
622
+ }
623
+
415
624
  // src/components/ui/table/features/cell-selection/fillData.ts
416
625
  function getColumnAccessorKey2(columnDef) {
417
626
  if ("accessorKey" in columnDef && columnDef.accessorKey) {
@@ -468,15 +677,103 @@ function hasFillExtension(sourceBounds, fillBounds) {
468
677
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
469
678
  }
470
679
 
680
+ // src/components/ui/table/features/cell-selection/pasteData.ts
681
+ function countLeadingEmptyCells(cells) {
682
+ let depth = 0;
683
+ while (depth < cells.length && cells[depth] === "") {
684
+ depth += 1;
685
+ }
686
+ return depth;
687
+ }
688
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
689
+ if (leadingEmptyCounts.length === 0) return false;
690
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
691
+ if (firstDepth !== 0) return false;
692
+ return leadingEmptyCounts.some((depth) => depth > 0);
693
+ }
694
+ function parseClipboardTSV(text) {
695
+ return parseClipboardTSVWithDepths(text).values;
696
+ }
697
+ function parseClipboardTSVWithDepths(text) {
698
+ if (!text) return { values: [], depths: [] };
699
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
700
+ const withoutTrailing = normalized.replace(/\n+$/, "");
701
+ if (!withoutTrailing) return { values: [], depths: [] };
702
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
703
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
704
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
705
+ const values = [];
706
+ const depths = [];
707
+ for (let index = 0; index < rows.length; index += 1) {
708
+ const cells = rows[index] ?? [];
709
+ const depth = leadingEmptyCounts[index] ?? 0;
710
+ if (treatAsDepth) {
711
+ values.push(cells.slice(depth));
712
+ depths.push(depth);
713
+ } else {
714
+ values.push(cells);
715
+ depths.push(0);
716
+ }
717
+ }
718
+ return { values, depths };
719
+ }
720
+ function resolvePasteColumnIds(rows, startCol, width) {
721
+ if (width <= 0) return [];
722
+ const cells = rows[0]?.getVisibleCells() ?? [];
723
+ const columnIds = [];
724
+ for (let offset = 0; offset < width; offset += 1) {
725
+ const cell = cells[startCol + offset];
726
+ if (!cell) break;
727
+ columnIds.push(cell.column.id);
728
+ }
729
+ return columnIds;
730
+ }
731
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
732
+ const { values, depths } = parseClipboardTSVWithDepths(text);
733
+ if (values.length === 0) return null;
734
+ const width = Math.max(...values.map((row) => row.length), 0);
735
+ if (width === 0) return null;
736
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
737
+ if (columnIds.length === 0) return null;
738
+ const rowIds = [];
739
+ for (let offset = 0; offset < values.length; offset += 1) {
740
+ const row = rows[startRow + offset];
741
+ if (!row) break;
742
+ rowIds.push(row.id);
743
+ }
744
+ const anchorRow = rows[endRow] ?? rows[startRow];
745
+ return {
746
+ mode,
747
+ startRow,
748
+ startCol,
749
+ endRow,
750
+ rowIds,
751
+ anchorRowId: anchorRow?.id ?? "",
752
+ columnIds,
753
+ values,
754
+ depths
755
+ };
756
+ }
757
+ function isEditablePasteTarget(target) {
758
+ if (!(target instanceof HTMLElement)) return false;
759
+ const tag = target.tagName;
760
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
761
+ return Boolean(target.isContentEditable);
762
+ }
763
+
471
764
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
472
765
  function useCellSelection({
473
766
  data,
474
767
  rows,
475
768
  enabled = true,
769
+ enableSubtreeCopy = false,
770
+ enableInsertPaste = true,
476
771
  onDataChange,
477
- onBatchChange
772
+ onBatchChange,
773
+ onRowsPaste
478
774
  }) {
479
775
  const [dragState, setDragState] = (0, import_react2.useState)(INITIAL_DRAG_STATE);
776
+ const pendingPasteModeRef = (0, import_react2.useRef)(null);
480
777
  const cellSelectionBounds = getCellSelectionBounds(dragState.start, dragState.end);
481
778
  const activeSelectionBounds = enabled ? getActiveSelectionBounds(dragState, cellSelectionBounds) : null;
482
779
  const handleCellMouseDown = (0, import_react2.useCallback)(
@@ -530,21 +827,113 @@ function useCellSelection({
530
827
  setDragState(INITIAL_DRAG_STATE);
531
828
  }
532
829
  }, [enabled]);
830
+ const copySelection = (0, import_react2.useCallback)(
831
+ async (options) => {
832
+ if (!enabled || !activeSelectionBounds) return false;
833
+ const mode = options?.includeDescendants && enableSubtreeCopy ? "subtree" : "visible";
834
+ return writeSelectionToClipboard(rows, activeSelectionBounds, mode);
835
+ },
836
+ [activeSelectionBounds, enableSubtreeCopy, enabled, rows]
837
+ );
533
838
  (0, import_react2.useEffect)(() => {
534
839
  if (!enabled) return;
535
840
  const handleKeyDown = (e) => {
536
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && activeSelectionBounds) {
537
- const { startRow, endRow, startCol, endCol } = activeSelectionBounds;
538
- const selectedData = rows.slice(startRow, endRow + 1).map((row) => {
539
- const cells = row.getVisibleCells();
540
- return cells.slice(startCol, endCol + 1).map((cell) => cell.getValue()).join(" ");
541
- }).join("\n");
542
- navigator.clipboard.writeText(selectedData);
543
- }
841
+ if (!activeSelectionBounds) return;
842
+ if (!(e.ctrlKey || e.metaKey)) return;
843
+ const isSubtreeShortcut = enableSubtreeCopy && e.shiftKey && e.key.toLowerCase() === "c";
844
+ const isVisibleCopyShortcut = !e.shiftKey && e.key.toLowerCase() === "c";
845
+ if (!isSubtreeShortcut && !isVisibleCopyShortcut) return;
846
+ e.preventDefault();
847
+ void copySelection({ includeDescendants: isSubtreeShortcut });
544
848
  };
545
849
  window.addEventListener("keydown", handleKeyDown);
546
850
  return () => window.removeEventListener("keydown", handleKeyDown);
547
- }, [activeSelectionBounds, enabled, rows]);
851
+ }, [activeSelectionBounds, copySelection, enableSubtreeCopy, enabled]);
852
+ const emitRowsPaste = (0, import_react2.useCallback)(
853
+ (text, mode) => {
854
+ if (!onRowsPaste || !activeSelectionBounds) return false;
855
+ const payload = buildRowsPastePayload(
856
+ rows,
857
+ activeSelectionBounds.startRow,
858
+ activeSelectionBounds.startCol,
859
+ text,
860
+ mode,
861
+ activeSelectionBounds.endRow
862
+ );
863
+ if (!payload) return false;
864
+ onRowsPaste(payload);
865
+ return true;
866
+ },
867
+ [activeSelectionBounds, onRowsPaste, rows]
868
+ );
869
+ (0, import_react2.useEffect)(() => {
870
+ if (!enabled || !onRowsPaste) return;
871
+ const pasteHandledRef = { current: false };
872
+ const ignoreNextPasteRef = { current: false };
873
+ const handleKeyDown = (e) => {
874
+ if (!activeSelectionBounds) return;
875
+ if (!(e.ctrlKey || e.metaKey)) return;
876
+ if (e.key.toLowerCase() !== "v") return;
877
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
878
+ return;
879
+ }
880
+ if (e.shiftKey && !enableInsertPaste) {
881
+ ignoreNextPasteRef.current = true;
882
+ pendingPasteModeRef.current = null;
883
+ return;
884
+ }
885
+ const mode = e.shiftKey ? "insert" : "overwrite";
886
+ pasteHandledRef.current = false;
887
+ ignoreNextPasteRef.current = false;
888
+ pendingPasteModeRef.current = mode;
889
+ void (async () => {
890
+ try {
891
+ const text = await navigator.clipboard.readText();
892
+ if (pasteHandledRef.current) return;
893
+ if (pendingPasteModeRef.current !== mode) return;
894
+ if (!text) return;
895
+ pasteHandledRef.current = true;
896
+ emitRowsPaste(text, mode);
897
+ pendingPasteModeRef.current = null;
898
+ } catch {
899
+ }
900
+ })();
901
+ };
902
+ const handlePaste = (e) => {
903
+ if (!activeSelectionBounds) return;
904
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
905
+ return;
906
+ }
907
+ if (ignoreNextPasteRef.current) {
908
+ ignoreNextPasteRef.current = false;
909
+ pendingPasteModeRef.current = null;
910
+ return;
911
+ }
912
+ const mode = pendingPasteModeRef.current ?? "overwrite";
913
+ if (pasteHandledRef.current) {
914
+ e.preventDefault();
915
+ return;
916
+ }
917
+ const text = e.clipboardData?.getData("text/plain");
918
+ if (text == null || text === "") return;
919
+ pasteHandledRef.current = true;
920
+ e.preventDefault();
921
+ emitRowsPaste(text, mode);
922
+ pendingPasteModeRef.current = null;
923
+ };
924
+ window.addEventListener("keydown", handleKeyDown);
925
+ window.addEventListener("paste", handlePaste);
926
+ return () => {
927
+ window.removeEventListener("keydown", handleKeyDown);
928
+ window.removeEventListener("paste", handlePaste);
929
+ };
930
+ }, [
931
+ activeSelectionBounds,
932
+ emitRowsPaste,
933
+ enableInsertPaste,
934
+ enabled,
935
+ onRowsPaste
936
+ ]);
548
937
  (0, import_react2.useEffect)(() => {
549
938
  if (!enabled) return;
550
939
  const handleMouseUp = () => {
@@ -590,7 +979,8 @@ function useCellSelection({
590
979
  activeSelectionBounds,
591
980
  handleCellMouseDown,
592
981
  handleCellMouseEnter,
593
- handleFillHandleMouseDown
982
+ handleFillHandleMouseDown,
983
+ copySelection
594
984
  };
595
985
  }
596
986
 
@@ -672,15 +1062,16 @@ var useConvertTreeData = ({
672
1062
  children: [],
673
1063
  processed: false
674
1064
  }));
675
- const itemMap = /* @__PURE__ */ new Map();
676
- dataWithLevels.forEach((item) => {
677
- const key = getFieldValue(item, toggleField);
678
- if (typeof key !== "string" || !key) return;
679
- if (!itemMap.has(key)) {
680
- itemMap.set(key, []);
1065
+ const findNearestPrecedingParent = (index, parentKey) => {
1066
+ for (let i = index - 1; i >= 0; i -= 1) {
1067
+ const candidate = dataWithLevels[i];
1068
+ if (!candidate) continue;
1069
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1070
+ return candidate;
1071
+ }
681
1072
  }
682
- itemMap.get(key)?.push(item);
683
- });
1073
+ return void 0;
1074
+ };
684
1075
  const rootItems = [];
685
1076
  dataWithLevels.forEach((item) => {
686
1077
  if (!getFieldValue(item, childField)) {
@@ -688,29 +1079,18 @@ var useConvertTreeData = ({
688
1079
  item.processed = true;
689
1080
  }
690
1081
  });
691
- dataWithLevels.forEach((item) => {
1082
+ dataWithLevels.forEach((item, index) => {
692
1083
  const parentKey = getFieldValue(item, childField);
693
1084
  if (!parentKey || item.processed) return;
694
- const parentItems = dataWithLevels.filter(
695
- (parent) => getFieldValue(parent, toggleField) === parentKey && !getFieldValue(parent, childField)
696
- );
697
- if (parentItems.length > 0) {
698
- const parent = parentItems[0];
1085
+ const parent = findNearestPrecedingParent(index, parentKey);
1086
+ if (parent) {
699
1087
  item.level = parent.level + 1;
700
1088
  parent.children.push(item);
701
1089
  item.processed = true;
702
- } else {
703
- const otherParents = itemMap.get(String(parentKey)) || [];
704
- if (otherParents.length > 0) {
705
- const parent = otherParents[0];
706
- item.level = parent.level + 1;
707
- parent.children.push(item);
708
- item.processed = true;
709
- } else {
710
- rootItems.push(item);
711
- item.processed = true;
712
- }
1090
+ return;
713
1091
  }
1092
+ rootItems.push(item);
1093
+ item.processed = true;
714
1094
  });
715
1095
  return rootItems;
716
1096
  }, [enabled, data, toggleField, childField, flattenField]);
@@ -735,16 +1115,23 @@ var useConvertTreeData = ({
735
1115
  return result;
736
1116
  };
737
1117
  const flattenedData = flatten(processedData, [], 0);
738
- flattenedData.forEach((item) => {
739
- if (getFieldValue(item, childField)) {
740
- const parentItem = flattenedData.find(
741
- (parent) => getFieldValue(parent, toggleField) === getFieldValue(item, childField)
742
- );
743
- const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
744
- item.parentCount = parentAmount || 1;
745
- } else {
1118
+ flattenedData.forEach((item, index) => {
1119
+ const parentKey = getFieldValue(item, childField);
1120
+ if (!parentKey) {
746
1121
  item.parentCount = 1;
1122
+ return;
747
1123
  }
1124
+ let parentItem;
1125
+ for (let i = index - 1; i >= 0; i -= 1) {
1126
+ const candidate = flattenedData[i];
1127
+ if (!candidate) continue;
1128
+ if (getFieldValue(candidate, toggleField) === parentKey) {
1129
+ parentItem = candidate;
1130
+ break;
1131
+ }
1132
+ }
1133
+ const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
1134
+ item.parentCount = parentAmount || 1;
748
1135
  });
749
1136
  return flattenedData;
750
1137
  }, [
@@ -891,6 +1278,10 @@ function useGlideTable(options) {
891
1278
  expandedRows: controlledExpandedRows,
892
1279
  onExpandedRowsChange,
893
1280
  preventExpand = false,
1281
+ enableSubtreeCopy,
1282
+ onCopyActionsReady,
1283
+ onRowsPaste,
1284
+ enableInsertPaste,
894
1285
  enableVirtualization = true,
895
1286
  estimateRowHeight = DATA_TABLE_ROW_HEIGHT,
896
1287
  virtualOverscan = DATA_TABLE_VIRTUAL_OVERSCAN
@@ -905,12 +1296,12 @@ function useGlideTable(options) {
905
1296
  };
906
1297
  }, [labelsProp, emptyText, loadingText, selectionLabel]);
907
1298
  const enableExpand = Boolean(toggleField);
1299
+ const resolvedEnableSubtreeCopy = enableSubtreeCopy ?? enableExpand;
908
1300
  const [internalRowSelection, setInternalRowSelection] = (0, import_react4.useState)({});
909
1301
  const [internalExpandedRows, setInternalExpandedRows] = (0, import_react4.useState)(
910
1302
  () => /* @__PURE__ */ new Set()
911
1303
  );
912
1304
  const [hoveredRowIndex, setHoveredRowIndex] = (0, import_react4.useState)(null);
913
- const [hoveredGroupKey, setHoveredGroupKey] = (0, import_react4.useState)(null);
914
1305
  const scrollRef = (0, import_react4.useRef)(null);
915
1306
  const shouldVirtualize = enableVirtualization && !enableRowSpan;
916
1307
  (0, import_react4.useEffect)(() => {
@@ -974,6 +1365,7 @@ function useGlideTable(options) {
974
1365
  return collectRowSpanColumns(columns);
975
1366
  }, [enableRowSpan, columns]);
976
1367
  const primaryRowSpanKey = rowSpanColumnKeys[0]?.rowSpanKey;
1368
+ const primaryRowSpanColumnId = rowSpanColumnKeys[0]?.columnId;
977
1369
  const columnRowSpanMap = (0, import_react4.useMemo)(
978
1370
  () => buildColumnRowSpanMap(tableData, rowSpanColumnKeys),
979
1371
  [tableData, rowSpanColumnKeys]
@@ -992,27 +1384,29 @@ function useGlideTable(options) {
992
1384
  const totalSize = rowVirtualizer.getTotalSize();
993
1385
  const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
994
1386
  const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;
995
- const selectedGroupKeys = (0, import_react4.useMemo)(() => {
996
- if (!enableRowSpan || !primaryRowSpanKey) return /* @__PURE__ */ new Set();
997
- const keys = /* @__PURE__ */ new Set();
1387
+ const selectedRowIndices = (0, import_react4.useMemo)(() => {
1388
+ const indices = /* @__PURE__ */ new Set();
998
1389
  for (const selectedRow of selectedRows) {
999
- const value = selectedRow.original[primaryRowSpanKey];
1000
- if (value !== null && value !== void 0) keys.add(String(value));
1390
+ indices.add(selectedRow.index);
1001
1391
  }
1002
- return keys;
1003
- }, [enableRowSpan, primaryRowSpanKey, selectedRows]);
1392
+ return indices;
1393
+ }, [selectedRows]);
1004
1394
  const {
1005
1395
  dragState,
1006
1396
  activeSelectionBounds,
1007
1397
  handleCellMouseDown,
1008
1398
  handleCellMouseEnter,
1009
- handleFillHandleMouseDown
1399
+ handleFillHandleMouseDown,
1400
+ copySelection
1010
1401
  } = useCellSelection({
1011
1402
  data: tableData,
1012
1403
  rows,
1013
1404
  enabled: enableCellSelection,
1405
+ enableSubtreeCopy: resolvedEnableSubtreeCopy,
1406
+ enableInsertPaste: enableInsertPaste ?? true,
1014
1407
  onDataChange,
1015
- onBatchChange
1408
+ onBatchChange,
1409
+ onRowsPaste
1016
1410
  });
1017
1411
  const {
1018
1412
  editingCell,
@@ -1034,22 +1428,10 @@ function useGlideTable(options) {
1034
1428
  );
1035
1429
  const clearHover = (0, import_react4.useCallback)(() => {
1036
1430
  setHoveredRowIndex(null);
1037
- setHoveredGroupKey(null);
1038
1431
  }, []);
1039
- const handleRowHover = (0, import_react4.useCallback)(
1040
- (rowIndex, rowData) => {
1041
- setHoveredRowIndex(rowIndex);
1042
- if (!primaryRowSpanKey) {
1043
- setHoveredGroupKey(null);
1044
- return;
1045
- }
1046
- const groupValue = rowData[primaryRowSpanKey];
1047
- setHoveredGroupKey(
1048
- groupValue === null || groupValue === void 0 ? null : String(groupValue)
1049
- );
1050
- },
1051
- [primaryRowSpanKey]
1052
- );
1432
+ const handleRowHover = (0, import_react4.useCallback)((rowIndex, _rowData) => {
1433
+ setHoveredRowIndex(rowIndex);
1434
+ }, []);
1053
1435
  const handleToggleSelect = (0, import_react4.useCallback)(
1054
1436
  (row) => {
1055
1437
  if (!row.getCanSelect()) return;
@@ -1072,10 +1454,10 @@ function useGlideTable(options) {
1072
1454
  rowSpan: {
1073
1455
  enableRowSpan,
1074
1456
  primaryRowSpanKey,
1457
+ primaryRowSpanColumnId,
1075
1458
  columnRowSpanMap,
1076
1459
  hoveredRowIndex,
1077
- hoveredGroupKey,
1078
- selectedGroupKeys,
1460
+ selectedRowIndices,
1079
1461
  onRowHover: handleRowHover
1080
1462
  },
1081
1463
  selection: {
@@ -1113,10 +1495,10 @@ function useGlideTable(options) {
1113
1495
  }, [
1114
1496
  enableRowSpan,
1115
1497
  primaryRowSpanKey,
1498
+ primaryRowSpanColumnId,
1116
1499
  columnRowSpanMap,
1117
1500
  hoveredRowIndex,
1118
- hoveredGroupKey,
1119
- selectedGroupKeys,
1501
+ selectedRowIndices,
1120
1502
  handleRowHover,
1121
1503
  rowSelectionMode,
1122
1504
  selectOnRowClick,
@@ -1142,6 +1524,14 @@ function useGlideTable(options) {
1142
1524
  labels.expandRow,
1143
1525
  labels.collapseRow
1144
1526
  ]);
1527
+ const copySelectionRef = (0, import_react4.useRef)(copySelection);
1528
+ (0, import_react4.useEffect)(() => {
1529
+ copySelectionRef.current = copySelection;
1530
+ }, [copySelection]);
1531
+ const stableCopySelection = (0, import_react4.useCallback)((options2) => copySelectionRef.current(options2), []);
1532
+ (0, import_react4.useEffect)(() => {
1533
+ onCopyActionsReady?.({ copySelection: stableCopySelection });
1534
+ }, [onCopyActionsReady, stableCopySelection]);
1145
1535
  return {
1146
1536
  table,
1147
1537
  tableData,
@@ -1161,7 +1551,8 @@ function useGlideTable(options) {
1161
1551
  paddingBottom,
1162
1552
  rowContextValue,
1163
1553
  handleToggleSelect,
1164
- clearHover
1554
+ clearHover,
1555
+ copySelection: stableCopySelection
1165
1556
  };
1166
1557
  }
1167
1558
  // Annotate the CommonJS export names for ESM import in node:
@@ -1176,9 +1567,13 @@ function useGlideTable(options) {
1176
1567
  applyFillData,
1177
1568
  applySelectionUpdater,
1178
1569
  buildColumnRowSpanMap,
1570
+ buildRowsPastePayload,
1179
1571
  canExpandRow,
1572
+ collectCopyRowEntries,
1573
+ collectCopyRows,
1180
1574
  collectFillChanges,
1181
1575
  collectRowSpanColumns,
1576
+ flattenSubtreeRows,
1182
1577
  getCellEditDraftValue,
1183
1578
  getCellSelectionEdgeStyle,
1184
1579
  getColumnEditType,
@@ -1186,13 +1581,22 @@ function useGlideTable(options) {
1186
1581
  hasCellSelectionEdges,
1187
1582
  isCellInSelection,
1188
1583
  isColumnEditable,
1584
+ isEditablePasteTarget,
1585
+ measureMergedSpanRowHeights,
1189
1586
  parseCellEditValue,
1587
+ parseClipboardTSV,
1588
+ parseClipboardTSVWithDepths,
1190
1589
  resolveDataTableLabels,
1590
+ resolvePasteColumnIds,
1191
1591
  resolveRowSelection,
1192
1592
  resolveRowSpanAt,
1593
+ rowRangeToHeightRatios,
1594
+ serializeCopyRowsToTSV,
1595
+ serializeSelectionToTSV,
1193
1596
  toggleExpandedRowId,
1194
1597
  useCellEdit,
1195
1598
  useCellSelection,
1196
1599
  useConvertTreeData,
1197
- useGlideTable
1600
+ useGlideTable,
1601
+ writeSelectionToClipboard
1198
1602
  });