stk-table-vue 1.0.3 → 1.1.0

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
@@ -12,13 +12,26 @@
12
12
  </p>
13
13
  </p>
14
14
 
15
- Stk Table Vue(Sticky Table) is a high-performance virtual list component based on Vue.
16
-
17
- Smooth performance with tens of thousands of rows
18
-
19
- Used for real-time data display, with data highlighting and dynamic effects.
20
-
21
- Support Vue3 and Vue2.7
15
+ Stk Table Vue(Sticky Table) is a high-performance virtual scrolling table component based on Vue 3 / Vue 2.7.
16
+
17
+ Smooth performance with tens of thousands of rows. Designed for real-time data display with built-in highlighting and animation effects.
18
+
19
+ - Vue 3 & Vue 2.7 support
20
+ - Virtual scrolling (X & Y axis)
21
+ - CSS `sticky` based fixed columns & fixed header
22
+ - Multi-level header with horizontal virtual scroll
23
+ - Tree table & expandable rows
24
+ - Area selection with keyboard navigation
25
+ - Cell highlighting with Animation API
26
+ - Built-in custom cells: Filter, Editable, Checkbox, Number, Change
27
+ - Column resize, header drag, row drag
28
+ - Merge cells (virtual mode supported)
29
+ - Custom scrollbar
30
+ - Sortable & filterable
31
+ - Theme support (dark / light)
32
+ - CSS variables for easy customization
33
+ - Based on native `<table>` element
34
+ - Zero third-party runtime dependencies
22
35
 
23
36
 
24
37
  ## Documentation
@@ -172,5 +185,5 @@ const dataSource = [
172
185
  Compare performance with other vue table [vue-table-compare](https://github.com/ja-plus/vue-table-compare)
173
186
 
174
187
 
175
- ## Other
176
- * `$*$`
188
+ ## License
189
+ [MIT](LICENSE)
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.0.2
3
+ * version: v1.1.0
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/
7
7
  * license: MIT
8
8
  */
9
- import { t as StkTable_default } from "./StkTable-CoLDxW4i.js";
9
+ import { t as StkTable_default } from "./StkTable-6OMgH-gT.js";
10
10
  import { createElementBlock, createElementVNode, createVNode, defineComponent, h, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, reactive, ref, withModifiers } from "vue";
11
11
  //#region src/StkTable/custom-cells/FilterCell/Dropdown/index.vue?vue&type=script&setup=true&lang.ts
12
12
  var DROPDOWN_DEFAULT_WIDTH = 300;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.0.2
3
+ * version: v1.1.0
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/
@@ -2170,7 +2170,7 @@ function useRowExpand(emits, dataSourceCopy, rowKeyGen, onDataSourceChange) {
2170
2170
  */
2171
2171
  function setRowExpand(rowKeyOrRow, expand, data) {
2172
2172
  let rowKey;
2173
- if (typeof rowKeyOrRow === "string") rowKey = rowKeyOrRow;
2173
+ if (typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number") rowKey = rowKeyOrRow;
2174
2174
  else rowKey = rowKeyGen(rowKeyOrRow);
2175
2175
  const tempData = dataSourceCopy.value.slice();
2176
2176
  const index = tempData.findIndex((it) => rowKeyGen(it) === rowKey);
@@ -2929,6 +2929,9 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
2929
2929
  * @param row rowKey or row
2930
2930
  * @param option
2931
2931
  * @param option.expand expand or collapse
2932
+ * @param option.all expand all descendants
2933
+ * @param option.level expand to the nth level
2934
+ * @param option.parents expand/collapse all ancestors of the given row, the target row itself is also expanded if it has children
2932
2935
  * @param option.silent if set true, not emit `toggle-tree-expand`, default:false
2933
2936
  */
2934
2937
  function privateSetTreeExpand(row, option) {
@@ -2937,7 +2940,7 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
2937
2940
  for (let i = 0; i < rowKeyOrRowArr.length; i++) {
2938
2941
  const rowKeyOrRow = rowKeyOrRowArr[i];
2939
2942
  let rowKey;
2940
- if (typeof rowKeyOrRow === "string") rowKey = rowKeyOrRow;
2943
+ if (typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number") rowKey = rowKeyOrRow;
2941
2944
  else rowKey = rowKeyGen(rowKeyOrRow);
2942
2945
  const index = tempData.findIndex((it) => rowKeyGen(it) === rowKey);
2943
2946
  if (index === -1) {
@@ -2946,12 +2949,22 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
2946
2949
  }
2947
2950
  const row = tempData[index];
2948
2951
  const level = row.__T_LV__ || 0;
2952
+ const wasExpanded = Boolean(row.__T_EXP__);
2949
2953
  let expanded = option === null || option === void 0 ? void 0 : option.expand;
2950
2954
  if (expanded === void 0) expanded = !row.__T_EXP__;
2951
- if (expanded) {
2955
+ if (option.all || option.level !== void 0) {
2956
+ const targetLevel = option.all ? Infinity : option.level || 0;
2957
+ setDescendantsToLevel(row, level + 1, targetLevel, expanded);
2958
+ }
2959
+ if (expanded) if (wasExpanded) {
2960
+ const deleteCount = foldNode(index, tempData, level);
2952
2961
  const children = expandNode(row, level);
2953
- tempData.splice(index + 1, 0, ...children);
2962
+ tempData.splice(index + 1, deleteCount, ...children);
2954
2963
  } else {
2964
+ const children = expandNode(row, level);
2965
+ tempData.splice(index + 1, 0, ...children);
2966
+ }
2967
+ else {
2955
2968
  const deleteCount = foldNode(index, tempData, level);
2956
2969
  tempData.splice(index + 1, deleteCount);
2957
2970
  }
@@ -2966,11 +2979,55 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
2966
2979
  onDataSourceChange();
2967
2980
  }
2968
2981
  function setTreeExpand(row, option) {
2982
+ if (option === null || option === void 0 ? void 0 : option.parents) {
2983
+ var _target$children;
2984
+ const rowKeyOrRow = Array.isArray(row) ? row[0] : row;
2985
+ const rowKey = typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number" ? rowKeyOrRow : rowKeyGen(rowKeyOrRow);
2986
+ const path = findPath(props.dataSource || [], rowKey);
2987
+ if (!path) {
2988
+ console.warn("treeExpandRow failed.rowKey:", rowKey);
2989
+ return;
2990
+ }
2991
+ const expanded = (option === null || option === void 0 ? void 0 : option.expand) !== false;
2992
+ const target = path[path.length - 1];
2993
+ const keys = path.slice(0, -1).map((it) => rowKeyGen(it));
2994
+ if (expanded && ((_target$children = target.children) === null || _target$children === void 0 ? void 0 : _target$children.length)) keys.push(rowKeyGen(target));
2995
+ if (!keys.length) return;
2996
+ if (!expanded) keys.reverse();
2997
+ privateSetTreeExpand(keys, {
2998
+ expand: expanded,
2999
+ isClick: false
3000
+ });
3001
+ return;
3002
+ }
2969
3003
  privateSetTreeExpand(row, {
2970
3004
  ...option,
2971
3005
  isClick: false
2972
3006
  });
2973
3007
  }
3008
+ /**
3009
+ * 在原始树形数据中查找目标节点,返回从根节点到目标节点的完整路径(含目标节点自身)
3010
+ * en: Find target node in raw tree data, return the full path from root to target node (target included)
3011
+ * @returns full path including target, or null if target not found
3012
+ */
3013
+ function findPath(data, targetKey) {
3014
+ const path = [];
3015
+ function dfs(list) {
3016
+ for (const item of list) {
3017
+ if (rowKeyGen(item) === targetKey) {
3018
+ path.push(item);
3019
+ return true;
3020
+ }
3021
+ if (item.children) {
3022
+ path.push(item);
3023
+ if (dfs(item.children)) return true;
3024
+ path.pop();
3025
+ }
3026
+ }
3027
+ return false;
3028
+ }
3029
+ return dfs(data) ? path : null;
3030
+ }
2974
3031
  function setNodeExpanded(row, expanded, level, parent) {
2975
3032
  row.__T_EXP__ = expanded;
2976
3033
  if (level !== void 0) row.__T_LV__ = level;
@@ -3006,6 +3063,17 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
3006
3063
  isFirstLoad = false;
3007
3064
  return result;
3008
3065
  }
3066
+ /**
3067
+ * 递归设置目标节点后代到指定层级的展开/折叠状态
3068
+ * en: Recursively set expand/collapse state for descendants up to the target level
3069
+ */
3070
+ function setDescendantsToLevel(row, currentLevel, targetLevel, expanded) {
3071
+ if (!row.children || currentLevel > targetLevel) return;
3072
+ for (const child of row.children) {
3073
+ setNodeExpanded(child, expanded, currentLevel, row);
3074
+ setDescendantsToLevel(child, currentLevel + 1, targetLevel, expanded);
3075
+ }
3076
+ }
3009
3077
  function expandNode(row, level) {
3010
3078
  let result = [];
3011
3079
  row.children && row.children.forEach((child) => {
@@ -3081,6 +3149,12 @@ function useColWidthCache(getColWidth) {
3081
3149
  /** vue2 优化滚动回收延时 */
3082
3150
  var VUE2_SCROLL_TIMEOUT_MS = 200;
3083
3151
  /**
3152
+ * 合并单元格可视范围修正的最大迭代次数。
3153
+ * 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
3154
+ * 此上限仅针对重叠合并区域等病态配置兜底,防止修正循环过长。
3155
+ */
3156
+ var MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
3157
+ /**
3084
3158
  * virtual scroll
3085
3159
  * @returns
3086
3160
  */
@@ -3139,20 +3213,145 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
3139
3213
  /** 是否多级表头 */
3140
3214
  const isMultiLevelHeader = computed(() => tableHeaders.value.length > 1);
3141
3215
  /**
3216
+ * 需要参与可视范围修正的合并列集合。仅依赖 tableHeaderLast(columns 变化),
3217
+ * 缓存后避免在数据更新、纵向滚动换窗等高频调用 buildColMergeRange 时重复收集。
3218
+ * 无需修正时返回 null。
3219
+ */
3220
+ const virtualX_mergeColsInfo = computed(() => {
3221
+ const headers = tableHeaderLast.value;
3222
+ const headerLength = headers.length;
3223
+ let maxColIndex = headerLength;
3224
+ const mergeCols = [];
3225
+ for (let i = 0; i < headerLength; i++) {
3226
+ const col = headers[i];
3227
+ if (maxColIndex === headerLength && col.fixed === "right") maxColIndex = i;
3228
+ if (col.mergeCells && !col.fixed) mergeCols.push({
3229
+ col,
3230
+ index: i
3231
+ });
3232
+ }
3233
+ if (!mergeCols.length || !maxColIndex) return null;
3234
+ return {
3235
+ headerLength,
3236
+ maxColIndex,
3237
+ mergeCols
3238
+ };
3239
+ });
3240
+ /**
3241
+ * 收集行集合中的列合并(colspan)区间
3242
+ * @param rows 行数据
3243
+ * @param rowIndexBase mergeCells 入参 rowIndex 的起始值
3244
+ */
3245
+ function buildColMergeRange(rows, rowIndexBase) {
3246
+ const mergeColsInfo = virtualX_mergeColsInfo.value;
3247
+ if (!mergeColsInfo) return null;
3248
+ const { headerLength, maxColIndex, mergeCols } = mergeColsInfo;
3249
+ const leftReach = new Int32Array(headerLength).fill(-1);
3250
+ const rightEnd = new Int32Array(headerLength);
3251
+ for (let r = 0; r < rows.length; r++) {
3252
+ const row = rows[r];
3253
+ for (let m = 0; m < mergeCols.length; m++) {
3254
+ const { col, index } = mergeCols[m];
3255
+ if (index >= maxColIndex) break;
3256
+ const { colspan = 1 } = col.mergeCells({
3257
+ row,
3258
+ col,
3259
+ rowIndex: rowIndexBase + r,
3260
+ colIndex: index
3261
+ }) || {};
3262
+ if (colspan > 1) {
3263
+ const end = Math.min(index + colspan, maxColIndex);
3264
+ for (let c = index; c < end; c++) {
3265
+ if (leftReach[c] === -1 || index < leftReach[c]) leftReach[c] = index;
3266
+ if (end > rightEnd[c]) rightEnd[c] = end;
3267
+ }
3268
+ }
3269
+ }
3270
+ }
3271
+ return {
3272
+ leftReach,
3273
+ rightEnd
3274
+ };
3275
+ }
3276
+ /**
3277
+ * 全量数据的列合并覆盖信息(懒计算)。
3278
+ * 仅在 Y 虚拟滚动未开启(可视行即全量数据)且被消费时才遍历全量数据;
3279
+ * 依赖 dataSourceCopy 与 columns,数据/列变化时自动重算。
3280
+ * virtual + virtualX 模式下消费方走可视行分支,本 computed 直接返回 null,
3281
+ * 避免 O(rows × mergeCols) 的全量 mergeCells 调用浪费。
3282
+ */
3283
+ const virtualX_colMergeRangeFull = computed(() => {
3284
+ if (!virtualX_on.value || virtual_on.value) return null;
3285
+ return buildColMergeRange(dataSourceCopy.value, 0);
3286
+ });
3287
+ /**
3288
+ * virtual-x 列合并覆盖信息。
3289
+ * Y 虚拟滚动开启时仅计算可视行(rowIndex 与渲染时 mergeCells 入参保持一致);
3290
+ * 否则使用全量数据懒计算缓存(此时可视行即全量数据)。
3291
+ */
3292
+ const virtualX_colMergeRange = computed(() => {
3293
+ if (!virtualX_on.value) return null;
3294
+ if (virtual_on.value) return buildColMergeRange(virtual_dataSourcePart.value, 0);
3295
+ return virtualX_colMergeRangeFull.value;
3296
+ });
3297
+ /**
3298
+ * 经列合并修正后的可视列范围。
3299
+ * 合并单元格的锚点列可能已滚出可视区(但合并区域仍覆盖可视列),
3300
+ * 也可能合并区域超出可视区右边界,需要将范围扩展到能完整渲染合并单元格。
3301
+ */
3302
+ const virtualX_colRange = computed(() => {
3303
+ let { startIndex, endIndex } = virtualScrollX.value;
3304
+ const mergeRange = virtualX_colMergeRange.value;
3305
+ if (mergeRange) {
3306
+ const { leftReach, rightEnd } = mergeRange;
3307
+ const len = leftReach.length;
3308
+ for (let k = 0; k < MAX_MERGE_RANGE_EXPAND_ITERATIONS; k++) {
3309
+ let newStart = startIndex;
3310
+ let newEnd = endIndex;
3311
+ const loopEnd = Math.min(endIndex, len);
3312
+ for (let i = Math.max(0, startIndex); i < loopEnd; i++) {
3313
+ const reach = leftReach[i];
3314
+ if (reach > -1 && reach < newStart) newStart = reach;
3315
+ if (rightEnd[i] > newEnd) newEnd = rightEnd[i];
3316
+ }
3317
+ if (newStart === startIndex && newEnd === endIndex) break;
3318
+ startIndex = newStart;
3319
+ endIndex = newEnd;
3320
+ }
3321
+ }
3322
+ return {
3323
+ startIndex,
3324
+ endIndex
3325
+ };
3326
+ });
3327
+ /**
3142
3328
  * 多级表头横向虚拟滚动参数:以顶层列组为单位计算开始/结束位置。
3143
3329
  * - 只有整个顶层组完全滚出视口时才移除(避免 colSpan 变化导致抖动)。
3144
3330
  * - 单级表头时退化为与 tbody 相同的参数。
3331
+ * - 范围基于列合并修正后的 virtualX_colRange,保证合并单元格完整渲染。
3145
3332
  */
3146
3333
  const theadVirtualX = computed(() => {
3147
- if (!virtualX_on.value || !isMultiLevelHeader.value) return {
3148
- startIndex: virtualScrollX.value.startIndex,
3149
- endIndex: virtualScrollX.value.endIndex,
3150
- offsetLeft: virtualScrollX.value.offsetLeft
3151
- };
3152
- const { scrollLeft, containerWidth } = virtualScrollX.value;
3334
+ if (!virtualX_on.value) {
3335
+ const { startIndex, endIndex, offsetLeft } = virtualScrollX.value;
3336
+ return {
3337
+ startIndex,
3338
+ endIndex,
3339
+ offsetLeft
3340
+ };
3341
+ }
3342
+ const { startIndex, endIndex } = virtualX_colRange.value;
3343
+ if (!isMultiLevelHeader.value) {
3344
+ const { nonFixedCols } = getColWidthCache(tableHeaderLast.value);
3345
+ const found = binarySearch(nonFixedCols, (mid) => nonFixedCols[mid].index < startIndex ? -1 : 1);
3346
+ return {
3347
+ startIndex,
3348
+ endIndex,
3349
+ offsetLeft: found > 0 ? nonFixedCols[found - 1].cumWidth : 0
3350
+ };
3351
+ }
3153
3352
  const topLevelCols = tableHeaders.value[0];
3154
3353
  const totalLeafCount = tableHeaderLast.value.length;
3155
- let theadStartIndex = 0;
3354
+ let theadStartIndex = totalLeafCount;
3156
3355
  let theadEndIndex = totalLeafCount;
3157
3356
  let theadOffsetLeft = 0;
3158
3357
  let cumLeft = 0;
@@ -3160,21 +3359,21 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
3160
3359
  for (let i = 0, len = topLevelCols.length; i < len; i++) {
3161
3360
  const col = topLevelCols[i];
3162
3361
  if (col.fixed === "left" || col.fixed === "right") continue;
3362
+ const groupStart = col.__LF_S__ ?? 0;
3363
+ const groupEnd = col.__LF_E__ ?? groupStart + 1;
3163
3364
  const groupWidth = col.__W__ || getCalculatedColWidth(col);
3164
- const groupRight = cumLeft + groupWidth;
3165
- if (!foundStart && groupRight > scrollLeft) {
3365
+ if (!foundStart && groupEnd > startIndex) {
3166
3366
  foundStart = true;
3167
- theadStartIndex = col.__LF_S__ ?? 0;
3367
+ theadStartIndex = groupStart;
3168
3368
  theadOffsetLeft = cumLeft;
3169
3369
  }
3170
- cumLeft = groupRight;
3171
- theadEndIndex = col.__LF_E__ ?? totalLeafCount;
3172
- if (foundStart && groupRight >= scrollLeft + containerWidth) break;
3173
- }
3174
- if (!foundStart) {
3175
- theadStartIndex = totalLeafCount;
3176
- theadOffsetLeft = cumLeft;
3370
+ if (foundStart) {
3371
+ if (groupStart >= endIndex) break;
3372
+ theadEndIndex = groupEnd;
3373
+ }
3374
+ cumLeft += groupWidth;
3177
3375
  }
3376
+ if (!foundStart) theadOffsetLeft = cumLeft;
3178
3377
  return {
3179
3378
  startIndex: theadStartIndex,
3180
3379
  endIndex: theadEndIndex,
@@ -3184,7 +3383,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
3184
3383
  const virtualX_columnPart = computed(() => {
3185
3384
  const tableHeaderLastValue = tableHeaderLast.value;
3186
3385
  if (virtualX_on.value) {
3187
- const { startIndex, endIndex } = virtualScrollX.value;
3386
+ const { startIndex, endIndex } = virtualX_colRange.value;
3188
3387
  const maxIndex = tableHeaderLastValue.length;
3189
3388
  const validEndIndex = Math.min(endIndex, maxIndex);
3190
3389
  const validStartIndex = Math.min(startIndex, maxIndex);
@@ -3252,7 +3451,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
3252
3451
  });
3253
3452
  const virtualX_offsetRight = computed(() => {
3254
3453
  if (!virtualX_on.value) return 0;
3255
- const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualScrollX.value.endIndex;
3454
+ const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualX_colRange.value.endIndex;
3256
3455
  let width = 0;
3257
3456
  const tableHeaderLastValue = tableHeaderLast.value;
3258
3457
  for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
@@ -381,6 +381,9 @@ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<im
381
381
  children?: (PrivateRowDT & /*elided*/ any)[];
382
382
  }))[], option?: {
383
383
  expand?: boolean;
384
+ all?: boolean;
385
+ level?: number;
386
+ parents?: boolean;
384
387
  }) => void;
385
388
  /**
386
389
  * 获取拖选选中的单元格信息
@@ -4,7 +4,32 @@ import { PrivateRowDT, RowKeyGen, UniqKey } from './types';
4
4
  type DT = PrivateRowDT & {
5
5
  children?: DT[];
6
6
  };
7
- export declare function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void): readonly [(row: DT, col: any) => void, (row: (UniqKey | DT) | (UniqKey | DT)[], option?: {
7
+ type SetTreeExpandOption = {
8
+ /**
9
+ * 是否展开
10
+ * en: Whether to expand
11
+ * @default false
12
+ */
8
13
  expand?: boolean;
9
- }) => void, (data: DT[]) => DT[]];
14
+ /**
15
+ * 是否展开所有子节点
16
+ * en: Whether to expand all child nodes
17
+ * @default false
18
+ * @version 1.0.4
19
+ */
20
+ all?: boolean;
21
+ /**
22
+ * 展开到第几层
23
+ * en: Expand to the nth level
24
+ * @version 1.0.4
25
+ */
26
+ level?: number;
27
+ /**
28
+ * 将传入 row 视为目标子节点,展开/收起其所有父节点;展开时若目标行自身有子节点则一并展开
29
+ * en: Treat the given row as a target child, expand/collapse all its ancestors. The target row itself is also expanded when expanding if it has children
30
+ * @version 1.1.0
31
+ */
32
+ parents?: boolean;
33
+ };
34
+ export declare function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void): readonly [(row: DT, col: any) => void, (row: (UniqKey | DT) | (UniqKey | DT)[], option?: SetTreeExpandOption) => void, (data: DT[]) => DT[]];
10
35
  export {};
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.0.2
3
+ * version: v1.1.0
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/
7
7
  * license: MIT
8
8
  */
9
- import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-CoLDxW4i.js";
9
+ import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-6OMgH-gT.js";
10
10
  import { Fragment, computed, createApp, createBlock, createElementBlock, createElementVNode, createTextVNode, defineComponent, getCurrentInstance, h, markRaw, nextTick, normalizeClass, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, watch, withModifiers } from "vue";
11
11
  //#region src/StkTable/custom-cells/FilterCell/Dropdown/index.ts
12
12
  var DropdownIns = null;
@@ -15,7 +15,7 @@ async function getDropdownIns() {
15
15
  const div = document.createElement("div");
16
16
  div.classList.add("stk-filter-dropdown-wrapper");
17
17
  document.body.appendChild(div);
18
- DropdownIns = createApp(await import("./Dropdown-gzfyZN4Y.js").then((module) => module.default)).mount(div);
18
+ DropdownIns = createApp(await import("./Dropdown-DknXtT1A.js").then((module) => module.default)).mount(div);
19
19
  }
20
20
  return DropdownIns;
21
21
  }
package/lib/style.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.0.2
3
+ * version: v1.1.0
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/
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stk-table-vue",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
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",
@@ -1,4 +1,4 @@
1
- import { ShallowRef, nextTick } from 'vue';
1
+ import { ShallowRef } from 'vue';
2
2
  import { EXPANDED_ROW_KEY_PREFIX } from './const';
3
3
  import { PrivateRowDT, RowKeyGen, StkTableColumn, UniqKey } from './types';
4
4
  type DT = PrivateRowDT;
@@ -24,7 +24,7 @@ export function useRowExpand(emits: any, dataSourceCopy: ShallowRef<DT[]>, rowKe
24
24
  */
25
25
  function setRowExpand(rowKeyOrRow: string | undefined | DT, expand?: boolean | null, data?: { col?: StkTableColumn<DT>; silent?: boolean }) {
26
26
  let rowKey: UniqKey;
27
- if (typeof rowKeyOrRow === 'string') {
27
+ if (typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number') {
28
28
  rowKey = rowKeyOrRow;
29
29
  } else {
30
30
  rowKey = rowKeyGen(rowKeyOrRow);
@@ -1,8 +1,36 @@
1
- import { ShallowRef, nextTick } from 'vue';
1
+ import { ShallowRef } from 'vue';
2
2
  import { PrivateRowDT, RowKeyGen, TreeConfig, UniqKey } from './types';
3
3
 
4
4
  type DT = PrivateRowDT & { children?: DT[] };
5
5
 
6
+ type SetTreeExpandOption = {
7
+ /**
8
+ * 是否展开
9
+ * en: Whether to expand
10
+ * @default false
11
+ */
12
+ expand?: boolean;
13
+ /**
14
+ * 是否展开所有子节点
15
+ * en: Whether to expand all child nodes
16
+ * @default false
17
+ * @version 1.0.4
18
+ */
19
+ all?: boolean;
20
+ /**
21
+ * 展开到第几层
22
+ * en: Expand to the nth level
23
+ * @version 1.0.4
24
+ */
25
+ level?: number;
26
+ /**
27
+ * 将传入 row 视为目标子节点,展开/收起其所有父节点;展开时若目标行自身有子节点则一并展开
28
+ * en: Treat the given row as a target child, expand/collapse all its ancestors. The target row itself is also expanded when expanding if it has children
29
+ * @version 1.1.0
30
+ */
31
+ parents?: boolean;
32
+ };
33
+
6
34
  export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void) {
7
35
  const { defaultExpandAll, defaultExpandKeys, defaultExpandLevel }: TreeConfig = props.treeConfig;
8
36
  /** It used to check if it is first load. To execute defaultExpandXXX */
@@ -18,16 +46,19 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
18
46
  * @param row rowKey or row
19
47
  * @param option
20
48
  * @param option.expand expand or collapse
49
+ * @param option.all expand all descendants
50
+ * @param option.level expand to the nth level
51
+ * @param option.parents expand/collapse all ancestors of the given row, the target row itself is also expanded if it has children
21
52
  * @param option.silent if set true, not emit `toggle-tree-expand`, default:false
22
53
  */
23
- function privateSetTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option: { expand?: boolean; col?: any; isClick: boolean }) {
54
+ function privateSetTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option: SetTreeExpandOption & { col?: any; isClick: boolean }) {
24
55
  const rowKeyOrRowArr: (UniqKey | DT)[] = Array.isArray(row) ? row : [row];
25
56
 
26
57
  const tempData = dataSourceCopy.value.slice();
27
58
  for (let i = 0; i < rowKeyOrRowArr.length; i++) {
28
59
  const rowKeyOrRow = rowKeyOrRowArr[i];
29
60
  let rowKey: UniqKey;
30
- if (typeof rowKeyOrRow === 'string') {
61
+ if (typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number') {
31
62
  rowKey = rowKeyOrRow;
32
63
  } else {
33
64
  rowKey = rowKeyGen(rowKeyOrRow);
@@ -40,13 +71,26 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
40
71
 
41
72
  const row = tempData[index];
42
73
  const level = row.__T_LV__ || 0;
74
+ const wasExpanded = Boolean(row.__T_EXP__);
43
75
  let expanded = option?.expand;
44
76
  if (expanded === void 0) {
45
77
  expanded = !row.__T_EXP__;
46
78
  }
79
+ if (option.all || option.level !== void 0) {
80
+ const targetLevel = option.all ? Infinity : option.level || 0;
81
+ setDescendantsToLevel(row, level + 1, targetLevel, expanded);
82
+ }
47
83
  if (expanded) {
48
- const children = expandNode(row, level);
49
- tempData.splice(index + 1, 0, ...children);
84
+ if (wasExpanded) {
85
+ // already expanded, rebuild the flattened subtree so newly expanded
86
+ // descendants are inserted into the visible data source
87
+ const deleteCount = foldNode(index, tempData, level);
88
+ const children = expandNode(row, level);
89
+ tempData.splice(index + 1, deleteCount, ...children);
90
+ } else {
91
+ const children = expandNode(row, level);
92
+ tempData.splice(index + 1, 0, ...children);
93
+ }
50
94
  } else {
51
95
  // delete all child nodes from i
52
96
  const deleteCount = foldNode(index, tempData, level);
@@ -64,10 +108,55 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
64
108
  onDataSourceChange();
65
109
  }
66
110
 
67
- function setTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option?: { expand?: boolean }) {
111
+ function setTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option?: SetTreeExpandOption) {
112
+ if (option?.parents) {
113
+ const rowKeyOrRow = Array.isArray(row) ? row[0] : row;
114
+ const rowKey = typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number' ? rowKeyOrRow : rowKeyGen(rowKeyOrRow);
115
+ const path = findPath(props.dataSource || [], rowKey);
116
+ if (!path) {
117
+ console.warn('treeExpandRow failed.rowKey:', rowKey);
118
+ return;
119
+ }
120
+ const expanded = option?.expand !== false;
121
+ const target = path[path.length - 1];
122
+ const keys = path.slice(0, -1).map(it => rowKeyGen(it));
123
+ // 展开时若目标行自身有子节点则一并展开;收起时仅处理父节点,目标行自身状态不变
124
+ // en: when expanding, also expand the target row itself if it has children; when collapsing, only ancestors are handled
125
+ if (expanded && target.children?.length) keys.push(rowKeyGen(target));
126
+ if (!keys.length) return;
127
+ // 展开时从根到目标逐级展开;收起时逆序处理,避免先折叠根节点导致其余节点从可见数据中移除而查找失败
128
+ // en: expand from root to target; collapse in reverse order, otherwise collapsing the root first removes the rest nodes from visible data
129
+ if (!expanded) keys.reverse();
130
+ privateSetTreeExpand(keys, { expand: expanded, isClick: false });
131
+ return;
132
+ }
68
133
  privateSetTreeExpand(row, { ...option, isClick: false });
69
134
  }
70
135
 
136
+ /**
137
+ * 在原始树形数据中查找目标节点,返回从根节点到目标节点的完整路径(含目标节点自身)
138
+ * en: Find target node in raw tree data, return the full path from root to target node (target included)
139
+ * @returns full path including target, or null if target not found
140
+ */
141
+ function findPath(data: DT[], targetKey: UniqKey): DT[] | null {
142
+ const path: DT[] = [];
143
+ function dfs(list: DT[]): boolean {
144
+ for (const item of list) {
145
+ if (rowKeyGen(item) === targetKey) {
146
+ path.push(item);
147
+ return true;
148
+ }
149
+ if (item.children) {
150
+ path.push(item);
151
+ if (dfs(item.children)) return true;
152
+ path.pop();
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+ return dfs(data) ? path : null;
158
+ }
159
+
71
160
  function setNodeExpanded(row: DT, expanded: boolean, level?: number, parent?: DT) {
72
161
  row.__T_EXP__ = expanded;
73
162
  if (level !== void 0) {
@@ -119,6 +208,18 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
119
208
  return result;
120
209
  }
121
210
 
211
+ /**
212
+ * 递归设置目标节点后代到指定层级的展开/折叠状态
213
+ * en: Recursively set expand/collapse state for descendants up to the target level
214
+ */
215
+ function setDescendantsToLevel(row: DT, currentLevel: number, targetLevel: number, expanded: boolean) {
216
+ if (!row.children || currentLevel > targetLevel) return;
217
+ for (const child of row.children) {
218
+ setNodeExpanded(child, expanded, currentLevel, row);
219
+ setDescendantsToLevel(child, currentLevel + 1, targetLevel, expanded);
220
+ }
221
+ }
222
+
122
223
  function expandNode(row: DT, level: number) {
123
224
  let result: DT[] = [];
124
225
  row.children &&
@@ -85,6 +85,13 @@ function useColWidthCache<T extends { fixed?: StkTableColumn<PrivateRowDT>['fixe
85
85
  /** vue2 优化滚动回收延时 */
86
86
  const VUE2_SCROLL_TIMEOUT_MS = 200;
87
87
 
88
+ /**
89
+ * 合并单元格可视范围修正的最大迭代次数。
90
+ * 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
91
+ * 此上限仅针对重叠合并区域等病态配置兜底,防止修正循环过长。
92
+ */
93
+ const MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
94
+
88
95
  /**
89
96
  * virtual scroll
90
97
  * @returns
@@ -170,24 +177,138 @@ export function useVirtualScroll(
170
177
  /** 是否多级表头 */
171
178
  const isMultiLevelHeader = computed(() => tableHeaders.value.length > 1);
172
179
 
180
+ /**
181
+ * 需要参与可视范围修正的合并列集合。仅依赖 tableHeaderLast(columns 变化),
182
+ * 缓存后避免在数据更新、纵向滚动换窗等高频调用 buildColMergeRange 时重复收集。
183
+ * 无需修正时返回 null。
184
+ */
185
+ const virtualX_mergeColsInfo = computed(() => {
186
+ const headers = tableHeaderLast.value;
187
+ const headerLength = headers.length;
188
+ // 右固定列始终渲染在可视区末尾,不参与主可视区合并修正
189
+ let maxColIndex = headerLength;
190
+ const mergeCols: { col: PrivateStkTableColumn<PrivateRowDT>; index: number }[] = [];
191
+ for (let i = 0; i < headerLength; i++) {
192
+ const col = headers[i];
193
+ if (maxColIndex === headerLength && col.fixed === 'right') maxColIndex = i;
194
+ // 固定列始终渲染,不需要修正可视范围
195
+ if (col.mergeCells && !col.fixed) mergeCols.push({ col, index: i });
196
+ }
197
+ if (!mergeCols.length || !maxColIndex) return null;
198
+ return { headerLength, maxColIndex, mergeCols };
199
+ });
200
+
201
+ /**
202
+ * 收集行集合中的列合并(colspan)区间
203
+ * @param rows 行数据
204
+ * @param rowIndexBase mergeCells 入参 rowIndex 的起始值
205
+ */
206
+ function buildColMergeRange(rows: PrivateRowDT[], rowIndexBase: number) {
207
+ const mergeColsInfo = virtualX_mergeColsInfo.value;
208
+ if (!mergeColsInfo) return null;
209
+ const { headerLength, maxColIndex, mergeCols } = mergeColsInfo;
210
+
211
+ const leftReach = new Int32Array(headerLength).fill(-1);
212
+ const rightEnd = new Int32Array(headerLength);
213
+ for (let r = 0; r < rows.length; r++) {
214
+ const row = rows[r];
215
+ for (let m = 0; m < mergeCols.length; m++) {
216
+ const { col, index } = mergeCols[m];
217
+ if (index >= maxColIndex) break;
218
+ const { colspan = 1 } = col.mergeCells!({ row, col, rowIndex: rowIndexBase + r, colIndex: index }) || {};
219
+ if (colspan > 1) {
220
+ const end = Math.min(index + colspan, maxColIndex);
221
+ for (let c = index; c < end; c++) {
222
+ if (leftReach[c] === -1 || index < leftReach[c]) leftReach[c] = index;
223
+ if (end > rightEnd[c]) rightEnd[c] = end;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ return { leftReach, rightEnd };
229
+ }
230
+
231
+ /**
232
+ * 全量数据的列合并覆盖信息(懒计算)。
233
+ * 仅在 Y 虚拟滚动未开启(可视行即全量数据)且被消费时才遍历全量数据;
234
+ * 依赖 dataSourceCopy 与 columns,数据/列变化时自动重算。
235
+ * virtual + virtualX 模式下消费方走可视行分支,本 computed 直接返回 null,
236
+ * 避免 O(rows × mergeCols) 的全量 mergeCells 调用浪费。
237
+ */
238
+ const virtualX_colMergeRangeFull = computed(() => {
239
+ if (!virtualX_on.value || virtual_on.value) return null;
240
+ return buildColMergeRange(dataSourceCopy.value, 0);
241
+ });
242
+
243
+ /**
244
+ * virtual-x 列合并覆盖信息。
245
+ * Y 虚拟滚动开启时仅计算可视行(rowIndex 与渲染时 mergeCells 入参保持一致);
246
+ * 否则使用全量数据懒计算缓存(此时可视行即全量数据)。
247
+ */
248
+ const virtualX_colMergeRange = computed(() => {
249
+ if (!virtualX_on.value) return null;
250
+ if (virtual_on.value) {
251
+ return buildColMergeRange(virtual_dataSourcePart.value, 0);
252
+ }
253
+ return virtualX_colMergeRangeFull.value;
254
+ });
255
+
256
+ /**
257
+ * 经列合并修正后的可视列范围。
258
+ * 合并单元格的锚点列可能已滚出可视区(但合并区域仍覆盖可视列),
259
+ * 也可能合并区域超出可视区右边界,需要将范围扩展到能完整渲染合并单元格。
260
+ */
261
+ const virtualX_colRange = computed(() => {
262
+ let { startIndex, endIndex } = virtualScrollX.value;
263
+ const mergeRange = virtualX_colMergeRange.value;
264
+ if (mergeRange) {
265
+ const { leftReach, rightEnd } = mergeRange;
266
+ const len = leftReach.length;
267
+ // 扩展后的范围可能引入新的合并单元格,迭代直到稳定(合法配置最多 2 轮,上限兜底病态配置)
268
+ for (let k = 0; k < MAX_MERGE_RANGE_EXPAND_ITERATIONS; k++) {
269
+ let newStart = startIndex;
270
+ let newEnd = endIndex;
271
+ const loopEnd = Math.min(endIndex, len);
272
+ for (let i = Math.max(0, startIndex); i < loopEnd; i++) {
273
+ const reach = leftReach[i];
274
+ if (reach > -1 && reach < newStart) newStart = reach;
275
+ if (rightEnd[i] > newEnd) newEnd = rightEnd[i];
276
+ }
277
+ if (newStart === startIndex && newEnd === endIndex) break;
278
+ startIndex = newStart;
279
+ endIndex = newEnd;
280
+ }
281
+ }
282
+ return { startIndex, endIndex };
283
+ });
284
+
173
285
  /**
174
286
  * 多级表头横向虚拟滚动参数:以顶层列组为单位计算开始/结束位置。
175
287
  * - 只有整个顶层组完全滚出视口时才移除(避免 colSpan 变化导致抖动)。
176
288
  * - 单级表头时退化为与 tbody 相同的参数。
289
+ * - 范围基于列合并修正后的 virtualX_colRange,保证合并单元格完整渲染。
177
290
  */
178
291
  const theadVirtualX = computed(() => {
179
- if (!virtualX_on.value || !isMultiLevelHeader.value) {
180
- return {
181
- startIndex: virtualScrollX.value.startIndex,
182
- endIndex: virtualScrollX.value.endIndex,
183
- offsetLeft: virtualScrollX.value.offsetLeft,
184
- };
292
+ if (!virtualX_on.value) {
293
+ const { startIndex, endIndex, offsetLeft } = virtualScrollX.value;
294
+ return { startIndex, endIndex, offsetLeft };
185
295
  }
186
- const { scrollLeft, containerWidth } = virtualScrollX.value;
296
+
297
+ const { startIndex, endIndex } = virtualX_colRange.value;
298
+
299
+ if (!isMultiLevelHeader.value) {
300
+ // 单级表头:offsetLeft 为起始列之前所有非固定列宽度之和
301
+ const { nonFixedCols } = getColWidthCache(tableHeaderLast.value);
302
+ const found = binarySearch(nonFixedCols, mid => (nonFixedCols[mid].index < startIndex ? -1 : 1));
303
+ const offsetLeft = found > 0 ? nonFixedCols[found - 1].cumWidth : 0;
304
+ return { startIndex, endIndex, offsetLeft };
305
+ }
306
+
307
+ // 多级表头:保留与修正后可视范围有交集的顶层列组
187
308
  const topLevelCols = tableHeaders.value[0];
188
309
  const totalLeafCount = tableHeaderLast.value.length;
189
310
 
190
- let theadStartIndex = 0;
311
+ let theadStartIndex = totalLeafCount;
191
312
  let theadEndIndex = totalLeafCount;
192
313
  let theadOffsetLeft = 0;
193
314
  let cumLeft = 0;
@@ -197,25 +318,23 @@ export function useVirtualScroll(
197
318
  const col = topLevelCols[i];
198
319
  if (col.fixed === 'left' || col.fixed === 'right') continue;
199
320
 
321
+ const groupStart = col.__LF_S__ ?? 0;
322
+ const groupEnd = col.__LF_E__ ?? groupStart + 1;
200
323
  const groupWidth = col.__W__ || getCalculatedColWidth(col);
201
- const groupRight = cumLeft + groupWidth;
202
324
 
203
- if (!foundStart && groupRight > scrollLeft) {
325
+ if (!foundStart && groupEnd > startIndex) {
204
326
  foundStart = true;
205
- theadStartIndex = col.__LF_S__ ?? 0;
327
+ theadStartIndex = groupStart;
206
328
  theadOffsetLeft = cumLeft;
207
329
  }
208
- cumLeft = groupRight;
209
-
210
- theadEndIndex = col.__LF_E__ ?? totalLeafCount;
211
- if (foundStart && groupRight >= scrollLeft + containerWidth) {
212
- // find end
213
- break;
330
+ if (foundStart) {
331
+ if (groupStart >= endIndex) break;
332
+ theadEndIndex = groupEnd;
214
333
  }
334
+ cumLeft += groupWidth;
215
335
  }
216
336
 
217
337
  if (!foundStart) {
218
- theadStartIndex = totalLeafCount;
219
338
  theadOffsetLeft = cumLeft;
220
339
  }
221
340
 
@@ -225,7 +344,8 @@ export function useVirtualScroll(
225
344
  const virtualX_columnPart = computed(() => {
226
345
  const tableHeaderLastValue = tableHeaderLast.value;
227
346
  if (virtualX_on.value) {
228
- const { startIndex, endIndex } = virtualScrollX.value;
347
+ // 使用列合并修正后的可视范围
348
+ const { startIndex, endIndex } = virtualX_colRange.value;
229
349
  // 将索引钳制到列数组范围内,防止列数减少时越界
230
350
  const maxIndex = tableHeaderLastValue.length;
231
351
  const validEndIndex = Math.min(endIndex, maxIndex);
@@ -324,8 +444,8 @@ export function useVirtualScroll(
324
444
 
325
445
  const virtualX_offsetRight = computed(() => {
326
446
  if (!virtualX_on.value) return 0;
327
- // 多级表头使用 theadEndIndex,单级使用 body endIndex
328
- const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualScrollX.value.endIndex;
447
+ // 多级表头使用 theadEndIndex,单级使用列合并修正后的 endIndex
448
+ const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualX_colRange.value.endIndex;
329
449
  let width = 0;
330
450
  const tableHeaderLastValue = tableHeaderLast.value;
331
451
  for (let i = endIndex; i < tableHeaderLastValue.length; i++) {