UNPKG

iles

Version:

Vite & Vue powered static site generator with partial hydration

163 lines (130 loc) 4.46 kB
// Provides SPA-like navigation by prefetching and replacing the page in-place. // Combines the techniques used in GoogleChromeLabs/quicklink and @hotwired/turbo const dom = document // Used to detect anchor tags in the viewport. let observer // Used to detect same path navigation (hash change) let currentPath = location.pathname const conn = navigator.connection const saveData = conn && (conn.saveData || /2g/.test(conn.effectiveType)) const toArray = Array.from const queryAll = (selector, el = dom) => toArray(el.querySelectorAll(selector)) const adoptNode = node => dom.adoptNode(node) const createElement = (tagName = 'link') => dom.createElement(tagName) const whenIdle = window.requestIdleCallback || setTimeout const normalizeURL = url => new URL(url, location.href).pathname const prefetchNow = (url, importance = 'high') => fetch(url, { credentials: 'include', importance }) const prefetchWhenIdle = hasPrefetch() ? prefetchWithLinkTag : url => prefetchNow(url, 'low') // Cache of URLs and Promises we've prefetched const hasFetched = new Set() hasFetched.add(normalizeURL(location.href)) function prefetch (url) { if (!saveData && !hasFetched.has(url)) { hasFetched.add(url) prefetchWhenIdle(url) } } watchLinks() addEventListener('popstate', (e) => { if (currentPath !== location.pathname) replacePage(location.href, (e.state && e.state.scrollPosition) || 0) }) /** * Detect links in the viewport and prefetch them, and intercept clicks to them * to perform a turbolinks-style replacement of the page. */ function watchLinks () { window.__ILE_DISPOSE__ = new Map() observer?.disconnect() observer = new IntersectionObserver((entries) => { entries.forEach(({ target: link, isIntersecting }) => { if (isIntersecting) { observer.unobserve(link) link.addEventListener('click', onLinkClick) prefetch(normalizeURL(link.href)) } }) }) whenIdle(() => { queryAll('a').forEach((link) => { const extMatch = link.pathname.match(/\.\w+$/) if ((!extMatch || extMatch[0] === '.html') && link.hostname === location.hostname) observer.observe(link) }) }) } function onLinkClick (e) { const link = e.target.closest('a') if (!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && event.which <= 1 && link.target !== '_blank') { const sameLocation = link.pathname === location.pathname const sameHash = link.hash === location.hash if (!sameLocation || sameHash) e.preventDefault() if (!sameLocation) { replacePage(link.href, 0, () => { history.replaceState({ scrollPosition: scrollY }, dom.title) history.pushState(null, '', link.href) }) } } } function replacePage (url, scrollPosition, callback) { url = normalizeURL(url) prefetchNow(url).then(p => p.text()).then((html) => { callback?.() currentPath = location.pathname replaceHtml(html) scrollTo(0, scrollPosition || dom.querySelector(location.hash || 'body').offsetTop) watchLinks() }) .catch((e) => { console.error(e) location.href = url }) } function replaceHtml (html) { const { head, body } = new DOMParser().parseFromString(html, 'text/html') const prevHead = dom.head queryAll(':not(link[rel="stylesheet"]):not(style)', prevHead).forEach(el => el.remove()) const prevHeadHrefs = new Set(queryAll('link', prevHead).map(el => el.href)) toArray(head.children).forEach((el) => { if (el.tagName !== 'LINK' || el.rel !== 'stylesheet' || !prevHeadHrefs.has(el.href)){ adoptNode(el) prevHead.appendChild(el) } }) adoptNode(body) toArray(__ILE_DISPOSE__).forEach(([_id, fn]) => fn()) dom.body.replaceWith(body) activateScripts(head) activateScripts(body) } function activateScripts (el) { queryAll('script', el).forEach(activateScript) } function activateScript (el) { if (el.getAttribute('once') === null) { const script = createElement('script') toArray(el.attributes) .forEach(attr => script.setAttribute(attr.nodeName, attr.nodeValue)) script.textContent = el.textContent el.replaceWith(script) } } function hasPrefetch (link) { link = createElement() return link.relList && link.relList.supports && link.relList.supports('prefetch') } function prefetchWithLinkTag (url, link) { link = createElement() link.rel = 'prefetch' link.href = url dom.head.appendChild(link) }