vue3-router-tab 1.0.9 → 1.1.1

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.
@@ -1,12 +1 @@
1
- (function(v,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue"),require("pinia")):typeof define=="function"&&define.amd?define(["exports","vue","pinia"],t):(v=typeof globalThis<"u"?globalThis:v||self,t(v["vue3-router-tab"]={},v.Vue,v.pinia))})(this,(function(v,t,re){"use strict";/*!
2
- * vue-router v4.5.1
3
- * (c) 2025 Eduardo San Martin Morote
4
- * @license MIT
5
- */const se=typeof document<"u",le=Object.assign,ce=Array.isArray;function ue(e){const a=Array.from(arguments).slice(1);console.warn.apply(console,["[Vue Router warn]: "+e].concat(a))}function fe(e,a){return(e.aliasOf||e)===(a.aliasOf||a)}var X;(function(e){e.pop="pop",e.push="push"})(X||(X={}));var Y;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Y||(Y={})),Symbol(process.env.NODE_ENV!=="production"?"navigation failure":"");var Q;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Q||(Q={}));const de=Symbol(process.env.NODE_ENV!=="production"?"router view location matched":""),W=Symbol(process.env.NODE_ENV!=="production"?"router view depth":"");Symbol(process.env.NODE_ENV!=="production"?"router":""),Symbol(process.env.NODE_ENV!=="production"?"route location":"");const Z=Symbol(process.env.NODE_ENV!=="production"?"router view location":""),pe=t.defineComponent({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:a,slots:o}){process.env.NODE_ENV!=="production"&&me();const n=t.inject(Z),c=t.computed(()=>e.route||n.value),b=t.inject(W,0),p=t.computed(()=>{let h=t.unref(b);const{matched:m}=c.value;let g;for(;(g=m[h])&&!g.components;)h++;return h}),s=t.computed(()=>c.value.matched[p.value]);t.provide(W,t.computed(()=>p.value+1)),t.provide(de,s),t.provide(Z,c);const d=t.ref();return t.watch(()=>[d.value,s.value,e.name],([h,m,g],[R,w,S])=>{m&&(m.instances[g]=h,w&&w!==m&&h&&h===R&&(m.leaveGuards.size||(m.leaveGuards=w.leaveGuards),m.updateGuards.size||(m.updateGuards=w.updateGuards))),h&&m&&(!w||!fe(m,w)||!R)&&(m.enterCallbacks[g]||[]).forEach(C=>C(h))},{flush:"post"}),()=>{const h=c.value,m=e.name,g=s.value,R=g&&g.components[m];if(!R)return ee(o.default,{Component:R,route:h});const w=g.props[m],S=w?w===!0?h.params:typeof w=="function"?w(h):w:null,C=E=>{E.component.isUnmounted&&(g.instances[m]=null)},T=t.h(R,le({},S,a,{onVnodeUnmounted:C,ref:d}));if(process.env.NODE_ENV!=="production"&&se&&T.ref){const E={depth:p.value,name:g.name,path:g.path,meta:g.meta};(ce(T.ref)?T.ref.map(A=>A.i):[T.ref.i]).forEach(A=>{A.__vrv_devtools=E})}return ee(o.default,{Component:T,route:h})||T}}});function ee(e,a){if(!e)return null;const o=e(a);return o.length===1?o[0]:o}const be=pe;function me(){const e=t.getCurrentInstance(),a=e.parent&&e.parent.type.name,o=e.parent&&e.parent.subTree&&e.parent.subTree.type;if(a&&(a==="KeepAlive"||a.includes("Transition"))&&typeof o=="object"&&o.name==="RouterView"){const n=a==="KeepAlive"?"keep-alive":"transition";ue(`<router-view> can no longer be used directly inside <transition> or <keep-alive>.
6
- Use slot props instead:
7
-
8
- <router-view v-slot="{ Component }">
9
- <${n}>
10
- <component :is="Component" />
11
- </${n}>
12
- </router-view>`)}}function he(e={}){return{initialTabs:e.initialTabs??[],keepAlive:e.keepAlive??!0,maxAlive:e.maxAlive??0,keepLastTab:e.keepLastTab??!0,appendPosition:e.appendPosition??"last",defaultRoute:e.defaultRoute??"/"}}function V(e,a){const o=e.resolve(a);if(!o||!o.matched.length)throw new Error(`[RouterTabs] Unable to resolve route: ${String(a)}`);return o}const ye={path:e=>e.path,fullpath:e=>e.fullPath,fullname:e=>e.fullPath,full:e=>e.fullPath,name:e=>e.name?String(e.name):e.fullPath};function N(e){const a=e.meta?.key;if(typeof a=="function"){const o=a(e);if(typeof o=="string"&&o.length)return o}else if(typeof a=="string"&&a.length){const o=ye[a.toLowerCase()];return o?o(e):a}return e.fullPath}function M(e,a){const o=e.meta?.keepAlive;return typeof o=="boolean"?o:a}function j(e,a){const o=e.meta?.reuse;return typeof o=="boolean"?o:a}function te(e){const a=e.meta??{},o={};return"title"in a&&(o.title=a.title),"tips"in a&&(o.tips=a.tips),"icon"in a&&(o.icon=a.icon),"closable"in a&&(o.closable=a.closable),"tabClass"in a&&(o.tabClass=a.tabClass),"target"in a&&(o.target=a.target),"href"in a&&(o.href=a.href),o}function $(e,a,o){const n=te(e);return{id:N(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:M(e,o),reusable:j(e,!1),closable:n.closable??!0,...n,...a}}function L(e,a,o,n){if(!e.find(b=>b.id===a.id)){if(o==="next"&&n){const b=e.findIndex(p=>p.id===n);if(b>-1){e.splice(b+1,0,a);return}}e.push(a)}}function ne(e,a,o){if(!a||a<=0)return;const n=e.filter(c=>c.alive);for(;n.length>a;){const c=n.shift();if(!c||c.id===o)continue;const b=e.findIndex(p=>p.id===c.id);b>-1&&(e[b].alive=!1)}}function ge(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function we(e){const a={};return"title"in e&&(a.title=e.title),"tips"in e&&(a.tips=e.tips),"icon"in e&&(a.icon=e.icon),"tabClass"in e&&(a.tabClass=e.tabClass),"closable"in e&&(a.closable=e.closable),a}function Te(e,a={}){const o=he(a),n=t.reactive([]),c=t.ref(null),b=t.shallowRef(),p=t.ref(null),s=t.computed(()=>n.filter(r=>r.alive).map(r=>r.id));let d=!1;function h(r){const l=typeof r.matched=="object"?r:V(e,r);return{key:N(l),fullPath:l.fullPath,alive:M(l,o.keepAlive),reusable:j(l,!1),matched:l}}function m(r){const l=N(r);let u=n.find(k=>k.id===l);return u?(u.fullPath=r.fullPath,u.to=r.fullPath,u.matched=r,u.alive=M(r,o.keepAlive),u.reusable=j(r,u.reusable),Object.assign(u,te(r)),u):(u=$(r,{},o.keepAlive),L(n,u,o.appendPosition,c.value),ne(n,o.maxAlive,c.value),u)}async function g(r,l=!1,u=!0){const k=V(e,r),P=N(k),x=c.value===P;u==="sameTab"&&(u=x),u&&await C(P,!0),await e[l?"replace":"push"](k),x&&await O()}function R(r){const l=n.findIndex(k=>k.id===r),u=n[l]||n[l-1]||n[0];return u?u.to:o.defaultRoute}async function w(r=c.value,l={}){if(r){if(!l.force&&o.keepLastTab&&n.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");if(await S(r,{force:l.force}),l.redirect!==null)if(c.value===r){const u=l.redirect??R(r);u&&await e.replace(u)}else l.redirect&&await e.replace(l.redirect)}}async function S(r,l={}){const u=n.findIndex(k=>k.id===r);u!==-1&&(n.splice(u,1),p.value===r&&(p.value=null),c.value===r&&(c.value=null,b.value=void 0))}async function C(r=c.value??void 0,l=!1){r&&(p.value=r,await t.nextTick(),l||await t.nextTick(),p.value=null)}async function T(r=!1){for(const l of n)await C(l.id,r)}async function E(r=o.defaultRoute){n.splice(0,n.length),c.value=null,b.value=void 0;for(const l of o.initialTabs){const u=V(e,l.to),k=$(u,l,o.keepAlive);n.push(k)}await e.replace(r)}async function O(){const r=c.value;r&&await C(r,!0)}function A(r){return typeof r.matched=="object"?N(r):N(V(e,r))}function J(){const r=n.find(l=>l.id===c.value);return{tabs:n.map(ge),active:r?r.to:null}}async function q(r){d=!0,n.splice(0,n.length),c.value=null,b.value=void 0;const l=r?.tabs??[];for(const k of l)try{const P=V(e,k.to),x=we(k),K=$(P,x,o.keepAlive);L(n,K,"last",null)}catch{}d=!1;const u=r?.active??l[l.length-1]?.to??o.defaultRoute;if(u)try{await e.replace(u)}catch{}}return t.watch(()=>e.currentRoute.value,r=>{if(d)return;const l=m(r);c.value=l.id,b.value=l,ne(n,o.maxAlive,c.value)},{immediate:!0}),o.initialTabs.length&&o.initialTabs.forEach(r=>{const l=V(e,r.to),u=$(l,r,o.keepAlive);L(n,u,"last",null)}),{options:o,tabs:n,activeId:c,current:b,includeKeys:s,refreshingKey:p,openTab:g,closeTab:w,removeTab:S,refreshTab:C,refreshAll:T,reset:E,reload:O,getRouteKey:A,matchRoute:h,snapshot:J,hydrate:q}}function oe(e){return e?typeof e=="string"?{name:e}:e:{}}const B=Symbol("RouterTabsContext"),D=typeof window<"u"&&"sessionStorage"in window,ke=t.defineComponent({name:"RouterTab",components:{RouterView:be},props:{tabs:{type:Array,default:()=>[]},keepAlive:{type:Boolean,default:!0},maxAlive:{type:Number,default:0},keepLastTab:{type:Boolean,default:!0},append:{type:String,default:"last"},defaultPage:{type:[String,Object],default:"/"},tabTransition:{type:[String,Object],default:"router-tab-zoom"},pageTransition:{type:[String,Object],default:()=>({name:"router-tab-swap",mode:"out-in"})},contextmenu:{type:[Boolean,Array],default:!0},storage:{type:[Boolean,String],default:!1}},setup(e){const a=t.getCurrentInstance();if(!a)throw new Error("[RouterTab] component must be used within a Vue application context.");const o=a.appContext.app.config.globalProperties.$router;if(!o)throw new Error("[RouterTab] Vue Router is required. Make sure to call app.use(router) before RouterTab.");const n=Te(o,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});t.provide(B,n),a.appContext.config.globalProperties.$tabs=n;const c=t.computed(()=>oe(e.tabTransition)),b=t.computed(()=>oe(e.pageTransition)),p=t.reactive({visible:!1,target:null,position:{x:0,y:0}}),s=t.computed(()=>!e.storage||!D?null:typeof e.storage=="string"?e.storage:`router-tabs:${(o.options?.history?.base??"")||"default"}`);let d=!!s.value;const h=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function m(i){return n.tabs.findIndex(f=>f.id===i)}function g(i){const f=m(i.id);return f>0?n.tabs.slice(0,f):[]}function R(i){const f=m(i.id);return f>-1?n.tabs.slice(f+1):[]}function w(i){return n.tabs.filter(f=>f.id!==i.id)}async function S(i,f){const y=i.filter(_=>_.closable!==!1);if(y.length){for(const _ of y)n.activeId.value===_.id?await n.closeTab(_.id,{redirect:f.to,force:!0}):await n.removeTab(_.id,{force:!0});n.activeId.value!==f.id&&await n.openTab(f.to,!0,!1)}}const C={refresh:{label:"Refresh",handler:async({target:i})=>{await n.refreshTab(i.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await n.refreshAll(!0)}},close:{label:"Close",handler:async({target:i})=>{await n.closeTab(i.id)},enable:({target:i})=>r(i)},closeLefts:{label:"Close to the Left",handler:async({target:i})=>{await S(g(i),i)},enable:({target:i})=>g(i).some(f=>f.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:i})=>{await S(R(i),i)},enable:({target:i})=>R(i).some(f=>f.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:i})=>{await S(w(i),i)},enable:({target:i})=>w(i).some(f=>f.closable!==!1)}};function T(){p.visible=!1,p.target=null}function E(i,f){e.contextmenu&&(p.visible=!0,p.target=i,p.position.x=f.clientX,p.position.y=f.clientY,document.addEventListener("click",T,{once:!0}))}function O(i,f){const y=typeof i=="string"?{id:i}:i,_=C[y.id],je=y.label??_?.label??String(y.id),H=y.visible??_?.visible??!0;if(!(typeof H=="function"?H(f):H!==!1))return null;const F=y.enable??_?.enable??!0,Le=typeof F=="function"?F(f):F!==!1,ie=y.handler??_?.handler;if(!ie)return null;const De=async()=>{await Promise.resolve(ie(f))};return{id:String(y.id),label:je,disabled:!Le,action:De}}const A=t.computed(()=>{if(!p.visible||!p.target||e.contextmenu===!1)return[];const i=Array.isArray(e.contextmenu)?e.contextmenu:h,f={target:p.target,controller:n};return i.map(y=>O(y,f)).filter(y=>!!y)});async function J(i){i.disabled||(T(),await i.action())}function q(i){return typeof i.title=="string"?i.title:Array.isArray(i.title)&&i.title.length?String(i.title[0]):i.fullPath}function r(i){return!(i.closable===!1||n.options.keepLastTab&&n.tabs.length<=1)}async function l(i){await n.closeTab(i.id)}function u(i){n.activeId.value!==i.id&&n.openTab(i.to,!1)}function k(i){return["router-tab__item",{"is-active":n.activeId.value===i.id,"is-closable":r(i)},i.tabClass]}function P(i){return n.refreshingKey.value===n.getRouteKey(i)}async function x(){const i=s.value;if(!i||!D)return;const f=window.sessionStorage.getItem(i);if(f)try{const y=JSON.parse(f);if(!y||!Array.isArray(y.tabs))return;d=!0,await n.hydrate(y)}catch{}finally{d=!1,K()}}function K(){const i=s.value;if(!(!i||!D||d))try{const f=n.snapshot();window.sessionStorage.setItem(i,JSON.stringify(f))}catch{}}t.onMounted(()=>{document.addEventListener("keydown",T),x()}),t.onBeforeUnmount(()=>{document.removeEventListener("keydown",T),a.appContext.config.globalProperties.$tabs=null,K()}),t.watch(()=>e.keepAlive,i=>{n.options.keepAlive=i}),t.watch(()=>n.activeId.value,()=>T()),t.watch(()=>e.contextmenu,i=>{i||T()}),t.watch(()=>A.value.length,i=>{p.visible&&i===0&&T()}),t.watch(()=>({key:s.value,tabs:n.tabs.map(i=>({to:i.to,title:i.title,tips:i.tips,icon:i.icon,tabClass:i.tabClass,closable:i.closable})),active:n.activeId.value}),()=>{K()},{deep:!0});const Me=n.includeKeys;return{controller:n,tabs:n.tabs,includeKeys:Me,tabTransitionProps:c,pageTransitionProps:b,buildTabClass:k,activate:u,close:l,context:p,menuItems:A,handleMenuAction:J,showContextMenu:E,hideContextMenu:T,tabTitle:q,isClosable:r,isRefreshing:P}}}),ve=(e,a)=>{const o=e.__vccOpts||e;for(const[n,c]of a)o[n]=c;return o},Re={class:"router-tab"},Ce={class:"router-tab__header"},_e={class:"router-tab__slot-start"},Se={class:"router-tab__scroll"},Ae=["onClick","onAuxclick","onContextmenu"],Ee=["title"],Pe=["onClick"],Ve={class:"router-tab__slot-end"},Ne={class:"router-tab__container"},xe=["aria-disabled","onClick"];function Be(e,a,o,n,c,b){const p=t.resolveComponent("RouterView");return t.openBlock(),t.createElementBlock("div",Re,[t.createElementVNode("header",Ce,[t.createElementVNode("div",_e,[t.renderSlot(e.$slots,"start")]),t.createElementVNode("div",Se,[t.createVNode(t.TransitionGroup,t.mergeProps({tag:"ul",class:"router-tab__nav"},e.tabTransitionProps),{default:t.withCtx(()=>[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(e.tabs,s=>(t.openBlock(),t.createElementBlock("li",{key:s.id,class:t.normalizeClass(e.buildTabClass(s)),onClick:d=>e.activate(s),onAuxclick:t.withModifiers(d=>e.close(s),["middle","prevent"]),onContextmenu:t.withModifiers(d=>e.showContextMenu(s,d),["prevent"])},[t.createElementVNode("span",{class:"router-tab__item-title",title:e.tabTitle(s)},[s.icon?(t.openBlock(),t.createElementBlock("i",{key:0,class:t.normalizeClass(["router-tab__item-icon",s.icon])},null,2)):t.createCommentVNode("",!0),t.createTextVNode(" "+t.toDisplayString(e.tabTitle(s)),1)],8,Ee),e.isClosable(s)?(t.openBlock(),t.createElementBlock("a",{key:0,class:"router-tab__item-close",onClick:t.withModifiers(d=>e.close(s),["stop"])},null,8,Pe)):t.createCommentVNode("",!0)],42,Ae))),128))]),_:1},16)]),t.createElementVNode("div",Ve,[t.renderSlot(e.$slots,"end")])]),t.createElementVNode("div",Ne,[t.createVNode(p,null,{default:t.withCtx(({Component:s,route:d})=>[t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[e.controller.options.keepAlive?(t.openBlock(),t.createBlock(t.KeepAlive,{key:0,include:e.includeKeys,max:e.controller.options.maxAlive||void 0},[e.isRefreshing(d)?t.createCommentVNode("",!0):(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s),{key:e.controller.getRouteKey(d),class:"router-tab-page"}))],1032,["include","max"])):t.createCommentVNode("",!0)]),_:2},1040),t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[!e.controller.options.keepAlive||e.isRefreshing(d)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s),{key:e.controller.getRouteKey(d)+(e.isRefreshing(d)?"-refresh":""),class:"router-tab-page"})):t.createCommentVNode("",!0)]),_:2},1040)]),_:1})]),e.context.visible&&e.context.target?(t.openBlock(),t.createElementBlock("div",{key:0,class:"router-tab__contextmenu",style:t.normalizeStyle({left:e.context.position.x+"px",top:e.context.position.y+"px"})},[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(e.menuItems,s=>(t.openBlock(),t.createElementBlock("a",{key:s.id,class:"router-tab__contextmenu-item","aria-disabled":s.disabled,onClick:t.withModifiers(d=>e.handleMenuAction(s),["prevent"])},t.toDisplayString(s.label),9,xe))),128))],4)):t.createCommentVNode("",!0)])}const z=ve(ke,[["render",Be]]);function G(e={}){const{optional:a=!1}=e,o=t.inject(B,null);if(o)return o;const n=t.inject("$tabs",null);if(n)return n;const b=t.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(b)return b;if(!a)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const Ie="router-tabs:persistent";function Oe(e){return typeof window>"u"?null:e===void 0?window.localStorage??null:e}function Ke(e){const a=Oe(e.storage),o=e.storageKey??Ie;return re.defineStore(e.storeId??"routerTabs",{state:()=>({snapshot:null}),actions:{load(){if(!(!a||this.snapshot))try{const n=a.getItem(o);n&&(this.snapshot=JSON.parse(n))}catch{}},setSnapshot(n){if(this.snapshot=n,!!a)try{n&&n.tabs.length?a.setItem(o,JSON.stringify(n)):a.removeItem(o)}catch{}},clear(){this.setSnapshot(null)}}})}function ae(e={}){const o=(e.store??Ke(e))(),n=t.ref(!1);let c=!1;const b=s=>{!s||c||(c=!0,t.onMounted(async()=>{o.load();const d=o.snapshot;if(d&&d.tabs?.length)try{n.value=!0,await s.hydrate(d)}finally{n.value=!1}else try{n.value=!0;const h=e.fallbackRoute??s.options.defaultRoute;await s.reset(h)}finally{n.value=!1}o.setSnapshot(s.snapshot())}),t.watch(()=>({tabs:s.tabs.map(d=>({to:d.to,title:d.title,tips:d.tips,icon:d.icon,tabClass:d.tabClass,closable:d.closable})),active:s.activeId.value}),()=>{n.value||o.setSnapshot(s.snapshot())},{deep:!0}))},p=G({optional:!0});return p?b(p):t.onMounted(()=>{const s=G({optional:!0});s&&b(s)}),o}const $e={class:"router-tabs","aria-hidden":"true"},I=t.defineComponent({name:"RouterTabs",__name:"RouterTabsPinia",props:{storeId:{},storageKey:{},storage:{},fallbackRoute:{},store:{type:[Function,Object]}},setup(e){return ae(e),(o,n)=>(t.openBlock(),t.createElementBlock("span",$e))}}),U={install(e){if(U._installed)return;U._installed=!0;const a=z.name||"RouterTab",o=I.name||"RouterTabs";e.component(a,z),e.component(o,I),o!=="router-tabs"&&e.component("router-tabs",I),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[B]},set(n){n&&e.provide(B,n)}})}};v.RouterTab=z,v.RouterTabs=I,v.RouterTabsPinia=I,v.default=U,v.routerTabsKey=B,v.useRouterTabs=G,v.useRouterTabsPiniaPersistence=ae,Object.defineProperties(v,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
1
+ (function(y,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue"),require("vue-router")):typeof define=="function"&&define.amd?define(["exports","vue","vue-router"],t):(y=typeof globalThis<"u"?globalThis:y||self,t(y["vue3-router-tab"]={},y.Vue,y.VueRouter))})(this,(function(y,t,Z){"use strict";function ee(e={}){return{initialTabs:e.initialTabs??[],keepAlive:e.keepAlive??!0,maxAlive:e.maxAlive??0,keepLastTab:e.keepLastTab??!0,appendPosition:e.appendPosition??"last",defaultRoute:e.defaultRoute??"/"}}function w(e,o){const a=e.resolve(o);if(!a||!a.matched.length)throw new Error(`[RouterTabs] Unable to resolve route: ${String(o)}`);return a}const te={path:e=>e.path,fullpath:e=>e.fullPath,fullname:e=>e.fullPath,full:e=>e.fullPath,name:e=>e.name?String(e.name):e.fullPath};function R(e){const o=e.meta?.key;if(typeof o=="function"){const a=o(e);if(typeof a=="string"&&a.length)return a}else if(typeof o=="string"&&o.length){const a=te[o.toLowerCase()];return a?a(e):o}return e.fullPath}function I(e,o){const a=e.meta?.keepAlive;return typeof a=="boolean"?a:o}function K(e,o){const a=e.meta?.reuse;return typeof a=="boolean"?a:o}function J(e){const o=e.meta??{},a={};return"title"in o&&(a.title=o.title),"tips"in o&&(a.tips=o.tips),"icon"in o&&(a.icon=o.icon),"closable"in o&&(a.closable=o.closable),"tabClass"in o&&(a.tabClass=o.tabClass),"target"in o&&(a.target=o.target),"href"in o&&(a.href=o.href),a}function B(e,o,a){const n=J(e);return{id:R(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:I(e,a),reusable:K(e,!1),closable:n.closable??!0,...n,...o}}function N(e,o,a,n){if(!e.find(b=>b.id===o.id)){if(a==="next"&&n){const b=e.findIndex(p=>p.id===n);if(b>-1){e.splice(b+1,0,o);return}}e.push(o)}}function H(e,o,a){if(!o||o<=0)return;const n=e.filter(c=>c.alive);for(;n.length>o;){const c=n.shift();if(!c||c.id===a)continue;const b=e.findIndex(p=>p.id===c.id);b>-1&&(e[b].alive=!1)}}function ne(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function oe(e){const o={};return"title"in e&&(o.title=e.title),"tips"in e&&(o.tips=e.tips),"icon"in e&&(o.icon=e.icon),"tabClass"in e&&(o.tabClass=e.tabClass),"closable"in e&&(o.closable=e.closable),o}function ie(e,o={}){const a=ee(o),n=t.reactive([]),c=t.ref(null),b=t.shallowRef(),p=t.ref(null),s=t.computed(()=>n.filter(l=>l.alive).map(l=>l.id));let f=!1;function T(l){const r=typeof l.matched=="object"?l:w(e,l);return{key:R(r),fullPath:r.fullPath,alive:I(r,a.keepAlive),reusable:K(r,!1),matched:r}}function A(l){const r=R(l);let u=n.find(m=>m.id===r);return u?(u.fullPath=l.fullPath,u.to=l.fullPath,u.matched=l,u.alive=I(l,a.keepAlive),u.reusable=K(l,u.reusable),Object.assign(u,J(l)),u):(u=B(l,{},a.keepAlive),N(n,u,a.appendPosition,c.value),H(n,a.maxAlive,c.value),u)}async function E(l,r=!1,u=!0){const m=w(e,l),C=R(m),i=c.value===C;u==="sameTab"&&(u=i),u&&await g(C,!0),await e[r?"replace":"push"](m),i&&await v()}function S(l){const r=n.findIndex(m=>m.id===l),u=n[r]||n[r-1]||n[0];return u?u.to:a.defaultRoute}async function P(l=c.value,r={}){if(l){if(!r.force&&a.keepLastTab&&n.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");if(await V(l,{force:r.force}),r.redirect!==null)if(c.value===l){const u=r.redirect??S(l);u&&await e.replace(u)}else r.redirect&&await e.replace(r.redirect)}}async function V(l,r={}){const u=n.findIndex(m=>m.id===l);u!==-1&&(n.splice(u,1),p.value===l&&(p.value=null),c.value===l&&(c.value=null,b.value=void 0))}async function g(l=c.value??void 0,r=!1){l&&(p.value=l,await t.nextTick(),r||await t.nextTick(),p.value=null)}async function D(l=!1){for(const r of n)await g(r.id,l)}async function O(l=a.defaultRoute){n.splice(0,n.length),c.value=null,b.value=void 0;for(const r of a.initialTabs){const u=w(e,r.to),m=B(u,r,a.keepAlive);n.push(m)}await e.replace(l)}async function v(){const l=c.value;l&&await g(l,!0)}function U(l){return typeof l.matched=="object"?R(l):R(w(e,l))}function F(){const l=n.find(r=>r.id===c.value);return{tabs:n.map(ne),active:l?l.to:null}}async function x(l){f=!0,n.splice(0,n.length),c.value=null,b.value=void 0;const r=l?.tabs??[];for(const m of r)try{const C=w(e,m.to),i=oe(m),d=B(C,i,a.keepAlive);N(n,d,"last",null)}catch{}f=!1;const u=l?.active??r[r.length-1]?.to??a.defaultRoute;if(u)try{await e.replace(u)}catch{}}return t.watch(()=>e.currentRoute.value,l=>{if(f)return;const r=A(l);c.value=r.id,b.value=r,H(n,a.maxAlive,c.value)},{immediate:!0}),a.initialTabs.length&&a.initialTabs.forEach(l=>{const r=w(e,l.to),u=B(r,l,a.keepAlive);N(n,u,"last",null)}),{options:a,tabs:n,activeId:c,current:b,includeKeys:s,refreshingKey:p,openTab:E,closeTab:P,removeTab:V,refreshTab:g,refreshAll:D,reset:O,reload:v,getRouteKey:U,matchRoute:T,snapshot:F,hydrate:x}}function Y(e){return e?typeof e=="string"?{name:e}:e:{}}const _=Symbol("RouterTabsContext"),ae="router-tabs:snapshot";function M(e={}){const{optional:o=!1}=e,a=t.inject(_,null);if(a)return a;const n=t.inject("$tabs",null);if(n)return n;const b=t.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(b)return b;if(!o)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const le=864e5;function se(e){if(typeof document>"u")return null;const o=`${encodeURIComponent(e)}=`,a=document.cookie?document.cookie.split("; "):[];for(const n of a)if(n.startsWith(o))return decodeURIComponent(n.slice(o.length));return null}function W(e,o,a){if(typeof document>"u")return;const{expiresInDays:n=7,path:c="/",domain:b,secure:p,sameSite:s="lax"}=a,f=[`${encodeURIComponent(e)}=${encodeURIComponent(o)}`];if(n!==1/0){const T=new Date(Date.now()+n*le).toUTCString();f.push(`Expires=${T}`)}c&&f.push(`Path=${c}`),b&&f.push(`Domain=${b}`),p&&f.push("Secure"),s&&f.push(`SameSite=${s.charAt(0).toUpperCase()}${s.slice(1)}`),document.cookie=f.join("; ")}function X(e,o){if(typeof document>"u")return;const{path:a="/",domain:n}=o,c=[`${encodeURIComponent(e)}=`];c.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),a&&c.push(`Path=${a}`),n&&c.push(`Domain=${n}`),document.cookie=c.join("; ")}const re=e=>JSON.stringify(e??null),ce=e=>{if(!e)return null;try{return JSON.parse(e)}catch{return null}};function L(e={}){const{cookieKey:o=ae,serialize:a=re,deserialize:n=ce}=e,c=M({optional:!0}),b=t.ref(!1),p=s=>{t.onMounted(async()=>{const f=n(se(o));if(f&&f.tabs?.length)try{b.value=!0,await s.hydrate(f)}finally{b.value=!1}else try{b.value=!0;const A=e.fallbackRoute??s.options.defaultRoute;await s.reset(A)}finally{b.value=!1}const T=s.snapshot();T.tabs.length?W(o,a(T),e):X(o,e)}),t.watch(()=>({tabs:s.tabs.map(f=>({to:f.to,title:f.title,tips:f.tips,icon:f.icon,tabClass:f.tabClass,closable:f.closable})),active:s.activeId.value}),()=>{if(b.value)return;const f=s.snapshot();f.tabs.length?W(o,a(f),e):X(o,e)},{deep:!0})};c?p(c):t.onMounted(()=>{const s=M({optional:!0});s&&p(s)})}const ue=t.defineComponent({name:"RouterTab",components:{RouterView:Z.RouterView},props:{tabs:{type:Array,default:()=>[]},keepAlive:{type:Boolean,default:!0},maxAlive:{type:Number,default:0},keepLastTab:{type:Boolean,default:!0},append:{type:String,default:"last"},defaultPage:{type:[String,Object],default:"/"},tabTransition:{type:[String,Object],default:"router-tab-zoom"},pageTransition:{type:[String,Object],default:()=>({name:"router-tab-swap",mode:"out-in"})},contextmenu:{type:[Boolean,Array],default:!0},cookieKey:{type:String,default:null},persistence:{type:Object,default:null}},setup(e){const o=t.getCurrentInstance();if(!o)throw new Error("[RouterTab] component must be used within a Vue application context.");const a=o.appContext.app.config.globalProperties.$router;if(!a)throw new Error("[RouterTab] Vue Router is required. Make sure to call app.use(router) before RouterTab.");const n=ie(a,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});t.provide(_,n),o.appContext.config.globalProperties.$tabs=n;const c=t.computed(()=>!!o?.slots?.default);if(e.cookieKey||e.persistence){const i={...e.persistence??{}};e.cookieKey&&(i.cookieKey=e.cookieKey),L(i)}const b=t.computed(()=>Y(e.tabTransition)),p=t.computed(()=>Y(e.pageTransition)),s=t.reactive({visible:!1,target:null,position:{x:0,y:0}}),f=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function T(i){return n.tabs.findIndex(d=>d.id===i)}function A(i){const d=T(i.id);return d>0?n.tabs.slice(0,d):[]}function E(i){const d=T(i.id);return d>-1?n.tabs.slice(d+1):[]}function S(i){return n.tabs.filter(d=>d.id!==i.id)}async function P(i,d){const h=i.filter(k=>k.closable!==!1);if(h.length){for(const k of h)n.activeId.value===k.id?await n.closeTab(k.id,{redirect:d.to,force:!0}):await n.removeTab(k.id,{force:!0});n.activeId.value!==d.id&&await n.openTab(d.to,!0,!1)}}const V={refresh:{label:"Refresh",handler:async({target:i})=>{await n.refreshTab(i.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await n.refreshAll(!0)}},close:{label:"Close",handler:async({target:i})=>{await n.closeTab(i.id)},enable:({target:i})=>x(i)},closeLefts:{label:"Close to the Left",handler:async({target:i})=>{await P(A(i),i)},enable:({target:i})=>A(i).some(d=>d.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:i})=>{await P(E(i),i)},enable:({target:i})=>E(i).some(d=>d.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:i})=>{await P(S(i),i)},enable:({target:i})=>S(i).some(d=>d.closable!==!1)}};function g(){s.visible=!1,s.target=null}function D(i,d){e.contextmenu&&(s.visible=!0,s.target=i,s.position.x=d.clientX,s.position.y=d.clientY,document.addEventListener("click",g,{once:!0}))}function O(i,d){const h=typeof i=="string"?{id:i}:i,k=V[h.id],Ae=h.label??k?.label??String(h.id),q=h.visible??k?.visible??!0;if(!(typeof q=="function"?q(d):q!==!1))return null;const G=h.enable??k?.enable??!0,_e=typeof G=="function"?G(d):G!==!1,Q=h.handler??k?.handler;if(!Q)return null;const Pe=async()=>{await Promise.resolve(Q(d))};return{id:String(h.id),label:Ae,disabled:!_e,action:Pe}}const v=t.computed(()=>{if(!s.visible||!s.target||e.contextmenu===!1)return[];const i=Array.isArray(e.contextmenu)?e.contextmenu:f,d={target:s.target,controller:n};return i.map(h=>O(h,d)).filter(h=>!!h)});async function U(i){i.disabled||(g(),await i.action())}function F(i){return typeof i.title=="string"?i.title:Array.isArray(i.title)&&i.title.length?String(i.title[0]):i.fullPath}function x(i){return!(i.closable===!1||n.options.keepLastTab&&n.tabs.length<=1)}async function l(i){await n.closeTab(i.id)}function r(i){n.activeId.value!==i.id&&n.openTab(i.to,!1)}function u(i){return["router-tab__item",{"is-active":n.activeId.value===i.id,"is-closable":x(i)},i.tabClass]}function m(i){return n.refreshingKey.value===n.getRouteKey(i)}t.onMounted(()=>{document.addEventListener("keydown",g)}),t.onBeforeUnmount(()=>{document.removeEventListener("keydown",g),o.appContext.config.globalProperties.$tabs=null}),t.watch(()=>e.keepAlive,i=>{n.options.keepAlive=i}),t.watch(()=>n.activeId.value,()=>g()),t.watch(()=>e.contextmenu,i=>{i||g()}),t.watch(()=>v.value.length,i=>{s.visible&&i===0&&g()});const C=n.includeKeys;return{controller:n,tabs:n.tabs,includeKeys:C,tabTransitionProps:b,pageTransitionProps:p,buildTabClass:u,activate:r,close:l,context:s,menuItems:v,handleMenuAction:U,showContextMenu:D,hideContextMenu:g,tabTitle:F,isClosable:x,isRefreshing:m,hasCustomSlot:c}}}),fe=(e,o)=>{const a=e.__vccOpts||e;for(const[n,c]of o)a[n]=c;return a},de={class:"router-tab"},be={class:"router-tab__header"},pe={class:"router-tab__slot-start"},me={class:"router-tab__scroll"},he=["onClick","onAuxclick","onContextmenu"],ye=["title"],ge=["onClick"],ke={class:"router-tab__slot-end"},Te={class:"router-tab__container"},Ce=["aria-disabled","onClick"];function we(e,o,a,n,c,b){const p=t.resolveComponent("RouterView");return t.openBlock(),t.createElementBlock("div",de,[t.createElementVNode("header",be,[t.createElementVNode("div",pe,[t.renderSlot(e.$slots,"start")]),t.createElementVNode("div",me,[t.createVNode(t.TransitionGroup,t.mergeProps({tag:"ul",class:"router-tab__nav"},e.tabTransitionProps),{default:t.withCtx(()=>[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(e.tabs,s=>(t.openBlock(),t.createElementBlock("li",{key:s.id,class:t.normalizeClass(e.buildTabClass(s)),onClick:f=>e.activate(s),onAuxclick:t.withModifiers(f=>e.close(s),["middle","prevent"]),onContextmenu:t.withModifiers(f=>e.showContextMenu(s,f),["prevent"])},[t.createElementVNode("span",{class:"router-tab__item-title",title:e.tabTitle(s)},[s.icon?(t.openBlock(),t.createElementBlock("i",{key:0,class:t.normalizeClass(["router-tab__item-icon",s.icon])},null,2)):t.createCommentVNode("",!0),t.createTextVNode(" "+t.toDisplayString(e.tabTitle(s)),1)],8,ye),e.isClosable(s)?(t.openBlock(),t.createElementBlock("a",{key:0,class:"router-tab__item-close",type:"button",onClick:t.withModifiers(f=>e.close(s),["stop"])},null,8,ge)):t.createCommentVNode("",!0)],42,he))),128))]),_:1},16)]),t.createElementVNode("div",ke,[t.renderSlot(e.$slots,"end")])]),t.createElementVNode("div",Te,[t.createVNode(p,null,{default:t.withCtx(s=>[e.hasCustomSlot?t.renderSlot(e.$slots,"default",t.normalizeProps(t.mergeProps({key:0},{...s,controller:e.controller}))):(t.openBlock(),t.createElementBlock(t.Fragment,{key:1},[t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[e.controller.options.keepAlive?(t.openBlock(),t.createBlock(t.KeepAlive,{key:0,include:e.includeKeys,max:e.controller.options.maxAlive||void 0},[e.isRefreshing(s.route)?t.createCommentVNode("",!0):(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s.Component),{key:e.controller.getRouteKey(s.route),class:"router-tab-page"}))],1032,["include","max"])):t.createCommentVNode("",!0)]),_:2},1040),t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[!e.controller.options.keepAlive||e.isRefreshing(s.route)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s.Component),{key:e.controller.getRouteKey(s.route)+(e.isRefreshing(s.route)?"-refresh":""),class:"router-tab-page"})):t.createCommentVNode("",!0)]),_:2},1040)],64))]),_:3})]),e.context.visible&&e.context.target?(t.openBlock(),t.createElementBlock("div",{key:0,class:"router-tab__contextmenu",style:t.normalizeStyle({left:e.context.position.x+"px",top:e.context.position.y+"px"})},[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(e.menuItems,s=>(t.openBlock(),t.createElementBlock("a",{key:s.id,class:"router-tab__contextmenu-item","aria-disabled":s.disabled,onClick:t.withModifiers(f=>e.handleMenuAction(s),["prevent"])},t.toDisplayString(s.label),9,Ce))),128))],4)):t.createCommentVNode("",!0)])}const j=fe(ue,[["render",we]]),Re={class:"router-tabs","aria-hidden":"true"},$=t.defineComponent({name:"RouterTabs",__name:"RouterTabs",props:{cookieKey:{},expiresInDays:{},path:{},domain:{},secure:{type:Boolean},sameSite:{},serialize:{type:Function},deserialize:{type:Function},fallbackRoute:{}},setup(e){return L(e),(a,n)=>(t.openBlock(),t.createElementBlock("span",Re))}}),z={install(e){if(z._installed)return;z._installed=!0;const o=j.name||"RouterTab",a=$.name||"RouterTabs";e.component(o,j),e.component(a,$),a!=="router-tabs"&&e.component("router-tabs",$),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[_]},set(n){n&&e.provide(_,n)}})}};y.RouterTab=j,y.RouterTabs=$,y.default=z,y.routerTabsKey=_,y.useRouterTabs=M,y.useRouterTabsPersistence=L,Object.defineProperties(y,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
@@ -26,9 +26,9 @@
26
26
  <a
27
27
  v-if="isClosable(tab)"
28
28
  class="router-tab__item-close"
29
+ type="button"
29
30
  @click.stop="close(tab)"
30
- >
31
- </a>
31
+ />
32
32
  </li>
33
33
  </transition-group>
34
34
  </div>
@@ -39,36 +39,41 @@
39
39
  </header>
40
40
 
41
41
  <div class="router-tab__container">
42
- <RouterView v-slot="{ Component, route }">
43
- <transition
44
- v-bind="pageTransitionProps"
45
- appear
46
- >
47
- <KeepAlive
48
- v-if="controller.options.keepAlive"
49
- :include="includeKeys"
50
- :max="controller.options.maxAlive || undefined"
42
+ <RouterView v-slot="routerSlot">
43
+ <template v-if="hasCustomSlot">
44
+ <slot v-bind="{ ...routerSlot, controller }" />
45
+ </template>
46
+ <template v-else>
47
+ <transition
48
+ v-bind="pageTransitionProps"
49
+ appear
50
+ >
51
+ <KeepAlive
52
+ v-if="controller.options.keepAlive"
53
+ :include="includeKeys"
54
+ :max="controller.options.maxAlive || undefined"
55
+ >
56
+ <component
57
+ v-if="!isRefreshing(routerSlot.route)"
58
+ :is="routerSlot.Component"
59
+ :key="controller.getRouteKey(routerSlot.route)"
60
+ class="router-tab-page"
61
+ />
62
+ </KeepAlive>
63
+ </transition>
64
+
65
+ <transition
66
+ v-bind="pageTransitionProps"
67
+ appear
51
68
  >
52
69
  <component
53
- v-if="!isRefreshing(route)"
54
- :is="Component"
55
- :key="controller.getRouteKey(route)"
70
+ v-if="!controller.options.keepAlive || isRefreshing(routerSlot.route)"
71
+ :is="routerSlot.Component"
72
+ :key="controller.getRouteKey(routerSlot.route) + (isRefreshing(routerSlot.route) ? '-refresh' : '')"
56
73
  class="router-tab-page"
57
74
  />
58
- </KeepAlive>
59
- </transition>
60
-
61
- <transition
62
- v-bind="pageTransitionProps"
63
- appear
64
- >
65
- <component
66
- v-if="!controller.options.keepAlive || isRefreshing(route)"
67
- :is="Component"
68
- :key="controller.getRouteKey(route) + (isRefreshing(route) ? '-refresh' : '')"
69
- class="router-tab-page"
70
- />
71
- </transition>
75
+ </transition>
76
+ </template>
72
77
  </RouterView>
73
78
  </div>
74
79
 
@@ -110,15 +115,15 @@ import type {
110
115
  RouterTabsMenuItem,
111
116
  RouterTabsMenuPreset,
112
117
  RouterTabsOptions,
113
- RouterTabsSnapshot,
118
+ RouterTabsPersistenceOptions,
114
119
  TabInput,
115
120
  TabRecord,
116
121
  TransitionLike
117
122
  } from '../core/types'
118
123
  import { getTransOpt } from '../util/index'
119
124
  import { routerTabsKey } from '../constants'
125
+ import { useRouterTabsPersistence } from '../persistence'
120
126
 
121
- const hasSessionStorage = typeof window !== 'undefined' && 'sessionStorage' in window
122
127
 
123
128
  interface ResolvedMenuItem {
124
129
  id: string
@@ -167,9 +172,13 @@ export default defineComponent({
167
172
  type: [Boolean, Array] as PropType<boolean | RouterTabsMenuConfig[]>,
168
173
  default: true
169
174
  },
170
- storage: {
171
- type: [Boolean, String],
172
- default: false
175
+ cookieKey: {
176
+ type: String,
177
+ default: null
178
+ },
179
+ persistence: {
180
+ type: Object as PropType<RouterTabsPersistenceOptions | null>,
181
+ default: null
173
182
  }
174
183
  },
175
184
  setup(props) {
@@ -195,6 +204,16 @@ export default defineComponent({
195
204
  provide(routerTabsKey, controller)
196
205
  instance.appContext.config.globalProperties.$tabs = controller
197
206
 
207
+ const hasCustomSlot = computed(() => Boolean(instance?.slots?.default))
208
+
209
+ if (props.cookieKey || props.persistence) {
210
+ const options: RouterTabsPersistenceOptions = {
211
+ ...(props.persistence ?? {})
212
+ }
213
+ if (props.cookieKey) options.cookieKey = props.cookieKey
214
+ useRouterTabsPersistence(options)
215
+ }
216
+
198
217
  const tabTransitionProps = computed(() => getTransOpt(props.tabTransition))
199
218
  const pageTransitionProps = computed(() => getTransOpt(props.pageTransition))
200
219
 
@@ -204,15 +223,6 @@ export default defineComponent({
204
223
  position: { x: 0, y: 0 }
205
224
  })
206
225
 
207
- const storageKey = computed(() => {
208
- if (!props.storage || !hasSessionStorage) return null
209
- if (typeof props.storage === 'string') return props.storage
210
- const base = router.options?.history?.base ?? ''
211
- return `router-tabs:${base || 'default'}`
212
- })
213
-
214
- let restoring = Boolean(storageKey.value)
215
-
216
226
  type MenuConfig = RouterTabsMenuConfig
217
227
  type MenuActionContext = RouterTabsMenuContext
218
228
  type CustomMenuOption = RouterTabsMenuItem
@@ -406,49 +416,13 @@ export default defineComponent({
406
416
  return controller.refreshingKey.value === controller.getRouteKey(route)
407
417
  }
408
418
 
409
- async function restoreTabsFromStorage() {
410
- const key = storageKey.value
411
- if (!key || !hasSessionStorage) return
412
- const raw = window.sessionStorage.getItem(key)
413
- if (!raw) return
414
-
415
- try {
416
- const parsed = JSON.parse(raw) as RouterTabsSnapshot
417
- if (!parsed || !Array.isArray(parsed.tabs)) return
418
- restoring = true
419
- await controller.hydrate(parsed)
420
- } catch (error) {
421
- if (import.meta.env?.DEV) {
422
- console.warn('[RouterTabs] Failed to restore tabs from storage', error)
423
- }
424
- } finally {
425
- restoring = false
426
- persistTabsSnapshot()
427
- }
428
- }
429
-
430
- function persistTabsSnapshot() {
431
- const key = storageKey.value
432
- if (!key || !hasSessionStorage || restoring) return
433
- try {
434
- const snapshot = controller.snapshot()
435
- window.sessionStorage.setItem(key, JSON.stringify(snapshot))
436
- } catch (error) {
437
- if (import.meta.env?.DEV) {
438
- console.warn('[RouterTabs] Failed to persist tabs snapshot', error)
439
- }
440
- }
441
- }
442
-
443
419
  onMounted(() => {
444
420
  document.addEventListener('keydown', hideContextMenu)
445
- restoreTabsFromStorage()
446
421
  })
447
422
 
448
423
  onBeforeUnmount(() => {
449
424
  document.removeEventListener('keydown', hideContextMenu)
450
425
  instance.appContext.config.globalProperties.$tabs = null
451
- persistTabsSnapshot()
452
426
  })
453
427
 
454
428
  watch(
@@ -479,25 +453,6 @@ export default defineComponent({
479
453
  }
480
454
  )
481
455
 
482
- watch(
483
- () => ({
484
- key: storageKey.value,
485
- tabs: controller.tabs.map(tab => ({
486
- to: tab.to,
487
- title: tab.title,
488
- tips: tab.tips,
489
- icon: tab.icon,
490
- tabClass: tab.tabClass,
491
- closable: tab.closable
492
- })),
493
- active: controller.activeId.value
494
- }),
495
- () => {
496
- persistTabsSnapshot()
497
- },
498
- { deep: true }
499
- )
500
-
501
456
  const includeKeys = controller.includeKeys
502
457
 
503
458
  return {
@@ -516,7 +471,8 @@ export default defineComponent({
516
471
  hideContextMenu,
517
472
  tabTitle,
518
473
  isClosable,
519
- isRefreshing
474
+ isRefreshing,
475
+ hasCustomSlot
520
476
  }
521
477
  }
522
478
  })
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <span class="router-tabs" aria-hidden="true" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { useRouterTabsPersistence } from '../persistence'
7
+ import type { RouterTabsPersistenceOptions } from '../core/types'
8
+
9
+ defineOptions({ name: 'RouterTabs' })
10
+
11
+ const props = defineProps<RouterTabsPersistenceOptions>()
12
+
13
+ useRouterTabsPersistence(props)
14
+ </script>
package/lib/constants.ts CHANGED
@@ -2,3 +2,5 @@ import type { InjectionKey } from 'vue'
2
2
  import type { RouterTabsContext } from './core/types'
3
3
 
4
4
  export const routerTabsKey: InjectionKey<RouterTabsContext> = Symbol('RouterTabsContext')
5
+
6
+ export const routerTabsCookie = 'router-tabs:snapshot'
package/lib/core/types.ts CHANGED
@@ -113,3 +113,15 @@ export interface RouterTabsContext {
113
113
  snapshot: () => RouterTabsSnapshot
114
114
  hydrate: (snapshot: RouterTabsSnapshot) => Promise<void>
115
115
  }
116
+
117
+ export interface RouterTabsPersistenceOptions {
118
+ cookieKey?: string
119
+ expiresInDays?: number
120
+ path?: string
121
+ domain?: string
122
+ secure?: boolean
123
+ sameSite?: 'lax' | 'strict' | 'none'
124
+ serialize?: (snapshot: RouterTabsSnapshot | null) => string
125
+ deserialize?: (value: string | null) => RouterTabsSnapshot | null
126
+ fallbackRoute?: RouteLocationRaw
127
+ }
package/lib/index.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { App, Plugin } from 'vue'
2
2
  import RouterTab from './components/RouterTab.vue'
3
- import RouterTabsPinia from './components/RouterTabsPinia.vue'
3
+ import RouterTabsComponent from './components/RouterTabs.vue'
4
4
  import { routerTabsKey } from './constants'
5
5
  import useRouterTabs from './useRouterTabs'
6
- import { useRouterTabsPiniaPersistence } from './pinia'
6
+ import { useRouterTabsPersistence } from './persistence'
7
7
 
8
8
  import type { RouterTabsContext } from './core/types'
9
9
 
10
- export type { TabRecord, TabInput, RouterTabsOptions, CloseTabOptions } from './core/types'
10
+ export type { TabRecord, TabInput, RouterTabsOptions, CloseTabOptions, RouterTabsPersistenceOptions } from './core/types'
11
11
 
12
- export { routerTabsKey, useRouterTabs, useRouterTabsPiniaPersistence, RouterTabsPinia }
12
+ export { routerTabsKey, useRouterTabs, useRouterTabsPersistence, RouterTab, RouterTabsComponent as RouterTabs }
13
13
 
14
14
  import "./scss/index.scss";
15
15
 
@@ -19,13 +19,13 @@ const plugin: Plugin = {
19
19
  ;(plugin as any)._installed = true
20
20
 
21
21
  const componentName = RouterTab.name || 'RouterTab'
22
- const piniaComponentName = RouterTabsPinia.name || 'RouterTabs'
23
-
22
+ const persistenceComponentName = RouterTabsComponent.name || 'RouterTabs'
23
+
24
24
  app.component(componentName, RouterTab)
25
- app.component(piniaComponentName, RouterTabsPinia)
25
+ app.component(persistenceComponentName, RouterTabsComponent)
26
26
 
27
- if (piniaComponentName !== 'router-tabs') {
28
- app.component('router-tabs', RouterTabsPinia)
27
+ if (persistenceComponentName !== 'router-tabs') {
28
+ app.component('router-tabs', RouterTabsComponent)
29
29
  }
30
30
 
31
31
  Object.defineProperty(app.config.globalProperties, '$tabs', {
@@ -44,5 +44,3 @@ const plugin: Plugin = {
44
44
  }
45
45
 
46
46
  export default plugin
47
-
48
- export { RouterTab, RouterTabsPinia as RouterTabs }
@@ -0,0 +1,171 @@
1
+ import { onMounted, ref, watch } from 'vue'
2
+ import type { RouteLocationRaw } from 'vue-router'
3
+ import { useRouterTabs } from './useRouterTabs'
4
+ import type { RouterTabsSnapshot } from './core/types'
5
+ import { routerTabsCookie } from './constants'
6
+
7
+ export interface RouterTabsPersistenceOptions {
8
+ /** Cookie key used to persist snapshots. Defaults to `router-tabs:snapshot`. */
9
+ cookieKey?: string
10
+ /** Number of days before the cookie expires. Defaults to 7 days. */
11
+ expiresInDays?: number
12
+ /** Cookie path. Defaults to `/`. */
13
+ path?: string
14
+ /** Cookie domain. */
15
+ domain?: string
16
+ /** Whether to set the `Secure` flag. */
17
+ secure?: boolean
18
+ /** SameSite value. Defaults to `Lax`. */
19
+ sameSite?: 'lax' | 'strict' | 'none'
20
+ /** Custom serializer before writing to the cookie. */
21
+ serialize?: (snapshot: RouterTabsSnapshot | null) => string
22
+ /** Custom deserializer when reading the cookie. */
23
+ deserialize?: (value: string | null) => RouterTabsSnapshot | null
24
+ /** Route to open when no snapshot exists. Defaults to RouterTab's default route. */
25
+ fallbackRoute?: RouteLocationRaw
26
+ }
27
+
28
+ const DAY_IN_MS = 86_400_000
29
+
30
+ function readCookie(key: string): string | null {
31
+ if (typeof document === 'undefined') return null
32
+ const encodedKey = `${encodeURIComponent(key)}=`
33
+ const cookies = document.cookie ? document.cookie.split('; ') : []
34
+ for (const cookie of cookies) {
35
+ if (cookie.startsWith(encodedKey)) {
36
+ return decodeURIComponent(cookie.slice(encodedKey.length))
37
+ }
38
+ }
39
+ return null
40
+ }
41
+
42
+ function writeCookie(key: string, value: string, options: RouterTabsPersistenceOptions) {
43
+ if (typeof document === 'undefined') return
44
+
45
+ const {
46
+ expiresInDays = 7,
47
+ path = '/',
48
+ domain,
49
+ secure,
50
+ sameSite = 'lax'
51
+ } = options
52
+
53
+ const parts = [`${encodeURIComponent(key)}=${encodeURIComponent(value)}`]
54
+
55
+ if (expiresInDays !== Infinity) {
56
+ const expires = new Date(Date.now() + expiresInDays * DAY_IN_MS).toUTCString()
57
+ parts.push(`Expires=${expires}`)
58
+ }
59
+
60
+ if (path) parts.push(`Path=${path}`)
61
+ if (domain) parts.push(`Domain=${domain}`)
62
+ if (secure) parts.push('Secure')
63
+ if (sameSite) parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`)
64
+
65
+ document.cookie = parts.join('; ')
66
+ }
67
+
68
+ function removeCookie(key: string, options: RouterTabsPersistenceOptions) {
69
+ if (typeof document === 'undefined') return
70
+
71
+ const { path = '/', domain } = options
72
+ const parts = [`${encodeURIComponent(key)}=`]
73
+ parts.push('Expires=Thu, 01 Jan 1970 00:00:01 GMT')
74
+ if (path) parts.push(`Path=${path}`)
75
+ if (domain) parts.push(`Domain=${domain}`)
76
+
77
+ document.cookie = parts.join('; ')
78
+ }
79
+
80
+ const defaultSerialize = (snapshot: RouterTabsSnapshot | null) => JSON.stringify(snapshot ?? null)
81
+ const defaultDeserialize = (value: string | null): RouterTabsSnapshot | null => {
82
+ if (!value) return null
83
+ try {
84
+ return JSON.parse(value) as RouterTabsSnapshot
85
+ } catch (error) {
86
+ if (import.meta.env?.DEV) {
87
+ console.warn('[RouterTabs] Failed to parse cookie snapshot', error)
88
+ }
89
+ return null
90
+ }
91
+ }
92
+
93
+ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions = {}) {
94
+ const {
95
+ cookieKey = routerTabsCookie,
96
+ serialize = defaultSerialize,
97
+ deserialize = defaultDeserialize
98
+ } = options
99
+
100
+ const controller = useRouterTabs({ optional: true })
101
+ const hydrating = ref(false)
102
+
103
+ const setup = (ctrl: NonNullable<typeof controller>) => {
104
+ onMounted(async () => {
105
+ const initialSnapshot = deserialize(readCookie(cookieKey))
106
+
107
+ if (initialSnapshot && initialSnapshot.tabs?.length) {
108
+ try {
109
+ hydrating.value = true
110
+ await ctrl.hydrate(initialSnapshot)
111
+ } finally {
112
+ hydrating.value = false
113
+ }
114
+ } else {
115
+ try {
116
+ hydrating.value = true
117
+ const fallback = options.fallbackRoute ?? ctrl.options.defaultRoute
118
+ await ctrl.reset(fallback)
119
+ } finally {
120
+ hydrating.value = false
121
+ }
122
+ }
123
+
124
+ const snapshot = ctrl.snapshot()
125
+ if (!snapshot.tabs.length) {
126
+ removeCookie(cookieKey, options)
127
+ } else {
128
+ writeCookie(cookieKey, serialize(snapshot), options)
129
+ }
130
+ })
131
+
132
+ watch(
133
+ () => ({
134
+ tabs: ctrl.tabs.map(tab => ({
135
+ to: tab.to,
136
+ title: tab.title,
137
+ tips: tab.tips,
138
+ icon: tab.icon,
139
+ tabClass: tab.tabClass,
140
+ closable: tab.closable
141
+ })),
142
+ active: ctrl.activeId.value
143
+ }),
144
+ () => {
145
+ if (hydrating.value) return
146
+ const snapshot = ctrl.snapshot()
147
+ if (!snapshot.tabs.length) {
148
+ removeCookie(cookieKey, options)
149
+ } else {
150
+ writeCookie(cookieKey, serialize(snapshot), options)
151
+ }
152
+ },
153
+ { deep: true }
154
+ )
155
+ }
156
+
157
+ if (controller) {
158
+ setup(controller)
159
+ } else {
160
+ onMounted(() => {
161
+ const lateController = useRouterTabs({ optional: true })
162
+ if (lateController) {
163
+ setup(lateController)
164
+ } else if (import.meta.env?.DEV) {
165
+ console.warn('[RouterTabs] Persistence helper must be used inside <router-tab>.')
166
+ }
167
+ })
168
+ }
169
+ }
170
+
171
+ export default useRouterTabsPersistence
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-router-tab",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -22,10 +22,6 @@
22
22
  "scripts": {
23
23
  "build": "vite build"
24
24
  },
25
- "dependencies": {
26
- "vue": "^3.5.22",
27
- "vue-router": "^4.5.1"
28
- },
29
25
  "devDependencies": {
30
26
  "@vitejs/plugin-vue": "^6.0.1",
31
27
  "pinia": "^3.0.3",
@@ -34,7 +30,9 @@
34
30
  "typescript": "^5.9.2",
35
31
  "vite": "^7.1.7",
36
32
  "vite-plugin-dts": "^4.5.4",
37
- "vite-plugin-libcss": "^1.1.2"
33
+ "vite-plugin-libcss": "^1.1.2",
34
+ "vue": "^3.5.22",
35
+ "vue-router": "^4.5.1"
38
36
  },
39
37
  "keywords": [
40
38
  "vue3-router-tab",
@@ -42,7 +40,10 @@
42
40
  "router",
43
41
  "tabs",
44
42
  "tab",
45
- "router-tab"
43
+ "router-tab",
44
+ "router-tabs",
45
+ "cookies",
46
+ "persistence"
46
47
  ],
47
48
  "license": "MIT",
48
49
  "homepage": "https://github.com/anilshr25/vue3-router-tab/#readme",
@@ -64,6 +65,8 @@
64
65
  ]
65
66
  },
66
67
  "peerDependencies": {
67
- "pinia": "^2.1.7"
68
+ "pinia": "^2.1.7",
69
+ "vue": "^3.3.0",
70
+ "vue-router": "^4.2.0"
68
71
  }
69
72
  }