| @@ -27,9 +27,20 @@ export function getRouterScript(): string { |
| 27 | 27 | var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{}); |
| 28 | 28 | var containerSel=o.container; |
| 29 | 29 | |
| 30 | + var CACHE_TTL=5*60*1000; |
| 30 | 31 | var cache={}; |
| 31 | 32 | var prefetching={}; |
| 32 | 33 | var isNavigating=false; |
| 34 | + var currentAbort=null; |
| 35 | + |
| 36 | + function normalizePath(p){return p.length>1&&p.charAt(p.length-1)==='/'?p.slice(0,-1):p} |
| 37 | + function getCached(key){ |
| 38 | + var entry=cache[key]; |
| 39 | + if(!entry)return null; |
| 40 | + if(Date.now()-entry.ts>CACHE_TTL){delete cache[key];return null} |
| 41 | + return entry.html; |
| 42 | + } |
| 43 | + function setCache(key,html){cache[key]={html:html,ts:Date.now()}} |
| 33 | 44 | |
| 34 | 45 | function getContainer(){ |
| 35 | 46 | return document.querySelector(containerSel)||document.querySelector('[data-stx-content]')||document.querySelector('main'); |
| @@ -37,14 +48,13 @@ export function getRouterScript(): string { |
| 37 | 48 | |
| 38 | 49 | |
| 39 | 50 | function navigate(url,pushState){ |
| 40 | | - if(isNavigating)return; |
| 41 | 51 | var t=new URL(url,location.origin); |
| 42 | 52 | |
| 43 | 53 | |
| 44 | 54 | if(t.origin!==location.origin){location.href=url;return} |
| 45 | 55 | |
| 46 | 56 | |
| 47 | | - if(t.pathname===location.pathname&&t.hash){ |
| 57 | + if(normalizePath(t.pathname)===normalizePath(location.pathname)&&t.hash){ |
| 48 | 58 | if(pushState!==false)history.pushState({},'',t.href); |
| 49 | 59 | var el=document.querySelector(t.hash); |
| 50 | 60 | if(el)el.scrollIntoView({behavior:'smooth'}); |
| @@ -54,27 +64,36 @@ export function getRouterScript(): string { |
| 54 | 64 | |
| 55 | 65 | if(t.href===location.href&&!t.hash)return; |
| 56 | 66 | |
| 67 | + |
| 68 | + history.replaceState({scrollX:window.scrollX,scrollY:window.scrollY},'',location.href); |
| 69 | + |
| 70 | + |
| 71 | + if(currentAbort){currentAbort.abort();currentAbort=null} |
| 72 | + |
| 57 | 73 | isNavigating=true; |
| 58 | 74 | document.body.classList.add(o.loadingClass); |
| 59 | 75 | |
| 60 | | - var targetHref=t.href; |
| 61 | | - var targetPath=t.pathname; |
| 76 | + var targetPath=normalizePath(t.pathname); |
| 62 | 77 | var targetHash=t.hash; |
| 63 | 78 | |
| 64 | | - function done(){isNavigating=false;document.body.classList.remove(o.loadingClass)} |
| 79 | + function done(){isNavigating=false;currentAbort=null;document.body.classList.remove(o.loadingClass)} |
| 65 | 80 | |
| 66 | | - if(o.cache&&cache[targetPath]){ |
| 67 | | - swap(cache[targetPath],targetPath,pushState,targetHash); |
| 81 | + var cached=o.cache&&getCached(targetPath); |
| 82 | + if(cached){ |
| 83 | + swap(cached,targetPath,pushState,targetHash); |
| 68 | 84 | done(); |
| 69 | 85 | } |
| 70 | | -else { |
| 71 | | - fetch(url,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){ |
| 86 | + else { |
| 87 | + var abort=new AbortController(); |
| 88 | + currentAbort=abort; |
| 89 | + fetch(url,{headers:{'X-STX-Router':'true','Accept':'text/html'},signal:abort.signal}).then(function(r){ |
| 72 | 90 | if(!r.ok)throw new Error(r.status); |
| 73 | 91 | return r.text(); |
| 74 | 92 | }).then(function(html){ |
| 75 | | - if(o.cache)cache[targetPath]=html; |
| 93 | + if(o.cache)setCache(targetPath,html); |
| 76 | 94 | swap(html,targetPath,pushState,targetHash); |
| 77 | | - }).catch(function(){ |
| 95 | + }).catch(function(err){ |
| 96 | + if(err&&err.name==='AbortError')return; |
| 78 | 97 | location.href=url; |
| 79 | 98 | }).finally(done); |
| 80 | 99 | } |
| @@ -181,17 +200,26 @@ else { |
| 181 | 200 | if(pushState!==false)history.pushState({},'',url+(hash||'')); |
| 182 | 201 | |
| 183 | 202 | |
| 184 | | - updateNav(url); |
| 185 | 203 | updateActiveLinks(); |
| 186 | 204 | |
| 187 | | - |
| 188 | | - if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'}); |
| 205 | + |
| 206 | + if(pushState===false&&history.state&&typeof history.state.scrollY==='number'){ |
| 207 | + window.scrollTo({top:history.state.scrollY,left:history.state.scrollX,behavior:'instant'}); |
| 208 | + } |
| 209 | + else if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'}); |
| 189 | 210 | else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})} |
| 190 | 211 | |
| 191 | 212 | |
| 192 | 213 | var newTitle=doc.querySelector('title'); |
| 193 | 214 | if(newTitle)document.title=newTitle.textContent; |
| 194 | 215 | |
| 216 | + |
| 217 | + var swappableMeta='meta[name="description"],meta[property^="og:"],meta[name^="twitter:"]'; |
| 218 | + document.querySelectorAll(swappableMeta).forEach(function(m){m.remove()}); |
| 219 | + doc.querySelectorAll(swappableMeta).forEach(function(m){ |
| 220 | + document.head.appendChild(m.cloneNode(true)); |
| 221 | + }); |
| 222 | + |
| 195 | 223 | |
| 196 | 224 | window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}})); |
| 197 | 225 | window.dispatchEvent(new Event('stx:load')); |
| @@ -220,7 +248,7 @@ else { |
| 220 | 248 | if(href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')||href.startsWith('javascript:'))return false; |
| 221 | 249 | if(link.target==='_blank')return false; |
| 222 | 250 | if(link.hasAttribute('data-stx-no-router')||link.hasAttribute('data-no-router')||link.hasAttribute('download'))return false; |
| 223 | | - if(href===location.pathname)return false; |
| 251 | + if(normalizePath(href)===normalizePath(location.pathname))return false; |
| 224 | 252 | if(!getContainer())return false; |
| 225 | 253 | return true; |
| 226 | 254 | } |
| @@ -236,7 +264,10 @@ else { |
| 236 | 264 | },true); |
| 237 | 265 | |
| 238 | 266 | |
| 239 | | - window.addEventListener('popstate',function(){ |
| 267 | + window.addEventListener('popstate',function(e){ |
| 268 | + |
| 269 | + if(currentAbort){currentAbort.abort();currentAbort=null} |
| 270 | + isNavigating=false; |
| 240 | 271 | navigate(location.pathname+location.search+location.hash,false); |
| 241 | 272 | }); |
| 242 | 273 | |
| @@ -248,37 +279,29 @@ else { |
| 248 | 279 | if(!shouldIntercept(link))return; |
| 249 | 280 | |
| 250 | 281 | var href=link.getAttribute('href'); |
| 251 | | - if(cache[href]||prefetching[href])return; |
| 252 | | - prefetching[href]=true; |
| 282 | + var cacheKey=normalizePath(new URL(href,location.origin).pathname); |
| 283 | + if(getCached(cacheKey)||prefetching[cacheKey])return; |
| 284 | + prefetching[cacheKey]=true; |
| 253 | 285 | fetch(href,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){return r.text()}).then(function(html){ |
| 254 | | - if(o.cache)cache[href]=html; |
| 255 | | - }).catch(function(){}).finally(function(){delete prefetching[href]}); |
| 286 | + if(o.cache)setCache(cacheKey,html); |
| 287 | + }).catch(function(){}).finally(function(){delete prefetching[cacheKey]}); |
| 256 | 288 | },true); |
| 257 | 289 | } |
| 258 | 290 | |
| 259 | 291 | |
| 260 | | - function updateNav(url){ |
| 261 | | - document.querySelectorAll('nav a[href], #mobileNav a[href], [data-stx-nav] a[href]').forEach(function(a){ |
| 262 | | - var href=a.getAttribute('href'); |
| 263 | | - if(!href||href.startsWith('#')||href.startsWith('http'))return; |
| 264 | | - var isActive=(href===url)||(href==='/'&&url==='/'); |
| 265 | | - if(a.hasAttribute('data-stx-link')){ |
| 266 | | - var ac=a.getAttribute('data-stx-active-class')||'active'; |
| 267 | | - if(isActive)a.classList.add(ac);else a.classList.remove(ac); |
| 268 | | - } |
| 269 | | - }); |
| 270 | | - } |
| 271 | | - |
| 272 | 292 | function updateActiveLinks(){ |
| 293 | + var cur=normalizePath(location.pathname); |
| 273 | 294 | var links=document.querySelectorAll('[data-stx-link]'); |
| 274 | | - var cur=location.pathname; |
| 275 | 295 | links.forEach(function(link){ |
| 276 | | - var href=link.getAttribute('href')||''; |
| 296 | + var href=normalizePath(link.getAttribute('href')||''); |
| 277 | 297 | var ac=link.getAttribute('data-stx-active-class')||'active'; |
| 278 | 298 | var eac=link.getAttribute('data-stx-exact-active-class')||'exact-active'; |
| 299 | + |
| 279 | 300 | ac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)}); |
| 280 | 301 | eac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)}); |
| 302 | + |
| 281 | 303 | var isExact=cur===href; |
| 304 | + |
| 282 | 305 | var isActive=href!=='/'?cur.startsWith(href):cur==='/'; |
| 283 | 306 | if(isExact)eac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)}); |
| 284 | 307 | if(isActive)ac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)}); |
| @@ -305,14 +328,15 @@ else { |
| 305 | 328 | navigate:navigate, |
| 306 | 329 | navigateTo:navigate, |
| 307 | 330 | prefetch:function(url){ |
| 308 | | - if(!cache[url]){ |
| 309 | | - fetch(url,{headers:{'X-STX-Router':'true'}}).then(function(r){return r.text()}).then(function(html){cache[url]=html}).catch(function(){}); |
| 331 | + var key=normalizePath(new URL(url,location.origin).pathname); |
| 332 | + if(!getCached(key)){ |
| 333 | + fetch(url,{headers:{'X-STX-Router':'true'}}).then(function(r){return r.text()}).then(function(html){setCache(key,html)}).catch(function(){}); |
| 310 | 334 | } |
| 311 | 335 | }, |
| 312 | 336 | clearCache:function(){for(var k in cache)delete cache[k]}, |
| 313 | 337 | cache:cache, |
| 314 | 338 | swap:swap, |
| 315 | | - updateNav:updateNav |
| 339 | + updateActiveLinks:updateActiveLinks |
| 316 | 340 | }; |
| 317 | 341 | |
| 318 | 342 | window.__stxRouter=router; |
| @@ -324,7 +348,6 @@ else { |
| 324 | 348 | injectStyles(); |
| 325 | 349 | injectViewTransitionCSS(); |
| 326 | 350 | updateActiveLinks(); |
| 327 | | - updateNav(location.pathname); |
| 328 | 351 | } |
| 329 | 352 | |
| 330 | 353 | if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init(); |