@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.cjs CHANGED
@@ -995,7 +995,7 @@ function computePivotScrollbarLayout(params) {
995
995
  }
996
996
  if (hasVertical) {
997
997
  const fullHeight = Math.max(
998
- params.bodyHeight - VERTICAL_TOP_INSET - (hasHorizontal ? SCROLLBAR_THICKNESS : 0),
998
+ params.bodyHeight - VERTICAL_TOP_INSET,
999
999
  0
1000
1000
  );
1001
1001
  const buttonSize = Math.min(SCROLLBAR_THICKNESS, fullHeight / 3);
@@ -1051,6 +1051,7 @@ var DEFAULT_ROW_HEIGHT = 40;
1051
1051
  var DEFAULT_COLUMN_WIDTH = 120;
1052
1052
  var DEFAULT_HEADER_SIZE = 40;
1053
1053
  var DEFAULT_ROW_DIMENSION_WIDTH2 = 140;
1054
+ var SCROLLBAR_THICKNESS2 = 10;
1054
1055
  function buildPivotLayoutSnapshot(params) {
1055
1056
  const { model, options, visibleRowNodes, visibleColumnNodes, width, height } = params;
1056
1057
  const indicatorLayout = options.indicatorLayout ?? "columns";
@@ -1083,8 +1084,8 @@ function buildPivotLayoutSnapshot(params) {
1083
1084
  const columnHeaderHeight = columnHeaderLevels * DEFAULT_HEADER_SIZE;
1084
1085
  const reportFilterHeight = (options.filterDimensions?.length ?? 0) * rowHeight;
1085
1086
  const tableHeaderHeight = reportFilterHeight + columnHeaderHeight;
1086
- const bodyWidth = Math.max(width - rowHeaderWidth, 0);
1087
- const availableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1087
+ const rawBodyWidth = Math.max(width - rowHeaderWidth, 0);
1088
+ const rawAvailableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1088
1089
  const rowSlots = createPivotAxisSlots({
1089
1090
  nodes: visibleRowNodes,
1090
1091
  indicators: options.indicators.map((indicator) => indicator.key),
@@ -1107,19 +1108,52 @@ function buildPivotLayoutSnapshot(params) {
1107
1108
  const columnOffsets = createOffsets(columnWidths);
1108
1109
  const totalBodyWidth = columnOffsets[columnOffsets.length - 1] ?? 0;
1109
1110
  const totalBodyHeight = rowSlots.length * rowHeight;
1111
+ let hasHorizontalScrollbar = totalBodyWidth > rawBodyWidth;
1112
+ let hasVerticalScrollbar = totalBodyHeight > rawAvailableBodyHeight;
1113
+ for (let index = 0; index < 2; index += 1) {
1114
+ hasHorizontalScrollbar = totalBodyWidth > Math.max(
1115
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1116
+ 0
1117
+ );
1118
+ hasVerticalScrollbar = totalBodyHeight > Math.max(
1119
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1120
+ 0
1121
+ );
1122
+ }
1123
+ const bodyWidth = Math.max(
1124
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1125
+ 0
1126
+ );
1127
+ const availableBodyHeight = Math.max(
1128
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1129
+ 0
1130
+ );
1131
+ const frozenBottomRowRange = resolveFrozenBottomRowRange(rowSlots);
1132
+ const frozenBottomRowCount = frozenBottomRowRange ? frozenBottomRowRange.end - frozenBottomRowRange.start : 0;
1133
+ const frozenBottomHeight = frozenBottomRowCount * rowHeight;
1134
+ const scrollableRowCount = frozenBottomRowRange?.start ?? rowSlots.length;
1135
+ const totalScrollableBodyHeight = scrollableRowCount * rowHeight;
1110
1136
  const tableWidth = rowHeaderWidth + Math.min(bodyWidth, totalBodyWidth);
1111
1137
  const bodyHeight = Math.min(availableBodyHeight, totalBodyHeight);
1112
- const fittedHeight = tableHeaderHeight + bodyHeight;
1138
+ const visibleFrozenBottomHeight = Math.min(frozenBottomHeight, bodyHeight);
1139
+ const scrollableBodyHeight = Math.max(
1140
+ bodyHeight - visibleFrozenBottomHeight,
1141
+ 0
1142
+ );
1143
+ const fittedHeight = tableHeaderHeight + bodyHeight + (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0);
1113
1144
  const maxScrollLeft = Math.max(totalBodyWidth - bodyWidth, 0);
1114
- const maxScrollTop = Math.max(totalBodyHeight - bodyHeight, 0);
1145
+ const maxScrollTop = Math.max(
1146
+ totalScrollableBodyHeight - scrollableBodyHeight,
1147
+ 0
1148
+ );
1115
1149
  const scroll = {
1116
1150
  left: utils.clamp(params.scroll.left, 0, maxScrollLeft),
1117
1151
  top: utils.clamp(params.scroll.top, 0, maxScrollTop)
1118
1152
  };
1119
1153
  const visibleRowRange = resolveVisibleRange({
1120
- count: rowSlots.length,
1154
+ count: scrollableRowCount,
1121
1155
  itemSize: rowHeight,
1122
- viewportSize: bodyHeight,
1156
+ viewportSize: scrollableBodyHeight,
1123
1157
  offset: scroll.top,
1124
1158
  overscan: options.overscanRowCount ?? 1
1125
1159
  });
@@ -1154,6 +1188,23 @@ function buildPivotLayoutSnapshot(params) {
1154
1188
  dimensionCount: options.rows.length,
1155
1189
  indicators: options.indicators
1156
1190
  });
1191
+ if (frozenBottomRowRange) {
1192
+ rowHeaderCells.push(
1193
+ ...buildRowHeaderCells({
1194
+ model,
1195
+ options,
1196
+ slots: rowSlots,
1197
+ range: frozenBottomRowRange,
1198
+ rowHeight,
1199
+ rowDimensionWidths,
1200
+ columnHeaderHeight: tableHeaderHeight,
1201
+ scrollTop: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight,
1202
+ expandedNodeIds: params.expandedRowNodeIds,
1203
+ dimensionCount: options.rows.length,
1204
+ indicators: options.indicators
1205
+ })
1206
+ );
1207
+ }
1157
1208
  const columnHeaderCells = buildColumnHeaderCells({
1158
1209
  model,
1159
1210
  options,
@@ -1180,14 +1231,34 @@ function buildPivotLayoutSnapshot(params) {
1180
1231
  columnHeaderHeight: tableHeaderHeight,
1181
1232
  scroll
1182
1233
  });
1234
+ if (frozenBottomRowRange) {
1235
+ bodyCells.push(
1236
+ ...buildBodyCells({
1237
+ model,
1238
+ rowSlots,
1239
+ columnSlots,
1240
+ rowRange: frozenBottomRowRange,
1241
+ columnRange: visibleColumnRange,
1242
+ rowHeight,
1243
+ columnWidths,
1244
+ columnOffsets,
1245
+ rowHeaderWidth,
1246
+ columnHeaderHeight: tableHeaderHeight,
1247
+ scroll: {
1248
+ left: scroll.left,
1249
+ top: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight
1250
+ }
1251
+ })
1252
+ );
1253
+ }
1183
1254
  const scrollbar = computePivotScrollbarLayout({
1184
- width,
1255
+ width: tableWidth + (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1185
1256
  height: fittedHeight,
1186
1257
  bodyTop: tableHeaderHeight,
1187
- bodyHeight,
1258
+ bodyHeight: scrollableBodyHeight,
1188
1259
  bodyWidth,
1189
1260
  totalBodyWidth,
1190
- totalBodyHeight,
1261
+ totalBodyHeight: totalScrollableBodyHeight,
1191
1262
  scrollLeft: scroll.left,
1192
1263
  scrollTop: scroll.top,
1193
1264
  maxScrollLeft,
@@ -1239,6 +1310,7 @@ function buildPivotLayoutSnapshot(params) {
1239
1310
  rowSlots,
1240
1311
  columnSlots,
1241
1312
  visibleRowRange,
1313
+ frozenBottomRowRange,
1242
1314
  visibleColumnRange,
1243
1315
  reportFilterCells,
1244
1316
  rowFieldHeaderCells,
@@ -1373,6 +1445,13 @@ function createAxisSlot(node, indicatorKey) {
1373
1445
  kind: node?.kind ?? "empty"
1374
1446
  };
1375
1447
  }
1448
+ function resolveFrozenBottomRowRange(slots) {
1449
+ let start = slots.length;
1450
+ while (start > 0 && slots[start - 1]?.kind === "grandTotal") {
1451
+ start -= 1;
1452
+ }
1453
+ return start < slots.length ? { start, end: slots.length } : null;
1454
+ }
1376
1455
  function buildRowHeaderCells(params) {
1377
1456
  const cells = [];
1378
1457
  for (let rowIndex = params.range.start; rowIndex < params.range.end; rowIndex += 1) {
@@ -2613,22 +2692,6 @@ function createPivotDomHost(container) {
2613
2692
  }
2614
2693
  };
2615
2694
  }
2616
- async function writePivotClipboardText(text, ownerDocument) {
2617
- const syncCopied = utils.copyTextSync(text, ownerDocument);
2618
- logPivotClipboard("sync-copy-result", { copied: syncCopied });
2619
- if (syncCopied) return true;
2620
- const copied = await utils.copyText(text, ownerDocument);
2621
- logPivotClipboard("copy-result", { copied });
2622
- return copied;
2623
- }
2624
- function copyPivotClipboardTextSync(text, ownerDocument) {
2625
- const copied = utils.copyTextSync(text, ownerDocument);
2626
- logPivotClipboard("sync-copy-result", { copied });
2627
- return copied;
2628
- }
2629
- function logPivotClipboard(stage, detail) {
2630
- console.info(`[PivotTable:clipboard] ${stage}`, detail);
2631
- }
2632
2695
 
2633
2696
  // src/adapters/canvas-host.ts
2634
2697
  function resizePivotCanvas(canvas, width, height) {
@@ -2651,8 +2714,8 @@ function resolvePivotSelectionRect(layout, selection) {
2651
2714
  const kind = resolvePivotSelectionKind(selection);
2652
2715
  const startX = kind === "row" || kind === "row-cell" ? layout.rowHeaderRect.x : layout.bodyRect.x + (layout.columnOffsets[indexes.startColumnIndex] ?? 0) - layout.scroll.left;
2653
2716
  const endX = layout.bodyRect.x + (layout.columnOffsets[indexes.endColumnIndex] ?? 0) + (layout.columnWidths[indexes.endColumnIndex] ?? layout.columnWidth) - layout.scroll.left;
2654
- const startY = kind === "column" ? layout.columnHeaderRect.y : layout.bodyRect.y + indexes.startRowIndex * layout.rowHeight - layout.scroll.top;
2655
- const endY = layout.bodyRect.y + (indexes.endRowIndex + 1) * layout.rowHeight - layout.scroll.top;
2717
+ const startY = kind === "column" ? layout.columnHeaderRect.y : resolvePivotRowTop(layout, indexes.startRowIndex);
2718
+ const endY = resolvePivotRowTop(layout, indexes.endRowIndex) + layout.rowHeight;
2656
2719
  const clipRect = resolvePivotSelectionClipRect(layout, selection);
2657
2720
  const visibleStartX = Math.max(startX, clipRect.x);
2658
2721
  const visibleEndX = Math.min(endX, clipRect.x + clipRect.width);
@@ -2666,6 +2729,13 @@ function resolvePivotSelectionRect(layout, selection) {
2666
2729
  height: visibleEndY - visibleStartY
2667
2730
  };
2668
2731
  }
2732
+ function resolvePivotRowTop(layout, rowIndex) {
2733
+ const frozenRange = layout.frozenBottomRowRange;
2734
+ if (frozenRange && rowIndex >= frozenRange.start && rowIndex < frozenRange.end) {
2735
+ return layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - rowIndex) * layout.rowHeight;
2736
+ }
2737
+ return layout.bodyRect.y + rowIndex * layout.rowHeight - layout.scroll.top;
2738
+ }
2669
2739
  function resolvePivotSelectionClipRect(layout, selection) {
2670
2740
  const kind = resolvePivotSelectionKind(selection);
2671
2741
  if (kind === "row" || kind === "row-cell") {
@@ -2810,26 +2880,29 @@ var defaultPivotTableTheme = {
2810
2880
  };
2811
2881
 
2812
2882
  // src/rendering/draw-scrollbars.ts
2813
- function drawPivotScrollbars(context, layout) {
2883
+ function drawPivotScrollbars(context, layout, borderColor) {
2814
2884
  context.save();
2815
2885
  if (layout.horizontal) {
2816
- drawAxis(context, layout.horizontal, "horizontal");
2886
+ drawAxis(context, layout.horizontal, "horizontal", borderColor);
2817
2887
  }
2818
2888
  if (layout.vertical) {
2819
- drawAxis(context, layout.vertical, "vertical");
2889
+ drawAxis(context, layout.vertical, "vertical", borderColor);
2820
2890
  }
2821
2891
  context.restore();
2822
2892
  }
2823
- function drawAxis(context, layout, axis) {
2824
- drawTrack(context, layout.track);
2893
+ function drawAxis(context, layout, axis, borderColor) {
2894
+ drawTrack(context, layout.track, borderColor);
2825
2895
  drawButton(context, layout.decreaseButton, axis, "decrease");
2826
2896
  drawButton(context, layout.increaseButton, axis, "increase");
2827
2897
  drawThumb(context, layout.thumb);
2828
2898
  }
2829
- function drawTrack(context, rect) {
2899
+ function drawTrack(context, rect, borderColor) {
2830
2900
  context.fillStyle = "rgba(148, 163, 184, 0.16)";
2901
+ context.strokeStyle = borderColor;
2902
+ context.lineWidth = 1;
2831
2903
  roundRect(context, rect, rect.height / 2);
2832
2904
  context.fill();
2905
+ context.stroke();
2833
2906
  }
2834
2907
  function drawThumb(context, rect) {
2835
2908
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -2993,7 +3066,7 @@ function renderPivotTable(params) {
2993
3066
  });
2994
3067
  withClip(context, layout.cornerRect, () => {
2995
3068
  fillRect(context, layout.cornerRect, theme.cornerBackgroundColor);
2996
- drawCorner(context, layout, theme);
3069
+ drawCorner(context, layout, theme, params.selection?.textSelection);
2997
3070
  });
2998
3071
  withClip(context, layout.rowHeaderRect, () => {
2999
3072
  layout.rowHeaderCells.forEach((cell) => {
@@ -3007,7 +3080,9 @@ function renderPivotTable(params) {
3007
3080
  cell,
3008
3081
  theme,
3009
3082
  !areaSelection && isHeaderSelected(cell, selectionPathKeys.row),
3010
- contentRect
3083
+ contentRect,
3084
+ void 0,
3085
+ params.selection?.textSelection
3011
3086
  );
3012
3087
  });
3013
3088
  layout.rowHeaderCells.forEach(
@@ -3027,7 +3102,10 @@ function renderPivotTable(params) {
3027
3102
  context,
3028
3103
  cell,
3029
3104
  theme,
3030
- !areaSelection && isHeaderSelected(cell, selectionPathKeys.column)
3105
+ !areaSelection && isHeaderSelected(cell, selectionPathKeys.column),
3106
+ cell.rect,
3107
+ void 0,
3108
+ params.selection?.textSelection
3031
3109
  )
3032
3110
  );
3033
3111
  layout.columnHeaderCells.forEach(
@@ -3062,7 +3140,8 @@ function renderPivotTable(params) {
3062
3140
  activeNodePathKey
3063
3141
  )
3064
3142
  },
3065
- visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0
3143
+ visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0,
3144
+ params.selection?.textSelection
3066
3145
  )
3067
3146
  );
3068
3147
  layout.bodyCells.forEach(
@@ -3091,7 +3170,7 @@ function renderPivotTable(params) {
3091
3170
  theme.borderColor
3092
3171
  );
3093
3172
  if (options.debug) drawDebugOverlay(context, layout, theme);
3094
- drawPivotScrollbars(context, layout.scrollbar);
3173
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor);
3095
3174
  drawPivotViewportRightBorder(
3096
3175
  context,
3097
3176
  { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
@@ -3203,7 +3282,7 @@ function drawReportFilterCell(context, cell, theme) {
3203
3282
  skipRight: true
3204
3283
  });
3205
3284
  }
3206
- function drawCorner(context, layout, theme) {
3285
+ function drawCorner(context, layout, theme, textSelection) {
3207
3286
  layout.rowFieldHeaderCells.forEach(
3208
3287
  (cell) => drawHeaderCell(
3209
3288
  context,
@@ -3211,7 +3290,8 @@ function drawCorner(context, layout, theme) {
3211
3290
  theme,
3212
3291
  false,
3213
3292
  cell.rect,
3214
- theme.cornerBackgroundColor
3293
+ theme.cornerBackgroundColor,
3294
+ textSelection
3215
3295
  )
3216
3296
  );
3217
3297
  if (layout.rowFieldHeaderCells.length === 0) {
@@ -3228,7 +3308,7 @@ function drawCorner(context, layout, theme) {
3228
3308
  })
3229
3309
  );
3230
3310
  }
3231
- function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor) {
3311
+ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor, textSelection) {
3232
3312
  const background = backgroundColor ?? (selected ? theme.selectionBackgroundColor : cell.kind === "grandTotal" ? theme.grandTotalBackgroundColor : cell.kind === "subtotal" ? theme.subtotalBackgroundColor : cell.axis === "row" ? theme.rowHeaderBackgroundColor : theme.columnHeaderBackgroundColor);
3233
3313
  fillRect(context, cell.rect, background);
3234
3314
  const iconPadding = cell.expandable ? 18 : 0;
@@ -3250,6 +3330,13 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3250
3330
  cell.filterable ? 32 : 10
3251
3331
  );
3252
3332
  }
3333
+ drawHeaderTextSelectionBackground(
3334
+ context,
3335
+ cell,
3336
+ contentRect,
3337
+ textSelection,
3338
+ theme.fontSize
3339
+ );
3253
3340
  drawText(
3254
3341
  context,
3255
3342
  cell.label,
@@ -3262,13 +3349,46 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3262
3349
  cell.align ?? "left"
3263
3350
  );
3264
3351
  }
3352
+ function drawHeaderTextSelectionBackground(context, cell, contentRect, selection, fontSize) {
3353
+ if (!selection) return;
3354
+ const index = selection.fragments.findIndex(
3355
+ (fragment) => fragment.region === "header" && fragment.key === `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`
3356
+ );
3357
+ if (index < 0) return;
3358
+ const range = resolveTextSelectionRange(selection, index, cell.label.length);
3359
+ if (!range) return;
3360
+ const width = context.measureText(cell.label).width;
3361
+ const align = cell.align ?? "left";
3362
+ 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);
3363
+ context.save();
3364
+ context.fillStyle = "#99c2ff";
3365
+ context.fillRect(
3366
+ origin + context.measureText(cell.label.slice(0, range.start)).width,
3367
+ contentRect.y + (contentRect.height - fontSize) / 2,
3368
+ context.measureText(cell.label.slice(range.start, range.end)).width,
3369
+ fontSize
3370
+ );
3371
+ context.restore();
3372
+ }
3373
+ function resolveTextSelectionRange(selection, index, length) {
3374
+ const forward = selection.anchorFragmentIndex <= selection.focusFragmentIndex;
3375
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3376
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3377
+ if (index < startIndex || index > endIndex) return null;
3378
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3379
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : length;
3380
+ return {
3381
+ start: Math.max(0, Math.min(start, length)),
3382
+ end: Math.max(0, Math.min(end, length))
3383
+ };
3384
+ }
3265
3385
  function isHeaderSelected(cell, selectedPathKey) {
3266
3386
  if (!selectedPathKey || !cell.nodeId) {
3267
3387
  return false;
3268
3388
  }
3269
3389
  return Boolean(isPathRelated2(selectedPathKey, cell.pathKey));
3270
3390
  }
3271
- function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3391
+ function drawBodyCell(context, cell, options, theme, state, visibleMaximum, textSelection) {
3272
3392
  const indicator = options.indicators.find(
3273
3393
  (item) => item.key === cell.indicatorKey
3274
3394
  );
@@ -3298,15 +3418,54 @@ function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3298
3418
  "#3370ff"
3299
3419
  );
3300
3420
  }
3421
+ const text = resolveBodyDisplayText(cell, indicator);
3422
+ const padding = 10;
3423
+ const align = indicator?.align ?? "right";
3424
+ drawTextSelectionBackground(
3425
+ context,
3426
+ cell,
3427
+ text,
3428
+ align,
3429
+ padding,
3430
+ textSelection,
3431
+ theme.fontSize
3432
+ );
3301
3433
  drawText(
3302
3434
  context,
3303
- resolveBodyDisplayText(cell, indicator),
3435
+ text,
3304
3436
  cell.rect,
3305
3437
  theme.textColor,
3306
3438
  10,
3307
3439
  indicator?.align ?? "right"
3308
3440
  );
3309
3441
  }
3442
+ function drawTextSelectionBackground(context, cell, text, align, padding, selection, fontSize) {
3443
+ if (!selection) return;
3444
+ const index = selection.fragments.findIndex(
3445
+ (fragment) => fragment.rowIndex === cell.rowIndex && fragment.columnIndex === cell.columnIndex && fragment.text === text
3446
+ );
3447
+ if (index < 0) return;
3448
+ const forward = selection.anchorFragmentIndex < selection.focusFragmentIndex || selection.anchorFragmentIndex === selection.focusFragmentIndex && selection.anchorOffset <= selection.focusOffset;
3449
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3450
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3451
+ if (index < startIndex || index > endIndex) return;
3452
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3453
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : text.length;
3454
+ if (start >= end) return;
3455
+ const availableWidth = Math.max(cell.rect.width - padding * 2, 0);
3456
+ const shown = truncateText(context, text, availableWidth);
3457
+ const textWidth = context.measureText(shown).width;
3458
+ 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;
3459
+ context.save();
3460
+ context.fillStyle = "#99c2ff";
3461
+ context.fillRect(
3462
+ origin + context.measureText(shown.slice(0, start)).width,
3463
+ cell.rect.y + (cell.rect.height - fontSize) / 2,
3464
+ context.measureText(shown.slice(start, end)).width,
3465
+ fontSize
3466
+ );
3467
+ context.restore();
3468
+ }
3310
3469
  function resolveSelectionPathKeys(layout, selection, activeNodePathKey) {
3311
3470
  const activeNode = selection?.activeNode;
3312
3471
  if (activeNode) {
@@ -3529,7 +3688,7 @@ function hitTestPivotLayout(layout, x, y) {
3529
3688
  onSortIcon: false
3530
3689
  };
3531
3690
  }
3532
- const bodyCell = layout.bodyCells.find((cell) => contains(cell.rect, x, y));
3691
+ const bodyCell = findLastContaining(layout.bodyCells, x, y);
3533
3692
  if (bodyCell) {
3534
3693
  return {
3535
3694
  region: "body",
@@ -3539,9 +3698,7 @@ function hitTestPivotLayout(layout, x, y) {
3539
3698
  onSortIcon: false
3540
3699
  };
3541
3700
  }
3542
- const rowHeaderCell = layout.rowHeaderCells.find(
3543
- (cell) => contains(cell.rect, x, y)
3544
- );
3701
+ const rowHeaderCell = findLastContaining(layout.rowHeaderCells, x, y);
3545
3702
  if (rowHeaderCell) {
3546
3703
  return {
3547
3704
  region: "rowHeader",
@@ -3594,6 +3751,68 @@ function isSortIconHit(cell, x) {
3594
3751
  function contains(rect, x, y) {
3595
3752
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3596
3753
  }
3754
+ function findLastContaining(cells, x, y) {
3755
+ for (let index = cells.length - 1; index >= 0; index -= 1) {
3756
+ const cell = cells[index];
3757
+ if (cell && contains(cell.rect, x, y)) return cell;
3758
+ }
3759
+ return void 0;
3760
+ }
3761
+
3762
+ // src/interaction/text-selection.ts
3763
+ function resolvePivotTextOffset(params) {
3764
+ const boundaries = [0];
3765
+ let offset = 0;
3766
+ for (const character of params.text) {
3767
+ offset += character.length;
3768
+ boundaries.push(offset);
3769
+ }
3770
+ const relativeX = params.pointerX - params.textOriginX;
3771
+ if (relativeX <= 0 || boundaries.length === 1) return 0;
3772
+ if (relativeX >= params.measureText(params.text)) return params.text.length;
3773
+ let low = 1;
3774
+ let high = boundaries.length - 1;
3775
+ while (low < high) {
3776
+ const middle = Math.floor((low + high) / 2);
3777
+ const boundary = boundaries[middle] ?? params.text.length;
3778
+ if (params.measureText(params.text.slice(0, boundary)) < relativeX) {
3779
+ low = middle + 1;
3780
+ } else {
3781
+ high = middle;
3782
+ }
3783
+ }
3784
+ const end = boundaries[low] ?? params.text.length;
3785
+ const start = boundaries[Math.max(low - 1, 0)] ?? 0;
3786
+ const startWidth = params.measureText(params.text.slice(0, start));
3787
+ const endWidth = params.measureText(params.text.slice(0, end));
3788
+ return relativeX - startWidth <= endWidth - relativeX ? start : end;
3789
+ }
3790
+ function getPivotSelectedText(selection) {
3791
+ const textSelection = selection.textSelection;
3792
+ if (!textSelection) return null;
3793
+ const forward = textSelection.anchorFragmentIndex < textSelection.focusFragmentIndex || textSelection.anchorFragmentIndex === textSelection.focusFragmentIndex && textSelection.anchorOffset <= textSelection.focusOffset;
3794
+ const startIndex = forward ? textSelection.anchorFragmentIndex : textSelection.focusFragmentIndex;
3795
+ const endIndex = forward ? textSelection.focusFragmentIndex : textSelection.anchorFragmentIndex;
3796
+ const startOffset = forward ? textSelection.anchorOffset : textSelection.focusOffset;
3797
+ const endOffset = forward ? textSelection.focusOffset : textSelection.anchorOffset;
3798
+ const selected = textSelection.fragments.slice(startIndex, endIndex + 1).map((fragment, index) => {
3799
+ const absoluteIndex = startIndex + index;
3800
+ const start = absoluteIndex === startIndex ? startOffset : 0;
3801
+ const end = absoluteIndex === endIndex ? endOffset : fragment.text.length;
3802
+ return {
3803
+ fragment,
3804
+ text: fragment.text.slice(
3805
+ Math.max(0, Math.min(start, fragment.text.length)),
3806
+ Math.max(0, Math.min(end, fragment.text.length))
3807
+ )
3808
+ };
3809
+ });
3810
+ return selected.reduce((result, item, index) => {
3811
+ const previous = selected[index - 1]?.fragment;
3812
+ const separator = previous ? previous.rowIndex === item.fragment.rowIndex ? " " : "\n" : "";
3813
+ return `${result}${separator}${item.text}`;
3814
+ }, "") || null;
3815
+ }
3597
3816
 
3598
3817
  // src/features/clipboard/pivot-selection-copy.ts
3599
3818
  function createPivotSelectionText(params) {
@@ -3775,6 +3994,16 @@ function createAddress(rowSlot, columnSlot, indicatorKey) {
3775
3994
  // src/domain/interaction/row-selection.ts
3776
3995
  function resolvePivotRowIndexAtY(layout, y) {
3777
3996
  if (layout.rowSlots.length === 0 || layout.rowHeight <= 0) return null;
3997
+ const frozenRange = layout.frozenBottomRowRange;
3998
+ if (frozenRange) {
3999
+ const frozenTop = layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - frozenRange.start) * layout.rowHeight;
4000
+ if (y >= frozenTop) {
4001
+ return Math.min(
4002
+ frozenRange.start + Math.floor((y - frozenTop) / layout.rowHeight),
4003
+ frozenRange.end - 1
4004
+ );
4005
+ }
4006
+ }
3778
4007
  const index = Math.floor(
3779
4008
  (y - layout.bodyRect.y + layout.scroll.top) / layout.rowHeight
3780
4009
  );
@@ -3898,6 +4127,8 @@ var PivotTable = class {
3898
4127
  __publicField(this, "suppressNextClick", false);
3899
4128
  __publicField(this, "scrollbarDrag", null);
3900
4129
  __publicField(this, "cellSelectionDrag", null);
4130
+ __publicField(this, "textSelectionDrag", null);
4131
+ __publicField(this, "headerTextSelectionDrag", null);
3901
4132
  __publicField(this, "rowHeaderSelectionDrag", null);
3902
4133
  __publicField(this, "handlePointerMove", (event) => {
3903
4134
  if (this.scrollbarDrag) {
@@ -3918,10 +4149,22 @@ var PivotTable = class {
3918
4149
  return;
3919
4150
  }
3920
4151
  if (this.cellSelectionDrag?.pointerId === event.pointerId) {
4152
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4153
+ this.updateTextSelectionDrag(event);
4154
+ if (this.textSelectionDrag.dragged) return;
4155
+ }
3921
4156
  this.updateCellSelectionDrag(event);
3922
4157
  return;
3923
4158
  }
4159
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4160
+ this.updateHeaderTextSelectionDrag(event);
4161
+ return;
4162
+ }
3924
4163
  if (this.rowHeaderSelectionDrag?.pointerId === event.pointerId) {
4164
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4165
+ this.updateHeaderTextSelectionDrag(event);
4166
+ if (this.headerTextSelectionDrag.dragged) return;
4167
+ }
3925
4168
  this.updateRowHeaderSelectionDrag(event);
3926
4169
  return;
3927
4170
  }
@@ -3968,6 +4211,7 @@ var PivotTable = class {
3968
4211
  this.openReportFilterPanel(hit.reportFilterCell);
3969
4212
  return;
3970
4213
  }
4214
+ if (hit.reportFilterCell) return;
3971
4215
  if (hit.bodyCell) {
3972
4216
  this.selectCell(toCellAddress(hit.bodyCell), event.shiftKey);
3973
4217
  return;
@@ -4009,52 +4253,20 @@ var PivotTable = class {
4009
4253
  this.scheduleRender();
4010
4254
  return;
4011
4255
  });
4012
- __publicField(this, "handleDocumentKeyDown", (event) => {
4013
- const root = this.host?.root;
4014
- const activeElement = this.container.ownerDocument.activeElement;
4015
- const belongsToTable = Boolean(
4016
- root && (activeElement === root || activeElement instanceof Element && root.contains(activeElement))
4017
- );
4256
+ __publicField(this, "handleKeyDown", (event) => {
4018
4257
  const isCopyShortcut = (event.ctrlKey || event.metaKey) && !event.altKey && (event.key.toLocaleLowerCase() === "c" || event.code === "KeyC");
4019
4258
  if (isCopyShortcut) {
4020
- logPivotClipboard("keydown-captured", {
4021
- belongsToTable,
4022
- key: event.key,
4023
- code: event.code,
4024
- ctrlKey: event.ctrlKey,
4025
- metaKey: event.metaKey,
4026
- defaultPrevented: event.defaultPrevented,
4027
- activeElement: describeElement(activeElement),
4028
- eventTarget: describeElement(event.target),
4029
- protocol: this.container.ownerDocument.location?.protocol ?? "unknown",
4030
- isSecureContext: this.container.ownerDocument.defaultView?.isSecureContext ?? false
4031
- });
4032
- }
4033
- if (!belongsToTable) return;
4034
- if (isCopyShortcut) {
4035
- const text = this.createSelectionTsv();
4036
- const selection = this.core?.getSelection() ?? null;
4037
- logPivotClipboard("selection-resolved", {
4038
- hasLayout: Boolean(this.layout),
4039
- selection,
4040
- textLength: text.length,
4041
- textPreview: text.slice(0, 120)
4042
- });
4259
+ const text = getPivotSelectedText(
4260
+ this.core?.getSelection() ?? {
4261
+ }
4262
+ ) ?? this.createSelectionTsv();
4043
4263
  if (!text) {
4044
- logPivotClipboard("copy-aborted", { reason: "empty-selection-text" });
4045
4264
  return;
4046
4265
  }
4047
4266
  event.preventDefault();
4048
- const copied = copyPivotClipboardTextSync(
4049
- text,
4050
- this.container.ownerDocument
4051
- );
4052
- if (!copied) {
4053
- void writePivotClipboardText(text, this.container.ownerDocument).then(
4054
- (fallbackCopied) => logPivotClipboard("fallback-complete", {
4055
- copied: fallbackCopied
4056
- })
4057
- );
4267
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
4268
+ if (!utils.copyTextSync(text, ownerDocument)) {
4269
+ void utils.copyText(text, ownerDocument);
4058
4270
  }
4059
4271
  return;
4060
4272
  }
@@ -4067,16 +4279,6 @@ var PivotTable = class {
4067
4279
  event.preventDefault();
4068
4280
  this.selectCell(next, event.shiftKey);
4069
4281
  });
4070
- __publicField(this, "handleCopy", (event) => {
4071
- const text = this.createSelectionTsv();
4072
- logPivotClipboard("copy-event", {
4073
- textLength: text.length,
4074
- hasClipboardData: Boolean(event.clipboardData)
4075
- });
4076
- if (!text) return;
4077
- event.preventDefault();
4078
- event.clipboardData?.setData("text/plain", text);
4079
- });
4080
4282
  __publicField(this, "handleWheel", (event) => {
4081
4283
  if (!this.layout) return;
4082
4284
  const delta = resolvePivotWheelScrollDelta({
@@ -4148,13 +4350,13 @@ var PivotTable = class {
4148
4350
  endColumnIndex: hit.bodyCell.columnIndex
4149
4351
  }) : "";
4150
4352
  if (text) {
4151
- void writePivotClipboardText(text, this.container.ownerDocument);
4353
+ this.copyPivotText(text);
4152
4354
  }
4153
4355
  },
4154
4356
  onCopySelection: selectionRange ? () => {
4155
4357
  const text = this.createSelectionTsv();
4156
4358
  if (text) {
4157
- void writePivotClipboardText(text, this.container.ownerDocument);
4359
+ this.copyPivotText(text);
4158
4360
  }
4159
4361
  } : void 0,
4160
4362
  customSelection: {
@@ -4177,7 +4379,7 @@ var PivotTable = class {
4177
4379
  endColumnIndex: range.endColumn - 1
4178
4380
  });
4179
4381
  if (text) {
4180
- void writePivotClipboardText(text, this.container.ownerDocument);
4382
+ this.copyPivotText(text);
4181
4383
  }
4182
4384
  },
4183
4385
  onExportCsv: () => downloadBlob(
@@ -4209,6 +4411,24 @@ var PivotTable = class {
4209
4411
  const address = toCellAddress(cellHit.bodyCell);
4210
4412
  this.host.root.focus({ preventScroll: true });
4211
4413
  const selection = this.selectCell(address, event.shiftKey);
4414
+ const text = this.resolveBodyText(cellHit.bodyCell);
4415
+ const textOriginX = this.resolveBodyTextOriginX(cellHit.bodyCell, text);
4416
+ const anchorOffset = text ? resolvePivotTextOffset({
4417
+ text,
4418
+ pointerX: event.clientX - this.host.root.getBoundingClientRect().left,
4419
+ textOriginX,
4420
+ measureText: (value) => this.measurePivotText(value)
4421
+ }) : 0;
4422
+ const anchorFragmentIndex = this.resolveTextFragmentIndex(
4423
+ `body:${cellHit.bodyCell.rowIndex}:${cellHit.bodyCell.columnIndex}`,
4424
+ this.createTextSelectionFragments("column")
4425
+ );
4426
+ this.textSelectionDrag = text ? {
4427
+ pointerId: event.pointerId,
4428
+ anchorFragmentIndex,
4429
+ anchorOffset,
4430
+ dragged: false
4431
+ } : null;
4212
4432
  this.cellSelectionDrag = {
4213
4433
  pointerId: event.pointerId,
4214
4434
  anchor: selection?.anchorCell ?? address,
@@ -4220,6 +4440,37 @@ var PivotTable = class {
4220
4440
  this.suppressNextClick = true;
4221
4441
  return;
4222
4442
  }
4443
+ if (cellHit?.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4444
+ const header = cellHit.headerCell;
4445
+ const text = header.label;
4446
+ const rootRect2 = this.host.root.getBoundingClientRect();
4447
+ const originX = header.rect.x + 10 + (header.expandable ? 18 : 0);
4448
+ const fragments = this.createTextSelectionFragments(header.axis);
4449
+ const key = `header:${header.axis}:${header.nodeId ?? ""}:${header.label}`;
4450
+ const anchorFragmentIndex = fragments.findIndex(
4451
+ (fragment) => fragment.key === key
4452
+ );
4453
+ this.host.root.focus({ preventScroll: true });
4454
+ if (header.nodeId) {
4455
+ this.selectNode(header.axis, header.nodeId);
4456
+ }
4457
+ this.textSelectionDrag = null;
4458
+ this.headerTextSelectionDrag = {
4459
+ pointerId: event.pointerId,
4460
+ fragments,
4461
+ anchorFragmentIndex: Math.max(anchorFragmentIndex, 0),
4462
+ anchorOffset: resolvePivotTextOffset({
4463
+ text,
4464
+ pointerX: event.clientX - rootRect2.left,
4465
+ textOriginX: originX,
4466
+ measureText: (value) => this.measurePivotText(value)
4467
+ }),
4468
+ dragged: false
4469
+ };
4470
+ event.preventDefault();
4471
+ this.host.root.setPointerCapture(event.pointerId);
4472
+ return;
4473
+ }
4223
4474
  if (cellHit?.region === "rowHeader" && cellHit.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4224
4475
  const rootRect2 = this.host.root.getBoundingClientRect();
4225
4476
  const rowIndex = resolvePivotRowIndexAtY(
@@ -4269,6 +4520,14 @@ var PivotTable = class {
4269
4520
  if (!this.host) return;
4270
4521
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4271
4522
  if (endedCellSelection) this.cellSelectionDrag = null;
4523
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4524
+ if (this.textSelectionDrag.dragged) this.suppressNextClick = true;
4525
+ this.textSelectionDrag = null;
4526
+ }
4527
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4528
+ if (this.headerTextSelectionDrag.dragged) this.suppressNextClick = true;
4529
+ this.headerTextSelectionDrag = null;
4530
+ }
4272
4531
  const endedRowHeaderSelection = this.rowHeaderSelectionDrag?.pointerId === event.pointerId;
4273
4532
  if (endedRowHeaderSelection) {
4274
4533
  if (this.rowHeaderSelectionDrag?.dragged) this.suppressNextClick = true;
@@ -4300,12 +4559,7 @@ var PivotTable = class {
4300
4559
  this.host.root.addEventListener("pointermove", this.handlePointerMove);
4301
4560
  this.host.root.addEventListener("pointerleave", this.handlePointerLeave);
4302
4561
  this.host.root.addEventListener("click", this.handleClick);
4303
- this.container.ownerDocument.addEventListener(
4304
- "keydown",
4305
- this.handleDocumentKeyDown,
4306
- true
4307
- );
4308
- this.host.root.addEventListener("copy", this.handleCopy);
4562
+ this.host.root.addEventListener("keydown", this.handleKeyDown);
4309
4563
  this.host.root.addEventListener("wheel", this.handleWheel, {
4310
4564
  passive: false
4311
4565
  });
@@ -4455,11 +4709,16 @@ var PivotTable = class {
4455
4709
  (slot) => (slot.nodeId ?? "") === address.columnNodeId && (!slot.indicatorKey || slot.indicatorKey === address.indicatorKey)
4456
4710
  );
4457
4711
  if (rowIndex < 0 || columnIndex < 0) return false;
4458
- const top = resolveVisibleOffset({
4712
+ const frozenRange = this.layout.frozenBottomRowRange;
4713
+ const frozenBottomHeight = frozenRange ? (frozenRange.end - frozenRange.start) * this.layout.rowHeight : 0;
4714
+ const top = frozenRange && rowIndex >= frozenRange.start ? this.scroll.top : resolveVisibleOffset({
4459
4715
  current: this.scroll.top,
4460
4716
  itemStart: rowIndex * this.layout.rowHeight,
4461
4717
  itemSize: this.layout.rowHeight,
4462
- viewportSize: this.layout.bodyRect.height
4718
+ viewportSize: Math.max(
4719
+ this.layout.bodyRect.height - frozenBottomHeight,
4720
+ 0
4721
+ )
4463
4722
  });
4464
4723
  const left = resolveVisibleOffset({
4465
4724
  current: this.scroll.left,
@@ -4531,12 +4790,7 @@ var PivotTable = class {
4531
4790
  this.handlePointerLeave
4532
4791
  );
4533
4792
  this.host?.root.removeEventListener("click", this.handleClick);
4534
- this.container.ownerDocument.removeEventListener(
4535
- "keydown",
4536
- this.handleDocumentKeyDown,
4537
- true
4538
- );
4539
- this.host?.root.removeEventListener("copy", this.handleCopy);
4793
+ this.host?.root.removeEventListener("keydown", this.handleKeyDown);
4540
4794
  this.host?.root.removeEventListener("wheel", this.handleWheel);
4541
4795
  this.host?.root.removeEventListener("contextmenu", this.handleContextMenu);
4542
4796
  this.host?.root.removeEventListener("pointerdown", this.handlePointerDown);
@@ -4648,6 +4902,194 @@ var PivotTable = class {
4648
4902
  activeNode: null
4649
4903
  });
4650
4904
  }
4905
+ updateTextSelectionDrag(event) {
4906
+ const drag = this.textSelectionDrag;
4907
+ if (!drag || !this.layout || !this.host) return;
4908
+ const rootRect = this.host.root.getBoundingClientRect();
4909
+ const x = event.clientX - rootRect.left;
4910
+ const y = event.clientY - rootRect.top;
4911
+ const hitResult = hitTestPivotLayout(this.layout, x, y);
4912
+ const hit = hitResult?.bodyCell;
4913
+ const header = hitResult?.headerCell;
4914
+ if (!hit && !header) return;
4915
+ const fragments = this.createTextSelectionFragments(
4916
+ header?.axis ?? "column"
4917
+ );
4918
+ const focusKey = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${hit?.rowIndex}:${hit?.columnIndex}`;
4919
+ const focusIndex = this.resolveTextFragmentIndex(focusKey, fragments);
4920
+ const text = header ? header.label : this.resolveBodyText(hit);
4921
+ if (!text || focusIndex < 0) return;
4922
+ const textOriginX = header ? header.rect.x + 10 + (header.expandable ? 18 : 0) : this.resolveBodyTextOriginX(hit, text);
4923
+ const focusOffset = resolvePivotTextOffset({
4924
+ text,
4925
+ pointerX: x,
4926
+ textOriginX,
4927
+ measureText: (value) => this.measurePivotText(value)
4928
+ });
4929
+ if (focusIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4930
+ return;
4931
+ drag.dragged = true;
4932
+ this.setSelection({
4933
+ activeCell: hit ? toCellAddress(hit) : this.core?.getSelection().activeCell ?? null,
4934
+ anchorCell: this.core?.getSelection().anchorCell ?? (hit ? toCellAddress(hit) : null),
4935
+ ranges: this.core?.getSelection().ranges ?? [],
4936
+ activeNode: null,
4937
+ textSelection: {
4938
+ fragments: fragments.map((fragment) => ({
4939
+ rowIndex: fragment.rowIndex ?? -1,
4940
+ columnIndex: fragment.columnIndex ?? -1,
4941
+ text: fragment.text,
4942
+ region: fragment.key.startsWith("header:") ? "header" : "body",
4943
+ key: fragment.key
4944
+ })),
4945
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4946
+ focusFragmentIndex: focusIndex,
4947
+ anchorOffset: drag.anchorOffset,
4948
+ focusOffset
4949
+ }
4950
+ });
4951
+ }
4952
+ updateHeaderTextSelectionDrag(event) {
4953
+ const drag = this.headerTextSelectionDrag;
4954
+ if (!drag || !this.host || !this.layout) return;
4955
+ const rootRect = this.host.root.getBoundingClientRect();
4956
+ const localX = event.clientX - rootRect.left;
4957
+ const localY = event.clientY - rootRect.top;
4958
+ const hit = hitTestPivotLayout(this.layout, localX, localY);
4959
+ const header = hit?.headerCell;
4960
+ const body = hit?.bodyCell;
4961
+ if (!header && !body) return;
4962
+ const key = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${body?.rowIndex}:${body?.columnIndex}`;
4963
+ const focusFragmentIndex = drag.fragments.findIndex(
4964
+ (fragment) => fragment.key === key
4965
+ );
4966
+ if (focusFragmentIndex < 0) return;
4967
+ const focusFragment = drag.fragments[focusFragmentIndex];
4968
+ const focusOffset = resolvePivotTextOffset({
4969
+ text: focusFragment.text,
4970
+ pointerX: localX,
4971
+ textOriginX: focusFragment.originX,
4972
+ measureText: (value) => this.measurePivotText(value)
4973
+ });
4974
+ if (focusFragmentIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4975
+ return;
4976
+ drag.dragged = true;
4977
+ const selection = this.core?.getSelection();
4978
+ if (!selection) return;
4979
+ this.setSelection({
4980
+ ...selection,
4981
+ textSelection: {
4982
+ fragments: drag.fragments.map((fragment) => ({
4983
+ rowIndex: fragment.rowIndex ?? -1,
4984
+ columnIndex: fragment.columnIndex ?? -1,
4985
+ text: fragment.text,
4986
+ region: "header",
4987
+ key: fragment.key
4988
+ })),
4989
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4990
+ focusFragmentIndex,
4991
+ anchorOffset: drag.anchorOffset,
4992
+ focusOffset
4993
+ }
4994
+ });
4995
+ }
4996
+ createTextSelectionFragments(axis) {
4997
+ const bodyFragments = (this.layout?.bodyCells ?? []).map((cell) => ({
4998
+ key: `body:${cell.rowIndex}:${cell.columnIndex}`,
4999
+ text: this.resolveBodyText(cell),
5000
+ originX: this.resolveBodyTextOriginX(cell, this.resolveBodyText(cell)),
5001
+ rowIndex: cell.rowIndex,
5002
+ columnIndex: cell.columnIndex
5003
+ })).filter((fragment) => fragment.text.length > 0);
5004
+ if (this.layout) {
5005
+ const columnHeaders = this.createHeaderTextFragments("column");
5006
+ const rowHeaders = this.layout.rowHeaderCells.filter((cell) => cell.label).map((cell) => ({
5007
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
5008
+ text: cell.label,
5009
+ rowIndex: void 0,
5010
+ columnIndex: void 0,
5011
+ originX: cell.rect.x + 10
5012
+ }));
5013
+ const rows = Array.from(
5014
+ { length: this.layout.rowSlots.length },
5015
+ (_, rowIndex) => [
5016
+ rowHeaders[rowIndex],
5017
+ ...bodyFragments.filter((fragment) => fragment.rowIndex === rowIndex)
5018
+ ]
5019
+ ).flat();
5020
+ if (axis === "column") {
5021
+ return [...columnHeaders, ...rows].filter(
5022
+ (fragment) => Boolean(fragment)
5023
+ );
5024
+ }
5025
+ const rowHeadersTitle = this.layout.rowFieldHeaderCells.filter((cell) => cell.label).map((cell) => ({
5026
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
5027
+ text: cell.label,
5028
+ rowIndex: void 0,
5029
+ columnIndex: void 0,
5030
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
5031
+ }));
5032
+ return [...rowHeadersTitle, ...columnHeaders, ...rows].filter(
5033
+ (fragment) => Boolean(fragment)
5034
+ );
5035
+ }
5036
+ return [...this.createHeaderTextFragments(axis), ...bodyFragments];
5037
+ }
5038
+ createHeaderTextFragments(axis) {
5039
+ if (!this.layout) return [];
5040
+ return (axis === "column" ? this.layout.columnHeaderCells : [...this.layout.rowHeaderCells, ...this.layout.rowFieldHeaderCells]).filter((cell) => cell.label).map((cell) => ({
5041
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
5042
+ text: cell.label,
5043
+ rowIndex: void 0,
5044
+ columnIndex: void 0,
5045
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
5046
+ }));
5047
+ }
5048
+ resolveBodyText(cell) {
5049
+ const indicator = this.options.indicators.find(
5050
+ (item) => item.key === cell.indicatorKey
5051
+ );
5052
+ if (!indicator || indicator.formatter || typeof cell.value !== "number")
5053
+ return cell.formattedValue;
5054
+ if (indicator.cellType === "percent")
5055
+ return `${(cell.value * 100).toFixed(indicator.precision ?? 2)}%`;
5056
+ if (indicator.cellType === "money")
5057
+ return cell.value.toLocaleString("zh-CN", {
5058
+ style: "currency",
5059
+ currency: "CNY",
5060
+ minimumFractionDigits: indicator.precision ?? 2,
5061
+ maximumFractionDigits: indicator.precision ?? 2
5062
+ });
5063
+ return cell.formattedValue;
5064
+ }
5065
+ createBodyTextFragments() {
5066
+ return (this.layout?.bodyCells ?? []).map((cell) => ({
5067
+ rowIndex: cell.rowIndex,
5068
+ columnIndex: cell.columnIndex,
5069
+ text: this.resolveBodyText(cell)
5070
+ })).filter((fragment) => fragment.text.length > 0);
5071
+ }
5072
+ resolveTextFragmentIndex(key, fragments) {
5073
+ return fragments.findIndex((fragment) => fragment.key === key);
5074
+ }
5075
+ resolveBodyFragmentIndex(cell) {
5076
+ if (!this.layout) return -1;
5077
+ return this.layout.bodyCells.slice(0, this.layout.bodyCells.indexOf(cell) + 1).filter((item) => this.resolveBodyText(item).length > 0).length - 1;
5078
+ }
5079
+ resolveBodyTextOriginX(cell, text) {
5080
+ const indicator = this.options.indicators.find(
5081
+ (item) => item.key === cell.indicatorKey
5082
+ );
5083
+ const width = this.measurePivotText(text);
5084
+ const align = indicator?.align ?? "right";
5085
+ 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;
5086
+ }
5087
+ measurePivotText(text) {
5088
+ const context = this.host?.canvas.getContext("2d");
5089
+ if (!context) return text.length * 8;
5090
+ context.font = `${this.options.theme?.fontSize ?? 13}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
5091
+ return context.measureText(text).width;
5092
+ }
4651
5093
  updateRowHeaderSelectionDrag(event) {
4652
5094
  const drag = this.rowHeaderSelectionDrag;
4653
5095
  if (!drag || !this.layout || !this.host) return;
@@ -4886,6 +5328,14 @@ var PivotTable = class {
4886
5328
  selection: this.core.getSelection()
4887
5329
  });
4888
5330
  }
5331
+ /** 复制表格选区文本;同步复制失败时回退到异步实现。 */
5332
+ copyPivotText(text) {
5333
+ if (!text) return;
5334
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
5335
+ if (!utils.copyTextSync(text, ownerDocument)) {
5336
+ void utils.copyText(text, ownerDocument);
5337
+ }
5338
+ }
4889
5339
  scheduleRender() {
4890
5340
  if (this.frameId !== null) return;
4891
5341
  this.frameId = requestAnimationFrame(() => {
@@ -4929,12 +5379,6 @@ var PivotTable = class {
4929
5379
  });
4930
5380
  }
4931
5381
  };
4932
- function describeElement(value) {
4933
- if (!(value instanceof Element)) return String(value);
4934
- const id = value.id ? `#${value.id}` : "";
4935
- const className = typeof value.className === "string" && value.className ? `.${value.className.trim().replace(/\s+/g, ".")}` : "";
4936
- return `${value.tagName.toLocaleLowerCase()}${id}${className}`;
4937
- }
4938
5382
  function toCellAddress(cell) {
4939
5383
  return {
4940
5384
  rowNodeId: cell.rowNodeId ?? "",