feat: set navigation through ajax
This commit is contained in:
@@ -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.
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user