Files
2026-11-04 12:30:17 +08:00

1 line
17 KiB
JavaScript

"use strict";"require baseclass";"require ui";"require poll";"require rpc";const RT=window.L,RENDER_TIMEOUT=15e3,PATCH_ATTR="data-luci-patch",NODE_CSS_ATTR="data-luci-node-css",SHELL_ATTR="data-luci-shell",PROGRESS_DELAY=150,INTENT_DELAY=65,WARM_CONCURRENCY=2,WARM_TIMEOUT=5e3,INSTANTIATE=/instantiateView\(\s*['"]([^'"]+)['"]/,TITLE_SLOT="\0",supported=()=>"object"==typeof navigation&&null!==navigation&&"function"==typeof navigation.addEventListener&&"function"==typeof NavigateEvent&&"intercept"in NavigateEvent.prototype,nodeWeight=e=>Math.min(e.order??9999,9999)+(e.auth?.login?1e4:0);function firstChild(e){let t=null;for(const[s,n]of Object.entries(e.children??{}))if(n.satisfied&&n.title&&"object"==typeof n.action)if("firstchild"===n.action?.type){if(t&&nodeWeight(t.node)<=nodeWeight(n))continue;firstChild(n)&&(t={name:s,node:n})}else n.firstchild_ineligible||(!t||nodeWeight(t.node)>nodeWeight(n))&&(t={name:s,node:n});return t}function resolve(e,t){let s=e,n=[],i=[],r=0;for(let o=0;o<32;o++){for(;r<t.length;){const e=s.children?.[t[r]];if(e&&!1===e.satisfied)return null;if(!e){if(!s.wildcard)return null;i=t.slice(r),r=t.length;break}s=e,n.push(t[r++])}const o=s.action?.type;if("alias"===o)t=[...String(s.action.path??"").split("/").filter(Boolean),...i],s=e,n=[],i=[],r=0;else{if("firstchild"!==o)return{node:s,path:n,args:i,request:t};{const e=firstChild(s);if(!e)return null;s=e.node,n.push(e.name)}}}return null}function readonlyAlong(e,t){const s=[];let n=e;for(const e of t)n=n?.children?.[e],n?.depends?.acl?.length&&s.push(n);return s.length>0&&s.every(e=>!0===e.readonly)}const actionOf=(e,t)=>t.length&&"object"==typeof e?.wildcardaction?e.wildcardaction:e?.action;function viewClass(e){return"view"===e?.type&&e.path?`view.${e.path.replace(/\//g,".")}`:null}const BUILTIN_MODULES=new Set(["baseclass","dom","poll","request","session","view"]),MODULE_NAME=/^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$/,PRAGMA=/(?:\s|;|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*(["'])(use[ \t]+strict|require[ \t]+[^"'\r\n]+)\1/y;function pragmaDeps(e){const t=e.slice(0,4096),s=[];PRAGMA.lastIndex=0;for(let e;e=PRAGMA.exec(t);){const t=/^require[ \t]+(\S+)(?:[ \t]+as[ \t]+[a-zA-Z_]\S*)?$/.exec(e[2]);if(t)MODULE_NAME.test(t[1])&&s.push(t[1]);else if(!/^use[ \t]+strict$/.test(e[2]))break}return s}const moduleUrl=e=>`${RT.env.base_url}/${e.replace(/\./g,"/")}.js${RT.env.resource_version?`?v=${RT.env.resource_version}`:""}`,TITLE_SEPARATOR=/\s*[|\-–—:·]\s*\0|\0\s*[|\-–—:·]\s*/,patchPrefixes=e=>{const t=[];let s=null;for(const n of e)t.push(s=null==s?n:`${s}-${n}`);return t},patchAssetUrl=e=>{const t=document.body.dataset.assetVersion;return t?`${e}?v=${t}`:e},installedPatches=()=>(document.body.dataset.patches??"").split(/\s+/).filter(Boolean),patchRegistry=()=>window.luciPatches??={};let livePatches=new Set,pendingPatches=[];function applyPatches(e){const t=new Set(patchPrefixes(e)),s=RT.env.media,n=[];for(const e of installedPatches()){const i=e.replace(/\.(css|js)$/,""),r=t.has(i);if(e.endsWith(".css")){let e=document.querySelector(`link[${PATCH_ATTR}="${i}"]`);!e&&r?(e=E("link",{rel:"stylesheet",href:patchAssetUrl(`${s}/patches/${i}.css`),[PATCH_ATTR]:i}),document.head.appendChild(e)):e&&(e.disabled=!r)}else r&&n.push(i)}return n}function mountPatches(e){const t=patchRegistry();livePatches=new Set(e);for(const s of e)if(document.querySelector(`script[${PATCH_ATTR}="${s}"]`))t[s]?.mount?.();else{const e=E("script",{src:patchAssetUrl(`${RT.env.media}/patches/${s}.js`),[PATCH_ATTR]:s});e.addEventListener("load",()=>{livePatches.has(s)||patchRegistry()[s]?.unmount?.()}),document.head.appendChild(e)}}function unmountPatches(){livePatches=new Set;const e=patchRegistry();for(const t in e)e[t].unmount?.()}return document.addEventListener("luci-navigate",e=>{unmountPatches(),pendingPatches=applyPatches(e.detail.request)}),document.addEventListener("luci-navigated",()=>{mountPatches(pendingPatches),pendingPatches=[]}),baseclass.extend({__init__(){if(!supported()||!RT?.env?.scriptname)return;const e=this.contract();if(e.length)return void console.warn(`luci-router: luci-base surface missing, staying MPA: ${e.join(", ")}`);this.gen=0,this.seen=new Set,this.warmed=new Map,this.warmQueue=[],this.warmActive=0,this.warmEpoch=0,this.inflight=Promise.resolve(),this.intervals=new Set,this.scrolls=new Map,this.knownSheets=new WeakSet(this.sheets().filter(e=>e.hasAttribute(SHELL_ATTR)||e.hasAttribute(PATCH_ATTR)||e.hasAttribute(NODE_CSS_ATTR))),this.hookIntervals(),this.hookListeners(),this.hookSession(),this.hookVisibility();const t=RT.env.media.split("/").filter(Boolean).pop();Promise.all([ui.menu.load(),RT.require(`menu-${t}`).catch(()=>null)]).then(([e,t])=>{this.tree=e,this.menu=t;const s=this.initialRoute();s&&(document.querySelector('script[type="speculationrules"]')?.remove(),this.status=document.body.appendChild(E("div",{id:"luci-nav-status",role:"status","aria-live":"polite","aria-atomic":"true"})),navigation.addEventListener("navigate",e=>this.onNavigate(e)),document.addEventListener("pointerover",e=>this.onIntent(e)),document.addEventListener("pointerdown",e=>this.onIntent(e)),document.addEventListener("focusin",e=>this.onIntent(e)),this.watchInitialRender(s))})},contract(){const e={L:RT,rpc:rpc,poll:poll,ui:ui,window:window};return["L.view","L.require","L.dom.content","L.env.base_url","L.env.resource","L.env.media","L.Request.addInterceptor","L.uci.load","L.uci.unload","L.uci.state","rpc.addInterceptor","poll.queue","poll.start","poll.stop","poll.active","ui.menu.load","ui.hideModal","ui.hideIndicator","window.E"].filter(t=>null==(t=>t.split(".").reduce((e,t)=>null==e?e:e[t],e))(t))},emit(e,t){document.dispatchEvent(new CustomEvent(e,{detail:t}))},hookVisibility(){document.addEventListener("visibilitychange",()=>{document.hidden?(this.pausedByVisibility=poll.active(),this.pausedByVisibility&&poll.stop()):this.pausedByVisibility&&(this.pausedByVisibility=!1,this.expired||poll.start())})},progressStart(){this.pending=(this.pending??0)+1,clearTimeout(this.progressTimer),this.progressTimer=setTimeout(()=>{this.bar||(this.bar=document.body.appendChild(E("div",{id:"luci-nav-progress","aria-hidden":"true"})),this.bar.offsetWidth,this.trickle(10))},150)},trickle(e){this.bar&&(this.bar.style.width=`${e}%`,this.trickleTimer=setTimeout(()=>this.trickle(e+(100-e)/30),300))},progressEnd(){if(--this.pending>0)return;clearTimeout(this.progressTimer),clearTimeout(this.trickleTimer);const e=this.bar;e&&(this.bar=null,e.dataset.state="done",e.style.width="100%",setTimeout(()=>e.remove(),300))},sheets(){const e=document.getElementById("view");return[...document.querySelectorAll('style, link[rel~="stylesheet"]')].filter(t=>!e?.contains(t))},poisoned(){return this.sheets().some(e=>!this.knownSheets.has(e)&&!e.hasAttribute(PATCH_ATTR)&&!e.hasAttribute(NODE_CSS_ATTR))},hookIntervals(){const e=window.setInterval,t=window.clearInterval,s=this.intervals;window.setInterval=function(...t){const n=e.apply(window,t);return s.add(n),n},window.clearInterval=function(e){return s.delete(e),t.call(window,e)},this.nativeClearInterval=t},hookListeners(){const e=this;for(const t of[window,document]){const s=t.addEventListener,n=t.removeEventListener;t.addEventListener=function(n,i,r){return e.renderWindow?.push({target:t,type:n,fn:i,opts:r}),s.call(this,n,i,r)},t.removeEventListener=function(t,s,i){return e.renderWindow&&(e.renderWindow=e.renderWindow.filter(e=>e.fn!==s||e.type!==t)),n.call(this,t,s,i)}}this.pageListeners=[],this.coldListeners=new Map},hookSession(){RT.Request?.addInterceptor?.(e=>{this.loginRequired(e)&&(this.expired=!0)}),rpc?.addInterceptor?.((e,t)=>{this.sessionGone(e,t)&&(this.expired=!0)})},loginRequired:e=>403===e?.status&&"yes"===e.headers?.get("X-LuCI-Login-Required"),sessionGone(e,t){if("session"!==t?.object||"access"!==t?.method||!e)return!1;if(e.error)return!0;const s=Array.isArray(e.result)?e.result[1]:e.result;return!1===s?.access},reloads:e=>e.split("#")[0]===window.location.href.split("#")[0],nodespec(e){return{...e.node,readonly:readonlyAlong(this.tree,e.path)}},nodeCss:e=>"string"==typeof e.node.css&&e.node.css?e.node.css:null,titleTemplate(e){const t=e.title?_(e.title):"";return t&&document.title.includes(t)?document.title.replace(t,"\0"):null},title(e){const t=this.titleFormat;return t?e.title?t.replace("\0",_(e.title)):t.replace(TITLE_SEPARATOR,"").replace("\0",""):document.title},scroller(){const e=document.getElementById("maincontent");if(!e)return null;const t=getComputedStyle(e).overflowY;return"auto"===t||"scroll"===t?e:null},openRenderWindow(){this.renderWindow=[]},closeRenderWindow(e,t){const s=this.renderWindow??[];if(this.renderWindow=null,t)return this.coldListeners.set(e,s),void(this.pageListeners=[]);const n=this.coldListeners.get(e)??[];for(const e of s)for(const t of n.filter(t=>t.target===e.target&&t.type===e.type))t.target.removeEventListener(t.type,t.fn,t.opts);this.coldListeners.set(e,n.filter(e=>!s.some(t=>t.target===e.target&&t.type===e.type))),this.pageListeners=s},clearViewListeners(){for(const e of this.pageListeners)e.target.removeEventListener(e.type,e.fn,e.opts);this.pageListeners=[]},clearViewIntervals(){for(const e of this.intervals)e!==poll.timer&&(this.nativeClearInterval.call(window,e),this.intervals.delete(e))},segsFromURL(e){const t=new URL(e,window.location.href),s=RT.env.scriptname;return t.origin===window.location.origin&&t.pathname.startsWith(s)?t.pathname.slice(s.length).split("/").filter(Boolean).map(decodeURIComponent):null},route(e,{intent:t=!1}={}){const s=this.segsFromURL(e),n=s&&this.resolve(s);if(!n)return null;const i=actionOf(n.node,n.args),r=viewClass(i),o=i?.path,a="template"===i?.type?new URL(e,window.location.href):null,l=a&&`${o}\0${a.pathname}`,c=a&&!a.search&&!n.args.length&&(t?!this.unservable?.has(l):this.templates?.has(l));return r||c?{segs:s,className:r,template:c&&o,shellKey:l,url:e,...n}:null},template(e){return this.templates??=new Map,this.templates.has(e.shellKey)?Promise.resolve(this.templates.get(e.shellKey)):(this.templateLoads??=new Map,this.templateLoads.has(e.shellKey)||this.templateLoads.set(e.shellKey,fetch(e.url,{credentials:"same-origin"}).then(e=>e.text()).then(t=>{const s=(new DOMParser).parseFromString(t,"text/html").getElementById("maincontent"),n=s?.querySelector("#tabmenu"),i=s?.querySelector(":scope > footer"),r=[];for(let e=n?.nextSibling;e&&e!==i;e=e.nextSibling)r.push(e);return this.rememberShell(e.shellKey,r)}).finally(()=>this.templateLoads.delete(e.shellKey))),this.templateLoads.get(e.shellKey))},rememberShell(e,t){const s={className:null,nodes:[],scripts:[]};for(const e of t)if("SCRIPT"===e.nodeName){const t=e.textContent.match(INSTANTIATE);t?s.className=`view.${t[1].replace(/\//g,".")}`:e.src||!e.textContent.trim()||/new LuCI\(/.test(e.textContent)||s.scripts.push(e.textContent)}else if(1===e.nodeType&&"view"===e.id)s.nodes.push(E("div",{id:"view"}));else{if(1===e.nodeType&&e.querySelector("#view")){s.className=null;break}s.nodes.push(document.importNode(e,!0))}return s.className?this.templates.set(e,s):(this.unservable??=new Set).add(e),s.className?s:null},resolve(e){return resolve(this.tree,e)},moduleDeps:e=>pragmaDeps(e),initialRoute(){const e=this.route(window.location.href,{intent:!0});if(!e)return null;if(e.template){this.templates??=new Map;const t=this.rememberShell(e.shellKey,this.region().nodes);if(!t)return null;e.className=t.className}return this.seen.add(e.className),this.titleFormat=this.titleTemplate(e.node),e},watchInitialRender(e){const t=document.getElementById("view");t&&(this.openRenderWindow(),this.inflight=this.rendered(t).finally(()=>this.closeRenderWindow(e.className,!0)),this.inflight.catch(()=>{}))},rendered(e){const t=e=>e.querySelector(":scope > :not(.spinning):not(script)")||0===e.childElementCount&&e.dataset.luciStarted;return t(e)?Promise.resolve():new Promise((s,n)=>{const i=setTimeout(()=>o(n,new Error("view did not render in time")),15e3),r=new MutationObserver(()=>{t(e)&&o(s)});function o(e,t){clearTimeout(i),r.disconnect(),e(t)}e.dataset.luciStarted="1",r.observe(e,{childList:!0})})},onIntent(e){const t=e.target?.closest?.("a[href]");if("pointerover"===e.type&&t?.contains(e.relatedTarget))return;if(clearTimeout(this.intentTimer),"pointerdown"===e.type||!t||t.target||t.hasAttribute("download")||!this.canWarm())return;if("focusin"===e.type&&!t.matches(":focus-visible"))return;const s=this.route(t.href,{intent:!0});if(!s)return;const n=()=>{this.canWarm()&&(s.template?this.template(s).then(e=>e&&this.warm(e.className)).catch(()=>{}):this.warm(s.className))};"pointerover"===e.type||s.template?(s.template||this.warm(s.className,!1),this.intentTimer=setTimeout(n,65)):n()},canWarm(){const e=navigator.connection;return!(this.expired||this.pending||document.hidden||e?.saveData||/^(slow-)?2g$/.test(e?.effectiveType??""))},moduleLoaded(e){const t=e.split(".").reduce((e,t)=>e?.[t],RT);return BUILTIN_MODULES.has(e)||!!t||this.seen.has(e)},warm(e,t=!0){if("string"!=typeof e||!MODULE_NAME.test(e)||this.moduleLoaded(e)||!this.canWarm())return Promise.resolve();const s=this.warmed.get(e);if(s)return t&&(s.expand=!0,this.expandWarm(s)),s.promise;const n={name:e,expand:t};return n.promise=new Promise(e=>n.finish=e),this.warmed.set(e,n),this.warmQueue.push(n),this.drainWarmQueue(),n.promise},expandWarm(e){if(e.expand&&e.expandedEpoch!==this.warmEpoch&&e.deps&&this.canWarm()&&!this.moduleLoaded(e.name)){e.expandedEpoch=this.warmEpoch;for(const t of e.deps)this.warm(t)}},drainWarmQueue(){for(;this.warmActive<2&&this.warmQueue.length;){const e=this.warmQueue.shift(),{name:t,finish:s}=e,n=this.moduleLoaded(t);if(!this.canWarm()||n){n||this.warmEpoch++,this.warmed.delete(t),s();continue}this.warmActive++;const i=new AbortController,r=setTimeout(()=>i.abort(),5e3);fetch(moduleUrl(t),{priority:"low",credentials:"same-origin",signal:i.signal}).then(e=>{if(!e.ok)throw new Error("module prewarm failed");return e.text()}).then(t=>{e.deps=pragmaDeps(t),this.expandWarm(e)}).catch(()=>{this.warmed.delete(t),this.warmEpoch++}).finally(()=>{clearTimeout(r),this.warmActive--,s(),this.drainWarmQueue()})}},onNavigate(e){if(this.bypass)return void(this.bypass=!1);if(!e.canIntercept||e.hashChange||null!==e.downloadRequest||e.formData||"reload"===e.navigationType||this.reloads(e.destination.url))return;const t=this.route(e.destination.url);if(!t||this.expired||this.poisoned())return;const s=this.scroller();s&&navigation.currentEntry&&this.scrolls.set(navigation.currentEntry.key,s.scrollTop),e.intercept({focusReset:"manual",scroll:"after-transition",handler:()=>this.navigate(t,e)})},async navigate(e,t){const s=++this.gen,n=this.inflight;let i;this.inflight=new Promise(e=>i=e),this.progressStart();try{if(await n,s!==this.gen||t.signal.aborted)return;const i=e.template?await this.template(e):null;if(i&&(e.className=i.className),s!==this.gen||t.signal.aborted)return;if(this.teardown(),this.setEnvironment(e),this.menu?.syncRoute?.(),this.emit("luci-navigate",e),await this.flushUci(),s!==this.gen||t.signal.aborted)return;this.applyNodeCss(this.nodeCss(e));const r=this.stage(i),o=this.rendered(r),a=!this.seen.has(e.className);this.seen.add(e.className),this.openRenderWindow();try{const t=await RT.require(e.className);if(!(t instanceof RT.view))throw new TypeError(`${e.className} is not a LuCI.view`);a||new t.constructor,await o}finally{this.closeRenderWindow(e.className,a)}if(s!==this.gen)return;await this.commit(r,t),this.emit("luci-navigated",e),this.status.textContent=document.title,document.getElementById("maincontent")?.focus({preventScroll:!0})}catch(e){if(console.error("luci-router:",e),s!==this.gen)return;this.bypass=!0,window.location.href=t.destination.url,await new Promise(()=>{})}finally{this.progressEnd(),i()}},teardown(){poll.queue.length=0,poll.stop(),poll.start(),ui.hideIndicator("poll-status"),this.clearViewIntervals(),this.clearViewListeners(),ui.hideModal(),this.menu?.closeSurfaces?.()},async flushUci(){const e=RT.uci;if(!e?.state)return;const t=Object.keys({...e.state.values,...e.loaded});if(t.length&&e.unload(t),RT.network){const t=["network","luci"];RT.hasSystemFeature?.("wifi")&&t.push("wireless"),await e.load(t)}},setEnvironment(e){RT.env.requestpath=e.request,RT.env.dispatchpath=e.path,RT.env.pathinfo=`/${e.segs.join("/")}`,RT.env.nodespec=this.nodespec(e),document.body.dataset.page=e.request.join("-"),document.title=this.title(e.node)},region(){const e=document.getElementById("maincontent"),t=document.getElementById("tabmenu"),s=e.querySelector(":scope > footer"),n=[];for(let i=t?t.nextSibling:e.firstChild;i&&i!==s;)n.push(i),i=i.nextSibling;return{main:e,start:t,end:s,nodes:n}},stage(e){const{main:t,start:s,end:n}=this.region(),i=E("div",{class:"view-staging"});for(const e of t.querySelectorAll(":scope > #view"))e.classList.add("view-leaving");if(e){for(const t of e.nodes)i.appendChild(t.cloneNode(!0));for(const t of e.scripts)i.appendChild(E("script",{},t))}else i.appendChild(E("div",{id:"view"}));return t.insertBefore(i,s?s.nextSibling:n??null),i.querySelector("#view")},commit(e,t){const s=e.parentNode,n=()=>{for(const e of this.region().nodes)e!==s&&(1===e.nodeType&&RT.dom.content(e,null),e.remove());s.replaceWith(...s.childNodes);const e=this.scroller();e&&(e.scrollTop="traverse"===t.navigationType?this.scrolls.get(t.destination.key)??0:0)},i=window.matchMedia("(prefers-reduced-motion: reduce)").matches;if(document.startViewTransition&&!i)return document.startViewTransition(n).updateCallbackDone;n()},applyNodeCss(e){for(const t of document.querySelectorAll(`link[${NODE_CSS_ATTR}]`))t.disabled=t.getAttribute(NODE_CSS_ATTR)!==e;e&&!document.querySelector(`link[${NODE_CSS_ATTR}="${e}"]`)&&document.head.appendChild(E("link",{rel:"stylesheet",href:`${RT.env.resource}/${e}`,[NODE_CSS_ATTR]:e}))}});