@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.cjs CHANGED
@@ -2613,22 +2613,6 @@ function createPivotDomHost(container) {
2613
2613
  }
2614
2614
  };
2615
2615
  }
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
2616
 
2633
2617
  // src/adapters/canvas-host.ts
2634
2618
  function resizePivotCanvas(canvas, width, height) {
@@ -2993,7 +2977,7 @@ function renderPivotTable(params) {
2993
2977
  });
2994
2978
  withClip(context, layout.cornerRect, () => {
2995
2979
  fillRect(context, layout.cornerRect, theme.cornerBackgroundColor);
2996
- drawCorner(context, layout, theme);
2980
+ drawCorner(context, layout, theme, params.selection?.textSelection);
2997
2981
  });
2998
2982
  withClip(context, layout.rowHeaderRect, () => {
2999
2983
  layout.rowHeaderCells.forEach((cell) => {
@@ -3007,7 +2991,9 @@ function renderPivotTable(params) {
3007
2991
  cell,
3008
2992
  theme,
3009
2993
  !areaSelection && isHeaderSelected(cell, selectionPathKeys.row),
3010
- contentRect
2994
+ contentRect,
2995
+ void 0,
2996
+ params.selection?.textSelection
3011
2997
  );
3012
2998
  });
3013
2999
  layout.rowHeaderCells.forEach(
@@ -3027,7 +3013,10 @@ function renderPivotTable(params) {
3027
3013
  context,
3028
3014
  cell,
3029
3015
  theme,
3030
- !areaSelection && isHeaderSelected(cell, selectionPathKeys.column)
3016
+ !areaSelection && isHeaderSelected(cell, selectionPathKeys.column),
3017
+ cell.rect,
3018
+ void 0,
3019
+ params.selection?.textSelection
3031
3020
  )
3032
3021
  );
3033
3022
  layout.columnHeaderCells.forEach(
@@ -3062,7 +3051,8 @@ function renderPivotTable(params) {
3062
3051
  activeNodePathKey
3063
3052
  )
3064
3053
  },
3065
- visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0
3054
+ visibleIndicatorMaxima.get(cell.indicatorKey) ?? 0,
3055
+ params.selection?.textSelection
3066
3056
  )
3067
3057
  );
3068
3058
  layout.bodyCells.forEach(
@@ -3203,7 +3193,7 @@ function drawReportFilterCell(context, cell, theme) {
3203
3193
  skipRight: true
3204
3194
  });
3205
3195
  }
3206
- function drawCorner(context, layout, theme) {
3196
+ function drawCorner(context, layout, theme, textSelection) {
3207
3197
  layout.rowFieldHeaderCells.forEach(
3208
3198
  (cell) => drawHeaderCell(
3209
3199
  context,
@@ -3211,7 +3201,8 @@ function drawCorner(context, layout, theme) {
3211
3201
  theme,
3212
3202
  false,
3213
3203
  cell.rect,
3214
- theme.cornerBackgroundColor
3204
+ theme.cornerBackgroundColor,
3205
+ textSelection
3215
3206
  )
3216
3207
  );
3217
3208
  if (layout.rowFieldHeaderCells.length === 0) {
@@ -3228,7 +3219,7 @@ function drawCorner(context, layout, theme) {
3228
3219
  })
3229
3220
  );
3230
3221
  }
3231
- function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor) {
3222
+ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect, backgroundColor, textSelection) {
3232
3223
  const background = backgroundColor ?? (selected ? theme.selectionBackgroundColor : cell.kind === "grandTotal" ? theme.grandTotalBackgroundColor : cell.kind === "subtotal" ? theme.subtotalBackgroundColor : cell.axis === "row" ? theme.rowHeaderBackgroundColor : theme.columnHeaderBackgroundColor);
3233
3224
  fillRect(context, cell.rect, background);
3234
3225
  const iconPadding = cell.expandable ? 18 : 0;
@@ -3250,6 +3241,13 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3250
3241
  cell.filterable ? 32 : 10
3251
3242
  );
3252
3243
  }
3244
+ drawHeaderTextSelectionBackground(
3245
+ context,
3246
+ cell,
3247
+ contentRect,
3248
+ textSelection,
3249
+ theme.fontSize
3250
+ );
3253
3251
  drawText(
3254
3252
  context,
3255
3253
  cell.label,
@@ -3262,13 +3260,46 @@ function drawHeaderCell(context, cell, theme, selected, contentRect = cell.rect,
3262
3260
  cell.align ?? "left"
3263
3261
  );
3264
3262
  }
3263
+ function drawHeaderTextSelectionBackground(context, cell, contentRect, selection, fontSize) {
3264
+ if (!selection) return;
3265
+ const index = selection.fragments.findIndex(
3266
+ (fragment) => fragment.region === "header" && fragment.key === `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`
3267
+ );
3268
+ if (index < 0) return;
3269
+ const range = resolveTextSelectionRange(selection, index, cell.label.length);
3270
+ if (!range) return;
3271
+ const width = context.measureText(cell.label).width;
3272
+ const align = cell.align ?? "left";
3273
+ 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);
3274
+ context.save();
3275
+ context.fillStyle = "#99c2ff";
3276
+ context.fillRect(
3277
+ origin + context.measureText(cell.label.slice(0, range.start)).width,
3278
+ contentRect.y + (contentRect.height - fontSize) / 2,
3279
+ context.measureText(cell.label.slice(range.start, range.end)).width,
3280
+ fontSize
3281
+ );
3282
+ context.restore();
3283
+ }
3284
+ function resolveTextSelectionRange(selection, index, length) {
3285
+ const forward = selection.anchorFragmentIndex <= selection.focusFragmentIndex;
3286
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3287
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3288
+ if (index < startIndex || index > endIndex) return null;
3289
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3290
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : length;
3291
+ return {
3292
+ start: Math.max(0, Math.min(start, length)),
3293
+ end: Math.max(0, Math.min(end, length))
3294
+ };
3295
+ }
3265
3296
  function isHeaderSelected(cell, selectedPathKey) {
3266
3297
  if (!selectedPathKey || !cell.nodeId) {
3267
3298
  return false;
3268
3299
  }
3269
3300
  return Boolean(isPathRelated2(selectedPathKey, cell.pathKey));
3270
3301
  }
3271
- function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3302
+ function drawBodyCell(context, cell, options, theme, state, visibleMaximum, textSelection) {
3272
3303
  const indicator = options.indicators.find(
3273
3304
  (item) => item.key === cell.indicatorKey
3274
3305
  );
@@ -3298,15 +3329,54 @@ function drawBodyCell(context, cell, options, theme, state, visibleMaximum) {
3298
3329
  "#3370ff"
3299
3330
  );
3300
3331
  }
3332
+ const text = resolveBodyDisplayText(cell, indicator);
3333
+ const padding = 10;
3334
+ const align = indicator?.align ?? "right";
3335
+ drawTextSelectionBackground(
3336
+ context,
3337
+ cell,
3338
+ text,
3339
+ align,
3340
+ padding,
3341
+ textSelection,
3342
+ theme.fontSize
3343
+ );
3301
3344
  drawText(
3302
3345
  context,
3303
- resolveBodyDisplayText(cell, indicator),
3346
+ text,
3304
3347
  cell.rect,
3305
3348
  theme.textColor,
3306
3349
  10,
3307
3350
  indicator?.align ?? "right"
3308
3351
  );
3309
3352
  }
3353
+ function drawTextSelectionBackground(context, cell, text, align, padding, selection, fontSize) {
3354
+ if (!selection) return;
3355
+ const index = selection.fragments.findIndex(
3356
+ (fragment) => fragment.rowIndex === cell.rowIndex && fragment.columnIndex === cell.columnIndex && fragment.text === text
3357
+ );
3358
+ if (index < 0) return;
3359
+ const forward = selection.anchorFragmentIndex < selection.focusFragmentIndex || selection.anchorFragmentIndex === selection.focusFragmentIndex && selection.anchorOffset <= selection.focusOffset;
3360
+ const startIndex = forward ? selection.anchorFragmentIndex : selection.focusFragmentIndex;
3361
+ const endIndex = forward ? selection.focusFragmentIndex : selection.anchorFragmentIndex;
3362
+ if (index < startIndex || index > endIndex) return;
3363
+ const start = index === startIndex ? forward ? selection.anchorOffset : selection.focusOffset : 0;
3364
+ const end = index === endIndex ? forward ? selection.focusOffset : selection.anchorOffset : text.length;
3365
+ if (start >= end) return;
3366
+ const availableWidth = Math.max(cell.rect.width - padding * 2, 0);
3367
+ const shown = truncateText(context, text, availableWidth);
3368
+ const textWidth = context.measureText(shown).width;
3369
+ 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;
3370
+ context.save();
3371
+ context.fillStyle = "#99c2ff";
3372
+ context.fillRect(
3373
+ origin + context.measureText(shown.slice(0, start)).width,
3374
+ cell.rect.y + (cell.rect.height - fontSize) / 2,
3375
+ context.measureText(shown.slice(start, end)).width,
3376
+ fontSize
3377
+ );
3378
+ context.restore();
3379
+ }
3310
3380
  function resolveSelectionPathKeys(layout, selection, activeNodePathKey) {
3311
3381
  const activeNode = selection?.activeNode;
3312
3382
  if (activeNode) {
@@ -3595,6 +3665,61 @@ function contains(rect, x, y) {
3595
3665
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3596
3666
  }
3597
3667
 
3668
+ // src/interaction/text-selection.ts
3669
+ function resolvePivotTextOffset(params) {
3670
+ const boundaries = [0];
3671
+ let offset = 0;
3672
+ for (const character of params.text) {
3673
+ offset += character.length;
3674
+ boundaries.push(offset);
3675
+ }
3676
+ const relativeX = params.pointerX - params.textOriginX;
3677
+ if (relativeX <= 0 || boundaries.length === 1) return 0;
3678
+ if (relativeX >= params.measureText(params.text)) return params.text.length;
3679
+ let low = 1;
3680
+ let high = boundaries.length - 1;
3681
+ while (low < high) {
3682
+ const middle = Math.floor((low + high) / 2);
3683
+ const boundary = boundaries[middle] ?? params.text.length;
3684
+ if (params.measureText(params.text.slice(0, boundary)) < relativeX) {
3685
+ low = middle + 1;
3686
+ } else {
3687
+ high = middle;
3688
+ }
3689
+ }
3690
+ const end = boundaries[low] ?? params.text.length;
3691
+ const start = boundaries[Math.max(low - 1, 0)] ?? 0;
3692
+ const startWidth = params.measureText(params.text.slice(0, start));
3693
+ const endWidth = params.measureText(params.text.slice(0, end));
3694
+ return relativeX - startWidth <= endWidth - relativeX ? start : end;
3695
+ }
3696
+ function getPivotSelectedText(selection) {
3697
+ const textSelection = selection.textSelection;
3698
+ if (!textSelection) return null;
3699
+ const forward = textSelection.anchorFragmentIndex < textSelection.focusFragmentIndex || textSelection.anchorFragmentIndex === textSelection.focusFragmentIndex && textSelection.anchorOffset <= textSelection.focusOffset;
3700
+ const startIndex = forward ? textSelection.anchorFragmentIndex : textSelection.focusFragmentIndex;
3701
+ const endIndex = forward ? textSelection.focusFragmentIndex : textSelection.anchorFragmentIndex;
3702
+ const startOffset = forward ? textSelection.anchorOffset : textSelection.focusOffset;
3703
+ const endOffset = forward ? textSelection.focusOffset : textSelection.anchorOffset;
3704
+ const selected = textSelection.fragments.slice(startIndex, endIndex + 1).map((fragment, index) => {
3705
+ const absoluteIndex = startIndex + index;
3706
+ const start = absoluteIndex === startIndex ? startOffset : 0;
3707
+ const end = absoluteIndex === endIndex ? endOffset : fragment.text.length;
3708
+ return {
3709
+ fragment,
3710
+ text: fragment.text.slice(
3711
+ Math.max(0, Math.min(start, fragment.text.length)),
3712
+ Math.max(0, Math.min(end, fragment.text.length))
3713
+ )
3714
+ };
3715
+ });
3716
+ return selected.reduce((result, item, index) => {
3717
+ const previous = selected[index - 1]?.fragment;
3718
+ const separator = previous ? previous.rowIndex === item.fragment.rowIndex ? " " : "\n" : "";
3719
+ return `${result}${separator}${item.text}`;
3720
+ }, "") || null;
3721
+ }
3722
+
3598
3723
  // src/features/clipboard/pivot-selection-copy.ts
3599
3724
  function createPivotSelectionText(params) {
3600
3725
  const activeNode = params.selection.activeNode;
@@ -3898,6 +4023,8 @@ var PivotTable = class {
3898
4023
  __publicField(this, "suppressNextClick", false);
3899
4024
  __publicField(this, "scrollbarDrag", null);
3900
4025
  __publicField(this, "cellSelectionDrag", null);
4026
+ __publicField(this, "textSelectionDrag", null);
4027
+ __publicField(this, "headerTextSelectionDrag", null);
3901
4028
  __publicField(this, "rowHeaderSelectionDrag", null);
3902
4029
  __publicField(this, "handlePointerMove", (event) => {
3903
4030
  if (this.scrollbarDrag) {
@@ -3918,10 +4045,22 @@ var PivotTable = class {
3918
4045
  return;
3919
4046
  }
3920
4047
  if (this.cellSelectionDrag?.pointerId === event.pointerId) {
4048
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4049
+ this.updateTextSelectionDrag(event);
4050
+ if (this.textSelectionDrag.dragged) return;
4051
+ }
3921
4052
  this.updateCellSelectionDrag(event);
3922
4053
  return;
3923
4054
  }
4055
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4056
+ this.updateHeaderTextSelectionDrag(event);
4057
+ return;
4058
+ }
3924
4059
  if (this.rowHeaderSelectionDrag?.pointerId === event.pointerId) {
4060
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4061
+ this.updateHeaderTextSelectionDrag(event);
4062
+ if (this.headerTextSelectionDrag.dragged) return;
4063
+ }
3925
4064
  this.updateRowHeaderSelectionDrag(event);
3926
4065
  return;
3927
4066
  }
@@ -3968,6 +4107,7 @@ var PivotTable = class {
3968
4107
  this.openReportFilterPanel(hit.reportFilterCell);
3969
4108
  return;
3970
4109
  }
4110
+ if (hit.reportFilterCell) return;
3971
4111
  if (hit.bodyCell) {
3972
4112
  this.selectCell(toCellAddress(hit.bodyCell), event.shiftKey);
3973
4113
  return;
@@ -4009,52 +4149,20 @@ var PivotTable = class {
4009
4149
  this.scheduleRender();
4010
4150
  return;
4011
4151
  });
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
- );
4152
+ __publicField(this, "handleKeyDown", (event) => {
4018
4153
  const isCopyShortcut = (event.ctrlKey || event.metaKey) && !event.altKey && (event.key.toLocaleLowerCase() === "c" || event.code === "KeyC");
4019
4154
  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
- });
4155
+ const text = getPivotSelectedText(
4156
+ this.core?.getSelection() ?? {
4157
+ }
4158
+ ) ?? this.createSelectionTsv();
4043
4159
  if (!text) {
4044
- logPivotClipboard("copy-aborted", { reason: "empty-selection-text" });
4045
4160
  return;
4046
4161
  }
4047
4162
  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
- );
4163
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
4164
+ if (!utils.copyTextSync(text, ownerDocument)) {
4165
+ void utils.copyText(text, ownerDocument);
4058
4166
  }
4059
4167
  return;
4060
4168
  }
@@ -4067,16 +4175,6 @@ var PivotTable = class {
4067
4175
  event.preventDefault();
4068
4176
  this.selectCell(next, event.shiftKey);
4069
4177
  });
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
4178
  __publicField(this, "handleWheel", (event) => {
4081
4179
  if (!this.layout) return;
4082
4180
  const delta = resolvePivotWheelScrollDelta({
@@ -4148,13 +4246,13 @@ var PivotTable = class {
4148
4246
  endColumnIndex: hit.bodyCell.columnIndex
4149
4247
  }) : "";
4150
4248
  if (text) {
4151
- void writePivotClipboardText(text, this.container.ownerDocument);
4249
+ this.copyPivotText(text);
4152
4250
  }
4153
4251
  },
4154
4252
  onCopySelection: selectionRange ? () => {
4155
4253
  const text = this.createSelectionTsv();
4156
4254
  if (text) {
4157
- void writePivotClipboardText(text, this.container.ownerDocument);
4255
+ this.copyPivotText(text);
4158
4256
  }
4159
4257
  } : void 0,
4160
4258
  customSelection: {
@@ -4177,7 +4275,7 @@ var PivotTable = class {
4177
4275
  endColumnIndex: range.endColumn - 1
4178
4276
  });
4179
4277
  if (text) {
4180
- void writePivotClipboardText(text, this.container.ownerDocument);
4278
+ this.copyPivotText(text);
4181
4279
  }
4182
4280
  },
4183
4281
  onExportCsv: () => downloadBlob(
@@ -4209,6 +4307,24 @@ var PivotTable = class {
4209
4307
  const address = toCellAddress(cellHit.bodyCell);
4210
4308
  this.host.root.focus({ preventScroll: true });
4211
4309
  const selection = this.selectCell(address, event.shiftKey);
4310
+ const text = this.resolveBodyText(cellHit.bodyCell);
4311
+ const textOriginX = this.resolveBodyTextOriginX(cellHit.bodyCell, text);
4312
+ const anchorOffset = text ? resolvePivotTextOffset({
4313
+ text,
4314
+ pointerX: event.clientX - this.host.root.getBoundingClientRect().left,
4315
+ textOriginX,
4316
+ measureText: (value) => this.measurePivotText(value)
4317
+ }) : 0;
4318
+ const anchorFragmentIndex = this.resolveTextFragmentIndex(
4319
+ `body:${cellHit.bodyCell.rowIndex}:${cellHit.bodyCell.columnIndex}`,
4320
+ this.createTextSelectionFragments("column")
4321
+ );
4322
+ this.textSelectionDrag = text ? {
4323
+ pointerId: event.pointerId,
4324
+ anchorFragmentIndex,
4325
+ anchorOffset,
4326
+ dragged: false
4327
+ } : null;
4212
4328
  this.cellSelectionDrag = {
4213
4329
  pointerId: event.pointerId,
4214
4330
  anchor: selection?.anchorCell ?? address,
@@ -4220,6 +4336,37 @@ var PivotTable = class {
4220
4336
  this.suppressNextClick = true;
4221
4337
  return;
4222
4338
  }
4339
+ if (cellHit?.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4340
+ const header = cellHit.headerCell;
4341
+ const text = header.label;
4342
+ const rootRect2 = this.host.root.getBoundingClientRect();
4343
+ const originX = header.rect.x + 10 + (header.expandable ? 18 : 0);
4344
+ const fragments = this.createTextSelectionFragments(header.axis);
4345
+ const key = `header:${header.axis}:${header.nodeId ?? ""}:${header.label}`;
4346
+ const anchorFragmentIndex = fragments.findIndex(
4347
+ (fragment) => fragment.key === key
4348
+ );
4349
+ this.host.root.focus({ preventScroll: true });
4350
+ if (header.nodeId) {
4351
+ this.selectNode(header.axis, header.nodeId);
4352
+ }
4353
+ this.textSelectionDrag = null;
4354
+ this.headerTextSelectionDrag = {
4355
+ pointerId: event.pointerId,
4356
+ fragments,
4357
+ anchorFragmentIndex: Math.max(anchorFragmentIndex, 0),
4358
+ anchorOffset: resolvePivotTextOffset({
4359
+ text,
4360
+ pointerX: event.clientX - rootRect2.left,
4361
+ textOriginX: originX,
4362
+ measureText: (value) => this.measurePivotText(value)
4363
+ }),
4364
+ dragged: false
4365
+ };
4366
+ event.preventDefault();
4367
+ this.host.root.setPointerCapture(event.pointerId);
4368
+ return;
4369
+ }
4223
4370
  if (cellHit?.region === "rowHeader" && cellHit.headerCell && !cellHit.onExpandIcon && !cellHit.onFilterIcon && !cellHit.onSortIcon) {
4224
4371
  const rootRect2 = this.host.root.getBoundingClientRect();
4225
4372
  const rowIndex = resolvePivotRowIndexAtY(
@@ -4269,6 +4416,14 @@ var PivotTable = class {
4269
4416
  if (!this.host) return;
4270
4417
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4271
4418
  if (endedCellSelection) this.cellSelectionDrag = null;
4419
+ if (this.textSelectionDrag?.pointerId === event.pointerId) {
4420
+ if (this.textSelectionDrag.dragged) this.suppressNextClick = true;
4421
+ this.textSelectionDrag = null;
4422
+ }
4423
+ if (this.headerTextSelectionDrag?.pointerId === event.pointerId) {
4424
+ if (this.headerTextSelectionDrag.dragged) this.suppressNextClick = true;
4425
+ this.headerTextSelectionDrag = null;
4426
+ }
4272
4427
  const endedRowHeaderSelection = this.rowHeaderSelectionDrag?.pointerId === event.pointerId;
4273
4428
  if (endedRowHeaderSelection) {
4274
4429
  if (this.rowHeaderSelectionDrag?.dragged) this.suppressNextClick = true;
@@ -4300,12 +4455,7 @@ var PivotTable = class {
4300
4455
  this.host.root.addEventListener("pointermove", this.handlePointerMove);
4301
4456
  this.host.root.addEventListener("pointerleave", this.handlePointerLeave);
4302
4457
  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);
4458
+ this.host.root.addEventListener("keydown", this.handleKeyDown);
4309
4459
  this.host.root.addEventListener("wheel", this.handleWheel, {
4310
4460
  passive: false
4311
4461
  });
@@ -4531,12 +4681,7 @@ var PivotTable = class {
4531
4681
  this.handlePointerLeave
4532
4682
  );
4533
4683
  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);
4684
+ this.host?.root.removeEventListener("keydown", this.handleKeyDown);
4540
4685
  this.host?.root.removeEventListener("wheel", this.handleWheel);
4541
4686
  this.host?.root.removeEventListener("contextmenu", this.handleContextMenu);
4542
4687
  this.host?.root.removeEventListener("pointerdown", this.handlePointerDown);
@@ -4648,6 +4793,194 @@ var PivotTable = class {
4648
4793
  activeNode: null
4649
4794
  });
4650
4795
  }
4796
+ updateTextSelectionDrag(event) {
4797
+ const drag = this.textSelectionDrag;
4798
+ if (!drag || !this.layout || !this.host) return;
4799
+ const rootRect = this.host.root.getBoundingClientRect();
4800
+ const x = event.clientX - rootRect.left;
4801
+ const y = event.clientY - rootRect.top;
4802
+ const hitResult = hitTestPivotLayout(this.layout, x, y);
4803
+ const hit = hitResult?.bodyCell;
4804
+ const header = hitResult?.headerCell;
4805
+ if (!hit && !header) return;
4806
+ const fragments = this.createTextSelectionFragments(
4807
+ header?.axis ?? "column"
4808
+ );
4809
+ const focusKey = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${hit?.rowIndex}:${hit?.columnIndex}`;
4810
+ const focusIndex = this.resolveTextFragmentIndex(focusKey, fragments);
4811
+ const text = header ? header.label : this.resolveBodyText(hit);
4812
+ if (!text || focusIndex < 0) return;
4813
+ const textOriginX = header ? header.rect.x + 10 + (header.expandable ? 18 : 0) : this.resolveBodyTextOriginX(hit, text);
4814
+ const focusOffset = resolvePivotTextOffset({
4815
+ text,
4816
+ pointerX: x,
4817
+ textOriginX,
4818
+ measureText: (value) => this.measurePivotText(value)
4819
+ });
4820
+ if (focusIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4821
+ return;
4822
+ drag.dragged = true;
4823
+ this.setSelection({
4824
+ activeCell: hit ? toCellAddress(hit) : this.core?.getSelection().activeCell ?? null,
4825
+ anchorCell: this.core?.getSelection().anchorCell ?? (hit ? toCellAddress(hit) : null),
4826
+ ranges: this.core?.getSelection().ranges ?? [],
4827
+ activeNode: null,
4828
+ textSelection: {
4829
+ fragments: fragments.map((fragment) => ({
4830
+ rowIndex: fragment.rowIndex ?? -1,
4831
+ columnIndex: fragment.columnIndex ?? -1,
4832
+ text: fragment.text,
4833
+ region: fragment.key.startsWith("header:") ? "header" : "body",
4834
+ key: fragment.key
4835
+ })),
4836
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4837
+ focusFragmentIndex: focusIndex,
4838
+ anchorOffset: drag.anchorOffset,
4839
+ focusOffset
4840
+ }
4841
+ });
4842
+ }
4843
+ updateHeaderTextSelectionDrag(event) {
4844
+ const drag = this.headerTextSelectionDrag;
4845
+ if (!drag || !this.host || !this.layout) return;
4846
+ const rootRect = this.host.root.getBoundingClientRect();
4847
+ const localX = event.clientX - rootRect.left;
4848
+ const localY = event.clientY - rootRect.top;
4849
+ const hit = hitTestPivotLayout(this.layout, localX, localY);
4850
+ const header = hit?.headerCell;
4851
+ const body = hit?.bodyCell;
4852
+ if (!header && !body) return;
4853
+ const key = header ? `header:${header.axis}:${header.nodeId ?? ""}:${header.label}` : `body:${body?.rowIndex}:${body?.columnIndex}`;
4854
+ const focusFragmentIndex = drag.fragments.findIndex(
4855
+ (fragment) => fragment.key === key
4856
+ );
4857
+ if (focusFragmentIndex < 0) return;
4858
+ const focusFragment = drag.fragments[focusFragmentIndex];
4859
+ const focusOffset = resolvePivotTextOffset({
4860
+ text: focusFragment.text,
4861
+ pointerX: localX,
4862
+ textOriginX: focusFragment.originX,
4863
+ measureText: (value) => this.measurePivotText(value)
4864
+ });
4865
+ if (focusFragmentIndex === drag.anchorFragmentIndex && focusOffset === drag.anchorOffset)
4866
+ return;
4867
+ drag.dragged = true;
4868
+ const selection = this.core?.getSelection();
4869
+ if (!selection) return;
4870
+ this.setSelection({
4871
+ ...selection,
4872
+ textSelection: {
4873
+ fragments: drag.fragments.map((fragment) => ({
4874
+ rowIndex: fragment.rowIndex ?? -1,
4875
+ columnIndex: fragment.columnIndex ?? -1,
4876
+ text: fragment.text,
4877
+ region: "header",
4878
+ key: fragment.key
4879
+ })),
4880
+ anchorFragmentIndex: drag.anchorFragmentIndex,
4881
+ focusFragmentIndex,
4882
+ anchorOffset: drag.anchorOffset,
4883
+ focusOffset
4884
+ }
4885
+ });
4886
+ }
4887
+ createTextSelectionFragments(axis) {
4888
+ const bodyFragments = (this.layout?.bodyCells ?? []).map((cell) => ({
4889
+ key: `body:${cell.rowIndex}:${cell.columnIndex}`,
4890
+ text: this.resolveBodyText(cell),
4891
+ originX: this.resolveBodyTextOriginX(cell, this.resolveBodyText(cell)),
4892
+ rowIndex: cell.rowIndex,
4893
+ columnIndex: cell.columnIndex
4894
+ })).filter((fragment) => fragment.text.length > 0);
4895
+ if (this.layout) {
4896
+ const columnHeaders = this.createHeaderTextFragments("column");
4897
+ const rowHeaders = this.layout.rowHeaderCells.filter((cell) => cell.label).map((cell) => ({
4898
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4899
+ text: cell.label,
4900
+ rowIndex: void 0,
4901
+ columnIndex: void 0,
4902
+ originX: cell.rect.x + 10
4903
+ }));
4904
+ const rows = Array.from(
4905
+ { length: this.layout.rowSlots.length },
4906
+ (_, rowIndex) => [
4907
+ rowHeaders[rowIndex],
4908
+ ...bodyFragments.filter((fragment) => fragment.rowIndex === rowIndex)
4909
+ ]
4910
+ ).flat();
4911
+ if (axis === "column") {
4912
+ return [...columnHeaders, ...rows].filter(
4913
+ (fragment) => Boolean(fragment)
4914
+ );
4915
+ }
4916
+ const rowHeadersTitle = this.layout.rowFieldHeaderCells.filter((cell) => cell.label).map((cell) => ({
4917
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4918
+ text: cell.label,
4919
+ rowIndex: void 0,
4920
+ columnIndex: void 0,
4921
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
4922
+ }));
4923
+ return [...rowHeadersTitle, ...columnHeaders, ...rows].filter(
4924
+ (fragment) => Boolean(fragment)
4925
+ );
4926
+ }
4927
+ return [...this.createHeaderTextFragments(axis), ...bodyFragments];
4928
+ }
4929
+ createHeaderTextFragments(axis) {
4930
+ if (!this.layout) return [];
4931
+ return (axis === "column" ? this.layout.columnHeaderCells : [...this.layout.rowHeaderCells, ...this.layout.rowFieldHeaderCells]).filter((cell) => cell.label).map((cell) => ({
4932
+ key: `header:${cell.axis}:${cell.nodeId ?? ""}:${cell.label}`,
4933
+ text: cell.label,
4934
+ rowIndex: void 0,
4935
+ columnIndex: void 0,
4936
+ originX: cell.rect.x + 10 + (cell.expandable ? 18 : 0)
4937
+ }));
4938
+ }
4939
+ resolveBodyText(cell) {
4940
+ const indicator = this.options.indicators.find(
4941
+ (item) => item.key === cell.indicatorKey
4942
+ );
4943
+ if (!indicator || indicator.formatter || typeof cell.value !== "number")
4944
+ return cell.formattedValue;
4945
+ if (indicator.cellType === "percent")
4946
+ return `${(cell.value * 100).toFixed(indicator.precision ?? 2)}%`;
4947
+ if (indicator.cellType === "money")
4948
+ return cell.value.toLocaleString("zh-CN", {
4949
+ style: "currency",
4950
+ currency: "CNY",
4951
+ minimumFractionDigits: indicator.precision ?? 2,
4952
+ maximumFractionDigits: indicator.precision ?? 2
4953
+ });
4954
+ return cell.formattedValue;
4955
+ }
4956
+ createBodyTextFragments() {
4957
+ return (this.layout?.bodyCells ?? []).map((cell) => ({
4958
+ rowIndex: cell.rowIndex,
4959
+ columnIndex: cell.columnIndex,
4960
+ text: this.resolveBodyText(cell)
4961
+ })).filter((fragment) => fragment.text.length > 0);
4962
+ }
4963
+ resolveTextFragmentIndex(key, fragments) {
4964
+ return fragments.findIndex((fragment) => fragment.key === key);
4965
+ }
4966
+ resolveBodyFragmentIndex(cell) {
4967
+ if (!this.layout) return -1;
4968
+ return this.layout.bodyCells.slice(0, this.layout.bodyCells.indexOf(cell) + 1).filter((item) => this.resolveBodyText(item).length > 0).length - 1;
4969
+ }
4970
+ resolveBodyTextOriginX(cell, text) {
4971
+ const indicator = this.options.indicators.find(
4972
+ (item) => item.key === cell.indicatorKey
4973
+ );
4974
+ const width = this.measurePivotText(text);
4975
+ const align = indicator?.align ?? "right";
4976
+ 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;
4977
+ }
4978
+ measurePivotText(text) {
4979
+ const context = this.host?.canvas.getContext("2d");
4980
+ if (!context) return text.length * 8;
4981
+ context.font = `${this.options.theme?.fontSize ?? 13}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
4982
+ return context.measureText(text).width;
4983
+ }
4651
4984
  updateRowHeaderSelectionDrag(event) {
4652
4985
  const drag = this.rowHeaderSelectionDrag;
4653
4986
  if (!drag || !this.layout || !this.host) return;
@@ -4886,6 +5219,14 @@ var PivotTable = class {
4886
5219
  selection: this.core.getSelection()
4887
5220
  });
4888
5221
  }
5222
+ /** 复制表格选区文本;同步复制失败时回退到异步实现。 */
5223
+ copyPivotText(text) {
5224
+ if (!text) return;
5225
+ const ownerDocument = this.host?.canvas.ownerDocument ?? globalThis.document;
5226
+ if (!utils.copyTextSync(text, ownerDocument)) {
5227
+ void utils.copyText(text, ownerDocument);
5228
+ }
5229
+ }
4889
5230
  scheduleRender() {
4890
5231
  if (this.frameId !== null) return;
4891
5232
  this.frameId = requestAnimationFrame(() => {
@@ -4929,12 +5270,6 @@ var PivotTable = class {
4929
5270
  });
4930
5271
  }
4931
5272
  };
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
5273
  function toCellAddress(cell) {
4939
5274
  return {
4940
5275
  rowNodeId: cell.rowNodeId ?? "",