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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.0-beta.4
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/
@@ -3,8 +3,9 @@ import { CellKeyGen, ColKeyGen, StkTableColumn, UniqKey } from '../types';
3
3
  import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll';
4
4
 
5
5
  /**
6
- * 单元格拖拽选区
7
- * en: Cell drag selection
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>>): readonly [Ref<{
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>>): readonly [Ref<{
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;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.0-beta.4
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 { top, bottom, left, right } = rect;
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 === "Escape" || key === "Esc") {
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 === "c" && selectionRange.value) {
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 = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(key);
615
- const isTabKey = key === "Tab";
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
- let rowDelta = 0;
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 = Math.max(0, Math.min(newEndRow, rowCount - 1));
652
- newEndCol = Math.max(0, Math.min(newEndCol, colCount - 1));
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 = Math.max(0, Math.min(newRow, rowCount - 1));
665
- newCol = Math.max(0, Math.min(newCol, colCount - 1));
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
- if (rawCol >= colCount) {
669
- newCol = 0;
670
- newRow = Math.min(minRow + 1, rowCount - 1);
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 = ["cell-range-selected"];
739
- if (absoluteRowIndex === nr.minRow) classes.push("cell-range-t");
740
- if (absoluteRowIndex === nr.maxRow) classes.push("cell-range-b");
741
- if (colIndex === nr.minCol) classes.push("cell-range-l");
742
- if (colIndex === nr.maxCol) classes.push("cell-range-r");
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 ((_a = props.experimental) == null ? void 0 : _a.scrollY) {
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 ((_a = props.experimental) == null ? void 0 : _a.scrollY) {
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(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, scrollbarOptions);
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 ((_a = props.experimental) == null ? void 0 : _a.scrollY) {
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": (_a = __props.experimental) == null ? void 0 : _a.scrollY
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
- !((_b = __props.experimental) == null ? void 0 : _b.scrollY) && SRBRTotalHeight.value ? (openBlock(), createElementBlock("div", {
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
- !((_c = __props.experimental) == null ? void 0 : _c.scrollY) && unref(virtual_on) && !unref(isSRBRActive) ? (openBlock(), createElementBlock("tr", {
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 _a2;
3744
+ var _a;
3649
3745
  return [
3650
- createTextVNode(toDisplayString(((_a2 = row.__EXP_R__) == null ? void 0 : _a2[row.__EXP_C__.dataIndex]) ?? ""), 1)
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 _a2;
3751
+ var _a;
3656
3752
  return openBlock(), createElementBlock(Fragment, null, [
3657
- !unref(hiddenCellMap) || !((_a2 = unref(hiddenCellMap)[rowKeyGen(row)]) == null ? void 0 : _a2.has(colKeyGen.value(col))) ? (openBlock(), createElementBlock("td", mergeProps({
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
- !((_d = __props.experimental) == null ? void 0 : _d.scrollY) ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [
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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.0-beta.4
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/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stk-table-vue",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "High performance realtime virtual table for vue3 and vue2.7",
5
5
  "main": "./lib/stk-table-vue.js",
6
6
  "types": "./lib/src/StkTable/index.d.ts",
@@ -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': experimental?.scrollY,
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="!experimental?.scrollY && SRBRTotalHeight" class="row-by-row-table-height" :style="`height: ${SRBRTotalHeight}px`"></div>
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="!experimental?.scrollY && virtual_on && !isSRBRActive"
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="!experimental?.scrollY">
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(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, scrollbarOptions);
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 (props.experimental?.scrollY) {
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
- * en: Cell drag selection
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 { top, bottom, left, right } = rect;
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 === 'Escape' || key === 'Esc') {
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 === 'c' && selectionRange.value) {
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 = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key);
424
- const isTabKey = key === 'Tab';
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
- let rowDelta = 0;
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 = Math.max(0, Math.min(newEndRow, rowCount - 1));
475
- newEndCol = Math.max(0, Math.min(newEndCol, colCount - 1));
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 = Math.max(0, Math.min(newRow, rowCount - 1));
494
- newCol = Math.max(0, Math.min(newCol, colCount - 1));
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
- if (rawCol >= colCount) {
501
- newCol = 0;
502
- newRow = Math.min(minRow + 1, rowCount - 1);
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
- let targetColLeft = 0;
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
- * 判断一个单元格的选区 class
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[] = ['cell-range-selected'];
610
- if (absoluteRowIndex === nr.minRow) classes.push('cell-range-t');
611
- if (absoluteRowIndex === nr.maxRow) classes.push('cell-range-b');
612
- if (colIndex === nr.minCol) classes.push('cell-range-l');
613
- if (colIndex === nr.maxCol) classes.push('cell-range-r');
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 (props.experimental?.scrollY) {
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 (props.experimental?.scrollY) {
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);