knave
Version:
Framework-agnostic client-side navigation library
280 lines (278 loc) • 8.15 kB
JavaScript
;
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
});