/** History-API router. Routes are patterns like '/watch/:id'. */
const routes = [];
let onRender = () => {};
export function define(pattern, view) {
const names = [];
// `:name` is picked up anywhere in the pattern, so '/@:username' works too.
const source = pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/:(\w+)/g, (_, name) => { names.push(name); return '([^/]+)'; });
routes.push({ regex: new RegExp(`^${source}/?$`), names, view });
}
export function resolve(pathname) {
for (const route of routes) {
const m = route.regex.exec(pathname);
if (!m) continue;
const params = {};
route.names.forEach((name, i) => { params[name] = decodeURIComponent(m[i + 1]); });
return { view: route.view, params };
}
return null;
}
export const currentPath = () => window.location.pathname.replace(/\/+$/, '') || '/';
export const currentQuery = () => new URLSearchParams(window.location.search);
export function navigate(href, { replace = false, keepScroll = false } = {}) {
const url = new URL(href, window.location.origin);
const same = url.pathname === window.location.pathname && url.search === window.location.search;
if (same) return;
window.history[replace ? 'replaceState' : 'pushState']({ keepScroll }, '', url);
onRender();
}
export function start(render) {
onRender = render;
window.addEventListener('popstate', render);
// Intercept same-origin links so navigation stays in the SPA.
document.addEventListener('click', (event) => {
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const link = event.target.closest?.('a[href]');
if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
const url = new URL(link.href, window.location.origin);
if (url.origin !== window.location.origin) return;
if (link.getAttribute('href')?.startsWith('#')) return;
event.preventDefault();
navigate(url.pathname + url.search);
});
render();
}