vue3-router-tab 1.2.6 → 1.2.8
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 +589 -78
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +685 -497
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +204 -10
- package/lib/core/createRouterTabs.ts +36 -2
- package/lib/core/types.ts +3 -0
- package/lib/index.ts +40 -9
- package/lib/persistence.ts +5 -2
- package/lib/scss/index.scss +310 -10
- package/lib/scss/variables.scss +14 -2
- package/lib/theme.ts +39 -4
- package/lib/useReactiveTab.ts +49 -49
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(T,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):(T=typeof globalThis<"u"?globalThis:T||self,t(T["vue3-router-tab"]={},T.Vue,T.VueRouter))})(this,(function(T,t,me){"use strict";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 $(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 ge={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 x(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=ge[o.toLowerCase()];return a?a(e):o}return e.fullPath}function J(e,o){const a=e.meta?.keepAlive;return typeof a=="boolean"?a:o}function q(e,o){const a=e.meta?.reuse;return typeof a=="boolean"?a:o}function oe(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 _(e,o,a){const r=oe(e);return{id:x(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:J(e,a),reusable:q(e,!1),closable:r.closable??!0,...r,...o}}function G(e,o,a,r){e.find(p=>p.id===o.id)||e.push(o)}function ae(e,o,a){if(!o||o<=0)return;const r=e.filter(i=>i.alive);for(;r.length>o;){const i=r.shift();if(!i||i.id===a)continue;const p=e.findIndex(k=>k.id===i.id);p>-1&&(e[p].alive=!1)}}function he(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function ye(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 Te(e,o={}){const a=pe(o),r=t.reactive([]),i=t.ref(null),p=t.shallowRef(),k=t.ref(null),c=t.computed(()=>r.filter(s=>s.alive).map(s=>s.id));let u=!1;function g(s){const f=typeof s.matched=="object"?s:$(e,s);return{key:x(f),fullPath:f.fullPath,alive:J(f,a.keepAlive),reusable:q(f,!1),matched:f}}function R(s){const f=x(s);let b=r.find(h=>h.id===f);return b?(b.fullPath=s.fullPath,b.to=s.fullPath,b.matched=s,b.alive=J(s,a.keepAlive),b.reusable=q(s,b.reusable),Object.assign(b,oe(s)),b):(b=_(s,{},a.keepAlive),G(r,b,a.appendPosition,i.value),ae(r,a.maxAlive,i.value),b)}async function I(s,f=!1,b=!0){const h=$(e,s),B=x(h),A=i.value===B;b==="sameTab"&&(b=A),b&&await M(B,!0),await e[f?"replace":"push"](h),A&&await z()}function K(s){const f=r.findIndex(D=>D.id===s);if(f===-1)return a.defaultRoute;const b=r[f+1],h=r[f-1],B=r.find(D=>D.id!==s),A=b||h||B;return A?A.to:a.defaultRoute}async function E(s=i.value,f={}){if(!s)return;if(!f.force&&a.keepLastTab&&r.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");const h=i.value===s&&f.redirect!==null,B=h?f.redirect??K(s):null;await U(s,{force:f.force}),f.redirect!==null&&h&&B&&await e.replace(B)}async function U(s,f={}){const b=r.findIndex(h=>h.id===s);b!==-1&&(r.splice(b,1),k.value===s&&(k.value=null),i.value===s&&(i.value=null,p.value=void 0))}async function M(s=i.value??void 0,f=!1){s&&(k.value=s,await t.nextTick(),f||await t.nextTick(),k.value=null)}async function w(s=!1){for(const f of r)await M(f.id,s)}async function y(s=a.defaultRoute){r.splice(0,r.length),i.value=null,p.value=void 0;for(const f of a.initialTabs){const b=$(e,f.to),h=_(b,f,a.keepAlive);r.push(h)}await e.replace(s)}async function z(){const s=i.value;s&&await M(s,!0)}function V(s){return typeof s.matched=="object"?x(s):x($(e,s))}function F(){const s=r.find(f=>f.id===i.value);return{tabs:r.map(he),active:s?s.to:null}}async function Y(s){u=!0,r.splice(0,r.length),i.value=null,p.value=void 0;const f=s?.tabs??[];for(const h of f)try{const B=$(e,h.to),A=ye(h),D=_(B,A,a.keepAlive);G(r,D,"last",null)}catch{}u=!1;const b=s?.active??f[f.length-1]?.to??a.defaultRoute;if(b)try{await e.replace(b)}catch{}}return t.watch(()=>e.currentRoute.value,s=>{if(u)return;const f=R(s);i.value=f.id,p.value=f,ae(r,a.maxAlive,i.value)},{immediate:!0}),a.initialTabs.length&&a.initialTabs.forEach(s=>{const f=$(e,s.to),b=_(f,s,a.keepAlive);G(r,b)}),{options:a,tabs:r,activeId:i,current:p,includeKeys:c,refreshingKey:k,openTab:I,closeTab:E,removeTab:U,refreshTab:M,refreshAll:w,reset:y,reload:z,getRouteKey:V,matchRoute:g,snapshot:F,hydrate:Y}}function ie(e){return e?typeof e=="string"?{name:e}:e:{}}const S=Symbol("RouterTabsContext"),N="router-tabs:snapshot";function W(e={}){const{optional:o=!1}=e,a=t.inject(S,null);if(a)return a;const r=t.inject("$tabs",null);if(r)return r;const p=t.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(p)return p;if(!o)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const ke=864e5;function ve(e){if(typeof document>"u")return null;const o=`${encodeURIComponent(e)}=`,a=document.cookie?document.cookie.split("; "):[];for(const r of a)if(r.startsWith(o))return decodeURIComponent(r.slice(o.length));return null}function re(e,o,a){if(typeof document>"u")return;const{expiresInDays:r=7,path:i="/",domain:p,secure:k,sameSite:c="lax"}=a,u=[`${encodeURIComponent(e)}=${encodeURIComponent(o)}`];if(r!==1/0){const g=new Date(Date.now()+r*ke).toUTCString();u.push(`Expires=${g}`)}i&&u.push(`Path=${i}`),p&&u.push(`Domain=${p}`),k&&u.push("Secure"),c&&u.push(`SameSite=${c.charAt(0).toUpperCase()}${c.slice(1)}`),document.cookie=u.join("; ")}function le(e,o){if(typeof document>"u")return;const{path:a="/",domain:r}=o,i=[`${encodeURIComponent(e)}=`];i.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),a&&i.push(`Path=${a}`),r&&i.push(`Domain=${r}`),document.cookie=i.join("; ")}const Ce=e=>JSON.stringify(e??null),Re=e=>{if(!e)return null;try{return JSON.parse(e)}catch{return null}};function H(e={}){const{cookieKey:o=N,serialize:a=Ce,deserialize:r=Re}=e,i=W({optional:!0}),p=t.ref(!0),k=c=>{t.onMounted(async()=>{const u=r(ve(o));if(u&&u.tabs?.length)try{if(p.value=!0,await c.hydrate(u),u.active){await t.nextTick();const R=c.tabs.find(I=>I.to===u.active);R&&(c.activeId.value=R.id,c.current.value=R)}}finally{p.value=!1}else try{p.value=!0;const R=e.fallbackRoute??c.options.defaultRoute;await c.reset(R)}finally{p.value=!1}const g=c.snapshot();g.tabs.length?re(o,a(g),e):le(o,e),p.value=!1}),t.watch(()=>({tabs:c.tabs.map(u=>({to:u.to,title:u.title,tips:u.tips,icon:u.icon,tabClass:u.tabClass,closable:u.closable})),active:c.activeId.value}),()=>{if(p.value)return;const u=c.snapshot();u.tabs.length?re(o,a(u),e):le(o,e)},{deep:!0})};i?k(i):t.onMounted(()=>{const c=W({optional:!0});c&&k(c)})}const we=t.defineComponent({name:"RouterTab",components:{RouterView:me.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:N},persistence:{type:Object,default:null},sortable:{type:Boolean,default:!0}},emits:["tab-sort","tab-sorted"],setup(e,{emit:o}){const a=t.getCurrentInstance();if(!a)throw new Error("[RouterTab] component must be used within a Vue application context.");const r=a.appContext.app.config.globalProperties.$router;if(!r)throw new Error("[RouterTab] Vue Router is required. Make sure to call app.use(router) before RouterTab.");const i=Te(r,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});t.provide(S,i),a.appContext.config.globalProperties.$tabs=i;const p=t.computed(()=>!!a?.slots?.default),k=t.ref(0),c=t.computed(()=>{k.value;const n={};return i.tabs.forEach(l=>{const m=typeof l.title=="string"?l.title:String(l.title||ee(l));n[l.id]=m}),n});function u(){k.value++}const g=new Map,R=new Map;function I(n,l){if(!l||g.has(n))return;g.set(n,l);const m=i.tabs.find(P=>i.getRouteKey(P.to)===n);if(!m)return;const v=[];if(l.routeTabTitle!==void 0){const P=t.watch(()=>{const d=l.routeTabTitle;return d&&typeof d=="object"&&"value"in d?d.value:d},d=>{if(d!=null){const fe=String(d);m.title=fe,u()}},{immediate:!0});v.push(P)}if(l.routeTabIcon!==void 0){const P=t.watch(()=>{const d=l.routeTabIcon;return d&&typeof d=="object"&&"value"in d?d.value:d},d=>{d!=null&&(m.icon=String(d),u())},{immediate:!0});v.push(P)}if(l.routeTabClosable!==void 0){const P=t.watch(()=>{const d=l.routeTabClosable;return d&&typeof d=="object"&&"value"in d?d.value:d},d=>{d!=null&&(m.closable=!!d,u())},{immediate:!0});v.push(P)}if(l.routeTabMeta!==void 0){const P=t.watch(()=>{const d=l.routeTabMeta;return d&&typeof d=="object"&&"value"in d?d.value:d},d=>{d&&typeof d=="object"&&(Object.assign(m,d),u())},{immediate:!0,deep:!0});v.push(P)}R.set(n,v)}function K(n){const l=R.get(n);l&&(l.forEach(m=>m()),R.delete(n)),g.delete(n)}function E(n,l){n?n.routeTabTitle!==void 0||n.routeTabIcon!==void 0||n.routeTabClosable!==void 0?I(l,n):n.$&&(n.$.routeTabTitle!==void 0||n.$.routeTabIcon!==void 0||n.$.routeTabClosable!==void 0)&&I(l,n.$):n===null&&K(l)}if(e.cookieKey!==null||e.persistence){const n={...e.persistence??{}};e.cookieKey!==null?n.cookieKey=e.cookieKey||N:n.cookieKey||(n.cookieKey=N),H(n)}const U=t.computed(()=>ie(e.tabTransition)),M=t.computed(()=>ie(e.pageTransition)),w=t.reactive({visible:!1,target:null,position:{x:0,y:0}}),y=t.reactive({dragging:!1,dragIndex:-1,dropIndex:-1,dragTab:null}),z=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function V(n){return i.tabs.findIndex(l=>l.id===n)}function F(n){const l=V(n.id);return l>0?i.tabs.slice(0,l):[]}function Y(n){const l=V(n.id);return l>-1?i.tabs.slice(l+1):[]}function s(n){return i.tabs.filter(l=>l.id!==n.id)}async function f(n,l){const m=n.filter(v=>v.closable!==!1);if(m.length){for(const v of m)i.activeId.value===v.id?await i.closeTab(v.id,{redirect:l.to,force:!0}):await i.removeTab(v.id,{force:!0});i.activeId.value!==l.id&&await i.openTab(l.to,!0,!1)}}const b={refresh:{label:"Refresh",handler:async({target:n})=>{await i.refreshTab(n.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await i.refreshAll(!0)}},close:{label:"Close",handler:async({target:n})=>{await i.closeTab(n.id)},enable:({target:n})=>te(n)},closeLefts:{label:"Close to the Left",handler:async({target:n})=>{await f(F(n),n)},enable:({target:n})=>F(n).some(l=>l.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:n})=>{await f(Y(n),n)},enable:({target:n})=>Y(n).some(l=>l.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:n})=>{await f(s(n),n)},enable:({target:n})=>s(n).some(l=>l.closable!==!1)}};function h(){w.visible=!1,w.target=null}function B(n,l){e.contextmenu&&(w.visible=!0,w.target=n,w.position.x=l.clientX,w.position.y=l.clientY,document.addEventListener("click",h,{once:!0}))}function A(n,l){const m=typeof n=="string"?{id:n}:n,v=b[m.id],P=m.label??v?.label??String(m.id),d=m.visible??v?.visible??!0;if(!(typeof d=="function"?d(l):d!==!1))return null;const ne=m.enable??v?.enable??!0,it=typeof ne=="function"?ne(l):ne!==!1,be=m.handler??v?.handler;if(!be)return null;const rt=async()=>{await Promise.resolve(be(l))};return{id:String(m.id),label:P,disabled:!it,action:rt}}const D=t.computed(()=>{if(!w.visible||!w.target||e.contextmenu===!1)return[];const n=Array.isArray(e.contextmenu)?e.contextmenu:z,l={target:w.target,controller:i};return n.map(m=>A(m,l)).filter(m=>!!m)});async function qe(n){n.disabled||(h(),await n.action())}function ee(n){return typeof n.title=="string"&&n.title.trim()?n.title:Array.isArray(n.title)&&n.title.length&&String(n.title[0]).trim()?String(n.title[0]):"Untitled"}function Ge(n){return c.value[n.id]||ee(n)}function te(n){return!(n.closable===!1||i.options.keepLastTab&&i.tabs.length<=1)}async function We(n){await i.closeTab(n.id)}function He(n){i.activeId.value!==n.id&&i.openTab(n.to,!1)}function Qe(n){return["router-tab__item",{"is-active":i.activeId.value===n.id,"is-closable":te(n),"is-dragging":y.dragging&&y.dragTab?.id===n.id,"is-drag-over":y.dropIndex===V(n.id)},n.tabClass]}function Xe(n){return i.refreshingKey.value===i.getRouteKey(n)}function Ze(n,l,m){e.sortable&&(y.dragging=!0,y.dragIndex=l,y.dragTab=n,m.dataTransfer&&(m.dataTransfer.effectAllowed="move",m.dataTransfer.setData("text/plain",n.id)),o("tab-sort",{tab:n,index:l}))}function et(n,l){!e.sortable||!y.dragging||(l.preventDefault(),l.dataTransfer&&(l.dataTransfer.dropEffect="move"))}function tt(n){!e.sortable||!y.dragging||(y.dropIndex=n)}function nt(){!e.sortable||y.dragging}function ot(n,l){if(!(!e.sortable||!y.dragging)){if(l.preventDefault(),y.dragIndex!==-1&&y.dragIndex!==n){const m=i.tabs.splice(y.dragIndex,1)[0];i.tabs.splice(n,0,m),o("tab-sorted",{tab:m,fromIndex:y.dragIndex,toIndex:n})}de()}}function de(){y.dragging=!1,y.dragIndex=-1,y.dropIndex=-1,y.dragTab=null}t.onMounted(()=>{document.addEventListener("keydown",h)}),t.onBeforeUnmount(()=>{document.removeEventListener("keydown",h),a.appContext.config.globalProperties.$tabs=null,R.forEach(n=>{n.forEach(l=>l())}),R.clear(),g.clear()}),t.watch(()=>e.keepAlive,n=>{i.options.keepAlive=n}),t.watch(()=>i.activeId.value,()=>h()),t.watch(()=>e.contextmenu,n=>{n||h()}),t.watch(()=>D.value.length,n=>{w.visible&&n===0&&h()});const at=i.includeKeys;return{controller:i,tabs:i.tabs,includeKeys:at,tabTransitionProps:U,pageTransitionProps:M,buildTabClass:Qe,activate:He,close:We,context:w,menuItems:D,handleMenuAction:qe,showContextMenu:B,hideContextMenu:h,getTabTitle:ee,isClosable:te,isRefreshing:Xe,hasCustomSlot:p,onDragStart:Ze,onDragOver:et,onDragEnter:tt,onDragLeave:nt,onDrop:ot,onDragEnd:de,setupComponentWatching:I,cleanupComponentWatching:K,handleComponentRef:E,getReactiveTabTitle:Ge,triggerTabUpdate:u}}}),Be=(e,o)=>{const a=e.__vccOpts||e;for(const[r,i]of o)a[r]=i;return a},Ee={class:"router-tab"},Pe={class:"router-tab__header"},Ae={class:"router-tab__slot-start"},Ie={class:"router-tab__scroll"},De=["data-title","draggable","onClick","onAuxclick","onContextmenu","onDragstart","onDragover","onDragenter","onDrop"],$e=["title"],xe=["onClick"],Me={class:"router-tab__slot-end"},Se={class:"router-tab__container"},Le=["aria-disabled","onClick"];function Ke(e,o,a,r,i,p){const k=t.resolveComponent("RouterView");return t.openBlock(),t.createElementBlock("div",Ee,[t.createElementVNode("header",Pe,[t.createElementVNode("div",Ae,[t.renderSlot(e.$slots,"start")]),t.createElementVNode("div",Ie,[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,(c,u)=>(t.openBlock(),t.createElementBlock("li",{key:c.id,class:t.normalizeClass(e.buildTabClass(c)),"data-title":e.getTabTitle(c),draggable:e.sortable,onClick:g=>e.activate(c),onAuxclick:t.withModifiers(g=>e.close(c),["middle","prevent"]),onContextmenu:t.withModifiers(g=>e.showContextMenu(c,g),["prevent"]),onDragstart:g=>e.onDragStart(c,u,g),onDragover:g=>e.onDragOver(u,g),onDragenter:g=>e.onDragEnter(u),onDragleave:o[0]||(o[0]=(...g)=>e.onDragLeave&&e.onDragLeave(...g)),onDrop:g=>e.onDrop(u,g),onDragend:o[1]||(o[1]=(...g)=>e.onDragEnd&&e.onDragEnd(...g))},[c.icon?(t.openBlock(),t.createElementBlock("i",{key:0,class:t.normalizeClass(["router-tab__item-icon",c.icon])},null,2)):t.createCommentVNode("",!0),t.createElementVNode("span",{class:"router-tab__item-title",title:e.getReactiveTabTitle(c)},t.toDisplayString(e.getReactiveTabTitle(c)),9,$e),e.isClosable(c)?(t.openBlock(),t.createElementBlock("a",{key:1,class:"router-tab__item-close",onClick:t.withModifiers(g=>e.close(c),["stop"])},null,8,xe)):t.createCommentVNode("",!0)],42,De))),128))]),_:1},16)]),t.createElementVNode("div",Me,[t.renderSlot(e.$slots,"end")])]),t.createElementVNode("div",Se,[t.createVNode(k,null,{default:t.withCtx(c=>[e.hasCustomSlot?t.renderSlot(e.$slots,"default",t.normalizeProps(t.mergeProps({key:0},{...c,controller:e.controller,pageRef:u=>e.handleComponentRef(u,e.controller.getRouteKey(c.route))}))):(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(c.route)?t.createCommentVNode("",!0):(t.openBlock(),t.createBlock(t.resolveDynamicComponent(c.Component),{key:e.controller.getRouteKey(c.route),ref:u=>e.handleComponentRef(u,e.controller.getRouteKey(c.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(c.route)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(c.Component),{key:e.controller.getRouteKey(c.route)+(e.isRefreshing(c.route)?"-refresh":""),ref:u=>e.handleComponentRef(u,e.controller.getRouteKey(c.route)),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,c=>(t.openBlock(),t.createElementBlock("a",{key:c.id,class:"router-tab__contextmenu-item","aria-disabled":c.disabled,onClick:t.withModifiers(u=>e.handleMenuAction(c),["prevent"])},t.toDisplayString(c.label),9,Le))),128))],4)):t.createCommentVNode("",!0)])}const Q=Be(we,[["render",Ke]]),Ve={class:"router-tabs","aria-hidden":"true"},j=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 H(e),(a,r)=>(t.openBlock(),t.createElementBlock("span",Ve))}}),se="tab-theme-style",_e="tab-theme-primary-color",Ne="system",je="(prefers-color-scheme: dark)";let L=null;const C={primary:"#034960",background:"#ffffff",text:"#1e293b",border:"#e2e8f0",activeBackground:"#034960",activeText:"#ffffff",activeBorder:"#034960",headerBackground:"#ffffff",buttonBackground:"#f8fafc",buttonColor:"#034960",activeButtonBackground:"#034960",activeButtonColor:"#ffffff",iconColor:"#475569"},Oe={primary:"#38bdf8",background:"#0f172a",text:"#f1f5f9",border:"#334155",activeBackground:"#1e293b",activeText:"#38bdf8",activeBorder:"#38bdf8",headerBackground:"#0c4a6e",buttonBackground:"#1e293b",buttonColor:"#f1f5f9",activeButtonBackground:"#38bdf8",activeButtonColor:"#0f172a",iconColor:"#cbd5e1"};function X(e){typeof document>"u"||(document.documentElement.style.setProperty("--router-tab-primary",e.primary??C.primary),document.documentElement.style.setProperty("--router-tab-header-bg",e.headerBackground??C.headerBackground),document.documentElement.style.setProperty("--router-tab-background",e.background??C.background),document.documentElement.style.setProperty("--router-tab-active-background",e.activeBackground??C.activeBackground),document.documentElement.style.setProperty("--router-tab-text",e.text??C.text),document.documentElement.style.setProperty("--router-tab-active-text",e.activeText??C.activeText),document.documentElement.style.setProperty("--router-tab-border",e.border??C.border),document.documentElement.style.setProperty("--router-tab-active-border",e.activeBorder??C.activeBorder),document.documentElement.style.setProperty("--router-tab-button-color",e.buttonColor??C.buttonColor),document.documentElement.style.setProperty("--router-tab-active-button-color",e.activeButtonColor??C.activeButtonColor),document.documentElement.style.setProperty("--router-tab-button-background",e.buttonBackground??C.buttonBackground),document.documentElement.style.setProperty("--router-tab-active-button-background",e.activeButtonBackground??C.activeButtonBackground),document.documentElement.style.setProperty("--router-tab-icon-color",e.iconColor??C.iconColor))}function ce(e){if(typeof document>"u")return;const o=document.documentElement,a=window.matchMedia(je),r=()=>{o.dataset.theme=a.matches?"dark":"light"};L&&(a.removeEventListener("change",L),L=null),e==="system"?(r(),L=()=>r(),a.addEventListener("change",L)):o.dataset.theme=e}function ue(e={}){if(typeof window>"u")return;const{styleKey:o=se,defaultStyle:a=Ne}=e,r=window.localStorage.getItem(o)??a;ce(r),X(r==="dark"?Oe:C)}function Ue(e,o){if(typeof window>"u")return;const a=o?.styleKey??se;window.localStorage.setItem(a,e),ce(e)}function ze(e,o){if(typeof window>"u")return;const a=o?.primaryKey??_e;window.localStorage.setItem(a,JSON.stringify(e)),X(e)}function O(e={}){const o=t.ref(e.title||"Untitled"),a=t.ref(e.icon||"mdi-tab"),r=t.ref(e.closable!==!1),i=t.ref(e.meta||{}),p=typeof e.title=="function"?t.computed(e.title):o,k=typeof e.icon=="function"?t.computed(e.icon):a,c=typeof e.closable=="function"?t.computed(e.closable):r,u=typeof e.meta=="function"?t.computed(e.meta):i;return{routeTabTitle:p,routeTabIcon:k,routeTabClosable:c,routeTabMeta:u,updateTitle:E=>{"value"in o&&(o.value=E)},updateIcon:E=>{"value"in a&&(a.value=E)},updateClosable:E=>{"value"in r&&(r.value=E)},updateMeta:E=>{"value"in i&&(i.value=E)}}}function Fe(e,o="Page"){return O({title:t.computed(()=>e.value?"Loading...":o),icon:t.computed(()=>e.value?"mdi-loading mdi-spin":"mdi-page"),closable:t.computed(()=>!e.value)})}function Ye(e,o="Page",a="mdi-page"){return O({title:t.computed(()=>e.value>0?`${o} (${e.value})`:o),icon:t.computed(()=>e.value>0?"mdi-bell-badge":a)})}function Je(e,o="Page"){const a={normal:{suffix:"",icon:"mdi-page"},loading:{suffix:" - Loading",icon:"mdi-loading mdi-spin"},error:{suffix:" - Error",icon:"mdi-alert"},success:{suffix:" - Success",icon:"mdi-check-circle"}};return O({title:t.computed(()=>o+a[e.value].suffix),icon:t.computed(()=>a[e.value].icon),closable:t.computed(()=>e.value!=="loading")})}const Z={install(e){if(Z._installed)return;Z._installed=!0,ue();const o=Q.name||"RouterTab",a=j.name||"RouterTabs";e.component(o,Q),e.component(a,j),a!=="router-tabs"&&e.component("router-tabs",j),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[S]},set(r){r&&e.provide(S,r)}})}};T.RouterTab=Q,T.RouterTabs=j,T.default=Z,T.initRouterTabsTheme=ue,T.routerTabsKey=S,T.setRouterTabsPrimary=ze,T.setRouterTabsTheme=Ue,T.useLoadingTab=Fe,T.useNotificationTab=Ye,T.useReactiveTab=O,T.useRouterTabs=W,T.useRouterTabsPersistence=H,T.useStatusTab=Je,Object.defineProperties(T,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
1
|
+
(function(w,n){typeof exports=="object"&&typeof module<"u"?n(exports,require("vue"),require("vue-router")):typeof define=="function"&&define.amd?define(["exports","vue","vue-router"],n):(w=typeof globalThis<"u"?globalThis:w||self,n(w["vue3-router-tab"]={},w.Vue,w.VueRouter))})(this,(function(w,n,Re){"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 D(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 Pe={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 $(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=Pe[o.toLowerCase()];return a?a(e):o}return e.fullPath}function G(e,o){const a=e.meta?.keepAlive;return typeof a=="boolean"?a:o}function X(e,o){const a=e.meta?.reuse;return typeof a=="boolean"?a:o}function se(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 _(e,o,a){const s=se(e);return{id:$(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:G(e,a),reusable:X(e,!1),closable:s.closable??!0,renderKey:typeof o.renderKey=="number"?o.renderKey:0,...s,...o}}function Q(e,o,a,s){if(!e.find(p=>p.id===o.id)){if(a==="next"&&s){const p=e.findIndex(v=>v.id===s);if(p!==-1){e.splice(p+1,0,o);return}}e.push(o)}}function ce(e,o,a){if(!o||o<=0)return;const s=e.filter(i=>i.alive);for(;s.length>o;){const i=s.shift();if(!i||i.id===a)continue;const p=e.findIndex(v=>v.id===i.id);p>-1&&(e[p].alive=!1)}}function Be(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable,renderKey:e.renderKey}}function Ae(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),"renderKey"in e&&typeof e.renderKey=="number"&&(o.renderKey=e.renderKey),o}function Ke(e,o={}){const a=Ee(o),s=n.reactive([]),i=n.ref(null),p=n.shallowRef(),v=n.ref(null),c=n.computed(()=>s.filter(l=>l.alive).map(l=>`${l.id}::${l.renderKey}`));let d=!1;function g(l){const b=typeof l.matched=="object"?l:D(e,l);return{key:$(b),fullPath:b.fullPath,alive:G(b,a.keepAlive),reusable:X(b,!1),matched:b}}function E(l){const b=$(l);let m=s.find(k=>k.id===b);return m?(m.fullPath=l.fullPath,m.to=l.fullPath,m.matched=l,m.alive=G(l,a.keepAlive),m.reusable=X(l,m.reusable),Object.assign(m,se(l)),m):(m=_(l,{},a.keepAlive),Q(s,m,a.appendPosition,i.value),ce(s,a.maxAlive,i.value),m)}async function K(l,b=!1,m=!0){const k=D(e,l),C=$(k),P=i.value===C;m==="sameTab"&&(m=P),m&&await M(C,!0),await e[b?"replace":"push"](k),P&&await T()}function S(l){const b=s.findIndex(A=>A.id===l);if(b===-1)return a.defaultRoute;const m=s[b+1],k=s[b-1],C=s.find(A=>A.id!==l),P=m||k||C;return P?P.to:a.defaultRoute}async function j(l=i.value,b={}){if(!l)return;if(!b.force&&a.keepLastTab&&s.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");const k=i.value===l&&b.redirect!==null,C=k?b.redirect??S(l):null;await O(l,{force:b.force}),b.redirect!==null&&k&&C&&await e.replace(C)}async function O(l,b={}){const m=s.findIndex(k=>k.id===l);m!==-1&&(s.splice(m,1),v.value===l&&(v.value=null),i.value===l&&(i.value=null,p.value=void 0))}async function M(l=i.value??void 0,b=!1){if(!l)return;const m=s.find(C=>C.id===l);if(!m)return;const k=a.keepAlive&&m.alive;k&&(m.alive=!1,await n.nextTick()),m.renderKey=(m.renderKey??0)+1,k&&(m.alive=!0,await n.nextTick()),v.value=l,await n.nextTick(),b||await n.nextTick(),v.value=null}async function oe(l=!1){for(const b of s)await M(b.id,l)}async function ae(l=a.defaultRoute){s.splice(0,s.length),i.value=null,p.value=void 0;for(const b of a.initialTabs){const m=D(e,b.to),k=_(m,b,a.keepAlive);s.push(k)}await e.replace(l)}async function T(){const l=i.value;l&&await M(l,!0)}function W(l){return typeof l.matched=="object"?$(l):$(D(e,l))}function x(){const l=s.find(b=>b.id===i.value);return{tabs:s.map(Be),active:l?l.to:null}}async function I(l){d=!0,s.splice(0,s.length),i.value=null,p.value=void 0;const b=l?.tabs??[];for(const k of b)try{const C=D(e,k.to),P=Ae(k),A=_(C,P,a.keepAlive);Q(s,A,"last",null)}catch{}d=!1;const m=l?.active??b[b.length-1]?.to??a.defaultRoute;if(m)try{await e.replace(m)}catch{}}return n.watch(()=>e.currentRoute.value,l=>{if(d)return;const b=E(l);i.value=b.id,p.value=b,ce(s,a.maxAlive,i.value)},{immediate:!0}),a.initialTabs.length&&a.initialTabs.forEach(l=>{const b=D(e,l.to),m=_(b,l,a.keepAlive);Q(s,m,"last",null)}),{options:a,tabs:s,activeId:i,current:p,includeKeys:c,refreshingKey:v,openTab:K,closeTab:j,removeTab:O,refreshTab:M,refreshAll:oe,reset:ae,reload:T,getRouteKey:W,matchRoute:g,snapshot:x,hydrate:I}}function ue(e){return e?typeof e=="string"?{name:e}:e:{}}const V=Symbol("RouterTabsContext"),Y="router-tabs:snapshot";function Z(e={}){const{optional:o=!1}=e,a=n.inject(V,null);if(a)return a;const s=n.inject("$tabs",null);if(s)return s;const p=n.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(p)return p;if(!o)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const Ie=864e5;function Se(e){if(typeof document>"u")return null;const o=`${encodeURIComponent(e)}=`,a=document.cookie?document.cookie.split("; "):[];for(const s of a)if(s.startsWith(o))return decodeURIComponent(s.slice(o.length));return null}function de(e,o,a){if(typeof document>"u")return;const{expiresInDays:s=7,path:i="/",domain:p,secure:v,sameSite:c="lax"}=a,d=[`${encodeURIComponent(e)}=${encodeURIComponent(o)}`];if(s!==1/0){const g=new Date(Date.now()+s*Ie).toUTCString();d.push(`Expires=${g}`)}i&&d.push(`Path=${i}`),p&&d.push(`Domain=${p}`),v&&d.push("Secure"),c&&d.push(`SameSite=${c.charAt(0).toUpperCase()}${c.slice(1)}`),document.cookie=d.join("; ")}function fe(e,o){if(typeof document>"u")return;const{path:a="/",domain:s}=o,i=[`${encodeURIComponent(e)}=`];i.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),a&&i.push(`Path=${a}`),s&&i.push(`Domain=${s}`),document.cookie=i.join("; ")}const xe=e=>JSON.stringify(e??null),De=e=>{if(!e)return null;try{return JSON.parse(e)}catch{return null}};function ee(e={}){const{cookieKey:o=Y,serialize:a=xe,deserialize:s=De}=e,i=Z({optional:!0}),p=n.ref(!0),v=c=>{n.onMounted(async()=>{const d=s(Se(o));if(d&&d.tabs?.length)try{if(p.value=!0,await c.hydrate(d),d.active){await n.nextTick();const E=c.tabs.find(K=>K.to===d.active);E&&(c.activeId.value=E.id,c.current.value=E)}}finally{p.value=!1}else if(Object.prototype.hasOwnProperty.call(e,"fallbackRoute"))try{p.value=!0;const E=e.fallbackRoute??c.options.defaultRoute;await c.reset(E)}finally{p.value=!1}else p.value=!1;const g=c.snapshot();g.tabs.length?de(o,a(g),e):fe(o,e),p.value=!1}),n.watch(()=>({tabs:c.tabs.map(d=>({to:d.to,title:d.title,tips:d.tips,icon:d.icon,tabClass:d.tabClass,closable:d.closable,renderKey:d.renderKey})),active:c.activeId.value}),()=>{if(p.value)return;const d=c.snapshot();d.tabs.length?de(o,a(d),e):fe(o,e)},{deep:!0})};i?v(i):n.onMounted(()=>{const c=Z({optional:!0});c&&v(c)})}const $e=n.defineComponent({name:"RouterTab",components:{RouterView:Re.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:Y},persistence:{type:Object,default:null},sortable:{type:Boolean,default:!0}},emits:["tab-sort","tab-sorted"],setup(e,{emit:o}){const a=n.getCurrentInstance();if(!a)throw new Error("[RouterTab] component must be used within a Vue application context.");const s=a.appContext.app.config.globalProperties.$router;if(!s)throw new Error("[RouterTab] Vue Router is required. Make sure to call app.use(router) before RouterTab.");const i=Ke(s,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});n.provide(V,i),a.appContext.config.globalProperties.$tabs=i;const p=n.computed(()=>!!a?.slots?.default),v=n.computed(()=>!!a?.slots?.start),c=n.computed(()=>!!a?.slots?.end),d=n.ref(0),g=n.computed(()=>{d.value;const t={};return i.tabs.forEach(r=>{const f=typeof r.title=="string"?r.title:String(r.title||re(r));t[r.id]=f}),t});function E(){d.value++}const K=new Map,S=new Map;function j(t,r){if(!r||K.has(t))return;K.set(t,r);const f=i.tabs.find(y=>i.getRouteKey(y.to)===t);if(!f)return;const h=[];if(r.routeTabTitle!==void 0){const y=n.watch(()=>{const u=r.routeTabTitle;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{if(u!=null){const L=String(u);f.title=L,E()}},{immediate:!0});h.push(y)}if(r.routeTabIcon!==void 0){const y=n.watch(()=>{const u=r.routeTabIcon;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u!=null&&(f.icon=String(u),E())},{immediate:!0});h.push(y)}if(r.routeTabClosable!==void 0){const y=n.watch(()=>{const u=r.routeTabClosable;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u!=null&&(f.closable=!!u,E())},{immediate:!0});h.push(y)}if(r.routeTabMeta!==void 0){const y=n.watch(()=>{const u=r.routeTabMeta;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u&&typeof u=="object"&&(Object.assign(f,u),E())},{immediate:!0,deep:!0});h.push(y)}S.set(t,h)}function O(t){const r=S.get(t);r&&(r.forEach(f=>f()),S.delete(t)),K.delete(t)}function M(t,r){t?t.routeTabTitle!==void 0||t.routeTabIcon!==void 0||t.routeTabClosable!==void 0?j(r,t):t.$&&(t.$.routeTabTitle!==void 0||t.$.routeTabIcon!==void 0||t.$.routeTabClosable!==void 0)&&j(r,t.$):t===null&&O(r)}if(e.cookieKey!==null||e.persistence){const t={...e.persistence??{}};e.cookieKey!==null?t.cookieKey=e.cookieKey||Y:t.cookieKey||(t.cookieKey=Y),ee(t)}const oe=n.computed(()=>ue(e.tabTransition)),ae=n.computed(()=>ue(e.pageTransition)),T=n.reactive({visible:!1,target:null,position:{x:0,y:0}}),W=n.ref(null),x=n.ref([]),I=n.ref(-1),l=n.reactive({dragging:!1,dragIndex:-1,dropIndex:-1,dragTab:null}),b=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function m(t){return i.tabs.findIndex(r=>r.id===t)}function k(t){const r=m(t.id);return r>0?i.tabs.slice(0,r):[]}function C(t){const r=m(t.id);return r>-1?i.tabs.slice(r+1):[]}function P(t){return i.tabs.filter(r=>r.id!==t.id)}async function A(t,r){const f=t.filter(h=>h.closable!==!1);if(f.length){for(const h of f)i.activeId.value===h.id?await i.closeTab(h.id,{redirect:r.to,force:!0}):await i.removeTab(h.id,{force:!0});i.activeId.value!==r.id&&await i.openTab(r.to,!0,!1)}}const tt={refresh:{label:"Refresh",handler:async({target:t})=>{await i.refreshTab(t.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await i.refreshAll(!0)}},close:{label:"Close",handler:async({target:t})=>{await i.closeTab(t.id)},enable:({target:t})=>ie(t)},closeLefts:{label:"Close to the Left",handler:async({target:t})=>{await A(k(t),t)},enable:({target:t})=>k(t).some(r=>r.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:t})=>{await A(C(t),t)},enable:({target:t})=>C(t).some(r=>r.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:t})=>{await A(P(t),t)},enable:({target:t})=>P(t).some(r=>r.closable!==!1)}};function B(){T.visible=!1,T.target=null,I.value=-1,x.value=[]}function nt(t,r){e.contextmenu&&(T.visible=!0,T.target=t,T.position.x=r.clientX,T.position.y=r.clientY,document.addEventListener("click",B,{once:!0}),n.nextTick(()=>{at()}))}function ot(t,r){const f=typeof t=="string"?{id:t}:t,h=tt[f.id],y=f.label??h?.label??String(f.id),u=f.visible??h?.visible??!0;if(!(typeof u=="function"?u(r):u!==!1))return null;const le=f.enable??h?.enable??!0,kt=typeof le=="function"?le(r):le!==!1,Ce=f.handler??h?.handler;if(!Ce)return null;const vt=async()=>{await Promise.resolve(Ce(r))};return{id:String(f.id),label:y,disabled:!kt,action:vt}}const U=n.computed(()=>{if(!T.visible||!T.target||e.contextmenu===!1)return[];const t=Array.isArray(e.contextmenu)?e.contextmenu:b,r={target:T.target,controller:i};return t.map(f=>ot(f,r)).filter(f=>!!f)});function at(){const t=W.value;if(!t)return;const r=8,{innerWidth:f,innerHeight:h}=window,y=t.getBoundingClientRect();let u=T.position.x,L=T.position.y;y.right>f-r&&(u=Math.max(r,f-y.width-r)),y.bottom>h-r&&(L=Math.max(r,h-y.height-r)),(u!==T.position.x||L!==T.position.y)&&(T.position.x=u,T.position.y=L)}function rt(t,r){x.value[r]=t??null}function it(t){if(t<0)return;x.value[t]?.focus({preventScroll:!0})}function q(t,r,f=U.value){if(!f.length)return-1;const h=f.length;let y=t;for(let u=0;u<h;u++)if(y=(y+r+h)%h,!f[y].disabled)return y;return-1}function z(t){I.value=t,!(t<0)&&n.nextTick(()=>it(t))}function Te(t){const r=q(I.value,t);r!==-1&&z(r)}function lt(t){if(!T.visible)return;const r=t.key,f=U.value;if(!f.length)return;if(r==="Tab"){B();return}if(["ArrowDown","ArrowUp","ArrowRight","ArrowLeft","Home","End","Enter"," ","Spacebar","Escape"].includes(r))switch(t.preventDefault(),r){case"ArrowDown":case"ArrowRight":Te(1);break;case"ArrowUp":case"ArrowLeft":Te(-1);break;case"Home":z(q(-1,1));break;case"End":z(q(f.length,-1));break;case"Enter":case" ":case"Spacebar":{const y=I.value;if(y>-1){const u=f[y];u.disabled||ke(u)}break}case"Escape":B();break}}async function ke(t){t.disabled||(B(),await t.action())}function re(t){return typeof t.title=="string"&&t.title.trim()?t.title:Array.isArray(t.title)&&t.title.length&&String(t.title[0]).trim()?String(t.title[0]):"Untitled"}function st(t){return g.value[t.id]||re(t)}function ve(t){const r=i.getRouteKey(t),h=i.tabs.find(y=>y.id===r)?.renderKey??0;return`${r}::${h}`}function ct(t){return`${ve(t)}::refresh`}function ie(t){return!(t.closable===!1||i.options.keepLastTab&&i.tabs.length<=1)}async function ut(t){await i.closeTab(t.id)}function dt(t){if(t.href&&typeof window<"u"){t.target&&t.target!=="_self"?window.open(t.href,t.target):window.location.assign(t.href);return}i.activeId.value!==t.id&&i.openTab(t.to,!1)}function ft(t){return["router-tab__item",{"is-active":i.activeId.value===t.id,"is-closable":ie(t),"is-dragging":l.dragging&&l.dragTab?.id===t.id,"is-drag-over":l.dropIndex===m(t.id)},t.tabClass]}function bt(t){return i.refreshingKey.value===i.getRouteKey(t)}function mt(t,r,f){e.sortable&&(l.dragging=!0,l.dragIndex=r,l.dragTab=t,f.dataTransfer&&(f.dataTransfer.effectAllowed="move",f.dataTransfer.setData("text/plain",t.id)),o("tab-sort",{tab:t,index:r}))}function pt(t,r){!e.sortable||!l.dragging||(r.preventDefault(),r.dataTransfer&&(r.dataTransfer.dropEffect="move"))}function gt(t){!e.sortable||!l.dragging||(l.dropIndex=t)}function ht(){!e.sortable||l.dragging}function yt(t,r){if(!(!e.sortable||!l.dragging)){if(r.preventDefault(),l.dragIndex!==-1&&l.dragIndex!==t){const f=i.tabs.splice(l.dragIndex,1)[0];i.tabs.splice(t,0,f),o("tab-sorted",{tab:f,fromIndex:l.dragIndex,toIndex:t})}we()}}function we(){l.dragging=!1,l.dragIndex=-1,l.dropIndex=-1,l.dragTab=null}n.onMounted(()=>{document.addEventListener("keydown",B)}),n.onBeforeUnmount(()=>{document.removeEventListener("keydown",B),a.appContext.config.globalProperties.$tabs=null,S.forEach(t=>{t.forEach(r=>r())}),S.clear(),K.clear()}),n.watch(()=>e.keepAlive,t=>{i.options.keepAlive=t}),n.watch(()=>i.activeId.value,()=>B()),n.watch(()=>e.contextmenu,t=>{t||B()}),n.watch(()=>U.value.length,t=>{T.visible&&t===0&&B()}),n.watch(U,t=>{if(x.value=new Array(t.length).fill(null),!T.visible)return;const r=q(-1,1,t);z(r)}),n.watch(()=>T.visible,t=>{t||(I.value=-1,x.value=[])});const Tt=i.includeKeys;return{controller:i,tabs:i.tabs,includeKeys:Tt,tabTransitionProps:oe,pageTransitionProps:ae,buildTabClass:ft,activate:dt,close:ut,context:T,menuItems:U,handleMenuAction:ke,showContextMenu:nt,hideContextMenu:B,getTabTitle:re,isClosable:ie,isRefreshing:bt,hasCustomSlot:p,hasStartSlot:v,hasEndSlot:c,onDragStart:mt,onDragOver:pt,onDragEnter:gt,onDragLeave:ht,onDrop:yt,onDragEnd:we,setupComponentWatching:j,cleanupComponentWatching:O,handleComponentRef:M,getReactiveTabTitle:st,getComponentCacheKey:ve,getRefreshComponentKey:ct,triggerTabUpdate:E,menuRef:W,highlightedIndex:I,setMenuItemRef:rt,onMenuKeydown:lt,highlightMenuIndex:z}}}),Me=(e,o)=>{const a=e.__vccOpts||e;for(const[s,i]of o)a[s]=i;return a},Le={class:"router-tab"},Ve={class:"router-tab__header"},Ne={class:"router-tab__scroll"},je=["data-title","draggable","onClick","onAuxclick","onContextmenu","onDragstart","onDragover","onDragenter","onDrop"],Oe=["title"],Ue=["onClick"],ze={class:"router-tab__container"},_e=["aria-disabled","disabled","tabindex","onMouseenter","onClick"];function Ye(e,o,a,s,i,p){const v=n.resolveComponent("RouterView");return n.openBlock(),n.createElementBlock("div",Le,[n.createElementVNode("header",Ve,[n.createElementVNode("div",{class:n.normalizeClass(["router-tab__slot-start",{"has-content":e.hasStartSlot}])},[n.renderSlot(e.$slots,"start")],2),n.createElementVNode("div",Ne,[n.createVNode(n.TransitionGroup,n.mergeProps({tag:"ul",class:"router-tab__nav"},e.tabTransitionProps),{default:n.withCtx(()=>[(n.openBlock(!0),n.createElementBlock(n.Fragment,null,n.renderList(e.tabs,(c,d)=>(n.openBlock(),n.createElementBlock("li",{key:c.id,class:n.normalizeClass(e.buildTabClass(c)),"data-title":e.getTabTitle(c),draggable:e.sortable,onClick:g=>e.activate(c),onAuxclick:n.withModifiers(g=>e.close(c),["middle","prevent"]),onContextmenu:n.withModifiers(g=>e.showContextMenu(c,g),["prevent"]),onDragstart:g=>e.onDragStart(c,d,g),onDragover:g=>e.onDragOver(d,g),onDragenter:g=>e.onDragEnter(d),onDragleave:o[0]||(o[0]=(...g)=>e.onDragLeave&&e.onDragLeave(...g)),onDrop:g=>e.onDrop(d,g),onDragend:o[1]||(o[1]=(...g)=>e.onDragEnd&&e.onDragEnd(...g))},[c.icon?(n.openBlock(),n.createElementBlock("i",{key:0,class:n.normalizeClass(["router-tab__item-icon",c.icon])},null,2)):n.createCommentVNode("",!0),n.createElementVNode("span",{class:"router-tab__item-title",title:e.getReactiveTabTitle(c)},n.toDisplayString(e.getReactiveTabTitle(c)),9,Oe),e.isClosable(c)?(n.openBlock(),n.createElementBlock("a",{key:1,class:"router-tab__item-close",onClick:n.withModifiers(g=>e.close(c),["stop"])},null,8,Ue)):n.createCommentVNode("",!0)],42,je))),128))]),_:1},16)]),n.createElementVNode("div",{class:n.normalizeClass(["router-tab__slot-end",{"has-content":e.hasEndSlot}])},[n.renderSlot(e.$slots,"end")],2)]),n.createElementVNode("div",ze,[n.createVNode(v,null,{default:n.withCtx(c=>[e.hasCustomSlot?n.renderSlot(e.$slots,"default",n.normalizeProps(n.mergeProps({key:0},{...c,controller:e.controller,pageRef:d=>e.handleComponentRef(d,e.controller.getRouteKey(c.route))}))):(n.openBlock(),n.createElementBlock(n.Fragment,{key:1},[n.createVNode(n.Transition,n.mergeProps(e.pageTransitionProps,{appear:""}),{default:n.withCtx(()=>[e.controller.options.keepAlive?(n.openBlock(),n.createBlock(n.KeepAlive,{key:0,include:e.includeKeys,max:e.controller.options.maxAlive||void 0},[e.isRefreshing(c.route)?n.createCommentVNode("",!0):(n.openBlock(),n.createBlock(n.resolveDynamicComponent(c.Component),{key:e.getComponentCacheKey(c.route),ref:d=>e.handleComponentRef(d,e.controller.getRouteKey(c.route)),class:"router-tab-page"}))],1032,["include","max"])):n.createCommentVNode("",!0)]),_:2},1040),n.createVNode(n.Transition,n.mergeProps(e.pageTransitionProps,{appear:""}),{default:n.withCtx(()=>[!e.controller.options.keepAlive||e.isRefreshing(c.route)?(n.openBlock(),n.createBlock(n.resolveDynamicComponent(c.Component),{key:e.getRefreshComponentKey(c.route),ref:d=>e.handleComponentRef(d,e.controller.getRouteKey(c.route)),class:"router-tab-page"})):n.createCommentVNode("",!0)]),_:2},1040)],64))]),_:3})]),e.context.visible&&e.context.target?(n.openBlock(),n.createElementBlock("div",{key:0,ref:"menuRef",class:"router-tab__contextmenu",role:"menu",onKeydown:o[2]||(o[2]=(...c)=>e.onMenuKeydown&&e.onMenuKeydown(...c)),style:n.normalizeStyle({left:e.context.position.x+"px",top:e.context.position.y+"px"})},[(n.openBlock(!0),n.createElementBlock(n.Fragment,null,n.renderList(e.menuItems,(c,d)=>(n.openBlock(),n.createElementBlock("a",{key:c.id,role:"menuitem",class:n.normalizeClass(["router-tab__contextmenu-item",{"is-focused":d===e.highlightedIndex}]),"aria-disabled":c.disabled,disabled:c.disabled,tabindex:c.disabled?-1:d===e.highlightedIndex?0:-1,ref_for:!0,ref:g=>e.setMenuItemRef(g,d),onMouseenter:g=>!c.disabled&&e.highlightMenuIndex(d),onClick:g=>e.handleMenuAction(c)},n.toDisplayString(c.label),43,_e))),128))],36)):n.createCommentVNode("",!0)])}const te=Me($e,[["render",Ye]]),Fe={class:"router-tabs","aria-hidden":"true"},F=n.defineComponent({name:"RouterTabs",__name:"RouterTabs",props:{cookieKey:{},expiresInDays:{},path:{},domain:{},secure:{type:Boolean},sameSite:{},serialize:{type:Function},deserialize:{type:Function},fallbackRoute:{}},setup(e){return ee(e),(a,s)=>(n.openBlock(),n.createElementBlock("span",Fe))}}),be="tab-theme-style",me="tab-theme-primary-color",He="system",pe="(prefers-color-scheme: dark)";let N=null;const R={primary:"#034960",background:"#ffffff",text:"#1e293b",border:"#e2e8f0",activeBackground:"#034960",activeText:"#ffffff",activeBorder:"#034960",headerBackground:"#ffffff",buttonBackground:"#f8fafc",buttonColor:"#034960",activeButtonBackground:"#034960",activeButtonColor:"#ffffff",iconColor:"#475569"},Je={primary:"#38bdf8",background:"#0f172a",text:"#f1f5f9",border:"#334155",activeBackground:"#1e293b",activeText:"#38bdf8",activeBorder:"#38bdf8",headerBackground:"#0c4a6e",buttonBackground:"#1e293b",buttonColor:"#f1f5f9",activeButtonBackground:"#38bdf8",activeButtonColor:"#0f172a",iconColor:"#cbd5e1"};function We(e){if(typeof window>"u")return null;const o=window.localStorage.getItem(e);if(!o)return null;try{const a=JSON.parse(o);return a&&typeof a=="object"?a:null}catch{return null}}function ne(e){typeof document>"u"||(document.documentElement.style.setProperty("--router-tab-primary",e.primary??R.primary),document.documentElement.style.setProperty("--router-tab-header-bg",e.headerBackground??R.headerBackground),document.documentElement.style.setProperty("--router-tab-background",e.background??R.background),document.documentElement.style.setProperty("--router-tab-active-background",e.activeBackground??R.activeBackground),document.documentElement.style.setProperty("--router-tab-text",e.text??R.text),document.documentElement.style.setProperty("--router-tab-active-text",e.activeText??R.activeText),document.documentElement.style.setProperty("--router-tab-border",e.border??R.border),document.documentElement.style.setProperty("--router-tab-active-border",e.activeBorder??R.activeBorder),document.documentElement.style.setProperty("--router-tab-button-color",e.buttonColor??R.buttonColor),document.documentElement.style.setProperty("--router-tab-active-button-color",e.activeButtonColor??R.activeButtonColor),document.documentElement.style.setProperty("--router-tab-button-background",e.buttonBackground??R.buttonBackground),document.documentElement.style.setProperty("--router-tab-active-button-background",e.activeButtonBackground??R.activeButtonBackground),document.documentElement.style.setProperty("--router-tab-icon-color",e.iconColor??R.iconColor))}function ge(e){if(typeof document>"u")return;const o=document.documentElement,a=window.matchMedia(pe),s=()=>{o.dataset.theme=a.matches?"dark":"light"};N&&(a.removeEventListener("change",N),N=null),e==="system"?(s(),N=()=>s(),a.addEventListener("change",N)):o.dataset.theme=e}function he(e={}){if(typeof window>"u")return;const{styleKey:o=be,primaryKey:a=me,defaultStyle:s=He,defaultPrimary:i}=e,p=window.localStorage.getItem(o)??s;ge(p);const c=p==="dark"||p==="system"&&window.matchMedia(pe).matches?{...Je}:{...R};i&&(c.primary=i);const d=We(a);ne(d?{...c,...d}:c)}function qe(e,o){if(typeof window>"u")return;const a=o?.styleKey??be;window.localStorage.setItem(a,e),ge(e)}function Ge(e,o){if(typeof window>"u")return;const a=o?.primaryKey??me;window.localStorage.setItem(a,JSON.stringify(e)),ne(e)}function H(e,o){if(n.isRef(e)){const s=!n.isReadonly(e);return{value:e,update:s?i=>{e.value=i}:()=>{}}}if(typeof e=="function"){const s=e;return{value:n.computed(s),update:()=>{}}}const a=n.ref(e===void 0?o:e);return{value:a,update:s=>{a.value=s}}}function J(e={}){const o=H(e.title,"Untitled"),a=H(e.icon,""),s=H(e.closable,!0),i=H(e.meta,{});return{routeTabTitle:o.value,routeTabIcon:a.value,routeTabClosable:s.value,routeTabMeta:i.value,updateTitle:o.update,updateIcon:a.update,updateClosable:s.update,updateMeta:i.update}}function Xe(e,o="Page"){return J({title:n.computed(()=>e.value?"Loading...":o),icon:n.computed(()=>e.value?"mdi-loading mdi-spin":"mdi-page"),closable:n.computed(()=>!e.value)})}function Qe(e,o="Page",a="mdi-page"){return J({title:n.computed(()=>e.value>0?`${o} (${e.value})`:o),icon:n.computed(()=>e.value>0?"mdi-bell-badge":a)})}function Ze(e,o="Page"){const a={normal:{suffix:"",icon:"mdi-page"},loading:{suffix:" - Loading",icon:"mdi-loading mdi-spin"},error:{suffix:" - Error",icon:"mdi-alert"},success:{suffix:" - Success",icon:"mdi-check-circle"}};return J({title:n.computed(()=>o+a[e.value].suffix),icon:n.computed(()=>a[e.value].icon),closable:n.computed(()=>e.value!=="loading")})}let ye=!1;const et={install(e,o){if(ye)return;ye=!0;const{initTheme:a=!0,themeOptions:s,componentName:i=te.name||"RouterTab",tabsComponentName:p=F.name||"RouterTabs"}=o??{};a&&he(s??{}),e.component(i,te),e.component(p,F),p.toLowerCase()!=="router-tabs"&&e.component("router-tabs",F),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[V]},set(v){v&&e.provide(V,v)}})}};w.RouterTab=te,w.RouterTabs=F,w.default=et,w.initRouterTabsTheme=he,w.routerTabsKey=V,w.setRouterTabsPrimary=Ge,w.setRouterTabsTheme=qe,w.useLoadingTab=Xe,w.useNotificationTab=Qe,w.useReactiveTab=J,w.useRouterTabs=Z,w.useRouterTabsPersistence=ee,w.useStatusTab=Ze,Object.defineProperties(w,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="router-tab">
|
|
3
3
|
<header class="router-tab__header">
|
|
4
|
-
<div
|
|
4
|
+
<div
|
|
5
|
+
class="router-tab__slot-start"
|
|
6
|
+
:class="{ 'has-content': hasStartSlot }"
|
|
7
|
+
>
|
|
5
8
|
<slot name="start" />
|
|
6
9
|
</div>
|
|
7
10
|
|
|
@@ -40,7 +43,10 @@
|
|
|
40
43
|
</transition-group>
|
|
41
44
|
</div>
|
|
42
45
|
|
|
43
|
-
<div
|
|
46
|
+
<div
|
|
47
|
+
class="router-tab__slot-end"
|
|
48
|
+
:class="{ 'has-content': hasEndSlot }"
|
|
49
|
+
>
|
|
44
50
|
<slot name="end" />
|
|
45
51
|
</div>
|
|
46
52
|
</header>
|
|
@@ -70,7 +76,7 @@
|
|
|
70
76
|
<component
|
|
71
77
|
v-if="!isRefreshing(routerSlot.route)"
|
|
72
78
|
:is="routerSlot.Component"
|
|
73
|
-
:key="
|
|
79
|
+
:key="getComponentCacheKey(routerSlot.route)"
|
|
74
80
|
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
75
81
|
class="router-tab-page"
|
|
76
82
|
/>
|
|
@@ -84,7 +90,7 @@
|
|
|
84
90
|
<component
|
|
85
91
|
v-if="!controller.options.keepAlive || isRefreshing(routerSlot.route)"
|
|
86
92
|
:is="routerSlot.Component"
|
|
87
|
-
:key="
|
|
93
|
+
:key="getRefreshComponentKey(routerSlot.route)"
|
|
88
94
|
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
89
95
|
class="router-tab-page"
|
|
90
96
|
/>
|
|
@@ -95,18 +101,26 @@
|
|
|
95
101
|
|
|
96
102
|
<div
|
|
97
103
|
v-if="context.visible && context.target"
|
|
104
|
+
ref="menuRef"
|
|
98
105
|
class="router-tab__contextmenu"
|
|
106
|
+
role="menu"
|
|
107
|
+
@keydown="onMenuKeydown"
|
|
99
108
|
:style="{ left: context.position.x + 'px', top: context.position.y + 'px' }"
|
|
100
109
|
>
|
|
101
110
|
<a
|
|
102
|
-
v-for="item in menuItems"
|
|
111
|
+
v-for="(item, index) in menuItems"
|
|
103
112
|
:key="item.id"
|
|
104
|
-
|
|
113
|
+
role="menuitem"
|
|
114
|
+
:class="['router-tab__contextmenu-item', { 'is-focused': index === highlightedIndex }]"
|
|
105
115
|
:aria-disabled="item.disabled"
|
|
106
|
-
|
|
116
|
+
:disabled="item.disabled"
|
|
117
|
+
:tabindex="item.disabled ? -1 : index === highlightedIndex ? 0 : -1"
|
|
118
|
+
:ref="el => setMenuItemRef(el, index)"
|
|
119
|
+
@mouseenter="!item.disabled && highlightMenuIndex(index)"
|
|
120
|
+
@click="handleMenuAction(item)"
|
|
107
121
|
>
|
|
108
122
|
{{ item.label }}
|
|
109
|
-
|
|
123
|
+
</a>
|
|
110
124
|
</div>
|
|
111
125
|
</div>
|
|
112
126
|
</template>
|
|
@@ -116,6 +130,7 @@ import {
|
|
|
116
130
|
computed,
|
|
117
131
|
defineComponent,
|
|
118
132
|
getCurrentInstance,
|
|
133
|
+
nextTick,
|
|
119
134
|
onBeforeUnmount,
|
|
120
135
|
onMounted,
|
|
121
136
|
provide,
|
|
@@ -226,6 +241,8 @@ export default defineComponent({
|
|
|
226
241
|
instance.appContext.config.globalProperties.$tabs = controller
|
|
227
242
|
|
|
228
243
|
const hasCustomSlot = computed(() => Boolean(instance?.slots?.default))
|
|
244
|
+
const hasStartSlot = computed(() => Boolean(instance?.slots?.start))
|
|
245
|
+
const hasEndSlot = computed(() => Boolean(instance?.slots?.end))
|
|
229
246
|
|
|
230
247
|
// Force reactivity by creating a trigger ref
|
|
231
248
|
const tabUpdateTrigger = ref(0)
|
|
@@ -390,6 +407,10 @@ export default defineComponent({
|
|
|
390
407
|
position: { x: 0, y: 0 }
|
|
391
408
|
})
|
|
392
409
|
|
|
410
|
+
const menuRef = ref<HTMLElement | null>(null)
|
|
411
|
+
const menuItemRefs = ref<(HTMLElement | null)[]>([])
|
|
412
|
+
const highlightedIndex = ref(-1)
|
|
413
|
+
|
|
393
414
|
// Drag and drop state
|
|
394
415
|
const dragState = reactive({
|
|
395
416
|
dragging: false,
|
|
@@ -500,6 +521,8 @@ export default defineComponent({
|
|
|
500
521
|
function hideContextMenu() {
|
|
501
522
|
context.visible = false
|
|
502
523
|
context.target = null
|
|
524
|
+
highlightedIndex.value = -1
|
|
525
|
+
menuItemRefs.value = []
|
|
503
526
|
}
|
|
504
527
|
|
|
505
528
|
function showContextMenu(tab: TabRecord, event: MouseEvent) {
|
|
@@ -510,6 +533,9 @@ export default defineComponent({
|
|
|
510
533
|
context.position.y = event.clientY
|
|
511
534
|
|
|
512
535
|
document.addEventListener('click', hideContextMenu, { once: true })
|
|
536
|
+
nextTick(() => {
|
|
537
|
+
adjustMenuPosition()
|
|
538
|
+
})
|
|
513
539
|
}
|
|
514
540
|
|
|
515
541
|
function normalizeMenuItem(raw: MenuConfig, ctx: MenuActionContext): ResolvedMenuItem | null {
|
|
@@ -549,6 +575,128 @@ export default defineComponent({
|
|
|
549
575
|
.filter((item): item is ResolvedMenuItem => !!item)
|
|
550
576
|
})
|
|
551
577
|
|
|
578
|
+
function adjustMenuPosition() {
|
|
579
|
+
const menuEl = menuRef.value
|
|
580
|
+
if (!menuEl) return
|
|
581
|
+
const margin = 8
|
|
582
|
+
const { innerWidth, innerHeight } = window
|
|
583
|
+
const rect = menuEl.getBoundingClientRect()
|
|
584
|
+
|
|
585
|
+
let nextX = context.position.x
|
|
586
|
+
let nextY = context.position.y
|
|
587
|
+
|
|
588
|
+
if (rect.right > innerWidth - margin) {
|
|
589
|
+
nextX = Math.max(margin, innerWidth - rect.width - margin)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (rect.bottom > innerHeight - margin) {
|
|
593
|
+
nextY = Math.max(margin, innerHeight - rect.height - margin)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (nextX !== context.position.x || nextY !== context.position.y) {
|
|
597
|
+
context.position.x = nextX
|
|
598
|
+
context.position.y = nextY
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function setMenuItemRef(el: Element | null, index: number) {
|
|
603
|
+
menuItemRefs.value[index] = (el as HTMLElement) ?? null
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function focusMenuItem(index: number) {
|
|
607
|
+
if (index < 0) return
|
|
608
|
+
const el = menuItemRefs.value[index]
|
|
609
|
+
el?.focus({ preventScroll: true })
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function findNextEnabledIndex(start: number, step: 1 | -1, items = menuItems.value): number {
|
|
613
|
+
if (!items.length) return -1
|
|
614
|
+
const total = items.length
|
|
615
|
+
let idx = start
|
|
616
|
+
|
|
617
|
+
for (let i = 0; i < total; i++) {
|
|
618
|
+
idx = (idx + step + total) % total
|
|
619
|
+
if (!items[idx].disabled) return idx
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return -1
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function highlightMenuIndex(index: number) {
|
|
626
|
+
highlightedIndex.value = index
|
|
627
|
+
if (index < 0) return
|
|
628
|
+
nextTick(() => focusMenuItem(index))
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function moveHighlight(step: 1 | -1) {
|
|
632
|
+
const nextIndex = findNextEnabledIndex(highlightedIndex.value, step)
|
|
633
|
+
if (nextIndex !== -1) {
|
|
634
|
+
highlightMenuIndex(nextIndex)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function onMenuKeydown(event: KeyboardEvent) {
|
|
639
|
+
if (!context.visible) return
|
|
640
|
+
|
|
641
|
+
const key = event.key
|
|
642
|
+
const items = menuItems.value
|
|
643
|
+
if (!items.length) return
|
|
644
|
+
|
|
645
|
+
if (key === 'Tab') {
|
|
646
|
+
hideContextMenu()
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const handledKeys = [
|
|
651
|
+
'ArrowDown',
|
|
652
|
+
'ArrowUp',
|
|
653
|
+
'ArrowRight',
|
|
654
|
+
'ArrowLeft',
|
|
655
|
+
'Home',
|
|
656
|
+
'End',
|
|
657
|
+
'Enter',
|
|
658
|
+
' ',
|
|
659
|
+
'Spacebar',
|
|
660
|
+
'Escape'
|
|
661
|
+
]
|
|
662
|
+
|
|
663
|
+
if (!handledKeys.includes(key)) return
|
|
664
|
+
|
|
665
|
+
event.preventDefault()
|
|
666
|
+
|
|
667
|
+
switch (key) {
|
|
668
|
+
case 'ArrowDown':
|
|
669
|
+
case 'ArrowRight':
|
|
670
|
+
moveHighlight(1)
|
|
671
|
+
break
|
|
672
|
+
case 'ArrowUp':
|
|
673
|
+
case 'ArrowLeft':
|
|
674
|
+
moveHighlight(-1)
|
|
675
|
+
break
|
|
676
|
+
case 'Home':
|
|
677
|
+
highlightMenuIndex(findNextEnabledIndex(-1, 1))
|
|
678
|
+
break
|
|
679
|
+
case 'End':
|
|
680
|
+
highlightMenuIndex(findNextEnabledIndex(items.length, -1))
|
|
681
|
+
break
|
|
682
|
+
case 'Enter':
|
|
683
|
+
case ' ':
|
|
684
|
+
case 'Spacebar': {
|
|
685
|
+
const index = highlightedIndex.value
|
|
686
|
+
if (index > -1) {
|
|
687
|
+
const item = items[index]
|
|
688
|
+
if (!item.disabled) {
|
|
689
|
+
handleMenuAction(item)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
break
|
|
693
|
+
}
|
|
694
|
+
case 'Escape':
|
|
695
|
+
hideContextMenu()
|
|
696
|
+
break
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
552
700
|
async function handleMenuAction(item: ResolvedMenuItem) {
|
|
553
701
|
if (item.disabled) return
|
|
554
702
|
hideContextMenu()
|
|
@@ -568,6 +716,17 @@ export default defineComponent({
|
|
|
568
716
|
return reactiveTitles[tab.id] || getTabTitle(tab)
|
|
569
717
|
}
|
|
570
718
|
|
|
719
|
+
function getComponentCacheKey(route: RouteLocationNormalizedLoaded): string {
|
|
720
|
+
const routeKey = controller.getRouteKey(route)
|
|
721
|
+
const tab = controller.tabs.find(item => item.id === routeKey)
|
|
722
|
+
const renderKey = tab?.renderKey ?? 0
|
|
723
|
+
return `${routeKey}::${renderKey}`
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function getRefreshComponentKey(route: RouteLocationNormalizedLoaded): string {
|
|
727
|
+
return `${getComponentCacheKey(route)}::refresh`
|
|
728
|
+
}
|
|
729
|
+
|
|
571
730
|
function isClosable(tab: TabRecord) {
|
|
572
731
|
if (tab.closable === false) return false
|
|
573
732
|
if (controller.options.keepLastTab && controller.tabs.length <= 1) return false
|
|
@@ -579,6 +738,15 @@ export default defineComponent({
|
|
|
579
738
|
}
|
|
580
739
|
|
|
581
740
|
function activate(tab: TabRecord) {
|
|
741
|
+
if (tab.href && typeof window !== 'undefined') {
|
|
742
|
+
if (tab.target && tab.target !== '_self') {
|
|
743
|
+
window.open(tab.href as string, tab.target)
|
|
744
|
+
} else {
|
|
745
|
+
window.location.assign(tab.href as string)
|
|
746
|
+
}
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
|
|
582
750
|
if (controller.activeId.value === tab.id) return
|
|
583
751
|
controller.openTab(tab.to, false)
|
|
584
752
|
}
|
|
@@ -704,6 +872,23 @@ export default defineComponent({
|
|
|
704
872
|
}
|
|
705
873
|
)
|
|
706
874
|
|
|
875
|
+
watch(menuItems, items => {
|
|
876
|
+
menuItemRefs.value = new Array(items.length).fill(null)
|
|
877
|
+
if (!context.visible) return
|
|
878
|
+
const first = findNextEnabledIndex(-1, 1, items)
|
|
879
|
+
highlightMenuIndex(first)
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
watch(
|
|
883
|
+
() => context.visible,
|
|
884
|
+
visible => {
|
|
885
|
+
if (!visible) {
|
|
886
|
+
highlightedIndex.value = -1
|
|
887
|
+
menuItemRefs.value = []
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
)
|
|
891
|
+
|
|
707
892
|
const includeKeys = controller.includeKeys
|
|
708
893
|
|
|
709
894
|
return {
|
|
@@ -724,6 +909,8 @@ export default defineComponent({
|
|
|
724
909
|
isClosable,
|
|
725
910
|
isRefreshing,
|
|
726
911
|
hasCustomSlot,
|
|
912
|
+
hasStartSlot,
|
|
913
|
+
hasEndSlot,
|
|
727
914
|
onDragStart,
|
|
728
915
|
onDragOver,
|
|
729
916
|
onDragEnter,
|
|
@@ -734,8 +921,15 @@ export default defineComponent({
|
|
|
734
921
|
cleanupComponentWatching,
|
|
735
922
|
handleComponentRef,
|
|
736
923
|
getReactiveTabTitle,
|
|
737
|
-
|
|
924
|
+
getComponentCacheKey,
|
|
925
|
+
getRefreshComponentKey,
|
|
926
|
+
triggerTabUpdate,
|
|
927
|
+
menuRef,
|
|
928
|
+
highlightedIndex,
|
|
929
|
+
setMenuItemRef,
|
|
930
|
+
onMenuKeydown,
|
|
931
|
+
highlightMenuIndex
|
|
738
932
|
}
|
|
739
933
|
}
|
|
740
934
|
})
|
|
741
|
-
</script>
|
|
935
|
+
</script>
|
|
@@ -96,6 +96,7 @@ function createTabFromRoute(
|
|
|
96
96
|
alive: resolveAlive(route, keepAliveDefault),
|
|
97
97
|
reusable: resolveReusable(route, false),
|
|
98
98
|
closable: meta.closable ?? true,
|
|
99
|
+
renderKey: typeof base.renderKey === 'number' ? base.renderKey : 0,
|
|
99
100
|
...meta,
|
|
100
101
|
...base
|
|
101
102
|
}
|
|
@@ -105,6 +106,14 @@ function insertTab(tabs: TabRecord[], tab: TabRecord, position: 'last' | 'next',
|
|
|
105
106
|
const exists = tabs.find(item => item.id === tab.id)
|
|
106
107
|
if (exists) return
|
|
107
108
|
|
|
109
|
+
if (position === 'next' && referenceId) {
|
|
110
|
+
const referenceIndex = tabs.findIndex(item => item.id === referenceId)
|
|
111
|
+
if (referenceIndex !== -1) {
|
|
112
|
+
tabs.splice(referenceIndex + 1, 0, tab)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
tabs.push(tab)
|
|
109
118
|
}
|
|
110
119
|
|
|
@@ -126,7 +135,8 @@ function toSnapshotTab(tab: TabRecord): RouterTabsSnapshotTab {
|
|
|
126
135
|
tips: tab.tips,
|
|
127
136
|
icon: tab.icon,
|
|
128
137
|
tabClass: tab.tabClass,
|
|
129
|
-
closable: tab.closable
|
|
138
|
+
closable: tab.closable,
|
|
139
|
+
renderKey: tab.renderKey
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
|
|
@@ -137,6 +147,9 @@ function fromSnapshotTab(snapshot: RouterTabsSnapshotTab): Partial<TabRecord> {
|
|
|
137
147
|
if ('icon' in snapshot) base.icon = snapshot.icon
|
|
138
148
|
if ('tabClass' in snapshot) base.tabClass = snapshot.tabClass
|
|
139
149
|
if ('closable' in snapshot) base.closable = snapshot.closable
|
|
150
|
+
if ('renderKey' in snapshot && typeof snapshot.renderKey === 'number') {
|
|
151
|
+
base.renderKey = snapshot.renderKey
|
|
152
|
+
}
|
|
140
153
|
return base
|
|
141
154
|
}
|
|
142
155
|
|
|
@@ -150,7 +163,11 @@ export function createRouterTabs(
|
|
|
150
163
|
const activeId = ref<string | null>(null)
|
|
151
164
|
const current = shallowRef<TabRecord>()
|
|
152
165
|
const refreshingKey = ref<string | null>(null)
|
|
153
|
-
const includeKeys = computed(() =>
|
|
166
|
+
const includeKeys = computed(() =>
|
|
167
|
+
tabs
|
|
168
|
+
.filter(tab => tab.alive)
|
|
169
|
+
.map(tab => `${tab.id}::${tab.renderKey}`)
|
|
170
|
+
)
|
|
154
171
|
|
|
155
172
|
let isHydrating = false
|
|
156
173
|
|
|
@@ -259,6 +276,23 @@ export function createRouterTabs(
|
|
|
259
276
|
|
|
260
277
|
async function refreshTab(id: string | undefined = activeId.value ?? undefined, force = false) {
|
|
261
278
|
if (!id) return
|
|
279
|
+
const tab = tabs.find(item => item.id === id)
|
|
280
|
+
if (!tab) return
|
|
281
|
+
|
|
282
|
+
const wasAlive = options.keepAlive && tab.alive
|
|
283
|
+
|
|
284
|
+
if (wasAlive) {
|
|
285
|
+
tab.alive = false
|
|
286
|
+
await nextTick()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
tab.renderKey = (tab.renderKey ?? 0) + 1
|
|
290
|
+
|
|
291
|
+
if (wasAlive) {
|
|
292
|
+
tab.alive = true
|
|
293
|
+
await nextTick()
|
|
294
|
+
}
|
|
295
|
+
|
|
262
296
|
refreshingKey.value = id
|
|
263
297
|
await nextTick()
|
|
264
298
|
if (!force) await nextTick()
|
package/lib/core/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface TabMeta {
|
|
|
23
23
|
export interface TabInput extends Partial<TabMeta> {
|
|
24
24
|
to: RouteLocationRaw
|
|
25
25
|
id?: string
|
|
26
|
+
renderKey?: number
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface TabRecord extends TabMeta {
|
|
@@ -32,6 +33,7 @@ export interface TabRecord extends TabMeta {
|
|
|
32
33
|
matched: RouteLocationNormalizedLoaded
|
|
33
34
|
alive: boolean
|
|
34
35
|
reusable: boolean
|
|
36
|
+
renderKey: number
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export type RouterTabsMenuPreset =
|
|
@@ -64,6 +66,7 @@ export interface RouterTabsSnapshotTab {
|
|
|
64
66
|
icon?: TabRecord['icon']
|
|
65
67
|
tabClass?: TabRecord['tabClass']
|
|
66
68
|
closable?: boolean
|
|
69
|
+
renderKey?: number
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export interface RouterTabsSnapshot {
|
package/lib/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './theme'
|
|
12
12
|
|
|
13
13
|
import type { RouterTabsContext } from './core/types'
|
|
14
|
+
import type { RouterTabsThemeOptions } from './theme'
|
|
14
15
|
|
|
15
16
|
export type {
|
|
16
17
|
TabRecord,
|
|
@@ -22,6 +23,28 @@ export type {
|
|
|
22
23
|
|
|
23
24
|
export type { RouterTabsThemeOptions } from './theme'
|
|
24
25
|
|
|
26
|
+
export interface RouterTabsPluginOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Whether to initialise the theme system automatically during install.
|
|
29
|
+
* Defaults to `true`.
|
|
30
|
+
*/
|
|
31
|
+
initTheme?: boolean
|
|
32
|
+
/**
|
|
33
|
+
* Theme options passed to `initRouterTabsTheme` when `initTheme` is enabled.
|
|
34
|
+
*/
|
|
35
|
+
themeOptions?: RouterTabsThemeOptions
|
|
36
|
+
/**
|
|
37
|
+
* Global component name used when registering `RouterTab`.
|
|
38
|
+
* Defaults to the component's `name` option or `"RouterTab"`.
|
|
39
|
+
*/
|
|
40
|
+
componentName?: string
|
|
41
|
+
/**
|
|
42
|
+
* Global component name used when registering `RouterTabs`.
|
|
43
|
+
* Defaults to the component's `name` option or `"RouterTabs"`.
|
|
44
|
+
*/
|
|
45
|
+
tabsComponentName?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
export {
|
|
26
49
|
routerTabsKey,
|
|
27
50
|
useRouterTabs,
|
|
@@ -45,22 +68,30 @@ export type {
|
|
|
45
68
|
ReactiveTabReturn
|
|
46
69
|
} from './useReactiveTab'
|
|
47
70
|
|
|
48
|
-
import
|
|
71
|
+
import './scss/index.scss'
|
|
72
|
+
|
|
73
|
+
let installed = false
|
|
49
74
|
|
|
50
75
|
const plugin: Plugin = {
|
|
51
|
-
install(app: App) {
|
|
52
|
-
if (
|
|
53
|
-
|
|
76
|
+
install(app: App, options?: RouterTabsPluginOptions) {
|
|
77
|
+
if (installed) return
|
|
78
|
+
installed = true
|
|
54
79
|
|
|
55
|
-
|
|
80
|
+
const {
|
|
81
|
+
initTheme = true,
|
|
82
|
+
themeOptions,
|
|
83
|
+
componentName = RouterTab.name || 'RouterTab',
|
|
84
|
+
tabsComponentName = RouterTabsComponent.name || 'RouterTabs'
|
|
85
|
+
} = options ?? {}
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
if (initTheme) {
|
|
88
|
+
initRouterTabsTheme(themeOptions ?? {})
|
|
89
|
+
}
|
|
59
90
|
|
|
60
91
|
app.component(componentName, RouterTab)
|
|
61
|
-
app.component(
|
|
92
|
+
app.component(tabsComponentName, RouterTabsComponent)
|
|
62
93
|
|
|
63
|
-
if (
|
|
94
|
+
if (tabsComponentName.toLowerCase() !== 'router-tabs') {
|
|
64
95
|
app.component('router-tabs', RouterTabsComponent)
|
|
65
96
|
}
|
|
66
97
|
|
package/lib/persistence.ts
CHANGED
|
@@ -119,7 +119,7 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
119
119
|
} finally {
|
|
120
120
|
hydrating.value = false
|
|
121
121
|
}
|
|
122
|
-
} else {
|
|
122
|
+
} else if (Object.prototype.hasOwnProperty.call(options, 'fallbackRoute')) {
|
|
123
123
|
try {
|
|
124
124
|
hydrating.value = true
|
|
125
125
|
const fallback = options.fallbackRoute ?? ctrl.options.defaultRoute
|
|
@@ -127,6 +127,8 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
127
127
|
} finally {
|
|
128
128
|
hydrating.value = false
|
|
129
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
hydrating.value = false
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
const snapshot = ctrl.snapshot()
|
|
@@ -147,7 +149,8 @@ export function useRouterTabsPersistence(options: RouterTabsPersistenceOptions =
|
|
|
147
149
|
tips: tab.tips,
|
|
148
150
|
icon: tab.icon,
|
|
149
151
|
tabClass: tab.tabClass,
|
|
150
|
-
closable: tab.closable
|
|
152
|
+
closable: tab.closable,
|
|
153
|
+
renderKey: tab.renderKey
|
|
151
154
|
})),
|
|
152
155
|
active: ctrl.activeId.value
|
|
153
156
|
}),
|