vue3-router-tab 1.2.6 → 1.2.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/README.md +589 -78
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +685 -497
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +204 -10
- package/lib/core/createRouterTabs.ts +36 -2
- package/lib/core/types.ts +3 -0
- package/lib/index.ts +40 -9
- package/lib/persistence.ts +5 -2
- package/lib/scss/index.scss +310 -10
- package/lib/scss/variables.scss +14 -2
- package/lib/theme.ts +39 -4
- package/lib/useReactiveTab.ts +49 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
A Vue 3 tab-bar plugin that keeps multiple routes alive with transition support, context menus, and optional cookie-based persistence.
|
|
4
4
|
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Multi-tab Navigation** - Keep multiple routes alive simultaneously
|
|
8
|
+
- 🔄 **7 Built-in Transitions** - Smooth page transitions (swap, slide, fade, scale, flip, rotate, bounce)
|
|
9
|
+
- 🎨 **Reactive Tab Titles** - Automatically update tab titles, icons, and closability from component state
|
|
10
|
+
- 🖱️ **Context Menu** - Right-click tabs for refresh, close, and navigation options
|
|
11
|
+
- 🔀 **Drag & Drop** - Reorder tabs with drag-and-drop
|
|
12
|
+
- 💾 **Cookie Persistence** - Restore tabs on page refresh
|
|
13
|
+
- 🎭 **Theme Support** - Light, dark, and system themes with customizable colors
|
|
14
|
+
- ⚡ **KeepAlive Support** - Preserve component state when switching tabs
|
|
15
|
+
- 🎛️ **Highly Configurable** - Extensive props and events for customization
|
|
16
|
+
- 📱 **TypeScript Support** - Full TypeScript definitions included
|
|
17
|
+
|
|
5
18
|
## Installation
|
|
6
19
|
|
|
7
20
|
```bash
|
|
@@ -121,136 +134,315 @@ const routes = [
|
|
|
121
134
|
| `persistence` | `object \| null` | `null` | Persistence configuration |
|
|
122
135
|
| `sortable` | `boolean` | `true` | Enable drag-and-drop tab sorting |
|
|
123
136
|
|
|
124
|
-
|
|
137
|
+
## Page Transitions
|
|
138
|
+
|
|
139
|
+
Vue3 Router Tab includes 7 built-in page transition effects that are displayed when switching between tabs or refreshing pages.
|
|
140
|
+
|
|
141
|
+
### Available Transitions
|
|
125
142
|
|
|
126
|
-
|
|
143
|
+
| Transition | Description | Use Case |
|
|
144
|
+
|------------|-------------|----------|
|
|
145
|
+
| `router-tab-swap` | Slides up/down with fade (default) | General purpose, smooth |
|
|
146
|
+
| `router-tab-slide` | Horizontal sliding animation | Dashboard-style navigation |
|
|
147
|
+
| `router-tab-fade` | Simple opacity fade | Subtle, minimal distraction |
|
|
148
|
+
| `router-tab-scale` | Zoom in/out effect | Dramatic, attention-grabbing |
|
|
149
|
+
| `router-tab-flip` | 3D flip animation | Creative, modern feel |
|
|
150
|
+
| `router-tab-rotate` | Rotation with scale | Playful, dynamic |
|
|
151
|
+
| `router-tab-bounce` | Elastic bounce effect | Fun, energetic |
|
|
127
152
|
|
|
128
|
-
|
|
153
|
+
### Using Transitions
|
|
129
154
|
|
|
130
|
-
|
|
155
|
+
#### Default Transition
|
|
131
156
|
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
```vue
|
|
158
|
+
<router-tab />
|
|
159
|
+
<!-- Uses router-tab-swap by default -->
|
|
160
|
+
```
|
|
136
161
|
|
|
137
|
-
####
|
|
162
|
+
#### Custom Transition
|
|
163
|
+
|
|
164
|
+
```vue
|
|
165
|
+
<router-tab
|
|
166
|
+
:page-transition="{ name: 'router-tab-scale', mode: 'out-in' }"
|
|
167
|
+
/>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### String Shorthand
|
|
171
|
+
|
|
172
|
+
```vue
|
|
173
|
+
<router-tab page-transition="router-tab-fade" />
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Dynamic Transitions
|
|
177
|
+
|
|
178
|
+
Change transitions at runtime:
|
|
138
179
|
|
|
139
180
|
```vue
|
|
140
181
|
<template>
|
|
141
182
|
<div>
|
|
142
|
-
<
|
|
143
|
-
|
|
183
|
+
<select v-model="currentTransition">
|
|
184
|
+
<option value="router-tab-swap">Swap</option>
|
|
185
|
+
<option value="router-tab-slide">Slide</option>
|
|
186
|
+
<option value="router-tab-fade">Fade</option>
|
|
187
|
+
<option value="router-tab-scale">Scale</option>
|
|
188
|
+
<option value="router-tab-flip">Flip</option>
|
|
189
|
+
<option value="router-tab-rotate">Rotate</option>
|
|
190
|
+
<option value="router-tab-bounce">Bounce</option>
|
|
191
|
+
</select>
|
|
192
|
+
|
|
193
|
+
<router-tab
|
|
194
|
+
:page-transition="{ name: currentTransition, mode: 'out-in' }"
|
|
195
|
+
/>
|
|
144
196
|
</div>
|
|
145
197
|
</template>
|
|
146
198
|
|
|
147
199
|
<script setup>
|
|
148
|
-
import { ref
|
|
200
|
+
import { ref } from 'vue'
|
|
201
|
+
const currentTransition = ref('router-tab-swap')
|
|
202
|
+
</script>
|
|
203
|
+
```
|
|
149
204
|
|
|
150
|
-
|
|
151
|
-
const routeTabTitle = ref('My Page')
|
|
205
|
+
### Custom Transitions
|
|
152
206
|
|
|
153
|
-
|
|
154
|
-
const isLoading = ref(false)
|
|
155
|
-
const routeTabTitle = computed(() =>
|
|
156
|
-
isLoading.value ? 'Loading...' : 'My Page'
|
|
157
|
-
)
|
|
207
|
+
Create your own transitions by defining CSS classes:
|
|
158
208
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
209
|
+
```css
|
|
210
|
+
/* Your custom transition */
|
|
211
|
+
.my-custom-enter-active,
|
|
212
|
+
.my-custom-leave-active {
|
|
213
|
+
transition: all 0.5s ease;
|
|
214
|
+
}
|
|
163
215
|
|
|
164
|
-
|
|
165
|
-
|
|
216
|
+
.my-custom-enter-from {
|
|
217
|
+
opacity: 0;
|
|
218
|
+
transform: translateX(100px);
|
|
219
|
+
}
|
|
166
220
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
221
|
+
.my-custom-leave-to {
|
|
222
|
+
opacity: 0;
|
|
223
|
+
transform: translateX(-100px);
|
|
170
224
|
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```vue
|
|
228
|
+
<router-tab page-transition="my-custom" />
|
|
229
|
+
```
|
|
171
230
|
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
### Transition Tips
|
|
232
|
+
|
|
233
|
+
- **Performance**: Use `mode: 'out-in'` for smooth transitions without layout shifts
|
|
234
|
+
- **Duration**: Built-in transitions are optimized at 0.5-0.8 seconds
|
|
235
|
+
- **Accessibility**: Consider users with motion sensitivity - provide options to disable
|
|
236
|
+
- **Context**: Match transition style to your app's design language
|
|
237
|
+
|
|
238
|
+
## Changing Tab Titles Dynamically
|
|
239
|
+
|
|
240
|
+
Vue3 Router Tab automatically watches for reactive properties in your page components and updates the corresponding tab information in real-time.
|
|
241
|
+
|
|
242
|
+
### Quick Start
|
|
243
|
+
|
|
244
|
+
Simply expose reactive properties from your component:
|
|
245
|
+
|
|
246
|
+
```vue
|
|
247
|
+
<template>
|
|
248
|
+
<div>
|
|
249
|
+
<h1>{{ routeTabTitle }}</h1>
|
|
250
|
+
<button @click="updateTitle">Change Tab Title</button>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
254
|
+
<script setup>
|
|
255
|
+
import { ref } from 'vue'
|
|
256
|
+
|
|
257
|
+
// This ref is automatically watched - tab title updates when it changes!
|
|
258
|
+
const routeTabTitle = ref('My Page')
|
|
259
|
+
|
|
260
|
+
function updateTitle() {
|
|
261
|
+
routeTabTitle.value = 'Updated Title'
|
|
174
262
|
}
|
|
175
263
|
</script>
|
|
176
264
|
```
|
|
177
265
|
|
|
178
|
-
|
|
266
|
+
### Watched Properties
|
|
267
|
+
|
|
268
|
+
The following reactive properties are automatically monitored:
|
|
269
|
+
|
|
270
|
+
| Property | Description | Example |
|
|
271
|
+
|----------|-------------|---------|
|
|
272
|
+
| `routeTabTitle` | Tab title text | `ref('Dashboard')` |
|
|
273
|
+
| `routeTabIcon` | Tab icon class | `ref('mdi-home')` |
|
|
274
|
+
| `routeTabClosable` | Can tab be closed | `ref(true)` |
|
|
275
|
+
| `routeTabMeta` | Additional metadata | `ref({ badge: 5 })` |
|
|
276
|
+
|
|
277
|
+
### Dynamic Titles with Computed
|
|
278
|
+
|
|
279
|
+
Create titles that update based on your component state:
|
|
179
280
|
|
|
180
281
|
```vue
|
|
181
282
|
<script setup>
|
|
182
283
|
import { ref, computed } from 'vue'
|
|
183
284
|
|
|
285
|
+
const isLoading = ref(false)
|
|
184
286
|
const notifications = ref(0)
|
|
185
|
-
const hasError = ref(false)
|
|
186
|
-
const isProcessing = ref(false)
|
|
187
287
|
|
|
188
|
-
//
|
|
288
|
+
// Tab title automatically updates when dependencies change
|
|
189
289
|
const routeTabTitle = computed(() => {
|
|
190
|
-
if (
|
|
191
|
-
if (isProcessing.value) return 'Processing...'
|
|
290
|
+
if (isLoading.value) return 'Loading...'
|
|
192
291
|
if (notifications.value > 0) return `Messages (${notifications.value})`
|
|
193
292
|
return 'Dashboard'
|
|
194
293
|
})
|
|
195
294
|
|
|
196
|
-
//
|
|
197
|
-
const routeTabIcon = computed(() =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
295
|
+
// Tab icon changes based on state
|
|
296
|
+
const routeTabIcon = computed(() =>
|
|
297
|
+
isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-view-dashboard'
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
// Prevent closing during operations
|
|
301
|
+
const routeTabClosable = computed(() => !isLoading.value)
|
|
302
|
+
</script>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Real-World Examples
|
|
306
|
+
|
|
307
|
+
#### Example 1: User Profile with Name
|
|
308
|
+
|
|
309
|
+
```vue
|
|
310
|
+
<script setup>
|
|
311
|
+
import { ref, computed, onMounted } from 'vue'
|
|
312
|
+
|
|
313
|
+
const user = ref(null)
|
|
314
|
+
const isLoading = ref(true)
|
|
315
|
+
|
|
316
|
+
const routeTabTitle = computed(() =>
|
|
317
|
+
isLoading.value ? 'Loading...' : `Profile - ${user.value?.name || 'Unknown'}`
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
const routeTabIcon = computed(() =>
|
|
321
|
+
isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-account'
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
onMounted(async () => {
|
|
325
|
+
user.value = await fetchUser()
|
|
326
|
+
isLoading.value = false
|
|
202
327
|
})
|
|
328
|
+
</script>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Example 2: Form with Unsaved Changes
|
|
332
|
+
|
|
333
|
+
```vue
|
|
334
|
+
<script setup>
|
|
335
|
+
import { ref, computed } from 'vue'
|
|
203
336
|
|
|
204
|
-
|
|
205
|
-
const
|
|
337
|
+
const formData = ref({})
|
|
338
|
+
const hasUnsavedChanges = ref(false)
|
|
339
|
+
|
|
340
|
+
const routeTabTitle = computed(() =>
|
|
341
|
+
hasUnsavedChanges.value ? '• Edit Form' : 'Edit Form'
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
const routeTabClosable = computed(() => !hasUnsavedChanges.value)
|
|
345
|
+
|
|
346
|
+
function onChange() {
|
|
347
|
+
hasUnsavedChanges.value = true
|
|
348
|
+
}
|
|
349
|
+
</script>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Example 3: Real-time Notifications
|
|
353
|
+
|
|
354
|
+
```vue
|
|
355
|
+
<script setup>
|
|
356
|
+
import { ref, computed, onMounted } from 'vue'
|
|
357
|
+
|
|
358
|
+
const unreadCount = ref(0)
|
|
359
|
+
|
|
360
|
+
const routeTabTitle = computed(() => {
|
|
361
|
+
if (unreadCount.value === 0) return 'Messages'
|
|
362
|
+
return `Messages (${unreadCount.value})`
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const routeTabIcon = computed(() =>
|
|
366
|
+
unreadCount.value > 0 ? 'mdi-bell-badge' : 'mdi-bell-outline'
|
|
367
|
+
)
|
|
206
368
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
})
|
|
369
|
+
// Simulate real-time updates
|
|
370
|
+
onMounted(() => {
|
|
371
|
+
setInterval(() => {
|
|
372
|
+
unreadCount.value = Math.floor(Math.random() * 10)
|
|
373
|
+
}, 5000)
|
|
374
|
+
})
|
|
213
375
|
</script>
|
|
214
376
|
```
|
|
215
377
|
|
|
216
|
-
|
|
378
|
+
### Using the Composable API
|
|
217
379
|
|
|
218
|
-
For
|
|
380
|
+
For advanced use cases, use the `useReactiveTab` composable:
|
|
219
381
|
|
|
220
382
|
```vue
|
|
221
383
|
<script setup>
|
|
222
|
-
import {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
384
|
+
import { useReactiveTab } from 'vue3-router-tab'
|
|
385
|
+
import { ref } from 'vue'
|
|
386
|
+
|
|
387
|
+
const user = ref({ name: 'John Doe', status: 'online' })
|
|
388
|
+
|
|
389
|
+
const {
|
|
390
|
+
routeTabTitle,
|
|
391
|
+
routeTabIcon,
|
|
392
|
+
routeTabClosable
|
|
393
|
+
} = useReactiveTab({
|
|
394
|
+
title: () => `${user.value.name} - ${user.value.status}`,
|
|
395
|
+
icon: () => user.value.status === 'online' ? 'mdi-account' : 'mdi-account-off',
|
|
396
|
+
closable: () => user.value.status !== 'editing'
|
|
233
397
|
})
|
|
398
|
+
</script>
|
|
399
|
+
```
|
|
234
400
|
|
|
235
|
-
|
|
236
|
-
const isLoading = ref(false)
|
|
237
|
-
const loadingTab = useLoadingTab(isLoading, 'Dashboard')
|
|
401
|
+
### Important Notes
|
|
238
402
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
403
|
+
1. **Automatic Exposure in `<script setup>`**: Properties defined in `<script setup>` are automatically exposed - no need for `defineExpose()`
|
|
404
|
+
2. **Reactive types**: Use `ref()` or `computed()` - plain values won't trigger updates
|
|
405
|
+
3. **Automatic watching**: No manual watchers needed - the plugin handles everything
|
|
406
|
+
4. **Performance**: Only active tab components are watched to minimize overhead
|
|
242
407
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
408
|
+
> 💡 **Try it yourself**: Check out the live demo at `/title-test` in the example app to see all these features in action!
|
|
409
|
+
|
|
410
|
+
### Options API Support
|
|
411
|
+
|
|
412
|
+
If you're using the Options API, you need to expose the properties:
|
|
413
|
+
|
|
414
|
+
```vue
|
|
415
|
+
<script>
|
|
416
|
+
import { ref, computed } from 'vue'
|
|
417
|
+
|
|
418
|
+
export default {
|
|
419
|
+
setup() {
|
|
420
|
+
const routeTabTitle = ref('My Page')
|
|
421
|
+
const routeTabIcon = computed(() => 'mdi-page')
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
routeTabTitle,
|
|
425
|
+
routeTabIcon
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
246
429
|
</script>
|
|
247
430
|
```
|
|
248
431
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
432
|
+
Or with `defineExpose`:
|
|
433
|
+
|
|
434
|
+
```vue
|
|
435
|
+
<script setup>
|
|
436
|
+
import { ref } from 'vue'
|
|
437
|
+
|
|
438
|
+
const routeTabTitle = ref('My Page')
|
|
439
|
+
|
|
440
|
+
// Only needed if you're NOT using top-level refs in <script setup>
|
|
441
|
+
defineExpose({ routeTabTitle })
|
|
442
|
+
</script>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Note**: With `<script setup>`, top-level bindings are automatically exposed, so `defineExpose` is typically not needed.
|
|
254
446
|
|
|
255
447
|
### Tab Closing Behavior
|
|
256
448
|
|
|
@@ -394,6 +586,141 @@ Pass `false` to disable the context menu entirely.
|
|
|
394
586
|
- `closeRights` - Close tabs to the right
|
|
395
587
|
- `closeOthers` - Close all other tabs
|
|
396
588
|
|
|
589
|
+
## Programmatic API
|
|
590
|
+
|
|
591
|
+
Access the router tabs controller to programmatically manage tabs.
|
|
592
|
+
|
|
593
|
+
### Using the Composable
|
|
594
|
+
|
|
595
|
+
```vue
|
|
596
|
+
<script setup>
|
|
597
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
598
|
+
|
|
599
|
+
const tabs = useRouterTabs()
|
|
600
|
+
|
|
601
|
+
// Available methods
|
|
602
|
+
tabs.openTab('/users') // Open a tab
|
|
603
|
+
tabs.closeTab(tabId) // Close a specific tab
|
|
604
|
+
tabs.refreshTab(tabId) // Refresh a tab
|
|
605
|
+
tabs.refreshAll() // Refresh all tabs
|
|
606
|
+
tabs.closeAll() // Close all tabs
|
|
607
|
+
tabs.closeOthers(tabId) // Close all except specified tab
|
|
608
|
+
</script>
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Using Global Property
|
|
612
|
+
|
|
613
|
+
```vue
|
|
614
|
+
<script setup>
|
|
615
|
+
import { getCurrentInstance } from 'vue'
|
|
616
|
+
|
|
617
|
+
const instance = getCurrentInstance()
|
|
618
|
+
const tabs = instance?.appContext.config.globalProperties.$tabs
|
|
619
|
+
|
|
620
|
+
// Same methods available
|
|
621
|
+
tabs?.openTab('/dashboard')
|
|
622
|
+
tabs?.refreshTab('users-123')
|
|
623
|
+
</script>
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Controller Methods
|
|
627
|
+
|
|
628
|
+
| Method | Parameters | Description |
|
|
629
|
+
|--------|------------|-------------|
|
|
630
|
+
| `openTab(to, active?, replace?)` | Route location, activate flag, replace flag | Open or activate a tab |
|
|
631
|
+
| `closeTab(id, options?)` | Tab ID, close options | Close a specific tab |
|
|
632
|
+
| `refreshTab(id, force?)` | Tab ID, force flag | Refresh tab component |
|
|
633
|
+
| `refreshAll(force?)` | Force flag | Refresh all tabs |
|
|
634
|
+
| `closeAll(options?)` | Close options | Close all closable tabs |
|
|
635
|
+
| `closeOthers(id, options?)` | Tab ID, options | Close all tabs except specified |
|
|
636
|
+
| `removeTab(id, options?)` | Tab ID, options | Remove tab without navigation |
|
|
637
|
+
|
|
638
|
+
### Example: Custom Tab Controls
|
|
639
|
+
|
|
640
|
+
```vue
|
|
641
|
+
<template>
|
|
642
|
+
<div>
|
|
643
|
+
<button @click="openDashboard">Open Dashboard</button>
|
|
644
|
+
<button @click="refreshCurrent">Refresh Current</button>
|
|
645
|
+
<button @click="closeAllTabs">Close All</button>
|
|
646
|
+
</div>
|
|
647
|
+
</template>
|
|
648
|
+
|
|
649
|
+
<script setup>
|
|
650
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
651
|
+
import { useRoute } from 'vue-router'
|
|
652
|
+
|
|
653
|
+
const tabs = useRouterTabs()
|
|
654
|
+
const route = useRoute()
|
|
655
|
+
|
|
656
|
+
function openDashboard() {
|
|
657
|
+
tabs.openTab('/dashboard', true)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function refreshCurrent() {
|
|
661
|
+
const currentTab = tabs.tabs.find(t => t.to.path === route.path)
|
|
662
|
+
if (currentTab) {
|
|
663
|
+
tabs.refreshTab(currentTab.id, true)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function closeAllTabs() {
|
|
668
|
+
tabs.closeAll({ force: true })
|
|
669
|
+
}
|
|
670
|
+
</script>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Tab State Access
|
|
674
|
+
|
|
675
|
+
```vue
|
|
676
|
+
<script setup>
|
|
677
|
+
import { useRouterTabs } from 'vue3-router-tab'
|
|
678
|
+
|
|
679
|
+
const controller = useRouterTabs()
|
|
680
|
+
|
|
681
|
+
// Access current state
|
|
682
|
+
console.log(controller.tabs) // Array of all tabs
|
|
683
|
+
console.log(controller.activeId.value) // Current active tab ID
|
|
684
|
+
console.log(controller.includeKeys) // KeepAlive include keys
|
|
685
|
+
</script>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Events
|
|
689
|
+
|
|
690
|
+
RouterTab emits events for tab interactions.
|
|
691
|
+
|
|
692
|
+
### Available Events
|
|
693
|
+
|
|
694
|
+
| Event | Payload | Description |
|
|
695
|
+
|-------|---------|-------------|
|
|
696
|
+
| `tab-sort` | `{ tab, index }` | Fired when tab drag starts |
|
|
697
|
+
| `tab-sorted` | `{ tab, fromIndex, toIndex }` | Fired when tab is dropped in new position |
|
|
698
|
+
|
|
699
|
+
### Usage Example
|
|
700
|
+
|
|
701
|
+
```vue
|
|
702
|
+
<template>
|
|
703
|
+
<router-tab
|
|
704
|
+
@tab-sort="onTabSort"
|
|
705
|
+
@tab-sorted="onTabSorted"
|
|
706
|
+
/>
|
|
707
|
+
</template>
|
|
708
|
+
|
|
709
|
+
<script setup>
|
|
710
|
+
function onTabSort({ tab, index }) {
|
|
711
|
+
console.log('Dragging tab:', tab.title, 'from index:', index)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function onTabSorted({ tab, fromIndex, toIndex }) {
|
|
715
|
+
console.log('Tab moved:', tab.title)
|
|
716
|
+
console.log('From:', fromIndex, 'To:', toIndex)
|
|
717
|
+
|
|
718
|
+
// Save new order to backend
|
|
719
|
+
saveTabOrder(tab, toIndex)
|
|
720
|
+
}
|
|
721
|
+
</script>
|
|
722
|
+
```
|
|
723
|
+
|
|
397
724
|
## Slots
|
|
398
725
|
|
|
399
726
|
- `start` / `end` – positioned on either side of the tab list (ideal for toolbars or the `<router-tabs>` helper).
|
|
@@ -401,7 +728,64 @@ Pass `false` to disable the context menu entirely.
|
|
|
401
728
|
|
|
402
729
|
## Styling
|
|
403
730
|
|
|
404
|
-
The package ships with its own CSS bundle (imported automatically). Override the `router-tab__*` classes
|
|
731
|
+
The package ships with its own CSS bundle (imported automatically). Override CSS custom properties or the `router-tab__*` classes to customize the appearance.
|
|
732
|
+
|
|
733
|
+
### CSS Custom Properties
|
|
734
|
+
|
|
735
|
+
```css
|
|
736
|
+
:root {
|
|
737
|
+
/* Layout */
|
|
738
|
+
--router-tab-header-height: 48px;
|
|
739
|
+
--router-tab-padding: 16px;
|
|
740
|
+
--router-tab-font-size: 14px;
|
|
741
|
+
|
|
742
|
+
/* Colors */
|
|
743
|
+
--router-tab-primary: #0f172a;
|
|
744
|
+
--router-tab-background: #ffffff;
|
|
745
|
+
--router-tab-text: #334155;
|
|
746
|
+
--router-tab-border: #e2e8f0;
|
|
747
|
+
--router-tab-active-background: #0f172a;
|
|
748
|
+
--router-tab-active-text: #ffffff;
|
|
749
|
+
|
|
750
|
+
/* Icons & Buttons */
|
|
751
|
+
--router-tab-icon-color: #64748b;
|
|
752
|
+
--router-tab-button-background: #f1f5f9;
|
|
753
|
+
--router-tab-button-color: #0f172a;
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Custom Styles Example
|
|
758
|
+
|
|
759
|
+
```css
|
|
760
|
+
/* Change tab height */
|
|
761
|
+
.router-tab__header {
|
|
762
|
+
height: 56px;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Custom tab hover effect */
|
|
766
|
+
.router-tab__item:hover {
|
|
767
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
768
|
+
color: white;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* Custom active tab */
|
|
772
|
+
.router-tab__item.is-active {
|
|
773
|
+
background: #4f46e5;
|
|
774
|
+
border-radius: 8px 8px 0 0;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* Custom close button */
|
|
778
|
+
.router-tab__item-close {
|
|
779
|
+
border-radius: 4px;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* Dark theme adjustments */
|
|
783
|
+
[data-theme="dark"] .router-tab {
|
|
784
|
+
--router-tab-background: #1e293b;
|
|
785
|
+
--router-tab-text: #f1f5f9;
|
|
786
|
+
--router-tab-border: #334155;
|
|
787
|
+
}
|
|
788
|
+
```
|
|
405
789
|
|
|
406
790
|
## Types
|
|
407
791
|
|
|
@@ -409,6 +793,133 @@ The package ships with its own CSS bundle (imported automatically). Override the
|
|
|
409
793
|
import type { TabRecord, RouterTabsSnapshot, RouterTabsPersistenceOptions } from 'vue3-router-tab'
|
|
410
794
|
```
|
|
411
795
|
|
|
796
|
+
## Examples
|
|
797
|
+
|
|
798
|
+
Check out the [example-app](./example-app) directory for comprehensive demos including:
|
|
799
|
+
|
|
800
|
+
- **Basic Usage** - Simple tab navigation
|
|
801
|
+
- **Dynamic Titles** - Reactive tab title updates ([/title-test](./example-app/src/views/TitleTestDemo.vue))
|
|
802
|
+
- **Transitions** - All 7 transition effects ([/transition-demo](./example-app/src/views/TransitionDemo.vue))
|
|
803
|
+
- **Composables** - Using helper composables ([/composable-demo](./example-app/src/views/ComposableDemo.vue))
|
|
804
|
+
- **Advanced Features** - Sorting, context menus, persistence
|
|
805
|
+
|
|
806
|
+
### Run Examples
|
|
807
|
+
|
|
808
|
+
```bash
|
|
809
|
+
cd example-app
|
|
810
|
+
npm install
|
|
811
|
+
npm run dev
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
## Troubleshooting
|
|
815
|
+
|
|
816
|
+
### Tab titles not updating
|
|
817
|
+
|
|
818
|
+
**Problem**: Tab title doesn't change when component state updates.
|
|
819
|
+
|
|
820
|
+
**Solution**: Ensure you're using reactive refs or computed properties:
|
|
821
|
+
|
|
822
|
+
```vue
|
|
823
|
+
<!-- ✅ Correct -->
|
|
824
|
+
<script setup>
|
|
825
|
+
const routeTabTitle = ref('My Page') // Reactive
|
|
826
|
+
</script>
|
|
827
|
+
|
|
828
|
+
<!-- ❌ Wrong -->
|
|
829
|
+
<script setup>
|
|
830
|
+
const routeTabTitle = 'My Page' // Plain string - won't update
|
|
831
|
+
</script>
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Transitions not working
|
|
835
|
+
|
|
836
|
+
**Problem**: Page transitions don't show when refreshing tabs.
|
|
837
|
+
|
|
838
|
+
**Solution**: Make sure you're not using a custom default slot that overrides transitions. Remove the custom slot or include transition components:
|
|
839
|
+
|
|
840
|
+
```vue
|
|
841
|
+
<!-- ✅ Default behavior with transitions -->
|
|
842
|
+
<router-tab page-transition="router-tab-fade" />
|
|
843
|
+
|
|
844
|
+
<!-- ❌ Custom slot without transitions -->
|
|
845
|
+
<router-tab>
|
|
846
|
+
<template #default="{ Component }">
|
|
847
|
+
<component :is="Component" /> <!-- No transition! -->
|
|
848
|
+
</template>
|
|
849
|
+
</router-tab>
|
|
850
|
+
|
|
851
|
+
<!-- ✅ Custom slot with transitions -->
|
|
852
|
+
<router-tab>
|
|
853
|
+
<template #default="{ Component }">
|
|
854
|
+
<transition name="router-tab-fade" mode="out-in">
|
|
855
|
+
<component :is="Component" />
|
|
856
|
+
</transition>
|
|
857
|
+
</template>
|
|
858
|
+
</router-tab>
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Tabs not persisting on refresh
|
|
862
|
+
|
|
863
|
+
**Problem**: Tabs are lost when refreshing the browser.
|
|
864
|
+
|
|
865
|
+
**Solution**: Add the `cookie-key` prop:
|
|
866
|
+
|
|
867
|
+
```vue
|
|
868
|
+
<router-tab cookie-key="my-app-tabs" />
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Check that cookies are enabled in the browser and not being blocked.
|
|
872
|
+
|
|
873
|
+
### KeepAlive not working
|
|
874
|
+
|
|
875
|
+
**Problem**: Component state is lost when switching tabs.
|
|
876
|
+
|
|
877
|
+
**Solution**: Ensure `:keep-alive="true"` (default) and components are properly keyed:
|
|
878
|
+
|
|
879
|
+
```ts
|
|
880
|
+
// In router config
|
|
881
|
+
meta: {
|
|
882
|
+
keepAlive: true, // Enable for this route
|
|
883
|
+
key: 'fullPath' // Unique key per instance
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### TypeScript errors
|
|
888
|
+
|
|
889
|
+
**Problem**: TypeScript shows errors for router tab properties.
|
|
890
|
+
|
|
891
|
+
**Solution**: Import types and use them:
|
|
892
|
+
|
|
893
|
+
```ts
|
|
894
|
+
import type { TabRecord, RouterTabsOptions } from 'vue3-router-tab'
|
|
895
|
+
|
|
896
|
+
const options: RouterTabsOptions = {
|
|
897
|
+
keepAlive: true,
|
|
898
|
+
maxAlive: 10
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
## Browser Support
|
|
903
|
+
|
|
904
|
+
- Chrome/Edge ≥ 90
|
|
905
|
+
- Firefox ≥ 88
|
|
906
|
+
- Safari ≥ 14
|
|
907
|
+
- Modern mobile browsers
|
|
908
|
+
|
|
909
|
+
## Contributing
|
|
910
|
+
|
|
911
|
+
Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) first.
|
|
912
|
+
|
|
913
|
+
1. Fork the repository
|
|
914
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
915
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
916
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
917
|
+
5. Open a Pull Request
|
|
918
|
+
|
|
412
919
|
## License
|
|
413
920
|
|
|
414
921
|
MIT
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
Made with ❤️ by the Vue community
|