feat: set navigation through ajax

This commit is contained in:
2026-06-09 03:38:06 +01:00
parent 8834feb5a1
commit 1140f87bf5
8 changed files with 200 additions and 0 deletions
+1
View File
@@ -199,6 +199,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script> <script>
'use strict'; 'use strict';
+193
View File
@@ -0,0 +1,193 @@
(function () {
'use strict';
// ── Track setIntervals so navigation can clear them ───────────────────────
const _origSetInterval = window.setInterval.bind(window);
const _origClearInterval = window.clearInterval.bind(window);
let _intervals = [];
window.setInterval = function (fn, ms, ...args) {
const id = _origSetInterval(fn, ms, ...args);
_intervals.push(id);
return id;
};
function clearTrackedIntervals() {
_intervals.forEach(id => _origClearInterval(id));
_intervals = [];
}
// ── Cleanup state left behind by the outgoing page ────────────────────────
function cleanupPage() {
clearTrackedIntervals();
// DataTables 2.x
if (window.DataTable) {
try { DataTable.tables({ api: true }).destroy(); } catch (_) {}
}
// Dropzone
if (window.Dropzone) {
try { [...Dropzone.instances].forEach(dz => dz.destroy()); } catch (_) {}
}
// Bootstrap modals hide any open one and remove leftover backdrops
document.querySelectorAll('.modal.show').forEach(el => {
try { bootstrap.Modal.getInstance(el)?.hide(); } catch (_) {}
});
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
}
// ── Per-page inline <style> management ───────────────────────────────────
const STYLE_ID = 'ajax-page-style';
function setPageStyles(cssText) {
let tag = document.getElementById(STYLE_ID);
if (!cssText) { tag?.remove(); return; }
if (!tag) {
tag = document.createElement('style');
tag.id = STYLE_ID;
document.head.appendChild(tag);
}
tag.textContent = cssText;
}
// ── Load external <script src> tags not already on the page ──────────────
const _loadedSrcs = new Set(
[...document.querySelectorAll('script[src]')].map(s => s.src)
);
function loadScript(src) {
const abs = new URL(src, location.href).href;
if (_loadedSrcs.has(abs)) return Promise.resolve();
_loadedSrcs.add(abs);
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
// ── Load external <link rel="stylesheet"> tags not already on the page ───
const _loadedHrefs = new Set(
[...document.querySelectorAll('link[rel="stylesheet"]')].map(l => l.href)
);
function loadStylesheet(href) {
const abs = new URL(href, location.href).href;
if (_loadedHrefs.has(abs)) return Promise.resolve();
_loadedHrefs.add(abs);
return new Promise((resolve, reject) => {
const l = document.createElement('link');
l.rel = 'stylesheet';
l.href = href;
l.onload = resolve;
l.onerror = reject;
document.head.appendChild(l);
});
}
// ── Re-execute inline scripts from the fetched document ──────────────────
function runInlineScripts(doc) {
doc.querySelectorAll('body script:not([src])').forEach(old => {
const code = old.textContent;
// Wrap in an IIFE so top-level `let`/`const` are function-scoped and
// don't collide with bindings from a previous visit to the same page.
// Then re-expose every named function declaration to `window` so that
// inline onclick="foo()" handlers continue to resolve them globally.
const funcNames = [
...code.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(/gm),
].map(m => m[1]);
const expose = funcNames
.map(n => `try{window.${n}=${n};}catch(_){}`)
.join('\n');
const s = document.createElement('script');
s.textContent = `(function(){\n${code}\n${expose}\n})();`;
document.head.appendChild(s);
s.remove();
});
}
// ── Update which nav link is highlighted as active ────────────────────────
function setActiveNav(filename) {
document.querySelectorAll('.nav-link-custom, .bottom-nav a').forEach(el => {
const hf = (el.getAttribute('href') || '').split('/').pop();
el.classList.toggle('active', hf === filename);
});
}
// ── Core AJAX navigation ──────────────────────────────────────────────────
let _navigating = false;
async function navigateTo(href) {
if (_navigating) return;
_navigating = true;
const mainInner = document.querySelector('.main-inner');
if (!mainInner) { window.location.href = href; return; }
// Dim content while loading
mainInner.style.transition = 'opacity 0.12s';
mainInner.style.opacity = '0.35';
try {
const r = await fetch(href, { redirect: 'manual' });
if (r.type === 'opaqueredirect') { window.location.reload(); return; }
if (!r.ok) { window.location.href = href; return; }
const text = await r.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
// Load any external libs/styles the new page needs that aren't already loaded.
// Scripts must load sequentially (order matters: e.g. jQuery before DataTables).
// Stylesheets have no dependency order so can load in parallel.
for (const s of doc.querySelectorAll('script[src]')) {
await loadScript(s.getAttribute('src'));
}
await Promise.all(
[...doc.querySelectorAll('link[rel="stylesheet"]')].map(l => loadStylesheet(l.getAttribute('href')))
);
// Swap page-specific inline styles
const styles = [...doc.head.querySelectorAll('style')].map(s => s.textContent).join('\n');
setPageStyles(styles || null);
// Cleanup outgoing page, then inject new content
cleanupPage();
mainInner.innerHTML = doc.querySelector('.main-inner')?.innerHTML ?? '';
document.title = doc.title;
const filename = href.split('/').pop();
setActiveNav(filename);
// Execute the new page's inline scripts in order
runInlineScripts(doc);
} catch {
// Fallback to hard navigation on any error
window.location.href = href;
} finally {
mainInner.style.opacity = '1';
_navigating = false;
}
}
// ── Intercept all local .html link clicks ────────────────────────────────
document.addEventListener('click', e => {
const link = e.target.closest('a[href]');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('#') || href.includes('://') || !href.endsWith('.html')) return;
e.preventDefault();
navigateTo(href);
}, true);
// History API intentionally omitted — no pushState/popstate.
})();
+1
View File
@@ -194,6 +194,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script> <script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" <script src="https://code.jquery.com/jquery-4.0.0.min.js"
integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script> integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
+1
View File
@@ -120,6 +120,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js" <script src="https://code.jquery.com/jquery-4.0.0.min.js"
integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script> integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js"></script> <script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js"></script>
+1
View File
@@ -230,6 +230,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script> <script>
function esc(str) { function esc(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+1
View File
@@ -213,6 +213,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script> <script>
// ── Mode toggle ────────────────────────────────────────── // ── Mode toggle ──────────────────────────────────────────
let currentMode = 'specific'; let currentMode = 'specific';
+1
View File
@@ -141,6 +141,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script> <script>
const SEND_MAX = 160; const SEND_MAX = 160;
+1
View File
@@ -110,6 +110,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/js/nav.js"></script>
<script> <script>
const MAX_CHARS = 160; const MAX_CHARS = 160;