vue3-router-tab 1.2.4 → 1.2.6
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 +225 -2
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +592 -460
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +179 -15
- package/lib/core/createRouterTabs.ts +18 -14
- package/lib/index.ts +12 -0
- package/lib/scss/index.scss +12 -0
- package/lib/theme.ts +2 -2
- package/lib/useReactiveTab.ts +130 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ app.mount('#app')
|
|
|
28
28
|
|
|
29
29
|
The plugin registers the `<router-tab>` component globally. It also exposes an optional `<router-tabs>` helper for advanced cookie options, but you rarely need it now that persistence can be enabled directly on the tab component.
|
|
30
30
|
|
|
31
|
-
## Basic
|
|
31
|
+
## Basic Usage
|
|
32
32
|
|
|
33
33
|
```vue
|
|
34
34
|
<template>
|
|
@@ -38,6 +38,29 @@ The plugin registers the `<router-tab>` component globally. It also exposes an o
|
|
|
38
38
|
|
|
39
39
|
> Hint: `cookie-key` is optional. Omit it to fall back to the default `router-tabs:snapshot` cookie, or set your own as shown above.
|
|
40
40
|
|
|
41
|
+
### Enhanced Usage
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<template>
|
|
45
|
+
<router-tab
|
|
46
|
+
cookie-key="app-tabs"
|
|
47
|
+
:sortable="true"
|
|
48
|
+
@tab-sort="onTabSort"
|
|
49
|
+
@tab-sorted="onTabSorted"
|
|
50
|
+
/>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
function onTabSort({ tab, index }) {
|
|
55
|
+
console.log('Tab drag started:', tab.title, 'at index', index)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onTabSorted({ tab, fromIndex, toIndex }) {
|
|
59
|
+
console.log('Tab moved:', tab.title, 'from', fromIndex, 'to', toIndex)
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
41
64
|
Need to customise the rendered output? Provide a slot and use the routed component directly—see [`example-app/src/App.vue`](example-app/src/App.vue) for a working sample:
|
|
42
65
|
|
|
43
66
|
```vue
|
|
@@ -79,6 +102,165 @@ const routes = [
|
|
|
79
102
|
|
|
80
103
|
`meta.key` accepts the shortcuts `fullPath`, `path`, or `name`, or you can supply your own function.
|
|
81
104
|
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
### RouterTab Props
|
|
108
|
+
|
|
109
|
+
| Prop | Type | Default | Description |
|
|
110
|
+
|------|------|---------|-------------|
|
|
111
|
+
| `tabs` | `TabInput[]` | `[]` | Initial tabs to display |
|
|
112
|
+
| `keepAlive` | `boolean` | `true` | Enable keep-alive for tab components |
|
|
113
|
+
| `maxAlive` | `number` | `0` | Maximum number of alive components (0 = unlimited) |
|
|
114
|
+
| `keepLastTab` | `boolean` | `true` | Prevent closing the last remaining tab |
|
|
115
|
+
| `append` | `'last' \| 'next'` | `'last'` | Where to append new tabs |
|
|
116
|
+
| `defaultPage` | `string \| object` | `'/'` | Default route to navigate to |
|
|
117
|
+
| `tabTransition` | `string \| object` | `'router-tab-zoom'` | Transition for tab changes |
|
|
118
|
+
| `pageTransition` | `string \| object` | `{ name: 'router-tab-swap', mode: 'out-in' }` | Transition for page changes |
|
|
119
|
+
| `contextmenu` | `boolean \| array` | `true` | Enable context menu or provide custom menu items |
|
|
120
|
+
| `cookieKey` | `string` | `'router-tabs:snapshot'` | Cookie key for persistence |
|
|
121
|
+
| `persistence` | `object \| null` | `null` | Persistence configuration |
|
|
122
|
+
| `sortable` | `boolean` | `true` | Enable drag-and-drop tab sorting |
|
|
123
|
+
|
|
124
|
+
### Reactive Tab Updates
|
|
125
|
+
|
|
126
|
+
RouterTab automatically watches for reactive properties in your page components and updates the corresponding tab information in real-time. The tab titles, icons, and other properties displayed in the tab bar will automatically update when the reactive properties in your components change.
|
|
127
|
+
|
|
128
|
+
#### Watched Properties
|
|
129
|
+
|
|
130
|
+
The following reactive properties in your page components are automatically monitored:
|
|
131
|
+
|
|
132
|
+
- **`routeTabTitle`** - Updates the tab title
|
|
133
|
+
- **`routeTabIcon`** - Updates the tab icon
|
|
134
|
+
- **`routeTabClosable`** - Updates whether the tab can be closed
|
|
135
|
+
- **`routeTabMeta`** - Updates additional tab metadata
|
|
136
|
+
|
|
137
|
+
#### Basic Usage
|
|
138
|
+
|
|
139
|
+
```vue
|
|
140
|
+
<template>
|
|
141
|
+
<div>
|
|
142
|
+
<button @click="updateTitle">Update Title</button>
|
|
143
|
+
<button @click="toggleLoading">Toggle Loading</button>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<script setup>
|
|
148
|
+
import { ref, computed } from 'vue'
|
|
149
|
+
|
|
150
|
+
// Simple reactive title - tab updates immediately when this changes
|
|
151
|
+
const routeTabTitle = ref('My Page')
|
|
152
|
+
|
|
153
|
+
// Dynamic title based on state
|
|
154
|
+
const isLoading = ref(false)
|
|
155
|
+
const routeTabTitle = computed(() =>
|
|
156
|
+
isLoading.value ? 'Loading...' : 'My Page'
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// Dynamic icon - tab icon updates automatically
|
|
160
|
+
const routeTabIcon = computed(() =>
|
|
161
|
+
isLoading.value ? 'mdi-loading mdi-spin' : 'mdi-page'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Conditional closability - tab close button appears/disappears automatically
|
|
165
|
+
const routeTabClosable = computed(() => !isLoading.value)
|
|
166
|
+
|
|
167
|
+
// Functions that trigger reactive updates
|
|
168
|
+
function updateTitle() {
|
|
169
|
+
routeTabTitle.value = `Updated ${Date.now()}`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toggleLoading() {
|
|
173
|
+
isLoading.value = !isLoading.value
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Advanced Examples
|
|
179
|
+
|
|
180
|
+
```vue
|
|
181
|
+
<script setup>
|
|
182
|
+
import { ref, computed } from 'vue'
|
|
183
|
+
|
|
184
|
+
const notifications = ref(0)
|
|
185
|
+
const hasError = ref(false)
|
|
186
|
+
const isProcessing = ref(false)
|
|
187
|
+
|
|
188
|
+
// Dynamic title with notification count
|
|
189
|
+
const routeTabTitle = computed(() => {
|
|
190
|
+
if (hasError.value) return 'Error!'
|
|
191
|
+
if (isProcessing.value) return 'Processing...'
|
|
192
|
+
if (notifications.value > 0) return `Messages (${notifications.value})`
|
|
193
|
+
return 'Dashboard'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// State-based icons
|
|
197
|
+
const routeTabIcon = computed(() => {
|
|
198
|
+
if (hasError.value) return 'mdi-alert'
|
|
199
|
+
if (isProcessing.value) return 'mdi-loading mdi-spin'
|
|
200
|
+
if (notifications.value > 0) return 'mdi-bell-badge'
|
|
201
|
+
return 'mdi-view-dashboard'
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Prevent closing during critical operations
|
|
205
|
+
const routeTabClosable = computed(() => !isProcessing.value)
|
|
206
|
+
|
|
207
|
+
// Custom metadata for advanced use cases
|
|
208
|
+
const routeTabMeta = computed(() => ({
|
|
209
|
+
status: hasError.value ? 'error' : 'normal',
|
|
210
|
+
count: notifications.value,
|
|
211
|
+
timestamp: Date.now()
|
|
212
|
+
}))
|
|
213
|
+
</script>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Using Composables
|
|
217
|
+
|
|
218
|
+
For easier reactive tab management, use the provided composables:
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<script setup>
|
|
222
|
+
import {
|
|
223
|
+
useReactiveTab,
|
|
224
|
+
useLoadingTab,
|
|
225
|
+
useNotificationTab,
|
|
226
|
+
useStatusTab
|
|
227
|
+
} from 'vue3-router-tab'
|
|
228
|
+
|
|
229
|
+
// Basic composable
|
|
230
|
+
const { routeTabTitle, routeTabIcon, updateTitle } = useReactiveTab({
|
|
231
|
+
title: 'My Page',
|
|
232
|
+
icon: 'mdi-page'
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Loading state preset
|
|
236
|
+
const isLoading = ref(false)
|
|
237
|
+
const loadingTab = useLoadingTab(isLoading, 'Dashboard')
|
|
238
|
+
|
|
239
|
+
// Notification preset
|
|
240
|
+
const count = ref(0)
|
|
241
|
+
const notificationTab = useNotificationTab(count, 'Messages')
|
|
242
|
+
|
|
243
|
+
// Status preset
|
|
244
|
+
const status = ref('normal')
|
|
245
|
+
const statusTab = useStatusTab(status, 'Process')
|
|
246
|
+
</script>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Available Composables:**
|
|
250
|
+
- `useReactiveTab(config)` - Basic reactive tab management
|
|
251
|
+
- `useLoadingTab(loading, baseTitle)` - Loading state management
|
|
252
|
+
- `useNotificationTab(count, baseTitle, baseIcon)` - Notification badges
|
|
253
|
+
- `useStatusTab(status, baseTitle)` - Status-based updates
|
|
254
|
+
|
|
255
|
+
### Tab Closing Behavior
|
|
256
|
+
|
|
257
|
+
When a tab is closed, the plugin automatically navigates to the next available tab with the following priority:
|
|
258
|
+
|
|
259
|
+
1. **Next tab** - The tab immediately to the right
|
|
260
|
+
2. **Previous tab** - The tab immediately to the left
|
|
261
|
+
3. **First tab** - If no adjacent tabs exist
|
|
262
|
+
4. **Default route** - If no other tabs exist
|
|
263
|
+
|
|
82
264
|
## Cookie persistence
|
|
83
265
|
|
|
84
266
|
`<router-tab cookie-key="…" />` is usually all you need. If you prefer fine grained control (custom expiry, same-site, etc.) you can still use the headless helper:
|
|
@@ -152,7 +334,39 @@ You can override the default routed view by providing a `#default` slot. The slo
|
|
|
152
334
|
</router-tab>
|
|
153
335
|
```
|
|
154
336
|
|
|
155
|
-
##
|
|
337
|
+
## Advanced Features
|
|
338
|
+
|
|
339
|
+
### Tab Sorting
|
|
340
|
+
|
|
341
|
+
Enable drag-and-drop tab reordering with the `sortable` prop:
|
|
342
|
+
|
|
343
|
+
```vue
|
|
344
|
+
<router-tab
|
|
345
|
+
:sortable="true"
|
|
346
|
+
@tab-sort="onTabSort"
|
|
347
|
+
@tab-sorted="onTabSorted"
|
|
348
|
+
/>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### Persistence Options
|
|
354
|
+
|
|
355
|
+
Fine-grained control over tab persistence:
|
|
356
|
+
|
|
357
|
+
```vue
|
|
358
|
+
<router-tab
|
|
359
|
+
:persistence="{
|
|
360
|
+
cookieKey: 'my-app-tabs',
|
|
361
|
+
expiresInDays: 30,
|
|
362
|
+
fallbackRoute: '/dashboard',
|
|
363
|
+
serialize: (snapshot) => btoa(JSON.stringify(snapshot)),
|
|
364
|
+
deserialize: (data) => JSON.parse(atob(data))
|
|
365
|
+
}"
|
|
366
|
+
/>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Customising the Context Menu
|
|
156
370
|
|
|
157
371
|
```vue
|
|
158
372
|
<router-tab
|
|
@@ -171,6 +385,15 @@ You can override the default routed view by providing a `#default` slot. The slo
|
|
|
171
385
|
|
|
172
386
|
Pass `false` to disable the context menu entirely.
|
|
173
387
|
|
|
388
|
+
### Built-in Menu Items
|
|
389
|
+
|
|
390
|
+
- `refresh` - Refresh current tab
|
|
391
|
+
- `refreshAll` - Refresh all tabs
|
|
392
|
+
- `close` - Close current tab
|
|
393
|
+
- `closeLefts` - Close tabs to the left
|
|
394
|
+
- `closeRights` - Close tabs to the right
|
|
395
|
+
- `closeOthers` - Close all other tabs
|
|
396
|
+
|
|
174
397
|
## Slots
|
|
175
398
|
|
|
176
399
|
- `start` / `end` – positioned on either side of the tab list (ideal for toolbars or the `<router-tabs>` helper).
|
package/dist/vue3-router-tab.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--router-tab-header-height: 42px;--router-tab-padding: 12px;--router-tab-font-size: 14px}.router-tab{display:flex;flex-direction:column;min-height:300px;background-color:var(--router-tab-background);color:var(--router-tab-text)}.router-tab__header{position:relative;z-index:10;display:flex;flex:none;align-items:center;box-sizing:border-box;height:var(--router-tab-header-height);background-color:var(--router-tab-header-bg);border-bottom:1px solid var(--router-tab-border);transition:all .2s ease}.router-tab__scroll{position:relative;flex:1 1 0px;height:100%;overflow:hidden}.router-tab__scroll-container{width:100%;height:100%;overflow:hidden}.router-tab__scroll-container.is-mobile{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.router-tab__scroll-container.is-mobile::-webkit-scrollbar{display:none}.router-tab__nav{position:relative;display:inline-flex;flex-wrap:nowrap;height:100%;margin:0;padding:0;list-style:none}.router-tab__item{position:relative;display:flex;flex:none;align-items:center;gap:.5rem;padding:0 var(--router-tab-padding);color:var(--router-tab-text);font-size:var(--router-tab-font-size);background-color:transparent;border:none;border-right:1px solid var(--router-tab-border);cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--router-tab-transition)}.router-tab__item:first-child{border-left:1px solid var(--router-tab-border)}.router-tab__item:hover{background-color:color-mix(in srgb,var(--router-tab-primary) 4%,transparent);color:var(--router-tab-primary)}.router-tab__item.is-active{background-color:var(--router-tab-active-background);color:var(--router-tab-active-text);font-weight:510}.router-tab__item.is-active:after{content:"";position:absolute;bottom:-1px;left:0;right:0;height:2px;background-color:var(--router-tab-active-border)}.router-tab__item.is-active+.router-tab__item{border-left-color:transparent}.router-tab__item-icon{flex-shrink:0;font-size:14px;color:var(--router-tab-icon-color)}.router-tab__item.is-active .router-tab__item-icon{color:var(--router-tab-active-text)}.router-tab__item:hover .router-tab__item-icon{color:currentColor}.router-tab__item-title{min-width:60px;max-width:180px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.router-tab__item-close{position:relative;display:block;width:0;height:12px;flex-shrink:0;overflow:hidden;border:none;border-radius:50%;background:var(--router-tab-button-bg);color:var(--router-tab-button-color);cursor:pointer;transition:var(--router-tab-transition)}.router-tab__item-close:before,.router-tab__item-close:after{position:absolute;top:50%;left:50%;display:block;width:8px;height:1px;margin-left:-4px;background-color:currentColor;transition:background-color .2s ease-in-out;content:""}.router-tab__item-close:before{transform:translateY(-50%) rotate(-45deg)}.router-tab__item-close:after{transform:translateY(-50%) rotate(45deg)}.router-tab__item-close:hover{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}.router-tab__item:hover.is-closable,.router-tab__item.is-active.is-closable{padding-right:calc(var(--router-tab-padding) - 8px)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.router-tab__container{position:relative;flex:1 1 auto;background-color:var(--router-tab-background);overflow:hidden}.router-tab__contextmenu{position:fixed;z-index:1000;min-width:160px;padding:6px 0;font-size:var(--router-tab-font-size);background:var(--router-tab-background);border:1px solid var(--router-tab-border);border-radius:8px;box-shadow:0 2px 8px #0000001a}.router-tab__contextmenu-item{display:block;width:100%;padding:0 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-bg);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out}.router-tab__contextmenu-item[aria-disabled=true]{color:color-mix(in srgb,var(--router-tab-text) 40%);pointer-events:none;cursor:not-allowed}.router-tab__contextmenu-item:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}
|
|
1
|
+
:root{--router-tab-header-height: 42px;--router-tab-padding: 12px;--router-tab-font-size: 14px;--router-tab-transition: all .3s ease;--router-tab-primary: #3b82f6;--router-tab-background: #ffffff;--router-tab-text: #0f172a;--router-tab-border: #e2e8f0;--router-tab-header-bg: #ffffff;--router-tab-active-background: #3b82f6;--router-tab-active-text: #ffffff;--router-tab-active-border: #3b82f6;--router-tab-icon-color: #475569}.router-tab{display:flex;flex-direction:column;min-height:300px;background-color:var(--router-tab-background);color:var(--router-tab-text)}.router-tab__header{position:relative;z-index:10;display:flex;flex:none;align-items:center;box-sizing:border-box;height:var(--router-tab-header-height);background-color:var(--router-tab-header-bg);border-bottom:1px solid var(--router-tab-border);transition:all .2s ease}.router-tab__scroll{position:relative;flex:1 1 0px;height:100%;overflow:hidden}.router-tab__scroll-container{width:100%;height:100%;overflow:hidden}.router-tab__scroll-container.is-mobile{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}.router-tab__scroll-container.is-mobile::-webkit-scrollbar{display:none}.router-tab__nav{position:relative;display:inline-flex;flex-wrap:nowrap;height:100%;margin:0;padding:0;list-style:none}.router-tab__item{position:relative;display:flex;flex:none;align-items:center;gap:.5rem;padding:0 var(--router-tab-padding);color:var(--router-tab-text);font-size:var(--router-tab-font-size);background-color:transparent;border:none;border-right:1px solid var(--router-tab-border);cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--router-tab-transition)}.router-tab__item:first-child{border-left:1px solid var(--router-tab-border)}.router-tab__item:hover{background-color:color-mix(in srgb,var(--router-tab-primary) 4%,transparent);color:var(--router-tab-primary)}.router-tab__item.is-active{background-color:var(--router-tab-active-background);color:var(--router-tab-active-text);font-weight:510}.router-tab__item.is-active:after{content:"";position:absolute;bottom:-1px;left:0;right:0;height:2px;background-color:var(--router-tab-active-border)}.router-tab__item.is-active+.router-tab__item{border-left-color:transparent}.router-tab__item-icon{flex-shrink:0;font-size:14px;color:var(--router-tab-icon-color)}.router-tab__item.is-active .router-tab__item-icon{color:var(--router-tab-active-text)}.router-tab__item:hover .router-tab__item-icon{color:currentColor}.router-tab__item-title{min-width:60px;max-width:180px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.router-tab__item-close{position:relative;display:block;width:0;height:12px;flex-shrink:0;overflow:hidden;border:none;border-radius:50%;background:var(--router-tab-button-bg);color:var(--router-tab-button-color);cursor:pointer;transition:var(--router-tab-transition)}.router-tab__item-close:before,.router-tab__item-close:after{position:absolute;top:50%;left:50%;display:block;width:8px;height:1px;margin-left:-4px;background-color:currentColor;transition:background-color .2s ease-in-out;content:""}.router-tab__item-close:before{transform:translateY(-50%) rotate(-45deg)}.router-tab__item-close:after{transform:translateY(-50%) rotate(45deg)}.router-tab__item-close:hover{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}.router-tab__item:hover.is-closable,.router-tab__item.is-active.is-closable{padding-right:calc(var(--router-tab-padding) - 8px)}.router-tab__item:hover .router-tab__item-close,.router-tab__item.is-active .router-tab__item-close{width:12px;margin-left:4px}.router-tab__container{position:relative;flex:1 1 auto;background-color:var(--router-tab-background);overflow:hidden}.router-tab__container{padding:10px;border:1px solid var(--router-tab-border)}.router-tab__contextmenu{position:fixed;z-index:1000;min-width:160px;padding:6px 0;font-size:var(--router-tab-font-size);background:var(--router-tab-background);border:1px solid var(--router-tab-border);border-radius:8px;box-shadow:0 2px 8px #0000001a}.router-tab__contextmenu-item{display:block;width:100%;padding:0 12px;line-height:32px;text-align:left;text-decoration:none;background:var(--router-tab-button-bg);border:none;color:var(--router-tab-button-color);cursor:pointer;font:inherit;border-radius:4px;transition:all .2s ease-in-out}.router-tab__contextmenu-item[aria-disabled=true]{color:color-mix(in srgb,var(--router-tab-text) 40%);pointer-events:none;cursor:not-allowed}.router-tab__contextmenu-item:hover:not([aria-disabled=true]),.router-tab__contextmenu-item:focus-visible{background:var(--router-tab-active-button-bg);color:var(--router-tab-active-button-color)}
|