feat: set navigation through ajax
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user