(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); 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)); })();