vue3-router-tab 1.3.5 → 1.3.7
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 +529 -47
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +747 -649
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +183 -56
- package/lib/core/createRouterTabs.ts +170 -27
- package/lib/core/types.ts +5 -0
- package/lib/scss/index.scss +22 -1
- package/package.json +1 -1
|
@@ -117,14 +117,33 @@ function insertTab(tabs: TabRecord[], tab: TabRecord, position: 'last' | 'next',
|
|
|
117
117
|
tabs.push(tab)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Enforces the maximum number of alive (cached) tabs.
|
|
122
|
+
* When exceeded, removes the oldest tabs from KeepAlive cache.
|
|
123
|
+
*/
|
|
124
|
+
function enforceMaxAlive(
|
|
125
|
+
tabs: TabRecord[],
|
|
126
|
+
maxAlive: number,
|
|
127
|
+
activeId: string | null,
|
|
128
|
+
aliveCache: Set<string>
|
|
129
|
+
) {
|
|
121
130
|
if (!maxAlive || maxAlive <= 0) return
|
|
131
|
+
|
|
122
132
|
const aliveTabs = tabs.filter(tab => tab.alive)
|
|
133
|
+
|
|
123
134
|
while (aliveTabs.length > maxAlive) {
|
|
124
135
|
const candidate = aliveTabs.shift()
|
|
125
136
|
if (!candidate || candidate.id === activeId) continue
|
|
137
|
+
|
|
126
138
|
const idx = tabs.findIndex(tab => tab.id === candidate.id)
|
|
127
|
-
if (idx > -1)
|
|
139
|
+
if (idx > -1) {
|
|
140
|
+
const tab = tabs[idx]
|
|
141
|
+
const cacheKey = `${tab.id}::${tab.renderKey ?? 0}`
|
|
142
|
+
|
|
143
|
+
// Remove from cache and mark as not alive
|
|
144
|
+
aliveCache.delete(cacheKey)
|
|
145
|
+
tab.alive = false
|
|
146
|
+
}
|
|
128
147
|
}
|
|
129
148
|
}
|
|
130
149
|
|
|
@@ -163,11 +182,16 @@ export function createRouterTabs(
|
|
|
163
182
|
const activeId = ref<string | null>(null)
|
|
164
183
|
const current = shallowRef<TabRecord>()
|
|
165
184
|
const refreshingKey = ref<string | null>(null)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
|
|
186
|
+
// Track which keys should be in KeepAlive cache
|
|
187
|
+
// This is the source of truth for KeepAlive's include prop
|
|
188
|
+
const aliveCache = reactive<Set<string>>(new Set())
|
|
189
|
+
|
|
190
|
+
const includeKeys = computed(() => {
|
|
191
|
+
// Convert Set to Array for KeepAlive's include prop
|
|
192
|
+
// Format: ['routeKey::renderKey', ...]
|
|
193
|
+
return Array.from(aliveCache)
|
|
194
|
+
})
|
|
171
195
|
|
|
172
196
|
let isHydrating = false
|
|
173
197
|
|
|
@@ -186,31 +210,86 @@ export function createRouterTabs(
|
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Ensures a tab exists for the given route and manages its KeepAlive cache state.
|
|
215
|
+
* This is called on every route navigation via the router watcher.
|
|
216
|
+
*/
|
|
189
217
|
function ensureTab(route: RouteLocationNormalizedLoaded) {
|
|
190
218
|
const key = resolveKey(route)
|
|
191
219
|
let tab = tabs.find(item => item.id === key)
|
|
220
|
+
const shouldBeAlive = resolveAlive(route, options.keepAlive)
|
|
192
221
|
|
|
193
222
|
if (tab) {
|
|
223
|
+
// Tab exists - update its properties
|
|
194
224
|
tab.fullPath = route.fullPath
|
|
195
225
|
tab.to = route.fullPath
|
|
196
226
|
tab.matched = route
|
|
197
|
-
tab.alive = resolveAlive(route, options.keepAlive)
|
|
198
227
|
tab.reusable = resolveReusable(route, tab.reusable)
|
|
228
|
+
|
|
199
229
|
// Ensure renderKey is initialized
|
|
200
230
|
if (typeof tab.renderKey !== 'number') {
|
|
201
231
|
tab.renderKey = 0
|
|
202
232
|
}
|
|
233
|
+
|
|
234
|
+
// Generate the current cache key for this tab
|
|
235
|
+
const currentCacheKey = `${key}::${tab.renderKey}`
|
|
236
|
+
|
|
237
|
+
// Debug logging for specific routes
|
|
238
|
+
if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
|
|
239
|
+
console.log(`[ensureTab] EXISTING tab: ${route.fullPath}`, {
|
|
240
|
+
key,
|
|
241
|
+
shouldBeAlive,
|
|
242
|
+
currentRenderKey: tab.renderKey,
|
|
243
|
+
currentCacheKey,
|
|
244
|
+
isInCache: aliveCache.has(currentCacheKey),
|
|
245
|
+
aliveCacheSize: aliveCache.size,
|
|
246
|
+
cacheContents: Array.from(aliveCache)
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Manage KeepAlive cache state
|
|
251
|
+
if (shouldBeAlive) {
|
|
252
|
+
// Check if tab's current key is in the cache
|
|
253
|
+
if (!aliveCache.has(currentCacheKey)) {
|
|
254
|
+
// Tab was evicted or never added to cache - add it back
|
|
255
|
+
aliveCache.add(currentCacheKey)
|
|
256
|
+
tab.alive = true
|
|
257
|
+
if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
|
|
258
|
+
console.log(`[ensureTab] ✅ Added to cache: ${currentCacheKey}`)
|
|
259
|
+
}
|
|
260
|
+
} else if (!tab.alive) {
|
|
261
|
+
// Tab is in cache but marked as not alive - just reactivate
|
|
262
|
+
tab.alive = true
|
|
263
|
+
if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
|
|
264
|
+
console.log(`[ensureTab] ✅ Reactivated: ${currentCacheKey}`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
203
269
|
Object.assign(tab, pickMeta(route))
|
|
204
270
|
return tab
|
|
205
271
|
}
|
|
206
272
|
|
|
273
|
+
// Create new tab
|
|
207
274
|
tab = createTabFromRoute(route, {}, options.keepAlive)
|
|
275
|
+
|
|
276
|
+
// Add to cache if it should be alive
|
|
277
|
+
if (tab.alive) {
|
|
278
|
+
const cacheKey = `${key}::${tab.renderKey ?? 0}`
|
|
279
|
+
aliveCache.add(cacheKey)
|
|
280
|
+
|
|
281
|
+
if (key.includes('students') || key.includes('classroom') || key.includes('quiz')) {
|
|
282
|
+
console.log(`[ensureTab] NEW tab created and cached: ${cacheKey}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
208
286
|
insertTab(tabs, tab, options.appendPosition, activeId.value)
|
|
209
|
-
enforceMaxAlive(tabs, options.maxAlive, activeId.value)
|
|
287
|
+
enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
|
|
288
|
+
|
|
210
289
|
return tab
|
|
211
290
|
}
|
|
212
291
|
|
|
213
|
-
async function openTab(path: RouteLocationRaw, replace = false, refresh: boolean | 'sameTab' =
|
|
292
|
+
async function openTab(path: RouteLocationRaw, replace = false, refresh: boolean | 'sameTab' = 'sameTab') {
|
|
214
293
|
const target = resolveRoute(router, path)
|
|
215
294
|
const targetKey = resolveKey(target)
|
|
216
295
|
const sameKey = activeId.value === targetKey
|
|
@@ -278,37 +357,50 @@ export function createRouterTabs(
|
|
|
278
357
|
}
|
|
279
358
|
}
|
|
280
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Refreshes a tab by incrementing its renderKey and cycling its KeepAlive state.
|
|
362
|
+
* This forces the component to unmount and remount, clearing all internal state.
|
|
363
|
+
*
|
|
364
|
+
* @param id - The tab ID to refresh. Defaults to the currently active tab.
|
|
365
|
+
* @param force - If true, skips transition delays for immediate refresh.
|
|
366
|
+
*/
|
|
281
367
|
async function refreshTab(id: string | undefined = activeId.value ?? undefined, force = false) {
|
|
282
368
|
if (!id) return
|
|
369
|
+
|
|
283
370
|
const tab = tabs.find(item => item.id === id)
|
|
284
371
|
if (!tab) return
|
|
285
372
|
|
|
286
|
-
const
|
|
373
|
+
const shouldRestoreCache = options.keepAlive && tab.alive
|
|
374
|
+
const oldCacheKey = `${id}::${tab.renderKey ?? 0}`
|
|
287
375
|
|
|
288
|
-
// Remove from KeepAlive cache
|
|
289
|
-
if (
|
|
376
|
+
// Step 1: Remove from KeepAlive cache to prepare for fresh mount
|
|
377
|
+
if (shouldRestoreCache) {
|
|
378
|
+
aliveCache.delete(oldCacheKey)
|
|
290
379
|
tab.alive = false
|
|
291
380
|
await nextTick()
|
|
292
381
|
}
|
|
293
382
|
|
|
294
|
-
// Increment
|
|
383
|
+
// Step 2: Increment renderKey to generate new cache key (e.g., /quiz::0 → /quiz::1)
|
|
384
|
+
// This ensures KeepAlive treats it as a completely new component instance
|
|
295
385
|
tab.renderKey = (tab.renderKey ?? 0) + 1
|
|
386
|
+
const newCacheKey = `${id}::${tab.renderKey}`
|
|
296
387
|
|
|
297
|
-
// Restore to KeepAlive cache
|
|
298
|
-
if (
|
|
388
|
+
// Step 3: Restore to KeepAlive cache with new renderKey
|
|
389
|
+
if (shouldRestoreCache) {
|
|
390
|
+
aliveCache.add(newCacheKey)
|
|
299
391
|
tab.alive = true
|
|
300
392
|
}
|
|
301
393
|
|
|
302
|
-
//
|
|
394
|
+
// Step 4: Trigger transition by marking tab as refreshing
|
|
303
395
|
refreshingKey.value = id
|
|
304
396
|
await nextTick()
|
|
305
397
|
|
|
306
|
-
//
|
|
398
|
+
// Step 5: Allow transition to complete unless force refresh
|
|
307
399
|
if (!force) {
|
|
308
400
|
await nextTick()
|
|
309
401
|
}
|
|
310
402
|
|
|
311
|
-
// Clear refreshing state to
|
|
403
|
+
// Step 6: Clear refreshing state to render the refreshed component
|
|
312
404
|
refreshingKey.value = null
|
|
313
405
|
}
|
|
314
406
|
|
|
@@ -318,6 +410,56 @@ export function createRouterTabs(
|
|
|
318
410
|
}
|
|
319
411
|
}
|
|
320
412
|
|
|
413
|
+
// Programmatic control: set whether a tab is kept alive
|
|
414
|
+
function setTabAlive(id: string, alive: boolean) {
|
|
415
|
+
const tab = tabs.find(t => t.id === id)
|
|
416
|
+
if (!tab) return
|
|
417
|
+
|
|
418
|
+
const cacheKey = `${id}::${tab.renderKey ?? 0}`
|
|
419
|
+
|
|
420
|
+
if (alive) {
|
|
421
|
+
aliveCache.add(cacheKey)
|
|
422
|
+
tab.alive = true
|
|
423
|
+
// enforce max alive if turning alive on
|
|
424
|
+
enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
|
|
425
|
+
} else {
|
|
426
|
+
aliveCache.delete(cacheKey)
|
|
427
|
+
tab.alive = false
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Evicts a tab from KeepAlive cache and increments its renderKey.
|
|
433
|
+
* When the tab is re-activated, it will mount as a fresh component instance.
|
|
434
|
+
*
|
|
435
|
+
* @param id - The tab ID to evict from cache.
|
|
436
|
+
*/
|
|
437
|
+
function evictCache(id: string) {
|
|
438
|
+
const tab = tabs.find(t => t.id === id)
|
|
439
|
+
if (!tab) return
|
|
440
|
+
|
|
441
|
+
const oldCacheKey = `${id}::${tab.renderKey ?? 0}`
|
|
442
|
+
|
|
443
|
+
// Remove from cache
|
|
444
|
+
aliveCache.delete(oldCacheKey)
|
|
445
|
+
tab.alive = false
|
|
446
|
+
|
|
447
|
+
// Increment renderKey to ensure fresh mount on next activation
|
|
448
|
+
tab.renderKey = (tab.renderKey ?? 0) + 1
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Clear keep-alive for all tabs
|
|
452
|
+
function clearCache() {
|
|
453
|
+
aliveCache.clear()
|
|
454
|
+
tabs.forEach(tab => {
|
|
455
|
+
tab.alive = false
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function getCacheKeys() {
|
|
460
|
+
return includeKeys.value.slice()
|
|
461
|
+
}
|
|
462
|
+
|
|
321
463
|
async function reset(route: RouteLocationRaw = options.defaultRoute) {
|
|
322
464
|
tabs.splice(0, tabs.length)
|
|
323
465
|
activeId.value = null
|
|
@@ -368,9 +510,7 @@ export function createRouterTabs(
|
|
|
368
510
|
const tab = createTabFromRoute(route, base, options.keepAlive)
|
|
369
511
|
insertTab(tabs, tab, 'last', null)
|
|
370
512
|
} catch (error) {
|
|
371
|
-
|
|
372
|
-
console.warn('[RouterTabs] Failed to restore tab', record, error)
|
|
373
|
-
}
|
|
513
|
+
console.warn('[RouterTabs] Failed to restore tab', record, error)
|
|
374
514
|
}
|
|
375
515
|
}
|
|
376
516
|
|
|
@@ -381,9 +521,7 @@ export function createRouterTabs(
|
|
|
381
521
|
try {
|
|
382
522
|
await router.replace(target)
|
|
383
523
|
} catch (error) {
|
|
384
|
-
|
|
385
|
-
console.warn('[RouterTabs] Failed to navigate to restored route', target, error)
|
|
386
|
-
}
|
|
524
|
+
console.warn('[RouterTabs] Failed to navigate to restored route', target, error)
|
|
387
525
|
}
|
|
388
526
|
}
|
|
389
527
|
}
|
|
@@ -395,7 +533,7 @@ export function createRouterTabs(
|
|
|
395
533
|
const tab = ensureTab(route as RouteLocationNormalizedLoaded)
|
|
396
534
|
activeId.value = tab.id
|
|
397
535
|
current.value = tab
|
|
398
|
-
enforceMaxAlive(tabs, options.maxAlive, activeId.value)
|
|
536
|
+
enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
|
|
399
537
|
},
|
|
400
538
|
{ immediate: true }
|
|
401
539
|
)
|
|
@@ -420,11 +558,16 @@ export function createRouterTabs(
|
|
|
420
558
|
removeTab,
|
|
421
559
|
refreshTab,
|
|
422
560
|
refreshAll,
|
|
561
|
+
setTabAlive,
|
|
562
|
+
evictCache,
|
|
563
|
+
clearCache,
|
|
564
|
+
getCacheKeys,
|
|
423
565
|
reset,
|
|
424
566
|
reload,
|
|
425
567
|
getRouteKey,
|
|
426
568
|
matchRoute,
|
|
427
569
|
snapshot,
|
|
428
|
-
hydrate
|
|
570
|
+
hydrate,
|
|
571
|
+
ensureTab
|
|
429
572
|
}
|
|
430
573
|
}
|
package/lib/core/types.ts
CHANGED
|
@@ -115,6 +115,11 @@ export interface RouterTabsContext {
|
|
|
115
115
|
matchRoute: (route: RouteLocationNormalizedLoaded | RouteLocationRaw) => RouteMatchResult
|
|
116
116
|
snapshot: () => RouterTabsSnapshot
|
|
117
117
|
hydrate: (snapshot: RouterTabsSnapshot) => Promise<void>
|
|
118
|
+
setTabAlive: (id: string, alive: boolean) => void
|
|
119
|
+
evictCache: (id: string) => void
|
|
120
|
+
clearCache: () => void
|
|
121
|
+
getCacheKeys: () => string[]
|
|
122
|
+
ensureTab: (route: RouteLocationNormalizedLoaded) => TabRecord | undefined
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
export interface RouterTabsPersistenceOptions {
|
package/lib/scss/index.scss
CHANGED
|
@@ -70,7 +70,28 @@
|
|
|
70
70
|
position: relative;
|
|
71
71
|
flex: 1 1 0px;
|
|
72
72
|
height: 100%;
|
|
73
|
-
overflow:
|
|
73
|
+
overflow-x: auto;
|
|
74
|
+
overflow-y: hidden;
|
|
75
|
+
scroll-behavior: smooth;
|
|
76
|
+
scrollbar-width: thin;
|
|
77
|
+
scrollbar-color: var(--router-tab-border) transparent;
|
|
78
|
+
|
|
79
|
+
&::-webkit-scrollbar {
|
|
80
|
+
height: 4px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&::-webkit-scrollbar-track {
|
|
84
|
+
background: transparent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&::-webkit-scrollbar-thumb {
|
|
88
|
+
background: var(--router-tab-border);
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
|
|
91
|
+
&:hover {
|
|
92
|
+
background: color-mix(in srgb, var(--router-tab-primary) 50%, transparent);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
|
|
75
96
|
&-container {
|
|
76
97
|
width: 100%;
|