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/compound.cjs CHANGED
@@ -128,10 +128,36 @@ function getCellSelectionBounds(start, end) {
128
128
  endCol: Math.max(start.col, end.col)
129
129
  };
130
130
  }
131
+ function measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement) {
132
+ if (rowSpan <= 1) return void 0;
133
+ const tbody = cellElement?.closest("tbody") ?? (typeof document !== "undefined" ? document.querySelector("tbody.data-table-body") : null);
134
+ if (!tbody) return void 0;
135
+ const rows = tbody.querySelectorAll(":scope > tr");
136
+ if (rows.length < rowIndex + rowSpan) return void 0;
137
+ const heights = [];
138
+ for (let i = 0; i < rowSpan; i++) {
139
+ const row = rows[rowIndex + i];
140
+ const height = row?.getBoundingClientRect().height ?? 0;
141
+ if (height <= 0) return void 0;
142
+ heights.push(height);
143
+ }
144
+ return heights;
145
+ }
131
146
  function getRowIndexInMergedCell(clientY, cellElement, rowIndex, rowSpan) {
132
147
  if (rowSpan <= 1) return rowIndex;
133
148
  const rect = cellElement.getBoundingClientRect();
134
149
  const relativeY = clientY - rect.top;
150
+ const heights = measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement);
151
+ if (heights && heights.length === rowSpan) {
152
+ let accrued = 0;
153
+ for (let i = 0; i < rowSpan; i++) {
154
+ accrued += heights[i];
155
+ if (relativeY < accrued) {
156
+ return rowIndex + i;
157
+ }
158
+ }
159
+ return rowIndex + rowSpan - 1;
160
+ }
135
161
  const rowHeight = rect.height / rowSpan;
136
162
  const offset = Math.min(
137
163
  Math.max(Math.floor(relativeY / rowHeight), 0),
@@ -153,7 +179,35 @@ function getActiveSelectionBounds(dragState, selectionBounds) {
153
179
  var SELECTION_EDGE_WIDTH_PX = 2;
154
180
  var SELECTION_EDGE_COLOR = "var(--color-brand-primary)";
155
181
  var CELL_SELECTION_EDGES_CLASS = "cell-selection-edges";
156
- function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt) {
182
+ function rowRangeToHeightRatios(rowIndex, span, fromRow, toRowExclusive, rowHeights) {
183
+ const clampedFrom = Math.max(fromRow, rowIndex);
184
+ const clampedTo = Math.min(toRowExclusive, rowIndex + span);
185
+ if (clampedTo <= clampedFrom) {
186
+ return { offsetRatio: 0, lengthRatio: 0 };
187
+ }
188
+ if (!rowHeights || rowHeights.length !== span) {
189
+ return {
190
+ offsetRatio: (clampedFrom - rowIndex) / span,
191
+ lengthRatio: (clampedTo - clampedFrom) / span
192
+ };
193
+ }
194
+ const total = rowHeights.reduce((sum, height) => sum + height, 0) || 1;
195
+ let offsetPx = 0;
196
+ for (let i = 0; i < clampedFrom - rowIndex; i++) {
197
+ offsetPx += rowHeights[i] ?? 0;
198
+ }
199
+ let lengthPx = 0;
200
+ for (let i = clampedFrom - rowIndex; i < clampedTo - rowIndex; i++) {
201
+ lengthPx += rowHeights[i] ?? 0;
202
+ }
203
+ return {
204
+ offsetRatio: offsetPx / total,
205
+ lengthRatio: lengthPx / total,
206
+ offsetPx,
207
+ lengthPx
208
+ };
209
+ }
210
+ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt, rowHeights) {
157
211
  const cellEndRow = rowIndex + rowSpan - 1;
158
212
  const span = cellEndRow - rowIndex + 1;
159
213
  if (span <= 1) return [];
@@ -172,20 +226,30 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
172
226
  continue;
173
227
  }
174
228
  if (runStart !== null) {
175
- edges.push({
176
- side,
177
- offsetRatio: (runStart - rowIndex) / span,
178
- heightRatio: (row - runStart) / span
179
- });
229
+ const ratios = rowRangeToHeightRatios(
230
+ rowIndex,
231
+ span,
232
+ runStart,
233
+ row,
234
+ rowHeights
235
+ );
236
+ if (ratios.lengthRatio > 0) {
237
+ edges.push({ side, ...ratios });
238
+ }
180
239
  runStart = null;
181
240
  }
182
241
  }
183
242
  if (runStart !== null) {
184
- edges.push({
185
- side,
186
- offsetRatio: (runStart - rowIndex) / span,
187
- heightRatio: (toRowExclusive - runStart) / span
188
- });
243
+ const ratios = rowRangeToHeightRatios(
244
+ rowIndex,
245
+ span,
246
+ runStart,
247
+ toRowExclusive,
248
+ rowHeights
249
+ );
250
+ if (ratios.lengthRatio > 0) {
251
+ edges.push({ side, ...ratios });
252
+ }
189
253
  }
190
254
  };
191
255
  const collectSide = (side, neighborCol) => {
@@ -214,14 +278,17 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
214
278
  }
215
279
  return edges;
216
280
  }
217
- function buildPartialVerticalGradient(edge) {
281
+ function buildPartialEdgeGradient(edge) {
282
+ const usePx = edge.offsetPx != null && edge.lengthPx != null;
218
283
  const startPct = edge.offsetRatio * 100;
219
- const endPct = (edge.offsetRatio + edge.heightRatio) * 100;
220
- const overlapPx = SELECTION_EDGE_WIDTH_PX + 1;
221
- const isTopProtrusion = edge.offsetRatio === 0 && edge.heightRatio < 1;
222
- const isBottomProtrusion = edge.offsetRatio > 0;
223
- const startStop = isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
224
- const endStop = isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
284
+ const endPct = (edge.offsetRatio + edge.lengthRatio) * 100;
285
+ const startPx = edge.offsetPx ?? 0;
286
+ const endPx = (edge.offsetPx ?? 0) + (edge.lengthPx ?? 0);
287
+ const overlapPx = SELECTION_EDGE_WIDTH_PX;
288
+ const isTopProtrusion = (usePx ? startPx === 0 : edge.offsetRatio === 0) && edge.lengthRatio < 1;
289
+ const isBottomProtrusion = usePx ? startPx > 0 : edge.offsetRatio > 0;
290
+ const startStop = usePx ? isBottomProtrusion ? `${Math.max(0, startPx - overlapPx)}px` : `${startPx}px` : isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
291
+ const endStop = usePx ? isTopProtrusion ? `${endPx + overlapPx}px` : `${endPx}px` : isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
225
292
  const xPos = edge.side === "left" ? "0" : "100%";
226
293
  const layers = [
227
294
  {
@@ -231,7 +298,7 @@ function buildPartialVerticalGradient(edge) {
231
298
  }
232
299
  ];
233
300
  if (isTopProtrusion || isBottomProtrusion) {
234
- const capTop = isTopProtrusion ? `calc(${endPct}% - ${SELECTION_EDGE_WIDTH_PX}px)` : `calc(${startPct}% - ${SELECTION_EDGE_WIDTH_PX}px)`;
301
+ 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)`;
235
302
  layers.push({
236
303
  image: `linear-gradient(${SELECTION_EDGE_COLOR}, ${SELECTION_EDGE_COLOR})`,
237
304
  size: `${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_WIDTH_PX}px`,
@@ -240,7 +307,7 @@ function buildPartialVerticalGradient(edge) {
240
307
  }
241
308
  return layers;
242
309
  }
243
- function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt) {
310
+ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt, rowHeights) {
244
311
  if (!isCellInSelection(rowIndex, colIndex, bounds, rowSpan) || !bounds)
245
312
  return void 0;
246
313
  const cellEndRow = rowIndex + rowSpan - 1;
@@ -249,39 +316,47 @@ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVi
249
316
  const isLeftEdge = colIndex === bounds.startCol;
250
317
  const isRightEdge = colIndex === bounds.endCol;
251
318
  const selectionContinuesBelow = cellEndRow < bounds.endRow;
252
- const shadows = [];
253
- if (isTopEdge) {
254
- shadows.push(
255
- `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
256
- );
257
- }
258
- if (isBottomEdge) {
259
- shadows.push(
260
- `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
261
- );
262
- }
263
- if (isLeftEdge) {
264
- shadows.push(
265
- `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
266
- );
267
- }
268
- if (isRightEdge) {
269
- shadows.push(
270
- `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
271
- );
272
- }
273
319
  const stepEdges = getMergedCellStepEdges(
274
320
  rowIndex,
275
321
  colIndex,
276
322
  bounds,
277
323
  rowSpan,
278
- isVisuallySelectedAt
324
+ isVisuallySelectedAt,
325
+ rowHeights
279
326
  );
327
+ const shadows = [];
328
+ const hasFullPerimeter = isTopEdge && isBottomEdge && isLeftEdge && isRightEdge && stepEdges.length === 0;
329
+ if (hasFullPerimeter) {
330
+ shadows.push(
331
+ `inset 0 0 0 ${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_COLOR}`
332
+ );
333
+ } else {
334
+ if (isTopEdge) {
335
+ shadows.push(
336
+ `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
337
+ );
338
+ }
339
+ if (isBottomEdge) {
340
+ shadows.push(
341
+ `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
342
+ );
343
+ }
344
+ if (isLeftEdge) {
345
+ shadows.push(
346
+ `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
347
+ );
348
+ }
349
+ if (isRightEdge) {
350
+ shadows.push(
351
+ `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
352
+ );
353
+ }
354
+ }
280
355
  const gradients = [];
281
356
  const sizes = [];
282
357
  const positions = [];
283
358
  for (const edge of stepEdges) {
284
- for (const partial of buildPartialVerticalGradient(edge)) {
359
+ for (const partial of buildPartialEdgeGradient(edge)) {
285
360
  gradients.push(partial.image);
286
361
  sizes.push(partial.size);
287
362
  positions.push(partial.position);
@@ -396,15 +471,16 @@ var useConvertTreeData = ({
396
471
  children: [],
397
472
  processed: false
398
473
  }));
399
- const itemMap = /* @__PURE__ */ new Map();
400
- dataWithLevels.forEach((item) => {
401
- const key = getFieldValue(item, toggleField);
402
- if (typeof key !== "string" || !key) return;
403
- if (!itemMap.has(key)) {
404
- itemMap.set(key, []);
474
+ const findNearestPrecedingParent = (index, parentKey) => {
475
+ for (let i = index - 1; i >= 0; i -= 1) {
476
+ const candidate = dataWithLevels[i];
477
+ if (!candidate) continue;
478
+ if (getFieldValue(candidate, toggleField) === parentKey) {
479
+ return candidate;
480
+ }
405
481
  }
406
- itemMap.get(key)?.push(item);
407
- });
482
+ return void 0;
483
+ };
408
484
  const rootItems = [];
409
485
  dataWithLevels.forEach((item) => {
410
486
  if (!getFieldValue(item, childField)) {
@@ -412,29 +488,18 @@ var useConvertTreeData = ({
412
488
  item.processed = true;
413
489
  }
414
490
  });
415
- dataWithLevels.forEach((item) => {
491
+ dataWithLevels.forEach((item, index) => {
416
492
  const parentKey = getFieldValue(item, childField);
417
493
  if (!parentKey || item.processed) return;
418
- const parentItems = dataWithLevels.filter(
419
- (parent) => getFieldValue(parent, toggleField) === parentKey && !getFieldValue(parent, childField)
420
- );
421
- if (parentItems.length > 0) {
422
- const parent = parentItems[0];
494
+ const parent = findNearestPrecedingParent(index, parentKey);
495
+ if (parent) {
423
496
  item.level = parent.level + 1;
424
497
  parent.children.push(item);
425
498
  item.processed = true;
426
- } else {
427
- const otherParents = itemMap.get(String(parentKey)) || [];
428
- if (otherParents.length > 0) {
429
- const parent = otherParents[0];
430
- item.level = parent.level + 1;
431
- parent.children.push(item);
432
- item.processed = true;
433
- } else {
434
- rootItems.push(item);
435
- item.processed = true;
436
- }
499
+ return;
437
500
  }
501
+ rootItems.push(item);
502
+ item.processed = true;
438
503
  });
439
504
  return rootItems;
440
505
  }, [enabled, data, toggleField, childField, flattenField]);
@@ -459,16 +524,23 @@ var useConvertTreeData = ({
459
524
  return result;
460
525
  };
461
526
  const flattenedData = flatten(processedData, [], 0);
462
- flattenedData.forEach((item) => {
463
- if (getFieldValue(item, childField)) {
464
- const parentItem = flattenedData.find(
465
- (parent) => getFieldValue(parent, toggleField) === getFieldValue(item, childField)
466
- );
467
- const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
468
- item.parentCount = parentAmount || 1;
469
- } else {
527
+ flattenedData.forEach((item, index) => {
528
+ const parentKey = getFieldValue(item, childField);
529
+ if (!parentKey) {
470
530
  item.parentCount = 1;
531
+ return;
532
+ }
533
+ let parentItem;
534
+ for (let i = index - 1; i >= 0; i -= 1) {
535
+ const candidate = flattenedData[i];
536
+ if (!candidate) continue;
537
+ if (getFieldValue(candidate, toggleField) === parentKey) {
538
+ parentItem = candidate;
539
+ break;
540
+ }
471
541
  }
542
+ const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
543
+ item.parentCount = parentAmount || 1;
472
544
  });
473
545
  return flattenedData;
474
546
  }, [
@@ -740,11 +812,10 @@ function DataTableRow({
740
812
  const { classNames, rowSpan, selection, cellSelection, cellEdit, expand } = useDataTableRowContext();
741
813
  const {
742
814
  enableRowSpan,
743
- primaryRowSpanKey,
815
+ primaryRowSpanColumnId,
744
816
  columnRowSpanMap,
745
817
  hoveredRowIndex,
746
- hoveredGroupKey,
747
- selectedGroupKeys,
818
+ selectedRowIndices,
748
819
  onRowHover
749
820
  } = rowSpan;
750
821
  const { rowSelectionMode, selectOnRowClick, onRowClick, getRowClassName } = selection;
@@ -777,9 +848,11 @@ function DataTableRow({
777
848
  const rowData = row.original;
778
849
  const isRowHovered = hoveredRowIndex === rowIndex;
779
850
  const isRowSelected = row.getIsSelected();
780
- const rowGroupKey = primaryRowSpanKey !== void 0 && rowData[primaryRowSpanKey] !== null && rowData[primaryRowSpanKey] !== void 0 ? String(rowData[primaryRowSpanKey]) : null;
781
- const isGroupHovered = enableRowSpan && hoveredGroupKey !== null && rowGroupKey === hoveredGroupKey;
782
- const isGroupSelected = enableRowSpan && rowGroupKey !== null && selectedGroupKeys.has(rowGroupKey);
851
+ const { startRow: primaryGroupStart, rowSpan: primaryGroupSpan } = resolveRowSpanAt(
852
+ primaryRowSpanColumnId ? columnRowSpanMap.get(primaryRowSpanColumnId) : void 0,
853
+ rowIndex
854
+ );
855
+ const isGroupHovered = enableRowSpan && hoveredRowIndex !== null && hoveredRowIndex >= primaryGroupStart && hoveredRowIndex <= primaryGroupStart + primaryGroupSpan - 1;
783
856
  const visibleCells = row.getVisibleCells();
784
857
  const columnIdsByIndex = visibleCells.map((cell) => cell.column.id);
785
858
  const isVisuallySelectedAt = activeSelectionBounds ? (targetRow, targetCol) => {
@@ -853,9 +926,19 @@ function DataTableRow({
853
926
  }
854
927
  }
855
928
  const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.colIndex === cellIndex;
856
- const showCellHover = isRowSpanColumn ? isGroupHovered : isRowHovered;
857
- const showCellSelected = isRowSpanColumn ? isGroupSelected : isRowSelected;
858
929
  const cellRowSpan = rowSpanInfo?.rowSpan ?? 1;
930
+ const isMergedCellHovered = hoveredRowIndex !== null && hoveredRowIndex >= rowIndex && hoveredRowIndex <= rowIndex + cellRowSpan - 1;
931
+ const showCellHover = isRowSpanColumn ? isMergedCellHovered : isRowHovered;
932
+ let isMergedCellSelected = false;
933
+ if (isRowSpanColumn) {
934
+ for (let r = rowIndex; r < rowIndex + cellRowSpan; r += 1) {
935
+ if (selectedRowIndices.has(r)) {
936
+ isMergedCellSelected = true;
937
+ break;
938
+ }
939
+ }
940
+ }
941
+ const showCellSelected = isRowSpanColumn ? isMergedCellSelected : isRowSelected;
859
942
  const isMerged = cellRowSpan > 1;
860
943
  const showMergedRightEdge = isMerged && (cellIndex === visibleCells.length - 1 || resolveRowSpanAt(
861
944
  columnRowSpanMap.get(columnIdsByIndex[cellIndex + 1]),
@@ -868,12 +951,14 @@ function DataTableRow({
868
951
  cellRowSpan
869
952
  );
870
953
  const isBottomRightCell = !isEditing && activeSelectionBounds && !dragState.isSelecting && activeSelectionBounds.endRow >= rowIndex && activeSelectionBounds.endRow <= rowIndex + cellRowSpan - 1 && cellIndex === activeSelectionBounds.endCol;
954
+ const spanRowHeights = enableCellSelection && activeSelectionBounds && isCellDragSelected && cellRowSpan > 1 ? measureMergedSpanRowHeights(rowIndex, cellRowSpan) : void 0;
871
955
  const selectionEdgeStyle = getCellSelectionEdgeStyle(
872
956
  rowIndex,
873
957
  cellIndex,
874
958
  activeSelectionBounds,
875
959
  cellRowSpan,
876
- isVisuallySelectedAt
960
+ isVisuallySelectedAt,
961
+ spanRowHeights
877
962
  );
878
963
  const resolveCellRowIndex = (clientY, element) => getRowIndexInMergedCell(clientY, element, rowIndex, cellRowSpan);
879
964
  const hasSelectionEdges = hasCellSelectionEdges(selectionEdgeStyle);
@@ -883,6 +968,7 @@ function DataTableRow({
883
968
  rowSpan: rowSpanInfo && rowSpanInfo.rowSpan > 1 ? rowSpanInfo.rowSpan : void 0,
884
969
  "data-merged": isMerged && cellIndex > 0 ? "" : void 0,
885
970
  "data-merged-edge-right": showMergedRightEdge ? "" : void 0,
971
+ "data-merged-row-first": isMerged && cellIndex === 0 && showMergedRightEdge ? "" : void 0,
886
972
  "data-group-selected": enableRowSpan && showCellSelected ? "" : void 0,
887
973
  "data-group-hovered": enableRowSpan && showCellHover && !showCellSelected ? "" : void 0,
888
974
  "data-selection-fill": isCellDragSelected ? "" : void 0,
@@ -927,7 +1013,8 @@ function DataTableRow({
927
1013
  "data-table-cell",
928
1014
  CELL_ALIGN_CLASS[align],
929
1015
  cellClassName,
930
- isMerged && cellIndex > 0 && "is-merged",
1016
+ isMerged && "is-merged",
1017
+ isMerged && cellIndex === 0 && showMergedRightEdge && "is-merged-row-first",
931
1018
  showMergedRightEdge && "is-merged-edge-right",
932
1019
  enableRowSpan && showCellSelected && "is-group-selected",
933
1020
  enableRowSpan && showCellHover && !showCellSelected && "is-group-hovered",
@@ -1176,6 +1263,108 @@ function useCellEdit({
1176
1263
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1177
1264
  var import_react5 = require("react");
1178
1265
 
1266
+ // src/components/ui/table/features/cell-selection/copyData.ts
1267
+ function formatCellValue(value) {
1268
+ if (value === null || value === void 0) return "";
1269
+ return String(value);
1270
+ }
1271
+ function getNestedValue(row, path) {
1272
+ if (!path.includes(".")) return row[path];
1273
+ return path.split(".").reduce((current, key) => {
1274
+ if (current === null || current === void 0 || typeof current !== "object") {
1275
+ return void 0;
1276
+ }
1277
+ return current[key];
1278
+ }, row);
1279
+ }
1280
+ function readRowColumnValue(rowData, columnDef) {
1281
+ if ("accessorFn" in columnDef && typeof columnDef.accessorFn === "function") {
1282
+ return columnDef.accessorFn(rowData, 0);
1283
+ }
1284
+ if ("accessorKey" in columnDef && columnDef.accessorKey != null && columnDef.accessorKey !== "") {
1285
+ return getNestedValue(rowData, String(columnDef.accessorKey));
1286
+ }
1287
+ return void 0;
1288
+ }
1289
+ function hasSubtree(row) {
1290
+ const children = row.children;
1291
+ return Array.isArray(children) && children.length > 0;
1292
+ }
1293
+ function getOriginalRowId(original) {
1294
+ return String(original.id ?? original.uniqueId ?? "");
1295
+ }
1296
+ function getRowDepth(original) {
1297
+ return typeof original.level === "number" ? original.level : 0;
1298
+ }
1299
+ function collectCopyRowEntries(visibleRows, bounds, mode = "visible") {
1300
+ const { startRow, endRow } = bounds;
1301
+ const result = [];
1302
+ const includedOriginalIds = /* @__PURE__ */ new Set();
1303
+ const appendSubtree = (node, depth) => {
1304
+ const children = node.children;
1305
+ if (!Array.isArray(children) || children.length === 0) return;
1306
+ for (const child of children) {
1307
+ const childId = getOriginalRowId(child);
1308
+ if (!(childId && includedOriginalIds.has(childId))) {
1309
+ result.push({ row: child, depth });
1310
+ if (childId) includedOriginalIds.add(childId);
1311
+ }
1312
+ appendSubtree(child, depth + 1);
1313
+ }
1314
+ };
1315
+ for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) {
1316
+ const row = visibleRows[rowIndex];
1317
+ if (!row) continue;
1318
+ const originalId = getOriginalRowId(row.original);
1319
+ if (originalId && includedOriginalIds.has(originalId)) continue;
1320
+ const depth = getRowDepth(row.original);
1321
+ result.push({ row: row.original, depth });
1322
+ if (originalId) includedOriginalIds.add(originalId);
1323
+ if (mode !== "subtree" || !hasSubtree(row.original)) continue;
1324
+ appendSubtree(row.original, depth + 1);
1325
+ }
1326
+ return result;
1327
+ }
1328
+ function serializeCopyRowsToTSV(copyRows, visibleRows, bounds, depths) {
1329
+ if (copyRows.length === 0) return "";
1330
+ const { startCol, endCol } = bounds;
1331
+ const columnCells = visibleRows[0]?.getVisibleCells().slice(startCol, endCol + 1) ?? [];
1332
+ if (columnCells.length === 0) return "";
1333
+ const resolvedDepths = depths && depths.length === copyRows.length ? depths : copyRows.map((row) => getRowDepth(row));
1334
+ const minDepth = Math.min(...resolvedDepths);
1335
+ return copyRows.map((rowData, index) => {
1336
+ const relativeDepth = Math.max(0, (resolvedDepths[index] ?? 0) - minDepth);
1337
+ const line = columnCells.map(
1338
+ (cell) => formatCellValue(
1339
+ readRowColumnValue(
1340
+ rowData,
1341
+ cell.column.columnDef
1342
+ )
1343
+ )
1344
+ ).join(" ");
1345
+ return `${" ".repeat(relativeDepth)}${line}`;
1346
+ }).join("\n");
1347
+ }
1348
+ function serializeSelectionToTSV(visibleRows, bounds, mode = "visible") {
1349
+ const entries = collectCopyRowEntries(visibleRows, bounds, mode);
1350
+ return serializeCopyRowsToTSV(
1351
+ entries.map((entry) => entry.row),
1352
+ visibleRows,
1353
+ bounds,
1354
+ entries.map((entry) => entry.depth)
1355
+ );
1356
+ }
1357
+ async function writeSelectionToClipboard(visibleRows, bounds, mode = "visible") {
1358
+ const text = serializeSelectionToTSV(visibleRows, bounds, mode);
1359
+ if (!text) return false;
1360
+ try {
1361
+ await navigator.clipboard.writeText(text);
1362
+ } catch {
1363
+ return false;
1364
+ }
1365
+ return true;
1366
+ }
1367
+
1179
1368
  // src/components/ui/table/features/cell-selection/fillData.ts
1180
1369
  function getColumnAccessorKey2(columnDef) {
1181
1370
  if ("accessorKey" in columnDef && columnDef.accessorKey) {
@@ -1232,15 +1421,100 @@ function hasFillExtension(sourceBounds, fillBounds) {
1232
1421
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
1233
1422
  }
1234
1423
 
1424
+ // src/components/ui/table/features/cell-selection/pasteData.ts
1425
+ function countLeadingEmptyCells(cells) {
1426
+ let depth = 0;
1427
+ while (depth < cells.length && cells[depth] === "") {
1428
+ depth += 1;
1429
+ }
1430
+ return depth;
1431
+ }
1432
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
1433
+ if (leadingEmptyCounts.length === 0) return false;
1434
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
1435
+ if (firstDepth !== 0) return false;
1436
+ return leadingEmptyCounts.some((depth) => depth > 0);
1437
+ }
1438
+ function parseClipboardTSVWithDepths(text) {
1439
+ if (!text) return { values: [], depths: [] };
1440
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1441
+ const withoutTrailing = normalized.replace(/\n+$/, "");
1442
+ if (!withoutTrailing) return { values: [], depths: [] };
1443
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
1444
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
1445
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
1446
+ const values = [];
1447
+ const depths = [];
1448
+ for (let index = 0; index < rows.length; index += 1) {
1449
+ const cells = rows[index] ?? [];
1450
+ const depth = leadingEmptyCounts[index] ?? 0;
1451
+ if (treatAsDepth) {
1452
+ values.push(cells.slice(depth));
1453
+ depths.push(depth);
1454
+ } else {
1455
+ values.push(cells);
1456
+ depths.push(0);
1457
+ }
1458
+ }
1459
+ return { values, depths };
1460
+ }
1461
+ function resolvePasteColumnIds(rows, startCol, width) {
1462
+ if (width <= 0) return [];
1463
+ const cells = rows[0]?.getVisibleCells() ?? [];
1464
+ const columnIds = [];
1465
+ for (let offset = 0; offset < width; offset += 1) {
1466
+ const cell = cells[startCol + offset];
1467
+ if (!cell) break;
1468
+ columnIds.push(cell.column.id);
1469
+ }
1470
+ return columnIds;
1471
+ }
1472
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
1473
+ const { values, depths } = parseClipboardTSVWithDepths(text);
1474
+ if (values.length === 0) return null;
1475
+ const width = Math.max(...values.map((row) => row.length), 0);
1476
+ if (width === 0) return null;
1477
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
1478
+ if (columnIds.length === 0) return null;
1479
+ const rowIds = [];
1480
+ for (let offset = 0; offset < values.length; offset += 1) {
1481
+ const row = rows[startRow + offset];
1482
+ if (!row) break;
1483
+ rowIds.push(row.id);
1484
+ }
1485
+ const anchorRow = rows[endRow] ?? rows[startRow];
1486
+ return {
1487
+ mode,
1488
+ startRow,
1489
+ startCol,
1490
+ endRow,
1491
+ rowIds,
1492
+ anchorRowId: anchorRow?.id ?? "",
1493
+ columnIds,
1494
+ values,
1495
+ depths
1496
+ };
1497
+ }
1498
+ function isEditablePasteTarget(target) {
1499
+ if (!(target instanceof HTMLElement)) return false;
1500
+ const tag = target.tagName;
1501
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
1502
+ return Boolean(target.isContentEditable);
1503
+ }
1504
+
1235
1505
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1236
1506
  function useCellSelection({
1237
1507
  data,
1238
1508
  rows,
1239
1509
  enabled = true,
1510
+ enableSubtreeCopy = false,
1511
+ enableInsertPaste = true,
1240
1512
  onDataChange,
1241
- onBatchChange
1513
+ onBatchChange,
1514
+ onRowsPaste
1242
1515
  }) {
1243
1516
  const [dragState, setDragState] = (0, import_react5.useState)(INITIAL_DRAG_STATE);
1517
+ const pendingPasteModeRef = (0, import_react5.useRef)(null);
1244
1518
  const cellSelectionBounds = getCellSelectionBounds(dragState.start, dragState.end);
1245
1519
  const activeSelectionBounds = enabled ? getActiveSelectionBounds(dragState, cellSelectionBounds) : null;
1246
1520
  const handleCellMouseDown = (0, import_react5.useCallback)(
@@ -1294,21 +1568,113 @@ function useCellSelection({
1294
1568
  setDragState(INITIAL_DRAG_STATE);
1295
1569
  }
1296
1570
  }, [enabled]);
1571
+ const copySelection = (0, import_react5.useCallback)(
1572
+ async (options) => {
1573
+ if (!enabled || !activeSelectionBounds) return false;
1574
+ const mode = options?.includeDescendants && enableSubtreeCopy ? "subtree" : "visible";
1575
+ return writeSelectionToClipboard(rows, activeSelectionBounds, mode);
1576
+ },
1577
+ [activeSelectionBounds, enableSubtreeCopy, enabled, rows]
1578
+ );
1297
1579
  (0, import_react5.useEffect)(() => {
1298
1580
  if (!enabled) return;
1299
1581
  const handleKeyDown = (e) => {
1300
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && activeSelectionBounds) {
1301
- const { startRow, endRow, startCol, endCol } = activeSelectionBounds;
1302
- const selectedData = rows.slice(startRow, endRow + 1).map((row) => {
1303
- const cells = row.getVisibleCells();
1304
- return cells.slice(startCol, endCol + 1).map((cell) => cell.getValue()).join(" ");
1305
- }).join("\n");
1306
- navigator.clipboard.writeText(selectedData);
1307
- }
1582
+ if (!activeSelectionBounds) return;
1583
+ if (!(e.ctrlKey || e.metaKey)) return;
1584
+ const isSubtreeShortcut = enableSubtreeCopy && e.shiftKey && e.key.toLowerCase() === "c";
1585
+ const isVisibleCopyShortcut = !e.shiftKey && e.key.toLowerCase() === "c";
1586
+ if (!isSubtreeShortcut && !isVisibleCopyShortcut) return;
1587
+ e.preventDefault();
1588
+ void copySelection({ includeDescendants: isSubtreeShortcut });
1308
1589
  };
1309
1590
  window.addEventListener("keydown", handleKeyDown);
1310
1591
  return () => window.removeEventListener("keydown", handleKeyDown);
1311
- }, [activeSelectionBounds, enabled, rows]);
1592
+ }, [activeSelectionBounds, copySelection, enableSubtreeCopy, enabled]);
1593
+ const emitRowsPaste = (0, import_react5.useCallback)(
1594
+ (text, mode) => {
1595
+ if (!onRowsPaste || !activeSelectionBounds) return false;
1596
+ const payload = buildRowsPastePayload(
1597
+ rows,
1598
+ activeSelectionBounds.startRow,
1599
+ activeSelectionBounds.startCol,
1600
+ text,
1601
+ mode,
1602
+ activeSelectionBounds.endRow
1603
+ );
1604
+ if (!payload) return false;
1605
+ onRowsPaste(payload);
1606
+ return true;
1607
+ },
1608
+ [activeSelectionBounds, onRowsPaste, rows]
1609
+ );
1610
+ (0, import_react5.useEffect)(() => {
1611
+ if (!enabled || !onRowsPaste) return;
1612
+ const pasteHandledRef = { current: false };
1613
+ const ignoreNextPasteRef = { current: false };
1614
+ const handleKeyDown = (e) => {
1615
+ if (!activeSelectionBounds) return;
1616
+ if (!(e.ctrlKey || e.metaKey)) return;
1617
+ if (e.key.toLowerCase() !== "v") return;
1618
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
1619
+ return;
1620
+ }
1621
+ if (e.shiftKey && !enableInsertPaste) {
1622
+ ignoreNextPasteRef.current = true;
1623
+ pendingPasteModeRef.current = null;
1624
+ return;
1625
+ }
1626
+ const mode = e.shiftKey ? "insert" : "overwrite";
1627
+ pasteHandledRef.current = false;
1628
+ ignoreNextPasteRef.current = false;
1629
+ pendingPasteModeRef.current = mode;
1630
+ void (async () => {
1631
+ try {
1632
+ const text = await navigator.clipboard.readText();
1633
+ if (pasteHandledRef.current) return;
1634
+ if (pendingPasteModeRef.current !== mode) return;
1635
+ if (!text) return;
1636
+ pasteHandledRef.current = true;
1637
+ emitRowsPaste(text, mode);
1638
+ pendingPasteModeRef.current = null;
1639
+ } catch {
1640
+ }
1641
+ })();
1642
+ };
1643
+ const handlePaste = (e) => {
1644
+ if (!activeSelectionBounds) return;
1645
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
1646
+ return;
1647
+ }
1648
+ if (ignoreNextPasteRef.current) {
1649
+ ignoreNextPasteRef.current = false;
1650
+ pendingPasteModeRef.current = null;
1651
+ return;
1652
+ }
1653
+ const mode = pendingPasteModeRef.current ?? "overwrite";
1654
+ if (pasteHandledRef.current) {
1655
+ e.preventDefault();
1656
+ return;
1657
+ }
1658
+ const text = e.clipboardData?.getData("text/plain");
1659
+ if (text == null || text === "") return;
1660
+ pasteHandledRef.current = true;
1661
+ e.preventDefault();
1662
+ emitRowsPaste(text, mode);
1663
+ pendingPasteModeRef.current = null;
1664
+ };
1665
+ window.addEventListener("keydown", handleKeyDown);
1666
+ window.addEventListener("paste", handlePaste);
1667
+ return () => {
1668
+ window.removeEventListener("keydown", handleKeyDown);
1669
+ window.removeEventListener("paste", handlePaste);
1670
+ };
1671
+ }, [
1672
+ activeSelectionBounds,
1673
+ emitRowsPaste,
1674
+ enableInsertPaste,
1675
+ enabled,
1676
+ onRowsPaste
1677
+ ]);
1312
1678
  (0, import_react5.useEffect)(() => {
1313
1679
  if (!enabled) return;
1314
1680
  const handleMouseUp = () => {
@@ -1354,7 +1720,8 @@ function useCellSelection({
1354
1720
  activeSelectionBounds,
1355
1721
  handleCellMouseDown,
1356
1722
  handleCellMouseEnter,
1357
- handleFillHandleMouseDown
1723
+ handleFillHandleMouseDown,
1724
+ copySelection
1358
1725
  };
1359
1726
  }
1360
1727
 
@@ -1418,6 +1785,10 @@ function useGlideTable(options) {
1418
1785
  expandedRows: controlledExpandedRows,
1419
1786
  onExpandedRowsChange,
1420
1787
  preventExpand = false,
1788
+ enableSubtreeCopy,
1789
+ onCopyActionsReady,
1790
+ onRowsPaste,
1791
+ enableInsertPaste,
1421
1792
  enableVirtualization = true,
1422
1793
  estimateRowHeight = DATA_TABLE_ROW_HEIGHT,
1423
1794
  virtualOverscan = DATA_TABLE_VIRTUAL_OVERSCAN
@@ -1432,12 +1803,12 @@ function useGlideTable(options) {
1432
1803
  };
1433
1804
  }, [labelsProp, emptyText, loadingText, selectionLabel]);
1434
1805
  const enableExpand = Boolean(toggleField);
1806
+ const resolvedEnableSubtreeCopy = enableSubtreeCopy ?? enableExpand;
1435
1807
  const [internalRowSelection, setInternalRowSelection] = (0, import_react6.useState)({});
1436
1808
  const [internalExpandedRows, setInternalExpandedRows] = (0, import_react6.useState)(
1437
1809
  () => /* @__PURE__ */ new Set()
1438
1810
  );
1439
1811
  const [hoveredRowIndex, setHoveredRowIndex] = (0, import_react6.useState)(null);
1440
- const [hoveredGroupKey, setHoveredGroupKey] = (0, import_react6.useState)(null);
1441
1812
  const scrollRef = (0, import_react6.useRef)(null);
1442
1813
  const shouldVirtualize = enableVirtualization && !enableRowSpan;
1443
1814
  (0, import_react6.useEffect)(() => {
@@ -1501,6 +1872,7 @@ function useGlideTable(options) {
1501
1872
  return collectRowSpanColumns(columns);
1502
1873
  }, [enableRowSpan, columns]);
1503
1874
  const primaryRowSpanKey = rowSpanColumnKeys[0]?.rowSpanKey;
1875
+ const primaryRowSpanColumnId = rowSpanColumnKeys[0]?.columnId;
1504
1876
  const columnRowSpanMap = (0, import_react6.useMemo)(
1505
1877
  () => buildColumnRowSpanMap(tableData, rowSpanColumnKeys),
1506
1878
  [tableData, rowSpanColumnKeys]
@@ -1519,27 +1891,29 @@ function useGlideTable(options) {
1519
1891
  const totalSize = rowVirtualizer.getTotalSize();
1520
1892
  const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
1521
1893
  const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;
1522
- const selectedGroupKeys = (0, import_react6.useMemo)(() => {
1523
- if (!enableRowSpan || !primaryRowSpanKey) return /* @__PURE__ */ new Set();
1524
- const keys = /* @__PURE__ */ new Set();
1894
+ const selectedRowIndices = (0, import_react6.useMemo)(() => {
1895
+ const indices = /* @__PURE__ */ new Set();
1525
1896
  for (const selectedRow of selectedRows) {
1526
- const value = selectedRow.original[primaryRowSpanKey];
1527
- if (value !== null && value !== void 0) keys.add(String(value));
1897
+ indices.add(selectedRow.index);
1528
1898
  }
1529
- return keys;
1530
- }, [enableRowSpan, primaryRowSpanKey, selectedRows]);
1899
+ return indices;
1900
+ }, [selectedRows]);
1531
1901
  const {
1532
1902
  dragState,
1533
1903
  activeSelectionBounds,
1534
1904
  handleCellMouseDown,
1535
1905
  handleCellMouseEnter,
1536
- handleFillHandleMouseDown
1906
+ handleFillHandleMouseDown,
1907
+ copySelection
1537
1908
  } = useCellSelection({
1538
1909
  data: tableData,
1539
1910
  rows,
1540
1911
  enabled: enableCellSelection,
1912
+ enableSubtreeCopy: resolvedEnableSubtreeCopy,
1913
+ enableInsertPaste: enableInsertPaste ?? true,
1541
1914
  onDataChange,
1542
- onBatchChange
1915
+ onBatchChange,
1916
+ onRowsPaste
1543
1917
  });
1544
1918
  const {
1545
1919
  editingCell,
@@ -1561,22 +1935,10 @@ function useGlideTable(options) {
1561
1935
  );
1562
1936
  const clearHover = (0, import_react6.useCallback)(() => {
1563
1937
  setHoveredRowIndex(null);
1564
- setHoveredGroupKey(null);
1565
1938
  }, []);
1566
- const handleRowHover = (0, import_react6.useCallback)(
1567
- (rowIndex, rowData) => {
1568
- setHoveredRowIndex(rowIndex);
1569
- if (!primaryRowSpanKey) {
1570
- setHoveredGroupKey(null);
1571
- return;
1572
- }
1573
- const groupValue = rowData[primaryRowSpanKey];
1574
- setHoveredGroupKey(
1575
- groupValue === null || groupValue === void 0 ? null : String(groupValue)
1576
- );
1577
- },
1578
- [primaryRowSpanKey]
1579
- );
1939
+ const handleRowHover = (0, import_react6.useCallback)((rowIndex, _rowData) => {
1940
+ setHoveredRowIndex(rowIndex);
1941
+ }, []);
1580
1942
  const handleToggleSelect = (0, import_react6.useCallback)(
1581
1943
  (row) => {
1582
1944
  if (!row.getCanSelect()) return;
@@ -1599,10 +1961,10 @@ function useGlideTable(options) {
1599
1961
  rowSpan: {
1600
1962
  enableRowSpan,
1601
1963
  primaryRowSpanKey,
1964
+ primaryRowSpanColumnId,
1602
1965
  columnRowSpanMap,
1603
1966
  hoveredRowIndex,
1604
- hoveredGroupKey,
1605
- selectedGroupKeys,
1967
+ selectedRowIndices,
1606
1968
  onRowHover: handleRowHover
1607
1969
  },
1608
1970
  selection: {
@@ -1640,10 +2002,10 @@ function useGlideTable(options) {
1640
2002
  }, [
1641
2003
  enableRowSpan,
1642
2004
  primaryRowSpanKey,
2005
+ primaryRowSpanColumnId,
1643
2006
  columnRowSpanMap,
1644
2007
  hoveredRowIndex,
1645
- hoveredGroupKey,
1646
- selectedGroupKeys,
2008
+ selectedRowIndices,
1647
2009
  handleRowHover,
1648
2010
  rowSelectionMode,
1649
2011
  selectOnRowClick,
@@ -1669,6 +2031,14 @@ function useGlideTable(options) {
1669
2031
  labels.expandRow,
1670
2032
  labels.collapseRow
1671
2033
  ]);
2034
+ const copySelectionRef = (0, import_react6.useRef)(copySelection);
2035
+ (0, import_react6.useEffect)(() => {
2036
+ copySelectionRef.current = copySelection;
2037
+ }, [copySelection]);
2038
+ const stableCopySelection = (0, import_react6.useCallback)((options2) => copySelectionRef.current(options2), []);
2039
+ (0, import_react6.useEffect)(() => {
2040
+ onCopyActionsReady?.({ copySelection: stableCopySelection });
2041
+ }, [onCopyActionsReady, stableCopySelection]);
1672
2042
  return {
1673
2043
  table,
1674
2044
  tableData,
@@ -1688,7 +2058,8 @@ function useGlideTable(options) {
1688
2058
  paddingBottom,
1689
2059
  rowContextValue,
1690
2060
  handleToggleSelect,
1691
- clearHover
2061
+ clearHover,
2062
+ copySelection: stableCopySelection
1692
2063
  };
1693
2064
  }
1694
2065
 
@@ -2036,10 +2407,26 @@ function parseTableChildren(children) {
2036
2407
  }
2037
2408
  return slots;
2038
2409
  }
2410
+ function flattenColumnElements(children) {
2411
+ const result = [];
2412
+ for (const child of import_react9.Children.toArray(children)) {
2413
+ if (isTableColumnElement(child)) {
2414
+ result.push(child);
2415
+ continue;
2416
+ }
2417
+ if ((0, import_react9.isValidElement)(child)) {
2418
+ const nested = child.props.children;
2419
+ if (nested != null) {
2420
+ result.push(...flattenColumnElements(nested));
2421
+ }
2422
+ }
2423
+ }
2424
+ return result;
2425
+ }
2039
2426
  function extractColumnElements(header) {
2040
2427
  if (!header) return [];
2041
2428
  const { children } = header.props;
2042
- return import_react9.Children.toArray(children).filter(isTableColumnElement);
2429
+ return flattenColumnElements(children);
2043
2430
  }
2044
2431
 
2045
2432
  // src/components/ui/table/components/Table/TableBody.tsx