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.
- package/README.md +47 -50
- package/dist/vue3-router-tab.js +479 -500
- package/dist/vue3-router-tab.umd.cjs +5 -5
- package/index.d.ts +25 -13
- package/lib/components/RouterTab.vue +19 -71
- 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 +5 -2
- package/lib/components/RouterTabsPinia.vue +0 -13
- package/lib/pinia.ts +0 -149
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
(function(
|
|
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
|
|
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
|
-
<${
|
|
9
|
+
<${o}>
|
|
10
10
|
<component :is="Component" />
|
|
11
|
-
</${
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
45
|
+
export declare function useRouterTabsPersistence(options?: RouterTabsPersistenceOptions): void
|
|
41
46
|
|
|
42
|
-
export declare const
|
|
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
|
-
|
|
82
|
-
type:
|
|
83
|
-
default:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
type:
|
|
172
|
-
default:
|
|
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
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.0
|
|
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>
|