UNPKG

knave

Version:

Framework-agnostic client-side navigation library

280 lines (278 loc) 8.15 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { addNavigationBlocker: () => addNavigationBlocker, addNavigationListener: () => addNavigationListener, finalize: () => finalize, initialize: () => initialize, navigate: () => navigate, removeNavigationBlocker: () => removeNavigationBlocker, removeNavigationListener: () => removeNavigationListener, shouldHandleClick: () => shouldHandleClick }); module.exports = __toCommonJS(src_exports); function initialize(renderFunction, installGlobalHandler) { if (render) { throw new Error("Knave already initialized"); } render = renderFunction; currentUrl = location.href; nextIndex = 0; savedScrollRestoration = history.scrollRestoration; history.scrollRestoration = "manual"; addEventListener("popstate", handleNavigation); base = document.head.querySelector("base"); if (!base) { base = document.createElement("base"); document.head.insertBefore(base, document.head.firstChild); } base.href = location.href; if (installGlobalHandler) { document.body.addEventListener("click", handleClick); } lastRenderedId = createUniqueId(); lastRenderedIndex = nextIndex++; history.replaceState( { id: lastRenderedId, index: lastRenderedIndex }, "", location.href ); } function finalize() { removeEventListener("popstate", handleNavigation); render = void 0; history.scrollRestoration = savedScrollRestoration; listeners = []; blockers = []; pending = void 0; } function handleClick(e) { if (!shouldHandleClick(e)) return; e.preventDefault(); navigate(e.target.href); } function shouldHandleClick(e) { const t = e.target; return (t instanceof HTMLAnchorElement || t instanceof SVGAElement || t instanceof HTMLAreaElement) && !e.defaultPrevented && t.href !== void 0 && e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && (!t.target || t.target !== "_self") && !t.hasAttribute("download") && !t.relList.contains("external"); } async function navigate(to, options) { if (!render) { throw new Error("Knave not initialized"); } const url = new URL(to, location.href); if (url.origin !== location.origin) { location.href = url.href; return new Promise(() => { }); } const { replace, scroll: scroll2, data } = options || {}; const id = createUniqueId(); if (replace) { history.replaceState({ id, data, index: history.state.index }, "", to); } else { const index = nextIndex++; history.pushState({ id, data, index }, "", to); } return handleNavigation(void 0, scroll2); } function handleBeforeUnload(e) { e.preventDefault(); e.returnValue = ""; } var blocking = false; var ignoring = false; var lastCanceled; var redoing = false; var pendingResolver; var cancelResolver; function ignoreNavigation() { ignoring = true; history.go(lastRenderedIndex - history.state.index); } function cancelNavigation() { lastCanceled = { delta: lastRenderedIndex - history.state.index, href: location.href, state: history.state }; return new Promise((resolve) => { cancelResolver = resolve; history.go(lastCanceled == null ? void 0 : lastCanceled.delta); if (scroll !== void 0) { nextIndex--; } }); } function redoNavigation() { redoing = true; history.go(-lastCanceled.delta); } async function handleNavigation(_, scroll2 = true) { if (ignoring) { ignoring = false; return false; } if (cancelResolver) { cancelResolver(); cancelResolver = void 0; return false; } if (redoing && lastCanceled) { history.replaceState(lastCanceled.state, "", lastCanceled.href); } if (blocking) { ignoreNavigation(); return false; } if (!redoing && blockers.length) { redoing = false; blocking = true; await cancelNavigation(); const result2 = await callNavigationBlockers(); blocking = false; if (!result2) { pendingResolver == null ? void 0 : pendingResolver(false); pendingResolver = void 0; return false; } redoNavigation(); return new Promise((resolve) => pendingResolver = resolve); } redoing = false; const scrollPosition = { x: scrollX, y: scrollY }; sessionStorage.setItem( `knave:${lastRenderedId}`, JSON.stringify(scrollPosition) ); if (pending) pending.abort(); const controller = new AbortController(); const result = render(controller.signal); if (isPromise(result)) { pending = controller; listeners.forEach((f) => f({ currentUrl, pendingUrl: location.href })); return result.then(() => { pending = void 0; if (controller.signal.aborted) { pendingResolver == null ? void 0 : pendingResolver(false); pendingResolver = void 0; return false; } currentUrl = location.href; listeners.forEach((f) => f({ currentUrl })); if (scroll2) restoreScrollPosition(); lastRenderedId = history.state.id; lastRenderedIndex = history.state.index; base.href = location.href; pendingResolver == null ? void 0 : pendingResolver(true); pendingResolver = void 0; return true; }); } else { currentUrl = location.href; listeners.forEach((f) => f({ currentUrl })); if (scroll2) restoreScrollPosition(); lastRenderedId = history.state.id; lastRenderedIndex = history.state.index; base.href = location.href; pendingResolver == null ? void 0 : pendingResolver(true); pendingResolver = void 0; return true; } } function addNavigationListener(listener) { listeners.push(listener); } function removeNavigationListener(listener) { listeners = listeners.filter((l) => l !== listener); } function addNavigationBlocker(blocker) { blockers.push(blocker); if (blockers.length === 1 && render) { addEventListener("beforeunload", handleBeforeUnload); } } function removeNavigationBlocker(blocker) { blockers = blockers.filter((b) => b !== blocker); if (blockers.length === 0) { removeEventListener("beforeunload", handleBeforeUnload); } } async function callNavigationBlockers() { for (const blocker of blockers) { let result = true; try { result = await blocker(); } catch { } if (!result) return false; } return true; } function restoreScrollPosition() { var _a; const scrollPosition = sessionStorage.getItem(`knave:${(_a = history.state) == null ? void 0 : _a.id}`); if (scrollPosition) { const { x, y } = JSON.parse(scrollPosition); scrollTo(x, y); } else { const hash = location.hash; if (hash) { const element = document.querySelector(hash); if (element) { element.scrollIntoView(); } } else { scrollTo(0, 0); } } } var render; var pending; var listeners = []; var blockers = []; var nextIndex = 0; var currentUrl; var lastRenderedId; var lastRenderedIndex; var savedScrollRestoration; var base; function createUniqueId() { return Math.random().toString(36).substr(2, 9); } function isPromise(value) { return value && typeof value.then === "function"; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { addNavigationBlocker, addNavigationListener, finalize, initialize, navigate, removeNavigationBlocker, removeNavigationListener, shouldHandleClick });