stk-table-vue 1.1.0 → 1.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ Smooth performance with tens of thousands of rows. Designed for real-time data d
25
25
  - Cell highlighting with Animation API
26
26
  - Built-in custom cells: Filter, Editable, Checkbox, Number, Change
27
27
  - Column resize, header drag, row drag
28
- - Merge cells (virtual mode supported)
28
+ - Merge cells (virtual mode, huge rowspan / colspan supported)
29
29
  - Custom scrollbar
30
30
  - Sortable & filterable
31
31
  - Theme support (dark / light)
@@ -182,7 +182,12 @@ const dataSource = [
182
182
  ```
183
183
 
184
184
  ## Compare
185
- Compare performance with other vue table [vue-table-compare](https://github.com/ja-plus/vue-table-compare)
185
+ Compare performance with other vue table [vue-virtual-table-benchmark](https://github.com/ja-plus/vue-virtual-table-benchmark)
186
+
187
+ [Benchmark Result Page](https://ja-plus.github.io/vue-virtual-table-benchmark/perf-report.html)
188
+
189
+ ## Changelog
190
+ [CHANGELOG](CHANGELOG.md)
186
191
 
187
192
 
188
193
  ## License
@@ -0,0 +1,166 @@
1
+ /**
2
+ * name: stk-table-vue
3
+ * version: v1.2.0-beta.2
4
+ * description: High performance realtime virtual table for vue3 and vue2.7
5
+ * author: japlus
6
+ * homepage: https://ja-plus.github.io/stk-table-vue/
7
+ * license: MIT
8
+ */
9
+ import { defineComponent, ref, reactive, h, onMounted, onUnmounted, createElementBlock, openBlock, withModifiers, normalizeStyle, normalizeClass, createVNode, createElementVNode, nextTick } from "vue";
10
+ import { StkTable as _sfc_main$1 } from "./stk-table-vue.js";
11
+ const DROPDOWN_DEFAULT_WIDTH = 300;
12
+ const DROPDOWN_DEFAULT_HEIGHT = 400;
13
+ const PADDING = 6;
14
+ const _sfc_main = /* @__PURE__ */ defineComponent({
15
+ __name: "index",
16
+ setup(__props, { expose: __expose }) {
17
+ const theme = ref("light");
18
+ const checkedTempValueSet = reactive(/* @__PURE__ */ new Set());
19
+ const columns = ref([
20
+ {
21
+ title: "",
22
+ dataIndex: "value",
23
+ width: 30,
24
+ className: "stk-filter-dropdown-checkbox",
25
+ customCell: ({ row }) => h("input", {
26
+ type: "checkbox",
27
+ checked: checkedTempValueSet.has(row.value)
28
+ })
29
+ },
30
+ { title: "", dataIndex: "label" }
31
+ ]);
32
+ const visible = ref(false);
33
+ const position = ref({ x: 0, y: 0 });
34
+ const options = ref([]);
35
+ const dropdownEl = ref();
36
+ onMounted(() => {
37
+ document.addEventListener("click", handleClickOutside);
38
+ });
39
+ onUnmounted(() => {
40
+ document.removeEventListener("click", handleClickOutside);
41
+ });
42
+ let onConfirmFn;
43
+ function getDropdownSize() {
44
+ if (!dropdownEl.value) {
45
+ return [DROPDOWN_DEFAULT_WIDTH, DROPDOWN_DEFAULT_HEIGHT];
46
+ }
47
+ const rect = dropdownEl.value.getBoundingClientRect();
48
+ return [rect.width || DROPDOWN_DEFAULT_WIDTH, rect.height || DROPDOWN_DEFAULT_HEIGHT];
49
+ }
50
+ function calculatePosition(docPos) {
51
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
52
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
53
+ const viewportWidth = document.documentElement.clientWidth;
54
+ const viewportHeight = document.documentElement.clientHeight;
55
+ const [dropdownWidth, dropdownHeight] = getDropdownSize();
56
+ let finalX = docPos.x;
57
+ let finalY = docPos.y;
58
+ const relativeX = docPos.x - scrollLeft;
59
+ if (relativeX + dropdownWidth > viewportWidth - PADDING) {
60
+ finalX = viewportWidth - dropdownWidth - PADDING + scrollLeft;
61
+ }
62
+ const relativeY = docPos.y - scrollTop;
63
+ if (relativeY + dropdownHeight > viewportHeight - PADDING) {
64
+ const triggerHeight = docPos.height || 30;
65
+ if (relativeY - triggerHeight >= dropdownHeight + PADDING) {
66
+ finalY = docPos.y - triggerHeight - dropdownHeight - PADDING;
67
+ } else {
68
+ finalY = PADDING + scrollTop;
69
+ }
70
+ }
71
+ finalX = Math.max(PADDING + scrollLeft, finalX);
72
+ finalY = Math.max(PADDING + scrollTop, finalY);
73
+ return { x: finalX, y: finalY };
74
+ }
75
+ async function show(pos, opt, onConfirm) {
76
+ if (dropdownEl.value) {
77
+ dropdownEl.value.style.visibility = "hidden";
78
+ }
79
+ visible.value = true;
80
+ options.value = opt || [];
81
+ initChecked();
82
+ onConfirmFn = onConfirm;
83
+ await nextTick();
84
+ position.value = calculatePosition(pos);
85
+ if (dropdownEl.value) {
86
+ dropdownEl.value.style.visibility = "visible";
87
+ }
88
+ }
89
+ async function handleClickOutside(e) {
90
+ var _a;
91
+ if (!visible.value || ((_a = dropdownEl.value) == null ? void 0 : _a.contains(e.target))) return;
92
+ hide();
93
+ }
94
+ function initChecked() {
95
+ options.value.forEach((opt) => {
96
+ if (opt.selected) {
97
+ checkedTempValueSet.add(opt.value);
98
+ }
99
+ });
100
+ }
101
+ function updateChecked(checked, row) {
102
+ if (checked) {
103
+ checkedTempValueSet.add(row.value);
104
+ } else {
105
+ checkedTempValueSet.delete(row.value);
106
+ }
107
+ }
108
+ function confirm() {
109
+ options.value.forEach((opt) => opt.selected = checkedTempValueSet.has(opt.value));
110
+ onConfirmFn(Array.from(checkedTempValueSet));
111
+ hide();
112
+ }
113
+ function hide() {
114
+ visible.value = false;
115
+ options.value = [];
116
+ checkedTempValueSet.clear();
117
+ }
118
+ function handleRowClick(e, row) {
119
+ const selected = checkedTempValueSet.has(row.value);
120
+ updateChecked(!selected, row);
121
+ }
122
+ function setTheme(t) {
123
+ theme.value = t;
124
+ }
125
+ function handleClear() {
126
+ checkedTempValueSet.clear();
127
+ confirm();
128
+ }
129
+ __expose({ visible, show, hide, setTheme });
130
+ return (_ctx, _cache) => {
131
+ return openBlock(), createElementBlock("div", {
132
+ ref_key: "dropdownEl",
133
+ ref: dropdownEl,
134
+ class: normalizeClass(["stk-filter-dropdown", [`stk-filter-dropdown--${theme.value}`]]),
135
+ style: normalizeStyle({
136
+ top: position.value.y + "px",
137
+ left: position.value.x + "px",
138
+ display: visible.value ? void 0 : "none"
139
+ }),
140
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => {
141
+ }, ["stop"]))
142
+ }, [
143
+ createVNode(_sfc_main$1, {
144
+ "row-key": "id",
145
+ headless: "",
146
+ virtual: "",
147
+ "no-data-full": "",
148
+ theme: theme.value,
149
+ "row-active": false,
150
+ "row-height": 20,
151
+ bordered: false,
152
+ columns: columns.value,
153
+ "data-source": options.value,
154
+ onRowClick: handleRowClick
155
+ }, null, 8, ["theme", "columns", "data-source"]),
156
+ createElementVNode("footer", null, [
157
+ createElementVNode("button", { onClick: handleClear }, "↺"),
158
+ createElementVNode("button", { onClick: confirm }, "✓")
159
+ ])
160
+ ], 6);
161
+ };
162
+ }
163
+ });
164
+ export {
165
+ _sfc_main as default
166
+ };
@@ -242,6 +242,13 @@ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<im
242
242
  * @see {@link initVirtualScrollY}
243
243
  */
244
244
  initVirtualScrollY: (height?: number) => void;
245
+ /**
246
+ * 清空 mergeCells 结果缓存并强制重算合并结果
247
+ *
248
+ * en: Clear mergeCells result cache and force recomputation
249
+ * @see {@link clearMergeCellsCache}
250
+ */
251
+ clearMergeCellsCache: () => void;
245
252
  /**
246
253
  * 选中一行
247
254
  *
@@ -757,13 +764,13 @@ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<im
757
764
  maxWidth: string;
758
765
  sortConfig: SortConfig<any>;
759
766
  colKey: UniqKeyProp;
760
- rowHeight: number;
761
767
  headless: boolean;
762
768
  autoRowHeight: boolean | AutoRowHeightConfig<any>;
763
769
  stripe: boolean;
764
770
  optimizeVue2Scroll: boolean;
765
771
  rowKey: UniqKeyProp;
766
772
  headerRowHeight: number | string;
773
+ rowHeight: number;
767
774
  scrollbar: boolean | ScrollbarOptions;
768
775
  fixedMode: boolean;
769
776
  theme: "light" | "dark";
@@ -814,7 +821,7 @@ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<im
814
821
  }): any;
815
822
  expand?(_: {
816
823
  row: any;
817
- col: StkTableColumn<any> | undefined;
824
+ col: any;
818
825
  }): any;
819
826
  empty?(_: {}): any;
820
827
  customBottom?(_: {}): any;
@@ -0,0 +1,26 @@
1
+ import { MergeCellsParam } from './types';
2
+
3
+ /** mergeCells 计算结果 */
4
+ export type MergeCellsResult = {
5
+ colspan: number;
6
+ rowspan: number;
7
+ };
8
+ /**
9
+ * mergeCells 结果缓存工厂。
10
+ *
11
+ * - 缓存键为「绝对行索引 + 叶子列索引」,所有 mergeCells 调用处
12
+ * (hiddenCellMap 构建、virtual-x 可视列范围修正、视口上方占位计算、单元格渲染)共享,
13
+ * 同一单元格只调用一次用户回调;
14
+ * - 缓存可跨虚拟滚动帧复用(键为绝对索引,不随窗口滑动变化),
15
+ * 稳态滚动时仅新进入窗口的行会触发回调,只需在数据/列真正变化时 clear;
16
+ * - 条目额外保存行引用并在命中时校验同一性:即使数据被重排/替换(如排序后同一批行对象
17
+ * 顺序变化),也不会取到错行的旧结果,缓存清理时机不再影响正确性。
18
+ * - 注意:引用同一性校验无法感知行字段被原地修改(行引用不变)。
19
+ * 业务原地改行且 mergeCells 结果依赖该字段时,需调用 clear() 显式失效
20
+ * (组件已暴露 clearMergeCellsCache 实例方法)。
21
+ */
22
+ export declare function createMergeCellsCache(): {
23
+ getMergeCellsResult: (row: MergeCellsParam<any>["row"], col: MergeCellsParam<any>["col"], rowIndex: number, colIndex: number) => MergeCellsResult;
24
+ clear: () => void;
25
+ };
26
+ export type MergeCellsCache = ReturnType<typeof createMergeCellsCache>;
@@ -165,6 +165,12 @@ export type PrivateStkTableColumn<T extends Record<string, any>> = StkTableColum
165
165
  * @private
166
166
  */
167
167
  __VT_C_SP__?: number;
168
+ /**
169
+ * Above-viewport placeholder colspan. Marks a placeholder td for rows
170
+ * above the viewport that only need structural colspan, not real content.
171
+ * @private
172
+ */
173
+ __VT_PH__?: number;
168
174
  };
169
175
  /** private row keys */
170
176
  export type PrivateRowDT = {
@@ -1,4 +1,4 @@
1
1
  import { ShallowRef } from 'vue';
2
2
  import { PrivateStkTableColumn, RowKeyGen, UniqKey } from './types';
3
3
 
4
- export declare function useMaxRowSpan(props: any, tableHeaderLast: ShallowRef<PrivateStkTableColumn<any>[]>, rowKeyGen: RowKeyGen, dataSourceCopy: ShallowRef<any[]>): readonly [Map<UniqKey, number>, () => void];
4
+ export declare function useMaxRowSpan(props: any, tableHeaderLast: ShallowRef<PrivateStkTableColumn<any>[]>, rowKeyGen: RowKeyGen, dataSourceCopy: ShallowRef<any[]>): readonly [Map<UniqKey, number>, () => void, () => number];
@@ -1,7 +1,29 @@
1
- import { Ref, ShallowRef } from 'vue';
2
- import { ColKeyGen, MergeCellsParam, PrivateStkTableColumn, RowActiveOption, RowKeyGen, UniqKey } from './types';
1
+ import { ComputedRef, Ref, ShallowRef } from 'vue';
2
+ import { MergeCellsCache } from './mergeCellsCache';
3
+ import { ColKeyGen, MergeCellsParam, PrivateRowDT, PrivateStkTableColumn, RowActiveOption, RowKeyGen, UniqKey } from './types';
4
+ import { VirtualScrollStore } from './useVirtualScroll';
3
5
 
4
- export declare function useMergeCells(rowActiveProp: Ref<RowActiveOption<any>>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<any>[]>, rowKeyGen: RowKeyGen, colKeyGen: ColKeyGen, virtual_dataSourcePart: ShallowRef<any[]>): readonly [Ref<Record<UniqKey, Set<UniqKey>> | null, Record<UniqKey, Set<UniqKey>> | null>, (row: MergeCellsParam<any>["row"], col: MergeCellsParam<any>["col"], rowIndex: MergeCellsParam<any>["rowIndex"], colIndex: MergeCellsParam<any>["colIndex"]) => {
6
+ /** Above-viewport cell descriptor: real column or placeholder colspan */
7
+ type AboveViewportCell = PrivateStkTableColumn<PrivateRowDT> | {
8
+ __VT_PH__: number;
9
+ };
10
+ export declare function useMergeCells(rowActiveProp: Ref<RowActiveOption<any>>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>, rowKeyGen: RowKeyGen, colKeyGen: ColKeyGen, virtual_dataSourcePart: ShallowRef<any[]>, virtualScroll: Ref<VirtualScrollStore>,
11
+ /** 视口行渲染使用的列列表(virtual-x 时为可视列子集,否则即 tableHeaderLast) */
12
+ virtualX_columnPart: ComputedRef<PrivateStkTableColumn<PrivateRowDT>[]>,
13
+ /** 全量数据(仅用于缓存失效判定) */
14
+ dataSourceCopy: ShallowRef<any[]>,
15
+ /** mergeCells 结果共享缓存(与 useVirtualScroll 共用,可跨滚动帧复用) */
16
+ mergeCellsCache: MergeCellsCache,
17
+ /** 是否允许把连续空行合并为占位 tr(virtual && !autoRowHeight) */
18
+ canMergeEmptyRows: Ref<boolean>,
19
+ /** 视口下方合并为占位 tr 的行数(渲染 rowspan 属性修正用) */
20
+ belowViewportRowCount: Ref<number>): readonly [Ref<Record<UniqKey, Set<UniqKey>> | null, Record<UniqKey, Set<UniqKey>> | null>, (row: MergeCellsParam<any>["row"], col: MergeCellsParam<any>["col"], rowIndex: MergeCellsParam<any>["rowIndex"], colIndex: MergeCellsParam<any>["colIndex"]) => {
5
21
  colspan?: number;
6
22
  rowspan?: number;
7
- } | undefined, Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>, (rowKey: UniqKey | undefined) => void, Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>, (clear?: boolean, rowKey?: UniqKey) => void];
23
+ } | undefined, Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>, (rowKey: UniqKey | undefined) => void, Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>, (clear?: boolean, rowKey?: UniqKey) => void, ComputedRef<Map<UniqKey, AboveViewportCell[]>>, ComputedRef<{
24
+ start: number;
25
+ count: number;
26
+ }[]>, ComputedRef<{
27
+ count: number;
28
+ }[]>];
29
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { Ref, ShallowRef } from 'vue';
2
+ import { MergeCellsCache } from './mergeCellsCache';
2
3
  import { PrivateRowDT, PrivateStkTableColumn, RowKeyGen, UniqKey } from './types';
3
4
  import { ScrollbarOptions } from './useScrollbar';
4
5
 
@@ -21,6 +22,10 @@ export type VirtualScrollStore = {
21
22
  /** 总滚动高度 */
22
23
  scrollHeight: number;
23
24
  translateY: number;
25
+ /** 视口实际起始行索引(rowspan 修正前的原始值,用于区分 above-viewport 行) */
26
+ viewportStartIndex: number;
27
+ /** 视口实际结束行索引(rowspan 修正前的原始值,用于区分 below-viewport 行) */
28
+ viewportEndIndex: number;
24
29
  };
25
30
  /** 暂存横向虚拟滚动的数据 */
26
31
  export type VirtualScrollXStore = {
@@ -41,42 +46,19 @@ export type VirtualScrollXStore = {
41
46
  * virtual scroll
42
47
  * @returns
43
48
  */
44
- export declare function useVirtualScroll(props: any, tableContainerRef: Ref<HTMLElement | undefined>, trRef: Ref<HTMLTableRowElement[] | undefined>, dataSourceCopy: ShallowRef<PrivateRowDT[]>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>, tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>, rowKeyGen: RowKeyGen, maxRowSpan: Map<UniqKey, number>, scrollbarOptions: Ref<Required<ScrollbarOptions>>, isExperimentalScrollY: Ref<boolean | undefined>): readonly [Ref<{
45
- containerHeight: number;
46
- pageSize: number;
47
- startIndex: number;
48
- endIndex: number;
49
- rowHeight: number;
50
- offsetTop: number;
51
- scrollTop: number;
52
- scrollHeight: number;
53
- translateY: number;
54
- }, VirtualScrollStore | {
55
- containerHeight: number;
56
- pageSize: number;
57
- startIndex: number;
58
- endIndex: number;
59
- rowHeight: number;
60
- offsetTop: number;
61
- scrollTop: number;
62
- scrollHeight: number;
63
- translateY: number;
64
- }>, Ref<{
65
- containerWidth: number;
66
- scrollWidth: number;
67
- startIndex: number;
68
- endIndex: number;
69
- offsetLeft: number;
70
- scrollLeft: number;
71
- }, VirtualScrollXStore | {
72
- containerWidth: number;
73
- scrollWidth: number;
74
- startIndex: number;
75
- endIndex: number;
76
- offsetLeft: number;
77
- scrollLeft: number;
78
- }>, import('vue').ComputedRef<any>, import('vue').ComputedRef<PrivateRowDT[]>, import('vue').ComputedRef<number>, import('vue').ComputedRef<any>, import('vue').ComputedRef<number>, import('vue').ComputedRef<number>, (height?: number) => void, (height?: number) => void, () => void, (sTop?: number) => void, (sLeft?: number) => void, (rowKey: UniqKey, height?: number | null) => void, () => void, () => void, import('vue').ComputedRef<PrivateStkTableColumn<PrivateRowDT>[][]>, import('vue').ComputedRef<number>, import('vue').ComputedRef<{
49
+ export declare function useVirtualScroll(props: any, tableContainerRef: Ref<HTMLElement | undefined>, trRef: Ref<HTMLTableRowElement[] | undefined>, dataSourceCopy: ShallowRef<PrivateRowDT[]>, tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>, tableHeaders: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[][]>, rowKeyGen: RowKeyGen, maxRowSpan: Map<UniqKey, number>,
50
+ /** 全局最大 rowspan(限定跨界修正扫描范围用) */
51
+ getMaxRowSpanValue: () => number, scrollbarOptions: Ref<Required<ScrollbarOptions>>, isExperimentalScrollY: Ref<boolean | undefined>,
52
+ /** mergeCells 结果共享缓存(与 useMergeCells 共用,避免重复调用用户回调) */
53
+ mergeCellsCache: MergeCellsCache): readonly [ShallowRef<VirtualScrollStore, VirtualScrollStore>, ShallowRef<VirtualScrollXStore, VirtualScrollXStore>, import('vue').ComputedRef<any>, import('vue').ComputedRef<PrivateRowDT[]>, import('vue').ComputedRef<number>, import('vue').ComputedRef<any>, import('vue').ComputedRef<number>, import('vue').ComputedRef<number>, (height?: number) => void, (height?: number) => void, () => void, (sTop?: number) => void, (sLeft?: number) => void, (rowKey: UniqKey, height?: number | null) => void, () => void, () => void, import('vue').ComputedRef<PrivateStkTableColumn<PrivateRowDT>[][]>, import('vue').ComputedRef<number>, import('vue').ComputedRef<{
79
54
  startIndex: number;
80
55
  endIndex: number;
81
56
  offsetLeft: number;
82
- }>, import('vue').ComputedRef<PrivateStkTableColumn<PrivateRowDT>[]>];
57
+ }>, import('vue').ComputedRef<PrivateStkTableColumn<PrivateRowDT>[]>, import('vue').ComputedRef<{
58
+ prefix: PrivateStkTableColumn<PrivateRowDT>[];
59
+ leftExpand: PrivateStkTableColumn<PrivateRowDT>[];
60
+ viewport: PrivateStkTableColumn<PrivateRowDT>[];
61
+ suffix: PrivateStkTableColumn<PrivateRowDT>[];
62
+ /** leftExpand 起始的绝对叶子列索引(mergeCells 缓存键用) */
63
+ leftExpandStart: number;
64
+ } | null>];