stk-table-vue 1.2.0 → 1.2.2
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/lib/{Dropdown-Bq4DxXYJ.js → Dropdown-D40QlY91.js} +2 -2
- package/lib/{StkTable-yOz9wkCz.js → StkTable-g_qwf6BE.js} +180 -124
- package/lib/src/StkTable/useSorter.d.ts +2 -1
- package/lib/src/StkTable/useVirtualScroll.d.ts +11 -2
- package/lib/stk-table-vue.js +3 -3
- package/lib/style.css +160 -1
- package/package.json +2 -2
- package/src/StkTable/StkTable.vue +19 -7
- package/src/StkTable/useFixedStyle.ts +2 -2
- package/src/StkTable/useMergeCells.ts +149 -111
- package/src/StkTable/useSorter.ts +9 -1
- package/src/StkTable/useVirtualScroll.ts +38 -11
- package/lib/src/StkTable/features/useAreaSelection.bak.d.ts +0 -25
- package/lib/test/StkTable.browser.test.d.ts +0 -1
- package/lib/test/autoHeightStripe.repro.test.d.ts +0 -1
- package/lib/test/autoRowHeightVirtualScroll.test.d.ts +0 -1
- package/lib/test/defaultSort.test.d.ts +0 -1
- package/lib/test/insertToOrderedArray.test.d.ts +0 -1
- package/lib/test/perf/scrollPerf.test.d.ts +0 -1
- package/lib/test/rowMergeVirtualScroll.repro.test.d.ts +0 -1
- package/lib/test/scrollTo.test.d.ts +0 -1
- package/lib/test/setSorter.test.d.ts +0 -1
- package/lib/test/setTreeExpandParents.test.d.ts +0 -1
- package/lib/test/trReuse.repro.test.d.ts +0 -1
- package/lib/test/utils/DragResize.d.ts +0 -28
- package/lib/test/utils/h.d.ts +0 -10
- package/lib/test/virtualXMerge.test.d.ts +0 -1
- package/src/StkTable/features/useAreaSelection.bak.ts +0 -951
|
@@ -1,951 +0,0 @@
|
|
|
1
|
-
import { Ref, ShallowRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
2
|
-
import {
|
|
3
|
-
AreaSelectionConfig,
|
|
4
|
-
AreaSelectionRange,
|
|
5
|
-
CellKeyGen,
|
|
6
|
-
ColKeyGen,
|
|
7
|
-
StkTableColumn,
|
|
8
|
-
UniqKey,
|
|
9
|
-
AreaSelectionSetterRange,
|
|
10
|
-
AreaSelectionSetterOption,
|
|
11
|
-
} from '../types';
|
|
12
|
-
import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll';
|
|
13
|
-
import { getClosestColKey, getClosestTd, getClosestTr, getClosestTrIndex } from '../utils';
|
|
14
|
-
import { getCalculatedColWidth } from '../utils/constRefUtils';
|
|
15
|
-
import { MY_FN_NAME } from './const';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 单元格区域选择功能
|
|
19
|
-
* 支持鼠标拖拽选择、键盘导航、复制粘贴等功能
|
|
20
|
-
* en: Cell area selection feature with mouse drag, keyboard navigation, copy-paste, etc.
|
|
21
|
-
*/
|
|
22
|
-
export function useAreaSelection<DT extends Record<string, any>>(
|
|
23
|
-
props: any,
|
|
24
|
-
emits: any,
|
|
25
|
-
tableContainerRef: Ref<HTMLDivElement | undefined>,
|
|
26
|
-
dataSourceCopy: ShallowRef<DT[]>,
|
|
27
|
-
tableHeaderLast: ShallowRef<StkTableColumn<DT>[]>,
|
|
28
|
-
colKeyGen: ColKeyGen,
|
|
29
|
-
cellKeyGen: CellKeyGen,
|
|
30
|
-
scrollTo: (top: number | null, left: number | null) => void,
|
|
31
|
-
virtualScroll: Ref<VirtualScrollStore>,
|
|
32
|
-
virtualScrollX: Ref<VirtualScrollXStore>,
|
|
33
|
-
getRowIndex: (row: DT) => number,
|
|
34
|
-
getColumnIndex: (col: StkTableColumn<DT>) => number,
|
|
35
|
-
) {
|
|
36
|
-
/**
|
|
37
|
-
* 自动滚动:鼠标距容器边缘多少px开始触发
|
|
38
|
-
* en: Mouse distance from container edge to start auto scroll
|
|
39
|
-
*/
|
|
40
|
-
const EDGE_ZONE = 40;
|
|
41
|
-
/**
|
|
42
|
-
* 自动滚动:每帧最大滚动像素
|
|
43
|
-
* en: Maximum scroll pixels per frame
|
|
44
|
-
*/
|
|
45
|
-
const SCROLL_SPEED_MAX = 15;
|
|
46
|
-
const POINT_EDGE_OFFSET = 2;
|
|
47
|
-
|
|
48
|
-
const KEY_ARROW_UP = 'ArrowUp';
|
|
49
|
-
const KEY_ARROW_DOWN = 'ArrowDown';
|
|
50
|
-
const KEY_ARROW_LEFT = 'ArrowLeft';
|
|
51
|
-
const KEY_ARROW_RIGHT = 'ArrowRight';
|
|
52
|
-
const KEY_TAB = 'Tab';
|
|
53
|
-
const KEY_ESCAPE = 'Escape';
|
|
54
|
-
const KEY_ESC = 'Esc';
|
|
55
|
-
const KEY_C = 'c';
|
|
56
|
-
|
|
57
|
-
// CSS
|
|
58
|
-
const CELL_RANGE_SELECTED = 'cell-range-selected';
|
|
59
|
-
const CELL_RANGE_TOP = 'cell-range-t';
|
|
60
|
-
const CELL_RANGE_BOTTOM = 'cell-range-b';
|
|
61
|
-
const CELL_RANGE_LEFT = 'cell-range-l';
|
|
62
|
-
const CELL_RANGE_RIGHT = 'cell-range-r';
|
|
63
|
-
|
|
64
|
-
const selectionRanges = ref<AreaSelectionRange[]>([]);
|
|
65
|
-
const isSelecting = ref(false);
|
|
66
|
-
/** start cell */
|
|
67
|
-
let anchorCell: { rowIndex: number; colIndex: number } | null = null;
|
|
68
|
-
|
|
69
|
-
/** auto scroll rAF id */
|
|
70
|
-
let autoScrollRafId = 0;
|
|
71
|
-
/**
|
|
72
|
-
* 最后一次鼠标位置(用于边界自动滚动计算)
|
|
73
|
-
* en: Last mouse position (for boundary auto scroll calculation)
|
|
74
|
-
*/
|
|
75
|
-
let lastMouseClientX = 0;
|
|
76
|
-
let lastMouseClientY = 0;
|
|
77
|
-
|
|
78
|
-
const config = computed<AreaSelectionConfig>(() => {
|
|
79
|
-
if (typeof props.areaSelection === 'boolean') {
|
|
80
|
-
const b = props.areaSelection;
|
|
81
|
-
return { enabled: b, keyboard: b, ctrl: b, shift: b, highlight: { cell: b, row: false } };
|
|
82
|
-
}
|
|
83
|
-
const { highlight: userHighlight, ...restConfig } = props.areaSelection || {};
|
|
84
|
-
return {
|
|
85
|
-
enabled: true,
|
|
86
|
-
ctrl: true,
|
|
87
|
-
shift: true,
|
|
88
|
-
highlight: {
|
|
89
|
-
cell: true,
|
|
90
|
-
row: false,
|
|
91
|
-
...userHighlight,
|
|
92
|
-
},
|
|
93
|
-
...restConfig,
|
|
94
|
-
};
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
/** 是否启用键盘控制选区移动 */
|
|
98
|
-
const keyboardEnabled = computed(() => config.value.keyboard);
|
|
99
|
-
|
|
100
|
-
/** 是否启用 Ctrl 多选 */
|
|
101
|
-
const ctrlEnabled = computed(() => config.value.ctrl);
|
|
102
|
-
|
|
103
|
-
/** 是否启用 Shift 扩选 */
|
|
104
|
-
const shiftEnabled = computed(() => config.value.shift);
|
|
105
|
-
|
|
106
|
-
/** 是否启用单元格高亮 */
|
|
107
|
-
const highlightCellEnabled = computed(() => config.value.highlight?.cell);
|
|
108
|
-
|
|
109
|
-
/** 是否启用行高亮 */
|
|
110
|
-
const highlightRowEnabled = computed(() => config.value.highlight?.row);
|
|
111
|
-
|
|
112
|
-
/** colKey → absolute index 映射 */
|
|
113
|
-
const colKeyToIndexMap = computed(() => {
|
|
114
|
-
const headers = tableHeaderLast.value;
|
|
115
|
-
const map = new Map<UniqKey, number>();
|
|
116
|
-
for (let i = 0; i < headers.length; i++) {
|
|
117
|
-
map.set(colKeyGen.value(headers[i]), i);
|
|
118
|
-
}
|
|
119
|
-
return map;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 获取固定列宽度的函数
|
|
124
|
-
* 缓存每个固定列位置的累计宽度,查询时直接返回
|
|
125
|
-
* @param colIndex 目标列索引
|
|
126
|
-
* @returns [leftFixedWidth, rightFixedWidth]
|
|
127
|
-
*/
|
|
128
|
-
const getFixedColWidths = computed(() => {
|
|
129
|
-
const cols = tableHeaderLast.value;
|
|
130
|
-
// 预计算每个列索引位置对应的左右固定列累计宽度
|
|
131
|
-
// 左固定列:从左向右累加,非固定列继承前一个值
|
|
132
|
-
const leftWidths: number[] = new Array(cols.length + 1).fill(0);
|
|
133
|
-
// 右固定列:从右向左累加,非固定列继承后一个值
|
|
134
|
-
const rightWidths: number[] = new Array(cols.length + 1).fill(0);
|
|
135
|
-
|
|
136
|
-
let leftSum = 0;
|
|
137
|
-
for (let i = 0; i < cols.length; i++) {
|
|
138
|
-
leftWidths[i] = leftSum;
|
|
139
|
-
if (cols[i]?.fixed === 'left') {
|
|
140
|
-
leftSum += getCalculatedColWidth(cols[i]);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
leftWidths[cols.length] = leftSum;
|
|
144
|
-
|
|
145
|
-
let rightSum = 0;
|
|
146
|
-
for (let i = cols.length - 1; i >= 0; i--) {
|
|
147
|
-
rightWidths[i] = rightSum;
|
|
148
|
-
if (cols[i]?.fixed === 'right') {
|
|
149
|
-
rightSum += getCalculatedColWidth(cols[i]);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return (colIndex: number) => {
|
|
154
|
-
return [leftWidths[colIndex] ?? 0, rightWidths[colIndex + 1] ?? 0] as const;
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
/** 根据 selectionRanges 计算所有选区内 cellKey 的并集 */
|
|
159
|
-
const selectedCellKeys = computed<Set<string>>(() => {
|
|
160
|
-
const ranges = selectionRanges.value;
|
|
161
|
-
if (!ranges.length) return new Set();
|
|
162
|
-
|
|
163
|
-
const keys = new Set<string>();
|
|
164
|
-
const cols = tableHeaderLast.value;
|
|
165
|
-
const data = dataSourceCopy.value;
|
|
166
|
-
for (const range of ranges) {
|
|
167
|
-
const {
|
|
168
|
-
begin: { row: r1, col: c1 },
|
|
169
|
-
end: { row: r2, col: c2 },
|
|
170
|
-
} = range.index;
|
|
171
|
-
|
|
172
|
-
const [rStart, rEnd] = r1 < r2 ? [r1, r2] : [r2, r1];
|
|
173
|
-
const [cStart, cEnd] = c1 < c2 ? [c1, c2] : [c2, c1];
|
|
174
|
-
|
|
175
|
-
for (let r = rStart; r <= rEnd; r++) {
|
|
176
|
-
const row = data[r];
|
|
177
|
-
if (!row) continue;
|
|
178
|
-
for (let c = cStart; c <= cEnd; c++) {
|
|
179
|
-
const col = cols[c];
|
|
180
|
-
if (col) keys.add(cellKeyGen(row, col));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return keys;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
onMounted(() => {
|
|
188
|
-
addListener();
|
|
189
|
-
});
|
|
190
|
-
onBeforeUnmount(() => {
|
|
191
|
-
removeListener();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 监听数据行数/列数变化,当行列变少时钳制选区与锚点,避免越界
|
|
196
|
-
* en: Watch row/col count changes, clamp selection ranges and anchor to avoid out-of-bounds
|
|
197
|
-
*/
|
|
198
|
-
watch([() => dataSourceCopy.value.length, () => tableHeaderLast.value.length], ([rowCount, colCount]) => {
|
|
199
|
-
if (!config.value.enabled) return;
|
|
200
|
-
|
|
201
|
-
// 钳制锚点
|
|
202
|
-
if (anchorCell) {
|
|
203
|
-
if (rowCount === 0 || colCount === 0) {
|
|
204
|
-
anchorCell = null;
|
|
205
|
-
} else {
|
|
206
|
-
anchorCell.rowIndex = clamp(anchorCell.rowIndex, 0, rowCount - 1);
|
|
207
|
-
anchorCell.colIndex = clamp(anchorCell.colIndex, 0, colCount - 1);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!selectionRanges.value.length) return;
|
|
212
|
-
|
|
213
|
-
// 行或列为 0 时清空选区
|
|
214
|
-
if (rowCount === 0 || colCount === 0) {
|
|
215
|
-
clearSelectedArea();
|
|
216
|
-
emitSelectionChange();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const maxRow = rowCount - 1;
|
|
221
|
-
const maxCol = colCount - 1;
|
|
222
|
-
let changed = false;
|
|
223
|
-
const newRanges: AreaSelectionRange[] = [];
|
|
224
|
-
for (const range of selectionRanges.value) {
|
|
225
|
-
const { begin, end } = range.index;
|
|
226
|
-
const nbRow = clamp(begin.row, 0, maxRow);
|
|
227
|
-
const nbCol = clamp(begin.col, 0, maxCol);
|
|
228
|
-
const neRow = clamp(end.row, 0, maxRow);
|
|
229
|
-
const neCol = clamp(end.col, 0, maxCol);
|
|
230
|
-
if (nbRow !== begin.row || nbCol !== begin.col || neRow !== end.row || neCol !== end.col) {
|
|
231
|
-
changed = true;
|
|
232
|
-
newRanges.push(makeRange(nbRow, nbCol, neRow, neCol));
|
|
233
|
-
} else {
|
|
234
|
-
newRanges.push(range);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (changed) {
|
|
239
|
-
selectionRanges.value = newRanges;
|
|
240
|
-
emitSelectionChange();
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
function addListener() {
|
|
245
|
-
removeListener();
|
|
246
|
-
tableContainerRef.value?.addEventListener('keydown', onKeydown);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function removeListener() {
|
|
250
|
-
tableContainerRef.value?.removeEventListener('keydown', onKeydown);
|
|
251
|
-
document.removeEventListener('mousemove', onDocumentMouseMove);
|
|
252
|
-
document.removeEventListener('mouseup', onDocumentMouseUp);
|
|
253
|
-
stopAutoScroll();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** 获取归一化(min/max)后的选区范围 */
|
|
257
|
-
function normalizeRange(range: AreaSelectionRange) {
|
|
258
|
-
const { begin, end } = range.index;
|
|
259
|
-
return {
|
|
260
|
-
minRow: Math.min(begin.row, end.row),
|
|
261
|
-
maxRow: Math.max(begin.row, end.row),
|
|
262
|
-
minCol: Math.min(begin.col, end.col),
|
|
263
|
-
maxCol: Math.max(begin.col, end.col),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* 构造选区范围。begin = 拖拽起点,end = 拖拽终点。
|
|
269
|
-
* 同时填充已废弃的 x/y 字段以保证向后兼容。
|
|
270
|
-
*/
|
|
271
|
-
function makeRange(beginRow: number, beginCol: number, endRow: number, endCol: number): AreaSelectionRange {
|
|
272
|
-
return {
|
|
273
|
-
index: {
|
|
274
|
-
x: [beginCol, endCol],
|
|
275
|
-
y: [beginRow, endRow],
|
|
276
|
-
begin: { row: beginRow, col: beginCol },
|
|
277
|
-
end: { row: endRow, col: endCol },
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** 根据colKey获取列的绝对索引 */
|
|
283
|
-
function getColIndexByKey(colKey: string | undefined): number {
|
|
284
|
-
if (!colKey) return -1;
|
|
285
|
-
return colKeyToIndexMap.value.get(colKey) ?? -1;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** 获取列的左边距和宽度
|
|
289
|
-
* @param colIndex 列的绝对索引
|
|
290
|
-
* @returns [left, width]
|
|
291
|
-
*/
|
|
292
|
-
function getColPosition(colIndex: number): [number, number] {
|
|
293
|
-
let left = 0;
|
|
294
|
-
const cols = tableHeaderLast.value;
|
|
295
|
-
for (let i = 0; i < cols.length; i++) {
|
|
296
|
-
const colWidth = getCalculatedColWidth(cols[i]);
|
|
297
|
-
if (i === colIndex) return [left, colWidth];
|
|
298
|
-
left += colWidth;
|
|
299
|
-
}
|
|
300
|
-
return [left, 0];
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** 根据按键计算移动方向 */
|
|
304
|
-
function getMovementDelta(key: string, shiftKey: boolean): [number, number] {
|
|
305
|
-
let rowDelta = 0;
|
|
306
|
-
let colDelta = 0;
|
|
307
|
-
|
|
308
|
-
switch (key) {
|
|
309
|
-
case KEY_ARROW_UP:
|
|
310
|
-
rowDelta = -1;
|
|
311
|
-
break;
|
|
312
|
-
case KEY_ARROW_DOWN:
|
|
313
|
-
rowDelta = 1;
|
|
314
|
-
break;
|
|
315
|
-
case KEY_ARROW_LEFT:
|
|
316
|
-
colDelta = -1;
|
|
317
|
-
break;
|
|
318
|
-
case KEY_ARROW_RIGHT:
|
|
319
|
-
colDelta = 1;
|
|
320
|
-
break;
|
|
321
|
-
case KEY_TAB:
|
|
322
|
-
// Tab: right; Shift+Tab: left
|
|
323
|
-
colDelta = shiftKey ? -1 : 1;
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return [rowDelta, colDelta];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** 钳制值到指定范围内 */
|
|
331
|
-
function clamp(value: number, min: number, max: number): number {
|
|
332
|
-
return Math.max(min, Math.min(value, max));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** 处理Tab键的换行逻辑 */
|
|
336
|
-
function handleTabWrap(row: number, col: number, rawCol: number, rowCount: number, colCount: number): [number, number] {
|
|
337
|
-
if (rawCol >= colCount) return [Math.min(row + 1, rowCount - 1), 0];
|
|
338
|
-
if (rawCol < 0) return [Math.max(row - 1, 0), colCount - 1];
|
|
339
|
-
return [row, col];
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** 计算自动滚动的增量 */
|
|
343
|
-
function calculateAutoScrollDelta(mouseX: number, mouseY: number, rect: DOMRect): { deltaX: number; deltaY: number } {
|
|
344
|
-
const { top, bottom, left, right } = rect;
|
|
345
|
-
let deltaX = 0;
|
|
346
|
-
let deltaY = 0;
|
|
347
|
-
|
|
348
|
-
// Y方向
|
|
349
|
-
if (mouseY < top + EDGE_ZONE) {
|
|
350
|
-
const dist = Math.max(0, top + EDGE_ZONE - mouseY);
|
|
351
|
-
deltaY = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
352
|
-
} else if (mouseY > bottom - EDGE_ZONE) {
|
|
353
|
-
const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE));
|
|
354
|
-
deltaY = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// X方向
|
|
358
|
-
if (mouseX < left + EDGE_ZONE) {
|
|
359
|
-
const dist = Math.max(0, left + EDGE_ZONE - mouseX);
|
|
360
|
-
deltaX = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
361
|
-
} else if (mouseX > right - EDGE_ZONE) {
|
|
362
|
-
const dist = Math.max(0, mouseX - (right - EDGE_ZONE));
|
|
363
|
-
deltaX = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return { deltaX, deltaY };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** mousedown 处理:设置锚点,开始拖选 */
|
|
370
|
-
function onSelectionMouseDown(e: MouseEvent) {
|
|
371
|
-
if (!config.value.enabled || e.button !== 0) return;
|
|
372
|
-
|
|
373
|
-
const rowIndex = getClosestTrIndex(e.target as HTMLElement);
|
|
374
|
-
const colKey = getClosestColKey(e.target as HTMLElement);
|
|
375
|
-
const colIndex = getColIndexByKey(colKey);
|
|
376
|
-
|
|
377
|
-
if (rowIndex < 0 || colIndex < 0) return;
|
|
378
|
-
|
|
379
|
-
const ctrlKey = e.ctrlKey || e.metaKey;
|
|
380
|
-
|
|
381
|
-
const range: AreaSelectionRange = makeRange(rowIndex, colIndex, rowIndex, colIndex);
|
|
382
|
-
// Shift 扩选:从锚点扩展到当前位置,更新最后一个区域
|
|
383
|
-
if (e.shiftKey && anchorCell && shiftEnabled.value) {
|
|
384
|
-
const ranges = selectionRanges.value.slice();
|
|
385
|
-
const shiftRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex);
|
|
386
|
-
if (ranges.length) {
|
|
387
|
-
ranges[ranges.length - 1] = shiftRange;
|
|
388
|
-
} else {
|
|
389
|
-
ranges.push(shiftRange);
|
|
390
|
-
}
|
|
391
|
-
selectionRanges.value = ranges;
|
|
392
|
-
} else {
|
|
393
|
-
anchorCell = { rowIndex, colIndex };
|
|
394
|
-
if (ctrlKey && ctrlEnabled.value) {
|
|
395
|
-
// Ctrl multiple
|
|
396
|
-
selectionRanges.value = selectionRanges.value.concat([range]);
|
|
397
|
-
} else {
|
|
398
|
-
// normal click
|
|
399
|
-
selectionRanges.value = [range];
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
isSelecting.value = true;
|
|
404
|
-
lastMouseClientX = e.clientX;
|
|
405
|
-
lastMouseClientY = e.clientY;
|
|
406
|
-
|
|
407
|
-
// 防止拖选时选中文字
|
|
408
|
-
// en: Prevent text selection during drag
|
|
409
|
-
document.addEventListener('mousemove', onDocumentMouseMove);
|
|
410
|
-
document.addEventListener('mouseup', onDocumentMouseUp);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** document mousemove 处理:更新选区终点 + 检测边界自动滚动 */
|
|
414
|
-
function onDocumentMouseMove(e: MouseEvent) {
|
|
415
|
-
if (!isSelecting.value) return;
|
|
416
|
-
|
|
417
|
-
lastMouseClientX = e.clientX;
|
|
418
|
-
lastMouseClientY = e.clientY;
|
|
419
|
-
|
|
420
|
-
// 尝试从当前鼠标位置更新选区
|
|
421
|
-
updateSelectionFromEvent(e);
|
|
422
|
-
|
|
423
|
-
// 检测是否需要边界自动滚动
|
|
424
|
-
checkAutoScroll();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** 从 MouseEvent 目标元素更新选区 */
|
|
428
|
-
function updateSelectionFromEvent(e: MouseEvent) {
|
|
429
|
-
const target = e.target as HTMLElement;
|
|
430
|
-
if (!target) return;
|
|
431
|
-
|
|
432
|
-
const rowIndex = getClosestTrIndex(target);
|
|
433
|
-
if (Number.isNaN(rowIndex) || rowIndex < 0) return;
|
|
434
|
-
|
|
435
|
-
const colKey = getClosestColKey(target);
|
|
436
|
-
const colIndex = getColIndexByKey(colKey);
|
|
437
|
-
if (colIndex < 0) return;
|
|
438
|
-
|
|
439
|
-
updateSelectionEnd(rowIndex, colIndex);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** 更新最后一个选区的终点(拖拽过程中) */
|
|
443
|
-
function updateSelectionEnd(endRowIndex: number, endColIndex: number) {
|
|
444
|
-
if (!anchorCell) return;
|
|
445
|
-
const newRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex);
|
|
446
|
-
const ranges = [...selectionRanges.value];
|
|
447
|
-
if (ranges.length > 0) {
|
|
448
|
-
ranges[ranges.length - 1] = newRange;
|
|
449
|
-
} else {
|
|
450
|
-
ranges.push(newRange);
|
|
451
|
-
}
|
|
452
|
-
selectionRanges.value = ranges;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// ---- 边界自动滚动 ----
|
|
456
|
-
|
|
457
|
-
/** 检查鼠标是否在容器边缘附近,启动或停止自动滚动 */
|
|
458
|
-
function checkAutoScroll() {
|
|
459
|
-
const container = tableContainerRef.value;
|
|
460
|
-
if (!container) return;
|
|
461
|
-
|
|
462
|
-
const rect = container.getBoundingClientRect();
|
|
463
|
-
const { top, bottom, left, right } = rect;
|
|
464
|
-
|
|
465
|
-
const nearEdge =
|
|
466
|
-
lastMouseClientY < top + EDGE_ZONE ||
|
|
467
|
-
lastMouseClientY > bottom - EDGE_ZONE ||
|
|
468
|
-
lastMouseClientX < left + EDGE_ZONE ||
|
|
469
|
-
lastMouseClientX > right - EDGE_ZONE;
|
|
470
|
-
|
|
471
|
-
if (nearEdge && !autoScrollRafId) {
|
|
472
|
-
autoScrollLoop();
|
|
473
|
-
} else if (!nearEdge && autoScrollRafId) {
|
|
474
|
-
stopAutoScroll();
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** rAF 循环:边界自动滚动 + 更新选区 */
|
|
479
|
-
function autoScrollLoop() {
|
|
480
|
-
const container = tableContainerRef.value;
|
|
481
|
-
if (!container || !isSelecting.value) {
|
|
482
|
-
stopAutoScroll();
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const rect = container.getBoundingClientRect();
|
|
487
|
-
const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect);
|
|
488
|
-
|
|
489
|
-
if (deltaX !== 0 || deltaY !== 0) {
|
|
490
|
-
container.scrollTop += deltaY;
|
|
491
|
-
container.scrollLeft += deltaX;
|
|
492
|
-
|
|
493
|
-
// 滚动后,在容器内边缘处用 elementFromPoint 找到当前单元格更新选区
|
|
494
|
-
updateSelectionFromPoint(container, rect);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// 如果还在拖选且仍需滚动,继续循环
|
|
498
|
-
if (isSelecting.value && (deltaX !== 0 || deltaY !== 0)) {
|
|
499
|
-
autoScrollRafId = requestAnimationFrame(autoScrollLoop);
|
|
500
|
-
} else {
|
|
501
|
-
autoScrollRafId = 0;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/** 将鼠标位置钳制到容器内部,用 elementFromPoint 找到边缘单元格并更新选区 */
|
|
506
|
-
function updateSelectionFromPoint(container: HTMLElement, containerRect: DOMRect) {
|
|
507
|
-
// 获取表头高度,钳制 Y 时跳过表头区域
|
|
508
|
-
const thead = container.querySelector('thead');
|
|
509
|
-
const { top, bottom, left, right } = containerRect;
|
|
510
|
-
|
|
511
|
-
const headerBottom = thead ? top + thead.offsetHeight : top;
|
|
512
|
-
|
|
513
|
-
const x = Math.max(left + POINT_EDGE_OFFSET, Math.min(lastMouseClientX, right - POINT_EDGE_OFFSET));
|
|
514
|
-
const y = Math.max(headerBottom + POINT_EDGE_OFFSET, Math.min(lastMouseClientY, bottom - POINT_EDGE_OFFSET));
|
|
515
|
-
|
|
516
|
-
const el = document.elementFromPoint(x, y);
|
|
517
|
-
if (!el) return;
|
|
518
|
-
|
|
519
|
-
const td = getClosestTd(el as HTMLElement);
|
|
520
|
-
const tr = getClosestTr(el as HTMLElement);
|
|
521
|
-
if (!td || !tr) return;
|
|
522
|
-
|
|
523
|
-
const rowIndex = getClosestTrIndex(tr);
|
|
524
|
-
const colKey = getClosestColKey(td);
|
|
525
|
-
const colIndex = getColIndexByKey(colKey);
|
|
526
|
-
|
|
527
|
-
if (Number.isNaN(rowIndex) || rowIndex < 0 || colIndex < 0) return;
|
|
528
|
-
|
|
529
|
-
updateSelectionEnd(rowIndex, colIndex);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/** 停止自动滚动 */
|
|
533
|
-
function stopAutoScroll() {
|
|
534
|
-
if (autoScrollRafId) {
|
|
535
|
-
cancelAnimationFrame(autoScrollRafId);
|
|
536
|
-
autoScrollRafId = 0;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** document mouseup 处理:结束拖选 */
|
|
541
|
-
function onDocumentMouseUp() {
|
|
542
|
-
if (!isSelecting.value) return;
|
|
543
|
-
isSelecting.value = false;
|
|
544
|
-
stopAutoScroll();
|
|
545
|
-
|
|
546
|
-
document.removeEventListener('mousemove', onDocumentMouseMove);
|
|
547
|
-
document.removeEventListener('mouseup', onDocumentMouseUp);
|
|
548
|
-
|
|
549
|
-
// 发出事件
|
|
550
|
-
emitSelectionChange();
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function emitSelectionChange() {
|
|
554
|
-
emits('area-selection-change', selectionRanges.value);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** 获取 areaSelection 配置中的格式化回调 */
|
|
558
|
-
function getFormatCellFn() {
|
|
559
|
-
const cfg = config.value;
|
|
560
|
-
return typeof cfg.formatCellForClipboard === 'function' ? cfg.formatCellForClipboard : null;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* 复制选区内容到剪贴板,只复制最后一个选区
|
|
565
|
-
* en: Copy selected area content to clipboard, only copy the last selected area
|
|
566
|
-
* @returns {string} text
|
|
567
|
-
*/
|
|
568
|
-
function copySelectedArea(): string {
|
|
569
|
-
const ranges = selectionRanges.value;
|
|
570
|
-
if (!ranges.length) return '';
|
|
571
|
-
|
|
572
|
-
const range = ranges[ranges.length - 1];
|
|
573
|
-
const { minRow, maxRow, minCol, maxCol } = normalizeRange(range);
|
|
574
|
-
const data = dataSourceCopy.value;
|
|
575
|
-
const cols = tableHeaderLast.value;
|
|
576
|
-
const formatCell = getFormatCellFn();
|
|
577
|
-
|
|
578
|
-
const lines: string[] = [];
|
|
579
|
-
for (let r = minRow; r <= maxRow; r++) {
|
|
580
|
-
const row = data[r];
|
|
581
|
-
if (!row) continue;
|
|
582
|
-
|
|
583
|
-
const cells: string[] = [];
|
|
584
|
-
for (let c = minCol; c <= maxCol; c++) {
|
|
585
|
-
const col = cols[c];
|
|
586
|
-
if (!col) {
|
|
587
|
-
cells.push('');
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
const rawValue = row[col.dataIndex];
|
|
591
|
-
cells.push(formatCell ? formatCell(row, col, rawValue) : !rawValue ? '' : String(rawValue));
|
|
592
|
-
}
|
|
593
|
-
lines.push(cells.join('\t'));
|
|
594
|
-
}
|
|
595
|
-
const text = lines.join('\n');
|
|
596
|
-
|
|
597
|
-
navigator.clipboard.writeText(text).catch(() => {
|
|
598
|
-
console.warn('Failed to copy to clipboard');
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
return text;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function blurCellElement() {
|
|
605
|
-
// 防止虚拟滚动移除 DOM 时焦点被动丢失,导致后续 keydown 无法冒泡到容器
|
|
606
|
-
// en: Prevent focus loss when virtual scroll removes DOM, causing subsequent keydown events to not bubble to the container
|
|
607
|
-
const container = tableContainerRef.value;
|
|
608
|
-
const activeEl = document.activeElement as HTMLElement | null;
|
|
609
|
-
if (container && activeEl && container.contains(activeEl) && activeEl !== container) {
|
|
610
|
-
container.focus({ preventScroll: true });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Ctrl+C / Cmd+C copy
|
|
616
|
-
* Esc :cancel
|
|
617
|
-
* Arrow keys / Tab move (when keyboard=true)
|
|
618
|
-
**/
|
|
619
|
-
function onKeydown(e: KeyboardEvent) {
|
|
620
|
-
if (!config.value.enabled) return;
|
|
621
|
-
|
|
622
|
-
const key = e.key;
|
|
623
|
-
|
|
624
|
-
// Esc :cancel
|
|
625
|
-
if (key === KEY_ESCAPE || key === KEY_ESC) {
|
|
626
|
-
blurCellElement();
|
|
627
|
-
if (selectionRanges.value.length) {
|
|
628
|
-
// clearSelectedArea();
|
|
629
|
-
// emitSelectionChange();
|
|
630
|
-
e.preventDefault();
|
|
631
|
-
}
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Ctrl/Cmd+C copy
|
|
636
|
-
if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRanges.value.length) {
|
|
637
|
-
copySelectedArea();
|
|
638
|
-
e.preventDefault();
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (!keyboardEnabled.value) return;
|
|
643
|
-
|
|
644
|
-
const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key);
|
|
645
|
-
const isTabKey = key === KEY_TAB;
|
|
646
|
-
const isNavigationKey = isArrowKey || isTabKey;
|
|
647
|
-
|
|
648
|
-
if (!isNavigationKey) return;
|
|
649
|
-
|
|
650
|
-
e.preventDefault();
|
|
651
|
-
|
|
652
|
-
const rowCount = dataSourceCopy.value.length;
|
|
653
|
-
const colCount = tableHeaderLast.value.length;
|
|
654
|
-
if (rowCount === 0 || colCount === 0) return;
|
|
655
|
-
|
|
656
|
-
// 如果没有选区,默认从第一个单元格开始
|
|
657
|
-
// en: If no selection, start from the first cell
|
|
658
|
-
if (!selectionRanges.value.length) {
|
|
659
|
-
anchorCell = { rowIndex: 0, colIndex: 0 };
|
|
660
|
-
selectionRanges.value = [makeRange(0, 0, 0, 0)];
|
|
661
|
-
emitSelectionChange();
|
|
662
|
-
scrollToCell(0, 0);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// 计算移动方向
|
|
667
|
-
const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey);
|
|
668
|
-
|
|
669
|
-
// Shift 扩展选区,否则移动单格选区
|
|
670
|
-
if (e.shiftKey && isArrowKey && shiftEnabled.value) {
|
|
671
|
-
blurCellElement();
|
|
672
|
-
// 扩展选区:保留 begin,更新最后一个区域的 end
|
|
673
|
-
const ranges = [...selectionRanges.value];
|
|
674
|
-
const range = ranges.length > 0 ? ranges[ranges.length - 1] : null;
|
|
675
|
-
if (!range) return;
|
|
676
|
-
const { begin, end } = range.index;
|
|
677
|
-
let newEndRow = end.row + rowDelta;
|
|
678
|
-
let newEndCol = end.col + colDelta;
|
|
679
|
-
|
|
680
|
-
// 边界检查
|
|
681
|
-
newEndRow = clamp(newEndRow, 0, rowCount - 1);
|
|
682
|
-
newEndCol = clamp(newEndCol, 0, colCount - 1);
|
|
683
|
-
|
|
684
|
-
ranges[ranges.length - 1] = makeRange(begin.row, begin.col, newEndRow, newEndCol);
|
|
685
|
-
selectionRanges.value = ranges;
|
|
686
|
-
|
|
687
|
-
scrollToCell(newEndRow, newEndCol);
|
|
688
|
-
} else {
|
|
689
|
-
blurCellElement();
|
|
690
|
-
// 移动单格选区
|
|
691
|
-
// 取最后一个区域的 end 位置作为基础,清空旧选区重建
|
|
692
|
-
const ranges = selectionRanges.value;
|
|
693
|
-
const range = ranges.length > 0 ? ranges[ranges.length - 1] : null;
|
|
694
|
-
const baseRow = range ? normalizeRange(range).minRow : 0;
|
|
695
|
-
const baseCol = range ? normalizeRange(range).minCol : 0;
|
|
696
|
-
let newRow = baseRow + rowDelta;
|
|
697
|
-
let newCol = baseCol + colDelta;
|
|
698
|
-
|
|
699
|
-
// 边界检查(先检查,避免越界)
|
|
700
|
-
newRow = clamp(newRow, 0, rowCount - 1);
|
|
701
|
-
newCol = clamp(newCol, 0, colCount - 1);
|
|
702
|
-
|
|
703
|
-
// Tab 换行逻辑:如果到达行尾/行首,换行
|
|
704
|
-
if (isTabKey) {
|
|
705
|
-
// 计算原始未 clamp 的值
|
|
706
|
-
const rawCol = baseCol + colDelta;
|
|
707
|
-
const [tabRow, tabCol] = handleTabWrap(baseRow, newCol, rawCol, rowCount, colCount);
|
|
708
|
-
newRow = tabRow;
|
|
709
|
-
newCol = tabCol;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// 更新锚点和选区(移动单格时清空其他区域,仅保留新位置)
|
|
713
|
-
anchorCell = { rowIndex: newRow, colIndex: newCol };
|
|
714
|
-
selectionRanges.value = [makeRange(newRow, newCol, newRow, newCol)];
|
|
715
|
-
|
|
716
|
-
scrollToCell(newRow, newCol);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
emitSelectionChange();
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* 滚动到指定单元格,确保其在可视区域内
|
|
724
|
-
* @param rowIndex 行索引
|
|
725
|
-
* @param colIndex 列索引
|
|
726
|
-
*/
|
|
727
|
-
function scrollToCell(rowIndex: number, colIndex: number) {
|
|
728
|
-
const container = tableContainerRef.value;
|
|
729
|
-
if (!container) return;
|
|
730
|
-
|
|
731
|
-
const row = dataSourceCopy.value[rowIndex];
|
|
732
|
-
const col = tableHeaderLast.value[colIndex];
|
|
733
|
-
if (!row || !col) return;
|
|
734
|
-
|
|
735
|
-
const thead = container.querySelector('thead');
|
|
736
|
-
const headerHeight = thead ? thead.offsetHeight : 0;
|
|
737
|
-
const tfoot = container.querySelector('tfoot');
|
|
738
|
-
const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
|
739
|
-
|
|
740
|
-
const vs = virtualScroll.value;
|
|
741
|
-
const vsx = virtualScrollX.value;
|
|
742
|
-
|
|
743
|
-
// 是否开启按行滚动模式(experimental.scrollY 模式)
|
|
744
|
-
const isScrollRowByRow = props.scrollRowByRow;
|
|
745
|
-
|
|
746
|
-
// 计算目标行的位置(基于虚拟滚动数据)
|
|
747
|
-
const rowHeight = vs.rowHeight;
|
|
748
|
-
const targetRowTop = rowIndex * rowHeight;
|
|
749
|
-
const targetRowBottom = targetRowTop + rowHeight;
|
|
750
|
-
|
|
751
|
-
// 计算可视区域
|
|
752
|
-
// experimental.scrollY 模式下,容器 scrollTop 始终为 0,需要使用 virtualScroll.scrollTop
|
|
753
|
-
const visibleTop = isScrollRowByRow ? vs.scrollTop : container.scrollTop;
|
|
754
|
-
const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight;
|
|
755
|
-
|
|
756
|
-
// 计算需要的垂直滚动位置
|
|
757
|
-
let newScrollTop: number | null = null;
|
|
758
|
-
if (targetRowTop < visibleTop) {
|
|
759
|
-
// 目标行在可视区域上方,滚动到使目标行位于顶部
|
|
760
|
-
newScrollTop = targetRowTop;
|
|
761
|
-
} else if (targetRowBottom > visibleBottom) {
|
|
762
|
-
// 目标行在可视区域下方
|
|
763
|
-
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// 计算目标列的位置
|
|
767
|
-
const [targetColLeft, targetColWidth] = getColPosition(colIndex);
|
|
768
|
-
const targetColRight = targetColLeft + targetColWidth;
|
|
769
|
-
|
|
770
|
-
// 计算可视区域(水平)
|
|
771
|
-
const visibleLeft = container.scrollLeft;
|
|
772
|
-
const visibleRight = visibleLeft + vsx.containerWidth;
|
|
773
|
-
|
|
774
|
-
// 计算固定列的宽度(用于检测遮挡)
|
|
775
|
-
const [leftFixedWidth, rightFixedWidth] = getFixedColWidths.value(colIndex);
|
|
776
|
-
let newScrollLeft: number | null = null;
|
|
777
|
-
if (targetColLeft < visibleLeft + leftFixedWidth) {
|
|
778
|
-
// 目标列在左侧固定列遮挡区域内,需要向左滚动
|
|
779
|
-
newScrollLeft = targetColLeft - leftFixedWidth;
|
|
780
|
-
} else if (targetColRight > visibleRight - rightFixedWidth) {
|
|
781
|
-
// 目标列在右侧固定列遮挡区域内,需要向右滚动
|
|
782
|
-
newScrollLeft = targetColRight - vsx.containerWidth + rightFixedWidth;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (newScrollTop !== null || newScrollLeft !== null) {
|
|
786
|
-
scrollTo(newScrollTop, newScrollLeft);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* 判断一个单元格的选区样式类名
|
|
792
|
-
* @param cellKey 单元格唯一键
|
|
793
|
-
* @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引
|
|
794
|
-
* @param colKey 列唯一键
|
|
795
|
-
* @returns 样式类名数组
|
|
796
|
-
*/
|
|
797
|
-
function getAreaSelectionClasses(cellKey: string, absoluteRowIndex: number, colKey: UniqKey): string[] {
|
|
798
|
-
if (!highlightCellEnabled.value) return [];
|
|
799
|
-
if (!selectedCellKeys.value.has(cellKey)) return [];
|
|
800
|
-
|
|
801
|
-
const colIndex = colKeyToIndexMap.value.get(colKey);
|
|
802
|
-
if (colIndex === void 0 || colIndex < 0) return [];
|
|
803
|
-
|
|
804
|
-
const classes: string[] = [CELL_RANGE_SELECTED];
|
|
805
|
-
const ranges = selectionRanges.value;
|
|
806
|
-
if (!ranges.length) return classes;
|
|
807
|
-
|
|
808
|
-
const lastRange = ranges[ranges.length - 1];
|
|
809
|
-
const { minRow, maxRow, minCol, maxCol } = normalizeRange(lastRange);
|
|
810
|
-
|
|
811
|
-
// 判断当前单元格是否在最后一个区域内
|
|
812
|
-
const isInLastRange = absoluteRowIndex >= minRow && absoluteRowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
|
|
813
|
-
|
|
814
|
-
if (isInLastRange) {
|
|
815
|
-
if (absoluteRowIndex === minRow) classes.push(CELL_RANGE_TOP);
|
|
816
|
-
if (absoluteRowIndex === maxRow) classes.push(CELL_RANGE_BOTTOM);
|
|
817
|
-
if (colIndex === minCol) classes.push(CELL_RANGE_LEFT);
|
|
818
|
-
if (colIndex === maxCol) classes.push(CELL_RANGE_RIGHT);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
return classes;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* 判断一行的选区样式类名(行高亮)
|
|
826
|
-
* @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引
|
|
827
|
-
* @returns 样式类名数组
|
|
828
|
-
*/
|
|
829
|
-
function getAreaSelectionRowClasses(absoluteRowIndex: number): string[] {
|
|
830
|
-
if (!highlightRowEnabled.value) return [];
|
|
831
|
-
|
|
832
|
-
const ranges = selectionRanges.value;
|
|
833
|
-
if (!ranges.length) return [];
|
|
834
|
-
|
|
835
|
-
// 检查该行是否在任何选区内
|
|
836
|
-
for (const range of ranges) {
|
|
837
|
-
const {
|
|
838
|
-
begin: { row: r1 },
|
|
839
|
-
end: { row: r2 },
|
|
840
|
-
} = range.index;
|
|
841
|
-
const [minRow, maxRow] = r1 < r2 ? [r1, r2] : [r2, r1];
|
|
842
|
-
if (absoluteRowIndex >= minRow && absoluteRowIndex <= maxRow) {
|
|
843
|
-
return ['row-range-selected'];
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return [];
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// expose function
|
|
851
|
-
|
|
852
|
-
/** 获取选中的单元格信息 */
|
|
853
|
-
function getSelectedArea() {
|
|
854
|
-
const ranges = selectionRanges.value;
|
|
855
|
-
if (!ranges.length) return { rows: [] as DT[], cols: [] as StkTableColumn<DT>[], ranges: [] as AreaSelectionRange[] };
|
|
856
|
-
const data = dataSourceCopy.value;
|
|
857
|
-
const cols = tableHeaderLast.value;
|
|
858
|
-
// 收集所有区域的行和列
|
|
859
|
-
const rowSet = new Set<number>();
|
|
860
|
-
const colSet = new Set<number>();
|
|
861
|
-
for (const range of ranges) {
|
|
862
|
-
const { minRow, maxRow, minCol, maxCol } = normalizeRange(range);
|
|
863
|
-
for (let r = minRow; r <= maxRow; r++) rowSet.add(r);
|
|
864
|
-
for (let c = minCol; c <= maxCol; c++) colSet.add(c);
|
|
865
|
-
}
|
|
866
|
-
const sortedRows = [...rowSet].sort((a, b) => a - b);
|
|
867
|
-
const sortedCols = [...colSet].sort((a, b) => a - b);
|
|
868
|
-
return {
|
|
869
|
-
rows: sortedRows.map(i => data[i]).filter(Boolean),
|
|
870
|
-
cols: sortedCols.map(i => cols[i]).filter(Boolean),
|
|
871
|
-
ranges: ranges.map(r => ({ ...r })),
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function clearSelectedArea() {
|
|
876
|
-
selectionRanges.value = [];
|
|
877
|
-
isSelecting.value = false;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function setAreaSelection(ranges?: AreaSelectionSetterRange<DT>, option: AreaSelectionSetterOption = {}): AreaSelectionRange[] {
|
|
881
|
-
if (!config.value.enabled) return selectionRanges.value;
|
|
882
|
-
|
|
883
|
-
const { silent = false, scrollToView = false } = option;
|
|
884
|
-
const rowCount = dataSourceCopy.value.length;
|
|
885
|
-
const colCount = tableHeaderLast.value.length;
|
|
886
|
-
|
|
887
|
-
if (rowCount <= 0 || colCount <= 0) {
|
|
888
|
-
clearSelectedArea();
|
|
889
|
-
if (!silent) emitSelectionChange();
|
|
890
|
-
return selectionRanges.value;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const maxRow = rowCount - 1;
|
|
894
|
-
const maxCol = colCount - 1;
|
|
895
|
-
|
|
896
|
-
let beginRow = 0;
|
|
897
|
-
let endRow = maxRow;
|
|
898
|
-
let beginCol = 0;
|
|
899
|
-
let endCol = maxCol;
|
|
900
|
-
|
|
901
|
-
if (ranges) {
|
|
902
|
-
const begin = ranges.begin;
|
|
903
|
-
const end = ranges.end ?? begin;
|
|
904
|
-
|
|
905
|
-
beginRow = typeof begin.row === 'number' ? begin.row : getRowIndex(begin.row);
|
|
906
|
-
endRow = typeof end.row === 'number' ? end.row : getRowIndex(end.row);
|
|
907
|
-
|
|
908
|
-
const beginColInput = typeof begin.col === 'number' ? begin.col : begin.col ? getColumnIndex(begin.col) : void 0;
|
|
909
|
-
const endColInput = typeof end.col === 'number' ? end.col : end.col ? getColumnIndex(end.col) : void 0;
|
|
910
|
-
|
|
911
|
-
if (beginColInput !== void 0) {
|
|
912
|
-
beginCol = beginColInput;
|
|
913
|
-
endCol = endColInput !== void 0 ? endColInput : beginColInput;
|
|
914
|
-
} else if (endColInput !== void 0) {
|
|
915
|
-
beginCol = 0;
|
|
916
|
-
endCol = endColInput;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
beginRow = clamp(beginRow, 0, maxRow);
|
|
921
|
-
endRow = clamp(endRow, 0, maxRow);
|
|
922
|
-
beginCol = clamp(beginCol, 0, maxCol);
|
|
923
|
-
endCol = clamp(endCol, 0, maxCol);
|
|
924
|
-
|
|
925
|
-
selectionRanges.value = [makeRange(beginRow, beginCol, endRow, endCol)];
|
|
926
|
-
anchorCell = { rowIndex: beginRow, colIndex: beginCol };
|
|
927
|
-
isSelecting.value = false;
|
|
928
|
-
|
|
929
|
-
if (scrollToView) {
|
|
930
|
-
scrollToCell(endRow, endCol);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (!silent) emitSelectionChange();
|
|
934
|
-
return selectionRanges.value;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
return {
|
|
938
|
-
config,
|
|
939
|
-
isSelecting,
|
|
940
|
-
getClass: getAreaSelectionClasses,
|
|
941
|
-
getRowClass: getAreaSelectionRowClasses,
|
|
942
|
-
get: getSelectedArea,
|
|
943
|
-
set: setAreaSelection,
|
|
944
|
-
clear: clearSelectedArea,
|
|
945
|
-
copy: copySelectedArea,
|
|
946
|
-
onMD: onSelectionMouseDown,
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
export const useAreaSelectionName = 'useAreaSelection';
|
|
950
|
-
|
|
951
|
-
(useAreaSelection as any)[MY_FN_NAME] = useAreaSelectionName;
|