Bri-Tunes/public/js/navigate.js

113 lines
3.9 KiB
JavaScript

(function () {
'use strict';
// Full page reload for these form actions — logout intentionally kills the session.
const FULL_RELOAD_ACTIONS = ['/logout'];
function isSameOrigin(href) {
try { return new URL(href, location.href).origin === location.origin; }
catch { return false; }
}
// Re-execute script tags injected via innerHTML (browser skips them otherwise).
function runScripts(container) {
container.querySelectorAll('script').forEach((old) => {
const s = document.createElement('script');
[...old.attributes].forEach((a) => s.setAttribute(a.name, a.value));
s.textContent = old.textContent;
old.replaceWith(s);
});
}
// Swap #page-shell content, update title, and optionally push history.
function applyPage(doc, finalUrl, pushState) {
const newShell = doc.getElementById('page-shell');
const curShell = document.getElementById('page-shell');
if (newShell && curShell) {
curShell.innerHTML = newShell.innerHTML;
runScripts(curShell);
}
document.title = doc.title;
if (pushState) history.pushState({ btiNav: true }, doc.title, finalUrl);
window.scrollTo(0, 0);
window.dispatchEvent(new CustomEvent('briTunes:navigate'));
}
async function navigate(url, pushState) {
if (pushState === undefined) pushState = true;
try {
const res = await fetch(url);
if (!isSameOrigin(res.url)) { location.href = url; return; }
const html = await res.text();
applyPage(new DOMParser().parseFromString(html, 'text/html'), res.url, pushState);
} catch {
location.href = url;
}
}
// Build the right body type for fetch POST:
// - Forms with actual file selections → multipart FormData (multer handles it)
// - All other forms → URL-encoded (express.urlencoded handles it)
function buildBody(form) {
const fd = new FormData(form);
const hasFile = [...fd.values()].some(v => v instanceof File && v.size > 0);
if (hasFile) return fd;
const params = new URLSearchParams();
for (const [k, v] of fd.entries()) {
if (typeof v === 'string') params.append(k, v);
}
return params;
}
// --- Intercept link clicks ---
document.addEventListener('click', (e) => {
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const a = e.target.closest('a[href]');
if (!a) return;
if (a.target && a.target !== '_self') return;
if (a.hasAttribute('download')) return;
const raw = a.getAttribute('href');
if (!raw || raw.startsWith('#') || raw.startsWith('mailto:') || raw.startsWith('tel:')) return;
if (!isSameOrigin(a.href)) return;
e.preventDefault();
navigate(a.href);
});
// --- Intercept form submissions ---
document.addEventListener('submit', (e) => {
const form = e.target;
if (!(form instanceof HTMLFormElement)) return;
if (!isSameOrigin(form.action)) return;
const action = new URL(form.action, location.href).pathname;
if (FULL_RELOAD_ACTIONS.some(ex => action.startsWith(ex))) return;
const method = (form.method || 'GET').toUpperCase();
if (method === 'GET') {
// Rebuild URL with form data then navigate.
e.preventDefault();
const url = new URL(form.action, location.href);
for (const [k, v] of new FormData(form).entries()) {
if (typeof v === 'string') url.searchParams.set(k, v);
}
navigate(url.toString());
return;
}
if (method === 'POST') {
e.preventDefault();
fetch(form.action, { method: 'POST', body: buildBody(form) })
.then(async (res) => {
if (!isSameOrigin(res.url)) { location.href = res.url; return; }
const html = await res.text();
applyPage(new DOMParser().parseFromString(html, 'text/html'), res.url, true);
})
.catch(() => form.submit());
}
});
// --- Back / forward ---
window.addEventListener('popstate', () => navigate(location.href, false));
})();