@roussos/pathway-router
Version:
A lightweight client-side router that manages navigation, history, caching, and rendering of content within a container element.
2 lines (1 loc) • 5.66 kB
JavaScript
"use strict";function n(t){this.options={containerSelector:"body",defaultLinkSelector:"a",preloadLinkSelector:"[data-preload-link]",excludeLinkSelector:"[data-exclude-link]",cacheCapacity:10,historyStackSize:10,transitionDuration:0,updateRouterHistory:!0,popstateEvent:!0,clickEvent:!0,mutationObserver:!0,scrollRestoration:!1,onNavigate:function(){},onLoadingChange:function(){},onBeforeLeave:function(){},onBeforeRender:function(){},onAfterRender:function(){},onError:function(){}},Object.assign(this.options,t),this.history=[],this.cache=new Map,this.linkElements=new Map,this.scrolls=new Map,this.isLoading=!1,this.container=document.querySelector(this.options.containerSelector)||document.body,this.mutation={observer:null},this.cacheResponse(window.location.href,document),this.initEvents()}n.prototype.initEvents=function(){this.options.popstateEvent&&window.addEventListener("popstate",()=>{this.navigate(window.location.href,null,!1,-1)}),this.options.clickEvent&&this.addClickListeners(),this.options.mutationObserver&&(this.mutation.observer=new MutationObserver((t,e)=>{this.mutationHandler(t,e)})),this.history.push({url:window.location.href,title:document.title,state:null,scroll:0}),this.options.preloadLinkSelector&&this.cacheContainerLinks()};n.prototype.clickEventCallback=function(t){if(this.isLoading)return;const e=t.currentTarget,i=e.href;if(!(e instanceof HTMLAnchorElement)||!i||(t.preventDefault(),t.stopPropagation(),window.location.pathname===i||window.location.href===i))return;const r={...e.dataset};this.navigate(i,r,this.options.updateRouterHistory)};n.prototype.isUnusableLink=function(t){return!t&&!(t instanceof HTMLAnchorElement)?!0:!t.href||t.matches(this.options.excludeLinkSelector)||t.hasAttribute("download")||t.href.startsWith("mailto:")||t.href.startsWith("tel:")||t.href.startsWith("javascript:")||t.href.startsWith("sms:")};n.prototype.addClickListeners=function(){for(const t of document.querySelectorAll(this.options.defaultLinkSelector)){if(this.isUnusableLink(t))continue;const e=this.clickEventCallback.bind(this);t.removeEventListener("click",this.linkElements.get(t)),t.addEventListener("click",e),this.linkElements.set(t,e)}};n.prototype.removeClickListeners=function(){this.linkElements.forEach((t,e)=>e.removeEventListener("click",t))};n.prototype.cacheContainerLinks=function(){const t=document.body.querySelectorAll(this.options.preloadLinkSelector),e=(i,r)=>{const s=this.options.preloadLinkSelector.replace(/[\[\]]/g,""),o=i.getAttribute(s),c=r.getAttribute(s);return o>c?1:o<c?-1:0};for(const i of Array.from(t).sort(e))this.isUnusableLink(i)||this.cache.has(i.href)||this.fetchLink(i.href)};n.prototype.fetchLink=function(t,e,i){const r=new Request(t,{method:"GET",headers:new Headers({Accept:"text/html; charset=UTF-8"})});if(this.cache.has(t)&&e)return e(this.__get(t));this.isLoading=!0,this.options.onLoadingChange(this,!0);const s=o=>Math.trunc(o/100)*100;window.fetch(r).then(async o=>{if(s(o.status)!==200)throw new Error(`(ERROR) Request failed with status code ${o.status}`);const c=await o.text(),a=new DOMParser().parseFromString(c,"text/html"),h=this.cacheResponse(t,a);e&&e(h)}).catch(o=>{console.warn("(ERROR) Parse document:",o),i&&i(o),this.options.onError(this,o)}).finally(()=>{this.isLoading=!1,this.options.onLoadingChange(this,!1)})};n.prototype.navigate=function(t,e,i,r=1){this.isLoading||(this.options.onNavigate(this,t),setTimeout(async()=>{this.options.onBeforeLeave(this);const s=await this.waitFetch(t);s.content.pathway_last_step=r,this.updateDocument(s,e,i)},this.options.transitionDuration))};n.prototype.updateDocument=function(t,e,i){i?(window.history.pushState(e,null,t.url),this.history.splice(0,this.history.length-this.options.historyStackSize),this.history.push({url:t.url,title:t.title,state:e,scroll:window.scrollY||document.documentElement.scrollTop})):window.history.replaceState(e,null,t.url),document.title=t.title,this.mutation.observer&&this.mutation.observer.observe(this.container.parentElement,{childList:!0}),this.options.onBeforeRender(this),this.container.replaceWith(t.content)};n.prototype.mutationHandler=function(t,e){var i,r;for(const s of t)if(s.type==="childList"&&!(s.addedNodes.length<=0)){if(this.container=s.addedNodes[0],this.options.onAfterRender(this),this.options.scrollRestoration){const o=this.container.hasOwnProperty("pathway_last_step")&&!isNaN(this.container.pathway_last_step)&&this.container.pathway_last_step<0&&(r=(i=this.history[this.history.length+this.container.pathway_last_step])==null?void 0:i.scroll)!=null?r:0;window.scrollTo({top:o,behavior:"instant"})}this.options.preloadLinkSelector&&this.cacheContainerLinks(),this.options.clickEvent&&this.addClickListeners(),e.disconnect();break}};n.prototype.waitFetch=async function(t){try{return await new Promise((e,i)=>this.fetchLink(t,e,i))}catch(e){console.warn("(ERROR) Async Fetch:",e)}};n.prototype.cacheResponse=function(t,e){const i=this.parseResponse(e),s={title:e.title,content:i,url:t};return this.__set(t,s)};n.prototype.parseResponse=function(t){return t.querySelector(this.options.containerSelector)||t.body};n.prototype.__get=function(t){if(!this.cache.has(t))return;let e=this.cache.get(t);return this.cache.delete(t),this.cache.set(t,e),e};n.prototype.__set=function(t,e){return this.cache.delete(t),this.options.cacheCapacity<=0||(this.cache.size===this.options.cacheCapacity?(this.cache.delete(this.cache.keys().next().value),this.cache.set(t,e)):this.cache.set(t,e)),e};n.prototype.getLeastRecent=function(){return Array.from(this.cache)[0]};n.prototype.getMostRecent=function(){return Array.from(this.cache)[this.cache.size-1]};module.exports=n;