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.js CHANGED
@@ -100,10 +100,36 @@ function getCellSelectionBounds(start, end) {
100
100
  endCol: Math.max(start.col, end.col)
101
101
  };
102
102
  }
103
+ function measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement) {
104
+ if (rowSpan <= 1) return void 0;
105
+ const tbody = cellElement?.closest("tbody") ?? (typeof document !== "undefined" ? document.querySelector("tbody.data-table-body") : null);
106
+ if (!tbody) return void 0;
107
+ const rows = tbody.querySelectorAll(":scope > tr");
108
+ if (rows.length < rowIndex + rowSpan) return void 0;
109
+ const heights = [];
110
+ for (let i = 0; i < rowSpan; i++) {
111
+ const row = rows[rowIndex + i];
112
+ const height = row?.getBoundingClientRect().height ?? 0;
113
+ if (height <= 0) return void 0;
114
+ heights.push(height);
115
+ }
116
+ return heights;
117
+ }
103
118
  function getRowIndexInMergedCell(clientY, cellElement, rowIndex, rowSpan) {
104
119
  if (rowSpan <= 1) return rowIndex;
105
120
  const rect = cellElement.getBoundingClientRect();
106
121
  const relativeY = clientY - rect.top;
122
+ const heights = measureMergedSpanRowHeights(rowIndex, rowSpan, cellElement);
123
+ if (heights && heights.length === rowSpan) {
124
+ let accrued = 0;
125
+ for (let i = 0; i < rowSpan; i++) {
126
+ accrued += heights[i];
127
+ if (relativeY < accrued) {
128
+ return rowIndex + i;
129
+ }
130
+ }
131
+ return rowIndex + rowSpan - 1;
132
+ }
107
133
  const rowHeight = rect.height / rowSpan;
108
134
  const offset = Math.min(
109
135
  Math.max(Math.floor(relativeY / rowHeight), 0),
@@ -125,7 +151,35 @@ function getActiveSelectionBounds(dragState, selectionBounds) {
125
151
  var SELECTION_EDGE_WIDTH_PX = 2;
126
152
  var SELECTION_EDGE_COLOR = "var(--color-brand-primary)";
127
153
  var CELL_SELECTION_EDGES_CLASS = "cell-selection-edges";
128
- function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt) {
154
+ function rowRangeToHeightRatios(rowIndex, span, fromRow, toRowExclusive, rowHeights) {
155
+ const clampedFrom = Math.max(fromRow, rowIndex);
156
+ const clampedTo = Math.min(toRowExclusive, rowIndex + span);
157
+ if (clampedTo <= clampedFrom) {
158
+ return { offsetRatio: 0, lengthRatio: 0 };
159
+ }
160
+ if (!rowHeights || rowHeights.length !== span) {
161
+ return {
162
+ offsetRatio: (clampedFrom - rowIndex) / span,
163
+ lengthRatio: (clampedTo - clampedFrom) / span
164
+ };
165
+ }
166
+ const total = rowHeights.reduce((sum, height) => sum + height, 0) || 1;
167
+ let offsetPx = 0;
168
+ for (let i = 0; i < clampedFrom - rowIndex; i++) {
169
+ offsetPx += rowHeights[i] ?? 0;
170
+ }
171
+ let lengthPx = 0;
172
+ for (let i = clampedFrom - rowIndex; i < clampedTo - rowIndex; i++) {
173
+ lengthPx += rowHeights[i] ?? 0;
174
+ }
175
+ return {
176
+ offsetRatio: offsetPx / total,
177
+ lengthRatio: lengthPx / total,
178
+ offsetPx,
179
+ lengthPx
180
+ };
181
+ }
182
+ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallySelectedAt, rowHeights) {
129
183
  const cellEndRow = rowIndex + rowSpan - 1;
130
184
  const span = cellEndRow - rowIndex + 1;
131
185
  if (span <= 1) return [];
@@ -144,20 +198,30 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
144
198
  continue;
145
199
  }
146
200
  if (runStart !== null) {
147
- edges.push({
148
- side,
149
- offsetRatio: (runStart - rowIndex) / span,
150
- heightRatio: (row - runStart) / span
151
- });
201
+ const ratios = rowRangeToHeightRatios(
202
+ rowIndex,
203
+ span,
204
+ runStart,
205
+ row,
206
+ rowHeights
207
+ );
208
+ if (ratios.lengthRatio > 0) {
209
+ edges.push({ side, ...ratios });
210
+ }
152
211
  runStart = null;
153
212
  }
154
213
  }
155
214
  if (runStart !== null) {
156
- edges.push({
157
- side,
158
- offsetRatio: (runStart - rowIndex) / span,
159
- heightRatio: (toRowExclusive - runStart) / span
160
- });
215
+ const ratios = rowRangeToHeightRatios(
216
+ rowIndex,
217
+ span,
218
+ runStart,
219
+ toRowExclusive,
220
+ rowHeights
221
+ );
222
+ if (ratios.lengthRatio > 0) {
223
+ edges.push({ side, ...ratios });
224
+ }
161
225
  }
162
226
  };
163
227
  const collectSide = (side, neighborCol) => {
@@ -186,14 +250,17 @@ function getMergedCellStepEdges(rowIndex, colIndex, bounds, rowSpan, isVisuallyS
186
250
  }
187
251
  return edges;
188
252
  }
189
- function buildPartialVerticalGradient(edge) {
253
+ function buildPartialEdgeGradient(edge) {
254
+ const usePx = edge.offsetPx != null && edge.lengthPx != null;
190
255
  const startPct = edge.offsetRatio * 100;
191
- const endPct = (edge.offsetRatio + edge.heightRatio) * 100;
192
- const overlapPx = SELECTION_EDGE_WIDTH_PX + 1;
193
- const isTopProtrusion = edge.offsetRatio === 0 && edge.heightRatio < 1;
194
- const isBottomProtrusion = edge.offsetRatio > 0;
195
- const startStop = isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
196
- const endStop = isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
256
+ const endPct = (edge.offsetRatio + edge.lengthRatio) * 100;
257
+ const startPx = edge.offsetPx ?? 0;
258
+ const endPx = (edge.offsetPx ?? 0) + (edge.lengthPx ?? 0);
259
+ const overlapPx = SELECTION_EDGE_WIDTH_PX;
260
+ const isTopProtrusion = (usePx ? startPx === 0 : edge.offsetRatio === 0) && edge.lengthRatio < 1;
261
+ const isBottomProtrusion = usePx ? startPx > 0 : edge.offsetRatio > 0;
262
+ const startStop = usePx ? isBottomProtrusion ? `${Math.max(0, startPx - overlapPx)}px` : `${startPx}px` : isBottomProtrusion ? `calc(${startPct}% - ${overlapPx}px)` : `${startPct}%`;
263
+ const endStop = usePx ? isTopProtrusion ? `${endPx + overlapPx}px` : `${endPx}px` : isTopProtrusion ? `calc(${endPct}% + ${overlapPx}px)` : `${endPct}%`;
197
264
  const xPos = edge.side === "left" ? "0" : "100%";
198
265
  const layers = [
199
266
  {
@@ -203,7 +270,7 @@ function buildPartialVerticalGradient(edge) {
203
270
  }
204
271
  ];
205
272
  if (isTopProtrusion || isBottomProtrusion) {
206
- const capTop = isTopProtrusion ? `calc(${endPct}% - ${SELECTION_EDGE_WIDTH_PX}px)` : `calc(${startPct}% - ${SELECTION_EDGE_WIDTH_PX}px)`;
273
+ 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)`;
207
274
  layers.push({
208
275
  image: `linear-gradient(${SELECTION_EDGE_COLOR}, ${SELECTION_EDGE_COLOR})`,
209
276
  size: `${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_WIDTH_PX}px`,
@@ -212,7 +279,7 @@ function buildPartialVerticalGradient(edge) {
212
279
  }
213
280
  return layers;
214
281
  }
215
- function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt) {
282
+ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVisuallySelectedAt, rowHeights) {
216
283
  if (!isCellInSelection(rowIndex, colIndex, bounds, rowSpan) || !bounds)
217
284
  return void 0;
218
285
  const cellEndRow = rowIndex + rowSpan - 1;
@@ -221,39 +288,47 @@ function getCellSelectionEdgeStyle(rowIndex, colIndex, bounds, rowSpan = 1, isVi
221
288
  const isLeftEdge = colIndex === bounds.startCol;
222
289
  const isRightEdge = colIndex === bounds.endCol;
223
290
  const selectionContinuesBelow = cellEndRow < bounds.endRow;
224
- const shadows = [];
225
- if (isTopEdge) {
226
- shadows.push(
227
- `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
228
- );
229
- }
230
- if (isBottomEdge) {
231
- shadows.push(
232
- `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
233
- );
234
- }
235
- if (isLeftEdge) {
236
- shadows.push(
237
- `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
238
- );
239
- }
240
- if (isRightEdge) {
241
- shadows.push(
242
- `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
243
- );
244
- }
245
291
  const stepEdges = getMergedCellStepEdges(
246
292
  rowIndex,
247
293
  colIndex,
248
294
  bounds,
249
295
  rowSpan,
250
- isVisuallySelectedAt
296
+ isVisuallySelectedAt,
297
+ rowHeights
251
298
  );
299
+ const shadows = [];
300
+ const hasFullPerimeter = isTopEdge && isBottomEdge && isLeftEdge && isRightEdge && stepEdges.length === 0;
301
+ if (hasFullPerimeter) {
302
+ shadows.push(
303
+ `inset 0 0 0 ${SELECTION_EDGE_WIDTH_PX}px ${SELECTION_EDGE_COLOR}`
304
+ );
305
+ } else {
306
+ if (isTopEdge) {
307
+ shadows.push(
308
+ `inset 0 ${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
309
+ );
310
+ }
311
+ if (isBottomEdge) {
312
+ shadows.push(
313
+ `inset 0 -${SELECTION_EDGE_WIDTH_PX}px 0 0 ${SELECTION_EDGE_COLOR}`
314
+ );
315
+ }
316
+ if (isLeftEdge) {
317
+ shadows.push(
318
+ `inset ${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
319
+ );
320
+ }
321
+ if (isRightEdge) {
322
+ shadows.push(
323
+ `inset -${SELECTION_EDGE_WIDTH_PX}px 0 0 0 ${SELECTION_EDGE_COLOR}`
324
+ );
325
+ }
326
+ }
252
327
  const gradients = [];
253
328
  const sizes = [];
254
329
  const positions = [];
255
330
  for (const edge of stepEdges) {
256
- for (const partial of buildPartialVerticalGradient(edge)) {
331
+ for (const partial of buildPartialEdgeGradient(edge)) {
257
332
  gradients.push(partial.image);
258
333
  sizes.push(partial.size);
259
334
  positions.push(partial.position);
@@ -368,15 +443,16 @@ var useConvertTreeData = ({
368
443
  children: [],
369
444
  processed: false
370
445
  }));
371
- const itemMap = /* @__PURE__ */ new Map();
372
- dataWithLevels.forEach((item) => {
373
- const key = getFieldValue(item, toggleField);
374
- if (typeof key !== "string" || !key) return;
375
- if (!itemMap.has(key)) {
376
- itemMap.set(key, []);
446
+ const findNearestPrecedingParent = (index, parentKey) => {
447
+ for (let i = index - 1; i >= 0; i -= 1) {
448
+ const candidate = dataWithLevels[i];
449
+ if (!candidate) continue;
450
+ if (getFieldValue(candidate, toggleField) === parentKey) {
451
+ return candidate;
452
+ }
377
453
  }
378
- itemMap.get(key)?.push(item);
379
- });
454
+ return void 0;
455
+ };
380
456
  const rootItems = [];
381
457
  dataWithLevels.forEach((item) => {
382
458
  if (!getFieldValue(item, childField)) {
@@ -384,29 +460,18 @@ var useConvertTreeData = ({
384
460
  item.processed = true;
385
461
  }
386
462
  });
387
- dataWithLevels.forEach((item) => {
463
+ dataWithLevels.forEach((item, index) => {
388
464
  const parentKey = getFieldValue(item, childField);
389
465
  if (!parentKey || item.processed) return;
390
- const parentItems = dataWithLevels.filter(
391
- (parent) => getFieldValue(parent, toggleField) === parentKey && !getFieldValue(parent, childField)
392
- );
393
- if (parentItems.length > 0) {
394
- const parent = parentItems[0];
466
+ const parent = findNearestPrecedingParent(index, parentKey);
467
+ if (parent) {
395
468
  item.level = parent.level + 1;
396
469
  parent.children.push(item);
397
470
  item.processed = true;
398
- } else {
399
- const otherParents = itemMap.get(String(parentKey)) || [];
400
- if (otherParents.length > 0) {
401
- const parent = otherParents[0];
402
- item.level = parent.level + 1;
403
- parent.children.push(item);
404
- item.processed = true;
405
- } else {
406
- rootItems.push(item);
407
- item.processed = true;
408
- }
471
+ return;
409
472
  }
473
+ rootItems.push(item);
474
+ item.processed = true;
410
475
  });
411
476
  return rootItems;
412
477
  }, [enabled, data, toggleField, childField, flattenField]);
@@ -431,16 +496,23 @@ var useConvertTreeData = ({
431
496
  return result;
432
497
  };
433
498
  const flattenedData = flatten(processedData, [], 0);
434
- flattenedData.forEach((item) => {
435
- if (getFieldValue(item, childField)) {
436
- const parentItem = flattenedData.find(
437
- (parent) => getFieldValue(parent, toggleField) === getFieldValue(item, childField)
438
- );
439
- const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
440
- item.parentCount = parentAmount || 1;
441
- } else {
499
+ flattenedData.forEach((item, index) => {
500
+ const parentKey = getFieldValue(item, childField);
501
+ if (!parentKey) {
442
502
  item.parentCount = 1;
503
+ return;
504
+ }
505
+ let parentItem;
506
+ for (let i = index - 1; i >= 0; i -= 1) {
507
+ const candidate = flattenedData[i];
508
+ if (!candidate) continue;
509
+ if (getFieldValue(candidate, toggleField) === parentKey) {
510
+ parentItem = candidate;
511
+ break;
512
+ }
443
513
  }
514
+ const parentAmount = parentItem ? Number(getFieldValue(parentItem, qtyField) ?? 1) : 1;
515
+ item.parentCount = parentAmount || 1;
444
516
  });
445
517
  return flattenedData;
446
518
  }, [
@@ -712,11 +784,10 @@ function DataTableRow({
712
784
  const { classNames, rowSpan, selection, cellSelection, cellEdit, expand } = useDataTableRowContext();
713
785
  const {
714
786
  enableRowSpan,
715
- primaryRowSpanKey,
787
+ primaryRowSpanColumnId,
716
788
  columnRowSpanMap,
717
789
  hoveredRowIndex,
718
- hoveredGroupKey,
719
- selectedGroupKeys,
790
+ selectedRowIndices,
720
791
  onRowHover
721
792
  } = rowSpan;
722
793
  const { rowSelectionMode, selectOnRowClick, onRowClick, getRowClassName } = selection;
@@ -749,9 +820,11 @@ function DataTableRow({
749
820
  const rowData = row.original;
750
821
  const isRowHovered = hoveredRowIndex === rowIndex;
751
822
  const isRowSelected = row.getIsSelected();
752
- const rowGroupKey = primaryRowSpanKey !== void 0 && rowData[primaryRowSpanKey] !== null && rowData[primaryRowSpanKey] !== void 0 ? String(rowData[primaryRowSpanKey]) : null;
753
- const isGroupHovered = enableRowSpan && hoveredGroupKey !== null && rowGroupKey === hoveredGroupKey;
754
- const isGroupSelected = enableRowSpan && rowGroupKey !== null && selectedGroupKeys.has(rowGroupKey);
823
+ const { startRow: primaryGroupStart, rowSpan: primaryGroupSpan } = resolveRowSpanAt(
824
+ primaryRowSpanColumnId ? columnRowSpanMap.get(primaryRowSpanColumnId) : void 0,
825
+ rowIndex
826
+ );
827
+ const isGroupHovered = enableRowSpan && hoveredRowIndex !== null && hoveredRowIndex >= primaryGroupStart && hoveredRowIndex <= primaryGroupStart + primaryGroupSpan - 1;
755
828
  const visibleCells = row.getVisibleCells();
756
829
  const columnIdsByIndex = visibleCells.map((cell) => cell.column.id);
757
830
  const isVisuallySelectedAt = activeSelectionBounds ? (targetRow, targetCol) => {
@@ -825,9 +898,19 @@ function DataTableRow({
825
898
  }
826
899
  }
827
900
  const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.colIndex === cellIndex;
828
- const showCellHover = isRowSpanColumn ? isGroupHovered : isRowHovered;
829
- const showCellSelected = isRowSpanColumn ? isGroupSelected : isRowSelected;
830
901
  const cellRowSpan = rowSpanInfo?.rowSpan ?? 1;
902
+ const isMergedCellHovered = hoveredRowIndex !== null && hoveredRowIndex >= rowIndex && hoveredRowIndex <= rowIndex + cellRowSpan - 1;
903
+ const showCellHover = isRowSpanColumn ? isMergedCellHovered : isRowHovered;
904
+ let isMergedCellSelected = false;
905
+ if (isRowSpanColumn) {
906
+ for (let r = rowIndex; r < rowIndex + cellRowSpan; r += 1) {
907
+ if (selectedRowIndices.has(r)) {
908
+ isMergedCellSelected = true;
909
+ break;
910
+ }
911
+ }
912
+ }
913
+ const showCellSelected = isRowSpanColumn ? isMergedCellSelected : isRowSelected;
831
914
  const isMerged = cellRowSpan > 1;
832
915
  const showMergedRightEdge = isMerged && (cellIndex === visibleCells.length - 1 || resolveRowSpanAt(
833
916
  columnRowSpanMap.get(columnIdsByIndex[cellIndex + 1]),
@@ -840,12 +923,14 @@ function DataTableRow({
840
923
  cellRowSpan
841
924
  );
842
925
  const isBottomRightCell = !isEditing && activeSelectionBounds && !dragState.isSelecting && activeSelectionBounds.endRow >= rowIndex && activeSelectionBounds.endRow <= rowIndex + cellRowSpan - 1 && cellIndex === activeSelectionBounds.endCol;
926
+ const spanRowHeights = enableCellSelection && activeSelectionBounds && isCellDragSelected && cellRowSpan > 1 ? measureMergedSpanRowHeights(rowIndex, cellRowSpan) : void 0;
843
927
  const selectionEdgeStyle = getCellSelectionEdgeStyle(
844
928
  rowIndex,
845
929
  cellIndex,
846
930
  activeSelectionBounds,
847
931
  cellRowSpan,
848
- isVisuallySelectedAt
932
+ isVisuallySelectedAt,
933
+ spanRowHeights
849
934
  );
850
935
  const resolveCellRowIndex = (clientY, element) => getRowIndexInMergedCell(clientY, element, rowIndex, cellRowSpan);
851
936
  const hasSelectionEdges = hasCellSelectionEdges(selectionEdgeStyle);
@@ -855,6 +940,7 @@ function DataTableRow({
855
940
  rowSpan: rowSpanInfo && rowSpanInfo.rowSpan > 1 ? rowSpanInfo.rowSpan : void 0,
856
941
  "data-merged": isMerged && cellIndex > 0 ? "" : void 0,
857
942
  "data-merged-edge-right": showMergedRightEdge ? "" : void 0,
943
+ "data-merged-row-first": isMerged && cellIndex === 0 && showMergedRightEdge ? "" : void 0,
858
944
  "data-group-selected": enableRowSpan && showCellSelected ? "" : void 0,
859
945
  "data-group-hovered": enableRowSpan && showCellHover && !showCellSelected ? "" : void 0,
860
946
  "data-selection-fill": isCellDragSelected ? "" : void 0,
@@ -899,7 +985,8 @@ function DataTableRow({
899
985
  "data-table-cell",
900
986
  CELL_ALIGN_CLASS[align],
901
987
  cellClassName,
902
- isMerged && cellIndex > 0 && "is-merged",
988
+ isMerged && "is-merged",
989
+ isMerged && cellIndex === 0 && showMergedRightEdge && "is-merged-row-first",
903
990
  showMergedRightEdge && "is-merged-edge-right",
904
991
  enableRowSpan && showCellSelected && "is-group-selected",
905
992
  enableRowSpan && showCellHover && !showCellSelected && "is-group-hovered",
@@ -1073,7 +1160,7 @@ import {
1073
1160
  useCallback as useCallback3,
1074
1161
  useEffect as useEffect5,
1075
1162
  useMemo as useMemo2,
1076
- useRef as useRef4,
1163
+ useRef as useRef5,
1077
1164
  useState as useState3
1078
1165
  } from "react";
1079
1166
 
@@ -1157,7 +1244,109 @@ function useCellEdit({
1157
1244
  }
1158
1245
 
1159
1246
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1160
- import { useCallback as useCallback2, useEffect as useEffect4, useState as useState2 } from "react";
1247
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
1248
+
1249
+ // src/components/ui/table/features/cell-selection/copyData.ts
1250
+ function formatCellValue(value) {
1251
+ if (value === null || value === void 0) return "";
1252
+ return String(value);
1253
+ }
1254
+ function getNestedValue(row, path) {
1255
+ if (!path.includes(".")) return row[path];
1256
+ return path.split(".").reduce((current, key) => {
1257
+ if (current === null || current === void 0 || typeof current !== "object") {
1258
+ return void 0;
1259
+ }
1260
+ return current[key];
1261
+ }, row);
1262
+ }
1263
+ function readRowColumnValue(rowData, columnDef) {
1264
+ if ("accessorFn" in columnDef && typeof columnDef.accessorFn === "function") {
1265
+ return columnDef.accessorFn(rowData, 0);
1266
+ }
1267
+ if ("accessorKey" in columnDef && columnDef.accessorKey != null && columnDef.accessorKey !== "") {
1268
+ return getNestedValue(rowData, String(columnDef.accessorKey));
1269
+ }
1270
+ return void 0;
1271
+ }
1272
+ function hasSubtree(row) {
1273
+ const children = row.children;
1274
+ return Array.isArray(children) && children.length > 0;
1275
+ }
1276
+ function getOriginalRowId(original) {
1277
+ return String(original.id ?? original.uniqueId ?? "");
1278
+ }
1279
+ function getRowDepth(original) {
1280
+ return typeof original.level === "number" ? original.level : 0;
1281
+ }
1282
+ function collectCopyRowEntries(visibleRows, bounds, mode = "visible") {
1283
+ const { startRow, endRow } = bounds;
1284
+ const result = [];
1285
+ const includedOriginalIds = /* @__PURE__ */ new Set();
1286
+ const appendSubtree = (node, depth) => {
1287
+ const children = node.children;
1288
+ if (!Array.isArray(children) || children.length === 0) return;
1289
+ for (const child of children) {
1290
+ const childId = getOriginalRowId(child);
1291
+ if (!(childId && includedOriginalIds.has(childId))) {
1292
+ result.push({ row: child, depth });
1293
+ if (childId) includedOriginalIds.add(childId);
1294
+ }
1295
+ appendSubtree(child, depth + 1);
1296
+ }
1297
+ };
1298
+ for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) {
1299
+ const row = visibleRows[rowIndex];
1300
+ if (!row) continue;
1301
+ const originalId = getOriginalRowId(row.original);
1302
+ if (originalId && includedOriginalIds.has(originalId)) continue;
1303
+ const depth = getRowDepth(row.original);
1304
+ result.push({ row: row.original, depth });
1305
+ if (originalId) includedOriginalIds.add(originalId);
1306
+ if (mode !== "subtree" || !hasSubtree(row.original)) continue;
1307
+ appendSubtree(row.original, depth + 1);
1308
+ }
1309
+ return result;
1310
+ }
1311
+ function serializeCopyRowsToTSV(copyRows, visibleRows, bounds, depths) {
1312
+ if (copyRows.length === 0) return "";
1313
+ const { startCol, endCol } = bounds;
1314
+ const columnCells = visibleRows[0]?.getVisibleCells().slice(startCol, endCol + 1) ?? [];
1315
+ if (columnCells.length === 0) return "";
1316
+ const resolvedDepths = depths && depths.length === copyRows.length ? depths : copyRows.map((row) => getRowDepth(row));
1317
+ const minDepth = Math.min(...resolvedDepths);
1318
+ return copyRows.map((rowData, index) => {
1319
+ const relativeDepth = Math.max(0, (resolvedDepths[index] ?? 0) - minDepth);
1320
+ const line = columnCells.map(
1321
+ (cell) => formatCellValue(
1322
+ readRowColumnValue(
1323
+ rowData,
1324
+ cell.column.columnDef
1325
+ )
1326
+ )
1327
+ ).join(" ");
1328
+ return `${" ".repeat(relativeDepth)}${line}`;
1329
+ }).join("\n");
1330
+ }
1331
+ function serializeSelectionToTSV(visibleRows, bounds, mode = "visible") {
1332
+ const entries = collectCopyRowEntries(visibleRows, bounds, mode);
1333
+ return serializeCopyRowsToTSV(
1334
+ entries.map((entry) => entry.row),
1335
+ visibleRows,
1336
+ bounds,
1337
+ entries.map((entry) => entry.depth)
1338
+ );
1339
+ }
1340
+ async function writeSelectionToClipboard(visibleRows, bounds, mode = "visible") {
1341
+ const text = serializeSelectionToTSV(visibleRows, bounds, mode);
1342
+ if (!text) return false;
1343
+ try {
1344
+ await navigator.clipboard.writeText(text);
1345
+ } catch {
1346
+ return false;
1347
+ }
1348
+ return true;
1349
+ }
1161
1350
 
1162
1351
  // src/components/ui/table/features/cell-selection/fillData.ts
1163
1352
  function getColumnAccessorKey2(columnDef) {
@@ -1215,15 +1404,100 @@ function hasFillExtension(sourceBounds, fillBounds) {
1215
1404
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
1216
1405
  }
1217
1406
 
1407
+ // src/components/ui/table/features/cell-selection/pasteData.ts
1408
+ function countLeadingEmptyCells(cells) {
1409
+ let depth = 0;
1410
+ while (depth < cells.length && cells[depth] === "") {
1411
+ depth += 1;
1412
+ }
1413
+ return depth;
1414
+ }
1415
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
1416
+ if (leadingEmptyCounts.length === 0) return false;
1417
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
1418
+ if (firstDepth !== 0) return false;
1419
+ return leadingEmptyCounts.some((depth) => depth > 0);
1420
+ }
1421
+ function parseClipboardTSVWithDepths(text) {
1422
+ if (!text) return { values: [], depths: [] };
1423
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1424
+ const withoutTrailing = normalized.replace(/\n+$/, "");
1425
+ if (!withoutTrailing) return { values: [], depths: [] };
1426
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
1427
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
1428
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
1429
+ const values = [];
1430
+ const depths = [];
1431
+ for (let index = 0; index < rows.length; index += 1) {
1432
+ const cells = rows[index] ?? [];
1433
+ const depth = leadingEmptyCounts[index] ?? 0;
1434
+ if (treatAsDepth) {
1435
+ values.push(cells.slice(depth));
1436
+ depths.push(depth);
1437
+ } else {
1438
+ values.push(cells);
1439
+ depths.push(0);
1440
+ }
1441
+ }
1442
+ return { values, depths };
1443
+ }
1444
+ function resolvePasteColumnIds(rows, startCol, width) {
1445
+ if (width <= 0) return [];
1446
+ const cells = rows[0]?.getVisibleCells() ?? [];
1447
+ const columnIds = [];
1448
+ for (let offset = 0; offset < width; offset += 1) {
1449
+ const cell = cells[startCol + offset];
1450
+ if (!cell) break;
1451
+ columnIds.push(cell.column.id);
1452
+ }
1453
+ return columnIds;
1454
+ }
1455
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
1456
+ const { values, depths } = parseClipboardTSVWithDepths(text);
1457
+ if (values.length === 0) return null;
1458
+ const width = Math.max(...values.map((row) => row.length), 0);
1459
+ if (width === 0) return null;
1460
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
1461
+ if (columnIds.length === 0) return null;
1462
+ const rowIds = [];
1463
+ for (let offset = 0; offset < values.length; offset += 1) {
1464
+ const row = rows[startRow + offset];
1465
+ if (!row) break;
1466
+ rowIds.push(row.id);
1467
+ }
1468
+ const anchorRow = rows[endRow] ?? rows[startRow];
1469
+ return {
1470
+ mode,
1471
+ startRow,
1472
+ startCol,
1473
+ endRow,
1474
+ rowIds,
1475
+ anchorRowId: anchorRow?.id ?? "",
1476
+ columnIds,
1477
+ values,
1478
+ depths
1479
+ };
1480
+ }
1481
+ function isEditablePasteTarget(target) {
1482
+ if (!(target instanceof HTMLElement)) return false;
1483
+ const tag = target.tagName;
1484
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
1485
+ return Boolean(target.isContentEditable);
1486
+ }
1487
+
1218
1488
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1219
1489
  function useCellSelection({
1220
1490
  data,
1221
1491
  rows,
1222
1492
  enabled = true,
1493
+ enableSubtreeCopy = false,
1494
+ enableInsertPaste = true,
1223
1495
  onDataChange,
1224
- onBatchChange
1496
+ onBatchChange,
1497
+ onRowsPaste
1225
1498
  }) {
1226
1499
  const [dragState, setDragState] = useState2(INITIAL_DRAG_STATE);
1500
+ const pendingPasteModeRef = useRef4(null);
1227
1501
  const cellSelectionBounds = getCellSelectionBounds(dragState.start, dragState.end);
1228
1502
  const activeSelectionBounds = enabled ? getActiveSelectionBounds(dragState, cellSelectionBounds) : null;
1229
1503
  const handleCellMouseDown = useCallback2(
@@ -1277,21 +1551,113 @@ function useCellSelection({
1277
1551
  setDragState(INITIAL_DRAG_STATE);
1278
1552
  }
1279
1553
  }, [enabled]);
1554
+ const copySelection = useCallback2(
1555
+ async (options) => {
1556
+ if (!enabled || !activeSelectionBounds) return false;
1557
+ const mode = options?.includeDescendants && enableSubtreeCopy ? "subtree" : "visible";
1558
+ return writeSelectionToClipboard(rows, activeSelectionBounds, mode);
1559
+ },
1560
+ [activeSelectionBounds, enableSubtreeCopy, enabled, rows]
1561
+ );
1280
1562
  useEffect4(() => {
1281
1563
  if (!enabled) return;
1282
1564
  const handleKeyDown = (e) => {
1283
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && activeSelectionBounds) {
1284
- const { startRow, endRow, startCol, endCol } = activeSelectionBounds;
1285
- const selectedData = rows.slice(startRow, endRow + 1).map((row) => {
1286
- const cells = row.getVisibleCells();
1287
- return cells.slice(startCol, endCol + 1).map((cell) => cell.getValue()).join(" ");
1288
- }).join("\n");
1289
- navigator.clipboard.writeText(selectedData);
1290
- }
1565
+ if (!activeSelectionBounds) return;
1566
+ if (!(e.ctrlKey || e.metaKey)) return;
1567
+ const isSubtreeShortcut = enableSubtreeCopy && e.shiftKey && e.key.toLowerCase() === "c";
1568
+ const isVisibleCopyShortcut = !e.shiftKey && e.key.toLowerCase() === "c";
1569
+ if (!isSubtreeShortcut && !isVisibleCopyShortcut) return;
1570
+ e.preventDefault();
1571
+ void copySelection({ includeDescendants: isSubtreeShortcut });
1291
1572
  };
1292
1573
  window.addEventListener("keydown", handleKeyDown);
1293
1574
  return () => window.removeEventListener("keydown", handleKeyDown);
1294
- }, [activeSelectionBounds, enabled, rows]);
1575
+ }, [activeSelectionBounds, copySelection, enableSubtreeCopy, enabled]);
1576
+ const emitRowsPaste = useCallback2(
1577
+ (text, mode) => {
1578
+ if (!onRowsPaste || !activeSelectionBounds) return false;
1579
+ const payload = buildRowsPastePayload(
1580
+ rows,
1581
+ activeSelectionBounds.startRow,
1582
+ activeSelectionBounds.startCol,
1583
+ text,
1584
+ mode,
1585
+ activeSelectionBounds.endRow
1586
+ );
1587
+ if (!payload) return false;
1588
+ onRowsPaste(payload);
1589
+ return true;
1590
+ },
1591
+ [activeSelectionBounds, onRowsPaste, rows]
1592
+ );
1593
+ useEffect4(() => {
1594
+ if (!enabled || !onRowsPaste) return;
1595
+ const pasteHandledRef = { current: false };
1596
+ const ignoreNextPasteRef = { current: false };
1597
+ const handleKeyDown = (e) => {
1598
+ if (!activeSelectionBounds) return;
1599
+ if (!(e.ctrlKey || e.metaKey)) return;
1600
+ if (e.key.toLowerCase() !== "v") return;
1601
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
1602
+ return;
1603
+ }
1604
+ if (e.shiftKey && !enableInsertPaste) {
1605
+ ignoreNextPasteRef.current = true;
1606
+ pendingPasteModeRef.current = null;
1607
+ return;
1608
+ }
1609
+ const mode = e.shiftKey ? "insert" : "overwrite";
1610
+ pasteHandledRef.current = false;
1611
+ ignoreNextPasteRef.current = false;
1612
+ pendingPasteModeRef.current = mode;
1613
+ void (async () => {
1614
+ try {
1615
+ const text = await navigator.clipboard.readText();
1616
+ if (pasteHandledRef.current) return;
1617
+ if (pendingPasteModeRef.current !== mode) return;
1618
+ if (!text) return;
1619
+ pasteHandledRef.current = true;
1620
+ emitRowsPaste(text, mode);
1621
+ pendingPasteModeRef.current = null;
1622
+ } catch {
1623
+ }
1624
+ })();
1625
+ };
1626
+ const handlePaste = (e) => {
1627
+ if (!activeSelectionBounds) return;
1628
+ if (isEditablePasteTarget(e.target) || isEditablePasteTarget(document.activeElement)) {
1629
+ return;
1630
+ }
1631
+ if (ignoreNextPasteRef.current) {
1632
+ ignoreNextPasteRef.current = false;
1633
+ pendingPasteModeRef.current = null;
1634
+ return;
1635
+ }
1636
+ const mode = pendingPasteModeRef.current ?? "overwrite";
1637
+ if (pasteHandledRef.current) {
1638
+ e.preventDefault();
1639
+ return;
1640
+ }
1641
+ const text = e.clipboardData?.getData("text/plain");
1642
+ if (text == null || text === "") return;
1643
+ pasteHandledRef.current = true;
1644
+ e.preventDefault();
1645
+ emitRowsPaste(text, mode);
1646
+ pendingPasteModeRef.current = null;
1647
+ };
1648
+ window.addEventListener("keydown", handleKeyDown);
1649
+ window.addEventListener("paste", handlePaste);
1650
+ return () => {
1651
+ window.removeEventListener("keydown", handleKeyDown);
1652
+ window.removeEventListener("paste", handlePaste);
1653
+ };
1654
+ }, [
1655
+ activeSelectionBounds,
1656
+ emitRowsPaste,
1657
+ enableInsertPaste,
1658
+ enabled,
1659
+ onRowsPaste
1660
+ ]);
1295
1661
  useEffect4(() => {
1296
1662
  if (!enabled) return;
1297
1663
  const handleMouseUp = () => {
@@ -1337,7 +1703,8 @@ function useCellSelection({
1337
1703
  activeSelectionBounds,
1338
1704
  handleCellMouseDown,
1339
1705
  handleCellMouseEnter,
1340
- handleFillHandleMouseDown
1706
+ handleFillHandleMouseDown,
1707
+ copySelection
1341
1708
  };
1342
1709
  }
1343
1710
 
@@ -1401,6 +1768,10 @@ function useGlideTable(options) {
1401
1768
  expandedRows: controlledExpandedRows,
1402
1769
  onExpandedRowsChange,
1403
1770
  preventExpand = false,
1771
+ enableSubtreeCopy,
1772
+ onCopyActionsReady,
1773
+ onRowsPaste,
1774
+ enableInsertPaste,
1404
1775
  enableVirtualization = true,
1405
1776
  estimateRowHeight = DATA_TABLE_ROW_HEIGHT,
1406
1777
  virtualOverscan = DATA_TABLE_VIRTUAL_OVERSCAN
@@ -1415,13 +1786,13 @@ function useGlideTable(options) {
1415
1786
  };
1416
1787
  }, [labelsProp, emptyText, loadingText, selectionLabel]);
1417
1788
  const enableExpand = Boolean(toggleField);
1789
+ const resolvedEnableSubtreeCopy = enableSubtreeCopy ?? enableExpand;
1418
1790
  const [internalRowSelection, setInternalRowSelection] = useState3({});
1419
1791
  const [internalExpandedRows, setInternalExpandedRows] = useState3(
1420
1792
  () => /* @__PURE__ */ new Set()
1421
1793
  );
1422
1794
  const [hoveredRowIndex, setHoveredRowIndex] = useState3(null);
1423
- const [hoveredGroupKey, setHoveredGroupKey] = useState3(null);
1424
- const scrollRef = useRef4(null);
1795
+ const scrollRef = useRef5(null);
1425
1796
  const shouldVirtualize = enableVirtualization && !enableRowSpan;
1426
1797
  useEffect5(() => {
1427
1798
  if (enableVirtualization && enableRowSpan) {
@@ -1484,6 +1855,7 @@ function useGlideTable(options) {
1484
1855
  return collectRowSpanColumns(columns);
1485
1856
  }, [enableRowSpan, columns]);
1486
1857
  const primaryRowSpanKey = rowSpanColumnKeys[0]?.rowSpanKey;
1858
+ const primaryRowSpanColumnId = rowSpanColumnKeys[0]?.columnId;
1487
1859
  const columnRowSpanMap = useMemo2(
1488
1860
  () => buildColumnRowSpanMap(tableData, rowSpanColumnKeys),
1489
1861
  [tableData, rowSpanColumnKeys]
@@ -1502,27 +1874,29 @@ function useGlideTable(options) {
1502
1874
  const totalSize = rowVirtualizer.getTotalSize();
1503
1875
  const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start ?? 0 : 0;
1504
1876
  const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0) : 0;
1505
- const selectedGroupKeys = useMemo2(() => {
1506
- if (!enableRowSpan || !primaryRowSpanKey) return /* @__PURE__ */ new Set();
1507
- const keys = /* @__PURE__ */ new Set();
1877
+ const selectedRowIndices = useMemo2(() => {
1878
+ const indices = /* @__PURE__ */ new Set();
1508
1879
  for (const selectedRow of selectedRows) {
1509
- const value = selectedRow.original[primaryRowSpanKey];
1510
- if (value !== null && value !== void 0) keys.add(String(value));
1880
+ indices.add(selectedRow.index);
1511
1881
  }
1512
- return keys;
1513
- }, [enableRowSpan, primaryRowSpanKey, selectedRows]);
1882
+ return indices;
1883
+ }, [selectedRows]);
1514
1884
  const {
1515
1885
  dragState,
1516
1886
  activeSelectionBounds,
1517
1887
  handleCellMouseDown,
1518
1888
  handleCellMouseEnter,
1519
- handleFillHandleMouseDown
1889
+ handleFillHandleMouseDown,
1890
+ copySelection
1520
1891
  } = useCellSelection({
1521
1892
  data: tableData,
1522
1893
  rows,
1523
1894
  enabled: enableCellSelection,
1895
+ enableSubtreeCopy: resolvedEnableSubtreeCopy,
1896
+ enableInsertPaste: enableInsertPaste ?? true,
1524
1897
  onDataChange,
1525
- onBatchChange
1898
+ onBatchChange,
1899
+ onRowsPaste
1526
1900
  });
1527
1901
  const {
1528
1902
  editingCell,
@@ -1544,22 +1918,10 @@ function useGlideTable(options) {
1544
1918
  );
1545
1919
  const clearHover = useCallback3(() => {
1546
1920
  setHoveredRowIndex(null);
1547
- setHoveredGroupKey(null);
1548
1921
  }, []);
1549
- const handleRowHover = useCallback3(
1550
- (rowIndex, rowData) => {
1551
- setHoveredRowIndex(rowIndex);
1552
- if (!primaryRowSpanKey) {
1553
- setHoveredGroupKey(null);
1554
- return;
1555
- }
1556
- const groupValue = rowData[primaryRowSpanKey];
1557
- setHoveredGroupKey(
1558
- groupValue === null || groupValue === void 0 ? null : String(groupValue)
1559
- );
1560
- },
1561
- [primaryRowSpanKey]
1562
- );
1922
+ const handleRowHover = useCallback3((rowIndex, _rowData) => {
1923
+ setHoveredRowIndex(rowIndex);
1924
+ }, []);
1563
1925
  const handleToggleSelect = useCallback3(
1564
1926
  (row) => {
1565
1927
  if (!row.getCanSelect()) return;
@@ -1582,10 +1944,10 @@ function useGlideTable(options) {
1582
1944
  rowSpan: {
1583
1945
  enableRowSpan,
1584
1946
  primaryRowSpanKey,
1947
+ primaryRowSpanColumnId,
1585
1948
  columnRowSpanMap,
1586
1949
  hoveredRowIndex,
1587
- hoveredGroupKey,
1588
- selectedGroupKeys,
1950
+ selectedRowIndices,
1589
1951
  onRowHover: handleRowHover
1590
1952
  },
1591
1953
  selection: {
@@ -1623,10 +1985,10 @@ function useGlideTable(options) {
1623
1985
  }, [
1624
1986
  enableRowSpan,
1625
1987
  primaryRowSpanKey,
1988
+ primaryRowSpanColumnId,
1626
1989
  columnRowSpanMap,
1627
1990
  hoveredRowIndex,
1628
- hoveredGroupKey,
1629
- selectedGroupKeys,
1991
+ selectedRowIndices,
1630
1992
  handleRowHover,
1631
1993
  rowSelectionMode,
1632
1994
  selectOnRowClick,
@@ -1652,6 +2014,14 @@ function useGlideTable(options) {
1652
2014
  labels.expandRow,
1653
2015
  labels.collapseRow
1654
2016
  ]);
2017
+ const copySelectionRef = useRef5(copySelection);
2018
+ useEffect5(() => {
2019
+ copySelectionRef.current = copySelection;
2020
+ }, [copySelection]);
2021
+ const stableCopySelection = useCallback3((options2) => copySelectionRef.current(options2), []);
2022
+ useEffect5(() => {
2023
+ onCopyActionsReady?.({ copySelection: stableCopySelection });
2024
+ }, [onCopyActionsReady, stableCopySelection]);
1655
2025
  return {
1656
2026
  table,
1657
2027
  tableData,
@@ -1671,7 +2041,8 @@ function useGlideTable(options) {
1671
2041
  paddingBottom,
1672
2042
  rowContextValue,
1673
2043
  handleToggleSelect,
1674
- clearHover
2044
+ clearHover,
2045
+ copySelection: stableCopySelection
1675
2046
  };
1676
2047
  }
1677
2048
 
@@ -1970,7 +2341,7 @@ function buildColumnDef(props, sort, onSort) {
1970
2341
  }
1971
2342
 
1972
2343
  // src/components/ui/table/components/Table/parseTableChildren.ts
1973
- import { Children } from "react";
2344
+ import { Children, isValidElement as isValidElement2 } from "react";
1974
2345
 
1975
2346
  // src/components/ui/table/components/Table/tableChildTypes.ts
1976
2347
  import { isValidElement } from "react";
@@ -2019,10 +2390,26 @@ function parseTableChildren(children) {
2019
2390
  }
2020
2391
  return slots;
2021
2392
  }
2393
+ function flattenColumnElements(children) {
2394
+ const result = [];
2395
+ for (const child of Children.toArray(children)) {
2396
+ if (isTableColumnElement(child)) {
2397
+ result.push(child);
2398
+ continue;
2399
+ }
2400
+ if (isValidElement2(child)) {
2401
+ const nested = child.props.children;
2402
+ if (nested != null) {
2403
+ result.push(...flattenColumnElements(nested));
2404
+ }
2405
+ }
2406
+ }
2407
+ return result;
2408
+ }
2022
2409
  function extractColumnElements(header) {
2023
2410
  if (!header) return [];
2024
2411
  const { children } = header.props;
2025
- return Children.toArray(children).filter(isTableColumnElement);
2412
+ return flattenColumnElements(children);
2026
2413
  }
2027
2414
 
2028
2415
  // src/components/ui/table/components/Table/TableBody.tsx