@canvas-components/pivot-table 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -974,7 +974,7 @@ function computePivotScrollbarLayout(params) {
974
974
  }
975
975
  if (hasVertical) {
976
976
  const fullHeight = Math.max(
977
- params.bodyHeight - VERTICAL_TOP_INSET - (hasHorizontal ? SCROLLBAR_THICKNESS : 0),
977
+ params.bodyHeight - VERTICAL_TOP_INSET,
978
978
  0
979
979
  );
980
980
  const buttonSize = Math.min(SCROLLBAR_THICKNESS, fullHeight / 3);
@@ -1030,6 +1030,7 @@ var DEFAULT_ROW_HEIGHT = 40;
1030
1030
  var DEFAULT_COLUMN_WIDTH = 120;
1031
1031
  var DEFAULT_HEADER_SIZE = 40;
1032
1032
  var DEFAULT_ROW_DIMENSION_WIDTH2 = 140;
1033
+ var SCROLLBAR_THICKNESS2 = 10;
1033
1034
  function buildPivotLayoutSnapshot(params) {
1034
1035
  const { model, options, visibleRowNodes, visibleColumnNodes, width, height } = params;
1035
1036
  const indicatorLayout = options.indicatorLayout ?? "columns";
@@ -1062,8 +1063,8 @@ function buildPivotLayoutSnapshot(params) {
1062
1063
  const columnHeaderHeight = columnHeaderLevels * DEFAULT_HEADER_SIZE;
1063
1064
  const reportFilterHeight = (options.filterDimensions?.length ?? 0) * rowHeight;
1064
1065
  const tableHeaderHeight = reportFilterHeight + columnHeaderHeight;
1065
- const bodyWidth = Math.max(width - rowHeaderWidth, 0);
1066
- const availableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1066
+ const rawBodyWidth = Math.max(width - rowHeaderWidth, 0);
1067
+ const rawAvailableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1067
1068
  const rowSlots = createPivotAxisSlots({
1068
1069
  nodes: visibleRowNodes,
1069
1070
  indicators: options.indicators.map((indicator) => indicator.key),
@@ -1086,19 +1087,52 @@ function buildPivotLayoutSnapshot(params) {
1086
1087
  const columnOffsets = createOffsets(columnWidths);
1087
1088
  const totalBodyWidth = columnOffsets[columnOffsets.length - 1] ?? 0;
1088
1089
  const totalBodyHeight = rowSlots.length * rowHeight;
1090
+ let hasHorizontalScrollbar = totalBodyWidth > rawBodyWidth;
1091
+ let hasVerticalScrollbar = totalBodyHeight > rawAvailableBodyHeight;
1092
+ for (let index = 0; index < 2; index += 1) {
1093
+ hasHorizontalScrollbar = totalBodyWidth > Math.max(
1094
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1095
+ 0
1096
+ );
1097
+ hasVerticalScrollbar = totalBodyHeight > Math.max(
1098
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1099
+ 0
1100
+ );
1101
+ }
1102
+ const bodyWidth = Math.max(
1103
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1104
+ 0
1105
+ );
1106
+ const availableBodyHeight = Math.max(
1107
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1108
+ 0
1109
+ );
1110
+ const frozenBottomRowRange = resolveFrozenBottomRowRange(rowSlots);
1111
+ const frozenBottomRowCount = frozenBottomRowRange ? frozenBottomRowRange.end - frozenBottomRowRange.start : 0;
1112
+ const frozenBottomHeight = frozenBottomRowCount * rowHeight;
1113
+ const scrollableRowCount = frozenBottomRowRange?.start ?? rowSlots.length;
1114
+ const totalScrollableBodyHeight = scrollableRowCount * rowHeight;
1089
1115
  const tableWidth = rowHeaderWidth + Math.min(bodyWidth, totalBodyWidth);
1090
1116
  const bodyHeight = Math.min(availableBodyHeight, totalBodyHeight);
1091
- const fittedHeight = tableHeaderHeight + bodyHeight;
1117
+ const visibleFrozenBottomHeight = Math.min(frozenBottomHeight, bodyHeight);
1118
+ const scrollableBodyHeight = Math.max(
1119
+ bodyHeight - visibleFrozenBottomHeight,
1120
+ 0
1121
+ );
1122
+ const fittedHeight = tableHeaderHeight + bodyHeight + (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0);
1092
1123
  const maxScrollLeft = Math.max(totalBodyWidth - bodyWidth, 0);
1093
- const maxScrollTop = Math.max(totalBodyHeight - bodyHeight, 0);
1124
+ const maxScrollTop = Math.max(
1125
+ totalScrollableBodyHeight - scrollableBodyHeight,
1126
+ 0
1127
+ );
1094
1128
  const scroll = {
1095
1129
  left: clamp(params.scroll.left, 0, maxScrollLeft),
1096
1130
  top: clamp(params.scroll.top, 0, maxScrollTop)
1097
1131
  };
1098
1132
  const visibleRowRange = resolveVisibleRange({
1099
- count: rowSlots.length,
1133
+ count: scrollableRowCount,
1100
1134
  itemSize: rowHeight,
1101
- viewportSize: bodyHeight,
1135
+ viewportSize: scrollableBodyHeight,
1102
1136
  offset: scroll.top,
1103
1137
  overscan: options.overscanRowCount ?? 1
1104
1138
  });
@@ -1133,6 +1167,23 @@ function buildPivotLayoutSnapshot(params) {
1133
1167
  dimensionCount: options.rows.length,
1134
1168
  indicators: options.indicators
1135
1169
  });
1170
+ if (frozenBottomRowRange) {
1171
+ rowHeaderCells.push(
1172
+ ...buildRowHeaderCells({
1173
+ model,
1174
+ options,
1175
+ slots: rowSlots,
1176
+ range: frozenBottomRowRange,
1177
+ rowHeight,
1178
+ rowDimensionWidths,
1179
+ columnHeaderHeight: tableHeaderHeight,
1180
+ scrollTop: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight,
1181
+ expandedNodeIds: params.expandedRowNodeIds,
1182
+ dimensionCount: options.rows.length,
1183
+ indicators: options.indicators
1184
+ })
1185
+ );
1186
+ }
1136
1187
  const columnHeaderCells = buildColumnHeaderCells({
1137
1188
  model,
1138
1189
  options,
@@ -1159,14 +1210,34 @@ function buildPivotLayoutSnapshot(params) {
1159
1210
  columnHeaderHeight: tableHeaderHeight,
1160
1211
  scroll
1161
1212
  });
1213
+ if (frozenBottomRowRange) {
1214
+ bodyCells.push(
1215
+ ...buildBodyCells({
1216
+ model,
1217
+ rowSlots,
1218
+ columnSlots,
1219
+ rowRange: frozenBottomRowRange,
1220
+ columnRange: visibleColumnRange,
1221
+ rowHeight,
1222
+ columnWidths,
1223
+ columnOffsets,
1224
+ rowHeaderWidth,
1225
+ columnHeaderHeight: tableHeaderHeight,
1226
+ scroll: {
1227
+ left: scroll.left,
1228
+ top: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight
1229
+ }
1230
+ })
1231
+ );
1232
+ }
1162
1233
  const scrollbar = computePivotScrollbarLayout({
1163
- width,
1234
+ width: tableWidth + (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1164
1235
  height: fittedHeight,
1165
1236
  bodyTop: tableHeaderHeight,
1166
- bodyHeight,
1237
+ bodyHeight: scrollableBodyHeight,
1167
1238
  bodyWidth,
1168
1239
  totalBodyWidth,
1169
- totalBodyHeight,
1240
+ totalBodyHeight: totalScrollableBodyHeight,
1170
1241
  scrollLeft: scroll.left,
1171
1242
  scrollTop: scroll.top,
1172
1243
  maxScrollLeft,
@@ -1218,6 +1289,7 @@ function buildPivotLayoutSnapshot(params) {
1218
1289
  rowSlots,
1219
1290
  columnSlots,
1220
1291
  visibleRowRange,
1292
+ frozenBottomRowRange,
1221
1293
  visibleColumnRange,
1222
1294
  reportFilterCells,
1223
1295
  rowFieldHeaderCells,
@@ -1352,6 +1424,13 @@ function createAxisSlot(node, indicatorKey) {
1352
1424
  kind: node?.kind ?? "empty"
1353
1425
  };
1354
1426
  }
1427
+ function resolveFrozenBottomRowRange(slots) {
1428
+ let start = slots.length;
1429
+ while (start > 0 && slots[start - 1]?.kind === "grandTotal") {
1430
+ start -= 1;
1431
+ }
1432
+ return start < slots.length ? { start, end: slots.length } : null;
1433
+ }
1355
1434
  function buildRowHeaderCells(params) {
1356
1435
  const cells = [];
1357
1436
  for (let rowIndex = params.range.start; rowIndex < params.range.end; rowIndex += 1) {
@@ -2592,22 +2671,6 @@ function createPivotDomHost(container) {
2592
2671
  }
2593
2672
  };
2594
2673
  }
2595
- async function writePivotClipboardText(text, ownerDocument) {
2596
- const syncCopied = copyTextSync(text, ownerDocument);
2597
- logPivotClipboard("sync-copy-result", { copied: syncCopied });
2598
- if (syncCopied) return true;
2599
- const copied = await copyText(text, ownerDocument);
2600
- logPivotClipboard("copy-result", { copied });
2601
- return copied;
2602
- }
2603
- function copyPivotClipboardTextSync(text, ownerDocument) {
2604
- const copied = copyTextSync(text, ownerDocument);
2605
- logPivotClipboard("sync-copy-result", { copied });
2606
- return copied;
2607
- }
2608
- function logPivotClipboard(stage, detail) {
2609
- console.info(`[PivotTable:clipboard] ${stage}`, detail);
2610
- }
2611
2674
 
2612
2675
  // src/adapters/canvas-host.ts
2613
2676
  function resizePivotCanvas(canvas, width, height) {
@@ -2630,8 +2693,8 @@ function resolvePivotSelectionRect(layout, selection) {
2630
2693
  const kind = resolvePivotSelectionKind(selection);
2631
2694
  const startX = kind === "row" || kind === "row-cell" ? layout.rowHeaderRect.x : layout.bodyRect.x + (layout.columnOffsets[indexes.startColumnIndex] ?? 0) - layout.scroll.left;
2632
2695
  const endX = layout.bodyRect.x + (layout.columnOffsets[indexes.endColumnIndex] ?? 0) + (layout.columnWidths[indexes.endColumnIndex] ?? layout.columnWidth) - layout.scroll.left;
2633
- const startY = kind === "column" ? layout.columnHeaderRect.y : layout.bodyRect.y + indexes.startRowIndex * layout.rowHeight - layout.scroll.top;
2634
- const endY = layout.bodyRect.y + (indexes.endRowIndex + 1) * layout.rowHeight - layout.scroll.top;
2696
+ const startY = kind === "column" ? layout.columnHeaderRect.y : resolvePivotRowTop(layout, indexes.startRowIndex);
2697
+ const endY = resolvePivotRowTop(layout, indexes.endRowIndex) + layout.rowHeight;
2635
2698
  const clipRect = resolvePivotSelectionClipRect(layout, selection);
2636
2699
  const visibleStartX = Math.max(startX, clipRect.x);
2637
2700
  const visibleEndX = Math.min(endX, clipRect.x + clipRect.width);
@@ -2645,6 +2708,13 @@ function resolvePivotSelectionRect(layout, selection) {
2645
2708
  height: visibleEndY - visibleStartY
2646
2709
  };
2647
2710
  }
2711
+ function resolvePivotRowTop(layout, rowIndex) {
2712
+ const frozenRange = layout.frozenBottomRowRange;
2713
+ if (frozenRange && rowIndex >= frozenRange.start && rowIndex < frozenRange.end) {
2714
+ return layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - rowIndex) * layout.rowHeight;
2715
+ }
2716
+ return layout.bodyRect.y + rowIndex * layout.rowHeight - layout.scroll.top;
2717
+ }
2648
2718
  function resolvePivotSelectionClipRect(layout, selection) {
2649
2719
  const kind = resolvePivotSelectionKind(selection);
2650
2720
  if (kind === "row" || kind === "row-cell") {
@@ -2789,26 +2859,29 @@ var defaultPivotTableTheme = {
2789
2859
  };
2790
2860
 
2791
2861
  // src/rendering/draw-scrollbars.ts
2792
- function drawPivotScrollbars(context, layout) {
2862
+ function drawPivotScrollbars(context, layout, borderColor) {
2793
2863
  context.save();
2794
2864
  if (layout.horizontal) {
2795
- drawAxis(context, layout.horizontal, "horizontal");
2865
+ drawAxis(context, layout.horizontal, "horizontal", borderColor);
2796
2866
  }
2797
2867
  if (layout.vertical) {
2798
- drawAxis(context, layout.vertical, "vertical");
2868
+ drawAxis(context, layout.vertical, "vertical", borderColor);
2799
2869
  }
2800
2870
  context.restore();
2801
2871
  }
2802
- function drawAxis(context, layout, axis) {
2803
- drawTrack(context, layout.track);
2872
+ function drawAxis(context, layout, axis, borderColor) {
2873
+ drawTrack(context, layout.track, borderColor);
2804
2874
  drawButton(context, layout.decreaseButton, axis, "decrease");
2805
2875
  drawButton(context, layout.increaseButton, axis, "increase");
2806
2876
  drawThumb(context, layout.thumb);
2807
2877
  }
2808
- function drawTrack(context, rect) {
2878
+ function drawTrack(context, rect, borderColor) {
2809
2879
  context.fillStyle = "rgba(148, 163, 184, 0.16)";
2880
+ context.strokeStyle = borderColor;
2881
+ context.lineWidth = 1;
2810
2882
  roundRect(context, rect, rect.height / 2);
2811
2883
  context.fill();
2884
+ context.stroke();
2812
2885
  }
2813
2886
  function drawThumb(context, rect) {
2814
2887
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -2972,7 +3045,7 @@ function renderPivotTable(params) {
2972
3045
  });
2973
3046
  withClip(context, layout.cornerRect, () => {
2974
3047
  fillRect(context, layout.cornerRect, theme.cornerBackgroundColor);
2975
- drawCorner(context, layout, theme);
3048
+ drawCorner(context, layout, theme, params.selection?.textSelection);
2976
3049
  });
2977
3050
  withClip(context, layout.rowHeaderRect, () => {
2978
3051
  layout.rowHeaderCells.forEach((cell) => {
@@ -2986,7 +3059,9 @@ function renderPivotTable(params) {
2986
3059
  cell,
2987
3060
  theme,
2988
3061
  !areaSelection && isHeaderSelected(cell, selectionPathKeys.row),
2989
- contentRect
3062
+ contentRect,
3063
+ void 0,
3064
+ params.selection?.textSelection
2990
3065
  );
2991
3066
  });
2992
3067
  layout.rowHeaderCells.forEach(
@@ -3006,7 +3081,10 @@ function renderPivotTable(params) {
3006
3081
  context,
3007
3082
  cell,
3008
3083
  theme,
3009
- !areaSelection && isHeaderSelected(cell, selectionPathKeys.column)
3084
+ !areaSelection && isHeaderSelected(cell, selectionPathKeys.column),
3085
+ cell.rect,
3086
+ void 0,
3087
+ params.selection?.textSelection
3010
3088
  )
3011
3089
  );
3012
3090
  layout.columnHeaderCells.forEach(
@@ -3041,7 +3119,8 @@ function renderPivotTable(params) {
3041
3119
  activeNodePathKey
3042
3120
  )
3043
3121
  },
3044
- visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0
3122
+ visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0,
3123
+ params.selection?.textSelection
3045
3124
  )
3046
3125
  );
3047
3126
  layout.bodyCells.forEach(
@@ -3070,7 +3149,7 @@ function renderPivotTable(params) {
3070
3149
  theme.borderColor
3071
3150
  );
3072
3151
  if (options.debug) drawDebugOverlay(context, layout, theme);
3073
- drawPivotScrollbars(context, layout.scrollbar);
3152
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor);
3074
3153
  drawPivotViewportRightBorder(
3075
3154
  context,
3076
3155
  { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
@@ -3182,7 +3261,7 @@ function drawReportFilterCell(context, cell, theme) {
3182
3261
  skipRight: true
3183
3262
  });
3184
3263
  }
3185
- function drawCorner(context, layout, theme) {
3264
+ function drawCorner(context, layout, theme, textSelection) {
3186
3265
  layout.rowFieldHeaderCells.forEach(
3187
3266
  (cell) => drawHeaderCell(
3188
3267
  context,
@@ -3190,7 +3269,8 @@ function drawCorner(context, layout, theme) {
3190
3269
  theme,
3191
3270
  false,
3192
3271
  cell.rect,
3193
- theme.cornerBackgroundColor
3272
+ theme.cornerBackgroundColor,
3273
+ textSelection
3194
3274
  )
3195
3275
  );
3196
3276
  if (layout.rowFieldHeaderCells.length === 0) {
@@ -3207,7 +3287,7 @@ function drawCorner(context, layout, theme) {
3207
3287
  })
3208
3288
  );
3209
3289
  }
3210
- function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor) {
3290
+ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor, textSelection) {
3211
3291
  const background = backgroundColor ?? (selected ? theme.selectionBackgroundColor : cell.kind === "grandTotal" ? theme.grandTotalBackgroundColor : cell.kind === "subtotal" ? theme.subtotalBackgroundColor : cell.axis === "row" ? theme.rowHeaderBackgroundColor : theme.columnHeaderBackgroundColor);
3212
3292
  fillRect(context, cell.rect, background);
3213
3293
  const iconPadding = cell.expandable ? 18 : 0;
@@ -3229,6 +3309,13 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3229
3309
  cell.filterable ? 32 : 10
3230
3310
  );
3231
3311
  }
3312
+ drawHeaderTextSelectionBackground(
3313
+ context,
3314
+ cell,
3315
+ contentRect,
3316
+ textSelection,
3317
+ theme.fontSize
3318
+ );
3232
3319
  drawText(
3233
3320
  context,
3234
3321
  cell.label,
@@ -3241,13 +3328,46 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3241
3328
  cell.align ?? "left"
3242
3329
  );
3243
3330
  }
3331
+ function drawHeaderTextSelectionBackground(context, cell, contentRect, selection, fontSize) {
3332
+ if (!selection) return;
3333
+ const index = selection.fragments.findIndex(
3334
+ (fragment) => fragment.region === "header" && fragment.key === `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`
3335
+ );
3336
+ if (index < 0) return;
3337
+ const range = resolveTextSelectionRange(selection, index, cell.label.length);
3338
+ if (!range) return;
3339
+ const width = context.measureText(cell.label).width;
3340
+ const align = cell.align ?? "left";
3341
+ const origin = align === "right" ? contentRect.x + contentRect.width - 10 - width : align === "center" ? contentRect.x + contentRect.width / 2 - width / 2 : contentRect.x + 10 + (cell.expandable ? 18 : 0);
3342
+ context.save();
3343
+ context.fillStyle = "#99c2ff";
3344
+ context.fillRect(
3345
+ origin + context.measureText(cell.label.slice(0, range.start)).width,
3346
+ contentRect.y + (contentRect.height - fontSize) / 2,
3347
+ context.measureText(cell.label.slice(range.start, range.end)).width,
3348
+ fontSize
3349
+ );
3350
+ context.restore();
3351
+ }
3352
+ function resolveTextSelectionRange(selection, index, length) {
3353
+ const forward = selection.anchorFragmentIndex <= selection.focusFragmentIndex;
3354
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3355
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3356
+ if (index < startIndex || index > endIndex) return null;
3357
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3358
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : length;
3359
+ return {
3360
+ start: Math.max(0, Math.min(start, length)),
3361
+ end: Math.max(0, Math.min(end, length))
3362
+ };
3363
+ }
3244
3364
  function isHeaderSelected(cell, selectedPathKey) {
3245
3365
  if (!selectedPathKey || !cell.nodeId) {
3246
3366
  return false;
3247
3367
  }
3248
3368
  return Boolean(isPathRelated2(selectedPathKey, cell.pathKey));
3249
3369
  }
3250
- function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3370
+ function drawBodyCell(context, cell, options, theme, state, visibleMaximum, textSelection) {
3251
3371
  const indicator = options.indicators.find(
3252
3372
  (item) => item.key === cell.indicatorKey
3253
3373
  );
@@ -3277,15 +3397,54 @@ function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3277
3397
  "#3370ff"
3278
3398
  );
3279
3399
  }
3400
+ const text = resolveBodyDisplayText(cell, indicator);
3401
+ const padding = 10;
3402
+ const align = indicator?.align ?? "right";
3403
+ drawTextSelectionBackground(
3404
+ context,
3405
+ cell,
3406
+ text,
3407
+ align,
3408
+ padding,
3409
+ textSelection,
3410
+ theme.fontSize
3411
+ );
3280
3412
  drawText(
3281
3413
  context,
3282
- resolveBodyDisplayText(cell, indicator),
3414
+ text,
3283
3415
  cell.rect,
3284
3416
  theme.textColor,
3285
3417
  10,
3286
3418
  indicator?.align ?? "right"
3287
3419
  );
3288
3420
  }
3421
+ function drawTextSelectionBackground(context, cell, text, align, padding, selection, fontSize) {
3422
+ if (!selection) return;
3423
+ const index = selection.fragments.findIndex(
3424
+ (fragment) => fragment.rowIndex === cell.rowIndex && fragment.columnIndex === cell.columnIndex && fragment.text === text
3425
+ );
3426
+ if (index < 0) return;
3427
+ const forward = selection.anchorFragmentIndex < selection.focusFragmentIndex || selection.anchorFragmentIndex === selection.focusFragmentIndex && selection.anchorOffset <= selection.focusOffset;
3428
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3429
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3430
+ if (index < startIndex || index > endIndex) return;
3431
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3432
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : text.length;
3433
+ if (start >= end) return;
3434
+ const availableWidth = Math.max(cell.rect.width - padding * 2, 0);
3435
+ const shown = truncateText(context, text, availableWidth);
3436
+ const textWidth = context.measureText(shown).width;
3437
+ const origin = align === "right" ? cell.rect.x + cell.rect.width - padding - textWidth : align === "center" ? cell.rect.x + cell.rect.width / 2 - textWidth / 2 : cell.rect.x + padding;
3438
+ context.save();
3439
+ context.fillStyle = "#99c2ff";
3440
+ context.fillRect(
3441
+ origin + context.measureText(shown.slice(0, start)).width,
3442
+ cell.rect.y + (cell.rect.height - fontSize) / 2,
3443
+ context.measureText(shown.slice(start, end)).width,
3444
+ fontSize
3445
+ );
3446
+ context.restore();
3447
+ }
3289
3448
  function resolveSelectionPathKeys(layout, selection, activeNodePathKey) {
3290
3449
  const activeNode = selection?.activeNode;
3291
3450
  if (activeNode) {
@@ -3508,7 +3667,7 @@ function hitTestPivotLayout(layout, x, y) {
3508
3667
  onSortIcon: false
3509
3668
  };
3510
3669
  }
3511
- const bodyCell = layout.bodyCells.find((cell) => contains(cell.rect, x, y));
3670
+ const bodyCell = findLastContaining(layout.bodyCells, x, y);
3512
3671
  if (bodyCell) {
3513
3672
  return {
3514
3673
  region: "body",
@@ -3518,9 +3677,7 @@ function hitTestPivotLayout(layout, x, y) {
3518
3677
  onSortIcon: false
3519
3678
  };
3520
3679
  }
3521
- const rowHeaderCell = layout.rowHeaderCells.find(
3522
- (cell) => contains(cell.rect, x, y)
3523
- );
3680
+ const rowHeaderCell = findLastContaining(layout.rowHeaderCells, x, y);
3524
3681
  if (rowHeaderCell) {
3525
3682
  return {
3526
3683
  region: "rowHeader",
@@ -3573,6 +3730,68 @@ function isSortIconHit(cell, x) {
3573
3730
  function contains(rect, x, y) {
3574
3731
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3575
3732
  }
3733
+ function findLastContaining(cells, x, y) {
3734
+ for (let index = cells.length - 1; index >= 0; index -= 1) {
3735
+ const cell = cells[index];
3736
+ if (cell && contains(cell.rect, x, y)) return cell;
3737
+ }
3738
+ return void 0;
3739
+ }
3740
+
3741
+ // src/interaction/text-selection.ts
3742
+ function resolvePivotTextOffset(params) {
3743
+ const boundaries = [0];
3744
+ let offset = 0;
3745
+ for (const character of params.text) {
3746
+ offset += character.length;
3747
+ boundaries.push(offset);
3748
+ }
3749
+ const relativeX = params.pointerX - params.textOriginX;
3750
+ if (relativeX <= 0 || boundaries.length === 1) return 0;
3751
+ if (relativeX >= params.measureText(params.text)) return params.text.length;
3752
+ let low = 1;
3753
+ let high = boundaries.length - 1;
3754
+ while (low < high) {
3755
+ const middle = Math.floor((low + high) / 2);
3756
+ const boundary = boundaries[middle] ?? params.text.length;
3757
+ if (params.measureText(params.text.slice(0, boundary)) < relativeX) {
3758
+ low = middle + 1;
3759
+ } else {
3760
+ high = middle;
3761
+ }
3762
+ }
3763
+ const end = boundaries[low] ?? params.text.length;
3764
+ const start = boundaries[Math.max(low - 1, 0)] ?? 0;
3765
+ const startWidth = params.measureText(params.text.slice(0, start));
3766
+ const endWidth = params.measureText(params.text.slice(0, end));
3767
+ return relativeX - startWidth <= endWidth - relativeX ? start : end;
3768
+ }
3769
+ function getPivotSelectedText(selection) {
3770
+ const textSelection = selection.textSelection;
3771
+ if (!textSelection) return null;
3772
+ const forward = textSelection.anchorFragmentIndex < textSelection.focusFragmentIndex || textSelection.anchorFragmentIndex === textSelection.focusFragmentIndex && textSelection.anchorOffset <= textSelection.focusOffset;
3773
+ const startIndex = forward ? textSelection.anchorFragmentIndex : textSelection.focusFragmentIndex;
3774
+ const endIndex = forward ? textSelection.focusFragmentIndex : textSelection.anchorFragmentIndex;
3775
+ const startOffset = forward ? textSelection.anchorOffset : textSelection.focusOffset;
3776
+ const endOffset = forward ? textSelection.focusOffset : textSelection.anchorOffset;
3777
+ const selected = textSelection.fragments.slice(startIndex, endIndex + 1).map((fragment, index) => {
3778
+ const absoluteIndex = startIndex + index;
3779
+ const start = absoluteIndex === startIndex ? startOffset : 0;
3780
+ const end = absoluteIndex === endIndex ? endOffset : fragment.text.length;
3781
+ return {
3782
+ fragment,
3783
+ text: fragment.text.slice(
3784
+ Math.max(0, Math.min(start, fragment.text.length)),
3785
+ Math.max(0, Math.min(end, fragment.text.length))
3786
+ )
3787
+ };
3788
+ });
3789
+ return selected.reduce((result, item, index) => {
3790
+ const previous = selected[index - 1]?.fragment;
3791
+ const separator = previous ? previous.rowIndex === item.fragment.rowIndex ? " " : "\n" : "";
3792
+ return `${result}${separator}${item.text}`;
3793
+ }, "") || null;
3794
+ }
3576
3795
 
3577
3796
  // src/features/clipboard/pivot-selection-copy.ts
3578
3797
  function createPivotSelectionText(params) {
@@ -3754,6 +3973,16 @@ function createAddress(rowSlot, columnSlot, indicatorKey) {
3754
3973
  // src/domain/interaction/row-selection.ts
3755
3974
  function resolvePivotRowIndexAtY(layout, y) {
3756
3975
  if (layout.rowSlots.length === 0 || layout.rowHeight <= 0) return null;
3976
+ const frozenRange = layout.frozenBottomRowRange;
3977
+ if (frozenRange) {
3978
+ const frozenTop = layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - frozenRange.start) * layout.rowHeight;
3979
+ if (y >= frozenTop) {
3980
+ return Math.min(
3981
+ frozenRange.start + Math.floor((y - frozenTop) / layout.rowHeight),
3982
+ frozenRange.end - 1
3983
+ );
3984
+ }
3985
+ }
3757
3986
  const index = Math.floor(
3758
3987
  (y - layout.bodyRect.y + layout.scroll.top) / layout.rowHeight
3759
3988
  );
@@ -3877,6 +4106,8 @@ var PivotTable = class {
3877
4106
  __publicField(this, "suppressNextClick", false);
3878
4107
  __publicField(this, "scrollbarDrag", null);
3879
4108
  __publicField(this, "cellSelectionDrag", null);
4109
+ __publicField(this, "textSelectionDrag", null);
4110
+ __publicField(this, "headerTextSelectionDrag", null);
3880
4111
  __publicField(this, "rowHeaderSelectionDrag", null);
3881
4112
  __publicField(this, "handlePointerMove", (event) => {
3882
4113
  if (this.scrollbarDrag) {
@@ -3897,10 +4128,22 @@ var PivotTable = class {
3897
4128
  return;
3898
4129
  }
3899
4130
  if (this.cellSelectionDrag?.pointerId === event.pointerId) {
4131
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4132
+ this.updateTextSelectionDrag(event);
4133
+ if (this.textSelectionDrag.dragged) return;
4134
+ }
3900
4135
  this.updateCellSelectionDrag(event);
3901
4136
  return;
3902
4137
  }
4138
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4139
+ this.updateHeaderTextSelectionDrag(event);
4140
+ return;
4141
+ }
3903
4142
  if (this.rowHeaderSelectionDrag?.pointerId === event.pointerId) {
4143
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4144
+ this.updateHeaderTextSelectionDrag(event);
4145
+ if (this.headerTextSelectionDrag.dragged) return;
4146
+ }
3904
4147
  this.updateRowHeaderSelectionDrag(event);
3905
4148
  return;
3906
4149
  }
@@ -3947,6 +4190,7 @@ var PivotTable = class {
3947
4190
  this.openReportFilterPanel(hit.reportFilterCell);
3948
4191
  return;
3949
4192
  }
4193
+ if (hit.reportFilterCell) return;
3950
4194
  if (hit.bodyCell) {
3951
4195
  this.selectCell(toCellAddress(hit.bodyCell), event.shiftKey);
3952
4196
  return;
@@ -3988,52 +4232,20 @@ var PivotTable = class {
3988
4232
  this.scheduleRender();
3989
4233
  return;
3990
4234
  });
3991
- __publicField(this, "handleDocumentKeyDown", (event) => {
3992
- const root = this.host?.root;
3993
- const activeElement = this.container.ownerDocument.activeElement;
3994
- const belongsToTable = Boolean(
3995
- root && (activeElement === root || activeElement instanceof Element && root.contains(activeElement))
3996
- );
4235
+ __publicField(this, "handleKeyDown", (event) => {
3997
4236
  const isCopyShortcut = (event.ctrlKey || event.metaKey) && !event.altKey && (event.key.toLocaleLowerCase() === "c" || event.code === "KeyC");
3998
4237
  if (isCopyShortcut) {
3999
- logPivotClipboard("keydown-captured", {
4000
- belongsToTable,
4001
- key: event.key,
4002
- code: event.code,
4003
- ctrlKey: event.ctrlKey,
4004
- metaKey: event.metaKey,
4005
- defaultPrevented: event.defaultPrevented,
4006
- activeElement: describeElement(activeElement),
4007
- eventTarget: describeElement(event.target),
4008
- protocol: this.container.ownerDocument.location?.protocol ?? "unknown",
4009
- isSecureContext: this.container.ownerDocument.defaultView?.isSecureContext ?? false
4010
- });
4011
- }
4012
- if (!belongsToTable) return;
4013
- if (isCopyShortcut) {
4014
- const text = this.createSelectionTsv();
4015
- const selection = this.core?.getSelection() ?? null;
4016
- logPivotClipboard("selection-resolved", {
4017
- hasLayout: Boolean(this.layout),
4018
- selection,
4019
- textLength: text.length,
4020
- textPreview: text.slice(0, 120)
4021
- });
4238
+ const text = getPivotSelectedText(
4239
+ this.core?.getSelection() ?? {
4240
+ }
4241
+ ) ?? this.createSelectionTsv();
4022
4242
  if (!text) {
4023
- logPivotClipboard("copy-aborted", { reason: "empty-selection-text" });
4024
4243
  return;
4025
4244
  }
4026
4245
  event.preventDefault();
4027
- const copied = copyPivotClipboardTextSync(
4028
- text,
4029
- this.container.ownerDocument
4030
- );
4031
- if (!copied) {
4032
- void writePivotClipboardText(text, this.container.ownerDocument).then(
4033
- (fallbackCopied) => logPivotClipboard("fallback-complete", {
4034
- copied: fallbackCopied
4035
- })
4036
- );
4246
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
4247
+ if (!copyTextSync(text, ownerDocument)) {
4248
+ void copyText(text, ownerDocument);
4037
4249
  }
4038
4250
  return;
4039
4251
  }
@@ -4046,16 +4258,6 @@ var PivotTable = class {
4046
4258
  event.preventDefault();
4047
4259
  this.selectCell(next, event.shiftKey);
4048
4260
  });
4049
- __publicField(this, "handleCopy", (event) => {
4050
- const text = this.createSelectionTsv();
4051
- logPivotClipboard("copy-event", {
4052
- textLength: text.length,
4053
- hasClipboardData: Boolean(event.clipboardData)
4054
- });
4055
- if (!text) return;
4056
- event.preventDefault();
4057
- event.clipboardData?.setData("text/plain", text);
4058
- });
4059
4261
  __publicField(this, "handleWheel", (event) => {
4060
4262
  if (!this.layout) return;
4061
4263
  const delta = resolvePivotWheelScrollDelta({
@@ -4127,13 +4329,13 @@ var PivotTable = class {
4127
4329
  endColumnIndex: hit.bodyCell.columnIndex
4128
4330
  }) : "";
4129
4331
  if (text) {
4130
- void writePivotClipboardText(text, this.container.ownerDocument);
4332
+ this.copyPivotText(text);
4131
4333
  }
4132
4334
  },
4133
4335
  onCopySelection: selectionRange ? () => {
4134
4336
  const text = this.createSelectionTsv();
4135
4337
  if (text) {
4136
- void writePivotClipboardText(text, this.container.ownerDocument);
4338
+ this.copyPivotText(text);
4137
4339
  }
4138
4340
  } : void 0,
4139
4341
  customSelection: {
@@ -4156,7 +4358,7 @@ var PivotTable = class {
4156
4358
  endColumnIndex: range.endColumn - 1
4157
4359
  });
4158
4360
  if (text) {
4159
- void writePivotClipboardText(text, this.container.ownerDocument);
4361
+ this.copyPivotText(text);
4160
4362
  }
4161
4363
  },
4162
4364
  onExportCsv: () => downloadBlob(
@@ -4188,6 +4390,24 @@ var PivotTable = class {
4188
4390
  const address = toCellAddress(cellHit.bodyCell);
4189
4391
  this.host.root.focus({ preventScroll: true });
4190
4392
  const selection = this.selectCell(address, event.shiftKey);
4393
+ const text = this.resolveBodyText(cellHit.bodyCell);
4394
+ const textOriginX = this.resolveBodyTextOriginX(cellHit.bodyCell, text);
4395
+ const anchorOffset = text ? resolvePivotTextOffset({
4396
+ text,
4397
+ pointerX: event.clientX - this.host.root.getBoundingClientRect().left,
4398
+ textOriginX,
4399
+ measureText: (value) => this.measurePivotText(value)
4400
+ }) : 0;
4401
+ const anchorFragmentIndex = this.resolveTextFragmentIndex(
4402
+ `body:${cellHit.bodyCell.rowIndex}:${cellHit.bodyCell.columnIndex}`,
4403
+ this.createTextSelectionFragments("column")
4404
+ );
4405
+ this.textSelectionDrag = text ? {
4406
+ pointerId: event.pointerId,
4407
+ anchorFragmentIndex,
4408
+ anchorOffset,
4409
+ dragged: false
4410
+ } : null;
4191
4411
  this.cellSelectionDrag = {
4192
4412
  pointerId: event.pointerId,
4193
4413
  anchor: selection?.anchorCell ?? address,
@@ -4199,6 +4419,37 @@ var PivotTable = class {
4199
4419
  this.suppressNextClick = true;
4200
4420
  return;
4201
4421
  }
4422
+ if (cellHit?.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4423
+ const header = cellHit.headerCell;
4424
+ const text = header.label;
4425
+ const rootRect2 = this.host.root.getBoundingClientRect();
4426
+ const originX = header.rect.x + 10 + (header.expandable ? 18 : 0);
4427
+ const fragments = this.createTextSelectionFragments(header.axis);
4428
+ const key = `header:${header.axis}:${header.nodeId ?? ""}:${header.label}`;
4429
+ const anchorFragmentIndex = fragments.findIndex(
4430
+ (fragment) => fragment.key === key
4431
+ );
4432
+ this.host.root.focus({ preventScroll: true });
4433
+ if (header.nodeId) {
4434
+ this.selectNode(header.axis, header.nodeId);
4435
+ }
4436
+ this.textSelectionDrag = null;
4437
+ this.headerTextSelectionDrag = {
4438
+ pointerId: event.pointerId,
4439
+ fragments,
4440
+ anchorFragmentIndex: Math.max(anchorFragmentIndex, 0),
4441
+ anchorOffset: resolvePivotTextOffset({
4442
+ text,
4443
+ pointerX: event.clientX - rootRect2.left,
4444
+ textOriginX: originX,
4445
+ measureText: (value) => this.measurePivotText(value)
4446
+ }),
4447
+ dragged: false
4448
+ };
4449
+ event.preventDefault();
4450
+ this.host.root.setPointerCapture(event.pointerId);
4451
+ return;
4452
+ }
4202
4453
  if (cellHit?.region === "rowHeader" && cellHit.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4203
4454
  const rootRect2 = this.host.root.getBoundingClientRect();
4204
4455
  const rowIndex = resolvePivotRowIndexAtY(
@@ -4248,6 +4499,14 @@ var PivotTable = class {
4248
4499
  if (!this.host) return;
4249
4500
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4250
4501
  if (endedCellSelection) this.cellSelectionDrag = null;
4502
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4503
+ if (this.textSelectionDrag.dragged) this.suppressNextClick = true;
4504
+ this.textSelectionDrag = null;
4505
+ }
4506
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4507
+ if (this.headerTextSelectionDrag.dragged) this.suppressNextClick = true;
4508
+ this.headerTextSelectionDrag = null;
4509
+ }
4251
4510
  const endedRowHeaderSelection = this.rowHeaderSelectionDrag?.pointerId === event.pointerId;
4252
4511
  if (endedRowHeaderSelection) {
4253
4512
  if (this.rowHeaderSelectionDrag?.dragged) this.suppressNextClick = true;
@@ -4279,12 +4538,7 @@ var PivotTable = class {
4279
4538
  this.host.root.addEventListener("pointermove", this.handlePointerMove);
4280
4539
  this.host.root.addEventListener("pointerleave", this.handlePointerLeave);
4281
4540
  this.host.root.addEventListener("click", this.handleClick);
4282
- this.container.ownerDocument.addEventListener(
4283
- "keydown",
4284
- this.handleDocumentKeyDown,
4285
- true
4286
- );
4287
- this.host.root.addEventListener("copy", this.handleCopy);
4541
+ this.host.root.addEventListener("keydown", this.handleKeyDown);
4288
4542
  this.host.root.addEventListener("wheel", this.handleWheel, {
4289
4543
  passive: false
4290
4544
  });
@@ -4434,11 +4688,16 @@ var PivotTable = class {
4434
4688
  (slot) => (slot.nodeId ?? "") === address.columnNodeId && (!slot.indicatorKey || slot.indicatorKey === address.indicatorKey)
4435
4689
  );
4436
4690
  if (rowIndex < 0 || columnIndex < 0) return false;
4437
- const top = resolveVisibleOffset({
4691
+ const frozenRange = this.layout.frozenBottomRowRange;
4692
+ const frozenBottomHeight = frozenRange ? (frozenRange.end - frozenRange.start) * this.layout.rowHeight : 0;
4693
+ const top = frozenRange && rowIndex >= frozenRange.start ? this.scroll.top : resolveVisibleOffset({
4438
4694
  current: this.scroll.top,
4439
4695
  itemStart: rowIndex * this.layout.rowHeight,
4440
4696
  itemSize: this.layout.rowHeight,
4441
- viewportSize: this.layout.bodyRect.height
4697
+ viewportSize: Math.max(
4698
+ this.layout.bodyRect.height - frozenBottomHeight,
4699
+ 0
4700
+ )
4442
4701
  });
4443
4702
  const left = resolveVisibleOffset({
4444
4703
  current: this.scroll.left,
@@ -4510,12 +4769,7 @@ var PivotTable = class {
4510
4769
  this.handlePointerLeave
4511
4770
  );
4512
4771
  this.host?.root.removeEventListener("click", this.handleClick);
4513
- this.container.ownerDocument.removeEventListener(
4514
- "keydown",
4515
- this.handleDocumentKeyDown,
4516
- true
4517
- );
4518
- this.host?.root.removeEventListener("copy", this.handleCopy);
4772
+ this.host?.root.removeEventListener("keydown", this.handleKeyDown);
4519
4773
  this.host?.root.removeEventListener("wheel", this.handleWheel);
4520
4774
  this.host?.root.removeEventListener("contextmenu", this.handleContextMenu);
4521
4775
  this.host?.root.removeEventListener("pointerdown", this.handlePointerDown);
@@ -4627,6 +4881,194 @@ var PivotTable = class {
4627
4881
  activeNode: null
4628
4882
  });
4629
4883
  }
4884
+ updateTextSelectionDrag(event) {
4885
+ const drag = this.textSelectionDrag;
4886
+ if (!drag || !this.layout || !this.host) return;
4887
+ const rootRect = this.host.root.getBoundingClientRect();
4888
+ const x = event.clientX - rootRect.left;
4889
+ const y = event.clientY - rootRect.top;
4890
+ const hitResult = hitTestPivotLayout(this.layout, x, y);
4891
+ const hit = hitResult?.bodyCell;
4892
+ const header = hitResult?.headerCell;
4893
+ if (!hit && !header) return;
4894
+ const fragments = this.createTextSelectionFragments(
4895
+ header?.axis ?? "column"
4896
+ );
4897
+ const focusKey = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${hit?.rowIndex}:${hit?.columnIndex}`;
4898
+ const focusIndex = this.resolveTextFragmentIndex(focusKey, fragments);
4899
+ const text = header ? header.label : this.resolveBodyText(hit);
4900
+ if (!text || focusIndex < 0) return;
4901
+ const textOriginX = header ? header.rect.x + 10 + (header.expandable ? 18 : 0) : this.resolveBodyTextOriginX(hit, text);
4902
+ const focusOffset = resolvePivotTextOffset({
4903
+ text,
4904
+ pointerX: x,
4905
+ textOriginX,
4906
+ measureText: (value) => this.measurePivotText(value)
4907
+ });
4908
+ if (focusIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4909
+ return;
4910
+ drag.dragged = true;
4911
+ this.setSelection({
4912
+ activeCell: hit ? toCellAddress(hit) : this.core?.getSelection().activeCell ?? null,
4913
+ anchorCell: this.core?.getSelection().anchorCell ?? (hit ? toCellAddress(hit) : null),
4914
+ ranges: this.core?.getSelection().ranges ?? [],
4915
+ activeNode: null,
4916
+ textSelection: {
4917
+ fragments: fragments.map((fragment) => ({
4918
+ rowIndex: fragment.rowIndex ?? -1,
4919
+ columnIndex: fragment.columnIndex ?? -1,
4920
+ text: fragment.text,
4921
+ region: fragment.key.startsWith("header:") ? "header" : "body",
4922
+ key: fragment.key
4923
+ })),
4924
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4925
+ focusFragmentIndex: focusIndex,
4926
+ anchorOffset: drag.anchorOffset,
4927
+ focusOffset
4928
+ }
4929
+ });
4930
+ }
4931
+ updateHeaderTextSelectionDrag(event) {
4932
+ const drag = this.headerTextSelectionDrag;
4933
+ if (!drag || !this.host || !this.layout) return;
4934
+ const rootRect = this.host.root.getBoundingClientRect();
4935
+ const localX = event.clientX - rootRect.left;
4936
+ const localY = event.clientY - rootRect.top;
4937
+ const hit = hitTestPivotLayout(this.layout, localX, localY);
4938
+ const header = hit?.headerCell;
4939
+ const body = hit?.bodyCell;
4940
+ if (!header && !body) return;
4941
+ const key = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${body?.rowIndex}:${body?.columnIndex}`;
4942
+ const focusFragmentIndex = drag.fragments.findIndex(
4943
+ (fragment) => fragment.key === key
4944
+ );
4945
+ if (focusFragmentIndex < 0) return;
4946
+ const focusFragment = drag.fragments[focusFragmentIndex];
4947
+ const focusOffset = resolvePivotTextOffset({
4948
+ text: focusFragment.text,
4949
+ pointerX: localX,
4950
+ textOriginX: focusFragment.originX,
4951
+ measureText: (value) => this.measurePivotText(value)
4952
+ });
4953
+ if (focusFragmentIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4954
+ return;
4955
+ drag.dragged = true;
4956
+ const selection = this.core?.getSelection();
4957
+ if (!selection) return;
4958
+ this.setSelection({
4959
+ ...selection,
4960
+ textSelection: {
4961
+ fragments: drag.fragments.map((fragment) => ({
4962
+ rowIndex: fragment.rowIndex ?? -1,
4963
+ columnIndex: fragment.columnIndex ?? -1,
4964
+ text: fragment.text,
4965
+ region: "header",
4966
+ key: fragment.key
4967
+ })),
4968
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4969
+ focusFragmentIndex,
4970
+ anchorOffset: drag.anchorOffset,
4971
+ focusOffset
4972
+ }
4973
+ });
4974
+ }
4975
+ createTextSelectionFragments(axis) {
4976
+ const bodyFragments = (this.layout?.bodyCells ?? []).map((cell) => ({
4977
+ key: `body:${cell.rowIndex}:${cell.columnIndex}`,
4978
+ text: this.resolveBodyText(cell),
4979
+ originX: this.resolveBodyTextOriginX(cell, this.resolveBodyText(cell)),
4980
+ rowIndex: cell.rowIndex,
4981
+ columnIndex: cell.columnIndex
4982
+ })).filter((fragment) => fragment.text.length > 0);
4983
+ if (this.layout) {
4984
+ const columnHeaders = this.createHeaderTextFragments("column");
4985
+ const rowHeaders = this.layout.rowHeaderCells.filter((cell) => cell.label).map((cell) => ({
4986
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4987
+ text: cell.label,
4988
+ rowIndex: void 0,
4989
+ columnIndex: void 0,
4990
+ originX: cell.rect.x + 10
4991
+ }));
4992
+ const rows = Array.from(
4993
+ { length: this.layout.rowSlots.length },
4994
+ (_, rowIndex) => [
4995
+ rowHeaders[rowIndex],
4996
+ ...bodyFragments.filter((fragment) => fragment.rowIndex === rowIndex)
4997
+ ]
4998
+ ).flat();
4999
+ if (axis === "column") {
5000
+ return [...columnHeaders, ...rows].filter(
5001
+ (fragment) => Boolean(fragment)
5002
+ );
5003
+ }
5004
+ const rowHeadersTitle = this.layout.rowFieldHeaderCells.filter((cell) => cell.label).map((cell) => ({
5005
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
5006
+ text: cell.label,
5007
+ rowIndex: void 0,
5008
+ columnIndex: void 0,
5009
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
5010
+ }));
5011
+ return [...rowHeadersTitle, ...columnHeaders, ...rows].filter(
5012
+ (fragment) => Boolean(fragment)
5013
+ );
5014
+ }
5015
+ return [...this.createHeaderTextFragments(axis), ...bodyFragments];
5016
+ }
5017
+ createHeaderTextFragments(axis) {
5018
+ if (!this.layout) return [];
5019
+ return (axis === "column" ? this.layout.columnHeaderCells : [...this.layout.rowHeaderCells, ...this.layout.rowFieldHeaderCells]).filter((cell) => cell.label).map((cell) => ({
5020
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
5021
+ text: cell.label,
5022
+ rowIndex: void 0,
5023
+ columnIndex: void 0,
5024
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
5025
+ }));
5026
+ }
5027
+ resolveBodyText(cell) {
5028
+ const indicator = this.options.indicators.find(
5029
+ (item) => item.key === cell.indicatorKey
5030
+ );
5031
+ if (!indicator || indicator.formatter || typeof cell.value !== "number")
5032
+ return cell.formattedValue;
5033
+ if (indicator.cellType === "percent")
5034
+ return `${(cell.value * 100).toFixed(indicator.precision ?? 2)}%`;
5035
+ if (indicator.cellType === "money")
5036
+ return cell.value.toLocaleString("zh-CN", {
5037
+ style: "currency",
5038
+ currency: "CNY",
5039
+ minimumFractionDigits: indicator.precision ?? 2,
5040
+ maximumFractionDigits: indicator.precision ?? 2
5041
+ });
5042
+ return cell.formattedValue;
5043
+ }
5044
+ createBodyTextFragments() {
5045
+ return (this.layout?.bodyCells ?? []).map((cell) => ({
5046
+ rowIndex: cell.rowIndex,
5047
+ columnIndex: cell.columnIndex,
5048
+ text: this.resolveBodyText(cell)
5049
+ })).filter((fragment) => fragment.text.length > 0);
5050
+ }
5051
+ resolveTextFragmentIndex(key, fragments) {
5052
+ return fragments.findIndex((fragment) => fragment.key === key);
5053
+ }
5054
+ resolveBodyFragmentIndex(cell) {
5055
+ if (!this.layout) return -1;
5056
+ return this.layout.bodyCells.slice(0, this.layout.bodyCells.indexOf(cell) + 1).filter((item) => this.resolveBodyText(item).length > 0).length - 1;
5057
+ }
5058
+ resolveBodyTextOriginX(cell, text) {
5059
+ const indicator = this.options.indicators.find(
5060
+ (item) => item.key === cell.indicatorKey
5061
+ );
5062
+ const width = this.measurePivotText(text);
5063
+ const align = indicator?.align ?? "right";
5064
+ return align === "right" ? cell.rect.x + cell.rect.width - 10 - width : align === "center" ? cell.rect.x + cell.rect.width / 2 - width / 2 : cell.rect.x + 10;
5065
+ }
5066
+ measurePivotText(text) {
5067
+ const context = this.host?.canvas.getContext("2d");
5068
+ if (!context) return text.length * 8;
5069
+ context.font = `${this.options.theme?.fontSize ?? 13}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
5070
+ return context.measureText(text).width;
5071
+ }
4630
5072
  updateRowHeaderSelectionDrag(event) {
4631
5073
  const drag = this.rowHeaderSelectionDrag;
4632
5074
  if (!drag || !this.layout || !this.host) return;
@@ -4865,6 +5307,14 @@ var PivotTable = class {
4865
5307
  selection: this.core.getSelection()
4866
5308
  });
4867
5309
  }
5310
+ /** 复制表格选区文本;同步复制失败时回退到异步实现。 */
5311
+ copyPivotText(text) {
5312
+ if (!text) return;
5313
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
5314
+ if (!copyTextSync(text, ownerDocument)) {
5315
+ void copyText(text, ownerDocument);
5316
+ }
5317
+ }
4868
5318
  scheduleRender() {
4869
5319
  if (this.frameId !== null) return;
4870
5320
  this.frameId = requestAnimationFrame(() => {
@@ -4908,12 +5358,6 @@ var PivotTable = class {
4908
5358
  });
4909
5359
  }
4910
5360
  };
4911
- function describeElement(value) {
4912
- if (!(value instanceof Element)) return String(value);
4913
- const id = value.id ? `#${value.id}` : "";
4914
- const className = typeof value.className === "string" && value.className ? `.${value.className.trim().replace(/\s+/g, ".")}` : "";
4915
- return `${value.tagName.toLocaleLowerCase()}${id}${className}`;
4916
- }
4917
5361
  function toCellAddress(cell) {
4918
5362
  return {
4919
5363
  rowNodeId: cell.rowNodeId ?? "",