Files
imf-sms-dashboard/web/assets/js/nav.js
T

212 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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');
}
function swapPageModals(doc) {
document.querySelectorAll('[data-ajax-modal="true"]').forEach(el => el.remove());
doc.querySelectorAll('body > .modal').forEach(old => {
const wrapper = document.createElement('div');
wrapper.innerHTML = old.outerHTML;
const modal = wrapper.firstElementChild;
if (!modal) return;
modal.setAttribute('data-ajax-modal', 'true');
document.body.appendChild(modal);
});
}
// ── 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(/^\s*(?: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 ?? '';
swapPageModals(doc);
document.title = doc.title;
const filename = href.split('/').pop();
setActiveNav(filename);
// Execute page scripts from the centralized app bundle when available.
if (typeof window.__runPageScripts === 'function') {
window.__runPageScripts(filename);
} else {
// Fallback for legacy pages that still carry inline scripts.
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.
})();