vue3-router-tab 1.2.4 → 1.2.6
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 +592 -460
- package/dist/vue3-router-tab.umd.cjs +1 -1
- package/lib/components/RouterTab.vue +179 -15
- package/lib/core/createRouterTabs.ts +18 -14
- package/lib/index.ts +12 -0
- package/lib/scss/index.scss +12 -0
- package/lib/theme.ts +2 -2
- 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(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"}})}));
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
@drop="onDrop(index, $event)"
|
|
28
28
|
@dragend="onDragEnd"
|
|
29
29
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
{{
|
|
30
|
+
<i v-if="tab.icon" :class="['router-tab__item-icon', tab.icon]" />
|
|
31
|
+
<span class="router-tab__item-title" :title="getReactiveTabTitle(tab)">
|
|
32
|
+
{{ getReactiveTabTitle(tab) }}
|
|
33
33
|
</span>
|
|
34
34
|
<a
|
|
35
35
|
v-if="isClosable(tab)"
|
|
@@ -48,7 +48,14 @@
|
|
|
48
48
|
<div class="router-tab__container">
|
|
49
49
|
<RouterView v-slot="routerSlot">
|
|
50
50
|
<template v-if="hasCustomSlot">
|
|
51
|
-
<slot
|
|
51
|
+
<slot
|
|
52
|
+
v-bind="{
|
|
53
|
+
...routerSlot,
|
|
54
|
+
controller,
|
|
55
|
+
// Expose a ref binder so custom slots can keep reactivity
|
|
56
|
+
pageRef: (el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))
|
|
57
|
+
}"
|
|
58
|
+
/>
|
|
52
59
|
</template>
|
|
53
60
|
<template v-else>
|
|
54
61
|
<transition
|
|
@@ -64,6 +71,7 @@
|
|
|
64
71
|
v-if="!isRefreshing(routerSlot.route)"
|
|
65
72
|
:is="routerSlot.Component"
|
|
66
73
|
:key="controller.getRouteKey(routerSlot.route)"
|
|
74
|
+
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
67
75
|
class="router-tab-page"
|
|
68
76
|
/>
|
|
69
77
|
</KeepAlive>
|
|
@@ -77,6 +85,7 @@
|
|
|
77
85
|
v-if="!controller.options.keepAlive || isRefreshing(routerSlot.route)"
|
|
78
86
|
:is="routerSlot.Component"
|
|
79
87
|
:key="controller.getRouteKey(routerSlot.route) + (isRefreshing(routerSlot.route) ? '-refresh' : '')"
|
|
88
|
+
:ref="(el: any) => handleComponentRef(el, controller.getRouteKey(routerSlot.route))"
|
|
80
89
|
class="router-tab-page"
|
|
81
90
|
/>
|
|
82
91
|
</transition>
|
|
@@ -111,6 +120,7 @@ import {
|
|
|
111
120
|
onMounted,
|
|
112
121
|
provide,
|
|
113
122
|
reactive,
|
|
123
|
+
ref,
|
|
114
124
|
watch
|
|
115
125
|
} from 'vue'
|
|
116
126
|
import { RouterView, type RouteLocationNormalizedLoaded } from 'vue-router'
|
|
@@ -189,10 +199,6 @@ export default defineComponent({
|
|
|
189
199
|
sortable: {
|
|
190
200
|
type: Boolean,
|
|
191
201
|
default: true
|
|
192
|
-
},
|
|
193
|
-
titleResolver: {
|
|
194
|
-
type: Function as PropType<(tab: TabRecord) => string>,
|
|
195
|
-
default: null
|
|
196
202
|
}
|
|
197
203
|
},
|
|
198
204
|
emits: ['tab-sort', 'tab-sorted'],
|
|
@@ -221,6 +227,148 @@ export default defineComponent({
|
|
|
221
227
|
|
|
222
228
|
const hasCustomSlot = computed(() => Boolean(instance?.slots?.default))
|
|
223
229
|
|
|
230
|
+
// Force reactivity by creating a trigger ref
|
|
231
|
+
const tabUpdateTrigger = ref(0)
|
|
232
|
+
|
|
233
|
+
// Reactive tab titles that update when tabs change
|
|
234
|
+
const reactiveTabTitles = computed(() => {
|
|
235
|
+
// Access the trigger to ensure reactivity
|
|
236
|
+
tabUpdateTrigger.value
|
|
237
|
+
const titles: Record<string, string> = {}
|
|
238
|
+
controller.tabs.forEach(tab => {
|
|
239
|
+
// Use the tab's current title (which is updated by watchers)
|
|
240
|
+
const currentTitle = typeof tab.title === 'string' ? tab.title : String(tab.title || getTabTitle(tab))
|
|
241
|
+
titles[tab.id] = currentTitle
|
|
242
|
+
})
|
|
243
|
+
return titles
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Function to trigger reactivity updates
|
|
247
|
+
function triggerTabUpdate() {
|
|
248
|
+
tabUpdateTrigger.value++
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Reactive tab update system
|
|
252
|
+
const componentInstances = new Map<string, any>()
|
|
253
|
+
const watchedProperties = new Map<string, (() => void)[]>()
|
|
254
|
+
|
|
255
|
+
// Watch for component instance changes and set up reactivity
|
|
256
|
+
function setupComponentWatching(routeKey: string, componentInstance: any) {
|
|
257
|
+
if (!componentInstance || componentInstances.has(routeKey)) return
|
|
258
|
+
|
|
259
|
+
// setup watchers for exposed reactive tab props
|
|
260
|
+
|
|
261
|
+
componentInstances.set(routeKey, componentInstance)
|
|
262
|
+
|
|
263
|
+
// Find the tab for this route
|
|
264
|
+
const tab = controller.tabs.find(t => controller.getRouteKey(t.to) === routeKey)
|
|
265
|
+
if (!tab) return
|
|
266
|
+
|
|
267
|
+
const unwatchers: (() => void)[] = []
|
|
268
|
+
|
|
269
|
+
// Watch routeTabTitle for title updates
|
|
270
|
+
if (componentInstance.routeTabTitle !== undefined) {
|
|
271
|
+
const unwatchTitle = watch(
|
|
272
|
+
() => {
|
|
273
|
+
// Handle both ref values and regular values
|
|
274
|
+
const titleValue = componentInstance.routeTabTitle
|
|
275
|
+
return titleValue && typeof titleValue === 'object' && 'value' in titleValue ? titleValue.value : titleValue
|
|
276
|
+
},
|
|
277
|
+
(newTitle) => {
|
|
278
|
+
if (newTitle !== undefined && newTitle !== null) {
|
|
279
|
+
const titleString = String(newTitle)
|
|
280
|
+
tab.title = titleString
|
|
281
|
+
triggerTabUpdate() // Trigger reactivity
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{ immediate: true }
|
|
285
|
+
)
|
|
286
|
+
unwatchers.push(unwatchTitle)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Watch routeTabIcon for icon updates
|
|
290
|
+
if (componentInstance.routeTabIcon !== undefined) {
|
|
291
|
+
const unwatchIcon = watch(
|
|
292
|
+
() => {
|
|
293
|
+
const iconValue = componentInstance.routeTabIcon
|
|
294
|
+
return iconValue && typeof iconValue === 'object' && 'value' in iconValue ? iconValue.value : iconValue
|
|
295
|
+
},
|
|
296
|
+
(newIcon) => {
|
|
297
|
+
if (newIcon !== undefined && newIcon !== null) {
|
|
298
|
+
tab.icon = String(newIcon)
|
|
299
|
+
triggerTabUpdate() // Trigger reactivity
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{ immediate: true }
|
|
303
|
+
)
|
|
304
|
+
unwatchers.push(unwatchIcon)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Watch routeTabClosable for closable state updates
|
|
308
|
+
if (componentInstance.routeTabClosable !== undefined) {
|
|
309
|
+
const unwatchClosable = watch(
|
|
310
|
+
() => {
|
|
311
|
+
const closableValue = componentInstance.routeTabClosable
|
|
312
|
+
return closableValue && typeof closableValue === 'object' && 'value' in closableValue ? closableValue.value : closableValue
|
|
313
|
+
},
|
|
314
|
+
(newClosable) => {
|
|
315
|
+
if (newClosable !== undefined && newClosable !== null) {
|
|
316
|
+
tab.closable = Boolean(newClosable)
|
|
317
|
+
triggerTabUpdate() // Trigger reactivity
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
{ immediate: true }
|
|
321
|
+
)
|
|
322
|
+
unwatchers.push(unwatchClosable)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Watch routeTabMeta for general meta updates
|
|
326
|
+
if (componentInstance.routeTabMeta !== undefined) {
|
|
327
|
+
const unwatchMeta = watch(
|
|
328
|
+
() => {
|
|
329
|
+
const metaValue = componentInstance.routeTabMeta
|
|
330
|
+
return metaValue && typeof metaValue === 'object' && 'value' in metaValue ? metaValue.value : metaValue
|
|
331
|
+
},
|
|
332
|
+
(newMeta) => {
|
|
333
|
+
if (newMeta && typeof newMeta === 'object') {
|
|
334
|
+
Object.assign(tab, newMeta)
|
|
335
|
+
triggerTabUpdate() // Trigger reactivity
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{ immediate: true, deep: true }
|
|
339
|
+
)
|
|
340
|
+
unwatchers.push(unwatchMeta)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
watchedProperties.set(routeKey, unwatchers)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Cleanup watchers when component is unmounted
|
|
347
|
+
function cleanupComponentWatching(routeKey: string) {
|
|
348
|
+
const unwatchers = watchedProperties.get(routeKey)
|
|
349
|
+
if (unwatchers) {
|
|
350
|
+
unwatchers.forEach(unwatcher => unwatcher())
|
|
351
|
+
watchedProperties.delete(routeKey)
|
|
352
|
+
}
|
|
353
|
+
componentInstances.delete(routeKey)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Handle component ref changes
|
|
357
|
+
function handleComponentRef(el: any, routeKey: string) {
|
|
358
|
+
if (el) {
|
|
359
|
+
// Component mounted - set up watching
|
|
360
|
+
// Check if properties are exposed directly on el (Vue 3 with defineExpose)
|
|
361
|
+
if (el.routeTabTitle !== undefined || el.routeTabIcon !== undefined || el.routeTabClosable !== undefined) {
|
|
362
|
+
setupComponentWatching(routeKey, el)
|
|
363
|
+
} else if (el.$ && (el.$.routeTabTitle !== undefined || el.$.routeTabIcon !== undefined || el.$.routeTabClosable !== undefined)) {
|
|
364
|
+
setupComponentWatching(routeKey, el.$)
|
|
365
|
+
}
|
|
366
|
+
} else if (el === null) {
|
|
367
|
+
// Component unmounted - cleanup
|
|
368
|
+
cleanupComponentWatching(routeKey)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
224
372
|
if (props.cookieKey !== null || props.persistence) {
|
|
225
373
|
const options: RouterTabsPersistenceOptions = {
|
|
226
374
|
...(props.persistence ?? {})
|
|
@@ -408,12 +556,16 @@ export default defineComponent({
|
|
|
408
556
|
}
|
|
409
557
|
|
|
410
558
|
function getTabTitle(tab: TabRecord): string {
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
559
|
+
if (typeof tab.title === 'string' && tab.title.trim()) return tab.title
|
|
560
|
+
if (Array.isArray(tab.title) && tab.title.length && String(tab.title[0]).trim()) return String(tab.title[0])
|
|
561
|
+
return 'Untitled'
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Reactive function to get tab title
|
|
565
|
+
function getReactiveTabTitle(tab: TabRecord): string {
|
|
566
|
+
// Access the reactive titles to ensure reactivity
|
|
567
|
+
const reactiveTitles = reactiveTabTitles.value
|
|
568
|
+
return reactiveTitles[tab.id] || getTabTitle(tab)
|
|
417
569
|
}
|
|
418
570
|
|
|
419
571
|
function isClosable(tab: TabRecord) {
|
|
@@ -515,6 +667,13 @@ export default defineComponent({
|
|
|
515
667
|
onBeforeUnmount(() => {
|
|
516
668
|
document.removeEventListener('keydown', hideContextMenu)
|
|
517
669
|
instance.appContext.config.globalProperties.$tabs = null
|
|
670
|
+
|
|
671
|
+
// Cleanup all component watchers
|
|
672
|
+
watchedProperties.forEach((unwatchers) => {
|
|
673
|
+
unwatchers.forEach(unwatcher => unwatcher())
|
|
674
|
+
})
|
|
675
|
+
watchedProperties.clear()
|
|
676
|
+
componentInstances.clear()
|
|
518
677
|
})
|
|
519
678
|
|
|
520
679
|
watch(
|
|
@@ -570,7 +729,12 @@ export default defineComponent({
|
|
|
570
729
|
onDragEnter,
|
|
571
730
|
onDragLeave,
|
|
572
731
|
onDrop,
|
|
573
|
-
onDragEnd
|
|
732
|
+
onDragEnd,
|
|
733
|
+
setupComponentWatching,
|
|
734
|
+
cleanupComponentWatching,
|
|
735
|
+
handleComponentRef,
|
|
736
|
+
getReactiveTabTitle,
|
|
737
|
+
triggerTabUpdate
|
|
574
738
|
}
|
|
575
739
|
}
|
|
576
740
|
})
|
|
@@ -105,14 +105,6 @@ function insertTab(tabs: TabRecord[], tab: TabRecord, position: 'last' | 'next',
|
|
|
105
105
|
const exists = tabs.find(item => item.id === tab.id)
|
|
106
106
|
if (exists) return
|
|
107
107
|
|
|
108
|
-
if (position === 'next' && referenceId) {
|
|
109
|
-
const idx = tabs.findIndex(item => item.id === referenceId)
|
|
110
|
-
if (idx > -1) {
|
|
111
|
-
tabs.splice(idx + 1, 0, tab)
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
108
|
tabs.push(tab)
|
|
117
109
|
}
|
|
118
110
|
|
|
@@ -211,7 +203,14 @@ export function createRouterTabs(
|
|
|
211
203
|
|
|
212
204
|
function fallbackAfterClose(closedId: string): RouteLocationRaw | null {
|
|
213
205
|
const idx = tabs.findIndex(item => item.id === closedId)
|
|
214
|
-
|
|
206
|
+
if (idx === -1) return options.defaultRoute
|
|
207
|
+
|
|
208
|
+
// Priority: next tab -> previous tab -> first available tab
|
|
209
|
+
const nextTab = tabs[idx + 1] // Next tab (after the one being closed)
|
|
210
|
+
const prevTab = tabs[idx - 1] // Previous tab
|
|
211
|
+
const firstTab = tabs.find(tab => tab.id !== closedId) // First available tab (excluding the one being closed)
|
|
212
|
+
|
|
213
|
+
const candidate = nextTab || prevTab || firstTab
|
|
215
214
|
if (candidate) return candidate.to
|
|
216
215
|
return options.defaultRoute
|
|
217
216
|
}
|
|
@@ -222,15 +221,20 @@ export function createRouterTabs(
|
|
|
222
221
|
throw new Error('[RouterTabs] Unable to close the final tab when keepLastTab is true.')
|
|
223
222
|
}
|
|
224
223
|
|
|
224
|
+
// Calculate fallback route BEFORE removing the tab
|
|
225
|
+
const isClosingActiveTab = activeId.value === id
|
|
226
|
+
const shouldRedirect = isClosingActiveTab && closeOptions.redirect !== null
|
|
227
|
+
const fallbackRoute = shouldRedirect ? (closeOptions.redirect ?? fallbackAfterClose(id)) : null
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
225
231
|
await removeTab(id, { force: closeOptions.force })
|
|
226
232
|
|
|
233
|
+
// Only skip redirect if explicitly set to null
|
|
227
234
|
if (closeOptions.redirect === null) return
|
|
228
235
|
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
if (redirect) await router.replace(redirect)
|
|
232
|
-
} else if (closeOptions.redirect) {
|
|
233
|
-
await router.replace(closeOptions.redirect)
|
|
236
|
+
if (shouldRedirect && fallbackRoute) {
|
|
237
|
+
await router.replace(fallbackRoute)
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|
package/lib/index.ts
CHANGED
|
@@ -33,6 +33,18 @@ export {
|
|
|
33
33
|
setRouterTabsPrimary
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export {
|
|
37
|
+
useReactiveTab,
|
|
38
|
+
useLoadingTab,
|
|
39
|
+
useNotificationTab,
|
|
40
|
+
useStatusTab
|
|
41
|
+
} from './useReactiveTab'
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
ReactiveTabState,
|
|
45
|
+
ReactiveTabReturn
|
|
46
|
+
} from './useReactiveTab'
|
|
47
|
+
|
|
36
48
|
import "./scss/index.scss";
|
|
37
49
|
|
|
38
50
|
const plugin: Plugin = {
|
package/lib/scss/index.scss
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
--router-tab-header-height: #{$router-tab-header-height};
|
|
13
13
|
--router-tab-padding: #{$router-tab-padding};
|
|
14
14
|
--router-tab-font-size: #{$router-tab-font-size};
|
|
15
|
+
--router-tab-transition: #{$router-tab-transition};
|
|
16
|
+
|
|
17
|
+
// Colors (will be overridden by theme system)
|
|
18
|
+
--router-tab-primary: #{$router-tab-primary};
|
|
19
|
+
--router-tab-background: #{$router-tab-bg-light};
|
|
20
|
+
--router-tab-text: #{$router-tab-text-light};
|
|
21
|
+
--router-tab-border: #{$router-tab-border-light};
|
|
22
|
+
--router-tab-header-bg: #{$router-tab-bg-light};
|
|
23
|
+
--router-tab-active-background: #{$router-tab-primary};
|
|
24
|
+
--router-tab-active-text: #ffffff;
|
|
25
|
+
--router-tab-active-border: #{$router-tab-primary};
|
|
26
|
+
--router-tab-icon-color: #475569;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
// =============================================================================
|
package/lib/theme.ts
CHANGED
|
@@ -46,7 +46,7 @@ const defaultColors: ColorStyle = {
|
|
|
46
46
|
activeText: "#ffffff",
|
|
47
47
|
activeBorder: "#034960",
|
|
48
48
|
|
|
49
|
-
headerBackground: "#
|
|
49
|
+
headerBackground: "#ffffff",
|
|
50
50
|
|
|
51
51
|
buttonBackground: "#f8fafc",
|
|
52
52
|
buttonColor: "#034960",
|
|
@@ -78,7 +78,6 @@ const defaultDarkColor: ColorStyle = {
|
|
|
78
78
|
|
|
79
79
|
function applyPrimary(color: ColorStyle) {
|
|
80
80
|
if (typeof document === 'undefined') return
|
|
81
|
-
console.log('applyPrimary', color)
|
|
82
81
|
document.documentElement.style.setProperty('--router-tab-primary', color.primary ?? defaultColors.primary)
|
|
83
82
|
document.documentElement.style.setProperty('--router-tab-header-bg', color.headerBackground ?? defaultColors.headerBackground)
|
|
84
83
|
document.documentElement.style.setProperty('--router-tab-background', color.background ?? defaultColors.background)
|
|
@@ -91,6 +90,7 @@ function applyPrimary(color: ColorStyle) {
|
|
|
91
90
|
document.documentElement.style.setProperty('--router-tab-active-button-color', color.activeButtonColor ?? defaultColors.activeButtonColor)
|
|
92
91
|
document.documentElement.style.setProperty('--router-tab-button-background', color.buttonBackground ?? defaultColors.buttonBackground)
|
|
93
92
|
document.documentElement.style.setProperty('--router-tab-active-button-background', color.activeButtonBackground ?? defaultColors.activeButtonBackground)
|
|
93
|
+
document.documentElement.style.setProperty('--router-tab-icon-color', color.iconColor ?? defaultColors.iconColor)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function applyStyle(style: 'light' | 'dark' | 'system') {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface ReactiveTabState {
|
|
4
|
+
title?: string | Ref<string> | ComputedRef<string>
|
|
5
|
+
icon?: string | Ref<string> | ComputedRef<string>
|
|
6
|
+
closable?: boolean | Ref<boolean> | ComputedRef<boolean>
|
|
7
|
+
meta?: any | Ref<any> | ComputedRef<any>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ReactiveTabReturn {
|
|
11
|
+
routeTabTitle: Ref<string> | ComputedRef<string>
|
|
12
|
+
routeTabIcon: Ref<string> | ComputedRef<string>
|
|
13
|
+
routeTabClosable: Ref<boolean> | ComputedRef<boolean>
|
|
14
|
+
routeTabMeta: Ref<any> | ComputedRef<any>
|
|
15
|
+
updateTitle: (title: string) => void
|
|
16
|
+
updateIcon: (icon: string) => void
|
|
17
|
+
updateClosable: (closable: boolean) => void
|
|
18
|
+
updateMeta: (meta: any) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Composable for managing reactive tab properties
|
|
23
|
+
* RouterTab will automatically watch these properties and update the tab accordingly
|
|
24
|
+
*/
|
|
25
|
+
export function useReactiveTab(initialState: ReactiveTabState = {}): ReactiveTabReturn {
|
|
26
|
+
// Create reactive references
|
|
27
|
+
const tabTitle = ref(initialState.title || 'Untitled')
|
|
28
|
+
const tabIcon = ref(initialState.icon || 'mdi-tab')
|
|
29
|
+
const tabClosable = ref(initialState.closable !== false)
|
|
30
|
+
const tabMeta = ref(initialState.meta || {})
|
|
31
|
+
|
|
32
|
+
// Convert to computed if needed
|
|
33
|
+
const routeTabTitle = typeof initialState.title === 'function'
|
|
34
|
+
? computed(initialState.title as () => string)
|
|
35
|
+
: tabTitle
|
|
36
|
+
|
|
37
|
+
const routeTabIcon = typeof initialState.icon === 'function'
|
|
38
|
+
? computed(initialState.icon as () => string)
|
|
39
|
+
: tabIcon
|
|
40
|
+
|
|
41
|
+
const routeTabClosable = typeof initialState.closable === 'function'
|
|
42
|
+
? computed(initialState.closable as () => boolean)
|
|
43
|
+
: tabClosable
|
|
44
|
+
|
|
45
|
+
const routeTabMeta = typeof initialState.meta === 'function'
|
|
46
|
+
? computed(initialState.meta as () => any)
|
|
47
|
+
: tabMeta
|
|
48
|
+
|
|
49
|
+
// Update functions
|
|
50
|
+
const updateTitle = (title: string) => {
|
|
51
|
+
if ('value' in tabTitle) {
|
|
52
|
+
tabTitle.value = title
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const updateIcon = (icon: string) => {
|
|
57
|
+
if ('value' in tabIcon) {
|
|
58
|
+
tabIcon.value = icon
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const updateClosable = (closable: boolean) => {
|
|
63
|
+
if ('value' in tabClosable) {
|
|
64
|
+
tabClosable.value = closable
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updateMeta = (meta: any) => {
|
|
69
|
+
if ('value' in tabMeta) {
|
|
70
|
+
tabMeta.value = meta
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
routeTabTitle,
|
|
76
|
+
routeTabIcon,
|
|
77
|
+
routeTabClosable,
|
|
78
|
+
routeTabMeta,
|
|
79
|
+
updateTitle,
|
|
80
|
+
updateIcon,
|
|
81
|
+
updateClosable,
|
|
82
|
+
updateMeta
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Preset for loading state tabs
|
|
88
|
+
*/
|
|
89
|
+
export function useLoadingTab(loadingState: Ref<boolean>, baseTitle = 'Page') {
|
|
90
|
+
return useReactiveTab({
|
|
91
|
+
title: computed(() => loadingState.value ? 'Loading...' : baseTitle),
|
|
92
|
+
icon: computed(() => loadingState.value ? 'mdi-loading mdi-spin' : 'mdi-page'),
|
|
93
|
+
closable: computed(() => !loadingState.value)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Preset for notification-aware tabs
|
|
99
|
+
*/
|
|
100
|
+
export function useNotificationTab(
|
|
101
|
+
count: Ref<number>,
|
|
102
|
+
baseTitle = 'Page',
|
|
103
|
+
baseIcon = 'mdi-page'
|
|
104
|
+
) {
|
|
105
|
+
return useReactiveTab({
|
|
106
|
+
title: computed(() => count.value > 0 ? `${baseTitle} (${count.value})` : baseTitle),
|
|
107
|
+
icon: computed(() => count.value > 0 ? 'mdi-bell-badge' : baseIcon)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Preset for status-aware tabs
|
|
113
|
+
*/
|
|
114
|
+
export function useStatusTab(
|
|
115
|
+
status: Ref<'normal' | 'loading' | 'error' | 'success'>,
|
|
116
|
+
baseTitle = 'Page'
|
|
117
|
+
) {
|
|
118
|
+
const statusConfig = {
|
|
119
|
+
normal: { suffix: '', icon: 'mdi-page' },
|
|
120
|
+
loading: { suffix: ' - Loading', icon: 'mdi-loading mdi-spin' },
|
|
121
|
+
error: { suffix: ' - Error', icon: 'mdi-alert' },
|
|
122
|
+
success: { suffix: ' - Success', icon: 'mdi-check-circle' }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return useReactiveTab({
|
|
126
|
+
title: computed(() => baseTitle + statusConfig[status.value].suffix),
|
|
127
|
+
icon: computed(() => statusConfig[status.value].icon),
|
|
128
|
+
closable: computed(() => status.value !== 'loading')
|
|
129
|
+
})
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue3-router-tab",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "vite build"
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@vitejs/plugin-vue": "^6.0.1",
|
|
@@ -46,7 +47,11 @@
|
|
|
46
47
|
],
|
|
47
48
|
"license": "MIT",
|
|
48
49
|
"homepage": "https://github.com/anilshr25/vue3-router-tab/#readme",
|
|
49
|
-
"description": "
|
|
50
|
+
"description": "Vue 3 Router Tabs component: tabbed navigation for Vue Router with persistence, context menu, drag-and-drop reordering, theming, and reactive page-driven tab titles.",
|
|
51
|
+
"sideEffects": [
|
|
52
|
+
"dist/vue3-router-tab.css",
|
|
53
|
+
"lib/scss/index.scss"
|
|
54
|
+
],
|
|
50
55
|
"repository": {
|
|
51
56
|
"type": "git",
|
|
52
57
|
"url": "https://github.com/anilshr25/vue3-router-tab.git"
|