stk-table-vue 0.11.10 → 0.11.11

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/README.md CHANGED
@@ -44,18 +44,18 @@ const stkTableRef = ref<InstanceType<typeof StkTable>>();
44
44
  const stkTableRef = useTemplateRef('stkTableRef');
45
45
 
46
46
  // highlight row
47
- stkTableRef.value.setHighlightDimRow([rowKey]{
48
- method: 'css'|'animation',// default animation
49
- className: 'custom-class-name', // method css
50
- keyframe: [{backgroundColor:'#aaa'}, {backgroundColor: '#222'}],//same as https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Animations_API/Keyframe_Formats
51
- duration: 2000,。
47
+ stkTableRef.value.setHighlightDimRow([rowKey], {
48
+ method: 'css' | 'animation', // default animation
49
+ className: 'custom-class-name', // for method 'css'
50
+ keyframe: [{ backgroundColor: '#aaa' }, { backgroundColor: '#222' }], // same as https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Animations_API/Keyframe_Formats
51
+ duration: 2000,
52
52
  });
53
- // highlight cell
53
+ // highlight cell
54
54
  stkTableRef.value.setHighlightDimCell(rowKey, colDataIndex, {
55
- method: 'css'|'animation',
56
- className:'custom-class-name', // method css
57
- keyframe: [{backgroundColor:'#aaa'}, {backgroundColor: '#222'}], // method animation
58
- duration: 2000,。
55
+ method: 'css' | 'animation',
56
+ className: 'custom-class-name', // for method 'css'
57
+ keyframe: [{ backgroundColor: '#aaa' }, { backgroundColor: '#222' }], // for method 'animation'
58
+ duration: 2000,
59
59
  });
60
60
 
61
61
  const columns = [
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.10
3
+ * version: v0.11.11
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/
@@ -320,8 +320,20 @@ export type RowActiveOption<DT> = {
320
320
  /** 单元格选区范围 */
321
321
  export type AreaSelectionRange = {
322
322
  index: {
323
+ /** column index range @deprecated */
323
324
  x: [number, number];
325
+ /** row index range @deprecated */
324
326
  y: [number, number];
327
+ /** start point index*/
328
+ begin: {
329
+ row: number;
330
+ col: number;
331
+ };
332
+ /** end point index */
333
+ end: {
334
+ row: number;
335
+ col: number;
336
+ };
325
337
  };
326
338
  };
327
339
  /** 单元格选区配置 */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.10
3
+ * version: v0.11.11
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/
@@ -458,6 +458,44 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
458
458
  onBeforeUnmount(() => {
459
459
  removeListener();
460
460
  });
461
+ watch([() => dataSourceCopy.value.length, () => tableHeaderLast.value.length], ([rowCount, colCount]) => {
462
+ if (!config.value.enabled) return;
463
+ if (anchorCell) {
464
+ if (rowCount === 0 || colCount === 0) {
465
+ anchorCell = null;
466
+ } else {
467
+ anchorCell.rowIndex = clamp(anchorCell.rowIndex, 0, rowCount - 1);
468
+ anchorCell.colIndex = clamp(anchorCell.colIndex, 0, colCount - 1);
469
+ }
470
+ }
471
+ if (!selectionRanges.value.length) return;
472
+ if (rowCount === 0 || colCount === 0) {
473
+ clearSelectedArea();
474
+ emitSelectionChange();
475
+ return;
476
+ }
477
+ const maxRow = rowCount - 1;
478
+ const maxCol = colCount - 1;
479
+ let changed = false;
480
+ const newRanges = [];
481
+ for (const range of selectionRanges.value) {
482
+ const { begin, end } = range.index;
483
+ const nbRow = clamp(begin.row, 0, maxRow);
484
+ const nbCol = clamp(begin.col, 0, maxCol);
485
+ const neRow = clamp(end.row, 0, maxRow);
486
+ const neCol = clamp(end.col, 0, maxCol);
487
+ if (nbRow !== begin.row || nbCol !== begin.col || neRow !== end.row || neCol !== end.col) {
488
+ changed = true;
489
+ newRanges.push(makeRange(nbRow, nbCol, neRow, neCol));
490
+ } else {
491
+ newRanges.push(range);
492
+ }
493
+ }
494
+ if (changed) {
495
+ selectionRanges.value = newRanges;
496
+ emitSelectionChange();
497
+ }
498
+ });
461
499
  function addListener() {
462
500
  removeListener();
463
501
  const el = tableContainerRef.value;
@@ -475,14 +513,22 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
475
513
  stopAutoScroll();
476
514
  }
477
515
  function normalizeRange(range) {
478
- const { index } = range;
479
- const [x1, x2] = index.x;
480
- const [y1, y2] = index.y;
516
+ const { begin, end } = range.index;
517
+ return {
518
+ minRow: Math.min(begin.row, end.row),
519
+ maxRow: Math.max(begin.row, end.row),
520
+ minCol: Math.min(begin.col, end.col),
521
+ maxCol: Math.max(begin.col, end.col)
522
+ };
523
+ }
524
+ function makeRange(beginRow, beginCol, endRow, endCol) {
481
525
  return {
482
- minRow: Math.min(y1, y2),
483
- maxRow: Math.max(y1, y2),
484
- minCol: Math.min(x1, x2),
485
- maxCol: Math.max(x1, x2)
526
+ index: {
527
+ x: [beginCol, endCol],
528
+ y: [beginRow, endRow],
529
+ begin: { row: beginRow, col: beginCol },
530
+ end: { row: endRow, col: endCol }
531
+ }
486
532
  };
487
533
  }
488
534
  function getColIndexByKey(colKey) {
@@ -568,20 +614,10 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
568
614
  const colIndex = getColIndexByKey(colKey);
569
615
  if (rowIndex < 0 || colIndex < 0) return;
570
616
  const ctrlKey = e.ctrlKey || e.metaKey;
571
- const range = {
572
- index: {
573
- x: [colIndex, colIndex],
574
- y: [rowIndex, rowIndex]
575
- }
576
- };
617
+ const range = makeRange(rowIndex, colIndex, rowIndex, colIndex);
577
618
  if (e.shiftKey && anchorCell && shiftEnabled.value) {
578
619
  const ranges = selectionRanges.value.slice();
579
- const shiftRange = {
580
- index: {
581
- x: [anchorCell.colIndex, colIndex],
582
- y: [anchorCell.rowIndex, rowIndex]
583
- }
584
- };
620
+ const shiftRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex);
585
621
  if (ranges.length) {
586
622
  ranges[ranges.length - 1] = shiftRange;
587
623
  } else {
@@ -621,12 +657,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
621
657
  }
622
658
  function updateSelectionEnd(endRowIndex, endColIndex) {
623
659
  if (!anchorCell) return;
624
- const newRange = {
625
- index: {
626
- x: [anchorCell.colIndex, endColIndex],
627
- y: [anchorCell.rowIndex, endRowIndex]
628
- }
629
- };
660
+ const newRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex);
630
661
  const ranges = [...selectionRanges.value];
631
662
  if (ranges.length > 0) {
632
663
  ranges[ranges.length - 1] = newRange;
@@ -762,14 +793,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
762
793
  if (rowCount === 0 || colCount === 0) return;
763
794
  if (!selectionRanges.value.length) {
764
795
  anchorCell = { rowIndex: 0, colIndex: 0 };
765
- selectionRanges.value = [
766
- {
767
- index: {
768
- x: [0, 0],
769
- y: [0, 0]
770
- }
771
- }
772
- ];
796
+ selectionRanges.value = [makeRange(0, 0, 0, 0)];
773
797
  emitSelectionChange();
774
798
  scrollToCell(0, 0);
775
799
  return;
@@ -779,17 +803,12 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
779
803
  const ranges = [...selectionRanges.value];
780
804
  const range = ranges.length > 0 ? ranges[ranges.length - 1] : null;
781
805
  if (!range) return;
782
- const { index } = range;
783
- let newEndRow = index.y[1] + rowDelta;
784
- let newEndCol = index.x[1] + colDelta;
806
+ const { begin, end } = range.index;
807
+ let newEndRow = end.row + rowDelta;
808
+ let newEndCol = end.col + colDelta;
785
809
  newEndRow = clamp(newEndRow, 0, rowCount - 1);
786
810
  newEndCol = clamp(newEndCol, 0, colCount - 1);
787
- ranges[ranges.length - 1] = {
788
- index: {
789
- x: [index.x[0], newEndCol],
790
- y: [index.y[0], newEndRow]
791
- }
792
- };
811
+ ranges[ranges.length - 1] = makeRange(begin.row, begin.col, newEndRow, newEndCol);
793
812
  selectionRanges.value = ranges;
794
813
  scrollToCell(newEndRow, newEndCol);
795
814
  } else {
@@ -808,14 +827,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
808
827
  newCol = tabCol;
809
828
  }
810
829
  anchorCell = { rowIndex: newRow, colIndex: newCol };
811
- selectionRanges.value = [
812
- {
813
- index: {
814
- x: [newCol, newCol],
815
- y: [newRow, newRow]
816
- }
817
- }
818
- ];
830
+ selectionRanges.value = [makeRange(newRow, newCol, newRow, newCol)];
819
831
  scrollToCell(newRow, newCol);
820
832
  }
821
833
  emitSelectionChange();
@@ -928,8 +940,10 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
928
940
  const useAreaSelectionName = "useAreaSelection";
929
941
  useAreaSelection[MY_FN_NAME] = useAreaSelectionName;
930
942
  const ON_DEMAND_FEATURE = {
931
- [useAreaSelectionName]: () => {
932
- console.warn("useAreaSelection is not registered");
943
+ [useAreaSelectionName]: (props) => {
944
+ if ("useAreaSelection" in props) {
945
+ console.warn("useAreaSelection is not registered");
946
+ }
933
947
  return {
934
948
  config: computed(() => ({ enabled: false })),
935
949
  isSelecting: ref(false),
@@ -947,7 +961,7 @@ const ON_DEMAND_FEATURE = {
947
961
  function registerFeature(feature) {
948
962
  const features = Array.isArray(feature) ? feature : [feature];
949
963
  features.forEach((f) => {
950
- const fnName = f.stkName;
964
+ const fnName = f[MY_FN_NAME];
951
965
  if (!fnName) {
952
966
  console.warn("invalid feature");
953
967
  return;
@@ -1209,9 +1223,12 @@ function useFixedCol(props, colKeyGen, getFixedColPosition, tableHeaders, tableH
1209
1223
  }
1210
1224
  const leftShadowCol = [];
1211
1225
  const rightShadowCol = [];
1212
- tableHeadersForCalc.value.forEach((row, level) => {
1226
+ const len = tableHeadersForCalc.value.length;
1227
+ for (let level = 0; level < len; level++) {
1228
+ const row = tableHeadersForCalc.value[level];
1213
1229
  let left = 0;
1214
- row.forEach((col) => {
1230
+ for (let i = 0, rowLen = row.length; i < rowLen; i++) {
1231
+ const col = row[i];
1215
1232
  const position = getFixedColPositionValue(col);
1216
1233
  const isFixedLeft = col.fixed === "left";
1217
1234
  const isFixedRight = col.fixed === "right";
@@ -1226,8 +1243,8 @@ function useFixedCol(props, colKeyGen, getFixedColPosition, tableHeaders, tableH
1226
1243
  rightShadowCol[level] = col;
1227
1244
  }
1228
1245
  }
1229
- });
1230
- });
1246
+ }
1247
+ }
1231
1248
  if (props.fixedColShadow) {
1232
1249
  fixedShadowCols.value = leftShadowCol.concat(rightShadowCol).filter(Boolean);
1233
1250
  }
@@ -1382,7 +1399,7 @@ function useHighlight(props, stkTableId, tableContainerRef) {
1382
1399
  if (method === "animation") {
1383
1400
  cellEl.animate(keyframe, duration);
1384
1401
  } else {
1385
- highlightCellsInCssKeyFrame(cellEl, rowKeyValue, className, duration);
1402
+ highlightCellsInCssKeyFrame(cellEl, rowKeyValue, colKeyValue, className, duration);
1386
1403
  }
1387
1404
  }
1388
1405
  function setHighlightDimRow(rowKeyValues, option = {}) {
@@ -1442,18 +1459,22 @@ function useHighlight(props, stkTableId, tableContainerRef) {
1442
1459
  }
1443
1460
  rowElTemp.forEach((el) => el.classList.add(className));
1444
1461
  }
1445
- function highlightCellsInCssKeyFrame(cellEl, rowKeyValue, className, duration) {
1462
+ function highlightCellsInCssKeyFrame(cellEl, rowKeyValue, colKeyValue, className, duration) {
1446
1463
  if (cellEl.classList.contains(className)) {
1447
1464
  cellEl.classList.remove(className);
1448
1465
  void cellEl.offsetHeight;
1449
1466
  }
1450
1467
  cellEl.classList.add(className);
1451
- window.clearTimeout(highlightDimCellsTimeout.get(rowKeyValue));
1468
+ const cellKey = `${rowKeyValue}-${colKeyValue}`;
1469
+ window.clearTimeout(highlightDimCellsTimeout.get(cellKey));
1470
+ if (!duration) {
1471
+ return;
1472
+ }
1452
1473
  highlightDimCellsTimeout.set(
1453
- rowKeyValue,
1474
+ cellKey,
1454
1475
  window.setTimeout(() => {
1455
1476
  cellEl.classList.remove(className);
1456
- highlightDimCellsTimeout.delete(rowKeyValue);
1477
+ highlightDimCellsTimeout.delete(cellKey);
1457
1478
  }, duration)
1458
1479
  );
1459
1480
  }
@@ -2819,11 +2840,15 @@ const _hoisted_14 = {
2819
2840
  class: "vt-x-left"
2820
2841
  };
2821
2842
  const _hoisted_15 = ["colspan"];
2822
- const _hoisted_16 = { class: "table-cell-wrapper" };
2843
+ const _hoisted_16 = {
2844
+ class: "table-cell-wrapper",
2845
+ tabindex: "-1"
2846
+ };
2823
2847
  const _hoisted_17 = ["title"];
2824
2848
  const _hoisted_18 = {
2825
2849
  key: 2,
2826
- class: "table-cell-wrapper"
2850
+ class: "table-cell-wrapper",
2851
+ tabindex: "-1"
2827
2852
  };
2828
2853
  const _hoisted_19 = ["title"];
2829
2854
  const _hoisted_20 = { key: 2 };
@@ -3495,9 +3520,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
3495
3520
  dom.scrollLeft += distance;
3496
3521
  }
3497
3522
  }
3523
+ let scrollRAFScheduled = false;
3498
3524
  function onTableScroll(e) {
3499
- if (!(e == null ? void 0 : e.target)) return;
3525
+ if (!(e == null ? void 0 : e.target) || scrollRAFScheduled) return;
3526
+ scrollRAFScheduled = true;
3500
3527
  requestAnimationFrame(() => {
3528
+ scrollRAFScheduled = false;
3501
3529
  const { scrollTop, scrollLeft } = e.target;
3502
3530
  const { scrollTop: vScrollTop } = virtualScroll.value;
3503
3531
  const { scrollLeft: vScrollLeft } = virtualScrollX.value;
@@ -3928,6 +3956,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
3928
3956
  col.customFooterCell ? (openBlock(), createBlock(resolveDynamicComponent(col.customFooterCell), {
3929
3957
  key: 0,
3930
3958
  class: "table-cell-wrapper",
3959
+ tabindex: "-1",
3931
3960
  col,
3932
3961
  row: footRow,
3933
3962
  rowIndex: footRowIndex,
@@ -3936,6 +3965,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
3936
3965
  }, null, 8, ["col", "row", "rowIndex", "colIndex", "cellValue"])) : createCommentVNode("", true),
3937
3966
  createElementVNode("div", {
3938
3967
  class: "table-cell-wrapper",
3968
+ tabindex: "-1",
3939
3969
  title: footRow[col.dataIndex] || ""
3940
3970
  }, [
3941
3971
  footRow[col.dataIndex] != null ? (openBlock(), createElementBlock("span", _hoisted_10, toDisplayString(footRow[col.dataIndex]), 1)) : createCommentVNode("", true)
@@ -4002,6 +4032,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
4002
4032
  col.customCell ? (openBlock(), createBlock(resolveDynamicComponent(col.customCell), {
4003
4033
  key: 0,
4004
4034
  class: "table-cell-wrapper",
4035
+ tabindex: "-1",
4005
4036
  col,
4006
4037
  row,
4007
4038
  rowIndex: getRowIndex(rowIndex),
@@ -4024,16 +4055,19 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
4024
4055
  }, 1032, ["col", "row", "rowIndex", "colIndex", "cellValue", "expanded", "tree-expanded"])) : !col.type ? (openBlock(), createElementBlock("div", {
4025
4056
  key: 1,
4026
4057
  class: "table-cell-wrapper",
4058
+ tabindex: "-1",
4027
4059
  title: row[col.dataIndex] || ""
4028
4060
  }, toDisplayString((row && row[col.dataIndex]) !== void 0 ? row && row[col.dataIndex] : getEmptyCellText.value(col, row)), 9, _hoisted_17)) : col.type === "seq" ? (openBlock(), createElementBlock("div", _hoisted_18, toDisplayString((props.seqConfig.startIndex || 0) + getRowIndex(rowIndex) + 1), 1)) : col.type === "tree-node" ? (openBlock(), createBlock(_sfc_main$2, {
4029
4061
  key: 3,
4030
4062
  class: "table-cell-wrapper",
4063
+ tabindex: "-1",
4031
4064
  col,
4032
4065
  row,
4033
4066
  onClick: ($event) => triangleClick($event, row, col)
4034
4067
  }, null, 8, ["col", "row", "onClick"])) : (openBlock(), createElementBlock("div", {
4035
4068
  key: 4,
4036
4069
  class: "table-cell-wrapper",
4070
+ tabindex: "-1",
4037
4071
  title: row[col.dataIndex] || ""
4038
4072
  }, [
4039
4073
  col.type === "dragRow" ? (openBlock(), createBlock(DragHandle, {
package/lib/style.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v0.11.10
3
+ * version: v0.11.11
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/
@@ -395,7 +395,7 @@
395
395
  .stk-table .stk-footer{
396
396
  position:sticky;
397
397
  bottom:0;
398
- z-index:1;
398
+ z-index:2;
399
399
  }
400
400
  .stk-table .stk-footer tr{
401
401
  background-color:var(--tf-bgc);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stk-table-vue",
3
- "version": "0.11.10",
3
+ "version": "0.11.11",
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",
@@ -116,13 +116,14 @@
116
116
  :is="col.customFooterCell"
117
117
  v-if="col.customFooterCell"
118
118
  class="table-cell-wrapper"
119
+ tabindex="-1"
119
120
  :col="col"
120
121
  :row="footRow"
121
122
  :rowIndex="footRowIndex"
122
123
  :colIndex="colIndex"
123
124
  :cellValue="footRow[col.dataIndex]"
124
125
  />
125
- <div class="table-cell-wrapper" :title="footRow[col.dataIndex] || ''">
126
+ <div class="table-cell-wrapper" tabindex="-1" :title="footRow[col.dataIndex] || ''">
126
127
  <span v-if="footRow[col.dataIndex] != null">{{ footRow[col.dataIndex] }}</span>
127
128
  </div>
128
129
  </td>
@@ -153,7 +154,7 @@
153
154
  >
154
155
  <td v-if="virtualX_on" class="vt-x-left"></td>
155
156
  <td v-if="row && row.__EXP_R__" :colspan="virtualX_columnPart.length">
156
- <div class="table-cell-wrapper">
157
+ <div class="table-cell-wrapper" tabindex="-1">
157
158
  <slot name="expand" :row="row.__EXP_R__" :col="row.__EXP_C__">
158
159
  {{ (row.__EXP_R__ && row.__EXP_C__ && row.__EXP_R__[row.__EXP_C__.dataIndex]) || '' }}
159
160
  </slot>
@@ -172,6 +173,7 @@
172
173
  :is="col.customCell"
173
174
  v-if="col.customCell"
174
175
  class="table-cell-wrapper"
176
+ tabindex="-1"
175
177
  :col="col"
176
178
  :row="row"
177
179
  :rowIndex="getRowIndex(rowIndex)"
@@ -187,20 +189,21 @@
187
189
  <DragHandle @dragstart="onTrDragStart($event, getRowIndex(rowIndex))" />
188
190
  </template>
189
191
  </component>
190
- <div v-else-if="!col.type" class="table-cell-wrapper" :title="row[col.dataIndex] || ''">
192
+ <div v-else-if="!col.type" class="table-cell-wrapper" tabindex="-1" :title="row[col.dataIndex] || ''">
191
193
  {{ (row && row[col.dataIndex]) !== void 0 ? row && row[col.dataIndex] : getEmptyCellText(col, row) }}
192
194
  </div>
193
- <div v-else-if="col.type === 'seq'" class="table-cell-wrapper">
195
+ <div v-else-if="col.type === 'seq'" class="table-cell-wrapper" tabindex="-1">
194
196
  {{ (props.seqConfig.startIndex || 0) + getRowIndex(rowIndex) + 1 }}
195
197
  </div>
196
198
  <TreeNodeCell
197
199
  v-else-if="col.type === 'tree-node'"
198
200
  class="table-cell-wrapper"
201
+ tabindex="-1"
199
202
  :col="col"
200
203
  :row="row"
201
204
  @click="triangleClick($event, row, col)"
202
205
  ></TreeNodeCell>
203
- <div v-else class="table-cell-wrapper" :title="row[col.dataIndex] || ''">
206
+ <div v-else class="table-cell-wrapper" tabindex="-1" :title="row[col.dataIndex] || ''">
204
207
  <DragHandle v-if="col.type === 'dragRow'" @dragstart="onTrDragStart($event, getRowIndex(rowIndex))" />
205
208
  <TriangleIcon v-else-if="col.type === 'expand'" @click="triangleClick($event, row, col)" />
206
209
  <span v-if="row[col.dataIndex] != null">{{ row[col.dataIndex] }}</span>
@@ -1450,13 +1453,17 @@ function onTableWheel(e: WheelEvent) {
1450
1453
  }
1451
1454
  }
1452
1455
 
1456
+ /** Prevent re-entrant requestAnimationFrame in onTableScroll */
1457
+ let scrollRAFScheduled = false;
1458
+
1453
1459
  /**
1454
1460
  * @param e scrollEvent
1455
1461
  */
1456
1462
  function onTableScroll(e: Event) {
1457
- if (!e?.target) return;
1458
- // const target = e.target;
1463
+ if (!e?.target || scrollRAFScheduled) return;
1464
+ scrollRAFScheduled = true;
1459
1465
  requestAnimationFrame(() => {
1466
+ scrollRAFScheduled = false;
1460
1467
  const { scrollTop, scrollLeft } = e.target as HTMLElement;
1461
1468
  const { scrollTop: vScrollTop } = virtualScroll.value;
1462
1469
  const { scrollLeft: vScrollLeft } = virtualScrollX.value;
@@ -1,4 +1,4 @@
1
- import { Ref, ShallowRef, computed, onBeforeUnmount, onMounted, ref } from 'vue';
1
+ import { Ref, ShallowRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
2
2
  import { AreaSelectionConfig, AreaSelectionRange, CellKeyGen, ColKeyGen, StkTableColumn, UniqKey } from '../types';
3
3
  import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll';
4
4
  import { getClosestColKey, getClosestTrIndex } from '../utils';
@@ -193,6 +193,56 @@ export function useAreaSelection<DT extends Record<string, any>>(
193
193
  removeListener();
194
194
  });
195
195
 
196
+ /**
197
+ * 监听数据行数/列数变化,当行列变少时钳制选区与锚点,避免越界
198
+ * en: Watch row/col count changes, clamp selection ranges and anchor to avoid out-of-bounds
199
+ */
200
+ watch([() => dataSourceCopy.value.length, () => tableHeaderLast.value.length], ([rowCount, colCount]) => {
201
+ if (!config.value.enabled) return;
202
+
203
+ // 钳制锚点
204
+ if (anchorCell) {
205
+ if (rowCount === 0 || colCount === 0) {
206
+ anchorCell = null;
207
+ } else {
208
+ anchorCell.rowIndex = clamp(anchorCell.rowIndex, 0, rowCount - 1);
209
+ anchorCell.colIndex = clamp(anchorCell.colIndex, 0, colCount - 1);
210
+ }
211
+ }
212
+
213
+ if (!selectionRanges.value.length) return;
214
+
215
+ // 行或列为 0 时清空选区
216
+ if (rowCount === 0 || colCount === 0) {
217
+ clearSelectedArea();
218
+ emitSelectionChange();
219
+ return;
220
+ }
221
+
222
+ const maxRow = rowCount - 1;
223
+ const maxCol = colCount - 1;
224
+ let changed = false;
225
+ const newRanges: AreaSelectionRange[] = [];
226
+ for (const range of selectionRanges.value) {
227
+ const { begin, end } = range.index;
228
+ const nbRow = clamp(begin.row, 0, maxRow);
229
+ const nbCol = clamp(begin.col, 0, maxCol);
230
+ const neRow = clamp(end.row, 0, maxRow);
231
+ const neCol = clamp(end.col, 0, maxCol);
232
+ if (nbRow !== begin.row || nbCol !== begin.col || neRow !== end.row || neCol !== end.col) {
233
+ changed = true;
234
+ newRanges.push(makeRange(nbRow, nbCol, neRow, neCol));
235
+ } else {
236
+ newRanges.push(range);
237
+ }
238
+ }
239
+
240
+ if (changed) {
241
+ selectionRanges.value = newRanges;
242
+ emitSelectionChange();
243
+ }
244
+ });
245
+
196
246
  function addListener() {
197
247
  removeListener();
198
248
  const el = tableContainerRef.value;
@@ -213,14 +263,27 @@ export function useAreaSelection<DT extends Record<string, any>>(
213
263
 
214
264
  /** 获取归一化(min/max)后的选区范围 */
215
265
  function normalizeRange(range: AreaSelectionRange) {
216
- const { index } = range;
217
- const [x1, x2] = index.x;
218
- const [y1, y2] = index.y;
266
+ const { begin, end } = range.index;
219
267
  return {
220
- minRow: Math.min(y1, y2),
221
- maxRow: Math.max(y1, y2),
222
- minCol: Math.min(x1, x2),
223
- maxCol: Math.max(x1, x2),
268
+ minRow: Math.min(begin.row, end.row),
269
+ maxRow: Math.max(begin.row, end.row),
270
+ minCol: Math.min(begin.col, end.col),
271
+ maxCol: Math.max(begin.col, end.col),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * 构造选区范围。begin = 拖拽起点,end = 拖拽终点。
277
+ * 同时填充已废弃的 x/y 字段以保证向后兼容。
278
+ */
279
+ function makeRange(beginRow: number, beginCol: number, endRow: number, endCol: number): AreaSelectionRange {
280
+ return {
281
+ index: {
282
+ x: [beginCol, endCol],
283
+ y: [beginRow, endRow],
284
+ begin: { row: beginRow, col: beginCol },
285
+ end: { row: endRow, col: endCol },
286
+ },
224
287
  };
225
288
  }
226
289
 
@@ -334,21 +397,11 @@ export function useAreaSelection<DT extends Record<string, any>>(
334
397
 
335
398
  const ctrlKey = e.ctrlKey || e.metaKey;
336
399
 
337
- const range: AreaSelectionRange = {
338
- index: {
339
- x: [colIndex, colIndex],
340
- y: [rowIndex, rowIndex],
341
- },
342
- };
400
+ const range: AreaSelectionRange = makeRange(rowIndex, colIndex, rowIndex, colIndex);
343
401
  // Shift 扩选:从锚点扩展到当前位置,更新最后一个区域
344
402
  if (e.shiftKey && anchorCell && shiftEnabled.value) {
345
403
  const ranges = selectionRanges.value.slice();
346
- const shiftRange: AreaSelectionRange = {
347
- index: {
348
- x: [anchorCell.colIndex, colIndex],
349
- y: [anchorCell.rowIndex, rowIndex],
350
- },
351
- };
404
+ const shiftRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex);
352
405
  if (ranges.length) {
353
406
  ranges[ranges.length - 1] = shiftRange;
354
407
  } else {
@@ -408,12 +461,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
408
461
  /** 更新最后一个选区的终点(拖拽过程中) */
409
462
  function updateSelectionEnd(endRowIndex: number, endColIndex: number) {
410
463
  if (!anchorCell) return;
411
- const newRange: AreaSelectionRange = {
412
- index: {
413
- x: [anchorCell.colIndex, endColIndex],
414
- y: [anchorCell.rowIndex, endRowIndex],
415
- },
416
- };
464
+ const newRange: AreaSelectionRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex);
417
465
  const ranges = [...selectionRanges.value];
418
466
  if (ranges.length > 0) {
419
467
  ranges[ranges.length - 1] = newRange;
@@ -617,14 +665,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
617
665
  // en: If no selection, start from the first cell
618
666
  if (!selectionRanges.value.length) {
619
667
  anchorCell = { rowIndex: 0, colIndex: 0 };
620
- selectionRanges.value = [
621
- {
622
- index: {
623
- x: [0, 0],
624
- y: [0, 0],
625
- },
626
- },
627
- ];
668
+ selectionRanges.value = [makeRange(0, 0, 0, 0)];
628
669
  emitSelectionChange();
629
670
  scrollToCell(0, 0);
630
671
  return;
@@ -635,24 +676,19 @@ export function useAreaSelection<DT extends Record<string, any>>(
635
676
 
636
677
  // Shift 扩展选区,否则移动单格选区
637
678
  if (e.shiftKey && isArrowKey && shiftEnabled.value) {
638
- // 扩展选区:更新最后一个区域的 endRow/endCol
679
+ // 扩展选区:保留 begin,更新最后一个区域的 end
639
680
  const ranges = [...selectionRanges.value];
640
681
  const range = ranges.length > 0 ? ranges[ranges.length - 1] : null;
641
682
  if (!range) return;
642
- const { index } = range;
643
- let newEndRow = index.y[1] + rowDelta;
644
- let newEndCol = index.x[1] + colDelta;
683
+ const { begin, end } = range.index;
684
+ let newEndRow = end.row + rowDelta;
685
+ let newEndCol = end.col + colDelta;
645
686
 
646
687
  // 边界检查
647
688
  newEndRow = clamp(newEndRow, 0, rowCount - 1);
648
689
  newEndCol = clamp(newEndCol, 0, colCount - 1);
649
690
 
650
- ranges[ranges.length - 1] = {
651
- index: {
652
- x: [index.x[0], newEndCol],
653
- y: [index.y[0], newEndRow],
654
- },
655
- };
691
+ ranges[ranges.length - 1] = makeRange(begin.row, begin.col, newEndRow, newEndCol);
656
692
  selectionRanges.value = ranges;
657
693
 
658
694
  scrollToCell(newEndRow, newEndCol);
@@ -681,14 +717,7 @@ export function useAreaSelection<DT extends Record<string, any>>(
681
717
 
682
718
  // 更新锚点和选区(移动单格时清空其他区域,仅保留新位置)
683
719
  anchorCell = { rowIndex: newRow, colIndex: newCol };
684
- selectionRanges.value = [
685
- {
686
- index: {
687
- x: [newCol, newCol],
688
- y: [newRow, newRow],
689
- },
690
- },
691
- ];
720
+ selectionRanges.value = [makeRange(newRow, newCol, newRow, newCol)];
692
721
 
693
722
  scrollToCell(newRow, newCol);
694
723
  }
@@ -1,13 +1,16 @@
1
1
  import { computed, ref } from 'vue';
2
2
  import { useAreaSelection, useAreaSelectionName } from './features';
3
+ import { MY_FN_NAME } from './features/const';
3
4
 
4
5
  type OnDemandFeature = {
5
6
  [useAreaSelectionName]: typeof useAreaSelection<any>;
6
7
  };
7
8
 
8
9
  export const ON_DEMAND_FEATURE: OnDemandFeature = {
9
- [useAreaSelectionName]: (() => {
10
- console.warn('useAreaSelection is not registered');
10
+ [useAreaSelectionName]: (props => {
11
+ if ('useAreaSelection' in props) {
12
+ console.warn('useAreaSelection is not registered');
13
+ }
11
14
  return {
12
15
  config: computed(() => ({ enabled: false })),
13
16
  isSelecting: ref(false),
@@ -26,7 +29,7 @@ type Feature = OnDemandFeature[keyof OnDemandFeature];
26
29
  export function registerFeature(feature: Feature | Feature[]) {
27
30
  const features = Array.isArray(feature) ? feature : [feature];
28
31
  features.forEach(f => {
29
- const fnName = (f as any).stkName;
32
+ const fnName = (f as any)[MY_FN_NAME];
30
33
  if (!fnName) {
31
34
  console.warn('invalid feature');
32
35
  return;
@@ -427,7 +427,7 @@
427
427
  .stk-footer {
428
428
  position: sticky;
429
429
  bottom: 0;
430
- z-index: 1;
430
+ z-index: 2;
431
431
  tr {
432
432
  background-color: var(--tf-bgc);
433
433
  height: var(--footer-row-height);
@@ -349,8 +349,14 @@ export type RowActiveOption<DT> = {
349
349
  /** 单元格选区范围 */
350
350
  export type AreaSelectionRange = {
351
351
  index: {
352
+ /** column index range @deprecated */
352
353
  x: [number, number];
354
+ /** row index range @deprecated */
353
355
  y: [number, number];
356
+ /** start point index*/
357
+ begin: { row: number; col: number };
358
+ /** end point index */
359
+ end: { row: number; col: number };
354
360
  };
355
361
  };
356
362
 
@@ -101,12 +101,15 @@ export function useFixedCol<DT extends Record<string, any>>(
101
101
  const leftShadowCol: StkTableColumn<DT>[] = [];
102
102
  /** 右侧展示阴影的列 */
103
103
  const rightShadowCol: StkTableColumn<DT>[] = [];
104
- tableHeadersForCalc.value.forEach((row, level) => {
104
+ const len = tableHeadersForCalc.value.length;
105
+ for (let level = 0; level < len; level++) {
106
+ const row = tableHeadersForCalc.value[level];
105
107
  /**
106
108
  * 左侧第n个fixed:left 计算要加上前面所有left 的列宽。
107
109
  */
108
110
  let left = 0;
109
- row.forEach(col => {
111
+ for (let i = 0, rowLen = row.length; i < rowLen; i++) {
112
+ const col = row[i];
110
113
  const position = getFixedColPositionValue(col);
111
114
  const isFixedLeft = col.fixed === 'left';
112
115
  const isFixedRight = col.fixed === 'right';
@@ -125,8 +128,8 @@ export function useFixedCol<DT extends Record<string, any>>(
125
128
  rightShadowCol[level] = col;
126
129
  }
127
130
  }
128
- });
129
- });
131
+ }
132
+ }
130
133
 
131
134
  if (props.fixedColShadow) {
132
135
  fixedShadowCols.value = leftShadowCol.concat(rightShadowCol).filter(Boolean) as StkTableColumn<DT>[];
@@ -114,7 +114,7 @@ export function useHighlight(props: any, stkTableId: string, tableContainerRef:
114
114
  if (method === 'animation') {
115
115
  cellEl.animate(keyframe, duration);
116
116
  } else {
117
- highlightCellsInCssKeyFrame(cellEl, rowKeyValue, className, duration);
117
+ highlightCellsInCssKeyFrame(cellEl, rowKeyValue, colKeyValue, className, duration);
118
118
  }
119
119
  }
120
120
 
@@ -198,18 +198,22 @@ export function useHighlight(props: any, stkTableId: string, tableContainerRef:
198
198
  * 使用css @keyframes动画,实现高亮单元格动画
199
199
  * 此方案作为兼容方式。v0.3.4 将使用Element.animate 接口实现动画。
200
200
  */
201
- function highlightCellsInCssKeyFrame(cellEl: HTMLElement, rowKeyValue: UniqKey, className: string, duration: number) {
201
+ function highlightCellsInCssKeyFrame(cellEl: HTMLElement, rowKeyValue: UniqKey, colKeyValue: string, className: string, duration: number) {
202
202
  if (cellEl.classList.contains(className)) {
203
203
  cellEl.classList.remove(className);
204
204
  void cellEl.offsetHeight; // 通知浏览器重绘
205
205
  }
206
206
  cellEl.classList.add(className);
207
- window.clearTimeout(highlightDimCellsTimeout.get(rowKeyValue));
207
+ const cellKey = `${rowKeyValue}-${colKeyValue}`;
208
+ window.clearTimeout(highlightDimCellsTimeout.get(cellKey));
209
+ if (!duration) {
210
+ return;
211
+ }
208
212
  highlightDimCellsTimeout.set(
209
- rowKeyValue,
213
+ cellKey,
210
214
  window.setTimeout(() => {
211
215
  cellEl.classList.remove(className);
212
- highlightDimCellsTimeout.delete(rowKeyValue);
216
+ highlightDimCellsTimeout.delete(cellKey);
213
217
  }, duration),
214
218
  );
215
219
  }