Files
op-packages/luci-theme-aurora/htdocs/luci-static/resources/router-aurora.js
T

1 line
15 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,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[n,s]of Object.entries(e.children??{}))if(s.satisfied&&s.title&&"object"==typeof s.action)if("firstchild"===s.action?.type){if(t&&nodeWeight(t.node)<=nodeWeight(s))continue;firstChild(s)&&(t={name:n,node:s})}else s.firstchild_ineligible||(!t||nodeWeight(t.node)>nodeWeight(s))&&(t={name:n,node:s});return t}function resolve(e,t){let n=e,s=[],i=[],r=0;for(let o=0;o<32;o++){for(;r<t.length;){const e=n.children?.[t[r]];if(e&&!1===e.satisfied)return null;if(!e){if(!n.wildcard)return null;i=t.slice(r),r=t.length;break}n=e,s.push(t[r++])}const o=n.action?.type;if("alias"===o)t=[...String(n.action.path??"").split("/").filter(Boolean),...i],n=e,s=[],i=[],r=0;else{if("firstchild"!==o)return{node:n,path:s,args:i,request:t};{const e=firstChild(n);if(!e)return null;n=e.node,s.push(e.name)}}}return null}function readonlyAlong(e,t){const n=[];let s=e;for(const e of t)s=s?.children?.[e],s?.depends?.acl?.length&&n.push(s);return n.length>0&&n.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 PRAGMA_HEAD=/^(?:\s*["'](?:use strict|require\s[^"']+)["'];)+/,PRAGMA=/["']require\s+([^\s"']+)/g;function pragmaDeps(e){return[...(e.slice(0,4096).match(PRAGMA_HEAD)?.[0]??"").matchAll(PRAGMA)].map(e=>e[1])}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 n=null;for(const s of e)t.push(n=null==n?s:`${n}-${s}`);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)),n=RT.env.media,s=[];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(`${n}/patches/${i}.css`),[PATCH_ATTR]:i}),document.head.appendChild(e)):e&&(e.disabled=!r)}else r&&s.push(i)}return s}function mountPatches(e){const t=patchRegistry();livePatches=new Set(e);for(const n of e)if(document.querySelector(`script[${PATCH_ATTR}="${n}"]`))t[n]?.mount?.();else{const e=E("script",{src:patchAssetUrl(`${RT.env.media}/patches/${n}.js`),[PATCH_ATTR]:n});e.addEventListener("load",()=>{livePatches.has(n)||patchRegistry()[n]?.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 Set,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 n=this.initialRoute();n&&(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(n))})},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,n=this.intervals;window.setInterval=function(...t){const s=e.apply(window,t);return n.add(s),s},window.clearInterval=function(e){return n.delete(e),t.call(window,e)},this.nativeClearInterval=t},hookListeners(){const e=this;for(const t of[window,document]){const n=t.addEventListener,s=t.removeEventListener;t.addEventListener=function(s,i,r){return e.renderWindow?.push({target:t,type:s,fn:i,opts:r}),n.call(this,s,i,r)},t.removeEventListener=function(t,n,i){return e.renderWindow&&(e.renderWindow=e.renderWindow.filter(e=>e.fn!==n||e.type!==t)),s.call(this,t,n,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 n=Array.isArray(e.result)?e.result[1]:e.result;return!1===n?.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 n=this.renderWindow??[];if(this.renderWindow=null,t)return this.coldListeners.set(e,n),void(this.pageListeners=[]);const s=this.coldListeners.get(e)??[];for(const e of n)for(const t of s.filter(t=>t.target===e.target&&t.type===e.type))t.target.removeEventListener(t.type,t.fn,t.opts);this.coldListeners.set(e,s.filter(e=>!n.some(t=>t.target===e.target&&t.type===e.type))),this.pageListeners=n},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),n=RT.env.scriptname;return t.origin===window.location.origin&&t.pathname.startsWith(n)?t.pathname.slice(n.length).split("/").filter(Boolean).map(decodeURIComponent):null},route(e,{intent:t=!1}={}){const n=this.segsFromURL(e),s=n&&this.resolve(n);if(!s)return null;const i=actionOf(s.node,s.args),r=viewClass(i),o=i?.path,a="template"===i?.type&&(t?!this.unservable?.has(o):this.templates?.has(o));return r||a?{segs:n,className:r,template:a&&o,url:e,...s}:null},template(e){return this.templates??=new Map,this.templates.has(e.template)?Promise.resolve(this.templates.get(e.template)):(this.templateLoads??=new Map,this.templateLoads.has(e.template)||this.templateLoads.set(e.template,fetch(e.url,{credentials:"same-origin"}).then(e=>e.text()).then(t=>{const n=(new DOMParser).parseFromString(t,"text/html").getElementById("maincontent"),s=n?.querySelector("#tabmenu"),i=n?.querySelector(":scope > footer"),r=[];for(let e=s?.nextSibling;e&&e!==i;e=e.nextSibling)r.push(e);return this.rememberShell(e.template,r)}).finally(()=>this.templateLoads.delete(e.template))),this.templateLoads.get(e.template))},rememberShell(e,t){const n={className:null,nodes:[],scripts:[]};for(const e of t)if("SCRIPT"===e.nodeName){const t=e.textContent.match(INSTANTIATE);t?n.className=`view.${t[1].replace(/\//g,".")}`:e.src||!e.textContent.trim()||/new LuCI\(/.test(e.textContent)||n.scripts.push(e.textContent)}else if(1===e.nodeType&&"view"===e.id)n.nodes.push(E("div",{id:"view"}));else{if(1===e.nodeType&&e.querySelector("#view")){n.className=null;break}n.nodes.push(document.importNode(e,!0))}return n.className?this.templates.set(e,n):(this.unservable??=new Set).add(e),n.className?n: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.template,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((n,s)=>{const i=setTimeout(()=>o(s,new Error("view did not render in time")),15e3),r=new MutationObserver(()=>{t(e)&&o(n)});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(!t||t.target||t.hasAttribute("download"))return;const n=this.route(t.href,{intent:!0});n&&(n.template?this.template(n).then(e=>e&&this.warm(e.className)).catch(()=>{}):this.seen.has(n.className)||this.warm(n.className))},warm(e){this.warmed.has(e)||(this.warmed.add(e),fetch(moduleUrl(e),{priority:"low",credentials:"same-origin"}).then(e=>e.ok?e.text():"").then(e=>{for(const t of pragmaDeps(e))t.includes(".")&&this.warm(t)}).catch(()=>{}))},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 n=this.scroller();n&&navigation.currentEntry&&this.scrolls.set(navigation.currentEntry.key,n.scrollTop),e.intercept({focusReset:"manual",scroll:"after-transition",handler:()=>this.navigate(t,e)})},async navigate(e,t){const n=++this.gen,s=this.inflight;let i;this.inflight=new Promise(e=>i=e),this.progressStart();try{if(await s,n!==this.gen||t.signal.aborted)return;const i=e.template?await this.template(e):null;if(i&&(e.className=i.className),n!==this.gen||t.signal.aborted)return;if(this.teardown(),this.setEnvironment(e),this.menu?.syncRoute?.(),this.emit("luci-navigate",e),await this.flushUci(),n!==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(n!==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),n!==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"),n=e.querySelector(":scope > footer"),s=[];for(let i=t?t.nextSibling:e.firstChild;i&&i!==n;)s.push(i),i=i.nextSibling;return{main:e,start:t,end:n,nodes:s}},stage(e){const{main:t,start:n,end:s}=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,n?n.nextSibling:s??null),i.querySelector("#view")},commit(e,t){const n=e.parentNode,s=()=>{for(const e of this.region().nodes)e!==n&&(1===e.nodeType&&RT.dom.content(e,null),e.remove());n.replaceWith(...n.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(s).updateCallbackDone;s()},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}))}});