stk-table-vue 1.2.0 → 1.2.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.
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.2.0
3
+ * version: v1.2.2
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-yOz9wkCz.js";
9
+ import { t as StkTable_default } from "./StkTable-g_qwf6BE.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.2.0
3
+ * version: v1.2.2
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/
@@ -1646,8 +1646,6 @@ function useFixedStyle(props, isRelativeMode, getFixedColPosition, virtualScroll
1646
1646
  if ((tagType === TagType.TD || tagType === TagType.TF) && !fixed) return "";
1647
1647
  const { headerRowHeight, rowHeight } = props;
1648
1648
  const isFixedLeft = fixed === "left";
1649
- const { scrollLeft, scrollWidth, offsetLeft, containerWidth } = virtualScrollX.value;
1650
- const scrollRight = scrollWidth - containerWidth - scrollLeft;
1651
1649
  let style = "";
1652
1650
  if (tagType === TagType.TH) if (!isRelativeMode.value) {
1653
1651
  if (depth) style += `top:${depth * (headerRowHeight ?? rowHeight)}px;`;
@@ -1657,8 +1655,12 @@ function useFixedStyle(props, isRelativeMode, getFixedColPosition, virtualScroll
1657
1655
  const lr = getFixedColPosition.value(col) + "px";
1658
1656
  if (isFixedLeft) style += `left:${lr};`;
1659
1657
  else style += `right:${lr};`;
1660
- } else if (isFixedLeft) style += `left:${scrollLeft - (virtualX_on.value ? offsetLeft : 0)}px;`;
1661
- else style += `right:${Math.max(scrollRight - (virtualX_on.value ? virtualX_offsetRight.value : 0), 0)}px;`;
1658
+ } else {
1659
+ const { scrollLeft, scrollWidth, offsetLeft, containerWidth } = virtualScrollX.value;
1660
+ const scrollRight = scrollWidth - containerWidth - scrollLeft;
1661
+ if (isFixedLeft) style += `left:${scrollLeft - (virtualX_on.value ? offsetLeft : 0)}px;`;
1662
+ else style += `right:${Math.max(scrollRight - (virtualX_on.value ? virtualX_offsetRight.value : 0), 0)}px;`;
1663
+ }
1662
1664
  return style;
1663
1665
  }
1664
1666
  return getFixedStyle;
@@ -2145,6 +2147,109 @@ function useMergeCells(rowActiveProp, tableHeaderLast, rowKeyGen, colKeyGen, vir
2145
2147
  const activeMergedCells = ref(/* @__PURE__ */ new Set());
2146
2148
  /** column index cache */
2147
2149
  let colIndexCache = null;
2150
+ /** 空合并段列表(start 为绝对行索引)。禁止合并时返回共享空数组 */
2151
+ const EMPTY_BLOCKS = [];
2152
+ /** 空下方占位段列表。无下方合并行时返回共享空数组 */
2153
+ const EMPTY_SEGMENTS = [];
2154
+ /**
2155
+ * mergeCellsWrapper 返回值记忆化缓存(WeakMap<行对象, Map<colKey, 结果>>)。
2156
+ * 签名 = 绝对行索引 + 代际(wrapperCacheGen):窗口/列/视口修正(上方空行段、
2157
+ * 下方占位段、viewportEndIndex、下方行数)任一变化即整体失效。
2158
+ */
2159
+ const wrapperCache = /* @__PURE__ */ new WeakMap();
2160
+ /** 是否存在合并列(仅依赖列配置,跨滚动帧缓存,不随数据/滚动重算) */
2161
+ const hasMergeColumn = computed(() => tableHeaderLast.value.some((col) => !!col.mergeCells));
2162
+ /**
2163
+ * 视口上方连续「无 td」空行段(行数 >= 2 才成段;单行保留独立 tr)。
2164
+ * 空行判定与渲染保持一致:aboveViewportColumnMap 中列结果为空且非展开行。
2165
+ * 供模板合并渲染(StkTable aboveRenderParts)与 rowspan 属性修正共用。
2166
+ */
2167
+ const aboveEmptyBlocks = computed(() => {
2168
+ if (!canMergeEmptyRows.value) return EMPTY_BLOCKS;
2169
+ const data = virtual_dataSourcePart.value;
2170
+ const { startIndex, viewportStartIndex } = virtualScroll.value;
2171
+ const aboveCount = Math.min(data.length, Math.max(0, viewportStartIndex - startIndex));
2172
+ if (aboveCount <= 0) return EMPTY_BLOCKS;
2173
+ const colMap = aboveViewportColumnMap.value;
2174
+ const isEmptyRow = (row) => {
2175
+ if (!row || row.__EXP_R__) return false;
2176
+ const cols = colMap.get(rowKeyGen(row));
2177
+ return cols !== void 0 && cols.length === 0;
2178
+ };
2179
+ const blocks = [];
2180
+ let runStart = -1;
2181
+ const flushRun = (endExclusive) => {
2182
+ if (endExclusive - runStart >= 2) blocks.push({
2183
+ start: startIndex + runStart,
2184
+ count: endExclusive - runStart
2185
+ });
2186
+ runStart = -1;
2187
+ };
2188
+ for (let i = 0; i < aboveCount; i++) {
2189
+ if (isEmptyRow(data[i])) {
2190
+ if (runStart < 0) runStart = i;
2191
+ continue;
2192
+ }
2193
+ if (runStart >= 0) flushRun(i);
2194
+ }
2195
+ if (runStart >= 0) flushRun(aboveCount);
2196
+ return blocks.length ? blocks : EMPTY_BLOCKS;
2197
+ });
2198
+ /**
2199
+ * 视口下方占位段:把 [viewportEndIndex+1, endIndex] 按跨界 rowspan 的
2200
+ * 去重结束行切分为多段,每段渲染一个占位 tr。
2201
+ *
2202
+ * 单个占位 tr 无法表达多个 rowspan 的不同逻辑结束位置(不规律合并下
2203
+ * 多个单元格会塌缩到同一 tr 底边,滚动时高度跳动);按结束行切段后,
2204
+ * 每个单元格的修正 rowspan 恰好止于包含其逻辑结束行的段。
2205
+ * 超长 rowspan 场景只有一个结束行(= 区域末端),仍只产生 1 段,不增加 DOM。
2206
+ */
2207
+ const belowPhSegments = computed(() => {
2208
+ const belowCount = belowViewportRowCount.value;
2209
+ if (belowCount <= 0) return EMPTY_SEGMENTS;
2210
+ const data = virtual_dataSourcePart.value;
2211
+ const { startIndex, viewportEndIndex, endIndex } = virtualScroll.value;
2212
+ const regionEnd = Math.min(endIndex, startIndex + data.length - 1);
2213
+ const columns = virtualX_columnPart.value;
2214
+ const endsSet = /* @__PURE__ */ new Set();
2215
+ const anchorRowEnd = Math.min(data.length - 1, viewportEndIndex - startIndex);
2216
+ for (let rowOffset = 0; rowOffset <= anchorRowEnd; rowOffset++) {
2217
+ const row = data[rowOffset];
2218
+ if (!row) continue;
2219
+ const absRowIndex = startIndex + rowOffset;
2220
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
2221
+ const col = columns[colIdx];
2222
+ if (!col.mergeCells) continue;
2223
+ const leafIndex = col.__LF_S__ ?? colIdx;
2224
+ const { rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
2225
+ if (rowspan <= 1) continue;
2226
+ const spanEnd = absRowIndex + rowspan - 1;
2227
+ if (spanEnd > viewportEndIndex) endsSet.add(Math.min(spanEnd, regionEnd));
2228
+ }
2229
+ }
2230
+ if (!endsSet.size) return [{ count: belowCount }];
2231
+ const ends = Array.from(endsSet).sort((a, b) => a - b);
2232
+ const segments = [];
2233
+ let prev = viewportEndIndex;
2234
+ for (let i = 0; i < ends.length; i++) {
2235
+ const count = ends[i] - prev;
2236
+ if (count > 0) segments.push({ count });
2237
+ prev = ends[i];
2238
+ }
2239
+ if (regionEnd > prev) segments.push({ count: regionEnd - prev });
2240
+ return segments;
2241
+ });
2242
+ let wrapperCacheGen = 0;
2243
+ watch([
2244
+ virtual_dataSourcePart,
2245
+ tableHeaderLast,
2246
+ aboveEmptyBlocks,
2247
+ belowPhSegments,
2248
+ () => virtualScroll.value.viewportEndIndex,
2249
+ belowViewportRowCount
2250
+ ], () => {
2251
+ wrapperCacheGen++;
2252
+ });
2148
2253
  /**
2149
2254
  * 数据/列真正变化时才清空 mergeCells 结果缓存。
2150
2255
  * 缓存键为绝对索引且命中时校验行引用同一性,滚动换窗(virtual_dataSourcePart 变化)
@@ -2159,8 +2264,6 @@ function useMergeCells(rowActiveProp, tableHeaderLast, rowKeyGen, colKeyGen, vir
2159
2264
  watch([virtual_dataSourcePart, tableHeaderLast], () => {
2160
2265
  buildHiddenCellMap();
2161
2266
  });
2162
- /** 是否存在合并列(仅依赖列配置,跨滚动帧缓存,不随数据/滚动重算) */
2163
- const hasMergeColumn = computed(() => tableHeaderLast.value.some((col) => !!col.mergeCells));
2164
2267
  /**
2165
2268
  * Pre-build hiddenCellMap and hoverRowMap before rendering to avoid
2166
2269
  * a two-render cycle (null → populated) that causes visual flicker.
@@ -2260,20 +2363,35 @@ function useMergeCells(rowActiveProp, tableHeaderLast, rowKeyGen, colKeyGen, vir
2260
2363
  function mergeCellsWrapper(row, col, rowIndex, colIndex) {
2261
2364
  if (!col.mergeCells) return;
2262
2365
  const absRowIndex = virtualScroll.value.startIndex + rowIndex;
2263
- const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, colIndex);
2264
- if (colspan === 1 && rowspan === 1) return;
2265
- const rowKey = rowKeyGen(row);
2266
2366
  const colKey = colKeyGen.value(col);
2267
- const mergedCellKey = pureCellKeyGen(rowKey, colKey);
2268
- for (let i = rowIndex; i < rowIndex + rowspan; i++) {
2269
- const targetRow = virtual_dataSourcePart.value[i];
2270
- if (!targetRow) break;
2271
- hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
2367
+ let rowCache = wrapperCache.get(row);
2368
+ if (!rowCache) {
2369
+ rowCache = /* @__PURE__ */ new Map();
2370
+ wrapperCache.set(row, rowCache);
2272
2371
  }
2273
- return {
2274
- colspan,
2275
- rowspan: adjustRowspanForMergedRows(absRowIndex, rowspan)
2276
- };
2372
+ const hit = rowCache.get(colKey);
2373
+ if (hit && hit.absRowIndex === absRowIndex && hit.gen === wrapperCacheGen) return hit.result;
2374
+ const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, colIndex);
2375
+ let result;
2376
+ if (colspan === 1 && rowspan === 1) result = void 0;
2377
+ else {
2378
+ const mergedCellKey = pureCellKeyGen(rowKeyGen(row), colKey);
2379
+ for (let i = rowIndex; i < rowIndex + rowspan; i++) {
2380
+ const targetRow = virtual_dataSourcePart.value[i];
2381
+ if (!targetRow) break;
2382
+ hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
2383
+ }
2384
+ result = {
2385
+ colspan,
2386
+ rowspan: adjustRowspanForMergedRows(absRowIndex, rowspan)
2387
+ };
2388
+ }
2389
+ rowCache.set(colKey, {
2390
+ absRowIndex,
2391
+ gen: wrapperCacheGen,
2392
+ result
2393
+ });
2394
+ return result;
2277
2395
  }
2278
2396
  /**
2279
2397
  * 渲染用 rowspan 修正:合并占位 tr 代表 N 个逻辑行,但在 DOM 中只算 1 行,
@@ -2395,90 +2513,6 @@ function useMergeCells(rowActiveProp, tableHeaderLast, rowKeyGen, colKeyGen, vir
2395
2513
  }
2396
2514
  return map;
2397
2515
  });
2398
- /** 空合并段列表(start 为绝对行索引)。禁止合并时返回共享空数组 */
2399
- const EMPTY_BLOCKS = [];
2400
- /** 空下方占位段列表。无下方合并行时返回共享空数组 */
2401
- const EMPTY_SEGMENTS = [];
2402
- /**
2403
- * 视口上方连续「无 td」空行段(行数 >= 2 才成段;单行保留独立 tr)。
2404
- * 空行判定与渲染保持一致:aboveViewportColumnMap 中列结果为空且非展开行。
2405
- * 供模板合并渲染(StkTable aboveRenderParts)与 rowspan 属性修正共用。
2406
- */
2407
- const aboveEmptyBlocks = computed(() => {
2408
- if (!canMergeEmptyRows.value) return EMPTY_BLOCKS;
2409
- const data = virtual_dataSourcePart.value;
2410
- const { startIndex, viewportStartIndex } = virtualScroll.value;
2411
- const aboveCount = Math.min(data.length, Math.max(0, viewportStartIndex - startIndex));
2412
- if (aboveCount <= 0) return EMPTY_BLOCKS;
2413
- const colMap = aboveViewportColumnMap.value;
2414
- const isEmptyRow = (row) => {
2415
- if (!row || row.__EXP_R__) return false;
2416
- const cols = colMap.get(rowKeyGen(row));
2417
- return cols !== void 0 && cols.length === 0;
2418
- };
2419
- const blocks = [];
2420
- let runStart = -1;
2421
- const flushRun = (endExclusive) => {
2422
- if (endExclusive - runStart >= 2) blocks.push({
2423
- start: startIndex + runStart,
2424
- count: endExclusive - runStart
2425
- });
2426
- runStart = -1;
2427
- };
2428
- for (let i = 0; i < aboveCount; i++) {
2429
- if (isEmptyRow(data[i])) {
2430
- if (runStart < 0) runStart = i;
2431
- continue;
2432
- }
2433
- if (runStart >= 0) flushRun(i);
2434
- }
2435
- if (runStart >= 0) flushRun(aboveCount);
2436
- return blocks.length ? blocks : EMPTY_BLOCKS;
2437
- });
2438
- /**
2439
- * 视口下方占位段:把 [viewportEndIndex+1, endIndex] 按跨界 rowspan 的
2440
- * 去重结束行切分为多段,每段渲染一个占位 tr。
2441
- *
2442
- * 单个占位 tr 无法表达多个 rowspan 的不同逻辑结束位置(不规律合并下
2443
- * 多个单元格会塌缩到同一 tr 底边,滚动时高度跳动);按结束行切段后,
2444
- * 每个单元格的修正 rowspan 恰好止于包含其逻辑结束行的段。
2445
- * 超长 rowspan 场景只有一个结束行(= 区域末端),仍只产生 1 段,不增加 DOM。
2446
- */
2447
- const belowPhSegments = computed(() => {
2448
- const belowCount = belowViewportRowCount.value;
2449
- if (belowCount <= 0) return EMPTY_SEGMENTS;
2450
- const data = virtual_dataSourcePart.value;
2451
- const { startIndex, viewportEndIndex, endIndex } = virtualScroll.value;
2452
- const regionEnd = Math.min(endIndex, startIndex + data.length - 1);
2453
- const columns = virtualX_columnPart.value;
2454
- const endsSet = /* @__PURE__ */ new Set();
2455
- const anchorRowEnd = Math.min(data.length - 1, viewportEndIndex - startIndex);
2456
- for (let rowOffset = 0; rowOffset <= anchorRowEnd; rowOffset++) {
2457
- const row = data[rowOffset];
2458
- if (!row) continue;
2459
- const absRowIndex = startIndex + rowOffset;
2460
- for (let colIdx = 0; colIdx < columns.length; colIdx++) {
2461
- const col = columns[colIdx];
2462
- if (!col.mergeCells) continue;
2463
- const leafIndex = col.__LF_S__ ?? colIdx;
2464
- const { rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
2465
- if (rowspan <= 1) continue;
2466
- const spanEnd = absRowIndex + rowspan - 1;
2467
- if (spanEnd > viewportEndIndex) endsSet.add(Math.min(spanEnd, regionEnd));
2468
- }
2469
- }
2470
- if (!endsSet.size) return [{ count: belowCount }];
2471
- const ends = Array.from(endsSet).sort((a, b) => a - b);
2472
- const segments = [];
2473
- let prev = viewportEndIndex;
2474
- for (let i = 0; i < ends.length; i++) {
2475
- const count = ends[i] - prev;
2476
- if (count > 0) segments.push({ count });
2477
- prev = ends[i];
2478
- }
2479
- if (regionEnd > prev) segments.push({ count: regionEnd - prev });
2480
- return segments;
2481
- });
2482
2516
  function updateActiveMergedCells(clear, rowKey) {
2483
2517
  if (!rowActiveProp.value.enabled) return;
2484
2518
  if (clear) {
@@ -2785,14 +2819,15 @@ var SORT_SWITCH_ORDER = [
2785
2819
  * 排序 Hook
2786
2820
  * 管理表格排序状态和相关操作
2787
2821
  * @param props 表格 props
2788
- * @param colKeyGen 列 key 生成函数
2822
+ * @param colKeyGen 列 key 生成函数
2789
2823
  * @param tableHeaderLast 表头最后一行(叶子节点)
2790
2824
  * @param dataSourceCopy 数据源副本 ref
2791
2825
  * @param initDataSource 初始化数据源函数
2792
2826
  * @param emits 事件发射函数
2827
+ * @param onDataSourceChange 数据源变化后的刷新回调(重算虚拟滚动等,见 #80)
2793
2828
  * @returns 排序相关状态和方法
2794
2829
  */
2795
- function useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, initDataSource) {
2830
+ function useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, initDataSource, onDataSourceChange) {
2796
2831
  /** 多列排序状态数组 */
2797
2832
  const sortStates = ref([]);
2798
2833
  /** 是否启用多列排序 */
@@ -2922,7 +2957,10 @@ function useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, ini
2922
2957
  ...col.sortConfig
2923
2958
  };
2924
2959
  const order = updateSortState(col, sortConfig);
2925
- if (!props.sortRemote) initDataSource();
2960
+ if (!props.sortRemote) {
2961
+ initDataSource();
2962
+ onDataSourceChange();
2963
+ }
2926
2964
  emits("sort-change", col, order, toRaw(dataSourceCopy.value), sortConfig);
2927
2965
  }
2928
2966
  /**
@@ -2950,7 +2988,10 @@ function useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, ini
2950
2988
  }, newOption.append && isMultiSort.value ? 1 : 0);
2951
2989
  } else sortStates.value = [];
2952
2990
  if (newOption.sort && ((_dataSourceCopy$value = dataSourceCopy.value) === null || _dataSourceCopy$value === void 0 ? void 0 : _dataSourceCopy$value.length)) {
2953
- if (!props.sortRemote || newOption.force) initDataSource(props.dataSource, { forceSort: newOption.force });
2991
+ if (!props.sortRemote || newOption.force) {
2992
+ initDataSource(props.dataSource, { forceSort: newOption.force });
2993
+ onDataSourceChange();
2994
+ }
2954
2995
  }
2955
2996
  if (!newOption.silent) {
2956
2997
  if (!column) column = newOption.sortOption || tableHeaderLast.value.find((it) => colKeyGenValue(it) === colKey);
@@ -2965,6 +3006,7 @@ function useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, ini
2965
3006
  function resetSorter() {
2966
3007
  sortStates.value = [];
2967
3008
  initDataSource();
3009
+ onDataSourceChange();
2968
3010
  }
2969
3011
  /**
2970
3012
  * 处理默认排序
@@ -3613,6 +3655,9 @@ function useColWidthCache(getColWidth) {
3613
3655
  }
3614
3656
  /** vue2 优化滚动回收延时 */
3615
3657
  var VUE2_SCROLL_TIMEOUT_MS = 200;
3658
+ /** 静默字段常量(模块级复用,避免每次滚动帧分配数组);relative 模式不静默(见 assignVs 注释) */
3659
+ var SILENT_Y_KEYS = ["scrollTop"];
3660
+ var SILENT_X_KEYS = ["scrollLeft"];
3616
3661
  /**
3617
3662
  * 合并单元格可视范围修正的最大迭代次数。
3618
3663
  * 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
@@ -3623,7 +3668,7 @@ var MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
3623
3668
  * virtual scroll
3624
3669
  * @returns
3625
3670
  */
3626
- function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, getMaxRowSpanValue, scrollbarOptions, isExperimentalScrollY, mergeCellsCache) {
3671
+ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, getMaxRowSpanValue, scrollbarOptions, isExperimentalScrollY, isRelativeMode, mergeCellsCache) {
3627
3672
  const tableHeaderHeight = computed(() => props.headerRowHeight * tableHeaders.value.length);
3628
3673
  const virtualScroll = shallowRef({
3629
3674
  containerHeight: 0,
@@ -3982,15 +4027,20 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
3982
4027
  * shallowRef 内部属性变更不会触发响应式,必须手动 triggerRef;但无条件触发会让
3983
4028
  * 「值未变」的调用也强制整表重渲染(每帧滚动多付 1~5ms 渲染成本)。
3984
4029
  * 本函数恢复 Vue 深响应式 ref 的 hasChanged 语义,滚动重算回到 0.02ms 级。
4030
+ *
4031
+ * silentKeys:写入 store 但不参与 triggerRef 判定的字段。滚动位置(scrollTop/scrollLeft)
4032
+ * 每帧必变且模板不直接消费,静默写入使「滚动窗口(startIndex/endIndex)未变」的帧
4033
+ * 完全跳过组件重渲染(消费方均在滚动处理函数中即时读取 .value)。
4034
+ * relative 固定模式例外:useFixedStyle 的样式计算响应式依赖滚动位置,该模式下不静默。
3985
4035
  */
3986
- function assignVs(ref, patch) {
4036
+ function assignVs(ref, patch, silentKeys) {
3987
4037
  const store = ref.value;
3988
4038
  let changed = false;
3989
4039
  for (const key in patch) {
3990
4040
  const k = key;
3991
4041
  if (store[k] !== patch[k]) {
3992
4042
  store[k] = patch[k];
3993
- changed = true;
4043
+ if (!(silentKeys === null || silentKeys === void 0 ? void 0 : silentKeys.includes(k))) changed = true;
3994
4044
  }
3995
4045
  }
3996
4046
  if (changed) triggerRef(ref);
@@ -4142,14 +4192,15 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
4142
4192
  }
4143
4193
  }
4144
4194
  vsValue.scrollTop = sTop;
4145
- assignVs(virtualScroll, vsValue);
4195
+ assignVs(virtualScroll, vsValue, isRelativeMode.value ? void 0 : SILENT_Y_KEYS);
4146
4196
  if (!virtual_on.value) {
4197
+ const lastIdx = Math.max(0, dataLength - 1);
4147
4198
  assignVs(virtualScroll, {
4148
4199
  startIndex: 0,
4149
- endIndex: 0,
4200
+ endIndex: lastIdx,
4150
4201
  offsetTop: 0,
4151
4202
  viewportStartIndex: 0,
4152
- viewportEndIndex: 0
4203
+ viewportEndIndex: lastIdx
4153
4204
  });
4154
4205
  return;
4155
4206
  }
@@ -4228,7 +4279,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
4228
4279
  }
4229
4280
  startIndex = Math.max(0, startIndex);
4230
4281
  endIndex = Math.min(endIndex, dataLength);
4231
- if (startIndex >= endIndex) startIndex = endIndex - pageSize;
4282
+ if (startIndex >= endIndex) startIndex = Math.max(0, endIndex - pageSize);
4232
4283
  if (vue2ScrollYTimeout) window.clearTimeout(vue2ScrollYTimeout);
4233
4284
  let offsetTop = 0;
4234
4285
  if (autoRowHeight || hasExpandCol.value) offsetTop = autoRowHeightTop;
@@ -4305,17 +4356,18 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
4305
4356
  }
4306
4357
  endIndex = Math.min(endIndex, headerLength);
4307
4358
  if (vue2ScrollXTimeout) window.clearTimeout(vue2ScrollXTimeout);
4359
+ const silentXKeys = isRelativeMode.value ? void 0 : SILENT_X_KEYS;
4308
4360
  if (!props.optimizeVue2Scroll || sLeft <= scrollLeft) assignVs(virtualScrollX, {
4309
4361
  startIndex,
4310
4362
  endIndex,
4311
4363
  offsetLeft,
4312
4364
  scrollLeft: sLeft
4313
- });
4365
+ }, silentXKeys);
4314
4366
  else {
4315
4367
  assignVs(virtualScrollX, {
4316
4368
  endIndex,
4317
4369
  scrollLeft: sLeft
4318
- });
4370
+ }, silentXKeys);
4319
4371
  vue2ScrollXTimeout = window.setTimeout(() => {
4320
4372
  assignVs(virtualScrollX, {
4321
4373
  startIndex,
@@ -4703,7 +4755,7 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
4703
4755
  return (_props$experimental = props.experimental) === null || _props$experimental === void 0 ? void 0 : _props$experimental.scrollY;
4704
4756
  });
4705
4757
  const rowKeyGenCache = /* @__PURE__ */ new WeakMap();
4706
- const [sortStates, sortCol, onColumnSort, setSorter, resetSorter, getSortColumns, dealDefaultSorter, getColumnSortState, sortData] = useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, initDataSource);
4758
+ const [sortStates, sortCol, onColumnSort, setSorter, resetSorter, getSortColumns, dealDefaultSorter, getColumnSortState, sortData] = useSorter(props, emits, colKeyGen, tableHeaderLast, dataSourceCopy, initDataSource, onDataSourceChange);
4707
4759
  const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
4708
4760
  const [onThDragStart, onThDragOver, onThDrop, isHeaderDraggable] = useThDrag(props, emits, colKeyGen);
4709
4761
  const [onTrDragStart, onTrDragEnter, onTrDragOver, onTrDrop, onTrDragEnd] = useTrDrag(props, emits, dataSourceCopy);
@@ -4714,7 +4766,7 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
4714
4766
  * 且可跨滚动帧复用(数据/列变化时才清空)。
4715
4767
  */
4716
4768
  const mergeCellsCache = createMergeCellsCache();
4717
- const [virtualScroll, virtualScrollX, virtual_on, virtual_dataSourcePart, virtual_offsetBottom, virtualX_on, virtualX_offsetRight, tableHeaderHeight, initVirtualScroll, initVirtualScrollY, initVirtualScrollX, updateVirtualScrollY, updateVirtualScrollX, setAutoHeight, clearAllAutoHeight, getRowsHeight, clearColWidthCache, virtualX_tableHeaders, expandRowColspan, theadVirtualX, virtualX_columnPart, virtualX_expandColSegments, getRowHeightCacheInfo] = useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, getMaxRowSpanValue, scrollbarOptions, isExperimentalScrollY, mergeCellsCache);
4769
+ const [virtualScroll, virtualScrollX, virtual_on, virtual_dataSourcePart, virtual_offsetBottom, virtualX_on, virtualX_offsetRight, tableHeaderHeight, initVirtualScroll, initVirtualScrollY, initVirtualScrollX, updateVirtualScrollY, updateVirtualScrollX, setAutoHeight, clearAllAutoHeight, getRowsHeight, clearColWidthCache, virtualX_tableHeaders, expandRowColspan, theadVirtualX, virtualX_columnPart, virtualX_expandColSegments, getRowHeightCacheInfo] = useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, tableHeaderLast, tableHeaders, rowKeyGen, maxRowSpan, getMaxRowSpanValue, scrollbarOptions, isExperimentalScrollY, isRelativeMode, mergeCellsCache);
4718
4770
  /** requestAnimationFrame throttled version of updateVirtualScrollY for smoother wheel scrolling */
4719
4771
  const rafUpdateVirtualScrollYForWheel = rafThrottle(updateVirtualScrollY);
4720
4772
  const [scrollbar, showScrollbar, onVerticalScrollbarMouseDown, onHorizontalScrollbarMouseDown, updateCustomScrollbar] = useScrollbar(props, tableContainerRef, virtualScroll, virtualScrollX, updateVirtualScrollY, scrollbarOptions, isExperimentalScrollY);
@@ -4871,6 +4923,7 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
4871
4923
  dealDefaultSorter();
4872
4924
  });
4873
4925
  async function onDataSourceChange() {
4926
+ updateMaxRowSpan();
4874
4927
  await nextTick();
4875
4928
  initVirtualScrollY();
4876
4929
  updateCustomScrollbar();
@@ -4885,7 +4938,10 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
4885
4938
  function setFilter(status, option) {
4886
4939
  status = status || {};
4887
4940
  filterStatus.value = status;
4888
- if (!(option === null || option === void 0 ? void 0 : option.remote)) initDataSource();
4941
+ if (!(option === null || option === void 0 ? void 0 : option.remote)) {
4942
+ initDataSource();
4943
+ onDataSourceChange();
4944
+ }
4889
4945
  if (!(option === null || option === void 0 ? void 0 : option.silent)) emits("filter-change", status);
4890
4946
  }
4891
4947
  function filterDataSource(dataSource) {
@@ -5097,11 +5153,11 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
5097
5153
  title: getHeaderTitle(col),
5098
5154
  class: [
5099
5155
  col.sorter ? "sortable" : "",
5100
- isSorted && "sorter-" + (sortState === null || sortState === void 0 ? void 0 : sortState.order),
5156
+ isSorted ? "sorter-" + (sortState === null || sortState === void 0 ? void 0 : sortState.order) : "",
5101
5157
  col.headerClassName,
5102
5158
  fixedColClassMap.value.get(colKey),
5103
- col.headerAlign && (col.headerAlign === "left" ? "text-l" : col.headerAlign === "right" ? "text-r" : col.headerAlign === "center" ? "text-c" : null)
5104
- ]
5159
+ col.headerAlign === "left" ? "text-l" : col.headerAlign === "right" ? "text-r" : col.headerAlign === "center" ? "text-c" : ""
5160
+ ].filter(Boolean).join(" ")
5105
5161
  };
5106
5162
  }
5107
5163
  function getTFProps(col) {
@@ -5114,7 +5170,7 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
5114
5170
  fixedColClassMap.value.get(colKey),
5115
5171
  col.type === "seq" ? "seq-column" : "",
5116
5172
  col.align === "center" ? "text-c" : col.align === "right" ? "text-r" : ""
5117
- ]
5173
+ ].filter(Boolean).join(" ")
5118
5174
  };
5119
5175
  }
5120
5176
  function getTDProps(row, col, rowIndex, colIndex) {
@@ -5136,7 +5192,7 @@ var StkTable_default = /* @__PURE__ */ defineComponent({
5136
5192
  return {
5137
5193
  "data-col-key": colKey,
5138
5194
  style: cellStyleMap.value[TagType.TD].get(colKey),
5139
- class: classList,
5195
+ class: classList.filter(Boolean).join(" "),
5140
5196
  ...mergeCellsWrapper(row, col, rowIndex, col.__LF_S__ ?? 0)
5141
5197
  };
5142
5198
  }
@@ -10,11 +10,12 @@ import { Order, SortOption, SortState, StkTableColumn, UniqKey } from './types/i
10
10
  * @param dataSourceCopy 数据源副本 ref
11
11
  * @param initDataSource 初始化数据源函数
12
12
  * @param emits 事件发射函数
13
+ * @param onDataSourceChange 数据源变化后的刷新回调(重算虚拟滚动等,见 #80)
13
14
  * @returns 排序相关状态和方法
14
15
  */
15
16
  export declare function useSorter<DT extends Record<string, any>>(props: any, emits: any, colKeyGen: Ref<(col: StkTableColumn<DT>) => string>, tableHeaderLast: Ref<StkTableColumn<DT>[]>, dataSourceCopy: Ref<DT[]>, initDataSource: (data?: DT[], option?: {
16
17
  forceSort?: boolean;
17
- }) => void): readonly [Ref<{
18
+ }) => void, onDataSourceChange: () => void): readonly [Ref<{
18
19
  key?: any;
19
20
  dataIndex: import('vue').UnwrapRef<keyof DT & string>;
20
21
  sortField?: import('vue').UnwrapRef<keyof DT> | undefined;
@@ -17,7 +17,11 @@ export type VirtualScrollStore = {
17
17
  rowHeight: number;
18
18
  /** 表格定位上边距 */
19
19
  offsetTop: number;
20
- /** 纵向滚动条位置,用于判断是横向滚动还是纵向 */
20
+ /**
21
+ * 纵向滚动条位置,用于判断是横向滚动还是纵向。
22
+ * 静默字段:非 relative 模式下写入不触发 triggerRef(滚动窗口未变的帧零重渲染),
23
+ * 不得在响应式上下文(computed/模板/watch)中依赖此字段。
24
+ */
21
25
  scrollTop: number;
22
26
  /** 总滚动高度 */
23
27
  scrollHeight: number;
@@ -39,7 +43,10 @@ export type VirtualScrollXStore = {
39
43
  endIndex: number;
40
44
  /** 表格定位左边距 */
41
45
  offsetLeft: number;
42
- /** 横向滚动位置,用于判断是横向滚动还是纵向 */
46
+ /**
47
+ * 横向滚动位置,用于判断是横向滚动还是纵向。
48
+ * 静默字段:非 relative 模式下写入不触发 triggerRef,不得在响应式上下文中依赖此字段。
49
+ */
43
50
  scrollLeft: number;
44
51
  };
45
52
  /**
@@ -49,6 +56,8 @@ export type VirtualScrollXStore = {
49
56
  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
57
  /** 全局最大 rowspan(限定跨界修正扫描范围用) */
51
58
  getMaxRowSpanValue: () => number, scrollbarOptions: Ref<Required<ScrollbarOptions>>, isExperimentalScrollY: Ref<boolean | undefined>,
59
+ /** relative 固定模式的样式(useFixedStyle)以响应式方式依赖 scrollTop/scrollLeft,该模式下保留滚动位置的响应式触发 */
60
+ isRelativeMode: Ref<boolean>,
52
61
  /** mergeCells 结果共享缓存(与 useMergeCells 共用,避免重复调用用户回调) */
53
62
  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, (count: number) => number, () => void, import('vue').ComputedRef<PrivateStkTableColumn<PrivateRowDT>[][]>, import('vue').ComputedRef<number>, import('vue').ComputedRef<{
54
63
  startIndex: number;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * name: stk-table-vue
3
- * version: v1.2.0
3
+ * version: v1.2.2
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-yOz9wkCz.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-g_qwf6BE.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-Bq4DxXYJ.js").then((module) => module.default)).mount(div);
18
+ DropdownIns = createApp(await import("./Dropdown-D40QlY91.js").then((module) => module.default)).mount(div);
19
19
  }
20
20
  return DropdownIns;
21
21
  }