vue3-router-tab 1.3.6 → 1.3.8
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/dist/vue3-router-tab.js +718 -669
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +130 -55
- package/lib/core/createRouterTabs.ts +139 -26
- package/package.json +2 -12
|
@@ -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
|
|
|
@@ -322,22 +414,43 @@ export function createRouterTabs(
|
|
|
322
414
|
function setTabAlive(id: string, alive: boolean) {
|
|
323
415
|
const tab = tabs.find(t => t.id === id)
|
|
324
416
|
if (!tab) return
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
429
|
}
|
|
329
430
|
|
|
330
|
-
|
|
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
|
+
*/
|
|
331
437
|
function evictCache(id: string) {
|
|
332
438
|
const tab = tabs.find(t => t.id === id)
|
|
333
439
|
if (!tab) return
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
448
|
tab.renderKey = (tab.renderKey ?? 0) + 1
|
|
337
449
|
}
|
|
338
450
|
|
|
339
451
|
// Clear keep-alive for all tabs
|
|
340
452
|
function clearCache() {
|
|
453
|
+
aliveCache.clear()
|
|
341
454
|
tabs.forEach(tab => {
|
|
342
455
|
tab.alive = false
|
|
343
456
|
})
|
|
@@ -420,7 +533,7 @@ export function createRouterTabs(
|
|
|
420
533
|
const tab = ensureTab(route as RouteLocationNormalizedLoaded)
|
|
421
534
|
activeId.value = tab.id
|
|
422
535
|
current.value = tab
|
|
423
|
-
enforceMaxAlive(tabs, options.maxAlive, activeId.value)
|
|
536
|
+
enforceMaxAlive(tabs, options.maxAlive, activeId.value, aliveCache)
|
|
424
537
|
},
|
|
425
538
|
{ immediate: true }
|
|
426
539
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue3-router-tab",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -9,16 +9,7 @@
|
|
|
9
9
|
],
|
|
10
10
|
"main": "dist/vue3-router-tab.umd.cjs",
|
|
11
11
|
"module": "dist/vue3-router-tab.js",
|
|
12
|
-
"
|
|
13
|
-
".": {
|
|
14
|
-
"types": "./index.d.ts",
|
|
15
|
-
"import": "./dist/vue3-router-tab.js",
|
|
16
|
-
"require": "./dist/vue3-router-tab.umd.cjs",
|
|
17
|
-
"default": "./dist/vue3-router-tab.js"
|
|
18
|
-
},
|
|
19
|
-
"./style": "./dist/vue3-router-tab.css",
|
|
20
|
-
"./package.json": "./package.json"
|
|
21
|
-
},
|
|
12
|
+
"types": "index.d.ts",
|
|
22
13
|
"scripts": {
|
|
23
14
|
"build": "vite build",
|
|
24
15
|
"prepublishOnly": "npm run build"
|
|
@@ -60,7 +51,6 @@
|
|
|
60
51
|
"bugs": {
|
|
61
52
|
"url": "https://github.com/anilshr25/vue3-router-tab/issues"
|
|
62
53
|
},
|
|
63
|
-
"types": "index.d.ts",
|
|
64
54
|
"pnpm": {
|
|
65
55
|
"onlyBuiltDependencies": [
|
|
66
56
|
"@parcel/watcher",
|