stk-table-vue 0.11.0 → 0.11.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/index-t5SJ6KNv.js +1 -1
- package/lib/src/StkTable/features/useAreaSelection.d.ts +3 -2
- package/lib/src/StkTable/useScrollbar.d.ts +1 -1
- package/lib/src/StkTable/useVirtualScroll.d.ts +1 -1
- package/lib/stk-table-vue.js +184 -88
- package/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/StkTable/StkTable.vue +25 -6
- package/src/StkTable/features/useAreaSelection.ts +200 -82
- package/src/StkTable/useScrollbar.ts +2 -1
- package/src/StkTable/useVirtualScroll.ts +2 -1
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>;
|
|
@@ -18,7 +18,7 @@ export type ScrollbarOptions = {
|
|
|
18
18
|
* @param options 滚动条配置选项
|
|
19
19
|
* @returns 滚动条相关状态和方法
|
|
20
20
|
*/
|
|
21
|
-
export declare function useScrollbar(props: any, containerRef: Ref<HTMLDivElement | undefined>, virtualScroll: Ref<VirtualScrollStore>, virtualScrollX: Ref<VirtualScrollXStore>, updateVirtualScrollY: (sTop?: number) => void, scrollbarOptions: Ref<Required<ScrollbarOptions
|
|
21
|
+
export declare function useScrollbar(props: any, containerRef: Ref<HTMLDivElement | undefined>, virtualScroll: Ref<VirtualScrollStore>, virtualScrollX: Ref<VirtualScrollXStore>, updateVirtualScrollY: (sTop?: number) => void, scrollbarOptions: Ref<Required<ScrollbarOptions>>, isExperimentalScrollY: Ref<boolean | undefined>): readonly [Ref<{
|
|
22
22
|
h: number;
|
|
23
23
|
w: number;
|
|
24
24
|
t: number;
|
|
@@ -41,7 +41,7 @@ export type VirtualScrollXStore = {
|
|
|
41
41
|
* virtual scroll
|
|
42
42
|
* @returns
|
|
43
43
|
*/
|
|
44
|
-
export declare function useVirtualScroll<DT extends Record<string, any>>(props: any, tableContainerRef: Ref<HTMLElement | undefined>, trRef: Ref<HTMLTableRowElement[] | undefined>, dataSourceCopy: ShallowRef<PrivateRowDT[]>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>, tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>, rowKeyGen: RowKeyGen, maxRowSpan: Map<UniqKey, number>, scrollbarOptions: Ref<Required<ScrollbarOptions
|
|
44
|
+
export declare function useVirtualScroll<DT extends Record<string, any>>(props: any, tableContainerRef: Ref<HTMLElement | undefined>, trRef: Ref<HTMLTableRowElement[] | undefined>, dataSourceCopy: ShallowRef<PrivateRowDT[]>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>, tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>, rowKeyGen: RowKeyGen, maxRowSpan: Map<UniqKey, number>, scrollbarOptions: Ref<Required<ScrollbarOptions>>, isExperimentalScrollY: Ref<boolean | undefined>): readonly [Ref<{
|
|
45
45
|
containerHeight: number;
|
|
46
46
|
pageSize: number;
|
|
47
47
|
startIndex: number;
|
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.2
|
|
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;
|
|
@@ -352,6 +365,42 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
352
365
|
}
|
|
353
366
|
return map;
|
|
354
367
|
});
|
|
368
|
+
const getFixedColWidths = computed(() => {
|
|
369
|
+
const cols = tableHeaderLast.value;
|
|
370
|
+
const leftAccumulated = [];
|
|
371
|
+
const rightAccumulated = [];
|
|
372
|
+
let leftSum = 0;
|
|
373
|
+
let rightSum = 0;
|
|
374
|
+
for (let i = 0, j = cols.length - 1; i < cols.length; i++, j--) {
|
|
375
|
+
const leftCol = cols[i];
|
|
376
|
+
const rightCol = cols[j];
|
|
377
|
+
if ((leftCol == null ? void 0 : leftCol.fixed) === "left") {
|
|
378
|
+
leftSum += getCalculatedColWidth(leftCol);
|
|
379
|
+
leftAccumulated.push({ i, w: leftSum });
|
|
380
|
+
}
|
|
381
|
+
if ((rightCol == null ? void 0 : rightCol.fixed) === "right") {
|
|
382
|
+
rightSum += getCalculatedColWidth(rightCol);
|
|
383
|
+
rightAccumulated.unshift({ i: j, w: rightSum });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return (colIndex) => {
|
|
387
|
+
let leftFixedWidth = 0;
|
|
388
|
+
for (let i = leftAccumulated.length - 1; i >= 0; i--) {
|
|
389
|
+
if (leftAccumulated[i].i < colIndex) {
|
|
390
|
+
leftFixedWidth = leftAccumulated[i].w;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
let rightFixedWidth = 0;
|
|
395
|
+
for (let i = rightAccumulated.length - 1; i >= 0; i--) {
|
|
396
|
+
if (rightAccumulated[i].i > colIndex) {
|
|
397
|
+
rightFixedWidth = rightAccumulated[i].w;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [leftFixedWidth, rightFixedWidth];
|
|
402
|
+
};
|
|
403
|
+
});
|
|
355
404
|
const selectedCellKeys = computed(() => {
|
|
356
405
|
const range = selectionRange.value;
|
|
357
406
|
if (!range) return /* @__PURE__ */ new Set();
|
|
@@ -411,6 +460,78 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
411
460
|
if (!colKey) return -1;
|
|
412
461
|
return colKeyToIndexMap.value.get(colKey) ?? -1;
|
|
413
462
|
}
|
|
463
|
+
function getColPosition(colIndex) {
|
|
464
|
+
let l = 0;
|
|
465
|
+
let w = 0;
|
|
466
|
+
const cols = tableHeaderLast.value;
|
|
467
|
+
for (let i = 0; i < cols.length; i++) {
|
|
468
|
+
const colWidth = getCalculatedColWidth(cols[i]);
|
|
469
|
+
if (i < colIndex) {
|
|
470
|
+
l += colWidth;
|
|
471
|
+
} else if (i === colIndex) {
|
|
472
|
+
w = colWidth;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { l, w };
|
|
477
|
+
}
|
|
478
|
+
function getMovementDelta(key, shiftKey) {
|
|
479
|
+
let rowDelta = 0;
|
|
480
|
+
let colDelta = 0;
|
|
481
|
+
switch (key) {
|
|
482
|
+
case KEY_ARROW_UP:
|
|
483
|
+
rowDelta = -1;
|
|
484
|
+
break;
|
|
485
|
+
case KEY_ARROW_DOWN:
|
|
486
|
+
rowDelta = 1;
|
|
487
|
+
break;
|
|
488
|
+
case KEY_ARROW_LEFT:
|
|
489
|
+
colDelta = -1;
|
|
490
|
+
break;
|
|
491
|
+
case KEY_ARROW_RIGHT:
|
|
492
|
+
colDelta = 1;
|
|
493
|
+
break;
|
|
494
|
+
case KEY_TAB:
|
|
495
|
+
colDelta = shiftKey ? -1 : 1;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
return [rowDelta, colDelta];
|
|
499
|
+
}
|
|
500
|
+
function clamp(value, min, max) {
|
|
501
|
+
return Math.max(min, Math.min(value, max));
|
|
502
|
+
}
|
|
503
|
+
function handleTabWrap(row, col, rawCol, rowCount, colCount) {
|
|
504
|
+
let newRow = row;
|
|
505
|
+
let newCol = col;
|
|
506
|
+
if (rawCol >= colCount) {
|
|
507
|
+
newCol = 0;
|
|
508
|
+
newRow = Math.min(row + 1, rowCount - 1);
|
|
509
|
+
} else if (rawCol < 0) {
|
|
510
|
+
newCol = colCount - 1;
|
|
511
|
+
newRow = Math.max(row - 1, 0);
|
|
512
|
+
}
|
|
513
|
+
return [newRow, newCol];
|
|
514
|
+
}
|
|
515
|
+
function calculateAutoScrollDelta(mouseX, mouseY, rect) {
|
|
516
|
+
const { top, bottom, left, right } = rect;
|
|
517
|
+
let deltaX = 0;
|
|
518
|
+
let deltaY = 0;
|
|
519
|
+
if (mouseY < top + EDGE_ZONE) {
|
|
520
|
+
const dist = Math.max(0, top + EDGE_ZONE - mouseY);
|
|
521
|
+
deltaY = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
522
|
+
} else if (mouseY > bottom - EDGE_ZONE) {
|
|
523
|
+
const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE));
|
|
524
|
+
deltaY = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
525
|
+
}
|
|
526
|
+
if (mouseX < left + EDGE_ZONE) {
|
|
527
|
+
const dist = Math.max(0, left + EDGE_ZONE - mouseX);
|
|
528
|
+
deltaX = -Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
529
|
+
} else if (mouseX > right - EDGE_ZONE) {
|
|
530
|
+
const dist = Math.max(0, mouseX - (right - EDGE_ZONE));
|
|
531
|
+
deltaX = Math.ceil(dist / EDGE_ZONE * SCROLL_SPEED_MAX);
|
|
532
|
+
}
|
|
533
|
+
return { deltaX, deltaY };
|
|
534
|
+
}
|
|
414
535
|
function onSelectionMouseDown(e) {
|
|
415
536
|
if (!props.areaSelection || e.button !== 0) return;
|
|
416
537
|
const rowIndex = getClosestTrIndex(e.target);
|
|
@@ -484,23 +605,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
484
605
|
return;
|
|
485
606
|
}
|
|
486
607
|
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
|
-
}
|
|
608
|
+
const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect);
|
|
504
609
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
505
610
|
container.scrollTop += deltaY;
|
|
506
611
|
container.scrollLeft += deltaX;
|
|
@@ -597,7 +702,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
597
702
|
function onKeydown(e) {
|
|
598
703
|
if (!props.areaSelection) return;
|
|
599
704
|
const key = e.key;
|
|
600
|
-
if (key ===
|
|
705
|
+
if (key === KEY_ESCAPE || key === KEY_ESC) {
|
|
601
706
|
if (selectionRange.value) {
|
|
602
707
|
clearSelectedArea();
|
|
603
708
|
emitSelectionChange();
|
|
@@ -605,14 +710,14 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
605
710
|
}
|
|
606
711
|
return;
|
|
607
712
|
}
|
|
608
|
-
if ((e.ctrlKey || e.metaKey) && key ===
|
|
713
|
+
if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRange.value) {
|
|
609
714
|
copySelectedArea();
|
|
610
715
|
e.preventDefault();
|
|
611
716
|
return;
|
|
612
717
|
}
|
|
613
718
|
if (!keyboardEnabled.value) return;
|
|
614
|
-
const isArrowKey = [
|
|
615
|
-
const isTabKey = key ===
|
|
719
|
+
const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key);
|
|
720
|
+
const isTabKey = key === KEY_TAB;
|
|
616
721
|
const isNavigationKey = isArrowKey || isTabKey;
|
|
617
722
|
if (!isNavigationKey) return;
|
|
618
723
|
e.preventDefault();
|
|
@@ -631,25 +736,13 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
631
736
|
scrollToCell(0, 0);
|
|
632
737
|
return;
|
|
633
738
|
}
|
|
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
|
-
}
|
|
739
|
+
const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey);
|
|
647
740
|
if (e.shiftKey && isArrowKey) {
|
|
648
741
|
const range = selectionRange.value;
|
|
649
742
|
let newEndRow = range.endRowIndex + rowDelta;
|
|
650
743
|
let newEndCol = range.endColIndex + colDelta;
|
|
651
|
-
newEndRow =
|
|
652
|
-
newEndCol =
|
|
744
|
+
newEndRow = clamp(newEndRow, 0, rowCount - 1);
|
|
745
|
+
newEndCol = clamp(newEndCol, 0, colCount - 1);
|
|
653
746
|
selectionRange.value = {
|
|
654
747
|
...range,
|
|
655
748
|
endRowIndex: newEndRow,
|
|
@@ -661,17 +754,13 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
661
754
|
const { minRow, minCol } = normalizeRange(range);
|
|
662
755
|
let newRow = minRow + rowDelta;
|
|
663
756
|
let newCol = minCol + colDelta;
|
|
664
|
-
newRow =
|
|
665
|
-
newCol =
|
|
757
|
+
newRow = clamp(newRow, 0, rowCount - 1);
|
|
758
|
+
newCol = clamp(newCol, 0, colCount - 1);
|
|
666
759
|
if (isTabKey) {
|
|
667
760
|
const rawCol = minCol + colDelta;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
} else if (rawCol < 0) {
|
|
672
|
-
newCol = colCount - 1;
|
|
673
|
-
newRow = Math.max(minRow - 1, 0);
|
|
674
|
-
}
|
|
761
|
+
const [tabRow, tabCol] = handleTabWrap(minRow, newCol, rawCol, rowCount, colCount);
|
|
762
|
+
newRow = tabRow;
|
|
763
|
+
newCol = tabCol;
|
|
675
764
|
}
|
|
676
765
|
anchorCell = { rowIndex: newRow, colIndex: newCol };
|
|
677
766
|
selectionRange.value = {
|
|
@@ -692,39 +781,31 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
692
781
|
if (!row || !col) return;
|
|
693
782
|
const thead = container.querySelector("thead");
|
|
694
783
|
const headerHeight = thead ? thead.offsetHeight : 0;
|
|
784
|
+
const tfoot = container.querySelector("tfoot");
|
|
785
|
+
const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
|
695
786
|
const vs = virtualScroll.value;
|
|
696
787
|
const vsx = virtualScrollX.value;
|
|
697
788
|
const rowHeight = vs.rowHeight;
|
|
698
789
|
const targetRowTop = rowIndex * rowHeight;
|
|
699
790
|
const targetRowBottom = targetRowTop + rowHeight;
|
|
700
791
|
const visibleTop = container.scrollTop;
|
|
701
|
-
const visibleBottom = visibleTop + vs.containerHeight - headerHeight;
|
|
792
|
+
const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight;
|
|
702
793
|
let newScrollTop = null;
|
|
703
794
|
if (targetRowTop < visibleTop) {
|
|
704
795
|
newScrollTop = targetRowTop;
|
|
705
796
|
} 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
|
-
}
|
|
797
|
+
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight);
|
|
719
798
|
}
|
|
799
|
+
const { l: targetColLeft, w: targetColWidth } = getColPosition(colIndex);
|
|
720
800
|
const targetColRight = targetColLeft + targetColWidth;
|
|
721
801
|
const visibleLeft = container.scrollLeft;
|
|
722
802
|
const visibleRight = visibleLeft + vsx.containerWidth;
|
|
803
|
+
const [leftFixedWidth, rightFixedWidth] = getFixedColWidths.value(colIndex);
|
|
723
804
|
let newScrollLeft = null;
|
|
724
|
-
if (targetColLeft < visibleLeft) {
|
|
725
|
-
newScrollLeft = targetColLeft;
|
|
726
|
-
} else if (targetColRight > visibleRight) {
|
|
727
|
-
newScrollLeft = targetColRight - vsx.containerWidth;
|
|
805
|
+
if (targetColLeft < visibleLeft + leftFixedWidth) {
|
|
806
|
+
newScrollLeft = targetColLeft - leftFixedWidth;
|
|
807
|
+
} else if (targetColRight > visibleRight - rightFixedWidth) {
|
|
808
|
+
newScrollLeft = targetColRight - vsx.containerWidth + rightFixedWidth;
|
|
728
809
|
}
|
|
729
810
|
if (newScrollTop !== null || newScrollLeft !== null) {
|
|
730
811
|
scrollTo(newScrollTop, newScrollLeft);
|
|
@@ -735,11 +816,11 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
|
|
|
735
816
|
if (!nr || !selectedCellKeys.value.has(cellKey)) return [];
|
|
736
817
|
const colIndex = colKeyToIndexMap.value.get(colKey);
|
|
737
818
|
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(
|
|
819
|
+
const classes = [CELL_RANGE_SELECTED];
|
|
820
|
+
if (absoluteRowIndex === nr.minRow) classes.push(CELL_RANGE_TOP);
|
|
821
|
+
if (absoluteRowIndex === nr.maxRow) classes.push(CELL_RANGE_BOTTOM);
|
|
822
|
+
if (colIndex === nr.minCol) classes.push(CELL_RANGE_LEFT);
|
|
823
|
+
if (colIndex === nr.maxCol) classes.push(CELL_RANGE_RIGHT);
|
|
743
824
|
return classes;
|
|
744
825
|
}
|
|
745
826
|
function getSelectedArea() {
|
|
@@ -1542,7 +1623,7 @@ function useRowExpand(emits, dataSourceCopy, rowKeyGen) {
|
|
|
1542
1623
|
}
|
|
1543
1624
|
return [toggleExpandRow, setRowExpand];
|
|
1544
1625
|
}
|
|
1545
|
-
function useScrollbar(props, containerRef, virtualScroll, virtualScrollX, updateVirtualScrollY, scrollbarOptions) {
|
|
1626
|
+
function useScrollbar(props, containerRef, virtualScroll, virtualScrollX, updateVirtualScrollY, scrollbarOptions, isExperimentalScrollY) {
|
|
1546
1627
|
const showScrollbar = ref({ x: false, y: false });
|
|
1547
1628
|
const scrollbar = ref({ h: 0, w: 0, t: 0, l: 0 });
|
|
1548
1629
|
let isDraggingVertical = false;
|
|
@@ -1606,7 +1687,6 @@ function useScrollbar(props, containerRef, virtualScroll, virtualScrollX, update
|
|
|
1606
1687
|
document.addEventListener("touchend", onDragEnd);
|
|
1607
1688
|
}
|
|
1608
1689
|
function onVerticalDrag(e) {
|
|
1609
|
-
var _a;
|
|
1610
1690
|
if (!isDraggingVertical) return;
|
|
1611
1691
|
e.preventDefault();
|
|
1612
1692
|
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
|
@@ -1615,7 +1695,7 @@ function useScrollbar(props, containerRef, virtualScroll, virtualScrollX, update
|
|
|
1615
1695
|
const scrollRange = scrollHeight - containerHeight;
|
|
1616
1696
|
const trackRange = containerHeight - scrollbar.value.h;
|
|
1617
1697
|
const scrollDelta = deltaY / trackRange * scrollRange;
|
|
1618
|
-
if (
|
|
1698
|
+
if (isExperimentalScrollY.value) {
|
|
1619
1699
|
const ratio = containerHeight / scrollHeight;
|
|
1620
1700
|
const top = Math.round((dragStartTop + scrollDelta) * ratio);
|
|
1621
1701
|
const maxTop = containerHeight - scrollbar.value.h;
|
|
@@ -2062,7 +2142,7 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits) {
|
|
|
2062
2142
|
return [toggleTreeNode, setTreeExpand, flatTreeData];
|
|
2063
2143
|
}
|
|
2064
2144
|
const VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
2065
|
-
function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, scrollbarOptions) {
|
|
2145
|
+
function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, scrollbarOptions, isExperimentalScrollY) {
|
|
2066
2146
|
const tableHeaderHeight = computed(() => props.headerRowHeight * tableHeaders.value.length);
|
|
2067
2147
|
const virtualScroll = ref({
|
|
2068
2148
|
containerHeight: 0,
|
|
@@ -2222,7 +2302,6 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
2222
2302
|
}
|
|
2223
2303
|
}
|
|
2224
2304
|
function updateVirtualScrollY(sTop = 0) {
|
|
2225
|
-
var _a;
|
|
2226
2305
|
const { pageSize, scrollTop, startIndex: oldStartIndex, endIndex: oldEndIndex, containerHeight } = virtualScroll.value;
|
|
2227
2306
|
const dataSourceCopyTemp = dataSourceCopy.value;
|
|
2228
2307
|
const dataLength = dataSourceCopyTemp.length;
|
|
@@ -2232,7 +2311,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
2232
2311
|
const { enabled: scrollbarEnable } = scrollbarOptions.value;
|
|
2233
2312
|
if (scrollbarEnable) {
|
|
2234
2313
|
vsValue.scrollHeight = scrollHeight;
|
|
2235
|
-
if (
|
|
2314
|
+
if (isExperimentalScrollY.value) {
|
|
2236
2315
|
let maxTop;
|
|
2237
2316
|
sTop = sTop < 0 ? 0 : sTop < (maxTop = scrollHeight - containerHeight) ? sTop : maxTop;
|
|
2238
2317
|
vsValue.translateY = props.scrollRowByRow ? 0 : -(sTop % rowHeight);
|
|
@@ -2603,6 +2682,13 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
2603
2682
|
height: 8,
|
|
2604
2683
|
...typeof props.scrollbar === "boolean" ? { enabled: props.scrollbar } : props.scrollbar
|
|
2605
2684
|
}));
|
|
2685
|
+
const isExperimentalScrollY = computed(() => {
|
|
2686
|
+
var _a, _b;
|
|
2687
|
+
if (((_a = scrollbarOptions.value) == null ? void 0 : _a.enabled) && props.scrollRowByRow) {
|
|
2688
|
+
return true;
|
|
2689
|
+
}
|
|
2690
|
+
return (_b = props.experimental) == null ? void 0 : _b.scrollY;
|
|
2691
|
+
});
|
|
2606
2692
|
const rowKeyGenCache = /* @__PURE__ */ new WeakMap();
|
|
2607
2693
|
const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
|
|
2608
2694
|
const [onThDragStart, onThDragOver, onThDrop, isHeaderDraggable] = useThDrag(props, emits, colKeyGen);
|
|
@@ -2625,7 +2711,18 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
2625
2711
|
updateVirtualScrollX,
|
|
2626
2712
|
setAutoHeight,
|
|
2627
2713
|
clearAllAutoHeight
|
|
2628
|
-
] = useVirtualScroll(
|
|
2714
|
+
] = useVirtualScroll(
|
|
2715
|
+
props,
|
|
2716
|
+
tableContainerRef,
|
|
2717
|
+
trRef,
|
|
2718
|
+
dataSourceCopy,
|
|
2719
|
+
tableHeaderLast,
|
|
2720
|
+
tableHeaders,
|
|
2721
|
+
rowKeyGen,
|
|
2722
|
+
maxRowSpan,
|
|
2723
|
+
scrollbarOptions,
|
|
2724
|
+
isExperimentalScrollY
|
|
2725
|
+
);
|
|
2629
2726
|
const rafUpdateVirtualScrollYForWheel = rafThrottle((scrollTop) => {
|
|
2630
2727
|
updateVirtualScrollY(scrollTop);
|
|
2631
2728
|
});
|
|
@@ -2635,7 +2732,8 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
2635
2732
|
virtualScroll,
|
|
2636
2733
|
virtualScrollX,
|
|
2637
2734
|
updateVirtualScrollY,
|
|
2638
|
-
scrollbarOptions
|
|
2735
|
+
scrollbarOptions,
|
|
2736
|
+
isExperimentalScrollY
|
|
2639
2737
|
);
|
|
2640
2738
|
const [hiddenCellMap, mergeCellsWrapper, hoverMergedCells, updateHoverMergedCells, activeMergedCells, updateActiveMergedCells] = useMergeCells(
|
|
2641
2739
|
rowActiveProp,
|
|
@@ -3107,7 +3205,6 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3107
3205
|
}
|
|
3108
3206
|
const [isWheeling, setIsWheeling] = useWheeling();
|
|
3109
3207
|
function onTableWheel(e) {
|
|
3110
|
-
var _a;
|
|
3111
3208
|
if (props.smoothScroll) return;
|
|
3112
3209
|
if (isColResizing.value) {
|
|
3113
3210
|
e.stopPropagation();
|
|
@@ -3125,7 +3222,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3125
3222
|
if (isWheeling()) {
|
|
3126
3223
|
e.preventDefault();
|
|
3127
3224
|
}
|
|
3128
|
-
if (
|
|
3225
|
+
if (isExperimentalScrollY.value) {
|
|
3129
3226
|
rafUpdateVirtualScrollYForWheel(scrollTop + deltaY);
|
|
3130
3227
|
updateCustomScrollbar();
|
|
3131
3228
|
} else {
|
|
@@ -3443,7 +3540,6 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3443
3540
|
setFilter
|
|
3444
3541
|
});
|
|
3445
3542
|
return (_ctx, _cache) => {
|
|
3446
|
-
var _a, _b, _c, _d;
|
|
3447
3543
|
return openBlock(), createElementBlock("div", {
|
|
3448
3544
|
ref_key: "tableContainerRef",
|
|
3449
3545
|
ref: tableContainerRef,
|
|
@@ -3470,7 +3566,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3470
3566
|
"scroll-row-by-row": unref(isSRBRActive),
|
|
3471
3567
|
"scrollbar-on": scrollbarOptions.value.enabled,
|
|
3472
3568
|
"is-area-selecting": unref(isAreaSelecting),
|
|
3473
|
-
"exp-scroll-y":
|
|
3569
|
+
"exp-scroll-y": isExperimentalScrollY.value
|
|
3474
3570
|
}]),
|
|
3475
3571
|
tabindex: props.areaSelection ? 0 : void 0,
|
|
3476
3572
|
style: normalizeStyle({
|
|
@@ -3485,7 +3581,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3485
3581
|
onScroll: onTableScroll,
|
|
3486
3582
|
onWheel: onTableWheel
|
|
3487
3583
|
}, [
|
|
3488
|
-
!
|
|
3584
|
+
!isExperimentalScrollY.value && SRBRTotalHeight.value ? (openBlock(), createElementBlock("div", {
|
|
3489
3585
|
key: 0,
|
|
3490
3586
|
class: "row-by-row-table-height",
|
|
3491
3587
|
style: normalizeStyle(`height: ${SRBRTotalHeight.value}px`)
|
|
@@ -3612,7 +3708,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3612
3708
|
onMousedown: onCellMouseDown,
|
|
3613
3709
|
onMouseover: onCellMouseOver
|
|
3614
3710
|
}, [
|
|
3615
|
-
!
|
|
3711
|
+
!isExperimentalScrollY.value && unref(virtual_on) && !unref(isSRBRActive) ? (openBlock(), createElementBlock("tr", {
|
|
3616
3712
|
key: 0,
|
|
3617
3713
|
style: normalizeStyle(`height:${unref(virtualScroll).offsetTop}px`),
|
|
3618
3714
|
class: "padding-top-tr"
|
|
@@ -3645,16 +3741,16 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3645
3741
|
row: row.__EXP_R__,
|
|
3646
3742
|
col: row.__EXP_C__
|
|
3647
3743
|
}, () => {
|
|
3648
|
-
var
|
|
3744
|
+
var _a;
|
|
3649
3745
|
return [
|
|
3650
|
-
createTextVNode(toDisplayString(((
|
|
3746
|
+
createTextVNode(toDisplayString(((_a = row.__EXP_R__) == null ? void 0 : _a[row.__EXP_C__.dataIndex]) ?? ""), 1)
|
|
3651
3747
|
];
|
|
3652
3748
|
})
|
|
3653
3749
|
])
|
|
3654
3750
|
], 8, _hoisted_16)) : (openBlock(true), createElementBlock(Fragment, { key: 2 }, renderList(unref(virtualX_columnPart), (col, colIndex) => {
|
|
3655
|
-
var
|
|
3751
|
+
var _a;
|
|
3656
3752
|
return openBlock(), createElementBlock(Fragment, null, [
|
|
3657
|
-
!unref(hiddenCellMap) || !((
|
|
3753
|
+
!unref(hiddenCellMap) || !((_a = unref(hiddenCellMap)[rowKeyGen(row)]) == null ? void 0 : _a.has(colKeyGen.value(col))) ? (openBlock(), createElementBlock("td", mergeProps({
|
|
3658
3754
|
key: colKeyGen.value(col)
|
|
3659
3755
|
}, { ref_for: true }, getTDProps(row, col, rowIndex, colIndex), {
|
|
3660
3756
|
onMouseenter: onCellMouseEnter,
|
|
@@ -3712,7 +3808,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
3712
3808
|
unref(virtualX_on) ? (openBlock(), createElementBlock("td", _hoisted_22)) : createCommentVNode("", true)
|
|
3713
3809
|
], 16, _hoisted_14);
|
|
3714
3810
|
}), 128)),
|
|
3715
|
-
!
|
|
3811
|
+
!isExperimentalScrollY.value ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
3716
3812
|
unref(virtual_on) && !unref(isSRBRActive) ? (openBlock(), createElementBlock("tr", {
|
|
3717
3813
|
key: 0,
|
|
3718
3814
|
style: normalizeStyle(`height: ${unref(virtual_offsetBottom)}px`)
|
package/lib/style.css
CHANGED
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
'scroll-row-by-row': isSRBRActive,
|
|
27
27
|
'scrollbar-on': scrollbarOptions.enabled,
|
|
28
28
|
'is-area-selecting': isAreaSelecting,
|
|
29
|
-
'exp-scroll-y':
|
|
29
|
+
'exp-scroll-y': isExperimentalScrollY,
|
|
30
30
|
}"
|
|
31
31
|
:tabindex="props.areaSelection ? 0 : void 0"
|
|
32
32
|
:style="{
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
@scroll="onTableScroll"
|
|
42
42
|
@wheel="onTableWheel"
|
|
43
43
|
>
|
|
44
|
-
<div v-if="!
|
|
44
|
+
<div v-if="!isExperimentalScrollY && SRBRTotalHeight" class="row-by-row-table-height" :style="`height: ${SRBRTotalHeight}px`"></div>
|
|
45
45
|
|
|
46
46
|
<div v-if="colResizable" ref="colResizeIndicatorRef" class="column-resize-indicator"></div>
|
|
47
47
|
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
@mouseover="onCellMouseOver"
|
|
133
133
|
>
|
|
134
134
|
<tr
|
|
135
|
-
v-if="!
|
|
135
|
+
v-if="!isExperimentalScrollY && virtual_on && !isSRBRActive"
|
|
136
136
|
:style="`height:${virtualScroll.offsetTop}px`"
|
|
137
137
|
class="padding-top-tr"
|
|
138
138
|
>
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
</template>
|
|
209
209
|
<td v-if="virtualX_on" class="vt-x-right"></td>
|
|
210
210
|
</tr>
|
|
211
|
-
<template v-if="!
|
|
211
|
+
<template v-if="!isExperimentalScrollY">
|
|
212
212
|
<tr v-if="virtual_on && !isSRBRActive" :style="`height: ${virtual_offsetBottom}px`"></tr>
|
|
213
213
|
<tr v-if="SRBRBottomHeight" :style="`height: ${SRBRBottomHeight}px`"></tr>
|
|
214
214
|
</template>
|
|
@@ -798,6 +798,13 @@ const scrollbarOptions = computed(() => ({
|
|
|
798
798
|
...(typeof props.scrollbar === 'boolean' ? { enabled: props.scrollbar } : props.scrollbar),
|
|
799
799
|
}));
|
|
800
800
|
|
|
801
|
+
const isExperimentalScrollY = computed(() => {
|
|
802
|
+
if (scrollbarOptions.value?.enabled && props.scrollRowByRow) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
return props.experimental?.scrollY;
|
|
806
|
+
});
|
|
807
|
+
|
|
801
808
|
const rowKeyGenCache = new WeakMap();
|
|
802
809
|
|
|
803
810
|
const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
|
|
@@ -825,7 +832,18 @@ const [
|
|
|
825
832
|
updateVirtualScrollX,
|
|
826
833
|
setAutoHeight,
|
|
827
834
|
clearAllAutoHeight,
|
|
828
|
-
] = useVirtualScroll(
|
|
835
|
+
] = useVirtualScroll(
|
|
836
|
+
props,
|
|
837
|
+
tableContainerRef,
|
|
838
|
+
trRef,
|
|
839
|
+
dataSourceCopy,
|
|
840
|
+
tableHeaderLast,
|
|
841
|
+
tableHeaders,
|
|
842
|
+
rowKeyGen,
|
|
843
|
+
maxRowSpan,
|
|
844
|
+
scrollbarOptions,
|
|
845
|
+
isExperimentalScrollY,
|
|
846
|
+
);
|
|
829
847
|
|
|
830
848
|
/** requestAnimationFrame throttled version of updateVirtualScrollY for smoother wheel scrolling */
|
|
831
849
|
const rafUpdateVirtualScrollYForWheel = rafThrottle((scrollTop: number) => {
|
|
@@ -839,6 +857,7 @@ const [scrollbar, showScrollbar, onVerticalScrollbarMouseDown, onHorizontalScrol
|
|
|
839
857
|
virtualScrollX,
|
|
840
858
|
updateVirtualScrollY,
|
|
841
859
|
scrollbarOptions,
|
|
860
|
+
isExperimentalScrollY,
|
|
842
861
|
);
|
|
843
862
|
|
|
844
863
|
const [hiddenCellMap, mergeCellsWrapper, hoverMergedCells, updateHoverMergedCells, activeMergedCells, updateActiveMergedCells] = useMergeCells(
|
|
@@ -1438,7 +1457,7 @@ function onTableWheel(e: WheelEvent) {
|
|
|
1438
1457
|
if (isWheeling()) {
|
|
1439
1458
|
e.preventDefault();
|
|
1440
1459
|
}
|
|
1441
|
-
if (
|
|
1460
|
+
if (isExperimentalScrollY.value) {
|
|
1442
1461
|
rafUpdateVirtualScrollYForWheel(scrollTop + deltaY);
|
|
1443
1462
|
updateCustomScrollbar();
|
|
1444
1463
|
} else {
|
|
@@ -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
|
/** 是否正在拖选 */
|
|
@@ -56,6 +72,60 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
56
72
|
return map;
|
|
57
73
|
});
|
|
58
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 获取固定列宽度的函数
|
|
77
|
+
* 缓存每个固定列位置的累计宽度,查询时直接返回
|
|
78
|
+
* @param colIndex 目标列索引
|
|
79
|
+
* @returns [leftFixedWidth, rightFixedWidth]
|
|
80
|
+
*/
|
|
81
|
+
const getFixedColWidths = computed(() => {
|
|
82
|
+
const cols = tableHeaderLast.value;
|
|
83
|
+
type FixedColWidth = { i: number; /** accumulated width */ w: number };
|
|
84
|
+
// 保存每个固定列位置的累计宽度(包含当前列)
|
|
85
|
+
const leftAccumulated: FixedColWidth[] = [];
|
|
86
|
+
const rightAccumulated: FixedColWidth[] = [];
|
|
87
|
+
|
|
88
|
+
let leftSum = 0;
|
|
89
|
+
let rightSum = 0;
|
|
90
|
+
|
|
91
|
+
for (let i = 0, j = cols.length - 1; i < cols.length; i++, j--) {
|
|
92
|
+
const leftCol = cols[i];
|
|
93
|
+
const rightCol = cols[j];
|
|
94
|
+
|
|
95
|
+
if (leftCol?.fixed === 'left') {
|
|
96
|
+
leftSum += getCalculatedColWidth(leftCol);
|
|
97
|
+
leftAccumulated.push({ i, w: leftSum });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (rightCol?.fixed === 'right') {
|
|
101
|
+
rightSum += getCalculatedColWidth(rightCol);
|
|
102
|
+
rightAccumulated.unshift({ i: j, w: rightSum });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (colIndex: number) => {
|
|
107
|
+
// 查找目标列左侧最近的固定列的累计宽度
|
|
108
|
+
let leftFixedWidth = 0;
|
|
109
|
+
for (let i = leftAccumulated.length - 1; i >= 0; i--) {
|
|
110
|
+
if (leftAccumulated[i].i < colIndex) {
|
|
111
|
+
leftFixedWidth = leftAccumulated[i].w;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 查找目标列右侧最近的固定列的累计宽度
|
|
117
|
+
let rightFixedWidth = 0;
|
|
118
|
+
for (let i = rightAccumulated.length - 1; i >= 0; i--) {
|
|
119
|
+
if (rightAccumulated[i].i > colIndex) {
|
|
120
|
+
rightFixedWidth = rightAccumulated[i].w;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [leftFixedWidth, rightFixedWidth] as const;
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
59
129
|
/** 根据 selectionRange 计算选区内所有 cellKey 的集合 */
|
|
60
130
|
const selectedCellKeys = computed<Set<string>>(() => {
|
|
61
131
|
const range = selectionRange.value;
|
|
@@ -128,6 +198,98 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
128
198
|
return colKeyToIndexMap.value.get(colKey) ?? -1;
|
|
129
199
|
}
|
|
130
200
|
|
|
201
|
+
/** 获取列的左边距和宽度 */
|
|
202
|
+
function getColPosition(colIndex: number): { l: number; w: number } {
|
|
203
|
+
let l = 0;
|
|
204
|
+
let w = 0;
|
|
205
|
+
const cols = tableHeaderLast.value;
|
|
206
|
+
for (let i = 0; i < cols.length; i++) {
|
|
207
|
+
const colWidth = getCalculatedColWidth(cols[i]);
|
|
208
|
+
if (i < colIndex) {
|
|
209
|
+
l += colWidth;
|
|
210
|
+
} else if (i === colIndex) {
|
|
211
|
+
w = colWidth;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { l, w };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 根据按键计算移动方向 */
|
|
219
|
+
function getMovementDelta(key: string, shiftKey: boolean): [number, number] {
|
|
220
|
+
let rowDelta = 0;
|
|
221
|
+
let colDelta = 0;
|
|
222
|
+
|
|
223
|
+
switch (key) {
|
|
224
|
+
case KEY_ARROW_UP:
|
|
225
|
+
rowDelta = -1;
|
|
226
|
+
break;
|
|
227
|
+
case KEY_ARROW_DOWN:
|
|
228
|
+
rowDelta = 1;
|
|
229
|
+
break;
|
|
230
|
+
case KEY_ARROW_LEFT:
|
|
231
|
+
colDelta = -1;
|
|
232
|
+
break;
|
|
233
|
+
case KEY_ARROW_RIGHT:
|
|
234
|
+
colDelta = 1;
|
|
235
|
+
break;
|
|
236
|
+
case KEY_TAB:
|
|
237
|
+
// Tab: 向右移动;Shift+Tab: 向左移动
|
|
238
|
+
colDelta = shiftKey ? -1 : 1;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return [rowDelta, colDelta];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 钳制值到指定范围内 */
|
|
246
|
+
function clamp(value: number, min: number, max: number): number {
|
|
247
|
+
return Math.max(min, Math.min(value, max));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 处理Tab键的换行逻辑 */
|
|
251
|
+
function handleTabWrap(row: number, col: number, rawCol: number, rowCount: number, colCount: number): [number, number] {
|
|
252
|
+
let newRow = row;
|
|
253
|
+
let newCol = col;
|
|
254
|
+
|
|
255
|
+
if (rawCol >= colCount) {
|
|
256
|
+
newCol = 0;
|
|
257
|
+
newRow = Math.min(row + 1, rowCount - 1);
|
|
258
|
+
} else if (rawCol < 0) {
|
|
259
|
+
newCol = colCount - 1;
|
|
260
|
+
newRow = Math.max(row - 1, 0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return [newRow, newCol];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 计算自动滚动的增量 */
|
|
267
|
+
function calculateAutoScrollDelta(mouseX: number, mouseY: number, rect: DOMRect): { deltaX: number; deltaY: number } {
|
|
268
|
+
const { top, bottom, left, right } = rect;
|
|
269
|
+
let deltaX = 0;
|
|
270
|
+
let deltaY = 0;
|
|
271
|
+
|
|
272
|
+
// Y方向
|
|
273
|
+
if (mouseY < top + EDGE_ZONE) {
|
|
274
|
+
const dist = Math.max(0, top + EDGE_ZONE - mouseY);
|
|
275
|
+
deltaY = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
276
|
+
} else if (mouseY > bottom - EDGE_ZONE) {
|
|
277
|
+
const dist = Math.max(0, mouseY - (bottom - EDGE_ZONE));
|
|
278
|
+
deltaY = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// X方向
|
|
282
|
+
if (mouseX < left + EDGE_ZONE) {
|
|
283
|
+
const dist = Math.max(0, left + EDGE_ZONE - mouseX);
|
|
284
|
+
deltaX = -Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
285
|
+
} else if (mouseX > right - EDGE_ZONE) {
|
|
286
|
+
const dist = Math.max(0, mouseX - (right - EDGE_ZONE));
|
|
287
|
+
deltaX = Math.ceil((dist / EDGE_ZONE) * SCROLL_SPEED_MAX);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { deltaX, deltaY };
|
|
291
|
+
}
|
|
292
|
+
|
|
131
293
|
/** mousedown 处理:设置锚点,开始拖选 */
|
|
132
294
|
function onSelectionMouseDown(e: MouseEvent) {
|
|
133
295
|
if (!props.areaSelection || e.button !== 0) return;
|
|
@@ -238,27 +400,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
238
400
|
}
|
|
239
401
|
|
|
240
402
|
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
|
-
}
|
|
403
|
+
const { deltaX, deltaY } = calculateAutoScrollDelta(lastMouseClientX, lastMouseClientY, rect);
|
|
262
404
|
|
|
263
405
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
264
406
|
container.scrollTop += deltaY;
|
|
@@ -401,7 +543,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
401
543
|
const key = e.key;
|
|
402
544
|
|
|
403
545
|
// Esc 键:取消当前选区
|
|
404
|
-
if (key ===
|
|
546
|
+
if (key === KEY_ESCAPE || key === KEY_ESC) {
|
|
405
547
|
if (selectionRange.value) {
|
|
406
548
|
clearSelectedArea();
|
|
407
549
|
emitSelectionChange();
|
|
@@ -411,7 +553,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
411
553
|
}
|
|
412
554
|
|
|
413
555
|
// Ctrl/Cmd+C 复制选区
|
|
414
|
-
if ((e.ctrlKey || e.metaKey) && key ===
|
|
556
|
+
if ((e.ctrlKey || e.metaKey) && key === KEY_C && selectionRange.value) {
|
|
415
557
|
copySelectedArea();
|
|
416
558
|
e.preventDefault();
|
|
417
559
|
return;
|
|
@@ -420,8 +562,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
420
562
|
// 键盘导航(需要启用 keyboard 选项)
|
|
421
563
|
if (!keyboardEnabled.value) return;
|
|
422
564
|
|
|
423
|
-
const isArrowKey = [
|
|
424
|
-
const isTabKey = key ===
|
|
565
|
+
const isArrowKey = [KEY_ARROW_UP, KEY_ARROW_DOWN, KEY_ARROW_LEFT, KEY_ARROW_RIGHT].includes(key);
|
|
566
|
+
const isTabKey = key === KEY_TAB;
|
|
425
567
|
const isNavigationKey = isArrowKey || isTabKey;
|
|
426
568
|
|
|
427
569
|
if (!isNavigationKey) return;
|
|
@@ -447,21 +589,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
447
589
|
}
|
|
448
590
|
|
|
449
591
|
// 计算移动方向
|
|
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
|
-
}
|
|
592
|
+
const [rowDelta, colDelta] = getMovementDelta(key, e.shiftKey);
|
|
465
593
|
|
|
466
594
|
// Shift 扩展选区,否则移动单格选区
|
|
467
595
|
if (e.shiftKey && isArrowKey) {
|
|
@@ -471,8 +599,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
471
599
|
let newEndCol = range.endColIndex + colDelta;
|
|
472
600
|
|
|
473
601
|
// 边界检查
|
|
474
|
-
newEndRow =
|
|
475
|
-
newEndCol =
|
|
602
|
+
newEndRow = clamp(newEndRow, 0, rowCount - 1);
|
|
603
|
+
newEndCol = clamp(newEndCol, 0, colCount - 1);
|
|
476
604
|
|
|
477
605
|
selectionRange.value = {
|
|
478
606
|
...range,
|
|
@@ -490,20 +618,16 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
490
618
|
let newCol = minCol + colDelta;
|
|
491
619
|
|
|
492
620
|
// 边界检查(先检查,避免越界)
|
|
493
|
-
newRow =
|
|
494
|
-
newCol =
|
|
621
|
+
newRow = clamp(newRow, 0, rowCount - 1);
|
|
622
|
+
newCol = clamp(newCol, 0, colCount - 1);
|
|
495
623
|
|
|
496
624
|
// Tab 换行逻辑:如果到达行尾/行首,换行
|
|
497
625
|
if (isTabKey) {
|
|
498
626
|
// 计算原始未 clamp 的值
|
|
499
627
|
const rawCol = minCol + colDelta;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
} else if (rawCol < 0) {
|
|
504
|
-
newCol = colCount - 1;
|
|
505
|
-
newRow = Math.max(minRow - 1, 0);
|
|
506
|
-
}
|
|
628
|
+
const [tabRow, tabCol] = handleTabWrap(minRow, newCol, rawCol, rowCount, colCount);
|
|
629
|
+
newRow = tabRow;
|
|
630
|
+
newCol = tabCol;
|
|
507
631
|
}
|
|
508
632
|
|
|
509
633
|
// 更新锚点和选区
|
|
@@ -523,6 +647,8 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
523
647
|
|
|
524
648
|
/**
|
|
525
649
|
* 滚动到指定单元格,确保其在可视区域内
|
|
650
|
+
* @param rowIndex 行索引
|
|
651
|
+
* @param colIndex 列索引
|
|
526
652
|
*/
|
|
527
653
|
function scrollToCell(rowIndex: number, colIndex: number) {
|
|
528
654
|
const container = tableContainerRef.value;
|
|
@@ -532,9 +658,10 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
532
658
|
const col = tableHeaderLast.value[colIndex];
|
|
533
659
|
if (!row || !col) return;
|
|
534
660
|
|
|
535
|
-
// 获取表头高度
|
|
536
661
|
const thead = container.querySelector('thead');
|
|
537
662
|
const headerHeight = thead ? thead.offsetHeight : 0;
|
|
663
|
+
const tfoot = container.querySelector('tfoot');
|
|
664
|
+
const footerHeight = tfoot ? tfoot.offsetHeight : 0;
|
|
538
665
|
|
|
539
666
|
const vs = virtualScroll.value;
|
|
540
667
|
const vsx = virtualScrollX.value;
|
|
@@ -546,7 +673,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
546
673
|
|
|
547
674
|
// 计算可视区域
|
|
548
675
|
const visibleTop = container.scrollTop;
|
|
549
|
-
const visibleBottom = visibleTop + vs.containerHeight - headerHeight;
|
|
676
|
+
const visibleBottom = visibleTop + vs.containerHeight - headerHeight - footerHeight;
|
|
550
677
|
|
|
551
678
|
// 计算需要的垂直滚动位置
|
|
552
679
|
let newScrollTop: number | null = null;
|
|
@@ -555,36 +682,26 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
555
682
|
newScrollTop = targetRowTop;
|
|
556
683
|
} else if (targetRowBottom > visibleBottom) {
|
|
557
684
|
// 目标行在可视区域下方,滚动到使目标行位于底部
|
|
558
|
-
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight);
|
|
685
|
+
newScrollTop = targetRowBottom - (vs.containerHeight - headerHeight - footerHeight);
|
|
559
686
|
}
|
|
560
687
|
|
|
561
688
|
// 计算目标列的位置
|
|
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
|
-
}
|
|
689
|
+
const { l: targetColLeft, w: targetColWidth } = getColPosition(colIndex);
|
|
574
690
|
const targetColRight = targetColLeft + targetColWidth;
|
|
575
691
|
|
|
576
692
|
// 计算可视区域(水平)
|
|
577
693
|
const visibleLeft = container.scrollLeft;
|
|
578
694
|
const visibleRight = visibleLeft + vsx.containerWidth;
|
|
579
695
|
|
|
580
|
-
//
|
|
696
|
+
// 计算固定列的宽度(用于检测遮挡)
|
|
697
|
+
const [leftFixedWidth, rightFixedWidth] = getFixedColWidths.value(colIndex);
|
|
581
698
|
let newScrollLeft: number | null = null;
|
|
582
|
-
if (targetColLeft < visibleLeft) {
|
|
583
|
-
//
|
|
584
|
-
newScrollLeft = targetColLeft;
|
|
585
|
-
} else if (targetColRight > visibleRight) {
|
|
586
|
-
//
|
|
587
|
-
newScrollLeft = targetColRight - vsx.containerWidth;
|
|
699
|
+
if (targetColLeft < visibleLeft + leftFixedWidth) {
|
|
700
|
+
// 目标列在左侧固定列遮挡区域内,需要向左滚动
|
|
701
|
+
newScrollLeft = targetColLeft - leftFixedWidth;
|
|
702
|
+
} else if (targetColRight > visibleRight - rightFixedWidth) {
|
|
703
|
+
// 目标列在右侧固定列遮挡区域内,需要向右滚动
|
|
704
|
+
newScrollLeft = targetColRight - vsx.containerWidth + rightFixedWidth;
|
|
588
705
|
}
|
|
589
706
|
|
|
590
707
|
// 执行滚动
|
|
@@ -594,10 +711,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
594
711
|
}
|
|
595
712
|
|
|
596
713
|
/**
|
|
597
|
-
*
|
|
714
|
+
* 判断一个单元格的选区样式类名
|
|
598
715
|
* @param cellKey 单元格唯一键
|
|
599
716
|
* @param absoluteRowIndex 行在 dataSourceCopy 中的绝对索引
|
|
600
717
|
* @param colKey 列唯一键
|
|
718
|
+
* @returns 样式类名数组
|
|
601
719
|
*/
|
|
602
720
|
function getAreaSelectionClasses(cellKey: string, absoluteRowIndex: number, colKey: UniqKey): string[] {
|
|
603
721
|
const nr = normalizedRange.value;
|
|
@@ -606,11 +724,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
|
|
|
606
724
|
const colIndex = colKeyToIndexMap.value.get(colKey);
|
|
607
725
|
if (colIndex === void 0 || colIndex < 0) return [];
|
|
608
726
|
|
|
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(
|
|
727
|
+
const classes: string[] = [CELL_RANGE_SELECTED];
|
|
728
|
+
if (absoluteRowIndex === nr.minRow) classes.push(CELL_RANGE_TOP);
|
|
729
|
+
if (absoluteRowIndex === nr.maxRow) classes.push(CELL_RANGE_BOTTOM);
|
|
730
|
+
if (colIndex === nr.minCol) classes.push(CELL_RANGE_LEFT);
|
|
731
|
+
if (colIndex === nr.maxCol) classes.push(CELL_RANGE_RIGHT);
|
|
614
732
|
return classes;
|
|
615
733
|
}
|
|
616
734
|
|
|
@@ -27,6 +27,7 @@ export function useScrollbar(
|
|
|
27
27
|
virtualScrollX: Ref<VirtualScrollXStore>,
|
|
28
28
|
updateVirtualScrollY: (sTop?: number) => void,
|
|
29
29
|
scrollbarOptions: Ref<Required<ScrollbarOptions>>,
|
|
30
|
+
isExperimentalScrollY: Ref<boolean | undefined>,
|
|
30
31
|
) {
|
|
31
32
|
const showScrollbar = ref({ x: false, y: false });
|
|
32
33
|
const scrollbar = ref({ h: 0, w: 0, t: 0, l: 0 });
|
|
@@ -114,7 +115,7 @@ export function useScrollbar(
|
|
|
114
115
|
const trackRange = containerHeight - scrollbar.value.h;
|
|
115
116
|
const scrollDelta = (deltaY / trackRange) * scrollRange;
|
|
116
117
|
|
|
117
|
-
if (
|
|
118
|
+
if (isExperimentalScrollY.value) {
|
|
118
119
|
const ratio = containerHeight / scrollHeight;
|
|
119
120
|
const top = Math.round((dragStartTop + scrollDelta) * ratio);
|
|
120
121
|
const maxTop = containerHeight - scrollbar.value.h;
|
|
@@ -57,6 +57,7 @@ export function useVirtualScroll<DT extends Record<string, any>>(
|
|
|
57
57
|
rowKeyGen: RowKeyGen,
|
|
58
58
|
maxRowSpan: Map<UniqKey, number>,
|
|
59
59
|
scrollbarOptions: Ref<Required<ScrollbarOptions>>,
|
|
60
|
+
isExperimentalScrollY: Ref<boolean | undefined>,
|
|
60
61
|
) {
|
|
61
62
|
const tableHeaderHeight = computed(() => props.headerRowHeight * tableHeaders.value.length);
|
|
62
63
|
|
|
@@ -276,7 +277,7 @@ export function useVirtualScroll<DT extends Record<string, any>>(
|
|
|
276
277
|
const { enabled: scrollbarEnable } = scrollbarOptions.value;
|
|
277
278
|
if (scrollbarEnable) {
|
|
278
279
|
vsValue.scrollHeight = scrollHeight;
|
|
279
|
-
if (
|
|
280
|
+
if (isExperimentalScrollY.value) {
|
|
280
281
|
let maxTop: number;
|
|
281
282
|
sTop = sTop < 0 ? 0 : sTop < (maxTop = scrollHeight - containerHeight) ? sTop : maxTop;
|
|
282
283
|
vsValue.translateY = props.scrollRowByRow ? 0 : -(sTop % rowHeight);
|