stk-table-vue 0.11.0 → 0.11.1
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/index-t5SJ6KNv.js
CHANGED
|
@@ -3,8 +3,9 @@ import { CellKeyGen, ColKeyGen, StkTableColumn, UniqKey } from '../types';
|
|
|
3
3
|
import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 单元格区域选择功能
|
|
7
|
+
* 支持鼠标拖拽选择、键盘导航、复制粘贴等功能
|
|
8
|
+
* en: Cell area selection feature with mouse drag, keyboard navigation, copy-paste, etc.
|
|
8
9
|
*/
|
|
9
10
|
export declare function useAreaSelection<DT extends Record<string, any>>(props: any, emits: any, tableContainerRef: Ref<HTMLDivElement | undefined>, dataSourceCopy: ShallowRef<DT[]>, tableHeaderLast: ShallowRef<StkTableColumn<DT>[]>, colKeyGen: ColKeyGen, cellKeyGen: CellKeyGen, scrollTo: (top: number | null, left: number | null) => void, virtualScroll: Ref<VirtualScrollStore>, virtualScrollX: Ref<VirtualScrollXStore>): {
|
|
10
11
|
isSelecting: Ref<boolean, boolean>;
|
package/lib/stk-table-vue.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v0.11.
|
|
3
|
+
* version: v0.11.1
|
|
4
4
|
* description: High performance realtime virtual table for vue3 and vue2.7
|
|
5
5
|
* author: japlus
|
|
6
6
|
* homepage: https://ja-plus.github.io/stk-table-vue/
|
|
@@ -338,6 +338,19 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
338
338
|
const EDGE_ZONE = 40;
|
|
339
339
|
const SCROLL_SPEED_MAX = 15;
|
|
340
340
|
const POINT_EDGE_OFFSET = 2;
|
|
341
|
+
const KEY_ARROW_UP = "ArrowUp";
|
|
342
|
+
const KEY_ARROW_DOWN = "ArrowDown";
|
|
343
|
+
const KEY_ARROW_LEFT = "ArrowLeft";
|
|
344
|
+
const KEY_ARROW_RIGHT = "ArrowRight";
|
|
345
|
+
const KEY_TAB = "Tab";
|
|
346
|
+
const KEY_ESCAPE = "Escape";
|
|
347
|
+
const KEY_ESC = "Esc";
|
|
348
|
+
const KEY_C = "c";
|
|
349
|
+
const CELL_RANGE_SELECTED = "cell-range-selected";
|
|
350
|
+
const CELL_RANGE_TOP = "cell-range-t";
|
|
351
|
+
const CELL_RANGE_BOTTOM = "cell-range-b";
|
|
352
|
+
const CELL_RANGE_LEFT = "cell-range-l";
|
|
353
|
+
const CELL_RANGE_RIGHT = "cell-range-r";
|
|
341
354
|
const selectionRange = ref(null);
|
|
342
355
|
const isSelecting = ref(false);
|
|
343
356
|
let anchorCell = null;
|
|
@@ -411,6 +424,78 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
411
424
|
if (!colKey) return -1;
|
|
412
425
|
return colKeyToIndexMap.value.get(colKey) ?? -1;
|
|
413
426
|
}
|
|
427
|
+
function getColPosition(colIndex) {
|
|
428
|
+
let l = 0;
|
|
429
|
+
let w = 0;
|
|
430
|
+
const cols = tableHeaderLast.value;
|
|
431
|
+
for (let i = 0; i < cols.length; i++) {
|
|
432
|
+
const colWidth = getCalculatedColWidth(cols[i]);
|
|
433
|
+
if (i < colIndex) {
|
|
434
|
+
l += colWidth;
|
|
435
|
+
} else if (i === colIndex) {
|
|
436
|
+
w = colWidth;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { l, w };
|
|
441
|
+
}
|
|
442
|
+
function getMovementDelta(key, shiftKey) {
|
|
443
|
+
let rowDelta = 0;
|
|
444
|
+
let colDelta = 0;
|
|
445
|
+
switch (key) {
|
|
446
|
+
case KEY_ARROW_UP:
|
|
447
|
+
rowDelta = -1;
|
|
448
|
+
break;
|
|
449
|
+
case KEY_ARROW_DOWN:
|
|
450
|
+
rowDelta = 1;
|
|
451
|
+
break;
|
|
452
|
+
case KEY_ARROW_LEFT:
|
|
453
|
+
colDelta = -1;
|
|
454
|
+
break;
|
|
455
|
+
case KEY_ARROW_RIGHT:
|
|
456
|
+
colDelta = 1;
|
|
457
|
+
break;
|
|
458
|
+
case KEY_TAB:
|
|
459
|
+
colDelta = shiftKey ? -1 : 1;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
return [rowDelta, colDelta];
|
|
463
|
+
}
|
|
464
|
+
function clamp(value, min, max) {
|
|
465
|
+
return Math.max(min, Math.min(value, max));
|
|
466
|
+
}
|
|
467
|
+
function handleTabWrap(row, col, rawCol, rowCount, colCount) {
|
|
468
|
+
let newRow = row;
|
|
469
|
+
let newCol = col;
|
|
470
|
+
if (rawCol >= colCount) {
|
|
471
|
+
newCol = 0;
|
|
472
|
+
newRow = Math.min(row + 1, rowCount - 1);
|
|
473
|
+
} else if (rawCol < 0) {
|
|
474
|
+
newCol = colCount - 1;
|
|
475
|
+
newRow = Math.max(row - 1, 0);
|
|
476
|
+
}
|
|
477
|
+
return [newRow, newCol];
|
|
478
|
+
}
|
|
479
|
+
function calculateAutoScrollDelta(mouseX, mouseY, rect) {
|
|
480
|
+
const { top, bottom, left, right } = rect;
|
|
481
|
+
let deltaX = 0;
|
|
482
|
+
let deltaY = 0;
|
|
483
|
+
if (mouseY < top + EDGE_ZONE) {
|
|
484
|
+
const dist = Math.max(0, top + EDGE_ZONE - mouseY);
|
|
485
|
+
deltaY = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
486
|
+
} else if (mouseY > bottom - EDGE_ZONE) {
|
|
487
|
+
const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE));
|
|
488
|
+
deltaY = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
489
|
+
}
|
|
490
|
+
if (mouseX < left + EDGE_ZONE) {
|
|
491
|
+
const dist = Math.max(0, left + EDGE_ZONE - mouseX);
|
|
492
|
+
deltaX = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
493
|
+
} else if (mouseX > right - EDGE_ZONE) {
|
|
494
|
+
const dist = Math.max(0, mouseX - (right - EDGE_ZONE));
|
|
495
|
+
deltaX = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
496
|
+
}
|
|
497
|
+
return { deltaX, deltaY };
|
|
498
|
+
}
|
|
414
499
|
function onSelectionMouseDown(e) {
|
|
415
500
|
if (!props.areaSelection || e.button !== 0) return;
|
|
416
501
|
const rowIndex = getClosestTrIndex(e.target);
|
|
@@ -484,23 +569,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
484
569
|
return;
|
|
485
570
|
}
|
|
486
571
|
const rect = container.getBoundingClientRect();
|
|
487
|
-
const {
|
|
488
|
-
let deltaX = 0;
|
|
489
|
-
let deltaY = 0;
|
|
490
|
-
if (lastMouseClientY < top + EDGE_ZONE) {
|
|
491
|
-
const dist = Math.max(0, top + EDGE_ZONE - lastMouseClientY);
|
|
492
|
-
deltaY = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
493
|
-
} else if (lastMouseClientY > bottom - EDGE_ZONE) {
|
|
494
|
-
const dist = Math.max(0, lastMouseClientY - (bottom - EDGE_ZONE));
|
|
495
|
-
deltaY = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
496
|
-
}
|
|
497
|
-
if (lastMouseClientX < left + EDGE_ZONE) {
|
|
498
|
-
const dist = Math.max(0, left + EDGE_ZONE - lastMouseClientX);
|
|
499
|
-
deltaX = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
500
|
-
} else if (lastMouseClientX > right - EDGE_ZONE) {
|
|
501
|
-
const dist = Math.max(0, lastMouseClientX - (right - EDGE_ZONE));
|
|
502
|
-
deltaX = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
503
|
-
}
|
|
572
|
+
const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect);
|
|
504
573
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
505
574
|
container.scrollTop += deltaY;
|
|
506
575
|
container.scrollLeft += deltaX;
|
|
@@ -597,7 +666,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
597
666
|
function onKeydown(e) {
|
|
598
667
|
if (!props.areaSelection) return;
|
|
599
668
|
const key = e.key;
|
|
600
|
-
if (key ===
|
|
669
|
+
if (key === KEY_ESCAPE || key === KEY_ESC) {
|
|
601
670
|
if (selectionRange.value) {
|
|
602
671
|
clearSelectedArea();
|
|
603
672
|
emitSelectionChange();
|
|
@@ -605,14 +674,14 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
605
674
|
}
|
|
606
675
|
return;
|
|
607
676
|
}
|
|
608
|
-
if ((e.ctrlKey || e.metaKey) && key ===
|
|
677
|
+
if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRange.value) {
|
|
609
678
|
copySelectedArea();
|
|
610
679
|
e.preventDefault();
|
|
611
680
|
return;
|
|
612
681
|
}
|
|
613
682
|
if (!keyboardEnabled.value) return;
|
|
614
|
-
const isArrowKey = [
|
|
615
|
-
const isTabKey = key ===
|
|
683
|
+
const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key);
|
|
684
|
+
const isTabKey = key === KEY_TAB;
|
|
616
685
|
const isNavigationKey = isArrowKey || isTabKey;
|
|
617
686
|
if (!isNavigationKey) return;
|
|
618
687
|
e.preventDefault();
|
|
@@ -631,25 +700,13 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
631
700
|
scrollToCell(0, 0);
|
|
632
701
|
return;
|
|
633
702
|
}
|
|
634
|
-
|
|
635
|
-
let colDelta = 0;
|
|
636
|
-
if (key === "ArrowUp") {
|
|
637
|
-
rowDelta = -1;
|
|
638
|
-
} else if (key === "ArrowDown") {
|
|
639
|
-
rowDelta = 1;
|
|
640
|
-
} else if (key === "ArrowLeft") {
|
|
641
|
-
colDelta = -1;
|
|
642
|
-
} else if (key === "ArrowRight") {
|
|
643
|
-
colDelta = 1;
|
|
644
|
-
} else if (key === "Tab") {
|
|
645
|
-
colDelta = e.shiftKey ? -1 : 1;
|
|
646
|
-
}
|
|
703
|
+
const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey);
|
|
647
704
|
if (e.shiftKey && isArrowKey) {
|
|
648
705
|
const range = selectionRange.value;
|
|
649
706
|
let newEndRow = range.endRowIndex + rowDelta;
|
|
650
707
|
let newEndCol = range.endColIndex + colDelta;
|
|
651
|
-
newEndRow =
|
|
652
|
-
newEndCol =
|
|
708
|
+
newEndRow = clamp(newEndRow, 0, rowCount - 1);
|
|
709
|
+
newEndCol = clamp(newEndCol, 0, colCount - 1);
|
|
653
710
|
selectionRange.value = {
|
|
654
711
|
...range,
|
|
655
712
|
endRowIndex: newEndRow,
|
|
@@ -661,17 +718,13 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
661
718
|
const { minRow, minCol } = normalizeRange(range);
|
|
662
719
|
let newRow = minRow + rowDelta;
|
|
663
720
|
let newCol = minCol + colDelta;
|
|
664
|
-
newRow =
|
|
665
|
-
newCol =
|
|
721
|
+
newRow = clamp(newRow, 0, rowCount - 1);
|
|
722
|
+
newCol = clamp(newCol, 0, colCount - 1);
|
|
666
723
|
if (isTabKey) {
|
|
667
724
|
const rawCol = minCol + colDelta;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
} else if (rawCol < 0) {
|
|
672
|
-
newCol = colCount - 1;
|
|
673
|
-
newRow = Math.max(minRow - 1, 0);
|
|
674
|
-
}
|
|
725
|
+
const [tabRow, tabCol] = handleTabWrap(minRow, newCol, rawCol, rowCount, colCount);
|
|
726
|
+
newRow = tabRow;
|
|
727
|
+
newCol = tabCol;
|
|
675
728
|
}
|
|
676
729
|
anchorCell = { rowIndex: newRow, colIndex: newCol };
|
|
677
730
|
selectionRange.value = {
|
|
@@ -692,31 +745,22 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
692
745
|
if (!row || !col) return;
|
|
693
746
|
const thead = container.querySelector("thead");
|
|
694
747
|
const headerHeight = thead ? thead.offsetHeight : 0;
|
|
748
|
+
const tfoot = container.querySelector("tfoot");
|
|
749
|
+
const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
|
695
750
|
const vs = virtualScroll.value;
|
|
696
751
|
const vsx = virtualScrollX.value;
|
|
697
752
|
const rowHeight = vs.rowHeight;
|
|
698
753
|
const targetRowTop = rowIndex * rowHeight;
|
|
699
754
|
const targetRowBottom = targetRowTop + rowHeight;
|
|
700
755
|
const visibleTop = container.scrollTop;
|
|
701
|
-
const visibleBottom = visibleTop + vs.containerHeight - headerHeight;
|
|
756
|
+
const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight;
|
|
702
757
|
let newScrollTop = null;
|
|
703
758
|
if (targetRowTop < visibleTop) {
|
|
704
759
|
newScrollTop = targetRowTop;
|
|
705
760
|
} else if (targetRowBottom > visibleBottom) {
|
|
706
|
-
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight);
|
|
707
|
-
}
|
|
708
|
-
let targetColLeft = 0;
|
|
709
|
-
let targetColWidth = 0;
|
|
710
|
-
const cols = tableHeaderLast.value;
|
|
711
|
-
for (let i = 0; i < cols.length; i++) {
|
|
712
|
-
const colWidth = getCalculatedColWidth(cols[i]) || 100;
|
|
713
|
-
if (i < colIndex) {
|
|
714
|
-
targetColLeft += colWidth;
|
|
715
|
-
} else if (i === colIndex) {
|
|
716
|
-
targetColWidth = colWidth;
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
761
|
+
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight);
|
|
719
762
|
}
|
|
763
|
+
const { l: targetColLeft, w: targetColWidth } = getColPosition(colIndex);
|
|
720
764
|
const targetColRight = targetColLeft + targetColWidth;
|
|
721
765
|
const visibleLeft = container.scrollLeft;
|
|
722
766
|
const visibleRight = visibleLeft + vsx.containerWidth;
|
|
@@ -735,11 +779,11 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
735
779
|
if (!nr || !selectedCellKeys.value.has(cellKey)) return [];
|
|
736
780
|
const colIndex = colKeyToIndexMap.value.get(colKey);
|
|
737
781
|
if (colIndex === void 0 || colIndex < 0) return [];
|
|
738
|
-
const classes = [
|
|
739
|
-
if (absoluteRowIndex === nr.minRow) classes.push(
|
|
740
|
-
if (absoluteRowIndex === nr.maxRow) classes.push(
|
|
741
|
-
if (colIndex === nr.minCol) classes.push(
|
|
742
|
-
if (colIndex === nr.maxCol) classes.push(
|
|
782
|
+
const classes = [CELL_RANGE_SELECTED];
|
|
783
|
+
if (absoluteRowIndex === nr.minRow) classes.push(CELL_RANGE_TOP);
|
|
784
|
+
if (absoluteRowIndex === nr.maxRow) classes.push(CELL_RANGE_BOTTOM);
|
|
785
|
+
if (colIndex === nr.minCol) classes.push(CELL_RANGE_LEFT);
|
|
786
|
+
if (colIndex === nr.maxCol) classes.push(CELL_RANGE_RIGHT);
|
|
743
787
|
return classes;
|
|
744
788
|
}
|
|
745
789
|
function getSelectedArea() {
|
package/lib/style.css
CHANGED
package/package.json
CHANGED
|
@@ -5,8 +5,9 @@ import { getClosestColKey, getClosestTrIndex } from '../utils';
|
|
|
5
5
|
import { getCalculatedColWidth } from '../utils/constRefUtils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 单元格区域选择功能
|
|
9
|
+
* 支持鼠标拖拽选择、键盘导航、复制粘贴等功能
|
|
10
|
+
* en: Cell area selection feature with mouse drag, keyboard navigation, copy-paste, etc.
|
|
10
11
|
*/
|
|
11
12
|
export function useAreaSelection<DT extends Record<string, any>>(
|
|
12
13
|
props: any,
|
|
@@ -30,9 +31,24 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
30
31
|
* en: Maximum scroll pixels per frame
|
|
31
32
|
*/
|
|
32
33
|
const SCROLL_SPEED_MAX = 15;
|
|
33
|
-
|
|
34
34
|
const POINT_EDGE_OFFSET = 2;
|
|
35
35
|
|
|
36
|
+
const KEY_ARROW_UP = 'ArrowUp';
|
|
37
|
+
const KEY_ARROW_DOWN = 'ArrowDown';
|
|
38
|
+
const KEY_ARROW_LEFT = 'ArrowLeft';
|
|
39
|
+
const KEY_ARROW_RIGHT = 'ArrowRight';
|
|
40
|
+
const KEY_TAB = 'Tab';
|
|
41
|
+
const KEY_ESCAPE = 'Escape';
|
|
42
|
+
const KEY_ESC = 'Esc';
|
|
43
|
+
const KEY_C = 'c';
|
|
44
|
+
|
|
45
|
+
// CSS
|
|
46
|
+
const CELL_RANGE_SELECTED = 'cell-range-selected';
|
|
47
|
+
const CELL_RANGE_TOP = 'cell-range-t';
|
|
48
|
+
const CELL_RANGE_BOTTOM = 'cell-range-b';
|
|
49
|
+
const CELL_RANGE_LEFT = 'cell-range-l';
|
|
50
|
+
const CELL_RANGE_RIGHT = 'cell-range-r';
|
|
51
|
+
|
|
36
52
|
/** 当前选区范围 */
|
|
37
53
|
const selectionRange = ref<AreaSelectionRange | null>(null) as Ref<AreaSelectionRange | null>;
|
|
38
54
|
/** 是否正在拖选 */
|
|
@@ -128,6 +144,98 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
128
144
|
return colKeyToIndexMap.value.get(colKey) ?? -1;
|
|
129
145
|
}
|
|
130
146
|
|
|
147
|
+
/** 获取列的左边距和宽度 */
|
|
148
|
+
function getColPosition(colIndex: number): { l: number; w: number } {
|
|
149
|
+
let l = 0;
|
|
150
|
+
let w = 0;
|
|
151
|
+
const cols = tableHeaderLast.value;
|
|
152
|
+
for (let i = 0; i < cols.length; i++) {
|
|
153
|
+
const colWidth = getCalculatedColWidth(cols[i]);
|
|
154
|
+
if (i < colIndex) {
|
|
155
|
+
l += colWidth;
|
|
156
|
+
} else if (i === colIndex) {
|
|
157
|
+
w = colWidth;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { l, w };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 根据按键计算移动方向 */
|
|
165
|
+
function getMovementDelta(key: string, shiftKey: boolean): [number, number] {
|
|
166
|
+
let rowDelta = 0;
|
|
167
|
+
let colDelta = 0;
|
|
168
|
+
|
|
169
|
+
switch (key) {
|
|
170
|
+
case KEY_ARROW_UP:
|
|
171
|
+
rowDelta = -1;
|
|
172
|
+
break;
|
|
173
|
+
case KEY_ARROW_DOWN:
|
|
174
|
+
rowDelta = 1;
|
|
175
|
+
break;
|
|
176
|
+
case KEY_ARROW_LEFT:
|
|
177
|
+
colDelta = -1;
|
|
178
|
+
break;
|
|
179
|
+
case KEY_ARROW_RIGHT:
|
|
180
|
+
colDelta = 1;
|
|
181
|
+
break;
|
|
182
|
+
case KEY_TAB:
|
|
183
|
+
// Tab: 向右移动;Shift+Tab: 向左移动
|
|
184
|
+
colDelta = shiftKey ? -1 : 1;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [rowDelta, colDelta];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 钳制值到指定范围内 */
|
|
192
|
+
function clamp(value: number, min: number, max: number): number {
|
|
193
|
+
return Math.max(min, Math.min(value, max));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 处理Tab键的换行逻辑 */
|
|
197
|
+
function handleTabWrap(row: number, col: number, rawCol: number, rowCount: number, colCount: number): [number, number] {
|
|
198
|
+
let newRow = row;
|
|
199
|
+
let newCol = col;
|
|
200
|
+
|
|
201
|
+
if (rawCol >= colCount) {
|
|
202
|
+
newCol = 0;
|
|
203
|
+
newRow = Math.min(row + 1, rowCount - 1);
|
|
204
|
+
} else if (rawCol < 0) {
|
|
205
|
+
newCol = colCount - 1;
|
|
206
|
+
newRow = Math.max(row - 1, 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [newRow, newCol];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 计算自动滚动的增量 */
|
|
213
|
+
function calculateAutoScrollDelta(mouseX: number, mouseY: number, rect: DOMRect): { deltaX: number; deltaY: number } {
|
|
214
|
+
const { top, bottom, left, right } = rect;
|
|
215
|
+
let deltaX = 0;
|
|
216
|
+
let deltaY = 0;
|
|
217
|
+
|
|
218
|
+
// Y方向
|
|
219
|
+
if (mouseY < top + EDGE_ZONE) {
|
|
220
|
+
const dist = Math.max(0, top + EDGE_ZONE - mouseY);
|
|
221
|
+
deltaY = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
222
|
+
} else if (mouseY > bottom - EDGE_ZONE) {
|
|
223
|
+
const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE));
|
|
224
|
+
deltaY = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// X方向
|
|
228
|
+
if (mouseX < left + EDGE_ZONE) {
|
|
229
|
+
const dist = Math.max(0, left + EDGE_ZONE - mouseX);
|
|
230
|
+
deltaX = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
231
|
+
} else if (mouseX > right - EDGE_ZONE) {
|
|
232
|
+
const dist = Math.max(0, mouseX - (right - EDGE_ZONE));
|
|
233
|
+
deltaX = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { deltaX, deltaY };
|
|
237
|
+
}
|
|
238
|
+
|
|
131
239
|
/** mousedown 处理:设置锚点,开始拖选 */
|
|
132
240
|
function onSelectionMouseDown(e: MouseEvent) {
|
|
133
241
|
if (!props.areaSelection || e.button !== 0) return;
|
|
@@ -238,27 +346,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
238
346
|
}
|
|
239
347
|
|
|
240
348
|
const rect = container.getBoundingClientRect();
|
|
241
|
-
const {
|
|
242
|
-
let deltaX = 0;
|
|
243
|
-
let deltaY = 0;
|
|
244
|
-
|
|
245
|
-
// Y方向
|
|
246
|
-
if (lastMouseClientY < top + EDGE_ZONE) {
|
|
247
|
-
const dist = Math.max(0, top + EDGE_ZONE - lastMouseClientY);
|
|
248
|
-
deltaY = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
249
|
-
} else if (lastMouseClientY > bottom - EDGE_ZONE) {
|
|
250
|
-
const dist = Math.max(0, lastMouseClientY - (bottom - EDGE_ZONE));
|
|
251
|
-
deltaY = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// X方向
|
|
255
|
-
if (lastMouseClientX < left + EDGE_ZONE) {
|
|
256
|
-
const dist = Math.max(0, left + EDGE_ZONE - lastMouseClientX);
|
|
257
|
-
deltaX = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
258
|
-
} else if (lastMouseClientX > right - EDGE_ZONE) {
|
|
259
|
-
const dist = Math.max(0, lastMouseClientX - (right - EDGE_ZONE));
|
|
260
|
-
deltaX = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
261
|
-
}
|
|
349
|
+
const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect);
|
|
262
350
|
|
|
263
351
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
264
352
|
container.scrollTop += deltaY;
|
|
@@ -401,7 +489,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
401
489
|
const key = e.key;
|
|
402
490
|
|
|
403
491
|
// Esc 键:取消当前选区
|
|
404
|
-
if (key ===
|
|
492
|
+
if (key === KEY_ESCAPE || key === KEY_ESC) {
|
|
405
493
|
if (selectionRange.value) {
|
|
406
494
|
clearSelectedArea();
|
|
407
495
|
emitSelectionChange();
|
|
@@ -411,7 +499,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
411
499
|
}
|
|
412
500
|
|
|
413
501
|
// Ctrl/Cmd+C 复制选区
|
|
414
|
-
if ((e.ctrlKey || e.metaKey) && key ===
|
|
502
|
+
if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRange.value) {
|
|
415
503
|
copySelectedArea();
|
|
416
504
|
e.preventDefault();
|
|
417
505
|
return;
|
|
@@ -420,8 +508,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
420
508
|
// 键盘导航(需要启用 keyboard 选项)
|
|
421
509
|
if (!keyboardEnabled.value) return;
|
|
422
510
|
|
|
423
|
-
const isArrowKey = [
|
|
424
|
-
const isTabKey = key ===
|
|
511
|
+
const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key);
|
|
512
|
+
const isTabKey = key === KEY_TAB;
|
|
425
513
|
const isNavigationKey = isArrowKey || isTabKey;
|
|
426
514
|
|
|
427
515
|
if (!isNavigationKey) return;
|
|
@@ -447,21 +535,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
447
535
|
}
|
|
448
536
|
|
|
449
537
|
// 计算移动方向
|
|
450
|
-
|
|
451
|
-
let colDelta = 0;
|
|
452
|
-
|
|
453
|
-
if (key === 'ArrowUp') {
|
|
454
|
-
rowDelta = -1;
|
|
455
|
-
} else if (key === 'ArrowDown') {
|
|
456
|
-
rowDelta = 1;
|
|
457
|
-
} else if (key === 'ArrowLeft') {
|
|
458
|
-
colDelta = -1;
|
|
459
|
-
} else if (key === 'ArrowRight') {
|
|
460
|
-
colDelta = 1;
|
|
461
|
-
} else if (key === 'Tab') {
|
|
462
|
-
// Tab: 向右移动;Shift+Tab: 向左移动
|
|
463
|
-
colDelta = e.shiftKey ? -1 : 1;
|
|
464
|
-
}
|
|
538
|
+
const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey);
|
|
465
539
|
|
|
466
540
|
// Shift 扩展选区,否则移动单格选区
|
|
467
541
|
if (e.shiftKey && isArrowKey) {
|
|
@@ -471,8 +545,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
471
545
|
let newEndCol = range.endColIndex + colDelta;
|
|
472
546
|
|
|
473
547
|
// 边界检查
|
|
474
|
-
newEndRow =
|
|
475
|
-
newEndCol =
|
|
548
|
+
newEndRow = clamp(newEndRow, 0, rowCount - 1);
|
|
549
|
+
newEndCol = clamp(newEndCol, 0, colCount - 1);
|
|
476
550
|
|
|
477
551
|
selectionRange.value = {
|
|
478
552
|
...range,
|
|
@@ -490,20 +564,16 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
490
564
|
let newCol = minCol + colDelta;
|
|
491
565
|
|
|
492
566
|
// 边界检查(先检查,避免越界)
|
|
493
|
-
newRow =
|
|
494
|
-
newCol =
|
|
567
|
+
newRow = clamp(newRow, 0, rowCount - 1);
|
|
568
|
+
newCol = clamp(newCol, 0, colCount - 1);
|
|
495
569
|
|
|
496
570
|
// Tab 换行逻辑:如果到达行尾/行首,换行
|
|
497
571
|
if (isTabKey) {
|
|
498
572
|
// 计算原始未 clamp 的值
|
|
499
573
|
const rawCol = minCol + colDelta;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
} else if (rawCol < 0) {
|
|
504
|
-
newCol = colCount - 1;
|
|
505
|
-
newRow = Math.max(minRow - 1, 0);
|
|
506
|
-
}
|
|
574
|
+
const [tabRow, tabCol] = handleTabWrap(minRow, newCol, rawCol, rowCount, colCount);
|
|
575
|
+
newRow = tabRow;
|
|
576
|
+
newCol = tabCol;
|
|
507
577
|
}
|
|
508
578
|
|
|
509
579
|
// 更新锚点和选区
|
|
@@ -523,6 +593,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
523
593
|
|
|
524
594
|
/**
|
|
525
595
|
* 滚动到指定单元格,确保其在可视区域内
|
|
596
|
+
* @param rowIndex 行索引
|
|
597
|
+
* @param colIndex 列索引
|
|
526
598
|
*/
|
|
527
599
|
function scrollToCell(rowIndex: number, colIndex: number) {
|
|
528
600
|
const container = tableContainerRef.value;
|
|
@@ -532,9 +604,10 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
532
604
|
const col = tableHeaderLast.value[colIndex];
|
|
533
605
|
if (!row || !col) return;
|
|
534
606
|
|
|
535
|
-
// 获取表头高度
|
|
536
607
|
const thead = container.querySelector('thead');
|
|
537
608
|
const headerHeight = thead ? thead.offsetHeight : 0;
|
|
609
|
+
const tfoot = container.querySelector('tfoot');
|
|
610
|
+
const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
|
538
611
|
|
|
539
612
|
const vs = virtualScroll.value;
|
|
540
613
|
const vsx = virtualScrollX.value;
|
|
@@ -546,7 +619,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
546
619
|
|
|
547
620
|
// 计算可视区域
|
|
548
621
|
const visibleTop = container.scrollTop;
|
|
549
|
-
const visibleBottom = visibleTop + vs.containerHeight - headerHeight;
|
|
622
|
+
const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight;
|
|
550
623
|
|
|
551
624
|
// 计算需要的垂直滚动位置
|
|
552
625
|
let newScrollTop: number | null = null;
|
|
@@ -555,22 +628,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
555
628
|
newScrollTop = targetRowTop;
|
|
556
629
|
} else if (targetRowBottom > visibleBottom) {
|
|
557
630
|
// 目标行在可视区域下方,滚动到使目标行位于底部
|
|
558
|
-
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight);
|
|
631
|
+
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight);
|
|
559
632
|
}
|
|
560
633
|
|
|
561
634
|
// 计算目标列的位置
|
|
562
|
-
|
|
563
|
-
let targetColWidth = 0;
|
|
564
|
-
const cols = tableHeaderLast.value;
|
|
565
|
-
for (let i = 0; i < cols.length; i++) {
|
|
566
|
-
const colWidth = getCalculatedColWidth(cols[i]) || 100; // 默认100px
|
|
567
|
-
if (i < colIndex) {
|
|
568
|
-
targetColLeft += colWidth;
|
|
569
|
-
} else if (i === colIndex) {
|
|
570
|
-
targetColWidth = colWidth;
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
635
|
+
const { l: targetColLeft, w: targetColWidth } = getColPosition(colIndex);
|
|
574
636
|
const targetColRight = targetColLeft + targetColWidth;
|
|
575
637
|
|
|
576
638
|
// 计算可视区域(水平)
|
|
@@ -594,10 +656,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
594
656
|
}
|
|
595
657
|
|
|
596
658
|
/**
|
|
597
|
-
*
|
|
659
|
+
* 判断一个单元格的选区样式类名
|
|
598
660
|
* @param cellKey 单元格唯一键
|
|
599
661
|
* @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引
|
|
600
662
|
* @param colKey 列唯一键
|
|
663
|
+
* @returns 样式类名数组
|
|
601
664
|
*/
|
|
602
665
|
function getAreaSelectionClasses(cellKey: string, absoluteRowIndex: number, colKey: UniqKey): string[] {
|
|
603
666
|
const nr = normalizedRange.value;
|
|
@@ -606,11 +669,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
606
669
|
const colIndex = colKeyToIndexMap.value.get(colKey);
|
|
607
670
|
if (colIndex === void 0 || colIndex < 0) return [];
|
|
608
671
|
|
|
609
|
-
const classes: string[] = [
|
|
610
|
-
if (absoluteRowIndex === nr.minRow) classes.push(
|
|
611
|
-
if (absoluteRowIndex === nr.maxRow) classes.push(
|
|
612
|
-
if (colIndex === nr.minCol) classes.push(
|
|
613
|
-
if (colIndex === nr.maxCol) classes.push(
|
|
672
|
+
const classes: string[] = [CELL_RANGE_SELECTED];
|
|
673
|
+
if (absoluteRowIndex === nr.minRow) classes.push(CELL_RANGE_TOP);
|
|
674
|
+
if (absoluteRowIndex === nr.maxRow) classes.push(CELL_RANGE_BOTTOM);
|
|
675
|
+
if (colIndex === nr.minCol) classes.push(CELL_RANGE_LEFT);
|
|
676
|
+
if (colIndex === nr.maxCol) classes.push(CELL_RANGE_RIGHT);
|
|
614
677
|
return classes;
|
|
615
678
|
}
|
|
616
679
|
|