vue3-router-tab 1.0.9 → 1.1.0

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,12 @@
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";/*!
1
+ (function(C,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(C=typeof globalThis<"u"?globalThis:C||self,t(C["vue3-router-tab"]={},C.Vue))})(this,(function(C,t){"use strict";/*!
2
2
  * vue-router v4.5.1
3
3
  * (c) 2025 Eduardo San Martin Morote
4
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>.
5
+ */const ae=typeof document<"u",ie=Object.assign,se=Array.isArray;function re(e){const n=Array.from(arguments).slice(1);console.warn.apply(console,["[Vue Router warn]: "+e].concat(n))}function le(e,n){return(e.aliasOf||e)===(n.aliasOf||n)}var q;(function(e){e.pop="pop",e.push="push"})(q||(q={}));var H;(function(e){e.back="back",e.forward="forward",e.unknown=""})(H||(H={})),Symbol(process.env.NODE_ENV!=="production"?"navigation failure":"");var Y;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Y||(Y={}));const ce=Symbol(process.env.NODE_ENV!=="production"?"router view location matched":""),F=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 W=Symbol(process.env.NODE_ENV!=="production"?"router view location":""),ue=t.defineComponent({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:n,slots:a}){process.env.NODE_ENV!=="production"&&de();const o=t.inject(W),r=t.computed(()=>e.route||o.value),b=t.inject(F,0),p=t.computed(()=>{let m=t.unref(b);const{matched:h}=r.value;let y;for(;(y=h[m])&&!y.components;)m++;return m}),l=t.computed(()=>r.value.matched[p.value]);t.provide(F,t.computed(()=>p.value+1)),t.provide(ce,l),t.provide(W,r);const c=t.ref();return t.watch(()=>[c.value,l.value,e.name],([m,h,y],[R,g,w])=>{h&&(h.instances[y]=m,g&&g!==h&&m&&m===R&&(h.leaveGuards.size||(h.leaveGuards=g.leaveGuards),h.updateGuards.size||(h.updateGuards=g.updateGuards))),m&&h&&(!g||!le(h,g)||!R)&&(h.enterCallbacks[y]||[]).forEach(v=>v(m))},{flush:"post"}),()=>{const m=r.value,h=e.name,y=l.value,R=y&&y.components[h];if(!R)return X(a.default,{Component:R,route:m});const g=y.props[h],w=g?g===!0?m.params:typeof g=="function"?g(m):g:null,v=E=>{E.component.isUnmounted&&(y.instances[h]=null)},_=t.h(R,ie({},w,n,{onVnodeUnmounted:v,ref:c}));if(process.env.NODE_ENV!=="production"&&ae&&_.ref){const E={depth:p.value,name:y.name,path:y.path,meta:y.meta};(se(_.ref)?_.ref.map(P=>P.i):[_.ref.i]).forEach(P=>{P.__vrv_devtools=E})}return X(a.default,{Component:_,route:m})||_}}});function X(e,n){if(!e)return null;const a=e(n);return a.length===1?a[0]:a}const fe=ue;function de(){const e=t.getCurrentInstance(),n=e.parent&&e.parent.type.name,a=e.parent&&e.parent.subTree&&e.parent.subTree.type;if(n&&(n==="KeepAlive"||n.includes("Transition"))&&typeof a=="object"&&a.name==="RouterView"){const o=n==="KeepAlive"?"keep-alive":"transition";re(`<router-view> can no longer be used directly inside <transition> or <keep-alive>.
6
6
  Use slot props instead:
7
7
 
8
8
  <router-view v-slot="{ Component }">
9
- <${n}>
9
+ <${o}>
10
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"}})}));
11
+ </${o}>
12
+ </router-view>`)}}function pe(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 S(e,n){const a=e.resolve(n);if(!a||!a.matched.length)throw new Error(`[RouterTabs] Unable to resolve route: ${String(n)}`);return a}const be={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 V(e){const n=e.meta?.key;if(typeof n=="function"){const a=n(e);if(typeof a=="string"&&a.length)return a}else if(typeof n=="string"&&n.length){const a=be[n.toLowerCase()];return a?a(e):n}return e.fullPath}function I(e,n){const a=e.meta?.keepAlive;return typeof a=="boolean"?a:n}function O(e,n){const a=e.meta?.reuse;return typeof a=="boolean"?a:n}function Q(e){const n=e.meta??{},a={};return"title"in n&&(a.title=n.title),"tips"in n&&(a.tips=n.tips),"icon"in n&&(a.icon=n.icon),"closable"in n&&(a.closable=n.closable),"tabClass"in n&&(a.tabClass=n.tabClass),"target"in n&&(a.target=n.target),"href"in n&&(a.href=n.href),a}function K(e,n,a){const o=Q(e);return{id:V(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:I(e,a),reusable:O(e,!1),closable:o.closable??!0,...o,...n}}function D(e,n,a,o){if(!e.find(b=>b.id===n.id)){if(a==="next"&&o){const b=e.findIndex(p=>p.id===o);if(b>-1){e.splice(b+1,0,n);return}}e.push(n)}}function Z(e,n,a){if(!n||n<=0)return;const o=e.filter(r=>r.alive);for(;o.length>n;){const r=o.shift();if(!r||r.id===a)continue;const b=e.findIndex(p=>p.id===r.id);b>-1&&(e[b].alive=!1)}}function me(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function he(e){const n={};return"title"in e&&(n.title=e.title),"tips"in e&&(n.tips=e.tips),"icon"in e&&(n.icon=e.icon),"tabClass"in e&&(n.tabClass=e.tabClass),"closable"in e&&(n.closable=e.closable),n}function ye(e,n={}){const a=pe(n),o=t.reactive([]),r=t.ref(null),b=t.shallowRef(),p=t.ref(null),l=t.computed(()=>o.filter(s=>s.alive).map(s=>s.id));let c=!1;function m(s){const u=typeof s.matched=="object"?s:S(e,s);return{key:V(u),fullPath:u.fullPath,alive:I(u,a.keepAlive),reusable:O(u,!1),matched:u}}function h(s){const u=V(s);let f=o.find(k=>k.id===u);return f?(f.fullPath=s.fullPath,f.to=s.fullPath,f.matched=s,f.alive=I(s,a.keepAlive),f.reusable=O(s,f.reusable),Object.assign(f,Q(s)),f):(f=K(s,{},a.keepAlive),D(o,f,a.appendPosition,r.value),Z(o,a.maxAlive,r.value),f)}async function y(s,u=!1,f=!0){const k=S(e,s),i=V(k),d=r.value===i;f==="sameTab"&&(f=d),f&&await v(i,!0),await e[u?"replace":"push"](k),d&&await N()}function R(s){const u=o.findIndex(k=>k.id===s),f=o[u]||o[u-1]||o[0];return f?f.to:a.defaultRoute}async function g(s=r.value,u={}){if(s){if(!u.force&&a.keepLastTab&&o.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");if(await w(s,{force:u.force}),u.redirect!==null)if(r.value===s){const f=u.redirect??R(s);f&&await e.replace(f)}else u.redirect&&await e.replace(u.redirect)}}async function w(s,u={}){const f=o.findIndex(k=>k.id===s);f!==-1&&(o.splice(f,1),p.value===s&&(p.value=null),r.value===s&&(r.value=null,b.value=void 0))}async function v(s=r.value??void 0,u=!1){s&&(p.value=s,await t.nextTick(),u||await t.nextTick(),p.value=null)}async function _(s=!1){for(const u of o)await v(u.id,s)}async function E(s=a.defaultRoute){o.splice(0,o.length),r.value=null,b.value=void 0;for(const u of a.initialTabs){const f=S(e,u.to),k=K(f,u,a.keepAlive);o.push(k)}await e.replace(s)}async function N(){const s=r.value;s&&await v(s,!0)}function P(s){return typeof s.matched=="object"?V(s):V(S(e,s))}function $(){const s=o.find(u=>u.id===r.value);return{tabs:o.map(me),active:s?s.to:null}}async function U(s){c=!0,o.splice(0,o.length),r.value=null,b.value=void 0;const u=s?.tabs??[];for(const k of u)try{const i=S(e,k.to),d=he(k),T=K(i,d,a.keepAlive);D(o,T,"last",null)}catch{}c=!1;const f=s?.active??u[u.length-1]?.to??a.defaultRoute;if(f)try{await e.replace(f)}catch{}}return t.watch(()=>e.currentRoute.value,s=>{if(c)return;const u=h(s);r.value=u.id,b.value=u,Z(o,a.maxAlive,r.value)},{immediate:!0}),a.initialTabs.length&&a.initialTabs.forEach(s=>{const u=S(e,s.to),f=K(u,s,a.keepAlive);D(o,f,"last",null)}),{options:a,tabs:o,activeId:r,current:b,includeKeys:l,refreshingKey:p,openTab:y,closeTab:g,removeTab:w,refreshTab:v,refreshAll:_,reset:E,reload:N,getRouteKey:P,matchRoute:m,snapshot:$,hydrate:U}}function ee(e){return e?typeof e=="string"?{name:e}:e:{}}const x=Symbol("RouterTabsContext"),ge="router-tabs:snapshot";function M(e={}){const{optional:n=!1}=e,a=t.inject(x,null);if(a)return a;const o=t.inject("$tabs",null);if(o)return o;const b=t.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(b)return b;if(!n)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const ke=864e5;function Te(e){if(typeof document>"u")return null;const n=`${encodeURIComponent(e)}=`,a=document.cookie?document.cookie.split("; "):[];for(const o of a)if(o.startsWith(n))return decodeURIComponent(o.slice(n.length));return null}function te(e,n,a){if(typeof document>"u")return;const{expiresInDays:o=7,path:r="/",domain:b,secure:p,sameSite:l="lax"}=a,c=[`${encodeURIComponent(e)}=${encodeURIComponent(n)}`];if(o!==1/0){const m=new Date(Date.now()+o*ke).toUTCString();c.push(`Expires=${m}`)}r&&c.push(`Path=${r}`),b&&c.push(`Domain=${b}`),p&&c.push("Secure"),l&&c.push(`SameSite=${l.charAt(0).toUpperCase()}${l.slice(1)}`),document.cookie=c.join("; ")}function ne(e,n){if(typeof document>"u")return;const{path:a="/",domain:o}=n,r=[`${encodeURIComponent(e)}=`];r.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),a&&r.push(`Path=${a}`),o&&r.push(`Domain=${o}`),document.cookie=r.join("; ")}const we=e=>JSON.stringify(e??null),Ce=e=>{if(!e)return null;try{return JSON.parse(e)}catch{return null}};function j(e={}){const{cookieKey:n=ge,serialize:a=we,deserialize:o=Ce}=e,r=M({optional:!0}),b=t.ref(!1),p=l=>{t.onMounted(async()=>{const c=o(Te(n));if(c&&c.tabs?.length)try{b.value=!0,await l.hydrate(c)}finally{b.value=!1}else try{b.value=!0;const h=e.fallbackRoute??l.options.defaultRoute;await l.reset(h)}finally{b.value=!1}const m=l.snapshot();m.tabs.length?te(n,a(m),e):ne(n,e)}),t.watch(()=>({tabs:l.tabs.map(c=>({to:c.to,title:c.title,tips:c.tips,icon:c.icon,tabClass:c.tabClass,closable:c.closable})),active:l.activeId.value}),()=>{if(b.value)return;const c=l.snapshot();c.tabs.length?te(n,a(c),e):ne(n,e)},{deep:!0})};r?p(r):t.onMounted(()=>{const l=M({optional:!0});l&&p(l)})}const Re=t.defineComponent({name:"RouterTab",components:{RouterView:fe},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 n=t.getCurrentInstance();if(!n)throw new Error("[RouterTab] component must be used within a Vue application context.");const a=n.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 o=ye(a,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});if(t.provide(x,o),n.appContext.config.globalProperties.$tabs=o,e.cookieKey||e.persistence){const i={...e.persistence??{}};e.cookieKey&&(i.cookieKey=e.cookieKey),j(i)}const r=t.computed(()=>ee(e.tabTransition)),b=t.computed(()=>ee(e.pageTransition)),p=t.reactive({visible:!1,target:null,position:{x:0,y:0}}),l=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function c(i){return o.tabs.findIndex(d=>d.id===i)}function m(i){const d=c(i.id);return d>0?o.tabs.slice(0,d):[]}function h(i){const d=c(i.id);return d>-1?o.tabs.slice(d+1):[]}function y(i){return o.tabs.filter(d=>d.id!==i.id)}async function R(i,d){const T=i.filter(A=>A.closable!==!1);if(T.length){for(const A of T)o.activeId.value===A.id?await o.closeTab(A.id,{redirect:d.to,force:!0}):await o.removeTab(A.id,{force:!0});o.activeId.value!==d.id&&await o.openTab(d.to,!0,!1)}}const g={refresh:{label:"Refresh",handler:async({target:i})=>{await o.refreshTab(i.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await o.refreshAll(!0)}},close:{label:"Close",handler:async({target:i})=>{await o.closeTab(i.id)},enable:({target:i})=>$(i)},closeLefts:{label:"Close to the Left",handler:async({target:i})=>{await R(m(i),i)},enable:({target:i})=>m(i).some(d=>d.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:i})=>{await R(h(i),i)},enable:({target:i})=>h(i).some(d=>d.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:i})=>{await R(y(i),i)},enable:({target:i})=>y(i).some(d=>d.closable!==!1)}};function w(){p.visible=!1,p.target=null}function v(i,d){e.contextmenu&&(p.visible=!0,p.target=i,p.position.x=d.clientX,p.position.y=d.clientY,document.addEventListener("click",w,{once:!0}))}function _(i,d){const T=typeof i=="string"?{id:i}:i,A=g[T.id],Oe=T.label??A?.label??String(T.id),G=T.visible??A?.visible??!0;if(!(typeof G=="function"?G(d):G!==!1))return null;const J=T.enable??A?.enable??!0,De=typeof J=="function"?J(d):J!==!1,oe=T.handler??A?.handler;if(!oe)return null;const Me=async()=>{await Promise.resolve(oe(d))};return{id:String(T.id),label:Oe,disabled:!De,action:Me}}const E=t.computed(()=>{if(!p.visible||!p.target||e.contextmenu===!1)return[];const i=Array.isArray(e.contextmenu)?e.contextmenu:l,d={target:p.target,controller:o};return i.map(T=>_(T,d)).filter(T=>!!T)});async function N(i){i.disabled||(w(),await i.action())}function P(i){return typeof i.title=="string"?i.title:Array.isArray(i.title)&&i.title.length?String(i.title[0]):i.fullPath}function $(i){return!(i.closable===!1||o.options.keepLastTab&&o.tabs.length<=1)}async function U(i){await o.closeTab(i.id)}function s(i){o.activeId.value!==i.id&&o.openTab(i.to,!1)}function u(i){return["router-tab__item",{"is-active":o.activeId.value===i.id,"is-closable":$(i)},i.tabClass]}function f(i){return o.refreshingKey.value===o.getRouteKey(i)}t.onMounted(()=>{document.addEventListener("keydown",w)}),t.onBeforeUnmount(()=>{document.removeEventListener("keydown",w),n.appContext.config.globalProperties.$tabs=null}),t.watch(()=>e.keepAlive,i=>{o.options.keepAlive=i}),t.watch(()=>o.activeId.value,()=>w()),t.watch(()=>e.contextmenu,i=>{i||w()}),t.watch(()=>E.value.length,i=>{p.visible&&i===0&&w()});const k=o.includeKeys;return{controller:o,tabs:o.tabs,includeKeys:k,tabTransitionProps:r,pageTransitionProps:b,buildTabClass:u,activate:s,close:U,context:p,menuItems:E,handleMenuAction:N,showContextMenu:v,hideContextMenu:w,tabTitle:P,isClosable:$,isRefreshing:f}}}),ve=(e,n)=>{const a=e.__vccOpts||e;for(const[o,r]of n)a[o]=r;return a},_e={class:"router-tab"},Ae={class:"router-tab__header"},Ee={class:"router-tab__slot-start"},Pe={class:"router-tab__scroll"},Se=["onClick","onAuxclick","onContextmenu"],Ve=["title"],xe=["onClick"],Ne={class:"router-tab__slot-end"},$e={class:"router-tab__container"},Ke=["aria-disabled","onClick"];function Be(e,n,a,o,r,b){const p=t.resolveComponent("RouterView");return t.openBlock(),t.createElementBlock("div",_e,[t.createElementVNode("header",Ae,[t.createElementVNode("div",Ee,[t.renderSlot(e.$slots,"start")]),t.createElementVNode("div",Pe,[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,l=>(t.openBlock(),t.createElementBlock("li",{key:l.id,class:t.normalizeClass(e.buildTabClass(l)),onClick:c=>e.activate(l),onAuxclick:t.withModifiers(c=>e.close(l),["middle","prevent"]),onContextmenu:t.withModifiers(c=>e.showContextMenu(l,c),["prevent"])},[t.createElementVNode("span",{class:"router-tab__item-title",title:e.tabTitle(l)},[l.icon?(t.openBlock(),t.createElementBlock("i",{key:0,class:t.normalizeClass(["router-tab__item-icon",l.icon])},null,2)):t.createCommentVNode("",!0),t.createTextVNode(" "+t.toDisplayString(e.tabTitle(l)),1)],8,Ve),e.isClosable(l)?(t.openBlock(),t.createElementBlock("a",{key:0,class:"router-tab__item-close",type:"button",onClick:t.withModifiers(c=>e.close(l),["stop"])},null,8,xe)):t.createCommentVNode("",!0)],42,Se))),128))]),_:1},16)]),t.createElementVNode("div",Ne,[t.renderSlot(e.$slots,"end")])]),t.createElementVNode("div",$e,[t.createVNode(p,null,{default:t.withCtx(({Component:l,route:c})=>[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(c)?t.createCommentVNode("",!0):(t.openBlock(),t.createBlock(t.resolveDynamicComponent(l),{key:e.controller.getRouteKey(c),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(c)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(l),{key:e.controller.getRouteKey(c)+(e.isRefreshing(c)?"-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,l=>(t.openBlock(),t.createElementBlock("a",{key:l.id,class:"router-tab__contextmenu-item","aria-disabled":l.disabled,onClick:t.withModifiers(c=>e.handleMenuAction(l),["prevent"])},t.toDisplayString(l.label),9,Ke))),128))],4)):t.createCommentVNode("",!0)])}const L=ve(Re,[["render",Be]]),Ie={class:"router-tabs","aria-hidden":"true"},B=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 j(e),(a,o)=>(t.openBlock(),t.createElementBlock("span",Ie))}}),z={install(e){if(z._installed)return;z._installed=!0;const n=L.name||"RouterTab",a=B.name||"RouterTabs";e.component(n,L),e.component(a,B),a!=="router-tabs"&&e.component("router-tabs",B),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[x]},set(o){o&&e.provide(x,o)}})}};C.RouterTab=L,C.RouterTabs=B,C.default=z,C.routerTabsKey=x,C.useRouterTabs=M,C.useRouterTabsPersistence=j,Object.defineProperties(C,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
package/index.d.ts CHANGED
@@ -30,16 +30,21 @@ export declare const routerTabsKey: import('vue').InjectionKey<RouterTabsContext
30
30
 
31
31
  export declare function useRouterTabs(options?: { optional?: boolean }): RouterTabsContext | null
32
32
 
33
- export interface RouterTabsPiniaOptions {
34
- storeId?: string
35
- storageKey?: string
36
- storage?: Storage | null
33
+ export interface RouterTabsPersistenceOptions {
34
+ cookieKey?: string
35
+ expiresInDays?: number
36
+ path?: string
37
+ domain?: string
38
+ secure?: boolean
39
+ sameSite?: 'lax' | 'strict' | 'none'
40
+ serialize?: (snapshot: RouterTabsSnapshot | null) => string
41
+ deserialize?: (value: string | null) => RouterTabsSnapshot | null
37
42
  fallbackRoute?: import('vue-router').RouteLocationRaw
38
43
  }
39
44
 
40
- export declare function useRouterTabsPiniaPersistence(options?: RouterTabsPiniaOptions & { store?: import('pinia').StoreDefinition<any, any, any, any> }): import('pinia').Store<any, any, any, any>
45
+ export declare function useRouterTabsPersistence(options?: RouterTabsPersistenceOptions): void
41
46
 
42
- export declare const RouterTabsPinia: import('vue').DefineComponent<RouterTabsPiniaOptions, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<RouterTabsPiniaOptions>, {}>
47
+ export declare const RouterTabs: import('vue').DefineComponent<RouterTabsPersistenceOptions, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<RouterTabsPersistenceOptions>, {}>
43
48
 
44
49
  export declare const RouterTab: import('vue').DefineComponent<{
45
50
  tabs: {
@@ -78,9 +83,13 @@ export declare const RouterTab: import('vue').DefineComponent<{
78
83
  type: import('vue').PropType<boolean | RouterTabsMenuConfig[]>
79
84
  default: true
80
85
  }
81
- storage: {
82
- type: BooleanConstructor | StringConstructor
83
- default: boolean
86
+ cookieKey: {
87
+ type: StringConstructor
88
+ default: string | null
89
+ }
90
+ persistence: {
91
+ type: import('vue').PropType<RouterTabsPersistenceOptions | null>
92
+ default: RouterTabsPersistenceOptions | null
84
93
  }
85
94
  }, any, any, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, Record<string, any>, string, import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, Readonly<{
86
95
  tabs?: TabInput[] | undefined
@@ -92,7 +101,8 @@ export declare const RouterTab: import('vue').DefineComponent<{
92
101
  tabTransition?: import('./lib/core/types').TransitionLike | undefined
93
102
  pageTransition?: import('./lib/core/types').TransitionLike | undefined
94
103
  contextmenu?: boolean | RouterTabsMenuConfig[] | undefined
95
- storage?: boolean | string | undefined
104
+ cookieKey?: string | undefined
105
+ persistence?: RouterTabsPersistenceOptions | null | undefined
96
106
  }> & {
97
107
  tabs?: TabInput[] | undefined
98
108
  keepAlive?: boolean | undefined
@@ -103,7 +113,8 @@ export declare const RouterTab: import('vue').DefineComponent<{
103
113
  tabTransition?: import('./lib/core/types').TransitionLike | undefined
104
114
  pageTransition?: import('./lib/core/types').TransitionLike | undefined
105
115
  contextmenu?: boolean | RouterTabsMenuConfig[] | undefined
106
- storage?: boolean | string | undefined
116
+ cookieKey?: string | undefined
117
+ persistence?: RouterTabsPersistenceOptions | null | undefined
107
118
  }, {
108
119
  tabs: TabInput[]
109
120
  keepAlive: boolean
@@ -114,7 +125,8 @@ export declare const RouterTab: import('vue').DefineComponent<{
114
125
  tabTransition: import('./lib/core/types').TransitionLike
115
126
  pageTransition: import('./lib/core/types').TransitionLike
116
127
  contextmenu: true
117
- storage: boolean
128
+ cookieKey: string | null
129
+ persistence: RouterTabsPersistenceOptions | null
118
130
  }>
119
131
 
120
132
  export interface RouterTabPlugin extends Plugin {}
@@ -131,4 +143,4 @@ declare module '@vue/runtime-core' {
131
143
  declare module './constants' {
132
144
  const value: any;
133
145
  export = value;
134
- }
146
+ }
@@ -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>
@@ -110,15 +110,15 @@ import type {
110
110
  RouterTabsMenuItem,
111
111
  RouterTabsMenuPreset,
112
112
  RouterTabsOptions,
113
- RouterTabsSnapshot,
113
+ RouterTabsPersistenceOptions,
114
114
  TabInput,
115
115
  TabRecord,
116
116
  TransitionLike
117
117
  } from '../core/types'
118
118
  import { getTransOpt } from '../util/index'
119
119
  import { routerTabsKey } from '../constants'
120
+ import { useRouterTabsPersistence } from '../persistence'
120
121
 
121
- const hasSessionStorage = typeof window !== 'undefined' && 'sessionStorage' in window
122
122
 
123
123
  interface ResolvedMenuItem {
124
124
  id: string
@@ -167,9 +167,13 @@ export default defineComponent({
167
167
  type: [Boolean, Array] as PropType<boolean | RouterTabsMenuConfig[]>,
168
168
  default: true
169
169
  },
170
- storage: {
171
- type: [Boolean, String],
172
- default: false
170
+ cookieKey: {
171
+ type: String,
172
+ default: null
173
+ },
174
+ persistence: {
175
+ type: Object as PropType<RouterTabsPersistenceOptions | null>,
176
+ default: null
173
177
  }
174
178
  },
175
179
  setup(props) {
@@ -195,6 +199,14 @@ export default defineComponent({
195
199
  provide(routerTabsKey, controller)
196
200
  instance.appContext.config.globalProperties.$tabs = controller
197
201
 
202
+ if (props.cookieKey || props.persistence) {
203
+ const options: RouterTabsPersistenceOptions = {
204
+ ...(props.persistence ?? {})
205
+ }
206
+ if (props.cookieKey) options.cookieKey = props.cookieKey
207
+ useRouterTabsPersistence(options)
208
+ }
209
+
198
210
  const tabTransitionProps = computed(() => getTransOpt(props.tabTransition))
199
211
  const pageTransitionProps = computed(() => getTransOpt(props.pageTransition))
200
212
 
@@ -204,15 +216,6 @@ export default defineComponent({
204
216
  position: { x: 0, y: 0 }
205
217
  })
206
218
 
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
219
  type MenuConfig = RouterTabsMenuConfig
217
220
  type MenuActionContext = RouterTabsMenuContext
218
221
  type CustomMenuOption = RouterTabsMenuItem
@@ -406,49 +409,13 @@ export default defineComponent({
406
409
  return controller.refreshingKey.value === controller.getRouteKey(route)
407
410
  }
408
411
 
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
412
  onMounted(() => {
444
413
  document.addEventListener('keydown', hideContextMenu)
445
- restoreTabsFromStorage()
446
414
  })
447
415
 
448
416
  onBeforeUnmount(() => {
449
417
  document.removeEventListener('keydown', hideContextMenu)
450
418
  instance.appContext.config.globalProperties.$tabs = null
451
- persistTabsSnapshot()
452
419
  })
453
420
 
454
421
  watch(
@@ -479,25 +446,6 @@ export default defineComponent({
479
446
  }
480
447
  )
481
448
 
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
449
  const includeKeys = controller.includeKeys
502
450
 
503
451
  return {
@@ -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.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -42,7 +42,10 @@
42
42
  "router",
43
43
  "tabs",
44
44
  "tab",
45
- "router-tab"
45
+ "router-tab",
46
+ "router-tabs",
47
+ "cookies",
48
+ "persistence"
46
49
  ],
47
50
  "license": "MIT",
48
51
  "homepage": "https://github.com/anilshr25/vue3-router-tab/#readme",
@@ -1,13 +0,0 @@
1
- <template>
2
- <span class="router-tabs" aria-hidden="true" />
3
- </template>
4
-
5
- <script setup lang="ts">
6
- import { useRouterTabsPiniaPersistence } from '../pinia'
7
- defineOptions({ name: 'RouterTabs' })
8
- import type { RouterTabsPiniaOptions } from '../pinia'
9
-
10
- const props = defineProps<RouterTabsPiniaOptions>()
11
-
12
- useRouterTabsPiniaPersistence(props as RouterTabsPiniaOptions)
13
- </script>