vue-stack-tabs 2.2.0 → 2.3.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.
Files changed (42) hide show
  1. package/dist/StackTabs.vue.d.ts +2 -2
  2. package/dist/components/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -2
  3. package/dist/hooks/stackTabsContext.d.ts +0 -1
  4. package/dist/hooks/tabPanel/evict.d.ts +2 -2
  5. package/dist/hooks/tabPanel/scroll.d.ts +0 -2
  6. package/dist/hooks/tabPanel/state.d.ts +0 -1
  7. package/dist/hooks/useIframeManager.d.ts +1 -1
  8. package/dist/hooks/useScrollShadow.d.ts +11 -2
  9. package/dist/hooks/useTabEventBus.d.ts +5 -0
  10. package/dist/model/TabModel.d.ts +12 -5
  11. package/dist/nuxt/module.mjs +1 -1
  12. package/dist/nuxt/runtime/plugin.mjs +8 -1598
  13. package/dist/style.css +1 -1
  14. package/dist/utils/scrollUtils.d.ts +0 -2
  15. package/dist/utils/tabInfoEncoder.d.ts +0 -2
  16. package/dist/utils/urlParser.d.ts +6 -7
  17. package/dist/vue-stack-tabs.es.js +1 -1
  18. package/package.json +1 -1
  19. package/src/lib/StackTabs.vue +16 -1
  20. package/src/lib/assets/style/stack-tab.scss +12 -6
  21. package/src/lib/components/PageLoading.vue +10 -9
  22. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +1 -5
  23. package/src/lib/components/TabHeader/TabHeaderScroll.vue +164 -90
  24. package/src/lib/components/TabHeader/index.vue +51 -10
  25. package/src/lib/hooks/stackTabsContext.ts +0 -1
  26. package/src/lib/hooks/tabPanel/evict.ts +14 -6
  27. package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +211 -42
  28. package/src/lib/hooks/tabPanel/scroll.ts +4 -15
  29. package/src/lib/hooks/tabPanel/session.ts +11 -4
  30. package/src/lib/hooks/tabPanel/state.ts +0 -2
  31. package/src/lib/hooks/tabPanel/tabStore.ts +28 -10
  32. package/src/lib/hooks/useIframeManager.ts +11 -15
  33. package/src/lib/hooks/useScrollShadow.ts +74 -19
  34. package/src/lib/hooks/useTabActions.ts +10 -4
  35. package/src/lib/hooks/useTabEventBus.ts +3 -0
  36. package/src/lib/hooks/useTabPanel.tsx +46 -34
  37. package/src/lib/model/TabModel.ts +14 -5
  38. package/src/lib/nuxt/module.ts +3 -1
  39. package/src/lib/nuxt/runtime/plugin.ts +7 -3
  40. package/src/lib/utils/scrollUtils.ts +0 -18
  41. package/src/lib/utils/tabInfoEncoder.ts +21 -2
  42. package/src/lib/utils/urlParser.ts +40 -37
@@ -2,6 +2,10 @@
2
2
  TabHeaderScroll - 可横向滚动的标签栏容器
3
3
 
4
4
  职责:左右箭头滚动、滚轮滚动、自定义滚动条拖拽、确保激活标签在可视区域内
5
+
6
+ 滚动条是自绘的:原生滚动条隐藏,滚动位置通过 scrollLeft 驱动。
7
+ 滚动过程中滑块位置直接写 DOM(不经过响应式),避免每帧触发组件重渲染、
8
+ 进而重新执行 slot 并 patch 整个标签列表。
5
9
  -->
6
10
  <template>
7
11
  <tab-header-button
@@ -35,11 +39,11 @@
35
39
  ref="thumb"
36
40
  class="stack-tab__scrollbar-thumb"
37
41
  :class="{ 'stack-tab__scrollbar-thumb--dragging': scrollDragging }"
38
- :style="{
39
- width: `${thumbWidth}px`,
40
- transform: `translateX(${thumbLeft}px)`
41
- }"
42
- @mousedown.prevent="handleScrollbarDragStart"
42
+ @pointerdown.prevent="handleScrollbarDragStart"
43
+ @pointermove="handleScrollbarDragMove"
44
+ @pointerup="handleScrollbarDragEnd"
45
+ @pointercancel="handleScrollbarDragEnd"
46
+ @lostpointercapture="handleScrollbarDragEnd"
43
47
  />
44
48
  </div>
45
49
  </div>
@@ -55,8 +59,8 @@
55
59
 
56
60
  <script lang="ts" setup>
57
61
  import { useI18n } from 'vue-i18n-lite'
58
- import { computed, ref, reactive, onMounted, watch, onUnmounted } from 'vue'
59
- import type { ScrollData, DragData } from '../../model/TabModel'
62
+ import { ref, onMounted, watch, onUnmounted } from 'vue'
63
+ import type { ScrollData } from '../../model/TabModel'
60
64
  import {
61
65
  scrollTo as scrollToUtil,
62
66
  scrollIntoView as scrollIntoViewUtil
@@ -79,6 +83,8 @@ const isDisabledLeftButton = ref<boolean>(false)
79
83
  const isDisabledRightButton = ref<boolean>(false)
80
84
  /** 是否显示左右滚动按钮:必须启用按钮模式且内容横向溢出 */
81
85
  const isScrollButtonVisible = ref<boolean>(false)
86
+ /** 内容是否超出容器宽度,需要显示滚动条 */
87
+ const hasScroller = ref<boolean>(false)
82
88
  /** 外层滚动区域,用于 ResizeObserver */
83
89
  const headerScroll = ref<HTMLElement>()
84
90
  const props = withDefaults(
@@ -96,66 +102,84 @@ const props = withDefaults(
96
102
  isScrollButton: true
97
103
  }
98
104
  )
99
- /** 滚动容器尺寸与滚动位置,用于计算滑块和按钮状态 */
100
- const scrollData = reactive<ScrollData>({
105
+
106
+ /**
107
+ * 滚动容器的尺寸与位置。
108
+ * 刻意保持非响应式:滚动时每帧更新它,若走 reactive 会触发组件重渲染。
109
+ */
110
+ const scrollData: ScrollData = {
101
111
  clientWidth: 0,
102
112
  scrollWidth: 0,
103
113
  scrollLeft: 0
104
- })
105
- /** 拖拽滚动条时的起始状态 */
106
- const dragData = reactive<DragData>({
107
- thumbLeft: 0,
108
- startScrollLeft: 0,
109
- startThumbLeft: 0,
110
- startPageX: 0
111
- })
112
-
113
- /** 滚动数据变化时更新箭头与滚动条的启用状态 */
114
- watch(scrollData, (data) => {
115
- updateScrollButtonState(data)
116
- })
117
-
118
- watch(
119
- () => props.isScrollButton,
120
- () => updateScrollButtonState(scrollData)
121
- )
122
-
123
- /** 根据滚动位置更新左右箭头禁用状态及滚动按钮可见性 */
124
- const updateScrollButtonState = (data: ScrollData) => {
125
- const hasHorizontalOverflow = data.scrollWidth > data.clientWidth
126
-
127
- isDisabledLeftButton.value = data.scrollLeft <= 10
128
- isDisabledRightButton.value = data.clientWidth + data.scrollLeft - data.scrollWidth >= -10
129
- isScrollButtonVisible.value = props.isScrollButton && hasHorizontalOverflow
130
114
  }
131
115
 
132
- /** 内容是否超出容器宽度,需要显示滚动条 */
133
- const hasScroller = computed<boolean>(() => {
134
- return scrollData.scrollWidth > scrollData.clientWidth
135
- })
116
+ /** 拖拽起始状态(非响应式,避免 pointermove 触发渲染) */
117
+ const dragData = {
118
+ startPageX: 0,
119
+ startThumbLeft: 0
120
+ }
121
+ /** 拖拽指针位置与 rAF 句柄,用于合并高频 pointermove */
122
+ let dragRafId = 0
123
+ let pendingPageX = 0
124
+ let capturedPointerId: number | null = null
136
125
 
137
126
  /** 滚动条滑块宽度(按内容比例计算) */
138
- const thumbWidth = computed<number>(() => {
139
- if (!hasScroller.value) return 0
140
-
127
+ const getThumbWidth = (): number => {
141
128
  const { clientWidth, scrollWidth } = scrollData
129
+ if (scrollWidth <= clientWidth || scrollWidth === 0) return 0
130
+
142
131
  return (clientWidth / scrollWidth) * clientWidth
143
- })
132
+ }
144
133
 
145
134
  /** 滚动条滑块 left 偏移(与 scrollLeft 对应) */
146
- const thumbLeft = computed(() => {
147
- if (!hasScroller.value) return 0
135
+ const getThumbLeft = (scrollLeft: number): number => {
136
+ const { clientWidth, scrollWidth } = scrollData
137
+ const maxScrollLeft = scrollWidth - clientWidth
138
+ if (maxScrollLeft <= 0) return 0
148
139
 
149
- const { clientWidth, scrollWidth, scrollLeft } = scrollData
140
+ return (clientWidth - getThumbWidth()) * (scrollLeft / maxScrollLeft)
141
+ }
150
142
 
151
- return (clientWidth - thumbWidth.value) * (scrollLeft / (scrollWidth - clientWidth))
152
- })
143
+ /**
144
+ * 直接写滑块样式,绕过模板绑定。
145
+ * 这是滚动路径上唯一的 DOM 写入,不触发组件重渲染。
146
+ */
147
+ const updateThumb = (scrollLeft: number) => {
148
+ const thumbEl = thumb.value
149
+ if (!thumbEl || !hasScroller.value) return
150
+
151
+ thumbEl.style.width = `${getThumbWidth()}px`
152
+ thumbEl.style.transform = `translateX(${getThumbLeft(scrollLeft)}px)`
153
+ }
154
+
155
+ /** 根据滚动位置更新左右箭头禁用状态 */
156
+ const updateScrollButtonState = () => {
157
+ isDisabledLeftButton.value = scrollData.scrollLeft <= 10
158
+ isDisabledRightButton.value =
159
+ scrollData.clientWidth + scrollData.scrollLeft - scrollData.scrollWidth >= -10
160
+ }
161
+
162
+ /** 更新溢出判断与滚动按钮可见性 */
163
+ const updateScrollVisibility = () => {
164
+ hasScroller.value = scrollData.scrollWidth > scrollData.clientWidth
165
+ isScrollButtonVisible.value = props.isScrollButton && hasScroller.value
166
+ }
153
167
 
154
- /** 更新滚动条数据 */
168
+ /**
169
+ * 同步容器尺寸与滚动位置。
170
+ * scroll 与 ResizeObserver 共用:尺寸只在滚动时顺带读取,不写入响应式状态,
171
+ * 因此不会触发组件重渲染。
172
+ */
155
173
  const updateScrollState = () => {
156
- if (!container.value) return
157
- const { clientWidth, scrollWidth, scrollLeft } = container.value
158
- Object.assign(scrollData, { clientWidth, scrollWidth, scrollLeft })
174
+ const el = container.value
175
+ if (!el) return
176
+
177
+ scrollData.clientWidth = el.clientWidth
178
+ scrollData.scrollWidth = el.scrollWidth
179
+ scrollData.scrollLeft = el.scrollLeft
180
+ updateScrollVisibility()
181
+ updateScrollButtonState()
182
+ updateThumb(scrollData.scrollLeft)
159
183
  }
160
184
 
161
185
  /** 确保激活的标签在可视区域内 */
@@ -170,6 +194,8 @@ const ensureActiveTabInView = () => {
170
194
 
171
195
  let scrollContainerResizeObserver: ResizeObserver | null = null
172
196
  let tabListResizeObserver: ResizeObserver | null = null
197
+ /** 上一次观察到的标签数量,用于区分“标签增删”和普通的尺寸抖动 */
198
+ let lastTabCount = 0
173
199
  /** 滚动到指定 left 位置 */
174
200
  const scrollTo = (left: number, smooth = true) => {
175
201
  scrollToUtil({ wrap: container.value, left, top: 0, smooth })
@@ -181,62 +207,103 @@ const handleScrollButtonClick = (delta: number) => {
181
207
 
182
208
  const handleWheelScroll = (e: WheelEvent) => {
183
209
  if (!props.isScrollWheel || !container.value) return
210
+ // 触控板横向手势交给原生滚动,避免与下面的定距滚动叠加
211
+ if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return
212
+ if (!e.deltaY) return
184
213
 
185
214
  const isScrollUp = e.deltaY < 0
186
215
  scrollTo(container.value.scrollLeft + (isScrollUp ? -props.space : props.space))
187
216
  }
188
217
 
189
- const handleScrollbarDragStart = (e: MouseEvent) => {
190
- Object.assign(dragData, {
191
- startPageX: e.pageX,
192
- startScrollLeft: container.value?.scrollLeft,
193
- startThumbLeft: thumbLeft.value,
194
- thumbLeft: thumbLeft.value
218
+ /** 应用一次拖拽位移(在 rAF 中执行,合并高频 pointermove) */
219
+ const applyDrag = () => {
220
+ dragRafId = 0
221
+ if (!scrollDragging.value || !container.value) return
222
+
223
+ const { clientWidth, scrollWidth } = scrollData
224
+ const maxScrollLeft = scrollWidth - clientWidth
225
+ const maxThumbLeft = clientWidth - getThumbWidth()
226
+ if (maxScrollLeft <= 0 || maxThumbLeft <= 0) return
227
+
228
+ const thumbLeft = Math.min(
229
+ Math.max(dragData.startThumbLeft + pendingPageX - dragData.startPageX, 0),
230
+ maxThumbLeft
231
+ )
232
+
233
+ // 同步滚动与滑块位置,拖拽跟手,不依赖异步 scroll 事件
234
+ scrollToUtil({
235
+ wrap: container.value,
236
+ left: (thumbLeft * maxScrollLeft) / maxThumbLeft,
237
+ top: 0,
238
+ smooth: false
195
239
  })
240
+ if (thumb.value) {
241
+ thumb.value.style.transform = `translateX(${thumbLeft}px)`
242
+ }
243
+ }
244
+
245
+ const handleScrollbarDragStart = (e: PointerEvent) => {
246
+ const thumbEl = thumb.value
247
+ const isPrimaryButton = e.pointerType !== 'mouse' || e.button === 0
248
+ if (!thumbEl || !isPrimaryButton) return
249
+
250
+ const scrollLeft = container.value?.scrollLeft ?? 0
251
+ dragData.startPageX = e.pageX
252
+ dragData.startThumbLeft = getThumbLeft(scrollLeft)
253
+ pendingPageX = e.pageX
196
254
  scrollDragging.value = true
197
- document.addEventListener('mousemove', handleScrollbarDragMove)
198
- document.addEventListener('mouseup', handleScrollbarDragEnd)
255
+ capturedPointerId = e.pointerId
256
+ thumbEl.setPointerCapture?.(e.pointerId)
199
257
  }
200
258
 
201
- const handleScrollbarDragMove = (e: MouseEvent) => {
202
- const { clientWidth, scrollWidth } = scrollData
203
- let thumbLeft = dragData.startThumbLeft + e.pageX - dragData.startPageX
204
- const maxThumbLeft = clientWidth - thumbWidth.value
259
+ const handleScrollbarDragMove = (e: PointerEvent) => {
260
+ if (!scrollDragging.value) return
205
261
 
206
- if (thumbLeft < 0) {
207
- thumbLeft = 0
208
- } else if (thumbLeft > maxThumbLeft) {
209
- thumbLeft = maxThumbLeft
210
- }
211
- // 更新滑块定位
212
- dragData.thumbLeft = thumbLeft
213
- // 滚动
214
- scrollTo((thumbLeft * (scrollWidth - clientWidth)) / (clientWidth - thumbWidth.value), false)
215
- e.preventDefault()
262
+ pendingPageX = e.pageX
263
+ if (dragRafId) return
264
+ dragRafId = requestAnimationFrame(applyDrag)
216
265
  }
217
266
 
218
267
  const handleScrollbarDragEnd = () => {
268
+ if (dragRafId) {
269
+ cancelAnimationFrame(dragRafId)
270
+ dragRafId = 0
271
+ }
219
272
  scrollDragging.value = false
220
- document.removeEventListener('mousemove', handleScrollbarDragMove)
221
- document.removeEventListener('mouseup', handleScrollbarDragEnd)
273
+
274
+ const pointerId = capturedPointerId
275
+ capturedPointerId = null
276
+ const thumbEl = thumb.value
277
+ if (thumbEl && pointerId !== null && thumbEl.hasPointerCapture?.(pointerId)) {
278
+ thumbEl.releasePointerCapture(pointerId)
279
+ }
222
280
  }
223
281
 
224
282
  onMounted(() => {
225
- if (typeof ResizeObserver === 'undefined') return
226
-
227
- scrollContainerResizeObserver = new ResizeObserver(() => {
228
- updateScrollState()
229
- ensureActiveTabInView()
230
- })
231
- tabListResizeObserver = new ResizeObserver(() => {
232
- // 内容溢出(存在滚动条)时才需要在标签增删后纠正滚动位置。
233
- if (hasScroller.value) ensureActiveTabInView()
234
- updateScrollState()
235
- })
283
+ if (typeof ResizeObserver !== 'undefined') {
284
+ scrollContainerResizeObserver = new ResizeObserver(() => {
285
+ updateScrollState()
286
+ ensureActiveTabInView()
287
+ })
288
+ tabListResizeObserver = new ResizeObserver(() => {
289
+ // 只有标签增删(数量变化)才需要纠正滚动位置。
290
+ // hover 展开关闭按钮、标题动态变化等也会改变列表宽度并触发本回调,
291
+ // 若此时无条件 ensureActiveTabInView,会把用户主动滚开的标签栏强行拉回 active 标签。
292
+ const tabCount = container.value?.querySelectorAll('.stack-tab__item').length ?? 0
293
+ const tabCountChanged = tabCount !== lastTabCount
294
+ lastTabCount = tabCount
295
+
296
+ if (hasScroller.value && tabCountChanged) ensureActiveTabInView()
297
+ updateScrollState()
298
+ })
299
+
300
+ if (headerScroll.value) scrollContainerResizeObserver.observe(headerScroll.value)
301
+ const tabNav = container.value?.querySelector('.stack-tab__nav')
302
+ lastTabCount = container.value?.querySelectorAll('.stack-tab__item').length ?? 0
303
+ if (tabNav) tabListResizeObserver.observe(tabNav)
304
+ }
236
305
 
237
- if (headerScroll.value) scrollContainerResizeObserver.observe(headerScroll.value)
238
- const tabNav = container.value?.querySelector('.stack-tab__nav')
239
- if (tabNav) tabListResizeObserver.observe(tabNav)
306
+ updateScrollState()
240
307
  })
241
308
  onUnmounted(() => {
242
309
  handleScrollbarDragEnd()
@@ -246,6 +313,13 @@ onUnmounted(() => {
246
313
  tabListResizeObserver = null
247
314
  })
248
315
 
316
+ watch(
317
+ () => props.isScrollButton,
318
+ () => {
319
+ isScrollButtonVisible.value = props.isScrollButton && hasScroller.value
320
+ }
321
+ )
322
+
249
323
  const scrollIntoView = (el: HTMLElement) => {
250
324
  scrollIntoViewUtil({ el, wrap: container.value, block: 'end', inline: 'center' })
251
325
  }
@@ -23,13 +23,10 @@
23
23
  <tab-header-item
24
24
  v-for="(item, index) in tabs"
25
25
  :key="item.id"
26
- :item="item as ITabItem"
26
+ :item="item"
27
27
  :drop-position="dropIndicator?.tabId === item.id ? dropIndicator.position : undefined"
28
- @contextmenu="
29
- (e: MouseEvent) =>
30
- handleTabContextMenu(e, item as unknown as ITabItem, index, tabs.length)
31
- "
32
- @click.middle.prevent="handleCloseTab(item as ITabItem)"
28
+ @contextmenu="(e: MouseEvent) => handleTabContextMenu(e, item, index, tabs.length)"
29
+ @click.middle.prevent="handleCloseTab(item)"
33
30
  @close="handleCloseTab"
34
31
  @active="handleActivateTab"
35
32
  @dragstart="handleTabDragStart"
@@ -75,7 +72,7 @@
75
72
  依赖:useTabActions(tabs/closeTab/activeTab)、useContextMenu
76
73
  -->
77
74
  <script lang="ts" setup>
78
- import { inject, computed, ref, nextTick } from 'vue'
75
+ import { inject, computed, ref, nextTick, watch, onUnmounted } from 'vue'
79
76
  import type { TransitionProps } from 'vue'
80
77
  import { TabScrollMode } from '../../model/TabModel'
81
78
  import type { IContextMenu, ITabItem } from '../../model/TabModel'
@@ -87,6 +84,7 @@ import TabHeaderScroll from './TabHeaderScroll.vue'
87
84
  import TabHeaderButton from './TabHeaderButton.vue'
88
85
  import { useI18n } from 'vue-i18n-lite'
89
86
  import { maximumKey } from '../../hooks/stackTabsContext'
87
+ import { TabEventType, useTabEmitter } from '../../hooks/useTabEventBus'
90
88
  const { t } = useI18n()
91
89
  /** 是否最大化,由上层 provide */
92
90
  const maximum = inject(maximumKey, ref(false))
@@ -121,6 +119,47 @@ const props = withDefaults(
121
119
  }
122
120
  )
123
121
  const { closeTab, activeTab, moveTab, tabs } = useTabActions()
122
+
123
+ /** 当前激活标签 id */
124
+ const activeTabId = computed<string>(() => tabs.value.find((tab) => tab.active)?.id ?? '')
125
+
126
+ /**
127
+ * 将指定标签滚动到可视区域;未指定 id 时使用当前激活标签。
128
+ * 仅在标签确实不可见时才滚动,避免干扰用户的手动滚动。
129
+ */
130
+ const ensureTabInView = (tabId?: string) => {
131
+ const scrollContainer = scrollContainerRef.value
132
+ if (!scrollContainer) return
133
+
134
+ const target = headerRef.value?.querySelector<HTMLElement>(
135
+ tabId
136
+ ? `.stack-tab__nav [role="tab"][data-tab-id="${CSS.escape(tabId)}"]`
137
+ : '.stack-tab__nav .stack-tab__item--active'
138
+ )
139
+ const tabElement = target?.closest<HTMLElement>('.stack-tab__item')
140
+ if (tabElement && !scrollContainer.isInView(tabElement)) {
141
+ scrollContainer.scrollIntoView(tabElement)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 激活标签变化后确保其位于可视区域:
147
+ * 覆盖新开标签、通过 openTab 唤醒已有标签、关闭标签后顺延激活等场景。
148
+ * 仅响应用户主动滚动时不会触发(滚动不改变 active),因此不会把视图强行拉回。
149
+ */
150
+ watch(activeTabId, () => {
151
+ nextTick(() => ensureTabInView())
152
+ })
153
+
154
+ /** 标签已是 active 但被滚开时,显式打开/激活请求仍需把标签栏滚动到它。 */
155
+ const emitter = useTabEmitter()
156
+ const handleTabScrollTo = ({ id }: { id: string }) => {
157
+ nextTick(() => ensureTabInView(id))
158
+ }
159
+ emitter.on(TabEventType.TAB_SCROLL_TO, handleTabScrollTo)
160
+ onUnmounted(() => {
161
+ emitter.off(TabEventType.TAB_SCROLL_TO, handleTabScrollTo)
162
+ })
124
163
  const draggingTabId = ref<string | null>(null)
125
164
  type DropPosition = 'before' | 'after'
126
165
  const dropIndicator = ref<{ tabId: string; position: DropPosition } | null>(null)
@@ -219,7 +258,7 @@ const handleTabListDragOver = (event: DragEvent) => {
219
258
  const handleTabListDrop = (event: DragEvent) => {
220
259
  if (isTabItemDragEvent(event)) return
221
260
  const lastTab = tabs.value[tabs.value.length - 1]
222
- if (lastTab) handleTabDrop(lastTab as ITabItem)
261
+ if (lastTab) handleTabDrop(lastTab)
223
262
  }
224
263
 
225
264
  /** 标签列表 transition 的 props,支持 string 或对象 */
@@ -248,7 +287,7 @@ const focusTabById = (tabId: string) => {
248
287
  const activateTabByIndex = (index: number) => {
249
288
  const item = tabs.value[index]
250
289
  if (!item) return
251
- handleActivateTab(item as ITabItem, undefined, true)
290
+ handleActivateTab(item, undefined, true)
252
291
  focusTabById(String(item.id))
253
292
  }
254
293
 
@@ -317,7 +356,9 @@ const handleActivateTab = (
317
356
  Promise.resolve(activeTab(item.id, isRoute))
318
357
  .then(() => emit('active', item.id))
319
358
  .catch((error: unknown) => {
320
- console.warn('[vue-stack-tabs] Failed to activate tab:', error)
359
+ if (!import.meta.env.PROD) {
360
+ console.warn('[vue-stack-tabs] Failed to activate tab:', error)
361
+ }
321
362
  })
322
363
  }
323
364
  </script>
@@ -23,7 +23,6 @@ export const STACK_TABS_DUPLICATE_INSTANCE_MESSAGE =
23
23
 
24
24
  export interface StackTabsRuntimeContext {
25
25
  tabs: Ref<ITabItem[]>
26
- defaultTabs: Ref<ITabItem[]>
27
26
  caches: ShallowRef<string[]>
28
27
  components: Map<string, DefineComponent>
29
28
  cacheIdsToEvict: Set<string>
@@ -9,8 +9,11 @@ import type { StackTabsRuntimeContext } from '../stackTabsContext'
9
9
 
10
10
  export interface TabPanelEvictionApi {
11
11
  markCacheForEviction: (cacheName: string) => void
12
- markTabPagesForEviction: (tab: { id: string; pages: { list(): ITabPage[] } }) => void
13
- markTabPagesForEvictionOnly: (tab: { pages: { list(): ITabPage[] } }) => void
12
+ markTabPagesForEviction: (tab: {
13
+ id: string
14
+ pages: { readonlyList(): readonly ITabPage[] }
15
+ }) => void
16
+ markTabPagesForEvictionOnly: (tab: { pages: { readonlyList(): readonly ITabPage[] } }) => void
14
17
  addCache: (cacheName: string) => void
15
18
  removeCache: (cacheName: string) => void
16
19
  evictPageCache: (cacheName: string) => void
@@ -39,13 +42,18 @@ export const createTabPanelEviction = (context: StackTabsRuntimeContext): TabPan
39
42
  cacheIdsToEvict.add(cacheName)
40
43
  }
41
44
 
42
- const markTabPagesForEviction = (tab: { id: string; pages: { list(): ITabPage[] } }): void => {
43
- for (const page of tab.pages.list()) markCacheForEviction(page.id)
45
+ const markTabPagesForEviction = (tab: {
46
+ id: string
47
+ pages: { readonlyList(): readonly ITabPage[] }
48
+ }): void => {
49
+ for (const page of tab.pages.readonlyList()) markCacheForEviction(page.id)
44
50
  tabIdsToEvict.add(tab.id)
45
51
  }
46
52
 
47
- const markTabPagesForEvictionOnly = (tab: { pages: { list(): ITabPage[] } }): void => {
48
- for (const page of tab.pages.list()) markCacheForEviction(page.id)
53
+ const markTabPagesForEvictionOnly = (tab: {
54
+ pages: { readonlyList(): readonly ITabPage[] }
55
+ }): void => {
56
+ for (const page of tab.pages.readonlyList()) markCacheForEviction(page.id)
49
57
  }
50
58
 
51
59
  const addCache = (cacheName: string): void => {