@canvas-components/pivot-table 0.1.7 → 0.1.8

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
@@ -2592,22 +2592,6 @@ function createPivotDomHost(container) {
2592
2592
  }
2593
2593
  };
2594
2594
  }
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
2595
 
2612
2596
  // src/adapters/canvas-host.ts
2613
2597
  function resizePivotCanvas(canvas, width, height) {
@@ -2972,7 +2956,7 @@ function renderPivotTable(params) {
2972
2956
  });
2973
2957
  withClip(context, layout.cornerRect, () => {
2974
2958
  fillRect(context, layout.cornerRect, theme.cornerBackgroundColor);
2975
- drawCorner(context, layout, theme);
2959
+ drawCorner(context, layout, theme, params.selection?.textSelection);
2976
2960
  });
2977
2961
  withClip(context, layout.rowHeaderRect, () => {
2978
2962
  layout.rowHeaderCells.forEach((cell) => {
@@ -2986,7 +2970,9 @@ function renderPivotTable(params) {
2986
2970
  cell,
2987
2971
  theme,
2988
2972
  !areaSelection && isHeaderSelected(cell, selectionPathKeys.row),
2989
- contentRect
2973
+ contentRect,
2974
+ void 0,
2975
+ params.selection?.textSelection
2990
2976
  );
2991
2977
  });
2992
2978
  layout.rowHeaderCells.forEach(
@@ -3006,7 +2992,10 @@ function renderPivotTable(params) {
3006
2992
  context,
3007
2993
  cell,
3008
2994
  theme,
3009
- !areaSelection && isHeaderSelected(cell, selectionPathKeys.column)
2995
+ !areaSelection && isHeaderSelected(cell, selectionPathKeys.column),
2996
+ cell.rect,
2997
+ void 0,
2998
+ params.selection?.textSelection
3010
2999
  )
3011
3000
  );
3012
3001
  layout.columnHeaderCells.forEach(
@@ -3041,7 +3030,8 @@ function renderPivotTable(params) {
3041
3030
  activeNodePathKey
3042
3031
  )
3043
3032
  },
3044
- visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0
3033
+ visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0,
3034
+ params.selection?.textSelection
3045
3035
  )
3046
3036
  );
3047
3037
  layout.bodyCells.forEach(
@@ -3182,7 +3172,7 @@ function drawReportFilterCell(context, cell, theme) {
3182
3172
  skipRight: true
3183
3173
  });
3184
3174
  }
3185
- function drawCorner(context, layout, theme) {
3175
+ function drawCorner(context, layout, theme, textSelection) {
3186
3176
  layout.rowFieldHeaderCells.forEach(
3187
3177
  (cell) => drawHeaderCell(
3188
3178
  context,
@@ -3190,7 +3180,8 @@ function drawCorner(context, layout, theme) {
3190
3180
  theme,
3191
3181
  false,
3192
3182
  cell.rect,
3193
- theme.cornerBackgroundColor
3183
+ theme.cornerBackgroundColor,
3184
+ textSelection
3194
3185
  )
3195
3186
  );
3196
3187
  if (layout.rowFieldHeaderCells.length === 0) {
@@ -3207,7 +3198,7 @@ function drawCorner(context, layout, theme) {
3207
3198
  })
3208
3199
  );
3209
3200
  }
3210
- function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor) {
3201
+ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor, textSelection) {
3211
3202
  const background = backgroundColor ?? (selected ? theme.selectionBackgroundColor : cell.kind === "grandTotal" ? theme.grandTotalBackgroundColor : cell.kind === "subtotal" ? theme.subtotalBackgroundColor : cell.axis === "row" ? theme.rowHeaderBackgroundColor : theme.columnHeaderBackgroundColor);
3212
3203
  fillRect(context, cell.rect, background);
3213
3204
  const iconPadding = cell.expandable ? 18 : 0;
@@ -3229,6 +3220,13 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3229
3220
  cell.filterable ? 32 : 10
3230
3221
  );
3231
3222
  }
3223
+ drawHeaderTextSelectionBackground(
3224
+ context,
3225
+ cell,
3226
+ contentRect,
3227
+ textSelection,
3228
+ theme.fontSize
3229
+ );
3232
3230
  drawText(
3233
3231
  context,
3234
3232
  cell.label,
@@ -3241,13 +3239,46 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3241
3239
  cell.align ?? "left"
3242
3240
  );
3243
3241
  }
3242
+ function drawHeaderTextSelectionBackground(context, cell, contentRect, selection, fontSize) {
3243
+ if (!selection) return;
3244
+ const index = selection.fragments.findIndex(
3245
+ (fragment) => fragment.region === "header" && fragment.key === `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`
3246
+ );
3247
+ if (index < 0) return;
3248
+ const range = resolveTextSelectionRange(selection, index, cell.label.length);
3249
+ if (!range) return;
3250
+ const width = context.measureText(cell.label).width;
3251
+ const align = cell.align ?? "left";
3252
+ 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);
3253
+ context.save();
3254
+ context.fillStyle = "#99c2ff";
3255
+ context.fillRect(
3256
+ origin + context.measureText(cell.label.slice(0, range.start)).width,
3257
+ contentRect.y + (contentRect.height - fontSize) / 2,
3258
+ context.measureText(cell.label.slice(range.start, range.end)).width,
3259
+ fontSize
3260
+ );
3261
+ context.restore();
3262
+ }
3263
+ function resolveTextSelectionRange(selection, index, length) {
3264
+ const forward = selection.anchorFragmentIndex <= selection.focusFragmentIndex;
3265
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3266
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3267
+ if (index < startIndex || index > endIndex) return null;
3268
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3269
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : length;
3270
+ return {
3271
+ start: Math.max(0, Math.min(start, length)),
3272
+ end: Math.max(0, Math.min(end, length))
3273
+ };
3274
+ }
3244
3275
  function isHeaderSelected(cell, selectedPathKey) {
3245
3276
  if (!selectedPathKey || !cell.nodeId) {
3246
3277
  return false;
3247
3278
  }
3248
3279
  return Boolean(isPathRelated2(selectedPathKey, cell.pathKey));
3249
3280
  }
3250
- function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3281
+ function drawBodyCell(context, cell, options, theme, state, visibleMaximum, textSelection) {
3251
3282
  const indicator = options.indicators.find(
3252
3283
  (item) => item.key === cell.indicatorKey
3253
3284
  );
@@ -3277,15 +3308,54 @@ function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3277
3308
  "#3370ff"
3278
3309
  );
3279
3310
  }
3311
+ const text = resolveBodyDisplayText(cell, indicator);
3312
+ const padding = 10;
3313
+ const align = indicator?.align ?? "right";
3314
+ drawTextSelectionBackground(
3315
+ context,
3316
+ cell,
3317
+ text,
3318
+ align,
3319
+ padding,
3320
+ textSelection,
3321
+ theme.fontSize
3322
+ );
3280
3323
  drawText(
3281
3324
  context,
3282
- resolveBodyDisplayText(cell, indicator),
3325
+ text,
3283
3326
  cell.rect,
3284
3327
  theme.textColor,
3285
3328
  10,
3286
3329
  indicator?.align ?? "right"
3287
3330
  );
3288
3331
  }
3332
+ function drawTextSelectionBackground(context, cell, text, align, padding, selection, fontSize) {
3333
+ if (!selection) return;
3334
+ const index = selection.fragments.findIndex(
3335
+ (fragment) => fragment.rowIndex === cell.rowIndex && fragment.columnIndex === cell.columnIndex && fragment.text === text
3336
+ );
3337
+ if (index < 0) return;
3338
+ const forward = selection.anchorFragmentIndex < selection.focusFragmentIndex || selection.anchorFragmentIndex === selection.focusFragmentIndex && selection.anchorOffset <= selection.focusOffset;
3339
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3340
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3341
+ if (index < startIndex || index > endIndex) return;
3342
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3343
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : text.length;
3344
+ if (start >= end) return;
3345
+ const availableWidth = Math.max(cell.rect.width - padding * 2, 0);
3346
+ const shown = truncateText(context, text, availableWidth);
3347
+ const textWidth = context.measureText(shown).width;
3348
+ 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;
3349
+ context.save();
3350
+ context.fillStyle = "#99c2ff";
3351
+ context.fillRect(
3352
+ origin + context.measureText(shown.slice(0, start)).width,
3353
+ cell.rect.y + (cell.rect.height - fontSize) / 2,
3354
+ context.measureText(shown.slice(start, end)).width,
3355
+ fontSize
3356
+ );
3357
+ context.restore();
3358
+ }
3289
3359
  function resolveSelectionPathKeys(layout, selection, activeNodePathKey) {
3290
3360
  const activeNode = selection?.activeNode;
3291
3361
  if (activeNode) {
@@ -3574,6 +3644,61 @@ function contains(rect, x, y) {
3574
3644
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3575
3645
  }
3576
3646
 
3647
+ // src/interaction/text-selection.ts
3648
+ function resolvePivotTextOffset(params) {
3649
+ const boundaries = [0];
3650
+ let offset = 0;
3651
+ for (const character of params.text) {
3652
+ offset += character.length;
3653
+ boundaries.push(offset);
3654
+ }
3655
+ const relativeX = params.pointerX - params.textOriginX;
3656
+ if (relativeX <= 0 || boundaries.length === 1) return 0;
3657
+ if (relativeX >= params.measureText(params.text)) return params.text.length;
3658
+ let low = 1;
3659
+ let high = boundaries.length - 1;
3660
+ while (low < high) {
3661
+ const middle = Math.floor((low + high) / 2);
3662
+ const boundary = boundaries[middle] ?? params.text.length;
3663
+ if (params.measureText(params.text.slice(0, boundary)) < relativeX) {
3664
+ low = middle + 1;
3665
+ } else {
3666
+ high = middle;
3667
+ }
3668
+ }
3669
+ const end = boundaries[low] ?? params.text.length;
3670
+ const start = boundaries[Math.max(low - 1, 0)] ?? 0;
3671
+ const startWidth = params.measureText(params.text.slice(0, start));
3672
+ const endWidth = params.measureText(params.text.slice(0, end));
3673
+ return relativeX - startWidth <= endWidth - relativeX ? start : end;
3674
+ }
3675
+ function getPivotSelectedText(selection) {
3676
+ const textSelection = selection.textSelection;
3677
+ if (!textSelection) return null;
3678
+ const forward = textSelection.anchorFragmentIndex < textSelection.focusFragmentIndex || textSelection.anchorFragmentIndex === textSelection.focusFragmentIndex && textSelection.anchorOffset <= textSelection.focusOffset;
3679
+ const startIndex = forward ? textSelection.anchorFragmentIndex : textSelection.focusFragmentIndex;
3680
+ const endIndex = forward ? textSelection.focusFragmentIndex : textSelection.anchorFragmentIndex;
3681
+ const startOffset = forward ? textSelection.anchorOffset : textSelection.focusOffset;
3682
+ const endOffset = forward ? textSelection.focusOffset : textSelection.anchorOffset;
3683
+ const selected = textSelection.fragments.slice(startIndex, endIndex + 1).map((fragment, index) => {
3684
+ const absoluteIndex = startIndex + index;
3685
+ const start = absoluteIndex === startIndex ? startOffset : 0;
3686
+ const end = absoluteIndex === endIndex ? endOffset : fragment.text.length;
3687
+ return {
3688
+ fragment,
3689
+ text: fragment.text.slice(
3690
+ Math.max(0, Math.min(start, fragment.text.length)),
3691
+ Math.max(0, Math.min(end, fragment.text.length))
3692
+ )
3693
+ };
3694
+ });
3695
+ return selected.reduce((result, item, index) => {
3696
+ const previous = selected[index - 1]?.fragment;
3697
+ const separator = previous ? previous.rowIndex === item.fragment.rowIndex ? " " : "\n" : "";
3698
+ return `${result}${separator}${item.text}`;
3699
+ }, "") || null;
3700
+ }
3701
+
3577
3702
  // src/features/clipboard/pivot-selection-copy.ts
3578
3703
  function createPivotSelectionText(params) {
3579
3704
  const activeNode = params.selection.activeNode;
@@ -3877,6 +4002,8 @@ var PivotTable = class {
3877
4002
  __publicField(this, "suppressNextClick", false);
3878
4003
  __publicField(this, "scrollbarDrag", null);
3879
4004
  __publicField(this, "cellSelectionDrag", null);
4005
+ __publicField(this, "textSelectionDrag", null);
4006
+ __publicField(this, "headerTextSelectionDrag", null);
3880
4007
  __publicField(this, "rowHeaderSelectionDrag", null);
3881
4008
  __publicField(this, "handlePointerMove", (event) => {
3882
4009
  if (this.scrollbarDrag) {
@@ -3897,10 +4024,22 @@ var PivotTable = class {
3897
4024
  return;
3898
4025
  }
3899
4026
  if (this.cellSelectionDrag?.pointerId === event.pointerId) {
4027
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4028
+ this.updateTextSelectionDrag(event);
4029
+ if (this.textSelectionDrag.dragged) return;
4030
+ }
3900
4031
  this.updateCellSelectionDrag(event);
3901
4032
  return;
3902
4033
  }
4034
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4035
+ this.updateHeaderTextSelectionDrag(event);
4036
+ return;
4037
+ }
3903
4038
  if (this.rowHeaderSelectionDrag?.pointerId === event.pointerId) {
4039
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4040
+ this.updateHeaderTextSelectionDrag(event);
4041
+ if (this.headerTextSelectionDrag.dragged) return;
4042
+ }
3904
4043
  this.updateRowHeaderSelectionDrag(event);
3905
4044
  return;
3906
4045
  }
@@ -3947,6 +4086,7 @@ var PivotTable = class {
3947
4086
  this.openReportFilterPanel(hit.reportFilterCell);
3948
4087
  return;
3949
4088
  }
4089
+ if (hit.reportFilterCell) return;
3950
4090
  if (hit.bodyCell) {
3951
4091
  this.selectCell(toCellAddress(hit.bodyCell), event.shiftKey);
3952
4092
  return;
@@ -3988,52 +4128,20 @@ var PivotTable = class {
3988
4128
  this.scheduleRender();
3989
4129
  return;
3990
4130
  });
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
- );
4131
+ __publicField(this, "handleKeyDown", (event) => {
3997
4132
  const isCopyShortcut = (event.ctrlKey || event.metaKey) && !event.altKey && (event.key.toLocaleLowerCase() === "c" || event.code === "KeyC");
3998
4133
  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
- });
4134
+ const text = getPivotSelectedText(
4135
+ this.core?.getSelection() ?? {
4136
+ }
4137
+ ) ?? this.createSelectionTsv();
4022
4138
  if (!text) {
4023
- logPivotClipboard("copy-aborted", { reason: "empty-selection-text" });
4024
4139
  return;
4025
4140
  }
4026
4141
  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
- );
4142
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
4143
+ if (!copyTextSync(text, ownerDocument)) {
4144
+ void copyText(text, ownerDocument);
4037
4145
  }
4038
4146
  return;
4039
4147
  }
@@ -4046,16 +4154,6 @@ var PivotTable = class {
4046
4154
  event.preventDefault();
4047
4155
  this.selectCell(next, event.shiftKey);
4048
4156
  });
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
4157
  __publicField(this, "handleWheel", (event) => {
4060
4158
  if (!this.layout) return;
4061
4159
  const delta = resolvePivotWheelScrollDelta({
@@ -4127,13 +4225,13 @@ var PivotTable = class {
4127
4225
  endColumnIndex: hit.bodyCell.columnIndex
4128
4226
  }) : "";
4129
4227
  if (text) {
4130
- void writePivotClipboardText(text, this.container.ownerDocument);
4228
+ this.copyPivotText(text);
4131
4229
  }
4132
4230
  },
4133
4231
  onCopySelection: selectionRange ? () => {
4134
4232
  const text = this.createSelectionTsv();
4135
4233
  if (text) {
4136
- void writePivotClipboardText(text, this.container.ownerDocument);
4234
+ this.copyPivotText(text);
4137
4235
  }
4138
4236
  } : void 0,
4139
4237
  customSelection: {
@@ -4156,7 +4254,7 @@ var PivotTable = class {
4156
4254
  endColumnIndex: range.endColumn - 1
4157
4255
  });
4158
4256
  if (text) {
4159
- void writePivotClipboardText(text, this.container.ownerDocument);
4257
+ this.copyPivotText(text);
4160
4258
  }
4161
4259
  },
4162
4260
  onExportCsv: () => downloadBlob(
@@ -4188,6 +4286,24 @@ var PivotTable = class {
4188
4286
  const address = toCellAddress(cellHit.bodyCell);
4189
4287
  this.host.root.focus({ preventScroll: true });
4190
4288
  const selection = this.selectCell(address, event.shiftKey);
4289
+ const text = this.resolveBodyText(cellHit.bodyCell);
4290
+ const textOriginX = this.resolveBodyTextOriginX(cellHit.bodyCell, text);
4291
+ const anchorOffset = text ? resolvePivotTextOffset({
4292
+ text,
4293
+ pointerX: event.clientX - this.host.root.getBoundingClientRect().left,
4294
+ textOriginX,
4295
+ measureText: (value) => this.measurePivotText(value)
4296
+ }) : 0;
4297
+ const anchorFragmentIndex = this.resolveTextFragmentIndex(
4298
+ `body:${cellHit.bodyCell.rowIndex}:${cellHit.bodyCell.columnIndex}`,
4299
+ this.createTextSelectionFragments("column")
4300
+ );
4301
+ this.textSelectionDrag = text ? {
4302
+ pointerId: event.pointerId,
4303
+ anchorFragmentIndex,
4304
+ anchorOffset,
4305
+ dragged: false
4306
+ } : null;
4191
4307
  this.cellSelectionDrag = {
4192
4308
  pointerId: event.pointerId,
4193
4309
  anchor: selection?.anchorCell ?? address,
@@ -4199,6 +4315,37 @@ var PivotTable = class {
4199
4315
  this.suppressNextClick = true;
4200
4316
  return;
4201
4317
  }
4318
+ if (cellHit?.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4319
+ const header = cellHit.headerCell;
4320
+ const text = header.label;
4321
+ const rootRect2 = this.host.root.getBoundingClientRect();
4322
+ const originX = header.rect.x + 10 + (header.expandable ? 18 : 0);
4323
+ const fragments = this.createTextSelectionFragments(header.axis);
4324
+ const key = `header:${header.axis}:${header.nodeId ?? ""}:${header.label}`;
4325
+ const anchorFragmentIndex = fragments.findIndex(
4326
+ (fragment) => fragment.key === key
4327
+ );
4328
+ this.host.root.focus({ preventScroll: true });
4329
+ if (header.nodeId) {
4330
+ this.selectNode(header.axis, header.nodeId);
4331
+ }
4332
+ this.textSelectionDrag = null;
4333
+ this.headerTextSelectionDrag = {
4334
+ pointerId: event.pointerId,
4335
+ fragments,
4336
+ anchorFragmentIndex: Math.max(anchorFragmentIndex, 0),
4337
+ anchorOffset: resolvePivotTextOffset({
4338
+ text,
4339
+ pointerX: event.clientX - rootRect2.left,
4340
+ textOriginX: originX,
4341
+ measureText: (value) => this.measurePivotText(value)
4342
+ }),
4343
+ dragged: false
4344
+ };
4345
+ event.preventDefault();
4346
+ this.host.root.setPointerCapture(event.pointerId);
4347
+ return;
4348
+ }
4202
4349
  if (cellHit?.region === "rowHeader" && cellHit.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4203
4350
  const rootRect2 = this.host.root.getBoundingClientRect();
4204
4351
  const rowIndex = resolvePivotRowIndexAtY(
@@ -4248,6 +4395,14 @@ var PivotTable = class {
4248
4395
  if (!this.host) return;
4249
4396
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4250
4397
  if (endedCellSelection) this.cellSelectionDrag = null;
4398
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4399
+ if (this.textSelectionDrag.dragged) this.suppressNextClick = true;
4400
+ this.textSelectionDrag = null;
4401
+ }
4402
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4403
+ if (this.headerTextSelectionDrag.dragged) this.suppressNextClick = true;
4404
+ this.headerTextSelectionDrag = null;
4405
+ }
4251
4406
  const endedRowHeaderSelection = this.rowHeaderSelectionDrag?.pointerId === event.pointerId;
4252
4407
  if (endedRowHeaderSelection) {
4253
4408
  if (this.rowHeaderSelectionDrag?.dragged) this.suppressNextClick = true;
@@ -4279,12 +4434,7 @@ var PivotTable = class {
4279
4434
  this.host.root.addEventListener("pointermove", this.handlePointerMove);
4280
4435
  this.host.root.addEventListener("pointerleave", this.handlePointerLeave);
4281
4436
  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);
4437
+ this.host.root.addEventListener("keydown", this.handleKeyDown);
4288
4438
  this.host.root.addEventListener("wheel", this.handleWheel, {
4289
4439
  passive: false
4290
4440
  });
@@ -4510,12 +4660,7 @@ var PivotTable = class {
4510
4660
  this.handlePointerLeave
4511
4661
  );
4512
4662
  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);
4663
+ this.host?.root.removeEventListener("keydown", this.handleKeyDown);
4519
4664
  this.host?.root.removeEventListener("wheel", this.handleWheel);
4520
4665
  this.host?.root.removeEventListener("contextmenu", this.handleContextMenu);
4521
4666
  this.host?.root.removeEventListener("pointerdown", this.handlePointerDown);
@@ -4627,6 +4772,194 @@ var PivotTable = class {
4627
4772
  activeNode: null
4628
4773
  });
4629
4774
  }
4775
+ updateTextSelectionDrag(event) {
4776
+ const drag = this.textSelectionDrag;
4777
+ if (!drag || !this.layout || !this.host) return;
4778
+ const rootRect = this.host.root.getBoundingClientRect();
4779
+ const x = event.clientX - rootRect.left;
4780
+ const y = event.clientY - rootRect.top;
4781
+ const hitResult = hitTestPivotLayout(this.layout, x, y);
4782
+ const hit = hitResult?.bodyCell;
4783
+ const header = hitResult?.headerCell;
4784
+ if (!hit && !header) return;
4785
+ const fragments = this.createTextSelectionFragments(
4786
+ header?.axis ?? "column"
4787
+ );
4788
+ const focusKey = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${hit?.rowIndex}:${hit?.columnIndex}`;
4789
+ const focusIndex = this.resolveTextFragmentIndex(focusKey, fragments);
4790
+ const text = header ? header.label : this.resolveBodyText(hit);
4791
+ if (!text || focusIndex < 0) return;
4792
+ const textOriginX = header ? header.rect.x + 10 + (header.expandable ? 18 : 0) : this.resolveBodyTextOriginX(hit, text);
4793
+ const focusOffset = resolvePivotTextOffset({
4794
+ text,
4795
+ pointerX: x,
4796
+ textOriginX,
4797
+ measureText: (value) => this.measurePivotText(value)
4798
+ });
4799
+ if (focusIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4800
+ return;
4801
+ drag.dragged = true;
4802
+ this.setSelection({
4803
+ activeCell: hit ? toCellAddress(hit) : this.core?.getSelection().activeCell ?? null,
4804
+ anchorCell: this.core?.getSelection().anchorCell ?? (hit ? toCellAddress(hit) : null),
4805
+ ranges: this.core?.getSelection().ranges ?? [],
4806
+ activeNode: null,
4807
+ textSelection: {
4808
+ fragments: fragments.map((fragment) => ({
4809
+ rowIndex: fragment.rowIndex ?? -1,
4810
+ columnIndex: fragment.columnIndex ?? -1,
4811
+ text: fragment.text,
4812
+ region: fragment.key.startsWith("header:") ? "header" : "body",
4813
+ key: fragment.key
4814
+ })),
4815
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4816
+ focusFragmentIndex: focusIndex,
4817
+ anchorOffset: drag.anchorOffset,
4818
+ focusOffset
4819
+ }
4820
+ });
4821
+ }
4822
+ updateHeaderTextSelectionDrag(event) {
4823
+ const drag = this.headerTextSelectionDrag;
4824
+ if (!drag || !this.host || !this.layout) return;
4825
+ const rootRect = this.host.root.getBoundingClientRect();
4826
+ const localX = event.clientX - rootRect.left;
4827
+ const localY = event.clientY - rootRect.top;
4828
+ const hit = hitTestPivotLayout(this.layout, localX, localY);
4829
+ const header = hit?.headerCell;
4830
+ const body = hit?.bodyCell;
4831
+ if (!header && !body) return;
4832
+ const key = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${body?.rowIndex}:${body?.columnIndex}`;
4833
+ const focusFragmentIndex = drag.fragments.findIndex(
4834
+ (fragment) => fragment.key === key
4835
+ );
4836
+ if (focusFragmentIndex < 0) return;
4837
+ const focusFragment = drag.fragments[focusFragmentIndex];
4838
+ const focusOffset = resolvePivotTextOffset({
4839
+ text: focusFragment.text,
4840
+ pointerX: localX,
4841
+ textOriginX: focusFragment.originX,
4842
+ measureText: (value) => this.measurePivotText(value)
4843
+ });
4844
+ if (focusFragmentIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4845
+ return;
4846
+ drag.dragged = true;
4847
+ const selection = this.core?.getSelection();
4848
+ if (!selection) return;
4849
+ this.setSelection({
4850
+ ...selection,
4851
+ textSelection: {
4852
+ fragments: drag.fragments.map((fragment) => ({
4853
+ rowIndex: fragment.rowIndex ?? -1,
4854
+ columnIndex: fragment.columnIndex ?? -1,
4855
+ text: fragment.text,
4856
+ region: "header",
4857
+ key: fragment.key
4858
+ })),
4859
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4860
+ focusFragmentIndex,
4861
+ anchorOffset: drag.anchorOffset,
4862
+ focusOffset
4863
+ }
4864
+ });
4865
+ }
4866
+ createTextSelectionFragments(axis) {
4867
+ const bodyFragments = (this.layout?.bodyCells ?? []).map((cell) => ({
4868
+ key: `body:${cell.rowIndex}:${cell.columnIndex}`,
4869
+ text: this.resolveBodyText(cell),
4870
+ originX: this.resolveBodyTextOriginX(cell, this.resolveBodyText(cell)),
4871
+ rowIndex: cell.rowIndex,
4872
+ columnIndex: cell.columnIndex
4873
+ })).filter((fragment) => fragment.text.length > 0);
4874
+ if (this.layout) {
4875
+ const columnHeaders = this.createHeaderTextFragments("column");
4876
+ const rowHeaders = this.layout.rowHeaderCells.filter((cell) => cell.label).map((cell) => ({
4877
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4878
+ text: cell.label,
4879
+ rowIndex: void 0,
4880
+ columnIndex: void 0,
4881
+ originX: cell.rect.x + 10
4882
+ }));
4883
+ const rows = Array.from(
4884
+ { length: this.layout.rowSlots.length },
4885
+ (_, rowIndex) => [
4886
+ rowHeaders[rowIndex],
4887
+ ...bodyFragments.filter((fragment) => fragment.rowIndex === rowIndex)
4888
+ ]
4889
+ ).flat();
4890
+ if (axis === "column") {
4891
+ return [...columnHeaders, ...rows].filter(
4892
+ (fragment) => Boolean(fragment)
4893
+ );
4894
+ }
4895
+ const rowHeadersTitle = this.layout.rowFieldHeaderCells.filter((cell) => cell.label).map((cell) => ({
4896
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4897
+ text: cell.label,
4898
+ rowIndex: void 0,
4899
+ columnIndex: void 0,
4900
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
4901
+ }));
4902
+ return [...rowHeadersTitle, ...columnHeaders, ...rows].filter(
4903
+ (fragment) => Boolean(fragment)
4904
+ );
4905
+ }
4906
+ return [...this.createHeaderTextFragments(axis), ...bodyFragments];
4907
+ }
4908
+ createHeaderTextFragments(axis) {
4909
+ if (!this.layout) return [];
4910
+ return (axis === "column" ? this.layout.columnHeaderCells : [...this.layout.rowHeaderCells, ...this.layout.rowFieldHeaderCells]).filter((cell) => cell.label).map((cell) => ({
4911
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4912
+ text: cell.label,
4913
+ rowIndex: void 0,
4914
+ columnIndex: void 0,
4915
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
4916
+ }));
4917
+ }
4918
+ resolveBodyText(cell) {
4919
+ const indicator = this.options.indicators.find(
4920
+ (item) => item.key === cell.indicatorKey
4921
+ );
4922
+ if (!indicator || indicator.formatter || typeof cell.value !== "number")
4923
+ return cell.formattedValue;
4924
+ if (indicator.cellType === "percent")
4925
+ return `${(cell.value * 100).toFixed(indicator.precision ?? 2)}%`;
4926
+ if (indicator.cellType === "money")
4927
+ return cell.value.toLocaleString("zh-CN", {
4928
+ style: "currency",
4929
+ currency: "CNY",
4930
+ minimumFractionDigits: indicator.precision ?? 2,
4931
+ maximumFractionDigits: indicator.precision ?? 2
4932
+ });
4933
+ return cell.formattedValue;
4934
+ }
4935
+ createBodyTextFragments() {
4936
+ return (this.layout?.bodyCells ?? []).map((cell) => ({
4937
+ rowIndex: cell.rowIndex,
4938
+ columnIndex: cell.columnIndex,
4939
+ text: this.resolveBodyText(cell)
4940
+ })).filter((fragment) => fragment.text.length > 0);
4941
+ }
4942
+ resolveTextFragmentIndex(key, fragments) {
4943
+ return fragments.findIndex((fragment) => fragment.key === key);
4944
+ }
4945
+ resolveBodyFragmentIndex(cell) {
4946
+ if (!this.layout) return -1;
4947
+ return this.layout.bodyCells.slice(0, this.layout.bodyCells.indexOf(cell) + 1).filter((item) => this.resolveBodyText(item).length > 0).length - 1;
4948
+ }
4949
+ resolveBodyTextOriginX(cell, text) {
4950
+ const indicator = this.options.indicators.find(
4951
+ (item) => item.key === cell.indicatorKey
4952
+ );
4953
+ const width = this.measurePivotText(text);
4954
+ const align = indicator?.align ?? "right";
4955
+ 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;
4956
+ }
4957
+ measurePivotText(text) {
4958
+ const context = this.host?.canvas.getContext("2d");
4959
+ if (!context) return text.length * 8;
4960
+ context.font = `${this.options.theme?.fontSize ?? 13}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
4961
+ return context.measureText(text).width;
4962
+ }
4630
4963
  updateRowHeaderSelectionDrag(event) {
4631
4964
  const drag = this.rowHeaderSelectionDrag;
4632
4965
  if (!drag || !this.layout || !this.host) return;
@@ -4865,6 +5198,14 @@ var PivotTable = class {
4865
5198
  selection: this.core.getSelection()
4866
5199
  });
4867
5200
  }
5201
+ /** 复制表格选区文本;同步复制失败时回退到异步实现。 */
5202
+ copyPivotText(text) {
5203
+ if (!text) return;
5204
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
5205
+ if (!copyTextSync(text, ownerDocument)) {
5206
+ void copyText(text, ownerDocument);
5207
+ }
5208
+ }
4868
5209
  scheduleRender() {
4869
5210
  if (this.frameId !== null) return;
4870
5211
  this.frameId = requestAnimationFrame(() => {
@@ -4908,12 +5249,6 @@ var PivotTable = class {
4908
5249
  });
4909
5250
  }
4910
5251
  };
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
5252
  function toCellAddress(cell) {
4918
5253
  return {
4919
5254
  rowNodeId: cell.rowNodeId ?? "",