99 lines
3.4 KiB
JavaScript
99 lines
3.4 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; }
|
|
}
|
|
|
|
// 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;
|
|
document.title = doc.title;
|
|
if (pushState) history.pushState({ btiNav: true }, doc.title, finalUrl);
|
|
window.scrollTo(0, 0);
|
|
}
|
|
|
|
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));
|
|
})();
|