diff --git a/dist/router.js b/dist/router.js index 92a97d9..ef0a0a2 100644 --- a/dist/router.js +++ b/dist/router.js @@ -1 +1 @@ -const waitFor=(t=(()=>!0),e=1e4)=>new Promise(((r,o)=>{const check=()=>{const s=t();s?r(s):(e-=100)<0?o(new Error("Timed out waiting!")):setTimeout(check,100)};setTimeout(check,100)}));function stripTrailingSlash(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t}function isPromise(t){return("object"==typeof t||"function"==typeof t)&&"function"==typeof t.then}function hasKey(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function renderOutlet(t,e){const r="string"==typeof e?document.querySelector(e):e;if(!r)return void console.warn("Unable to find outlet: "+e);let o;if(function(t){return t instanceof Element||t instanceof HTMLElement}(t))o=t;else{o=window.customElements.get(t)?document.createElement(t):function(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstChild}(t)}r.appendChild(o);const s="data-router-outlet";r.hasAttribute(s)||r.setAttribute(s,"")}class t extends Array{peek(){return this[this.length-1]}}function flatten(t,e,r,o=!0){r.forEach((r=>{if(r.children)!function(t,e,r){let o,s=[...r.children];if(r.action||r.component)o={...r},delete o.children;else{const t=s.find((t=>""===t.path));t&&(o={...t},o.path=r.path,delete o.children,s=s.filter((t=>""!==t.path)))}const n=copyRouteAndAdjustPath(o,e);t.push(n),e.push(n),flatten(t,e,s,!1)}(t,e,r);else{const o=copyRouteAndAdjustPath(r,e);t.push(o)}o&&e.splice(0,e.length)}))}function copyRouteAndAdjustPath(t,e){const r={...t},o=e.peek();return o&&(r.path=`${o.path}${r.path}`),r}class e{constructor(t){let e,r;this.urlParamMap={};let o=[];t.includes("?")?(e=t.substring(t.indexOf("?")+1),e.includes("#")&&(e=e.substring(0,e.indexOf("#")),r=e.substring(e.indexOf("#")+1))):t.includes("#")&&(e="",r=t.substring(t.indexOf("#")+1,t.length)),o=e?e.split("&"):[],r&&r.includes("&")&&r.includes("=")&&(o=[...o,...r.split("&")]);for(let t=0;te[0].toLowerCase()?1:t[0].toLowerCase()/g,">").replace(/&/g,"&")}_unesc(t){return String(t).replace(/</g,"<").replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/&/g,"&")}}class r{constructor(t,e,r={}){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router;this._rootOutletSelector=e,this._routes=this._processRoutes(t),this._routerOptions=this._processOptions(r),this._outletCache=new WeakMap,"undefined"!=typeof window&&(window.addEventListener("popstate",(()=>{this._resolveRoute(this.location.pathname).catch((t=>this._renderError(this.location.pathname,t)))})),window.__ficusjs__=window.__ficusjs__||{},window.__ficusjs__.router=window.__ficusjs__.router||this)}get location(){let t;return"undefined"!=typeof window&&(t={host:window.location.host,protocol:window.location.protocol,pathname:window.location.pathname,hash:window.location.hash,href:window.location.href,search:window.location.search,state:window.history.state},"hash"===this._routerOptions.mode&&(t.pathname=this._getHashPathname(),t.hash="")),t}get options(){return this._routerOptions}setOptions(t={}){this._routerOptions=this._processOptions(t)}addRoutes(t){this._routes=[...this._routes,...this._processRoutes(t)]}hasRoute(t){return!!this._findRoute(t)}_getQueryStringParams(){return Object.fromEntries(new e(this.location.href).entries())}_getUrlParams(t,e){const r=e.matcher(t);return"string"==typeof r?void 0:r}_findRoute(t){return this._routes.find((e=>void 0!==e.matcher(t)))}_getHashPathname(){let t=window.location.hash.substring(1);return""===t&&(t="/"),t}_processRoutes(e){return function(e){const r=new t;return flatten(r,new t,e),r}(e).map((t=>{const e={...t,matcher:e=>stripTrailingSlash(t.path)===stripTrailingSlash(e)?e:void 0};if(t.component&&!t.action&&(e.action=()=>t.component),/:[^/]+/.test(t.path)){const r=t.path.match(/(:[^/]+)/gm);r&&r.length>0&&(e.urlParamKeys=r.map((t=>t.substring(1))));const o=stripTrailingSlash(t.path.replace(/:[^/]+/g,"([^/]+)"));e.pathRegex=new RegExp(`^${o}$`),e.pathRegexCapture=new RegExp(o,"gm"),e.matcher=t=>{if(e.pathRegex.test(t)){const r={};let o;for(;null!==(o=e.pathRegexCapture.exec(t));)if(o&&o.length===e.urlParamKeys.length+1){const t=o.slice(1);for(let o=0;ot(e,r)}}return e}_render(t,e,r,o={}){if(r.redirect){const e={from:t};this.push(r.redirect,e)}else waitFor((()=>document.querySelector(this._rootOutletSelector))).then((t=>{const s=Object.keys(o),n=document.querySelectorAll("[data-router-outlet]");n.length&&[...n].filter((e=>e!==t)).forEach((t=>function(t){for(;t.firstChild;)t.removeChild(t.firstChild)}(t))),this._renderIntoOutlet(r,t),s.length&&s.forEach((t=>{waitFor((()=>document.querySelectorAll(t))).then((r=>{const s=o[t](e,e&&e.params);isPromise(s)?s.then((t=>this._renderIntoAllOutlets(t,r))):this._renderIntoAllOutlets(s,r)})).catch((t=>this._routerOptions.warnOnMissingOutlets&&console.warn(t)))}))})).catch((t=>console.warn(t)))}_renderIntoOutlet(t,e){this._isSameOutletContent(t,e)||(this._routerOptions.render(t,e),this._outletCache.set(e,t))}_renderIntoAllOutlets(t,e){for(let r=0;rthis._renderError(s,t))))}_resolveRoute(t){if(this._routerOptions.resolveRoute){const e=this._findRoute(t);let r={...this._getQueryStringParams()};const o={...this._routerOptions.context,router:this,route:e||{},path:t,params:r};if(e){r={...r,...this._getUrlParams(t,e)},o.route=e,o.params=r;const s=this._routerOptions.resolveRoute(o,r);return s?hasKey(s,"template")&&hasKey(s,"outlets")?this._renderRouteActionResult(t,o,s.template,s.outlets):this._renderRouteActionResult(t,o,s):this._findAndRenderRoute(t)}{const e=this._routerOptions.resolveRoute(o,r);return e?hasKey(e,"template")&&hasKey(e,"outlets")?this._renderRouteActionResult(t,o,e.template,e.outlets):this._renderRouteActionResult(t,o,e):Promise.reject(new Error("not_found"))}}return this._findAndRenderRoute(t)}_findAndRenderRoute(t){return new Promise(((e,r)=>{const o=this._findRoute(t);if(o){const s={...this._getQueryStringParams(),...this._getUrlParams(t,o)},n={...this._routerOptions.context,router:this,route:o,path:t,params:s},i=o.action(n,s);isPromise(i)?i.then((r=>{this._render(t,n,r,o.outlets),e()})).catch((t=>{r(t)})):(this._render(t,n,i,o.outlets),e())}else r(new Error("not_found"))}))}_renderRouteActionResult(t,e,r,o={}){return new Promise(((s,n)=>{isPromise(r)?r.then((t=>hasKey(t,"template")&&hasKey(t,"outlets")?isPromise(t.template)?t.template.then((e=>({template:e,outlets:t.outlets}))):Promise.resolve({template:t.template,outlets:t.outlets}):Promise.resolve({template:t,outlets:o}))).then((r=>{this._render(t,e,r.template,r.outlets),s()})).catch((t=>{n(t)})):(this._render(t,e,r,o),s())}))}_setState(t,e,r=!1){r?window.history.replaceState(e,null,t):window.history.pushState(e,null,t)}_renderError(t,e){if(console.error(`A router error occurred for path '${t}'`,e),this._routerOptions.errorHandler){const r={message:e.message,status:"not_found"===e.message?404:500},o={...this._routerOptions.context,router:this,path:t,location:{...this.location,pathname:t}},s=this._routerOptions.errorHandler(r,o);isPromise(s)?s.then((e=>{this._render(t,o,e)})).catch((r=>{this._render(t,o,`
Router error from errorHandler: ${r.message}, original error: ${e.message}
`)})):this._render(t,o,s)}else this._render(t,null,`
Router error: ${e.message}
`)}go(t){window.history.go(t)}goBack(){this.go(-1)}goForward(){this.go(1)}start(t=this.location){/complete|interactive|loaded/.test(document.readyState)?this.replace(t):document.addEventListener("DOMContentLoaded",(()=>this.replace(t)))}_getUrl(t){return new URL(`${/https?/.test(t)?"":`${this.location.protocol}//${this.location.host}`}${t}`)}}function createRouter(t,e,o={}){const s=new r(t,e,o);return s.options.autoStart&&s.start(),s}function getRouter(){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router}export{createRouter,getRouter}; +const waitFor=(t=(()=>!0),e=1e4)=>new Promise(((r,o)=>{const check=()=>{const s=t();s?r(s):(e-=100)<0?o(new Error("Timed out waiting!")):setTimeout(check,100)};setTimeout(check,100)}));function stripTrailingSlash(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t}function isPromise(t){return("object"==typeof t||"function"==typeof t)&&"function"==typeof t.then}function hasKey(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function elementEmpty(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function renderOutlet(t,e){const r="string"==typeof e?document.querySelector(e):e;if(!r)return void console.warn("Unable to find outlet: "+e);let o;if(function(t){return t instanceof Element||t instanceof HTMLElement}(t))o=t;else{o=window.customElements.get(t)?document.createElement(t):function(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstChild}(t)}elementEmpty(r),r.appendChild(o);const s="data-router-outlet";r.hasAttribute(s)||r.setAttribute(s,"")}class t extends Array{peek(){return this[this.length-1]}}function flatten(t,e,r,o=!0){r.forEach((r=>{if(r.children)!function(t,e,r){let o,s=[...r.children];if(r.action||r.component)o={...r},delete o.children;else{const t=s.find((t=>""===t.path));t&&(o={...t},o.path=r.path,delete o.children,s=s.filter((t=>""!==t.path)))}const n=copyRouteAndAdjustPath(o,e);t.push(n),e.push(n),flatten(t,e,s,!1)}(t,e,r);else{const o=copyRouteAndAdjustPath(r,e);t.push(o)}o&&e.splice(0,e.length)}))}function copyRouteAndAdjustPath(t,e){const r={...t},o=e.peek();return o&&(r.path=`${o.path}${r.path}`),r}class e{constructor(t){let e,r;this.urlParamMap={};let o=[];t.includes("?")?(e=t.substring(t.indexOf("?")+1),e.includes("#")&&(e=e.substring(0,e.indexOf("#")),r=e.substring(e.indexOf("#")+1))):t.includes("#")&&(e="",r=t.substring(t.indexOf("#")+1,t.length)),o=e?e.split("&"):[],r&&r.includes("&")&&r.includes("=")&&(o=[...o,...r.split("&")]);for(let t=0;te[0].toLowerCase()?1:t[0].toLowerCase()/g,">").replace(/&/g,"&")}_unesc(t){return String(t).replace(/</g,"<").replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/&/g,"&")}}class r{constructor(t,e,r={}){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router;this._rootOutletSelector=e,this._routes=this._processRoutes(t),this._routerOptions=this._processOptions(r),this._outletCache=new WeakMap,"undefined"!=typeof window&&(window.addEventListener("popstate",(()=>{this._resolveRoute(this.location.pathname).catch((t=>this._renderError(this.location.pathname,t)))})),window.__ficusjs__=window.__ficusjs__||{},window.__ficusjs__.router=window.__ficusjs__.router||this)}get location(){let t;return"undefined"!=typeof window&&(t={host:window.location.host,protocol:window.location.protocol,pathname:window.location.pathname,hash:window.location.hash,href:window.location.href,search:window.location.search,state:window.history.state},"hash"===this._routerOptions.mode&&(t.pathname=this._getHashPathname(),t.hash="")),t}get options(){return this._routerOptions}setOptions(t={}){this._routerOptions=this._processOptions(t)}addRoutes(t){this._routes=[...this._routes,...this._processRoutes(t)]}hasRoute(t){return!!this._findRoute(t)}_getQueryStringParams(){return Object.fromEntries(new e(this.location.href).entries())}_getUrlParams(t,e){const r=e.matcher(t);return"string"==typeof r?void 0:r}_findRoute(t){return this._routes.find((e=>void 0!==e.matcher(t)))}_getHashPathname(){let t=window.location.hash.substring(1);return""===t&&(t="/"),t}_processRoutes(e){return function(e){const r=new t;return flatten(r,new t,e),r}(e).map((t=>{const e={...t,matcher:e=>stripTrailingSlash(t.path)===stripTrailingSlash(e)?e:void 0};if(t.component&&!t.action&&(e.action=()=>t.component),/:[^/]+/.test(t.path)){const r=t.path.match(/(:[^/]+)/gm);r&&r.length>0&&(e.urlParamKeys=r.map((t=>t.substring(1))));const o=stripTrailingSlash(t.path.replace(/:[^/]+/g,"([^/]+)"));e.pathRegex=new RegExp(`^${o}$`),e.pathRegexCapture=new RegExp(o,"gm"),e.matcher=t=>{if(e.pathRegex.test(t)){const r={};let o;for(;null!==(o=e.pathRegexCapture.exec(t));)if(o&&o.length===e.urlParamKeys.length+1){const t=o.slice(1);for(let o=0;ot(e,r)}}return e}_render(t,e,r,o={}){if(r.redirect){const e={from:t};this.push(r.redirect,e)}else waitFor((()=>document.querySelector(this._rootOutletSelector))).then((t=>{const s=Object.keys(o),n=document.querySelectorAll("[data-router-outlet]");n.length&&[...n].filter((e=>e!==t)).forEach((t=>elementEmpty(t))),this._renderIntoOutlet(r,t),s.length&&s.forEach((t=>{waitFor((()=>document.querySelectorAll(t))).then((r=>{const s=o[t](e,e&&e.params);isPromise(s)?s.then((t=>this._renderIntoAllOutlets(t,r))):this._renderIntoAllOutlets(s,r)})).catch((t=>this._routerOptions.warnOnMissingOutlets&&console.warn(t)))}))})).catch((t=>console.warn(t)))}_renderIntoOutlet(t,e){this._isSameOutletContent(t,e)||(this._routerOptions.render(t,e),this._outletCache.set(e,t))}_renderIntoAllOutlets(t,e){for(let r=0;rthis._renderError(s,t))))}_resolveRoute(t){if(this._routerOptions.resolveRoute){const e=this._findRoute(t);let r={...this._getQueryStringParams()};const o={...this._routerOptions.context,router:this,route:e||{},path:t,params:r};if(e){r={...r,...this._getUrlParams(t,e)},o.route=e,o.params=r;const s=this._routerOptions.resolveRoute(o,r);return s?hasKey(s,"template")&&hasKey(s,"outlets")?this._renderRouteActionResult(t,o,s.template,s.outlets):this._renderRouteActionResult(t,o,s):this._findAndRenderRoute(t)}{const e=this._routerOptions.resolveRoute(o,r);return e?hasKey(e,"template")&&hasKey(e,"outlets")?this._renderRouteActionResult(t,o,e.template,e.outlets):this._renderRouteActionResult(t,o,e):Promise.reject(new Error("not_found"))}}return this._findAndRenderRoute(t)}_findAndRenderRoute(t){return new Promise(((e,r)=>{const o=this._findRoute(t);if(o){const s={...this._getQueryStringParams(),...this._getUrlParams(t,o)},n={...this._routerOptions.context,router:this,route:o,path:t,params:s},i=o.action(n,s);isPromise(i)?i.then((r=>{this._render(t,n,r,o.outlets),e()})).catch((t=>{r(t)})):(this._render(t,n,i,o.outlets),e())}else r(new Error("not_found"))}))}_renderRouteActionResult(t,e,r,o={}){return new Promise(((s,n)=>{isPromise(r)?r.then((t=>hasKey(t,"template")&&hasKey(t,"outlets")?isPromise(t.template)?t.template.then((e=>({template:e,outlets:t.outlets}))):Promise.resolve({template:t.template,outlets:t.outlets}):Promise.resolve({template:t,outlets:o}))).then((r=>{this._render(t,e,r.template,r.outlets),s()})).catch((t=>{n(t)})):(this._render(t,e,r,o),s())}))}_setState(t,e,r=!1){r?window.history.replaceState(e,null,t):window.history.pushState(e,null,t)}_renderError(t,e){if(console.error(`A router error occurred for path '${t}'`,e),this._routerOptions.errorHandler){const r={message:e.message,status:"not_found"===e.message?404:500},o={...this._routerOptions.context,router:this,path:t,location:{...this.location,pathname:t}},s=this._routerOptions.errorHandler(r,o);isPromise(s)?s.then((e=>{this._render(t,o,e)})).catch((r=>{this._render(t,o,`
Router error from errorHandler: ${r.message}, original error: ${e.message}
`)})):this._render(t,o,s)}else this._render(t,null,`
Router error: ${e.message}
`)}go(t){window.history.go(t)}goBack(){this.go(-1)}goForward(){this.go(1)}start(t=this.location){/complete|interactive|loaded/.test(document.readyState)?this.replace(t):document.addEventListener("DOMContentLoaded",(()=>this.replace(t)))}_getUrl(t){return new URL(`${/https?/.test(t)?"":`${this.location.protocol}//${this.location.host}`}${t}`)}}function createRouter(t,e,o={}){const s=new r(t,e,o);return s.options.autoStart&&s.start(),s}function getRouter(){if("undefined"!=typeof window&&window.__ficusjs__&&window.__ficusjs__.router)return window.__ficusjs__.router}export{createRouter,getRouter};