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.
- package/README.md +72 -49
- package/dist/vue3-router-tab.js +450 -574
- package/dist/vue3-router-tab.umd.cjs +1 -12
- package/lib/components/RouterTab.vue +54 -98
- package/lib/components/RouterTabs.vue +14 -0
- package/lib/constants.ts +2 -0
- package/lib/core/types.ts +12 -0
- package/lib/index.ts +9 -11
- package/lib/persistence.ts +171 -0
- package/package.json +11 -8
- package/index.d.ts +0 -134
- package/lib/components/RouterTabsPinia.vue +0 -13
- package/lib/pinia.ts +0 -149
|
@@ -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="
|
|
43
|
-
<
|
|
44
|
-
v-bind="
|
|
45
|
-
|
|
46
|
-
>
|
|
47
|
-
<
|
|
48
|
-
v-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
</
|
|
59
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
type:
|
|
172
|
-
default:
|
|
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
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
|
|
3
|
+
import RouterTabsComponent from './components/RouterTabs.vue'
|
|
4
4
|
import { routerTabsKey } from './constants'
|
|
5
5
|
import useRouterTabs from './useRouterTabs'
|
|
6
|
-
import {
|
|
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,
|
|
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
|
|
23
|
-
|
|
22
|
+
const persistenceComponentName = RouterTabsComponent.name || 'RouterTabs'
|
|
23
|
+
|
|
24
24
|
app.component(componentName, RouterTab)
|
|
25
|
-
app.component(
|
|
25
|
+
app.component(persistenceComponentName, RouterTabsComponent)
|
|
26
26
|
|
|
27
|
-
if (
|
|
28
|
-
app.component('router-tabs',
|
|
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.
|
|
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
|
}
|