vue-stack-tabs 2.5.0 → 2.6.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.
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  cloneVNode,
9
9
  defineComponent,
10
+ h,
10
11
  nextTick,
11
12
  onActivated,
12
13
  onBeforeUnmount,
@@ -20,6 +21,11 @@ import {
20
21
  } from 'vue'
21
22
  import type { DefineComponent, PropType, VNode } from 'vue'
22
23
  import type { ITabBase } from '../../model/TabModel'
24
+ import {
25
+ readAdapterScrollMetrics,
26
+ resolveScrollAdapter,
27
+ type StackTabsScrollAdapter
28
+ } from '../../model/ScrollAdapter'
23
29
  import PageLoading from '../../components/PageLoading.vue'
24
30
  import type { StackTabsRuntimeContext } from '../stackTabsContext'
25
31
  import { tabPageContextKey } from '../tabPageContext'
@@ -34,6 +40,10 @@ interface PageComponentFactoryOptions {
34
40
  evictMarkedCaches: () => void
35
41
  clearTabEvictionMarks: () => void
36
42
  addPageScroller: (pageId: string, ...selectors: string[]) => void
43
+ setPageScrollAdapter: (
44
+ pageId: string,
45
+ getter: (() => StackTabsScrollAdapter | null) | null
46
+ ) => void
37
47
  saveScroller: (pageId: string) => void
38
48
  restoreScroller: (pageId: string) => void
39
49
  removeScroller: (pageId: string) => void
@@ -79,6 +89,7 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
79
89
  evictMarkedCaches,
80
90
  clearTabEvictionMarks,
81
91
  addPageScroller,
92
+ setPageScrollAdapter,
82
93
  saveScroller,
83
94
  restoreScroller,
84
95
  removeScroller
@@ -154,6 +165,9 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
154
165
  const lastCloned = shallowRef<VNode | null>(null)
155
166
  const lastBackParams = shallowRef<Record<string, unknown> | null>(null)
156
167
  const pageRoot = ref<HTMLElement | null>(null)
168
+ /** 自定义滚动组件的实例(expose proxy),用于解析滚动适配器。 */
169
+ const scrollContainerRef = ref<unknown>(null)
170
+ const customScrollAdapter = shallowRef<StackTabsScrollAdapter | null>(null)
157
171
  /**
158
172
  * 实际渲染的刷新版本。
159
173
  *
@@ -163,6 +177,62 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
163
177
  */
164
178
  const renderedRefreshVersion = ref(props.refreshVersion ?? 0)
165
179
 
180
+ const hasCustomScrollContainer = (): boolean =>
181
+ Boolean(runtimeContext.scrollContainer.value)
182
+
183
+ /**
184
+ * 当前页面的主滚动适配器。
185
+ *
186
+ * 未传 `scrollContainer` 时把 wrapper 本身当作适配器,默认路径行为保持不变;
187
+ * 传入时读取自定义组件 expose 的能力,解析不到则按「无滚动容器」处理。
188
+ */
189
+ const getScrollAdapter = (): StackTabsScrollAdapter | null => {
190
+ if (!hasCustomScrollContainer()) {
191
+ return pageRoot.value ? { scrollElement: pageRoot.value } : null
192
+ }
193
+ return customScrollAdapter.value
194
+ }
195
+
196
+ /** 重新解析自定义滚动组件的适配器(运行时切换容器后也会调用)。 */
197
+ const syncScrollAdapter = (): void => {
198
+ customScrollAdapter.value = hasCustomScrollContainer()
199
+ ? resolveScrollAdapter(scrollContainerRef.value)
200
+ : null
201
+ }
202
+
203
+ /** 阴影订阅:优先用适配器的 onScroll,缺失时监听原生滚动元素。 */
204
+ const subscribeAdapterScroll = (listener: () => void): (() => void) => {
205
+ const adapter = getScrollAdapter()
206
+ if (adapter?.onScroll) return adapter.onScroll(listener)
207
+ const element = adapter?.scrollElement
208
+ if (!element) return () => undefined
209
+ element.addEventListener('scroll', listener, { passive: true })
210
+ return () => element.removeEventListener('scroll', listener)
211
+ }
212
+
213
+ // 阴影类挂到不被刷新动画 transform 的 panel 上;滚动监听绑定真实滚动元素。
214
+ // 否则 ::after 的绝对定位包含块会随 transform 变化,顶部阴影会错位/消失。
215
+ const scrollShadow = useScrollShadow({
216
+ target: () => getScrollAdapter()?.scrollElement ?? null,
217
+ classTarget: () => pageRoot.value?.parentElement ?? null,
218
+ getMetrics: () => readAdapterScrollMetrics(getScrollAdapter()),
219
+ onScroll: subscribeAdapterScroll
220
+ })
221
+
222
+ /**
223
+ * 同步主滚动容器的注册方式:
224
+ * - 默认路径:保持 `#W-<cacheName>` 选择器,行为与旧版本一致;
225
+ * - 自定义路径:改用适配器读写位置,跳过 wrapper 的注册。
226
+ */
227
+ const syncScrollRegistration = (): void => {
228
+ if (hasCustomScrollContainer()) {
229
+ setPageScrollAdapter(cacheName, () => customScrollAdapter.value)
230
+ return
231
+ }
232
+ addPageScroller(cacheName, `#W-${cacheName}`)
233
+ setPageScrollAdapter(cacheName, null)
234
+ }
235
+
166
236
  let refreshDisposed = false
167
237
  let isFlushingRefresh = false
168
238
  let queuedRefreshVersion: number | null = null
@@ -309,11 +379,22 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
309
379
 
310
380
  onMounted(() => {
311
381
  context.emit('onLoaded')
312
- addPageScroller(cacheName, `#W-${cacheName}`)
382
+ syncScrollRegistration()
383
+ syncScrollAdapter()
313
384
  // 新页面经 Transition(out-in) 挂载时,setup 后的 microtask 可能早于 ref 赋值;
314
385
  // 挂载后重新 sync,确保 scroll shadow 监听与阴影类目标正确绑定。
315
- scrollShadow.sync()
386
+ scrollShadow.sync(true)
316
387
  checkAndConsumeBackParams()
388
+
389
+ // 自定义滚动组件可能异步加载(异步组件 / Suspense),首帧尚未 expose。
390
+ // 下一 tick 再解析一次;首次解析成功时补一次位置恢复。
391
+ void nextTick(() => {
392
+ if (refreshDisposed) return
393
+ const wasResolved = Boolean(customScrollAdapter.value)
394
+ syncScrollAdapter()
395
+ if (!wasResolved && customScrollAdapter.value) restoreScroller(cacheName)
396
+ scrollShadow.sync(true)
397
+ })
317
398
  })
318
399
  onDeactivated(() => {
319
400
  scrollShadow.clear()
@@ -321,8 +402,9 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
321
402
  })
322
403
  onActivated(() => {
323
404
  context.emit('onLoaded')
405
+ syncScrollAdapter()
324
406
  restoreScroller(cacheName)
325
- scrollShadow.sync()
407
+ scrollShadow.sync(true)
326
408
  evictMarkedCaches()
327
409
  clearTabEvictionMarks()
328
410
  checkAndConsumeBackParams()
@@ -336,12 +418,21 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
336
418
  })
337
419
  onUnmounted(() => removeScroller(cacheName))
338
420
 
339
- // 阴影类挂到不被刷新动画 transform 的 panel 上;滚动监听仍绑定 wrapper。
340
- // 否则 ::after 的绝对定位包含块会随 transform 变化,顶部阴影会错位/消失。
341
- const scrollShadow = useScrollShadow({
342
- target: pageRoot,
343
- classTarget: () => pageRoot.value?.parentElement ?? null
344
- })
421
+ // 运行时切换滚动组件:只重新解析适配器并重新同步阴影,不重建缓存页。
422
+ watch(
423
+ () => runtimeContext.scrollContainer.value,
424
+ () => {
425
+ // 切换前保存旧容器的主滚动位置,切换后由新容器 / 默认 wrapper 恢复。
426
+ saveScroller(cacheName)
427
+ void nextTick(() => {
428
+ if (refreshDisposed) return
429
+ syncScrollRegistration()
430
+ syncScrollAdapter()
431
+ restoreScroller(cacheName)
432
+ scrollShadow.sync(true)
433
+ })
434
+ }
435
+ )
345
436
 
346
437
  return () => {
347
438
  const vnode = props.vnode
@@ -356,24 +447,33 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
356
447
  lastBackParams.value = backParams
357
448
  }
358
449
 
450
+ const scrollContainer = runtimeContext.scrollContainer.value
451
+ const pageContent = lastCloned.value ? (
452
+ <PageShell
453
+ key={`${cacheName}-${renderedRefreshVersion.value}`}
454
+ vnode={lastCloned.value}
455
+ />
456
+ ) : null
457
+
359
458
  return (
360
459
  <div
361
460
  ref={pageRoot}
362
461
  class="stack-tab__cache-page-wrapper"
363
462
  id={`W-${cacheName}`}
364
463
  style={[
464
+ // 自定义滚动容器优先:wrapper 不再承担滚动,避免出现两层滚动条。
365
465
  // 动画作用于 wrapper 本身,页面内容不会相对滚动容器位移,
366
466
  // 因此无需临时隐藏 overflow,避免动画期间滚动条消失导致内容宽度跳动。
367
- runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
467
+ !scrollContainer && runtimeContext.useGlobalScroll.value
468
+ ? 'overflow:auto'
469
+ : 'overflow:hidden',
368
470
  'height: 100%'
369
471
  ]}
370
472
  >
371
- {lastCloned.value ? (
372
- <PageShell
373
- key={`${cacheName}-${renderedRefreshVersion.value}`}
374
- vnode={lastCloned.value}
375
- />
376
- ) : null}
473
+ {scrollContainer
474
+ ? h(scrollContainer, { ref: scrollContainerRef }, { default: () => pageContent })
475
+ : pageContent}
476
+ {/* PageLoading 必须在滚动容器外层:它是覆盖整个内容区的遮罩。 */}
377
477
  <PageLoading tabId={tabInfo.id!} />
378
478
  </div>
379
479
  )
@@ -6,14 +6,33 @@
6
6
  * 避免每次 deactivate 都全量 querySelectorAll。
7
7
  */
8
8
  import type { StackTabsRuntimeContext } from '../stackTabsContext'
9
+ import {
10
+ readAdapterScrollPosition,
11
+ writeAdapterScrollPosition,
12
+ type StackTabsScrollAdapter
13
+ } from '../../model/ScrollAdapter'
9
14
 
10
15
  export interface TabPanelScrollApi {
11
16
  restoreScroller: (pageCacheId: string) => void
12
17
  saveScroller: (pageCacheId: string) => void
13
18
  removeScroller: (pageCacheId: string) => void
14
19
  addPageScroller: (pageCacheId: string, ...selectorIds: string[]) => void
20
+ /**
21
+ * 注册页面的自定义滚动适配器。
22
+ *
23
+ * 传 null 表示该页面回退到默认 wrapper(`#W-<cacheName>`)路径。
24
+ * getter 而不是适配器实例:运行时切换 `scrollContainer` 时,
25
+ * 位置保存/恢复总是读取最新的适配器。
26
+ */
27
+ setPageScrollAdapter: (
28
+ pageCacheId: string,
29
+ getter: (() => StackTabsScrollAdapter | null) | null
30
+ ) => void
15
31
  }
16
32
 
33
+ /** 自定义滚动容器对应的主滚动位置 key;无自定义容器时仍使用 `#W-<cacheName>`。 */
34
+ const MAIN_SCROLLER_KEY = '__stack-tab-main-scroller__'
35
+
17
36
  const resolveScrollElement = (selector: string): HTMLElement | null =>
18
37
  selector.startsWith('#')
19
38
  ? document.getElementById(selector.slice(1))
@@ -88,12 +107,20 @@ const resolveAutoScrollElement = (pageCacheId: string, key: string): HTMLElement
88
107
 
89
108
  export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanelScrollApi => {
90
109
  const { scrollPositionsByPageId } = context
110
+ /** 已注册自定义滚动容器的页面;值为读取最新适配器的 getter。 */
111
+ const adapterGetters = new Map<string, () => StackTabsScrollAdapter | null>()
112
+
113
+ const getPageAdapter = (pageCacheId: string): StackTabsScrollAdapter | null =>
114
+ adapterGetters.get(pageCacheId)?.() ?? null
91
115
 
92
116
  const restoreScroller = (pageCacheId: string): void => {
93
117
  const positions = scrollPositionsByPageId.get(pageCacheId)
94
118
  if (!positions) return
95
119
 
120
+ // 先恢复页面内其它滚动元素,最后恢复主滚动容器:
121
+ // 运行时切换滚动组件时,普通 key 可能仍指向旧 wrapper,最后写入主位置可避免被覆盖。
96
122
  for (const [selector, position] of positions) {
123
+ if (selector === MAIN_SCROLLER_KEY) continue
97
124
  const element = selector.startsWith(AUTO_SCROLLER_PREFIX)
98
125
  ? resolveAutoScrollElement(pageCacheId, selector)
99
126
  : resolveScrollElement(selector)
@@ -102,12 +129,31 @@ export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanel
102
129
  element.scrollLeft = position.left
103
130
  }
104
131
  }
132
+
133
+ const mainPosition = positions.get(MAIN_SCROLLER_KEY)
134
+ if (!mainPosition) return
135
+ const adapter = getPageAdapter(pageCacheId)
136
+ if (adapter && writeAdapterScrollPosition(adapter, mainPosition)) return
137
+ // 自定义组件被移除(切换回默认路径)或适配器暂时不可用时回退 wrapper。
138
+ const wrapper = document.getElementById(`W-${pageCacheId}`)
139
+ if (wrapper) {
140
+ wrapper.scrollTop = mainPosition.top
141
+ wrapper.scrollLeft = mainPosition.left
142
+ }
105
143
  }
106
144
 
107
145
  const saveScroller = (pageCacheId: string): void => {
108
146
  const positions = scrollPositionsByPageId.get(pageCacheId)
109
147
  if (!positions) return
110
148
 
149
+ // 自定义滚动容器存在时,主滚动位置由适配器读写;wrapper 不再承载滚动,
150
+ // 必须跳过 `#W-...` 的 DOM 读取,否则会把 0 写入位置缓存。
151
+ if (adapterGetters.has(pageCacheId)) {
152
+ positions.delete(`#W-${pageCacheId}`)
153
+ const position = readAdapterScrollPosition(getPageAdapter(pageCacheId))
154
+ if (position) positions.set(MAIN_SCROLLER_KEY, position)
155
+ }
156
+
111
157
  // 与当前页面 DOM 同步自动标记的容器;移除的元素不保留过期位置。
112
158
  for (const key of positions.keys()) {
113
159
  if (key.startsWith(AUTO_SCROLLER_PREFIX)) positions.delete(key)
@@ -122,8 +168,8 @@ export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanel
122
168
  })
123
169
 
124
170
  for (const selector of positions.keys()) {
125
- // auto key 已在上一步按 DOM 顺序写入,无需再次解析。
126
- if (selector.startsWith(AUTO_SCROLLER_PREFIX)) continue
171
+ // auto key 已在上一步按 DOM 顺序写入,主滚动位置由适配器维护,均无需再次解析。
172
+ if (selector.startsWith(AUTO_SCROLLER_PREFIX) || selector === MAIN_SCROLLER_KEY) continue
127
173
  const element = resolveScrollElement(selector)
128
174
  positions.set(selector, {
129
175
  top: element?.scrollTop ?? 0,
@@ -138,6 +184,7 @@ export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanel
138
184
  positions.clear()
139
185
  scrollPositionsByPageId.delete(pageCacheId)
140
186
  }
187
+ adapterGetters.delete(pageCacheId)
141
188
  // 同步卸载 MutationObserver,避免页面被驱逐后 observer 仍持有 DOM 引用。
142
189
  const cache = autoScrollCaches.get(pageCacheId)
143
190
  if (cache) {
@@ -156,10 +203,29 @@ export const createTabPanelScroll = (context: StackTabsRuntimeContext): TabPanel
156
203
  }
157
204
  }
158
205
 
206
+ const setPageScrollAdapter = (
207
+ pageCacheId: string,
208
+ getter: (() => StackTabsScrollAdapter | null) | null
209
+ ): void => {
210
+ if (!getter) {
211
+ adapterGetters.delete(pageCacheId)
212
+ return
213
+ }
214
+ adapterGetters.set(pageCacheId, getter)
215
+ if (!scrollPositionsByPageId.has(pageCacheId)) {
216
+ scrollPositionsByPageId.set(pageCacheId, new Map())
217
+ }
218
+ const positions = scrollPositionsByPageId.get(pageCacheId)!
219
+ if (!positions.has(MAIN_SCROLLER_KEY)) {
220
+ positions.set(MAIN_SCROLLER_KEY, { top: 0, left: 0 })
221
+ }
222
+ }
223
+
159
224
  return {
160
225
  restoreScroller,
161
226
  saveScroller,
162
227
  removeScroller,
163
- addPageScroller
228
+ addPageScroller,
229
+ setPageScrollAdapter
164
230
  }
165
231
  }
@@ -43,11 +43,25 @@ export interface UseScrollShadowOptions {
43
43
  * 距离边界 ≤ 阈值视为已贴边(不显示阴影);距离边界 > 阈值视为可继续滚动(显示阴影)。
44
44
  */
45
45
  edgeThreshold?: number
46
+ /**
47
+ * 自定义尺寸读取(可选)。提供时优先于 `target` 的原生
48
+ * `scrollTop / scrollHeight / clientHeight`,用于命令式滚动组件。
49
+ */
50
+ getMetrics?: () => { scrollTop: number; scrollHeight: number; clientHeight: number } | null
51
+ /**
52
+ * 自定义滚动订阅(可选)。提供时优先于在 `target` 上监听 scroll 事件,
53
+ * 返回取消订阅函数。`target` 可以为 null(例如纯 transform 驱动的滚动组件)。
54
+ */
55
+ onScroll?: (listener: () => void) => () => void
46
56
  }
47
57
 
48
58
  export interface UseScrollShadowReturn {
49
- /** 重新解析滚动/阴影目标并同步一次(用于 keep-alive 激活、DOM 重新挂载后) */
50
- sync: () => void
59
+ /**
60
+ * 重新解析滚动/阴影目标并同步一次(用于 keep-alive 激活、DOM 重新挂载后)。
61
+ * `force` 为 true 时即使 target 未变化也重建订阅,用于适配器内部状态
62
+ * (自定义滚动元素的 expose)在两次 sync 之间发生变化的场景。
63
+ */
64
+ sync: (force?: boolean) => void
51
65
  /** 移除阴影类并取消待执行的 rAF(用于 keep-alive 失活) */
52
66
  clear: () => void
53
67
  }
@@ -65,6 +79,8 @@ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShado
65
79
  const edgeThreshold = Math.max(0, options.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)
66
80
  let rafId: number | null = null
67
81
  let attached: HTMLElement | null = null
82
+ let attachedOnScroll: ((listener: () => void) => () => void) | null = null
83
+ let scrollCleanup: (() => void) | null = null
68
84
  let classElement: HTMLElement | null = null
69
85
  let resizeObserver: ResizeObserver | null = null
70
86
 
@@ -78,8 +94,8 @@ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShado
78
94
  options.classTarget ? resolveTarget(options.classTarget) : attached
79
95
 
80
96
  const recompute = (): void => {
81
- if (!attached) return
82
- const target = attached
97
+ // 命令式滚动组件可能没有原生滚动元素,但通过 getMetrics 提供尺寸。
98
+ if (!attached && !options.getMetrics) return
83
99
  const nextClassTarget = resolveClassTarget()
84
100
  if (classElement && classElement !== nextClassTarget) {
85
101
  clearClasses(classElement)
@@ -88,7 +104,19 @@ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShado
88
104
  classElement = nextClassTarget
89
105
  if (!classElement) return
90
106
 
91
- const { scrollTop, scrollHeight, clientHeight } = target
107
+ // 自定义尺寸优先;返回 null 时回退到原生滚动元素。
108
+ const metrics =
109
+ options.getMetrics?.() ??
110
+ (attached
111
+ ? {
112
+ scrollTop: attached.scrollTop,
113
+ scrollHeight: attached.scrollHeight,
114
+ clientHeight: attached.clientHeight
115
+ }
116
+ : null)
117
+ if (!metrics) return
118
+
119
+ const { scrollTop, scrollHeight, clientHeight } = metrics
92
120
  const distanceToTop = scrollTop
93
121
  const distanceToBottom = scrollHeight - clientHeight - scrollTop
94
122
  // 顶部阴影:表示上方还有内容,离开顶部(距离顶部 > 阈值)时显示。
@@ -111,23 +139,40 @@ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShado
111
139
  })
112
140
  }
113
141
 
114
- const attach = (): void => {
142
+ /** 释放滚动事件订阅 / ResizeObserver,不清理阴影类。 */
143
+ const releaseScrollSource = (): void => {
144
+ if (attached) attached.removeEventListener('scroll', schedule)
145
+ if (scrollCleanup) {
146
+ scrollCleanup()
147
+ scrollCleanup = null
148
+ }
149
+ attachedOnScroll = null
150
+ resizeObserver?.disconnect()
151
+ resizeObserver = null
152
+ }
153
+
154
+ const attach = (force = false): void => {
115
155
  const el = resolveTarget(options.target)
116
- if (!el) return
117
- if (el === attached) {
156
+ const onScroll = options.onScroll ?? null
157
+ if (!el && !onScroll && !options.getMetrics) return
158
+ if (!force && el === attached && onScroll === attachedOnScroll) {
118
159
  schedule()
119
160
  return
120
161
  }
121
- if (attached) {
122
- attached.removeEventListener('scroll', schedule)
123
- resizeObserver?.disconnect()
124
- resizeObserver = null
125
- clearClasses(classElement)
126
- classElement = null
127
- }
162
+ releaseScrollSource()
163
+ clearClasses(classElement)
164
+ classElement = null
165
+
128
166
  attached = el
129
- el.addEventListener('scroll', schedule, { passive: true })
130
- if (watchResize && typeof ResizeObserver !== 'undefined') {
167
+ if (onScroll) {
168
+ // 自定义滚动订阅优先于原生 scroll 事件;没有原生滚动元素时也能工作。
169
+ attachedOnScroll = onScroll
170
+ const cleanup = onScroll(schedule)
171
+ scrollCleanup = typeof cleanup === 'function' ? cleanup : null
172
+ } else if (el) {
173
+ el.addEventListener('scroll', schedule, { passive: true })
174
+ }
175
+ if (watchResize && el && typeof ResizeObserver !== 'undefined') {
131
176
  resizeObserver = new ResizeObserver(() => schedule())
132
177
  resizeObserver.observe(el)
133
178
  }
@@ -147,24 +192,23 @@ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShado
147
192
  }
148
193
 
149
194
  /** 重新解析目标:keep-alive 激活或 DOM 重新挂载后由调用方触发 */
150
- const sync = (): void => {
195
+ const sync = (force = false): void => {
151
196
  const el = resolveTarget(options.target)
152
- if (!el) {
153
- clear()
197
+ const onScroll = options.onScroll ?? null
198
+ if (!el && !onScroll && !options.getMetrics) {
199
+ detach()
154
200
  return
155
201
  }
156
- if (el !== attached) {
157
- attach()
202
+ if (force || el !== attached || onScroll !== attachedOnScroll) {
203
+ attach(force)
158
204
  return
159
205
  }
160
206
  schedule()
161
207
  }
162
208
 
163
209
  const detach = (): void => {
164
- if (attached) attached.removeEventListener('scroll', schedule)
210
+ releaseScrollSource()
165
211
  attached = null
166
- resizeObserver?.disconnect()
167
- resizeObserver = null
168
212
  clearClasses(classElement)
169
213
  classElement = null
170
214
  cancelScheduled()
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { ITabBase, ITabData, ITabItem, ITabPage } from '../model/TabModel'
15
15
  import { computed, ref } from 'vue'
16
- import type { DefineComponent, VNode } from 'vue'
16
+ import type { Component, DefineComponent, VNode } from 'vue'
17
17
  import { isNavigationFailure, useRouter } from 'vue-router'
18
18
  import type { RouteLocationNormalizedLoaded } from 'vue-router'
19
19
  import { encodeTabInfo, createId, createPageId, decodeTabInfo } from '../utils/tabInfoEncoder'
@@ -108,7 +108,7 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
108
108
  restoreCaches,
109
109
  reset: resetStore
110
110
  } = store
111
- const { restoreScroller, saveScroller, removeScroller, addPageScroller } =
111
+ const { restoreScroller, saveScroller, removeScroller, addPageScroller, setPageScrollAdapter } =
112
112
  createTabPanelScroll(runtimeContext)
113
113
  const {
114
114
  saveActiveTabToSession,
@@ -246,6 +246,7 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
246
246
  evictMarkedCaches,
247
247
  clearTabEvictionMarks,
248
248
  addPageScroller,
249
+ setPageScrollAdapter,
249
250
  saveScroller,
250
251
  restoreScroller,
251
252
  removeScroller
@@ -813,6 +814,9 @@ const createTabPanelApi = (runtimeContext: StackTabsRuntimeContext) => {
813
814
  setGlobalScroll: (value: boolean) => {
814
815
  runtimeContext.useGlobalScroll.value = value
815
816
  },
817
+ setScrollContainer: (value: Component | null | undefined) => {
818
+ runtimeContext.scrollContainer.value = value ?? null
819
+ },
816
820
  clearSession,
817
821
  setSessionPrefix: (value: string) => {
818
822
  runtimeContext.sessionPrefix.value = value
package/src/lib/index.ts CHANGED
@@ -37,6 +37,12 @@ export {
37
37
  }
38
38
  export { postOpenTab, onRefreshRequest, MSG_REFRESH, MSG_OPEN_TAB } from './utils/iframeBridge'
39
39
  export type { IframeOpenTabPayload } from './utils/iframeBridge'
40
+ export type {
41
+ StackTabsScrollAdapter,
42
+ StackTabsScrollElementInstance,
43
+ StackTabsScrollMetrics,
44
+ StackTabsScrollPosition
45
+ } from './model/ScrollAdapter'
40
46
  export default {
41
47
  install(Vue: App, options?: VueStackTabsPluginOptions): void {
42
48
  logVersion(import.meta.env.PACKAGE_VERSION)