| | |
| | | import micromorph from "micromorph" |
| | | import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" |
| | | import { fetchCanonical } from "./util" |
| | | |
| | | // adapted from `micromorph` |
| | | // https://github.com/natemoo-re/micromorph |
| | |
| | | document.dispatchEvent(event) |
| | | } |
| | | |
| | | const cleanupFns: Set<(...args: any[]) => void> = new Set() |
| | | window.addCleanup = (fn) => cleanupFns.add(fn) |
| | | |
| | | function startLoading() { |
| | | const loadingBar = document.createElement("div") |
| | | loadingBar.className = "navigation-progress" |
| | | loadingBar.style.width = "0" |
| | | if (!document.body.contains(loadingBar)) { |
| | | document.body.appendChild(loadingBar) |
| | | } |
| | | |
| | | setTimeout(() => { |
| | | loadingBar.style.width = "80%" |
| | | }, 100) |
| | | } |
| | | |
| | | let isNavigating = false |
| | | let p: DOMParser |
| | | async function navigate(url: URL, isBack: boolean = false) { |
| | | async function _navigate(url: URL, isBack: boolean = false) { |
| | | isNavigating = true |
| | | startLoading() |
| | | p = p || new DOMParser() |
| | | const contents = await fetch(`${url}`) |
| | | const contents = await fetchCanonical(url) |
| | | .then((res) => { |
| | | const contentType = res.headers.get("content-type") |
| | | if (contentType?.startsWith("text/html")) { |
| | |
| | | |
| | | if (!contents) return |
| | | |
| | | // notify about to nav |
| | | const event: CustomEventMap["prenav"] = new CustomEvent("prenav", { detail: {} }) |
| | | document.dispatchEvent(event) |
| | | |
| | | // cleanup old |
| | | cleanupFns.forEach((fn) => fn()) |
| | | cleanupFns.clear() |
| | | |
| | | const html = p.parseFromString(contents, "text/html") |
| | | normalizeRelativeURLs(html, url) |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // now, patch head |
| | | const elementsToRemove = document.head.querySelectorAll(":not([spa-preserve])") |
| | | // now, patch head, re-executing scripts |
| | | const elementsToRemove = document.head.querySelectorAll(":not([data-persist])") |
| | | elementsToRemove.forEach((el) => el.remove()) |
| | | const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])") |
| | | const elementsToAdd = html.head.querySelectorAll(":not([data-persist])") |
| | | elementsToAdd.forEach((el) => document.head.appendChild(el)) |
| | | |
| | | // delay setting the url until now |
| | |
| | | if (!isBack) { |
| | | history.pushState({}, "", url) |
| | | } |
| | | |
| | | notifyNav(getFullSlug(window)) |
| | | delete announcer.dataset.persist |
| | | } |
| | | |
| | | async function navigate(url: URL, isBack: boolean = false) { |
| | | if (isNavigating) return |
| | | isNavigating = true |
| | | try { |
| | | await _navigate(url, isBack) |
| | | } catch (e) { |
| | | console.error(e) |
| | | window.location.assign(url) |
| | | } finally { |
| | | isNavigating = false |
| | | } |
| | | } |
| | | |
| | | window.spaNavigate = navigate |
| | | |
| | | function createRouter() { |
| | |
| | | return |
| | | } |
| | | |
| | | try { |
| | | navigate(url, false) |
| | | } catch (e) { |
| | | window.location.assign(url) |
| | | } |
| | | navigate(url, false) |
| | | }) |
| | | |
| | | window.addEventListener("popstate", (event) => { |
| | | const { url } = getOpts(event) ?? {} |
| | | if (window.location.hash && window.location.pathname === url?.pathname) return |
| | | try { |
| | | navigate(new URL(window.location.toString()), true) |
| | | } catch (e) { |
| | | window.location.reload() |
| | | } |
| | | navigate(new URL(window.location.toString()), true) |
| | | return |
| | | }) |
| | | } |