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