stk-table-vue 0.8.13 → 0.9.0-beta.1

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.
Files changed (38) hide show
  1. package/README.md +172 -180
  2. package/lib/src/StkTable/StkTable.vue.d.ts +22 -2
  3. package/lib/src/StkTable/useScrollbar.d.ts +57 -0
  4. package/lib/src/StkTable/utils/index.d.ts +10 -0
  5. package/lib/stk-table-vue.js +563 -294
  6. package/lib/style.css +49 -2
  7. package/package.json +74 -72
  8. package/src/StkTable/StkTable.vue +1730 -1653
  9. package/src/StkTable/components/DragHandle.vue +9 -9
  10. package/src/StkTable/components/SortIcon.vue +6 -6
  11. package/src/StkTable/components/TriangleIcon.vue +3 -3
  12. package/src/StkTable/const.ts +50 -50
  13. package/src/StkTable/index.ts +4 -4
  14. package/src/StkTable/style.less +627 -580
  15. package/src/StkTable/types/highlightDimOptions.ts +26 -26
  16. package/src/StkTable/types/index.ts +297 -297
  17. package/src/StkTable/useAutoResize.ts +91 -91
  18. package/src/StkTable/useColResize.ts +216 -216
  19. package/src/StkTable/useFixedCol.ts +150 -148
  20. package/src/StkTable/useFixedStyle.ts +75 -75
  21. package/src/StkTable/useGetFixedColPosition.ts +65 -65
  22. package/src/StkTable/useHighlight.ts +257 -257
  23. package/src/StkTable/useKeyboardArrowScroll.ts +112 -112
  24. package/src/StkTable/useMaxRowSpan.ts +55 -55
  25. package/src/StkTable/useMergeCells.ts +120 -123
  26. package/src/StkTable/useRowExpand.ts +88 -88
  27. package/src/StkTable/useScrollRowByRow.ts +114 -79
  28. package/src/StkTable/useScrollbar.ts +187 -0
  29. package/src/StkTable/useThDrag.ts +102 -102
  30. package/src/StkTable/useTrDrag.ts +113 -118
  31. package/src/StkTable/useTree.ts +161 -161
  32. package/src/StkTable/useVirtualScroll.ts +494 -494
  33. package/src/StkTable/utils/constRefUtils.ts +29 -29
  34. package/src/StkTable/utils/index.ts +287 -242
  35. package/src/StkTable/utils/useTriggerRef.ts +33 -33
  36. package/src/VirtualTree.vue +622 -622
  37. package/src/VirtualTreeSelect.vue +367 -367
  38. package/src/vite-env.d.ts +10 -10
@@ -1,494 +1,494 @@
1
- import { Ref, ShallowRef, computed, ref } from 'vue';
2
- import { DEFAULT_ROW_HEIGHT, DEFAULT_TABLE_HEIGHT, DEFAULT_TABLE_WIDTH } from './const';
3
- import { AutoRowHeightConfig, PrivateRowDT, PrivateStkTableColumn, RowKeyGen, StkTableColumn, UniqKey } from './types';
4
- import { getCalculatedColWidth } from './utils/constRefUtils';
5
-
6
- type Option<DT extends Record<string, any>> = {
7
- props: any;
8
- tableContainerRef: Ref<HTMLElement | undefined>;
9
- trRef: Ref<HTMLTableRowElement[] | undefined>;
10
- dataSourceCopy: ShallowRef<PrivateRowDT[]>;
11
- tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>;
12
- tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>;
13
- rowKeyGen: RowKeyGen;
14
- maxRowSpan: Map<UniqKey, number>;
15
- };
16
-
17
- /** 暂存纵向虚拟滚动的数据 */
18
- export type VirtualScrollStore = {
19
- /** 容器高度 */
20
- containerHeight: number;
21
- /** 一页的大小 */
22
- pageSize: number;
23
- /** 数组开始位置 */
24
- startIndex: number;
25
- /** 数组结束位置 */
26
- endIndex: number;
27
- /** 行高 */
28
- rowHeight: number;
29
- /** 表格定位上边距 */
30
- offsetTop: number;
31
- /** 纵向滚动条位置,用于判断是横向滚动还是纵向 */
32
- scrollTop: number;
33
- /** 总滚动高度 */
34
- scrollHeight: number;
35
- };
36
- /** 暂存横向虚拟滚动的数据 */
37
- export type VirtualScrollXStore = {
38
- /** 父容器宽度 */
39
- containerWidth: number;
40
- /** 滚动容器的宽度 */
41
- scrollWidth: number;
42
- /** 开始位置 */
43
- startIndex: number;
44
- /** 结束始位置 */
45
- endIndex: number;
46
- /** 表格定位左边距 */
47
- offsetLeft: number;
48
- /** 横向滚动位置,用于判断是横向滚动还是纵向 */
49
- scrollLeft: number;
50
- };
51
-
52
- /** vue2 优化滚动回收延时 */
53
- const VUE2_SCROLL_TIMEOUT_MS = 200;
54
-
55
- /**
56
- * virtual scroll
57
- * @param param0
58
- * @returns
59
- */
60
- export function useVirtualScroll<DT extends Record<string, any>>({
61
- props,
62
- tableContainerRef,
63
- trRef,
64
- dataSourceCopy,
65
- tableHeaderLast,
66
- tableHeaders,
67
- rowKeyGen,
68
- maxRowSpan,
69
- }: Option<DT>) {
70
- const tableHeaderHeight = computed(() => props.headerRowHeight * tableHeaders.value.length);
71
-
72
-
73
- const virtualScroll = ref<VirtualScrollStore>({
74
- containerHeight: 0,
75
- rowHeight: props.rowHeight,
76
- pageSize: 0,
77
- startIndex: 0,
78
- endIndex: 0,
79
- offsetTop: 0,
80
- scrollTop: 0,
81
- scrollHeight: 0,
82
- });
83
-
84
- // TODO: init pageSize
85
-
86
- const virtualScrollX = ref<VirtualScrollXStore>({
87
- containerWidth: 0,
88
- scrollWidth: 0,
89
- startIndex: 0,
90
- endIndex: 0,
91
- offsetLeft: 0,
92
- scrollLeft: 0,
93
- });
94
-
95
- const hasExpandCol = computed(() => {
96
- return tableHeaderLast.value.some(col => col.type === 'expand');
97
- });
98
-
99
- /** 是否虚拟滚动标志 */
100
- const virtual_on = computed(() => {
101
- return props.virtual && dataSourceCopy.value.length > virtualScroll.value.pageSize;
102
- });
103
-
104
- const virtual_dataSourcePart = computed(() => {
105
- if (!virtual_on.value) return dataSourceCopy.value;
106
- const { startIndex, endIndex } = virtualScroll.value;
107
- return dataSourceCopy.value.slice(startIndex, endIndex + 1);
108
- });
109
-
110
- const virtual_offsetBottom = computed(() => {
111
- if (!virtual_on.value) return 0;
112
- const { startIndex, endIndex } = virtualScroll.value;
113
- const dataSourceCopyValue = dataSourceCopy.value;
114
- const rowHeight = getRowHeightFn.value();
115
- if (props.autoRowHeight) {
116
- let offsetBottom = 0;
117
- for (let i = endIndex + 1; i < dataSourceCopyValue.length; i++) {
118
- const rowHeight = getRowHeightFn.value(dataSourceCopyValue[i]);
119
- offsetBottom += rowHeight;
120
- }
121
- return offsetBottom;
122
- }
123
-
124
- return (dataSourceCopyValue.length - startIndex - virtual_dataSourcePart.value.length) * rowHeight;
125
- });
126
-
127
- const virtualX_on = computed(() => {
128
- return (
129
- props.virtualX &&
130
- tableHeaderLast.value.reduce((sum, col) => (sum += getCalculatedColWidth(col)), 0) > virtualScrollX.value.containerWidth + 100
131
- );
132
- });
133
-
134
- const virtualX_columnPart = computed(() => {
135
- const tableHeaderLastValue = tableHeaderLast.value;
136
- if (virtualX_on.value) {
137
- // 虚拟横向滚动,固定列要一直保持存在
138
- const leftCols: PrivateStkTableColumn<PrivateRowDT>[] = [];
139
- const rightCols: PrivateStkTableColumn<PrivateRowDT>[] = [];
140
- /**
141
- * 存在问题:
142
- * table columns 从多到少时。比方原来的start=5,end=10,现在start=4,end=8。这时候endIndex就超出数组范围了。
143
- * FIXME: 如果新列数 < endIndex,此时需要重新计算列startIndex和endIndex。
144
- */
145
- const { startIndex, endIndex } = virtualScrollX.value;
146
-
147
- // 左侧固定列,如果在左边不可见区。则需要拿出来放在前面
148
- for (let i = 0; i < startIndex; i++) {
149
- const col = tableHeaderLastValue[i];
150
- if (col?.fixed === 'left') leftCols.push(col);
151
- }
152
- // 右侧固定列,如果在右边不可见区。则需要拿出来放在后面
153
- for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
154
- const col = tableHeaderLastValue[i];
155
- if (col?.fixed === 'right') rightCols.push(col);
156
- }
157
-
158
- const mainColumns = tableHeaderLastValue.slice(startIndex, endIndex);
159
-
160
- return leftCols.concat(mainColumns).concat(rightCols);
161
- }
162
- return tableHeaderLastValue;
163
- });
164
-
165
- const virtualX_offsetRight = computed(() => {
166
- if (!virtualX_on.value) return 0;
167
- let width = 0;
168
- const tableHeaderLastValue = tableHeaderLast.value;
169
- for (let i = virtualScrollX.value.endIndex; i < tableHeaderLastValue.length; i++) {
170
- const col = tableHeaderLastValue[i];
171
- if (col.fixed !== 'right') {
172
- width += getCalculatedColWidth(col);
173
- }
174
- }
175
- return width;
176
- });
177
-
178
- const getRowHeightFn = computed(() => {
179
- let rowHeightFn: (row?: PrivateRowDT) => number = () => props.rowHeight || DEFAULT_ROW_HEIGHT;
180
- if (props.autoRowHeight) {
181
- const tempRowHeightFn = rowHeightFn;
182
- rowHeightFn = (row?: PrivateRowDT) => getAutoRowHeight(row) || tempRowHeightFn(row);
183
- }
184
- if (hasExpandCol.value) {
185
- const expandedRowHeight = props.expandConfig?.height;
186
- const tempRowHeightFn = rowHeightFn;
187
- rowHeightFn = (row?: PrivateRowDT) => (row && row.__EXP_R__ && expandedRowHeight) || tempRowHeightFn(row);
188
- }
189
- return rowHeightFn;
190
- });
191
-
192
- /**
193
- * 初始化虚拟滚动参数
194
- * @param {number} [height] 虚拟滚动的高度
195
- */
196
- function initVirtualScroll(height?: number) {
197
- initVirtualScrollY(height);
198
- initVirtualScrollX();
199
- }
200
-
201
- /**
202
- * 初始化Y虚拟滚动参数
203
- * @param {number} [height] 虚拟滚动的高度
204
- */
205
- function initVirtualScrollY(height?: number) {
206
- if (height !== void 0 && typeof height !== 'number') {
207
- console.warn('initVirtualScrollY: height must be a number');
208
- height = 0;
209
- }
210
- const { clientHeight, scrollHeight } = tableContainerRef.value || {};
211
- let scrollTop = tableContainerRef.value?.scrollTop || 0;
212
-
213
- const rowHeight = getRowHeightFn.value();
214
- const containerHeight = height || clientHeight || DEFAULT_TABLE_HEIGHT;
215
- const { headless } = props;
216
- let pageSize = Math.ceil(containerHeight / rowHeight);
217
- if (!headless) {
218
- /** 表头高度占几行表体高度数 */
219
- const headerToBodyRowHeightCount = Math.floor(tableHeaderHeight.value / rowHeight);
220
- pageSize -= headerToBodyRowHeightCount; //减去表头行数
221
- }
222
- const maxScrollTop = dataSourceCopy.value.length * rowHeight + tableHeaderHeight.value - containerHeight;
223
- if (scrollTop > maxScrollTop) {
224
- /** fix: 滚动条不在顶部时,表格数据变少,导致滚动条位置有误 */
225
- scrollTop = maxScrollTop;
226
- }
227
- Object.assign(virtualScroll.value, { containerHeight, pageSize, scrollHeight });
228
- updateVirtualScrollY(scrollTop);
229
- }
230
-
231
- function initVirtualScrollX() {
232
- const { clientWidth, scrollLeft, scrollWidth } = tableContainerRef.value || {};
233
- virtualScrollX.value.containerWidth = clientWidth || DEFAULT_TABLE_WIDTH;
234
- virtualScrollX.value.scrollWidth = scrollWidth || DEFAULT_TABLE_WIDTH;
235
- updateVirtualScrollX(scrollLeft);
236
- }
237
-
238
- let vue2ScrollYTimeout: null | number = null;
239
-
240
- /** every row actual height */
241
- const autoRowHeightMap = new Map<string, number>();
242
- /** 如果行高度有变化,则要调用此方法清除保存的行高 */
243
- function setAutoHeight(rowKey: UniqKey, height?: number | null) {
244
- if (!height) {
245
- autoRowHeightMap.delete(String(rowKey));
246
- } else {
247
- autoRowHeightMap.set(String(rowKey), height);
248
- }
249
- }
250
-
251
- function clearAllAutoHeight() {
252
- autoRowHeightMap.clear();
253
- }
254
-
255
- function getAutoRowHeight(row?: PrivateRowDT) {
256
- if (!row) return;
257
- const rowKey = rowKeyGen(row);
258
- const storedHeight = autoRowHeightMap.get(String(rowKey));
259
- if (storedHeight) {
260
- return storedHeight;
261
- }
262
- const expectedHeight: AutoRowHeightConfig<PrivateRowDT>['expectedHeight'] = props.autoRowHeight?.expectedHeight;
263
- if (expectedHeight) {
264
- if (typeof expectedHeight === 'function') {
265
- return expectedHeight(row);
266
- } else {
267
- return expectedHeight;
268
- }
269
- }
270
- }
271
-
272
- /** 通过滚动条位置,计算虚拟滚动的参数 */
273
- function updateVirtualScrollY(sTop = 0) {
274
- const { pageSize, scrollTop, startIndex: oldStartIndex, endIndex: oldEndIndex, containerHeight } = virtualScroll.value;
275
- // 先更新滚动条位置记录,其他地方有依赖。(stripe 时ArrowUp/Down滚动依赖)
276
- virtualScroll.value.scrollTop = sTop;
277
-
278
- if (!virtual_on.value) {
279
- return;
280
- }
281
-
282
- const dataSourceCopyTemp = dataSourceCopy.value;
283
- const rowHeight = getRowHeightFn.value();
284
- const { autoRowHeight, stripe, optimizeVue2Scroll } = props;
285
- const dataLength = dataSourceCopyTemp.length;
286
-
287
- let startIndex = 0;
288
- let endIndex = dataLength;
289
- let autoRowHeightTop = 0;
290
-
291
- if (autoRowHeight || hasExpandCol.value) {
292
- if (autoRowHeight) {
293
- trRef.value?.forEach(tr => {
294
- const { rowKey } = tr.dataset;
295
- if (!rowKey || autoRowHeightMap.has(rowKey)) return;
296
- autoRowHeightMap.set(rowKey, tr.offsetHeight);
297
- });
298
- }
299
- // calculate startIndex
300
- for (let i = 0; i < dataLength; i++) {
301
- const height = getRowHeightFn.value(dataSourceCopyTemp[i]);
302
- autoRowHeightTop += height;
303
- if (autoRowHeightTop >= sTop) {
304
- startIndex = i;
305
- autoRowHeightTop -= height;
306
- break;
307
- }
308
- }
309
- // calculate endIndex
310
- let containerHeightSum = 0;
311
- for (let i = startIndex + 1; i < dataLength; i++) {
312
- containerHeightSum += getRowHeightFn.value(dataSourceCopyTemp[i]);
313
- if (containerHeightSum >= containerHeight) {
314
- endIndex = i;
315
- break;
316
- }
317
- }
318
- } else {
319
- startIndex = Math.floor(sTop / rowHeight);
320
- endIndex = startIndex + pageSize;
321
- }
322
-
323
- if (maxRowSpan.size) {
324
- // fix startIndex:查找是否有合并行跨越当前startIndex
325
- let correctedStartIndex = startIndex;
326
- let correctedEndIndex = endIndex;
327
-
328
- for (let i = 0; i < startIndex; i++) {
329
- const row = dataSourceCopyTemp[i];
330
- if (!row) continue;
331
- const spanEndIndex = i + (maxRowSpan.get(rowKeyGen(row)) || 1);
332
- if (spanEndIndex > startIndex) {
333
- // 找到跨越startIndex的合并行,将startIndex修正为合并行的起始索引
334
- correctedStartIndex = i;
335
- if (spanEndIndex > endIndex) {
336
- // 合并行跨越了整个可视区
337
- correctedEndIndex = spanEndIndex;
338
- }
339
- break;
340
- }
341
- }
342
-
343
- // fix endIndex:查找是否有合并行跨越当前endIndex
344
- for (let i = correctedStartIndex; i < endIndex; i++) {
345
- const row = dataSourceCopyTemp[i];
346
- if (!row) continue;
347
- const spanEndIndex = i + (maxRowSpan.get(rowKeyGen(row)) || 1);
348
- if (spanEndIndex > correctedEndIndex) {
349
- // 找到跨越endIndex的合并行,将endIndex修正为合并行的结束索引
350
- correctedEndIndex = Math.max(spanEndIndex, correctedEndIndex);
351
- }
352
- }
353
-
354
- startIndex = correctedStartIndex;
355
- endIndex = correctedEndIndex;
356
- }
357
-
358
- if (stripe && startIndex > 0 && startIndex % 2) {
359
- // 斑马纹情况下,每滚动偶数行才加载。防止斑马纹错位。
360
- startIndex -= 1; // 奇数-1变成偶数
361
- if (autoRowHeight || hasExpandCol.value) {
362
- const height = getRowHeightFn.value(dataSourceCopyTemp[startIndex]);
363
- autoRowHeightTop -= height;
364
- }
365
- }
366
-
367
- startIndex = Math.max(0, startIndex);
368
- endIndex = Math.min(endIndex, dataLength);
369
-
370
- if (startIndex >= endIndex) {
371
- // fallback
372
- startIndex = endIndex - pageSize;
373
- }
374
-
375
- if (vue2ScrollYTimeout) {
376
- window.clearTimeout(vue2ScrollYTimeout);
377
- }
378
-
379
- let offsetTop = 0;
380
- if (autoRowHeight || hasExpandCol.value) {
381
- offsetTop = autoRowHeightTop;
382
- } else {
383
- if (oldStartIndex === startIndex && oldEndIndex === endIndex) {
384
- // Not change: not update
385
- return;
386
- }
387
- offsetTop = startIndex * rowHeight;
388
- }
389
-
390
- /**
391
- * en: If scroll faster than one page, roll back
392
- */
393
- if (!optimizeVue2Scroll || sTop <= scrollTop || Math.abs(oldStartIndex - startIndex) >= pageSize) {
394
- // scroll up
395
- Object.assign(virtualScroll.value, { startIndex, endIndex, offsetTop });
396
- } else {
397
- // vue2 scroll down optimize
398
- virtualScroll.value.endIndex = endIndex;
399
- vue2ScrollYTimeout = window.setTimeout(() => {
400
- Object.assign(virtualScroll.value, { startIndex, offsetTop });
401
- }, VUE2_SCROLL_TIMEOUT_MS);
402
- }
403
- }
404
-
405
- let vue2ScrollXTimeout: null | number = null;
406
-
407
- /**
408
- * Calculate virtual scroll parameters based on horizontal scroll bar position
409
- */
410
- function updateVirtualScrollX(sLeft = 0) {
411
- if (!props.virtualX) return;
412
- const tableHeaderLastValue = tableHeaderLast.value;
413
- const headerLength = tableHeaderLastValue?.length;
414
- if (!headerLength) return;
415
-
416
- const { scrollLeft, containerWidth } = virtualScrollX.value;
417
- let startIndex = 0;
418
- let offsetLeft = 0;
419
- let colWidthSum = 0;
420
- /** 固定左侧列宽 */
421
- let leftColWidthSum = 0;
422
- /** 横向滚动时,第一列的剩余宽度 */
423
- let leftFirstColRestWidth = 0;
424
-
425
- for (let colIndex = 0; colIndex < headerLength; colIndex++) {
426
- const col = tableHeaderLastValue[colIndex];
427
- const colWidth = getCalculatedColWidth(col);
428
- startIndex++;
429
- // fixed left 不进入计算列宽
430
- if (col.fixed === 'left') {
431
- leftColWidthSum += colWidth;
432
- continue;
433
- }
434
- colWidthSum += colWidth;
435
- // 列宽(非固定列)加到超过scrollLeft的时候,表示startIndex从上一个开始下标
436
- if (colWidthSum >= sLeft) {
437
- offsetLeft = colWidthSum - colWidth;
438
- startIndex--;
439
- leftFirstColRestWidth = colWidthSum - sLeft;
440
- break;
441
- }
442
- }
443
- // -----
444
- colWidthSum = leftFirstColRestWidth;
445
- const containerW = containerWidth - leftColWidthSum;
446
- let endIndex = headerLength;
447
- for (let colIndex = startIndex + 1; colIndex < headerLength; colIndex++) {
448
- const col = tableHeaderLastValue[colIndex];
449
- colWidthSum += getCalculatedColWidth(col);
450
- // 列宽大于容器宽度则停止
451
- if (colWidthSum >= containerW) {
452
- endIndex = colIndex + 1; // slice endIndex + 1
453
- break;
454
- }
455
- }
456
-
457
- endIndex = Math.min(endIndex, headerLength);
458
-
459
- if (vue2ScrollXTimeout) {
460
- window.clearTimeout(vue2ScrollXTimeout);
461
- }
462
-
463
- // <= 等于是因为初始化时要赋值
464
- if (!props.optimizeVue2Scroll || sLeft <= scrollLeft) {
465
- // 向左滚动
466
- Object.assign(virtualScrollX.value, { startIndex, endIndex, offsetLeft, scrollLeft: sLeft });
467
- } else {
468
- //vue2 向右滚动 优化
469
- Object.assign(virtualScrollX.value, { endIndex, scrollLeft: sLeft });
470
- vue2ScrollXTimeout = window.setTimeout(() => {
471
- Object.assign(virtualScrollX.value, { startIndex, offsetLeft });
472
- }, VUE2_SCROLL_TIMEOUT_MS);
473
- }
474
- }
475
-
476
- return {
477
- virtualScroll,
478
- virtualScrollX,
479
- virtual_on,
480
- virtual_dataSourcePart,
481
- virtual_offsetBottom,
482
- virtualX_on,
483
- virtualX_columnPart,
484
- virtualX_offsetRight,
485
- tableHeaderHeight,
486
- initVirtualScroll,
487
- initVirtualScrollY,
488
- initVirtualScrollX,
489
- updateVirtualScrollY,
490
- updateVirtualScrollX,
491
- setAutoHeight,
492
- clearAllAutoHeight,
493
- };
494
- }
1
+ import { Ref, ShallowRef, computed, ref } from 'vue';
2
+ import { DEFAULT_ROW_HEIGHT, DEFAULT_TABLE_HEIGHT, DEFAULT_TABLE_WIDTH } from './const';
3
+ import { AutoRowHeightConfig, PrivateRowDT, PrivateStkTableColumn, RowKeyGen, StkTableColumn, UniqKey } from './types';
4
+ import { getCalculatedColWidth } from './utils/constRefUtils';
5
+
6
+ type Option<DT extends Record<string, any>> = {
7
+ props: any;
8
+ tableContainerRef: Ref<HTMLElement | undefined>;
9
+ trRef: Ref<HTMLTableRowElement[] | undefined>;
10
+ dataSourceCopy: ShallowRef<PrivateRowDT[]>;
11
+ tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>;
12
+ tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>;
13
+ rowKeyGen: RowKeyGen;
14
+ maxRowSpan: Map<UniqKey, number>;
15
+ };
16
+
17
+ /** 暂存纵向虚拟滚动的数据 */
18
+ export type VirtualScrollStore = {
19
+ /** 容器高度 */
20
+ containerHeight: number;
21
+ /** 一页的大小 */
22
+ pageSize: number;
23
+ /** 数组开始位置 */
24
+ startIndex: number;
25
+ /** 数组结束位置 */
26
+ endIndex: number;
27
+ /** 行高 */
28
+ rowHeight: number;
29
+ /** 表格定位上边距 */
30
+ offsetTop: number;
31
+ /** 纵向滚动条位置,用于判断是横向滚动还是纵向 */
32
+ scrollTop: number;
33
+ /** 总滚动高度 */
34
+ scrollHeight: number;
35
+ };
36
+ /** 暂存横向虚拟滚动的数据 */
37
+ export type VirtualScrollXStore = {
38
+ /** 父容器宽度 */
39
+ containerWidth: number;
40
+ /** 滚动容器的宽度 */
41
+ scrollWidth: number;
42
+ /** 开始位置 */
43
+ startIndex: number;
44
+ /** 结束始位置 */
45
+ endIndex: number;
46
+ /** 表格定位左边距 */
47
+ offsetLeft: number;
48
+ /** 横向滚动位置,用于判断是横向滚动还是纵向 */
49
+ scrollLeft: number;
50
+ };
51
+
52
+ /** vue2 优化滚动回收延时 */
53
+ const VUE2_SCROLL_TIMEOUT_MS = 200;
54
+
55
+ /**
56
+ * virtual scroll
57
+ * @param param0
58
+ * @returns
59
+ */
60
+ export function useVirtualScroll<DT extends Record<string, any>>({
61
+ props,
62
+ tableContainerRef,
63
+ trRef,
64
+ dataSourceCopy,
65
+ tableHeaderLast,
66
+ tableHeaders,
67
+ rowKeyGen,
68
+ maxRowSpan,
69
+ }: Option<DT>) {
70
+ const tableHeaderHeight = computed(() => props.headerRowHeight * tableHeaders.value.length);
71
+
72
+
73
+ const virtualScroll = ref<VirtualScrollStore>({
74
+ containerHeight: 0,
75
+ rowHeight: props.rowHeight,
76
+ pageSize: 0,
77
+ startIndex: 0,
78
+ endIndex: 0,
79
+ offsetTop: 0,
80
+ scrollTop: 0,
81
+ scrollHeight: 0,
82
+ });
83
+
84
+ // TODO: init pageSize
85
+
86
+ const virtualScrollX = ref<VirtualScrollXStore>({
87
+ containerWidth: 0,
88
+ scrollWidth: 0,
89
+ startIndex: 0,
90
+ endIndex: 0,
91
+ offsetLeft: 0,
92
+ scrollLeft: 0,
93
+ });
94
+
95
+ const hasExpandCol = computed(() => {
96
+ return tableHeaderLast.value.some(col => col.type === 'expand');
97
+ });
98
+
99
+ /** 是否虚拟滚动标志 */
100
+ const virtual_on = computed(() => {
101
+ return props.virtual && dataSourceCopy.value.length > virtualScroll.value.pageSize;
102
+ });
103
+
104
+ const virtual_dataSourcePart = computed(() => {
105
+ if (!virtual_on.value) return dataSourceCopy.value;
106
+ const { startIndex, endIndex } = virtualScroll.value;
107
+ return dataSourceCopy.value.slice(startIndex, endIndex + 1);
108
+ });
109
+
110
+ const virtual_offsetBottom = computed(() => {
111
+ if (!virtual_on.value) return 0;
112
+ const { startIndex, endIndex } = virtualScroll.value;
113
+ const dataSourceCopyValue = dataSourceCopy.value;
114
+ const rowHeight = getRowHeightFn.value();
115
+ if (props.autoRowHeight) {
116
+ let offsetBottom = 0;
117
+ for (let i = endIndex + 1; i < dataSourceCopyValue.length; i++) {
118
+ const rowHeight = getRowHeightFn.value(dataSourceCopyValue[i]);
119
+ offsetBottom += rowHeight;
120
+ }
121
+ return offsetBottom;
122
+ }
123
+
124
+ return (dataSourceCopyValue.length - startIndex - virtual_dataSourcePart.value.length) * rowHeight;
125
+ });
126
+
127
+ const virtualX_on = computed(() => {
128
+ return (
129
+ props.virtualX &&
130
+ tableHeaderLast.value.reduce((sum, col) => (sum += getCalculatedColWidth(col)), 0) > virtualScrollX.value.containerWidth + 100
131
+ );
132
+ });
133
+
134
+ const virtualX_columnPart = computed(() => {
135
+ const tableHeaderLastValue = tableHeaderLast.value;
136
+ if (virtualX_on.value) {
137
+ // 虚拟横向滚动,固定列要一直保持存在
138
+ const leftCols: PrivateStkTableColumn<PrivateRowDT>[] = [];
139
+ const rightCols: PrivateStkTableColumn<PrivateRowDT>[] = [];
140
+ /**
141
+ * 存在问题:
142
+ * table columns 从多到少时。比方原来的start=5,end=10,现在start=4,end=8。这时候endIndex就超出数组范围了。
143
+ * FIXME: 如果新列数 < endIndex,此时需要重新计算列startIndex和endIndex。
144
+ */
145
+ const { startIndex, endIndex } = virtualScrollX.value;
146
+
147
+ // 左侧固定列,如果在左边不可见区。则需要拿出来放在前面
148
+ for (let i = 0; i < startIndex; i++) {
149
+ const col = tableHeaderLastValue[i];
150
+ if (col?.fixed === 'left') leftCols.push(col);
151
+ }
152
+ // 右侧固定列,如果在右边不可见区。则需要拿出来放在后面
153
+ for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
154
+ const col = tableHeaderLastValue[i];
155
+ if (col?.fixed === 'right') rightCols.push(col);
156
+ }
157
+
158
+ const mainColumns = tableHeaderLastValue.slice(startIndex, endIndex);
159
+
160
+ return leftCols.concat(mainColumns).concat(rightCols);
161
+ }
162
+ return tableHeaderLastValue;
163
+ });
164
+
165
+ const virtualX_offsetRight = computed(() => {
166
+ if (!virtualX_on.value) return 0;
167
+ let width = 0;
168
+ const tableHeaderLastValue = tableHeaderLast.value;
169
+ for (let i = virtualScrollX.value.endIndex; i < tableHeaderLastValue.length; i++) {
170
+ const col = tableHeaderLastValue[i];
171
+ if (col.fixed !== 'right') {
172
+ width += getCalculatedColWidth(col);
173
+ }
174
+ }
175
+ return width;
176
+ });
177
+
178
+ const getRowHeightFn = computed(() => {
179
+ let rowHeightFn: (row?: PrivateRowDT) => number = () => props.rowHeight || DEFAULT_ROW_HEIGHT;
180
+ if (props.autoRowHeight) {
181
+ const tempRowHeightFn = rowHeightFn;
182
+ rowHeightFn = (row?: PrivateRowDT) => getAutoRowHeight(row) || tempRowHeightFn(row);
183
+ }
184
+ if (hasExpandCol.value) {
185
+ const expandedRowHeight = props.expandConfig?.height;
186
+ const tempRowHeightFn = rowHeightFn;
187
+ rowHeightFn = (row?: PrivateRowDT) => (row && row.__EXP_R__ && expandedRowHeight) || tempRowHeightFn(row);
188
+ }
189
+ return rowHeightFn;
190
+ });
191
+
192
+ /**
193
+ * 初始化虚拟滚动参数
194
+ * @param {number} [height] 虚拟滚动的高度
195
+ */
196
+ function initVirtualScroll(height?: number) {
197
+ initVirtualScrollY(height);
198
+ initVirtualScrollX();
199
+ }
200
+
201
+ /**
202
+ * 初始化Y虚拟滚动参数
203
+ * @param {number} [height] 虚拟滚动的高度
204
+ */
205
+ function initVirtualScrollY(height?: number) {
206
+ if (height !== void 0 && typeof height !== 'number') {
207
+ console.warn('initVirtualScrollY: height must be a number');
208
+ height = 0;
209
+ }
210
+ const { clientHeight, scrollHeight } = tableContainerRef.value || {};
211
+ let scrollTop = tableContainerRef.value?.scrollTop || 0;
212
+
213
+ const rowHeight = getRowHeightFn.value();
214
+ const containerHeight = height || clientHeight || DEFAULT_TABLE_HEIGHT;
215
+ const { headless } = props;
216
+ let pageSize = Math.ceil(containerHeight / rowHeight);
217
+ if (!headless) {
218
+ /** 表头高度占几行表体高度数 */
219
+ const headerToBodyRowHeightCount = Math.floor(tableHeaderHeight.value / rowHeight);
220
+ pageSize -= headerToBodyRowHeightCount; //减去表头行数
221
+ }
222
+ const maxScrollTop = dataSourceCopy.value.length * rowHeight + tableHeaderHeight.value - containerHeight;
223
+ if (scrollTop > maxScrollTop) {
224
+ /** fix: 滚动条不在顶部时,表格数据变少,导致滚动条位置有误 */
225
+ scrollTop = maxScrollTop;
226
+ }
227
+ Object.assign(virtualScroll.value, { containerHeight, pageSize, scrollHeight });
228
+ updateVirtualScrollY(scrollTop);
229
+ }
230
+
231
+ function initVirtualScrollX() {
232
+ const { clientWidth, scrollLeft, scrollWidth } = tableContainerRef.value || {};
233
+ virtualScrollX.value.containerWidth = clientWidth || DEFAULT_TABLE_WIDTH;
234
+ virtualScrollX.value.scrollWidth = scrollWidth || DEFAULT_TABLE_WIDTH;
235
+ updateVirtualScrollX(scrollLeft);
236
+ }
237
+
238
+ let vue2ScrollYTimeout: null | number = null;
239
+
240
+ /** every row actual height */
241
+ const autoRowHeightMap = new Map<string, number>();
242
+ /** 如果行高度有变化,则要调用此方法清除保存的行高 */
243
+ function setAutoHeight(rowKey: UniqKey, height?: number | null) {
244
+ if (!height) {
245
+ autoRowHeightMap.delete(String(rowKey));
246
+ } else {
247
+ autoRowHeightMap.set(String(rowKey), height);
248
+ }
249
+ }
250
+
251
+ function clearAllAutoHeight() {
252
+ autoRowHeightMap.clear();
253
+ }
254
+
255
+ function getAutoRowHeight(row?: PrivateRowDT) {
256
+ if (!row) return;
257
+ const rowKey = rowKeyGen(row);
258
+ const storedHeight = autoRowHeightMap.get(String(rowKey));
259
+ if (storedHeight) {
260
+ return storedHeight;
261
+ }
262
+ const expectedHeight: AutoRowHeightConfig<PrivateRowDT>['expectedHeight'] = props.autoRowHeight?.expectedHeight;
263
+ if (expectedHeight) {
264
+ if (typeof expectedHeight === 'function') {
265
+ return expectedHeight(row);
266
+ } else {
267
+ return expectedHeight;
268
+ }
269
+ }
270
+ }
271
+
272
+ /** 通过滚动条位置,计算虚拟滚动的参数 */
273
+ function updateVirtualScrollY(sTop = 0) {
274
+ const { pageSize, scrollTop, startIndex: oldStartIndex, endIndex: oldEndIndex, containerHeight } = virtualScroll.value;
275
+ // 先更新滚动条位置记录,其他地方有依赖。(stripe 时ArrowUp/Down滚动依赖)
276
+ virtualScroll.value.scrollTop = sTop;
277
+
278
+ if (!virtual_on.value) {
279
+ return;
280
+ }
281
+
282
+ const dataSourceCopyTemp = dataSourceCopy.value;
283
+ const rowHeight = getRowHeightFn.value();
284
+ const { autoRowHeight, stripe, optimizeVue2Scroll } = props;
285
+ const dataLength = dataSourceCopyTemp.length;
286
+
287
+ let startIndex = 0;
288
+ let endIndex = dataLength;
289
+ let autoRowHeightTop = 0;
290
+
291
+ if (autoRowHeight || hasExpandCol.value) {
292
+ if (autoRowHeight) {
293
+ trRef.value?.forEach(tr => {
294
+ const { rowKey } = tr.dataset;
295
+ if (!rowKey || autoRowHeightMap.has(rowKey)) return;
296
+ autoRowHeightMap.set(rowKey, tr.offsetHeight);
297
+ });
298
+ }
299
+ // calculate startIndex
300
+ for (let i = 0; i < dataLength; i++) {
301
+ const height = getRowHeightFn.value(dataSourceCopyTemp[i]);
302
+ autoRowHeightTop += height;
303
+ if (autoRowHeightTop >= sTop) {
304
+ startIndex = i;
305
+ autoRowHeightTop -= height;
306
+ break;
307
+ }
308
+ }
309
+ // calculate endIndex
310
+ let containerHeightSum = 0;
311
+ for (let i = startIndex + 1; i < dataLength; i++) {
312
+ containerHeightSum += getRowHeightFn.value(dataSourceCopyTemp[i]);
313
+ if (containerHeightSum >= containerHeight) {
314
+ endIndex = i;
315
+ break;
316
+ }
317
+ }
318
+ } else {
319
+ startIndex = Math.floor(sTop / rowHeight);
320
+ endIndex = startIndex + pageSize;
321
+ }
322
+
323
+ if (maxRowSpan.size) {
324
+ // fix startIndex:查找是否有合并行跨越当前startIndex
325
+ let correctedStartIndex = startIndex;
326
+ let correctedEndIndex = endIndex;
327
+
328
+ for (let i = 0; i < startIndex; i++) {
329
+ const row = dataSourceCopyTemp[i];
330
+ if (!row) continue;
331
+ const spanEndIndex = i + (maxRowSpan.get(rowKeyGen(row)) || 1);
332
+ if (spanEndIndex > startIndex) {
333
+ // 找到跨越startIndex的合并行,将startIndex修正为合并行的起始索引
334
+ correctedStartIndex = i;
335
+ if (spanEndIndex > endIndex) {
336
+ // 合并行跨越了整个可视区
337
+ correctedEndIndex = spanEndIndex;
338
+ }
339
+ break;
340
+ }
341
+ }
342
+
343
+ // fix endIndex:查找是否有合并行跨越当前endIndex
344
+ for (let i = correctedStartIndex; i < endIndex; i++) {
345
+ const row = dataSourceCopyTemp[i];
346
+ if (!row) continue;
347
+ const spanEndIndex = i + (maxRowSpan.get(rowKeyGen(row)) || 1);
348
+ if (spanEndIndex > correctedEndIndex) {
349
+ // 找到跨越endIndex的合并行,将endIndex修正为合并行的结束索引
350
+ correctedEndIndex = Math.max(spanEndIndex, correctedEndIndex);
351
+ }
352
+ }
353
+
354
+ startIndex = correctedStartIndex;
355
+ endIndex = correctedEndIndex;
356
+ }
357
+
358
+ if (stripe && startIndex > 0 && startIndex % 2) {
359
+ // 斑马纹情况下,每滚动偶数行才加载。防止斑马纹错位。
360
+ startIndex -= 1; // 奇数-1变成偶数
361
+ if (autoRowHeight || hasExpandCol.value) {
362
+ const height = getRowHeightFn.value(dataSourceCopyTemp[startIndex]);
363
+ autoRowHeightTop -= height;
364
+ }
365
+ }
366
+
367
+ startIndex = Math.max(0, startIndex);
368
+ endIndex = Math.min(endIndex, dataLength);
369
+
370
+ if (startIndex >= endIndex) {
371
+ // fallback
372
+ startIndex = endIndex - pageSize;
373
+ }
374
+
375
+ if (vue2ScrollYTimeout) {
376
+ window.clearTimeout(vue2ScrollYTimeout);
377
+ }
378
+
379
+ let offsetTop = 0;
380
+ if (autoRowHeight || hasExpandCol.value) {
381
+ offsetTop = autoRowHeightTop;
382
+ } else {
383
+ if (oldStartIndex === startIndex && oldEndIndex === endIndex) {
384
+ // Not change: not update
385
+ return;
386
+ }
387
+ offsetTop = startIndex * rowHeight;
388
+ }
389
+
390
+ /**
391
+ * en: If scroll faster than one page, roll back
392
+ */
393
+ if (!optimizeVue2Scroll || sTop <= scrollTop || Math.abs(oldStartIndex - startIndex) >= pageSize) {
394
+ // scroll up
395
+ Object.assign(virtualScroll.value, { startIndex, endIndex, offsetTop });
396
+ } else {
397
+ // vue2 scroll down optimize
398
+ virtualScroll.value.endIndex = endIndex;
399
+ vue2ScrollYTimeout = window.setTimeout(() => {
400
+ Object.assign(virtualScroll.value, { startIndex, offsetTop });
401
+ }, VUE2_SCROLL_TIMEOUT_MS);
402
+ }
403
+ }
404
+
405
+ let vue2ScrollXTimeout: null | number = null;
406
+
407
+ /**
408
+ * Calculate virtual scroll parameters based on horizontal scroll bar position
409
+ */
410
+ function updateVirtualScrollX(sLeft = 0) {
411
+ if (!props.virtualX) return;
412
+ const tableHeaderLastValue = tableHeaderLast.value;
413
+ const headerLength = tableHeaderLastValue?.length;
414
+ if (!headerLength) return;
415
+
416
+ const { scrollLeft, containerWidth } = virtualScrollX.value;
417
+ let startIndex = 0;
418
+ let offsetLeft = 0;
419
+ let colWidthSum = 0;
420
+ /** 固定左侧列宽 */
421
+ let leftColWidthSum = 0;
422
+ /** 横向滚动时,第一列的剩余宽度 */
423
+ let leftFirstColRestWidth = 0;
424
+
425
+ for (let colIndex = 0; colIndex < headerLength; colIndex++) {
426
+ const col = tableHeaderLastValue[colIndex];
427
+ const colWidth = getCalculatedColWidth(col);
428
+ startIndex++;
429
+ // fixed left 不进入计算列宽
430
+ if (col.fixed === 'left') {
431
+ leftColWidthSum += colWidth;
432
+ continue;
433
+ }
434
+ colWidthSum += colWidth;
435
+ // 列宽(非固定列)加到超过scrollLeft的时候,表示startIndex从上一个开始下标
436
+ if (colWidthSum >= sLeft) {
437
+ offsetLeft = colWidthSum - colWidth;
438
+ startIndex--;
439
+ leftFirstColRestWidth = colWidthSum - sLeft;
440
+ break;
441
+ }
442
+ }
443
+ // -----
444
+ colWidthSum = leftFirstColRestWidth;
445
+ const containerW = containerWidth - leftColWidthSum;
446
+ let endIndex = headerLength;
447
+ for (let colIndex = startIndex + 1; colIndex < headerLength; colIndex++) {
448
+ const col = tableHeaderLastValue[colIndex];
449
+ colWidthSum += getCalculatedColWidth(col);
450
+ // 列宽大于容器宽度则停止
451
+ if (colWidthSum >= containerW) {
452
+ endIndex = colIndex + 1; // slice endIndex + 1
453
+ break;
454
+ }
455
+ }
456
+
457
+ endIndex = Math.min(endIndex, headerLength);
458
+
459
+ if (vue2ScrollXTimeout) {
460
+ window.clearTimeout(vue2ScrollXTimeout);
461
+ }
462
+
463
+ // <= 等于是因为初始化时要赋值
464
+ if (!props.optimizeVue2Scroll || sLeft <= scrollLeft) {
465
+ // 向左滚动
466
+ Object.assign(virtualScrollX.value, { startIndex, endIndex, offsetLeft, scrollLeft: sLeft });
467
+ } else {
468
+ //vue2 向右滚动 优化
469
+ Object.assign(virtualScrollX.value, { endIndex, scrollLeft: sLeft });
470
+ vue2ScrollXTimeout = window.setTimeout(() => {
471
+ Object.assign(virtualScrollX.value, { startIndex, offsetLeft });
472
+ }, VUE2_SCROLL_TIMEOUT_MS);
473
+ }
474
+ }
475
+
476
+ return {
477
+ virtualScroll,
478
+ virtualScrollX,
479
+ virtual_on,
480
+ virtual_dataSourcePart,
481
+ virtual_offsetBottom,
482
+ virtualX_on,
483
+ virtualX_columnPart,
484
+ virtualX_offsetRight,
485
+ tableHeaderHeight,
486
+ initVirtualScroll,
487
+ initVirtualScrollY,
488
+ initVirtualScrollX,
489
+ updateVirtualScrollY,
490
+ updateVirtualScrollX,
491
+ setAutoHeight,
492
+ clearAllAutoHeight,
493
+ };
494
+ }