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