|
| 1 | +importtype{ModuleBuiltinTab,ModuleCustomTab}from'~/../src/types' |
| 2 | +import{computed,watch}from'vue' |
| 3 | +import{useRouter}from'#app/composables/router' |
| 4 | +import{useEnabledTabs}from'~/composables/state-tabs' |
| 5 | + |
| 6 | +// `devframe:frame-nav` — the embedded-app half of the shared-iframe soft-nav |
| 7 | +// protocol (devframe#128 / vitejs/devtools#464). The Vite DevTools dock owns |
| 8 | +// one kept-alive iframe (the anchor); this shim announces one member dock per |
| 9 | +// DevTools tab and switches views by client-side navigation, so no per-tab |
| 10 | +// iframe/reload is needed. The shim is transport-only: it takes no hub/RPC |
| 11 | +// dependency, just `postMessage`. |
| 12 | +// |
| 13 | +// The announced set comes from `useEnabledTabs()` — the same conditional list |
| 14 | +// the SideNav used — so a tab's `show()` condition, hidden/pinned settings and |
| 15 | +// experimental gating decide whether its dock appears, and the manifest is |
| 16 | +// re-announced whenever that set changes. |
| 17 | +constCHANNEL='devframe:frame-nav' |
| 18 | +constVERSION=1 |
| 19 | +// Must match the anchor's `frameId` registered in `module-main.ts`. |
| 20 | +constFRAME_ID='nuxt:devtools' |
| 21 | + |
| 22 | +constICON_CLASS_RE=/\s.*$/ |
| 23 | +constFIRST_DASH_RE=/-/ |
| 24 | + |
| 25 | +interfaceFrameNavTab{ |
| 26 | +id: string |
| 27 | +title: string |
| 28 | +icon?: string |
| 29 | +category?: string |
| 30 | +defaultOrder?: number |
| 31 | +navTarget: {path: string} |
| 32 | +} |
| 33 | + |
| 34 | +/** The DevTools settings page, surfaced as its own dock member. */ |
| 35 | +constSETTINGS_TAB: FrameNavTab={ |
| 36 | +id: 'settings', |
| 37 | +title: 'Settings', |
| 38 | +icon: 'carbon:settings', |
| 39 | +category: '~builtin', |
| 40 | +// sort last within its sub-category (higher `order` renders earlier) |
| 41 | +defaultOrder: 1000, |
| 42 | +navTarget: {path: '/settings'}, |
| 43 | +} |
| 44 | + |
| 45 | +/** Normalise a tab icon (UnoCSS `i-carbon-foo` / `carbon-foo`) to iconify `carbon:foo`. */ |
| 46 | +functionnormalizeIcon(icon: string|undefined): string|undefined{ |
| 47 | +if(!icon) |
| 48 | +returnundefined |
| 49 | +lettoken=icon.replace(ICON_CLASS_RE,'') |
| 50 | +if(/^(?:https?:)?\/\//.test(token)||token.startsWith('/')||token.startsWith('data:')) |
| 51 | +returntoken |
| 52 | +if(token.startsWith('i-')) |
| 53 | +token=token.slice(2) |
| 54 | +if(!token.includes(':')) |
| 55 | +token=token.replace(FIRST_DASH_RE,':') |
| 56 | +returntoken |
| 57 | +} |
| 58 | + |
| 59 | +functiontabPath(tab: ModuleBuiltinTab|ModuleCustomTab): string{ |
| 60 | +return'path'intab&&tab.path ? tab.path : `/modules/custom-${tab.name}` |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * Start the frame-nav shim. No-op unless running inside an iframe. Announces the |
| 65 | + * (conditional) tab manifest, answers `navigate` with client-side navigation, |
| 66 | + * and reports `navigated` so the dock highlight follows in-app navigation. |
| 67 | + */ |
| 68 | +exportfunctionsetupFrameNav(): void{ |
| 69 | +if(typeofwindow==='undefined'||window.parent===window) |
| 70 | +return |
| 71 | + |
| 72 | +constrouter=useRouter() |
| 73 | +consttabs=useEnabledTabs() |
| 74 | + |
| 75 | +constmanifest=computed<FrameNavTab[]>(()=>[ |
| 76 | + ...tabs.value.map(tab=>({ |
| 77 | +id: tab.name, |
| 78 | +title: tab.title??tab.name, |
| 79 | +icon: normalizeIcon(tab.icon), |
| 80 | +defaultOrder: 'defaultOrder'intab ? tab.defaultOrder : undefined, |
| 81 | +// Mirror `getCategorizedTabs`: an uncategorised tab belongs to `app`, so |
| 82 | +// the `Nuxt` group's `categoryOrder` weights apply to it. |
| 83 | +category: tab.category||'app', |
| 84 | +navTarget: {path: tabPath(tab)}, |
| 85 | +})), |
| 86 | +SETTINGS_TAB, |
| 87 | +]) |
| 88 | + |
| 89 | +functioncurrentTabId(): string|undefined{ |
| 90 | +constpath=router.currentRoute.value.path |
| 91 | +returnmanifest.value.find(entry=>entry.navTarget.path===path)?.id |
| 92 | +} |
| 93 | + |
| 94 | +functionpost(message: Record<string,unknown>){ |
| 95 | +window.parent.postMessage({channel: CHANNEL,v: VERSION,frameId: FRAME_ID,from: 'frame', ...message},'*') |
| 96 | +} |
| 97 | + |
| 98 | +functionannounce(type: 'ready'|'manifest'){ |
| 99 | +post({ type,tabs: manifest.value,current: currentTabId()}) |
| 100 | +} |
| 101 | + |
| 102 | +window.addEventListener('message',(ev: MessageEvent)=>{ |
| 103 | +constdata=ev.data |
| 104 | +if(!data||data.channel!==CHANNEL||data.v!==VERSION||data.frameId!==FRAME_ID||data.from!=='host') |
| 105 | +return |
| 106 | +if(data.type==='hello'){ |
| 107 | +announce('ready') |
| 108 | +} |
| 109 | +elseif(data.type==='navigate'){ |
| 110 | +constpath=data.navTarget?.path |
| 111 | +if(typeofpath==='string') |
| 112 | +router.push(path).then(()=>post({type: 'navigated',tabId: data.tabId})) |
| 113 | +} |
| 114 | +}) |
| 115 | + |
| 116 | +// Announce proactively in case the host attached before this shim loaded. |
| 117 | +announce('ready') |
| 118 | + |
| 119 | +// Re-announce when the conditional tab set changes (show()/settings/etc). |
| 120 | +watch(()=>manifest.value.map(entry=>entry.id).join(','),()=>announce('manifest')) |
| 121 | + |
| 122 | +// Report in-app navigation so the dock highlight follows. |
| 123 | +watch(()=>router.currentRoute.value.path,()=>{ |
| 124 | +constid=currentTabId() |
| 125 | +if(id) |
| 126 | +post({type: 'navigated',tabId: id}) |
| 127 | +}) |
| 128 | +} |
0 commit comments