vue3-router-tab 1.2.5 → 1.2.7
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 +225 -2
- package/dist/vue3-router-tab.css +1 -1
- package/dist/vue3-router-tab.js +767 -463
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +367 -22
- package/lib/core/createRouterTabs.ts +31 -9
- package/lib/index.ts +52 -9
- package/lib/persistence.ts +3 -1
- package/lib/scss/index.scss +73 -9
- package/lib/scss/variables.scss +14 -2
- package/lib/theme.ts +41 -5
- package/lib/useReactiveTab.ts +130 -0
- package/package.json +8 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(y,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue"),require("vue-router")):typeof define=="function"&&define.amd?define(["exports","vue","vue-router"],t):(y=typeof globalThis<"u"?globalThis:y||self,t(y["vue3-router-tab"]={},y.Vue,y.VueRouter))})(this,(function(y,t,ce){"use strict";function ue(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 P(e,a){const n=e.resolve(a);if(!n||!n.matched.length)throw new Error(`[RouterTabs] Unable to resolve route: ${String(a)}`);return n}const de={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(e){const a=e.meta?.key;if(typeof a=="function"){const n=a(e);if(typeof n=="string"&&n.length)return n}else if(typeof a=="string"&&a.length){const n=de[a.toLowerCase()];return n?n(e):a}return e.fullPath}function N(e,a){const n=e.meta?.keepAlive;return typeof n=="boolean"?n:a}function j(e,a){const n=e.meta?.reuse;return typeof n=="boolean"?n:a}function Z(e){const a=e.meta??{},n={};return"title"in a&&(n.title=a.title),"tips"in a&&(n.tips=a.tips),"icon"in a&&(n.icon=a.icon),"closable"in a&&(n.closable=a.closable),"tabClass"in a&&(n.tabClass=a.tabClass),"target"in a&&(n.target=a.target),"href"in a&&(n.href=a.href),n}function S(e,a,n){const i=Z(e);return{id:E(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:N(e,n),reusable:j(e,!1),closable:i.closable??!0,...i,...a}}function O(e,a,n,i){if(!e.find(m=>m.id===a.id)){if(n==="next"&&i){const m=e.findIndex(g=>g.id===i);if(m>-1){e.splice(m+1,0,a);return}}e.push(a)}}function ee(e,a,n){if(!a||a<=0)return;const i=e.filter(r=>r.alive);for(;i.length>a;){const r=i.shift();if(!r||r.id===n)continue;const m=e.findIndex(g=>g.id===r.id);m>-1&&(e[m].alive=!1)}}function fe(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function be(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 me(e,a={}){const n=ue(a),i=t.reactive([]),r=t.ref(null),m=t.shallowRef(),g=t.ref(null),s=t.computed(()=>i.filter(l=>l.alive).map(l=>l.id));let c=!1;function u(l){const d=typeof l.matched=="object"?l:P(e,l);return{key:E(d),fullPath:d.fullPath,alive:N(d,n.keepAlive),reusable:j(d,!1),matched:d}}function C(l){const d=E(l);let b=i.find(h=>h.id===d);return b?(b.fullPath=l.fullPath,b.to=l.fullPath,b.matched=l,b.alive=N(l,n.keepAlive),b.reusable=j(l,b.reusable),Object.assign(b,Z(l)),b):(b=S(l,{},n.keepAlive),O(i,b,n.appendPosition,r.value),ee(i,n.maxAlive,r.value),b)}async function R(l,d=!1,b=!0){const h=P(e,l),B=E(h),A=r.value===B;b==="sameTab"&&(b=A),b&&await w(B,!0),await e[d?"replace":"push"](h),A&&await V()}function $(l){const d=i.findIndex(h=>h.id===l),b=i[d]||i[d-1]||i[0];return b?b.to:n.defaultRoute}async function L(l=r.value,d={}){if(l){if(!d.force&&n.keepLastTab&&i.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");if(await D(l,{force:d.force}),d.redirect!==null)if(r.value===l){const b=d.redirect??$(l);b&&await e.replace(b)}else d.redirect&&await e.replace(d.redirect)}}async function D(l,d={}){const b=i.findIndex(h=>h.id===l);b!==-1&&(i.splice(b,1),g.value===l&&(g.value=null),r.value===l&&(r.value=null,m.value=void 0))}async function w(l=r.value??void 0,d=!1){l&&(g.value=l,await t.nextTick(),d||await t.nextTick(),g.value=null)}async function q(l=!1){for(const d of i)await w(d.id,l)}async function T(l=n.defaultRoute){i.splice(0,i.length),r.value=null,m.value=void 0;for(const d of n.initialTabs){const b=P(e,d.to),h=S(b,d,n.keepAlive);i.push(h)}await e.replace(l)}async function V(){const l=r.value;l&&await w(l,!0)}function G(l){return typeof l.matched=="object"?E(l):E(P(e,l))}function M(){const l=i.find(d=>d.id===r.value);return{tabs:i.map(fe),active:l?l.to:null}}async function H(l){c=!0,i.splice(0,i.length),r.value=null,m.value=void 0;const d=l?.tabs??[];for(const h of d)try{const B=P(e,h.to),A=be(h),Q=S(B,A,n.keepAlive);O(i,Q,"last",null)}catch{}c=!1;const b=l?.active??d[d.length-1]?.to??n.defaultRoute;if(b)try{await e.replace(b)}catch{}}return t.watch(()=>e.currentRoute.value,l=>{if(c)return;const d=C(l);r.value=d.id,m.value=d,ee(i,n.maxAlive,r.value)},{immediate:!0}),n.initialTabs.length&&n.initialTabs.forEach(l=>{const d=P(e,l.to),b=S(d,l,n.keepAlive);O(i,b,"last",null)}),{options:n,tabs:i,activeId:r,current:m,includeKeys:s,refreshingKey:g,openTab:R,closeTab:L,removeTab:D,refreshTab:w,refreshAll:q,reset:T,reload:V,getRouteKey:G,matchRoute:u,snapshot:M,hydrate:H}}function te(e){return e?typeof e=="string"?{name:e}:e:{}}const I=Symbol("RouterTabsContext"),K="router-tabs:snapshot";function z(e={}){const{optional:a=!1}=e,n=t.inject(I,null);if(n)return n;const i=t.inject("$tabs",null);if(i)return i;const m=t.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(m)return m;if(!a)throw new Error("[RouterTabs] useRouterTabs must be used within <router-tab>.");return null}const pe=864e5;function ge(e){if(typeof document>"u")return null;const a=`${encodeURIComponent(e)}=`,n=document.cookie?document.cookie.split("; "):[];for(const i of n)if(i.startsWith(a))return decodeURIComponent(i.slice(a.length));return null}function ne(e,a,n){if(typeof document>"u")return;const{expiresInDays:i=7,path:r="/",domain:m,secure:g,sameSite:s="lax"}=n,c=[`${encodeURIComponent(e)}=${encodeURIComponent(a)}`];if(i!==1/0){const u=new Date(Date.now()+i*pe).toUTCString();c.push(`Expires=${u}`)}r&&c.push(`Path=${r}`),m&&c.push(`Domain=${m}`),g&&c.push("Secure"),s&&c.push(`SameSite=${s.charAt(0).toUpperCase()}${s.slice(1)}`),document.cookie=c.join("; ")}function oe(e,a){if(typeof document>"u")return;const{path:n="/",domain:i}=a,r=[`${encodeURIComponent(e)}=`];r.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),n&&r.push(`Path=${n}`),i&&r.push(`Domain=${i}`),document.cookie=r.join("; ")}const ye=e=>JSON.stringify(e??null),he=e=>{if(!e)return null;try{return JSON.parse(e)}catch{return null}};function U(e={}){const{cookieKey:a=K,serialize:n=ye,deserialize:i=he}=e,r=z({optional:!0}),m=t.ref(!0),g=s=>{t.onMounted(async()=>{const c=i(ge(a));if(c&&c.tabs?.length)try{if(m.value=!0,await s.hydrate(c),c.active){await t.nextTick();const C=s.tabs.find(R=>R.to===c.active);C&&(s.activeId.value=C.id,s.current.value=C)}}finally{m.value=!1}else try{m.value=!0;const C=e.fallbackRoute??s.options.defaultRoute;await s.reset(C)}finally{m.value=!1}const u=s.snapshot();u.tabs.length?ne(a,n(u),e):oe(a,e),m.value=!1}),t.watch(()=>({tabs:s.tabs.map(c=>({to:c.to,title:c.title,tips:c.tips,icon:c.icon,tabClass:c.tabClass,closable:c.closable})),active:s.activeId.value}),()=>{if(m.value)return;const c=s.snapshot();c.tabs.length?ne(a,n(c),e):oe(a,e)},{deep:!0})};r?g(r):t.onMounted(()=>{const s=z({optional:!0});s&&g(s)})}const ke=t.defineComponent({name:"RouterTab",components:{RouterView:ce.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:K},persistence:{type:Object,default:null},sortable:{type:Boolean,default:!0},titleResolver:{type:Function,default:null}},emits:["tab-sort","tab-sorted"],setup(e,{emit:a}){const n=t.getCurrentInstance();if(!n)throw new Error("[RouterTab] component must be used within a Vue application context.");const i=n.appContext.app.config.globalProperties.$router;if(!i)throw new Error("[RouterTab] Vue Router is required. Make sure to call app.use(router) before RouterTab.");const r=me(i,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});t.provide(I,r),n.appContext.config.globalProperties.$tabs=r;const m=t.computed(()=>!!n?.slots?.default);if(e.cookieKey!==null||e.persistence){const o={...e.persistence??{}};e.cookieKey!==null?o.cookieKey=e.cookieKey||K:o.cookieKey||(o.cookieKey=K),U(o)}const g=t.computed(()=>te(e.tabTransition)),s=t.computed(()=>te(e.pageTransition)),c=t.reactive({visible:!1,target:null,position:{x:0,y:0}}),u=t.reactive({dragging:!1,dragIndex:-1,dropIndex:-1,dragTab:null}),C=["refresh","refreshAll","close","closeLefts","closeRights","closeOthers"];function R(o){return r.tabs.findIndex(f=>f.id===o)}function $(o){const f=R(o.id);return f>0?r.tabs.slice(0,f):[]}function L(o){const f=R(o.id);return f>-1?r.tabs.slice(f+1):[]}function D(o){return r.tabs.filter(f=>f.id!==o.id)}async function w(o,f){const p=o.filter(v=>v.closable!==!1);if(p.length){for(const v of p)r.activeId.value===v.id?await r.closeTab(v.id,{redirect:f.to,force:!0}):await r.removeTab(v.id,{force:!0});r.activeId.value!==f.id&&await r.openTab(f.to,!0,!1)}}const q={refresh:{label:"Refresh",handler:async({target:o})=>{await r.refreshTab(o.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await r.refreshAll(!0)}},close:{label:"Close",handler:async({target:o})=>{await r.closeTab(o.id)},enable:({target:o})=>d(o)},closeLefts:{label:"Close to the Left",handler:async({target:o})=>{await w($(o),o)},enable:({target:o})=>$(o).some(f=>f.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:o})=>{await w(L(o),o)},enable:({target:o})=>L(o).some(f=>f.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:o})=>{await w(D(o),o)},enable:({target:o})=>D(o).some(f=>f.closable!==!1)}};function T(){c.visible=!1,c.target=null}function V(o,f){e.contextmenu&&(c.visible=!0,c.target=o,c.position.x=f.clientX,c.position.y=f.clientY,document.addEventListener("click",T,{once:!0}))}function G(o,f){const p=typeof o=="string"?{id:o}:o,v=q[p.id],Fe=p.label??v?.label??String(p.id),W=p.visible??v?.visible??!0;if(!(typeof W=="function"?W(f):W!==!1))return null;const X=p.enable??v?.enable??!0,Ye=typeof X=="function"?X(f):X!==!1,se=p.handler??v?.handler;if(!se)return null;const Je=async()=>{await Promise.resolve(se(f))};return{id:String(p.id),label:Fe,disabled:!Ye,action:Je}}const M=t.computed(()=>{if(!c.visible||!c.target||e.contextmenu===!1)return[];const o=Array.isArray(e.contextmenu)?e.contextmenu:C,f={target:c.target,controller:r};return o.map(p=>G(p,f)).filter(p=>!!p)});async function H(o){o.disabled||(T(),await o.action())}function l(o){return e.titleResolver?e.titleResolver(o):typeof o.title=="string"?o.title:Array.isArray(o.title)&&o.title.length?String(o.title[0]):o.fullPath}function d(o){return!(o.closable===!1||r.options.keepLastTab&&r.tabs.length<=1)}async function b(o){await r.closeTab(o.id)}function h(o){r.activeId.value!==o.id&&r.openTab(o.to,!1)}function B(o){return["router-tab__item",{"is-active":r.activeId.value===o.id,"is-closable":d(o),"is-dragging":u.dragging&&u.dragTab?.id===o.id,"is-drag-over":u.dropIndex===R(o.id)},o.tabClass]}function A(o){return r.refreshingKey.value===r.getRouteKey(o)}function Q(o,f,p){e.sortable&&(u.dragging=!0,u.dragIndex=f,u.dragTab=o,p.dataTransfer&&(p.dataTransfer.effectAllowed="move",p.dataTransfer.setData("text/plain",o.id)),a("tab-sort",{tab:o,index:f}))}function Ne(o,f){!e.sortable||!u.dragging||(f.preventDefault(),f.dataTransfer&&(f.dataTransfer.dropEffect="move"))}function je(o){!e.sortable||!u.dragging||(u.dropIndex=o)}function Oe(){!e.sortable||u.dragging}function ze(o,f){if(!(!e.sortable||!u.dragging)){if(f.preventDefault(),u.dragIndex!==-1&&u.dragIndex!==o){const p=r.tabs.splice(u.dragIndex,1)[0];r.tabs.splice(o,0,p),a("tab-sorted",{tab:p,fromIndex:u.dragIndex,toIndex:o})}le()}}function le(){u.dragging=!1,u.dragIndex=-1,u.dropIndex=-1,u.dragTab=null}t.onMounted(()=>{document.addEventListener("keydown",T)}),t.onBeforeUnmount(()=>{document.removeEventListener("keydown",T),n.appContext.config.globalProperties.$tabs=null}),t.watch(()=>e.keepAlive,o=>{r.options.keepAlive=o}),t.watch(()=>r.activeId.value,()=>T()),t.watch(()=>e.contextmenu,o=>{o||T()}),t.watch(()=>M.value.length,o=>{c.visible&&o===0&&T()});const Ue=r.includeKeys;return{controller:r,tabs:r.tabs,includeKeys:Ue,tabTransitionProps:g,pageTransitionProps:s,buildTabClass:B,activate:h,close:b,context:c,menuItems:M,handleMenuAction:H,showContextMenu:V,hideContextMenu:T,getTabTitle:l,isClosable:d,isRefreshing:A,hasCustomSlot:m,onDragStart:Q,onDragOver:Ne,onDragEnter:je,onDragLeave:Oe,onDrop:ze,onDragEnd:le}}}),Te=(e,a)=>{const n=e.__vccOpts||e;for(const[i,r]of a)n[i]=r;return n},ve={class:"router-tab"},Ce={class:"router-tab__header"},we={class:"router-tab__slot-start"},Re={class:"router-tab__scroll"},Be=["data-title","draggable","onClick","onAuxclick","onContextmenu","onDragstart","onDragover","onDragenter","onDrop"],Pe=["title"],Ee=["onClick"],Ae={class:"router-tab__slot-end"},Ie={class:"router-tab__container"},xe=["aria-disabled","onClick"];function De(e,a,n,i,r,m){const g=t.resolveComponent("RouterView");return t.openBlock(),t.createElementBlock("div",ve,[t.createElementVNode("header",Ce,[t.createElementVNode("div",we,[t.renderSlot(e.$slots,"start")]),t.createElementVNode("div",Re,[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,c)=>(t.openBlock(),t.createElementBlock("li",{key:s.id,class:t.normalizeClass(e.buildTabClass(s)),"data-title":e.getTabTitle(s),draggable:e.sortable,onClick:u=>e.activate(s),onAuxclick:t.withModifiers(u=>e.close(s),["middle","prevent"]),onContextmenu:t.withModifiers(u=>e.showContextMenu(s,u),["prevent"]),onDragstart:u=>e.onDragStart(s,c,u),onDragover:u=>e.onDragOver(c,u),onDragenter:u=>e.onDragEnter(c),onDragleave:a[0]||(a[0]=(...u)=>e.onDragLeave&&e.onDragLeave(...u)),onDrop:u=>e.onDrop(c,u),onDragend:a[1]||(a[1]=(...u)=>e.onDragEnd&&e.onDragEnd(...u))},[t.createElementVNode("span",{class:"router-tab__item-title",title:e.getTabTitle(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.getTabTitle(s)),1)],8,Pe),e.isClosable(s)?(t.openBlock(),t.createElementBlock("a",{key:0,class:"router-tab__item-close",onClick:t.withModifiers(u=>e.close(s),["stop"])},null,8,Ee)):t.createCommentVNode("",!0)],42,Be))),128))]),_:1},16)]),t.createElementVNode("div",Ae,[t.renderSlot(e.$slots,"end")])]),t.createElementVNode("div",Ie,[t.createVNode(g,null,{default:t.withCtx(s=>[e.hasCustomSlot?t.renderSlot(e.$slots,"default",t.normalizeProps(t.mergeProps({key:0},{...s,controller:e.controller}))):(t.openBlock(),t.createElementBlock(t.Fragment,{key:1},[t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[e.controller.options.keepAlive?(t.openBlock(),t.createBlock(t.KeepAlive,{key:0,include:e.includeKeys,max:e.controller.options.maxAlive||void 0},[e.isRefreshing(s.route)?t.createCommentVNode("",!0):(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s.Component),{key:e.controller.getRouteKey(s.route),class:"router-tab-page"}))],1032,["include","max"])):t.createCommentVNode("",!0)]),_:2},1040),t.createVNode(t.Transition,t.mergeProps(e.pageTransitionProps,{appear:""}),{default:t.withCtx(()=>[!e.controller.options.keepAlive||e.isRefreshing(s.route)?(t.openBlock(),t.createBlock(t.resolveDynamicComponent(s.Component),{key:e.controller.getRouteKey(s.route)+(e.isRefreshing(s.route)?"-refresh":""),class:"router-tab-page"})):t.createCommentVNode("",!0)]),_:2},1040)],64))]),_:3})]),e.context.visible&&e.context.target?(t.openBlock(),t.createElementBlock("div",{key:0,class:"router-tab__contextmenu",style:t.normalizeStyle({left:e.context.position.x+"px",top:e.context.position.y+"px"})},[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(e.menuItems,s=>(t.openBlock(),t.createElementBlock("a",{key:s.id,class:"router-tab__contextmenu-item","aria-disabled":s.disabled,onClick:t.withModifiers(c=>e.handleMenuAction(s),["prevent"])},t.toDisplayString(s.label),9,xe))),128))],4)):t.createCommentVNode("",!0)])}const F=Te(ke,[["render",De]]),Se={class:"router-tabs","aria-hidden":"true"},_=t.defineComponent({name:"RouterTabs",__name:"RouterTabs",props:{cookieKey:{},expiresInDays:{},path:{},domain:{},secure:{type:Boolean},sameSite:{},serialize:{type:Function},deserialize:{type:Function},fallbackRoute:{}},setup(e){return U(e),(n,i)=>(t.openBlock(),t.createElementBlock("span",Se))}}),ae="tab-theme-style",Ke="tab-theme-primary-color",_e="system",$e="(prefers-color-scheme: dark)";let x=null;const k={primary:"#034960",background:"#ffffff",text:"#1e293b",border:"#e2e8f0",activeBackground:"#034960",activeText:"#ffffff",activeBorder:"#034960",headerBackground:"#ffff",buttonBackground:"#f8fafc",buttonColor:"#034960",activeButtonBackground:"#034960",activeButtonColor:"#ffffff",iconColor:"#475569"},Le={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 Y(e){typeof document>"u"||(console.log("applyPrimary",e),document.documentElement.style.setProperty("--router-tab-primary",e.primary??k.primary),document.documentElement.style.setProperty("--router-tab-header-bg",e.headerBackground??k.headerBackground),document.documentElement.style.setProperty("--router-tab-background",e.background??k.background),document.documentElement.style.setProperty("--router-tab-active-background",e.activeBackground??k.activeBackground),document.documentElement.style.setProperty("--router-tab-text",e.text??k.text),document.documentElement.style.setProperty("--router-tab-active-text",e.activeText??k.activeText),document.documentElement.style.setProperty("--router-tab-border",e.border??k.border),document.documentElement.style.setProperty("--router-tab-active-border",e.activeBorder??k.activeBorder),document.documentElement.style.setProperty("--router-tab-button-color",e.buttonColor??k.buttonColor),document.documentElement.style.setProperty("--router-tab-active-button-color",e.activeButtonColor??k.activeButtonColor),document.documentElement.style.setProperty("--router-tab-button-background",e.buttonBackground??k.buttonBackground),document.documentElement.style.setProperty("--router-tab-active-button-background",e.activeButtonBackground??k.activeButtonBackground))}function re(e){if(typeof document>"u")return;const a=document.documentElement,n=window.matchMedia($e),i=()=>{a.dataset.theme=n.matches?"dark":"light"};x&&(n.removeEventListener("change",x),x=null),e==="system"?(i(),x=()=>i(),n.addEventListener("change",x)):a.dataset.theme=e}function ie(e={}){if(typeof window>"u")return;const{styleKey:a=ae,defaultStyle:n=_e}=e,i=window.localStorage.getItem(a)??n;re(i),Y(i==="dark"?Le:k)}function Ve(e,a){if(typeof window>"u")return;const n=a?.styleKey??ae;window.localStorage.setItem(n,e),re(e)}function Me(e,a){if(typeof window>"u")return;const n=a?.primaryKey??Ke;window.localStorage.setItem(n,JSON.stringify(e)),Y(e)}const J={install(e){if(J._installed)return;J._installed=!0,ie();const a=F.name||"RouterTab",n=_.name||"RouterTabs";e.component(a,F),e.component(n,_),n!=="router-tabs"&&e.component("router-tabs",_),Object.defineProperty(e.config.globalProperties,"$tabs",{configurable:!0,enumerable:!1,get(){return e._context.provides[I]},set(i){i&&e.provide(I,i)}})}};y.RouterTab=F,y.RouterTabs=_,y.default=J,y.initRouterTabsTheme=ie,y.routerTabsKey=I,y.setRouterTabsPrimary=Me,y.setRouterTabsTheme=Ve,y.useRouterTabs=z,y.useRouterTabsPersistence=U,Object.defineProperties(y,{__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,Ce){"use strict";function Re(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 M(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 Ee={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 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=Ee[a.toLowerCase()];return o?o(e):a}return e.fullPath}function G(e,a){const o=e.meta?.keepAlive;return typeof o=="boolean"?o:a}function X(e,a){const o=e.meta?.reuse;return typeof o=="boolean"?o:a}function se(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 s=se(e);return{id:$(e),to:e.fullPath,fullPath:e.fullPath,matched:e,alive:G(e,o),reusable:X(e,!1),closable:s.closable??!0,...s,...a}}function Q(e,a,o,s){if(!e.find(p=>p.id===a.id)){if(o==="next"&&s){const p=e.findIndex(k=>k.id===s);if(p!==-1){e.splice(p+1,0,a);return}}e.push(a)}}function ce(e,a,o){if(!a||a<=0)return;const s=e.filter(r=>r.alive);for(;s.length>a;){const r=s.shift();if(!r||r.id===o)continue;const p=e.findIndex(k=>k.id===r.id);p>-1&&(e[p].alive=!1)}}function Pe(e){return{to:e.to,title:e.title,tips:e.tips,icon:e.icon,tabClass:e.tabClass,closable:e.closable}}function Be(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 Ae(e,a={}){const o=Re(a),s=n.reactive([]),r=n.ref(null),p=n.shallowRef(),k=n.ref(null),c=n.computed(()=>s.filter(l=>l.alive).map(l=>l.id));let d=!1;function g(l){const b=typeof l.matched=="object"?l:M(e,l);return{key:$(b),fullPath:b.fullPath,alive:G(b,o.keepAlive),reusable:X(b,!1),matched:b}}function R(l){const b=$(l);let m=s.find(v=>v.id===b);return m?(m.fullPath=l.fullPath,m.to=l.fullPath,m.matched=l,m.alive=G(l,o.keepAlive),m.reusable=X(l,m.reusable),Object.assign(m,se(l)),m):(m=_(l,{},o.keepAlive),Q(s,m,o.appendPosition,r.value),ce(s,o.maxAlive,r.value),m)}async function I(l,b=!1,m=!0){const v=M(e,l),E=$(v),P=r.value===E;m==="sameTab"&&(m=P),m&&await K(E,!0),await e[b?"replace":"push"](v),P&&await T()}function x(l){const b=s.findIndex(A=>A.id===l);if(b===-1)return o.defaultRoute;const m=s[b+1],v=s[b-1],E=s.find(A=>A.id!==l),P=m||v||E;return P?P.to:o.defaultRoute}async function j(l=r.value,b={}){if(!l)return;if(!b.force&&o.keepLastTab&&s.length===1)throw new Error("[RouterTabs] Unable to close the final tab when keepLastTab is true.");const v=r.value===l&&b.redirect!==null,E=v?b.redirect??x(l):null;await O(l,{force:b.force}),b.redirect!==null&&v&&E&&await e.replace(E)}async function O(l,b={}){const m=s.findIndex(v=>v.id===l);m!==-1&&(s.splice(m,1),k.value===l&&(k.value=null),r.value===l&&(r.value=null,p.value=void 0))}async function K(l=r.value??void 0,b=!1){if(!l)return;const m=s.find(v=>v.id===l);m&&(o.keepAlive&&m.alive&&(m.alive=!1,await n.nextTick(),m.alive=!0,await n.nextTick()),k.value=l,await n.nextTick(),b||await n.nextTick(),k.value=null)}async function oe(l=!1){for(const b of s)await K(b.id,l)}async function ae(l=o.defaultRoute){s.splice(0,s.length),r.value=null,p.value=void 0;for(const b of o.initialTabs){const m=M(e,b.to),v=_(m,b,o.keepAlive);s.push(v)}await e.replace(l)}async function T(){const l=r.value;l&&await K(l,!0)}function W(l){return typeof l.matched=="object"?$(l):$(M(e,l))}function D(){const l=s.find(b=>b.id===r.value);return{tabs:s.map(Pe),active:l?l.to:null}}async function S(l){d=!0,s.splice(0,s.length),r.value=null,p.value=void 0;const b=l?.tabs??[];for(const v of b)try{const E=M(e,v.to),P=Be(v),A=_(E,P,o.keepAlive);Q(s,A,"last",null)}catch{}d=!1;const m=l?.active??b[b.length-1]?.to??o.defaultRoute;if(m)try{await e.replace(m)}catch{}}return n.watch(()=>e.currentRoute.value,l=>{if(d)return;const b=R(l);r.value=b.id,p.value=b,ce(s,o.maxAlive,r.value)},{immediate:!0}),o.initialTabs.length&&o.initialTabs.forEach(l=>{const b=M(e,l.to),m=_(b,l,o.keepAlive);Q(s,m,"last",null)}),{options:o,tabs:s,activeId:r,current:p,includeKeys:c,refreshingKey:k,openTab:I,closeTab:j,removeTab:O,refreshTab:K,refreshAll:oe,reset:ae,reload:T,getRouteKey:W,matchRoute:g,snapshot:D,hydrate:S}}function ue(e){return e?typeof e=="string"?{name:e}:e:{}}const V=Symbol("RouterTabsContext"),Y="router-tabs:snapshot";function Z(e={}){const{optional:a=!1}=e,o=n.inject(V,null);if(o)return o;const s=n.inject("$tabs",null);if(s)return s;const p=n.getCurrentInstance()?.appContext.config.globalProperties.$tabs;if(p)return p;if(!a)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 a=`${encodeURIComponent(e)}=`,o=document.cookie?document.cookie.split("; "):[];for(const s of o)if(s.startsWith(a))return decodeURIComponent(s.slice(a.length));return null}function de(e,a,o){if(typeof document>"u")return;const{expiresInDays:s=7,path:r="/",domain:p,secure:k,sameSite:c="lax"}=o,d=[`${encodeURIComponent(e)}=${encodeURIComponent(a)}`];if(s!==1/0){const g=new Date(Date.now()+s*Ie).toUTCString();d.push(`Expires=${g}`)}r&&d.push(`Path=${r}`),p&&d.push(`Domain=${p}`),k&&d.push("Secure"),c&&d.push(`SameSite=${c.charAt(0).toUpperCase()}${c.slice(1)}`),document.cookie=d.join("; ")}function fe(e,a){if(typeof document>"u")return;const{path:o="/",domain:s}=a,r=[`${encodeURIComponent(e)}=`];r.push("Expires=Thu, 01 Jan 1970 00:00:01 GMT"),o&&r.push(`Path=${o}`),s&&r.push(`Domain=${s}`),document.cookie=r.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:a=Y,serialize:o=xe,deserialize:s=De}=e,r=Z({optional:!0}),p=n.ref(!0),k=c=>{n.onMounted(async()=>{const d=s(Se(a));if(d&&d.tabs?.length)try{if(p.value=!0,await c.hydrate(d),d.active){await n.nextTick();const R=c.tabs.find(I=>I.to===d.active);R&&(c.activeId.value=R.id,c.current.value=R)}}finally{p.value=!1}else if(Object.prototype.hasOwnProperty.call(e,"fallbackRoute"))try{p.value=!0;const R=e.fallbackRoute??c.options.defaultRoute;await c.reset(R)}finally{p.value=!1}else p.value=!1;const g=c.snapshot();g.tabs.length?de(a,o(g),e):fe(a,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})),active:c.activeId.value}),()=>{if(p.value)return;const d=c.snapshot();d.tabs.length?de(a,o(d),e):fe(a,e)},{deep:!0})};r?k(r):n.onMounted(()=>{const c=Z({optional:!0});c&&k(c)})}const Me=n.defineComponent({name:"RouterTab",components:{RouterView:Ce.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:a}){const o=n.getCurrentInstance();if(!o)throw new Error("[RouterTab] component must be used within a Vue application context.");const s=o.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 r=Ae(s,{initialTabs:e.tabs,keepAlive:e.keepAlive,maxAlive:e.maxAlive,keepLastTab:e.keepLastTab,appendPosition:e.append,defaultRoute:e.defaultPage});n.provide(V,r),o.appContext.config.globalProperties.$tabs=r;const p=n.computed(()=>!!o?.slots?.default),k=n.computed(()=>!!o?.slots?.start),c=n.computed(()=>!!o?.slots?.end),d=n.ref(0),g=n.computed(()=>{d.value;const t={};return r.tabs.forEach(i=>{const f=typeof i.title=="string"?i.title:String(i.title||ie(i));t[i.id]=f}),t});function R(){d.value++}const I=new Map,x=new Map;function j(t,i){if(!i||I.has(t))return;I.set(t,i);const f=r.tabs.find(y=>r.getRouteKey(y.to)===t);if(!f)return;const h=[];if(i.routeTabTitle!==void 0){const y=n.watch(()=>{const u=i.routeTabTitle;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{if(u!=null){const L=String(u);f.title=L,R()}},{immediate:!0});h.push(y)}if(i.routeTabIcon!==void 0){const y=n.watch(()=>{const u=i.routeTabIcon;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u!=null&&(f.icon=String(u),R())},{immediate:!0});h.push(y)}if(i.routeTabClosable!==void 0){const y=n.watch(()=>{const u=i.routeTabClosable;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u!=null&&(f.closable=!!u,R())},{immediate:!0});h.push(y)}if(i.routeTabMeta!==void 0){const y=n.watch(()=>{const u=i.routeTabMeta;return u&&typeof u=="object"&&"value"in u?u.value:u},u=>{u&&typeof u=="object"&&(Object.assign(f,u),R())},{immediate:!0,deep:!0});h.push(y)}x.set(t,h)}function O(t){const i=x.get(t);i&&(i.forEach(f=>f()),x.delete(t)),I.delete(t)}function K(t,i){t?t.routeTabTitle!==void 0||t.routeTabIcon!==void 0||t.routeTabClosable!==void 0?j(i,t):t.$&&(t.$.routeTabTitle!==void 0||t.$.routeTabIcon!==void 0||t.$.routeTabClosable!==void 0)&&j(i,t.$):t===null&&O(i)}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),D=n.ref([]),S=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 r.tabs.findIndex(i=>i.id===t)}function v(t){const i=m(t.id);return i>0?r.tabs.slice(0,i):[]}function E(t){const i=m(t.id);return i>-1?r.tabs.slice(i+1):[]}function P(t){return r.tabs.filter(i=>i.id!==t.id)}async function A(t,i){const f=t.filter(h=>h.closable!==!1);if(f.length){for(const h of f)r.activeId.value===h.id?await r.closeTab(h.id,{redirect:i.to,force:!0}):await r.removeTab(h.id,{force:!0});r.activeId.value!==i.id&&await r.openTab(i.to,!0,!1)}}const et={refresh:{label:"Refresh",handler:async({target:t})=>{await r.refreshTab(t.id,!0)}},refreshAll:{label:"Refresh All",handler:async()=>{await r.refreshAll(!0)}},close:{label:"Close",handler:async({target:t})=>{await r.closeTab(t.id)},enable:({target:t})=>re(t)},closeLefts:{label:"Close to the Left",handler:async({target:t})=>{await A(v(t),t)},enable:({target:t})=>v(t).some(i=>i.closable!==!1)},closeRights:{label:"Close to the Right",handler:async({target:t})=>{await A(E(t),t)},enable:({target:t})=>E(t).some(i=>i.closable!==!1)},closeOthers:{label:"Close Others",handler:async({target:t})=>{await A(P(t),t)},enable:({target:t})=>P(t).some(i=>i.closable!==!1)}};function B(){T.visible=!1,T.target=null,S.value=-1,D.value=[]}function tt(t,i){e.contextmenu&&(T.visible=!0,T.target=t,T.position.x=i.clientX,T.position.y=i.clientY,document.addEventListener("click",B,{once:!0}),n.nextTick(()=>{ot()}))}function nt(t,i){const f=typeof t=="string"?{id:t}:t,h=et[f.id],y=f.label??h?.label??String(f.id),u=f.visible??h?.visible??!0;if(!(typeof u=="function"?u(i):u!==!1))return null;const le=f.enable??h?.enable??!0,yt=typeof le=="function"?le(i):le!==!1,we=f.handler??h?.handler;if(!we)return null;const Tt=async()=>{await Promise.resolve(we(i))};return{id:String(f.id),label:y,disabled:!yt,action:Tt}}const U=n.computed(()=>{if(!T.visible||!T.target||e.contextmenu===!1)return[];const t=Array.isArray(e.contextmenu)?e.contextmenu:b,i={target:T.target,controller:r};return t.map(f=>nt(f,i)).filter(f=>!!f)});function ot(){const t=W.value;if(!t)return;const i=8,{innerWidth:f,innerHeight:h}=window,y=t.getBoundingClientRect();let u=T.position.x,L=T.position.y;y.right>f-i&&(u=Math.max(i,f-y.width-i)),y.bottom>h-i&&(L=Math.max(i,h-y.height-i)),(u!==T.position.x||L!==T.position.y)&&(T.position.x=u,T.position.y=L)}function at(t,i){D.value[i]=t??null}function it(t){if(t<0)return;D.value[t]?.focus({preventScroll:!0})}function q(t,i,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+i+h)%h,!f[y].disabled)return y;return-1}function z(t){S.value=t,!(t<0)&&n.nextTick(()=>it(t))}function Te(t){const i=q(S.value,t);i!==-1&&z(i)}function rt(t){if(!T.visible)return;const i=t.key,f=U.value;if(!f.length)return;if(i==="Tab"){B();return}if(["ArrowDown","ArrowUp","ArrowRight","ArrowLeft","Home","End","Enter"," ","Spacebar","Escape"].includes(i))switch(t.preventDefault(),i){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=S.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 ie(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 lt(t){return g.value[t.id]||ie(t)}function re(t){return!(t.closable===!1||r.options.keepLastTab&&r.tabs.length<=1)}async function st(t){await r.closeTab(t.id)}function ct(t){if(t.href&&typeof window<"u"){t.target&&t.target!=="_self"?window.open(t.href,t.target):window.location.assign(t.href);return}r.activeId.value!==t.id&&r.openTab(t.to,!1)}function ut(t){return["router-tab__item",{"is-active":r.activeId.value===t.id,"is-closable":re(t),"is-dragging":l.dragging&&l.dragTab?.id===t.id,"is-drag-over":l.dropIndex===m(t.id)},t.tabClass]}function dt(t){return r.refreshingKey.value===r.getRouteKey(t)}function ft(t,i,f){e.sortable&&(l.dragging=!0,l.dragIndex=i,l.dragTab=t,f.dataTransfer&&(f.dataTransfer.effectAllowed="move",f.dataTransfer.setData("text/plain",t.id)),a("tab-sort",{tab:t,index:i}))}function bt(t,i){!e.sortable||!l.dragging||(i.preventDefault(),i.dataTransfer&&(i.dataTransfer.dropEffect="move"))}function mt(t){!e.sortable||!l.dragging||(l.dropIndex=t)}function pt(){!e.sortable||l.dragging}function gt(t,i){if(!(!e.sortable||!l.dragging)){if(i.preventDefault(),l.dragIndex!==-1&&l.dragIndex!==t){const f=r.tabs.splice(l.dragIndex,1)[0];r.tabs.splice(t,0,f),a("tab-sorted",{tab:f,fromIndex:l.dragIndex,toIndex:t})}ve()}}function ve(){l.dragging=!1,l.dragIndex=-1,l.dropIndex=-1,l.dragTab=null}n.onMounted(()=>{document.addEventListener("keydown",B)}),n.onBeforeUnmount(()=>{document.removeEventListener("keydown",B),o.appContext.config.globalProperties.$tabs=null,x.forEach(t=>{t.forEach(i=>i())}),x.clear(),I.clear()}),n.watch(()=>e.keepAlive,t=>{r.options.keepAlive=t}),n.watch(()=>r.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(D.value=new Array(t.length).fill(null),!T.visible)return;const i=q(-1,1,t);z(i)}),n.watch(()=>T.visible,t=>{t||(S.value=-1,D.value=[])});const ht=r.includeKeys;return{controller:r,tabs:r.tabs,includeKeys:ht,tabTransitionProps:oe,pageTransitionProps:ae,buildTabClass:ut,activate:ct,close:st,context:T,menuItems:U,handleMenuAction:ke,showContextMenu:tt,hideContextMenu:B,getTabTitle:ie,isClosable:re,isRefreshing:dt,hasCustomSlot:p,hasStartSlot:k,hasEndSlot:c,onDragStart:ft,onDragOver:bt,onDragEnter:mt,onDragLeave:pt,onDrop:gt,onDragEnd:ve,setupComponentWatching:j,cleanupComponentWatching:O,handleComponentRef:K,getReactiveTabTitle:lt,triggerTabUpdate:R,menuRef:W,highlightedIndex:S,setMenuItemRef:at,onMenuKeydown:rt,highlightMenuIndex:z}}}),$e=(e,a)=>{const o=e.__vccOpts||e;for(const[s,r]of a)o[s]=r;return o},Ke={class:"router-tab"},Le={class:"router-tab__header"},Ve={class:"router-tab__scroll"},Ne=["data-title","draggable","onClick","onAuxclick","onContextmenu","onDragstart","onDragover","onDragenter","onDrop"],je=["title"],Oe=["onClick"],Ue={class:"router-tab__container"},ze=["aria-disabled","disabled","tabindex","onMouseenter","onClick"];function _e(e,a,o,s,r,p){const k=n.resolveComponent("RouterView");return n.openBlock(),n.createElementBlock("div",Ke,[n.createElementVNode("header",Le,[n.createElementVNode("div",{class:n.normalizeClass(["router-tab__slot-start",{"has-content":e.hasStartSlot}])},[n.renderSlot(e.$slots,"start")],2),n.createElementVNode("div",Ve,[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:a[0]||(a[0]=(...g)=>e.onDragLeave&&e.onDragLeave(...g)),onDrop:g=>e.onDrop(d,g),onDragend:a[1]||(a[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,je),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,Oe)):n.createCommentVNode("",!0)],42,Ne))),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",Ue,[n.createVNode(k,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.controller.getRouteKey(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.controller.getRouteKey(c.route)+(e.isRefreshing(c.route)?"-refresh":""),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:a[2]||(a[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,ze))),128))],36)):n.createCommentVNode("",!0)])}const te=$e(Me,[["render",_e]]),Ye={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),(o,s)=>(n.openBlock(),n.createElementBlock("span",Ye))}}),be="tab-theme-style",me="tab-theme-primary-color",Fe="system",pe="(prefers-color-scheme: dark)";let N=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"},He={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 Je(e){if(typeof window>"u")return null;const a=window.localStorage.getItem(e);if(!a)return null;try{const o=JSON.parse(a);return o&&typeof o=="object"?o:null}catch{return null}}function ne(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 ge(e){if(typeof document>"u")return;const a=document.documentElement,o=window.matchMedia(pe),s=()=>{a.dataset.theme=o.matches?"dark":"light"};N&&(o.removeEventListener("change",N),N=null),e==="system"?(s(),N=()=>s(),o.addEventListener("change",N)):a.dataset.theme=e}function he(e={}){if(typeof window>"u")return;const{styleKey:a=be,primaryKey:o=me,defaultStyle:s=Fe,defaultPrimary:r}=e,p=window.localStorage.getItem(a)??s;ge(p);const c=p==="dark"||p==="system"&&window.matchMedia(pe).matches?{...He}:{...C};r&&(c.primary=r);const d=Je(o);ne(d?{...c,...d}:c)}function We(e,a){if(typeof window>"u")return;const o=a?.styleKey??be;window.localStorage.setItem(o,e),ge(e)}function qe(e,a){if(typeof window>"u")return;const o=a?.primaryKey??me;window.localStorage.setItem(o,JSON.stringify(e)),ne(e)}function H(e,a){if(n.isRef(e)){const s=!n.isReadonly(e);return{value:e,update:s?r=>{e.value=r}:()=>{}}}if(typeof e=="function"){const s=e;return{value:n.computed(s),update:()=>{}}}const o=n.ref(e===void 0?a:e);return{value:o,update:s=>{o.value=s}}}function J(e={}){const a=H(e.title,"Untitled"),o=H(e.icon,""),s=H(e.closable,!0),r=H(e.meta,{});return{routeTabTitle:a.value,routeTabIcon:o.value,routeTabClosable:s.value,routeTabMeta:r.value,updateTitle:a.update,updateIcon:o.update,updateClosable:s.update,updateMeta:r.update}}function Ge(e,a="Page"){return J({title:n.computed(()=>e.value?"Loading...":a),icon:n.computed(()=>e.value?"mdi-loading mdi-spin":"mdi-page"),closable:n.computed(()=>!e.value)})}function Xe(e,a="Page",o="mdi-page"){return J({title:n.computed(()=>e.value>0?`${a} (${e.value})`:a),icon:n.computed(()=>e.value>0?"mdi-bell-badge":o)})}function Qe(e,a="Page"){const o={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(()=>a+o[e.value].suffix),icon:n.computed(()=>o[e.value].icon),closable:n.computed(()=>e.value!=="loading")})}let ye=!1;const Ze={install(e,a){if(ye)return;ye=!0;const{initTheme:o=!0,themeOptions:s,componentName:r=te.name||"RouterTab",tabsComponentName:p=F.name||"RouterTabs"}=a??{};o&&he(s??{}),e.component(r,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(k){k&&e.provide(V,k)}})}};w.RouterTab=te,w.RouterTabs=F,w.default=Ze,w.initRouterTabsTheme=he,w.routerTabsKey=V,w.setRouterTabsPrimary=qe,w.setRouterTabsTheme=We,w.useLoadingTab=Ge,w.useNotificationTab=Xe,w.useReactiveTab=J,w.useRouterTabs=Z,w.useRouterTabsPersistence=ee,w.useStatusTab=Qe,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
|
|
|
@@ -27,9 +30,9 @@
|
|
|
27
30
|
@drop="onDrop(index, $event)"
|
|
28
31
|
@dragend="onDragEnd"
|
|
29
32
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
{{
|
|
33
|
+
<i v-if="tab.icon" :class="['router-tab__item-icon', tab.icon]" />
|
|
34
|
+
<span class="router-tab__item-title" :title="getReactiveTabTitle(tab)">
|
|
35
|
+
{{ getReactiveTabTitle(tab) }}
|
|
33
36
|
</span>
|
|
34
37
|
<a
|
|
35
38
|
v-if="isClosable(tab)"
|
|
@@ -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>
|
|
@@ -48,7 +54,14 @@
|
|
|
48
54
|
<div class="router-tab__container">
|
|
49
55
|
<RouterView v-slot="routerSlot">
|
|
50
56
|
<template v-if="hasCustomSlot">
|
|
51
|
-
<slot
|
|
57
|
+
<slot
|
|
58
|
+
v-bind="{
|
|
59
|
+
...routerSlot,
|
|
60
|
+
controller,
|
|
61
|
+
// Expose a ref binder so custom slots can keep reactivity
|
|
62
|
+
pageRef: (el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))
|
|
63
|
+
}"
|
|
64
|
+
/>
|
|
52
65
|
</template>
|
|
53
66
|
<template v-else>
|
|
54
67
|
<transition
|
|
@@ -64,6 +77,7 @@
|
|
|
64
77
|
v-if="!isRefreshing(routerSlot.route)"
|
|
65
78
|
:is="routerSlot.Component"
|
|
66
79
|
:key="controller.getRouteKey(routerSlot.route)"
|
|
80
|
+
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
67
81
|
class="router-tab-page"
|
|
68
82
|
/>
|
|
69
83
|
</KeepAlive>
|
|
@@ -77,6 +91,7 @@
|
|
|
77
91
|
v-if="!controller.options.keepAlive || isRefreshing(routerSlot.route)"
|
|
78
92
|
:is="routerSlot.Component"
|
|
79
93
|
:key="controller.getRouteKey(routerSlot.route) + (isRefreshing(routerSlot.route) ? '-refresh' : '')"
|
|
94
|
+
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
80
95
|
class="router-tab-page"
|
|
81
96
|
/>
|
|
82
97
|
</transition>
|
|
@@ -86,18 +101,26 @@
|
|
|
86
101
|
|
|
87
102
|
<div
|
|
88
103
|
v-if="context.visible && context.target"
|
|
104
|
+
ref="menuRef"
|
|
89
105
|
class="router-tab__contextmenu"
|
|
106
|
+
role="menu"
|
|
107
|
+
@keydown="onMenuKeydown"
|
|
90
108
|
:style="{ left: context.position.x + 'px', top: context.position.y + 'px' }"
|
|
91
109
|
>
|
|
92
110
|
<a
|
|
93
|
-
v-for="item in menuItems"
|
|
111
|
+
v-for="(item, index) in menuItems"
|
|
94
112
|
:key="item.id"
|
|
95
|
-
|
|
113
|
+
role="menuitem"
|
|
114
|
+
:class="['router-tab__contextmenu-item', { 'is-focused': index === highlightedIndex }]"
|
|
96
115
|
:aria-disabled="item.disabled"
|
|
97
|
-
|
|
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)"
|
|
98
121
|
>
|
|
99
122
|
{{ item.label }}
|
|
100
|
-
|
|
123
|
+
</a>
|
|
101
124
|
</div>
|
|
102
125
|
</div>
|
|
103
126
|
</template>
|
|
@@ -107,10 +130,12 @@ import {
|
|
|
107
130
|
computed,
|
|
108
131
|
defineComponent,
|
|
109
132
|
getCurrentInstance,
|
|
133
|
+
nextTick,
|
|
110
134
|
onBeforeUnmount,
|
|
111
135
|
onMounted,
|
|
112
136
|
provide,
|
|
113
137
|
reactive,
|
|
138
|
+
ref,
|
|
114
139
|
watch
|
|
115
140
|
} from 'vue'
|
|
116
141
|
import { RouterView, type RouteLocationNormalizedLoaded } from 'vue-router'
|
|
@@ -189,10 +214,6 @@ export default defineComponent({
|
|
|
189
214
|
sortable: {
|
|
190
215
|
type: Boolean,
|
|
191
216
|
default: true
|
|
192
|
-
},
|
|
193
|
-
titleResolver: {
|
|
194
|
-
type: Function as PropType<(tab: TabRecord) => string>,
|
|
195
|
-
default: null
|
|
196
217
|
}
|
|
197
218
|
},
|
|
198
219
|
emits: ['tab-sort', 'tab-sorted'],
|
|
@@ -220,6 +241,150 @@ export default defineComponent({
|
|
|
220
241
|
instance.appContext.config.globalProperties.$tabs = controller
|
|
221
242
|
|
|
222
243
|
const hasCustomSlot = computed(() => Boolean(instance?.slots?.default))
|
|
244
|
+
const hasStartSlot = computed(() => Boolean(instance?.slots?.start))
|
|
245
|
+
const hasEndSlot = computed(() => Boolean(instance?.slots?.end))
|
|
246
|
+
|
|
247
|
+
// Force reactivity by creating a trigger ref
|
|
248
|
+
const tabUpdateTrigger = ref(0)
|
|
249
|
+
|
|
250
|
+
// Reactive tab titles that update when tabs change
|
|
251
|
+
const reactiveTabTitles = computed(() => {
|
|
252
|
+
// Access the trigger to ensure reactivity
|
|
253
|
+
tabUpdateTrigger.value
|
|
254
|
+
const titles: Record<string, string> = {}
|
|
255
|
+
controller.tabs.forEach(tab => {
|
|
256
|
+
// Use the tab's current title (which is updated by watchers)
|
|
257
|
+
const currentTitle = typeof tab.title === 'string' ? tab.title : String(tab.title || getTabTitle(tab))
|
|
258
|
+
titles[tab.id] = currentTitle
|
|
259
|
+
})
|
|
260
|
+
return titles
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Function to trigger reactivity updates
|
|
264
|
+
function triggerTabUpdate() {
|
|
265
|
+
tabUpdateTrigger.value++
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Reactive tab update system
|
|
269
|
+
const componentInstances = new Map<string, any>()
|
|
270
|
+
const watchedProperties = new Map<string, (() => void)[]>()
|
|
271
|
+
|
|
272
|
+
// Watch for component instance changes and set up reactivity
|
|
273
|
+
function setupComponentWatching(routeKey: string, componentInstance: any) {
|
|
274
|
+
if (!componentInstance || componentInstances.has(routeKey)) return
|
|
275
|
+
|
|
276
|
+
// setup watchers for exposed reactive tab props
|
|
277
|
+
|
|
278
|
+
componentInstances.set(routeKey, componentInstance)
|
|
279
|
+
|
|
280
|
+
// Find the tab for this route
|
|
281
|
+
const tab = controller.tabs.find(t => controller.getRouteKey(t.to) === routeKey)
|
|
282
|
+
if (!tab) return
|
|
283
|
+
|
|
284
|
+
const unwatchers: (() => void)[] = []
|
|
285
|
+
|
|
286
|
+
// Watch routeTabTitle for title updates
|
|
287
|
+
if (componentInstance.routeTabTitle !== undefined) {
|
|
288
|
+
const unwatchTitle = watch(
|
|
289
|
+
() => {
|
|
290
|
+
// Handle both ref values and regular values
|
|
291
|
+
const titleValue = componentInstance.routeTabTitle
|
|
292
|
+
return titleValue && typeof titleValue === 'object' && 'value' in titleValue ? titleValue.value : titleValue
|
|
293
|
+
},
|
|
294
|
+
(newTitle) => {
|
|
295
|
+
if (newTitle !== undefined && newTitle !== null) {
|
|
296
|
+
const titleString = String(newTitle)
|
|
297
|
+
tab.title = titleString
|
|
298
|
+
triggerTabUpdate() // Trigger reactivity
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{ immediate: true }
|
|
302
|
+
)
|
|
303
|
+
unwatchers.push(unwatchTitle)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Watch routeTabIcon for icon updates
|
|
307
|
+
if (componentInstance.routeTabIcon !== undefined) {
|
|
308
|
+
const unwatchIcon = watch(
|
|
309
|
+
() => {
|
|
310
|
+
const iconValue = componentInstance.routeTabIcon
|
|
311
|
+
return iconValue && typeof iconValue === 'object' && 'value' in iconValue ? iconValue.value : iconValue
|
|
312
|
+
},
|
|
313
|
+
(newIcon) => {
|
|
314
|
+
if (newIcon !== undefined && newIcon !== null) {
|
|
315
|
+
tab.icon = String(newIcon)
|
|
316
|
+
triggerTabUpdate() // Trigger reactivity
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{ immediate: true }
|
|
320
|
+
)
|
|
321
|
+
unwatchers.push(unwatchIcon)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Watch routeTabClosable for closable state updates
|
|
325
|
+
if (componentInstance.routeTabClosable !== undefined) {
|
|
326
|
+
const unwatchClosable = watch(
|
|
327
|
+
() => {
|
|
328
|
+
const closableValue = componentInstance.routeTabClosable
|
|
329
|
+
return closableValue && typeof closableValue === 'object' && 'value' in closableValue ? closableValue.value : closableValue
|
|
330
|
+
},
|
|
331
|
+
(newClosable) => {
|
|
332
|
+
if (newClosable !== undefined && newClosable !== null) {
|
|
333
|
+
tab.closable = Boolean(newClosable)
|
|
334
|
+
triggerTabUpdate() // Trigger reactivity
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
{ immediate: true }
|
|
338
|
+
)
|
|
339
|
+
unwatchers.push(unwatchClosable)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Watch routeTabMeta for general meta updates
|
|
343
|
+
if (componentInstance.routeTabMeta !== undefined) {
|
|
344
|
+
const unwatchMeta = watch(
|
|
345
|
+
() => {
|
|
346
|
+
const metaValue = componentInstance.routeTabMeta
|
|
347
|
+
return metaValue && typeof metaValue === 'object' && 'value' in metaValue ? metaValue.value : metaValue
|
|
348
|
+
},
|
|
349
|
+
(newMeta) => {
|
|
350
|
+
if (newMeta && typeof newMeta === 'object') {
|
|
351
|
+
Object.assign(tab, newMeta)
|
|
352
|
+
triggerTabUpdate() // Trigger reactivity
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{ immediate: true, deep: true }
|
|
356
|
+
)
|
|
357
|
+
unwatchers.push(unwatchMeta)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
watchedProperties.set(routeKey, unwatchers)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Cleanup watchers when component is unmounted
|
|
364
|
+
function cleanupComponentWatching(routeKey: string) {
|
|
365
|
+
const unwatchers = watchedProperties.get(routeKey)
|
|
366
|
+
if (unwatchers) {
|
|
367
|
+
unwatchers.forEach(unwatcher => unwatcher())
|
|
368
|
+
watchedProperties.delete(routeKey)
|
|
369
|
+
}
|
|
370
|
+
componentInstances.delete(routeKey)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle component ref changes
|
|
374
|
+
function handleComponentRef(el: any, routeKey: string) {
|
|
375
|
+
if (el) {
|
|
376
|
+
// Component mounted - set up watching
|
|
377
|
+
// Check if properties are exposed directly on el (Vue 3 with defineExpose)
|
|
378
|
+
if (el.routeTabTitle !== undefined || el.routeTabIcon !== undefined || el.routeTabClosable !== undefined) {
|
|
379
|
+
setupComponentWatching(routeKey, el)
|
|
380
|
+
} else if (el.$ && (el.$.routeTabTitle !== undefined || el.$.routeTabIcon !== undefined || el.$.routeTabClosable !== undefined)) {
|
|
381
|
+
setupComponentWatching(routeKey, el.$)
|
|
382
|
+
}
|
|
383
|
+
} else if (el === null) {
|
|
384
|
+
// Component unmounted - cleanup
|
|
385
|
+
cleanupComponentWatching(routeKey)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
223
388
|
|
|
224
389
|
if (props.cookieKey !== null || props.persistence) {
|
|
225
390
|
const options: RouterTabsPersistenceOptions = {
|
|
@@ -242,6 +407,10 @@ export default defineComponent({
|
|
|
242
407
|
position: { x: 0, y: 0 }
|
|
243
408
|
})
|
|
244
409
|
|
|
410
|
+
const menuRef = ref<HTMLElement | null>(null)
|
|
411
|
+
const menuItemRefs = ref<(HTMLElement | null)[]>([])
|
|
412
|
+
const highlightedIndex = ref(-1)
|
|
413
|
+
|
|
245
414
|
// Drag and drop state
|
|
246
415
|
const dragState = reactive({
|
|
247
416
|
dragging: false,
|
|
@@ -352,6 +521,8 @@ export default defineComponent({
|
|
|
352
521
|
function hideContextMenu() {
|
|
353
522
|
context.visible = false
|
|
354
523
|
context.target = null
|
|
524
|
+
highlightedIndex.value = -1
|
|
525
|
+
menuItemRefs.value = []
|
|
355
526
|
}
|
|
356
527
|
|
|
357
528
|
function showContextMenu(tab: TabRecord, event: MouseEvent) {
|
|
@@ -362,6 +533,9 @@ export default defineComponent({
|
|
|
362
533
|
context.position.y = event.clientY
|
|
363
534
|
|
|
364
535
|
document.addEventListener('click', hideContextMenu, { once: true })
|
|
536
|
+
nextTick(() => {
|
|
537
|
+
adjustMenuPosition()
|
|
538
|
+
})
|
|
365
539
|
}
|
|
366
540
|
|
|
367
541
|
function normalizeMenuItem(raw: MenuConfig, ctx: MenuActionContext): ResolvedMenuItem | null {
|
|
@@ -401,6 +575,128 @@ export default defineComponent({
|
|
|
401
575
|
.filter((item): item is ResolvedMenuItem => !!item)
|
|
402
576
|
})
|
|
403
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
|
+
|
|
404
700
|
async function handleMenuAction(item: ResolvedMenuItem) {
|
|
405
701
|
if (item.disabled) return
|
|
406
702
|
hideContextMenu()
|
|
@@ -408,12 +704,16 @@ export default defineComponent({
|
|
|
408
704
|
}
|
|
409
705
|
|
|
410
706
|
function getTabTitle(tab: TabRecord): string {
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
707
|
+
if (typeof tab.title === 'string' && tab.title.trim()) return tab.title
|
|
708
|
+
if (Array.isArray(tab.title) && tab.title.length && String(tab.title[0]).trim()) return String(tab.title[0])
|
|
709
|
+
return 'Untitled'
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Reactive function to get tab title
|
|
713
|
+
function getReactiveTabTitle(tab: TabRecord): string {
|
|
714
|
+
// Access the reactive titles to ensure reactivity
|
|
715
|
+
const reactiveTitles = reactiveTabTitles.value
|
|
716
|
+
return reactiveTitles[tab.id] || getTabTitle(tab)
|
|
417
717
|
}
|
|
418
718
|
|
|
419
719
|
function isClosable(tab: TabRecord) {
|
|
@@ -427,6 +727,15 @@ export default defineComponent({
|
|
|
427
727
|
}
|
|
428
728
|
|
|
429
729
|
function activate(tab: TabRecord) {
|
|
730
|
+
if (tab.href && typeof window !== 'undefined') {
|
|
731
|
+
if (tab.target && tab.target !== '_self') {
|
|
732
|
+
window.open(tab.href as string, tab.target)
|
|
733
|
+
} else {
|
|
734
|
+
window.location.assign(tab.href as string)
|
|
735
|
+
}
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
430
739
|
if (controller.activeId.value === tab.id) return
|
|
431
740
|
controller.openTab(tab.to, false)
|
|
432
741
|
}
|
|
@@ -515,6 +824,13 @@ export default defineComponent({
|
|
|
515
824
|
onBeforeUnmount(() => {
|
|
516
825
|
document.removeEventListener('keydown', hideContextMenu)
|
|
517
826
|
instance.appContext.config.globalProperties.$tabs = null
|
|
827
|
+
|
|
828
|
+
// Cleanup all component watchers
|
|
829
|
+
watchedProperties.forEach((unwatchers) => {
|
|
830
|
+
unwatchers.forEach(unwatcher => unwatcher())
|
|
831
|
+
})
|
|
832
|
+
watchedProperties.clear()
|
|
833
|
+
componentInstances.clear()
|
|
518
834
|
})
|
|
519
835
|
|
|
520
836
|
watch(
|
|
@@ -545,6 +861,23 @@ export default defineComponent({
|
|
|
545
861
|
}
|
|
546
862
|
)
|
|
547
863
|
|
|
864
|
+
watch(menuItems, items => {
|
|
865
|
+
menuItemRefs.value = new Array(items.length).fill(null)
|
|
866
|
+
if (!context.visible) return
|
|
867
|
+
const first = findNextEnabledIndex(-1, 1, items)
|
|
868
|
+
highlightMenuIndex(first)
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
watch(
|
|
872
|
+
() => context.visible,
|
|
873
|
+
visible => {
|
|
874
|
+
if (!visible) {
|
|
875
|
+
highlightedIndex.value = -1
|
|
876
|
+
menuItemRefs.value = []
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
|
|
548
881
|
const includeKeys = controller.includeKeys
|
|
549
882
|
|
|
550
883
|
return {
|
|
@@ -565,13 +898,25 @@ export default defineComponent({
|
|
|
565
898
|
isClosable,
|
|
566
899
|
isRefreshing,
|
|
567
900
|
hasCustomSlot,
|
|
901
|
+
hasStartSlot,
|
|
902
|
+
hasEndSlot,
|
|
568
903
|
onDragStart,
|
|
569
904
|
onDragOver,
|
|
570
905
|
onDragEnter,
|
|
571
906
|
onDragLeave,
|
|
572
907
|
onDrop,
|
|
573
|
-
onDragEnd
|
|
908
|
+
onDragEnd,
|
|
909
|
+
setupComponentWatching,
|
|
910
|
+
cleanupComponentWatching,
|
|
911
|
+
handleComponentRef,
|
|
912
|
+
getReactiveTabTitle,
|
|
913
|
+
triggerTabUpdate,
|
|
914
|
+
menuRef,
|
|
915
|
+
highlightedIndex,
|
|
916
|
+
setMenuItemRef,
|
|
917
|
+
onMenuKeydown,
|
|
918
|
+
highlightMenuIndex
|
|
574
919
|
}
|
|
575
920
|
}
|
|
576
921
|
})
|
|
577
|
-
</script>
|
|
922
|
+
</script>
|