stk-table-vue 1.0.4 → 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/lib/{Dropdown-D7PIq_F7.js → Dropdown-DknXtT1A.js} +2 -2
- package/lib/{StkTable-DuT_ezDm.js → StkTable-6OMgH-gT.js} +196 -20
- package/lib/src/StkTable/StkTable.vue.d.ts +1 -0
- package/lib/src/StkTable/useTree.d.ts +6 -0
- package/lib/stk-table-vue.js +3 -3
- package/lib/style.css +1 -1
- package/lib/test/setTreeExpandParents.test.d.ts +1 -0
- package/lib/test/virtualXMerge.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/StkTable/useTree.ts +52 -0
- package/src/StkTable/useVirtualScroll.ts +141 -21
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v1.0
|
|
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-
|
|
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
|
|
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/
|
|
@@ -2931,6 +2931,7 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2931
2931
|
* @param option.expand expand or collapse
|
|
2932
2932
|
* @param option.all expand all descendants
|
|
2933
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
|
|
2934
2935
|
* @param option.silent if set true, not emit `toggle-tree-expand`, default:false
|
|
2935
2936
|
*/
|
|
2936
2937
|
function privateSetTreeExpand(row, option) {
|
|
@@ -2978,11 +2979,55 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2978
2979
|
onDataSourceChange();
|
|
2979
2980
|
}
|
|
2980
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
|
+
}
|
|
2981
3003
|
privateSetTreeExpand(row, {
|
|
2982
3004
|
...option,
|
|
2983
3005
|
isClick: false
|
|
2984
3006
|
});
|
|
2985
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
|
+
}
|
|
2986
3031
|
function setNodeExpanded(row, expanded, level, parent) {
|
|
2987
3032
|
row.__T_EXP__ = expanded;
|
|
2988
3033
|
if (level !== void 0) row.__T_LV__ = level;
|
|
@@ -3104,6 +3149,12 @@ function useColWidthCache(getColWidth) {
|
|
|
3104
3149
|
/** vue2 优化滚动回收延时 */
|
|
3105
3150
|
var VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
3106
3151
|
/**
|
|
3152
|
+
* 合并单元格可视范围修正的最大迭代次数。
|
|
3153
|
+
* 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
|
|
3154
|
+
* 此上限仅针对重叠合并区域等病态配置兜底,防止修正循环过长。
|
|
3155
|
+
*/
|
|
3156
|
+
var MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
|
|
3157
|
+
/**
|
|
3107
3158
|
* virtual scroll
|
|
3108
3159
|
* @returns
|
|
3109
3160
|
*/
|
|
@@ -3162,20 +3213,145 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3162
3213
|
/** 是否多级表头 */
|
|
3163
3214
|
const isMultiLevelHeader = computed(() => tableHeaders.value.length > 1);
|
|
3164
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
|
+
/**
|
|
3165
3328
|
* 多级表头横向虚拟滚动参数:以顶层列组为单位计算开始/结束位置。
|
|
3166
3329
|
* - 只有整个顶层组完全滚出视口时才移除(避免 colSpan 变化导致抖动)。
|
|
3167
3330
|
* - 单级表头时退化为与 tbody 相同的参数。
|
|
3331
|
+
* - 范围基于列合并修正后的 virtualX_colRange,保证合并单元格完整渲染。
|
|
3168
3332
|
*/
|
|
3169
3333
|
const theadVirtualX = computed(() => {
|
|
3170
|
-
if (!virtualX_on.value
|
|
3171
|
-
startIndex
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
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
|
+
}
|
|
3176
3352
|
const topLevelCols = tableHeaders.value[0];
|
|
3177
3353
|
const totalLeafCount = tableHeaderLast.value.length;
|
|
3178
|
-
let theadStartIndex =
|
|
3354
|
+
let theadStartIndex = totalLeafCount;
|
|
3179
3355
|
let theadEndIndex = totalLeafCount;
|
|
3180
3356
|
let theadOffsetLeft = 0;
|
|
3181
3357
|
let cumLeft = 0;
|
|
@@ -3183,21 +3359,21 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3183
3359
|
for (let i = 0, len = topLevelCols.length; i < len; i++) {
|
|
3184
3360
|
const col = topLevelCols[i];
|
|
3185
3361
|
if (col.fixed === "left" || col.fixed === "right") continue;
|
|
3362
|
+
const groupStart = col.__LF_S__ ?? 0;
|
|
3363
|
+
const groupEnd = col.__LF_E__ ?? groupStart + 1;
|
|
3186
3364
|
const groupWidth = col.__W__ || getCalculatedColWidth(col);
|
|
3187
|
-
|
|
3188
|
-
if (!foundStart && groupRight > scrollLeft) {
|
|
3365
|
+
if (!foundStart && groupEnd > startIndex) {
|
|
3189
3366
|
foundStart = true;
|
|
3190
|
-
theadStartIndex =
|
|
3367
|
+
theadStartIndex = groupStart;
|
|
3191
3368
|
theadOffsetLeft = cumLeft;
|
|
3192
3369
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
theadStartIndex = totalLeafCount;
|
|
3199
|
-
theadOffsetLeft = cumLeft;
|
|
3370
|
+
if (foundStart) {
|
|
3371
|
+
if (groupStart >= endIndex) break;
|
|
3372
|
+
theadEndIndex = groupEnd;
|
|
3373
|
+
}
|
|
3374
|
+
cumLeft += groupWidth;
|
|
3200
3375
|
}
|
|
3376
|
+
if (!foundStart) theadOffsetLeft = cumLeft;
|
|
3201
3377
|
return {
|
|
3202
3378
|
startIndex: theadStartIndex,
|
|
3203
3379
|
endIndex: theadEndIndex,
|
|
@@ -3207,7 +3383,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3207
3383
|
const virtualX_columnPart = computed(() => {
|
|
3208
3384
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
3209
3385
|
if (virtualX_on.value) {
|
|
3210
|
-
const { startIndex, endIndex } =
|
|
3386
|
+
const { startIndex, endIndex } = virtualX_colRange.value;
|
|
3211
3387
|
const maxIndex = tableHeaderLastValue.length;
|
|
3212
3388
|
const validEndIndex = Math.min(endIndex, maxIndex);
|
|
3213
3389
|
const validStartIndex = Math.min(startIndex, maxIndex);
|
|
@@ -3275,7 +3451,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3275
3451
|
});
|
|
3276
3452
|
const virtualX_offsetRight = computed(() => {
|
|
3277
3453
|
if (!virtualX_on.value) return 0;
|
|
3278
|
-
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex :
|
|
3454
|
+
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualX_colRange.value.endIndex;
|
|
3279
3455
|
let width = 0;
|
|
3280
3456
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
3281
3457
|
for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
|
|
@@ -24,6 +24,12 @@ type SetTreeExpandOption = {
|
|
|
24
24
|
* @version 1.0.4
|
|
25
25
|
*/
|
|
26
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;
|
|
27
33
|
};
|
|
28
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[]];
|
|
29
35
|
export {};
|
package/lib/stk-table-vue.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v1.0
|
|
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-
|
|
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-
|
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
package/src/StkTable/useTree.ts
CHANGED
|
@@ -23,6 +23,12 @@ type SetTreeExpandOption = {
|
|
|
23
23
|
* @version 1.0.4
|
|
24
24
|
*/
|
|
25
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;
|
|
26
32
|
};
|
|
27
33
|
|
|
28
34
|
export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void) {
|
|
@@ -42,6 +48,7 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
42
48
|
* @param option.expand expand or collapse
|
|
43
49
|
* @param option.all expand all descendants
|
|
44
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
|
|
45
52
|
* @param option.silent if set true, not emit `toggle-tree-expand`, default:false
|
|
46
53
|
*/
|
|
47
54
|
function privateSetTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option: SetTreeExpandOption & { col?: any; isClick: boolean }) {
|
|
@@ -102,9 +109,54 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
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
|
+
}
|
|
105
133
|
privateSetTreeExpand(row, { ...option, isClick: false });
|
|
106
134
|
}
|
|
107
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
|
+
|
|
108
160
|
function setNodeExpanded(row: DT, expanded: boolean, level?: number, parent?: DT) {
|
|
109
161
|
row.__T_EXP__ = expanded;
|
|
110
162
|
if (level !== void 0) {
|
|
@@ -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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
325
|
+
if (!foundStart && groupEnd > startIndex) {
|
|
204
326
|
foundStart = true;
|
|
205
|
-
theadStartIndex =
|
|
327
|
+
theadStartIndex = groupStart;
|
|
206
328
|
theadOffsetLeft = cumLeft;
|
|
207
329
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
328
|
-
const endIndex = isMultiLevelHeader.value ? theadVirtualX.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++) {
|