Files
qihuo/static/js/turbonav.js
T
dekun 6d55a54946 Fix turbo nav layout flash and stats page not loading.
Wait for page CSS before swapping content, hoist inline styles to head, and boot page scripts immediately when DOM markers exist.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 20:55:36 +08:00

249 lines
9.0 KiB
JavaScript

/* Copyright (c) 2025-2026 马建军. All rights reserved.
* 专有软件 — 未经授权禁止复制、传播、转售。
* 详见 LICENSE.zh-CN.txt
*/
(function () {
var PERMANENT_CSS = ['/static/css/base.css', '/static/css/tech.css', '/static/css/responsive.css'];
var CORE_SCRIPT_MARKERS = ['theme.js', 'symbol.js', 'page.js', 'nav.js', 'pwa.js', 'turbonav.js'];
var pageCache = new Map();
var MAX_CACHE = 10;
var inflight = null;
var currentUrl = normalizeUrl(window.location.href);
function normalizeUrl(href) {
var u = new URL(href, window.location.origin);
u.hash = '';
return u.href;
}
function isPermanentStylesheet(el) {
var href = el.getAttribute('href') || '';
return PERMANENT_CSS.some(function (p) { return href.indexOf(p) !== -1; });
}
function isCoreScript(el) {
var src = el.getAttribute('src') || '';
if (!src) return false;
return CORE_SCRIPT_MARKERS.some(function (m) { return src.indexOf(m) !== -1; });
}
function shouldTurboClick(e, link) {
if (!link || !link.href) return false;
if (e.defaultPrevented) return false;
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return false;
if (link.target && link.target !== '_self') return false;
if (link.hasAttribute('download')) return false;
if (link.origin !== window.location.origin) return false;
if (normalizeUrl(link.href) === currentUrl) return false;
return true;
}
function parseHtml(html) {
return new DOMParser().parseFromString(html, 'text/html');
}
function fetchPage(url, signal) {
var key = normalizeUrl(url);
if (pageCache.has(key)) {
return Promise.resolve(pageCache.get(key));
}
return fetch(key, {
credentials: 'same-origin',
headers: { Accept: 'text/html' },
signal: signal
}).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
}).then(function (html) {
var doc = parseHtml(html);
pageCache.set(key, doc);
if (pageCache.size > MAX_CACHE) {
var first = pageCache.keys().next().value;
pageCache.delete(first);
}
return doc;
});
}
function collectPageCss(doc) {
var out = [];
doc.querySelectorAll('head link[rel="stylesheet"], head style').forEach(function (el) {
if (el.tagName === 'LINK' && isPermanentStylesheet(el)) return;
out.push(el);
});
doc.querySelectorAll('.main style').forEach(function (el) {
out.push(el);
});
return out;
}
function prepareMainHtml(doc) {
var fetchedMain = doc.querySelector('.main');
if (!fetchedMain) return '';
fetchedMain.querySelectorAll('style').forEach(function (el) { el.remove(); });
return fetchedMain.innerHTML;
}
function collectPageScripts(doc) {
var out = [];
var pastCore = false;
doc.body.querySelectorAll(':scope > script').forEach(function (el) {
if (isCoreScript(el)) {
if ((el.getAttribute('src') || '').indexOf('pwa.js') !== -1) pastCore = true;
return;
}
if (!pastCore) return;
if (isCoreScript(el)) return;
out.push(el);
});
return out;
}
function removePageAssets() {
document.querySelectorAll('[data-page-css]').forEach(function (el) { el.remove(); });
document.querySelectorAll('body script[data-page-js]').forEach(function (el) { el.remove(); });
}
function applyPageCss(items) {
return items.reduce(function (chain, srcEl) {
return chain.then(function () {
return new Promise(function (resolve) {
var el = srcEl.cloneNode(true);
el.setAttribute('data-page-css', '');
if (el.tagName === 'LINK') {
el.onload = function () { resolve(); };
el.onerror = function () { resolve(); };
document.head.appendChild(el);
} else {
document.head.appendChild(el);
resolve();
}
});
});
}, Promise.resolve());
}
function runPageScripts(items) {
return items.reduce(function (chain, srcEl) {
return chain.then(function () {
return new Promise(function (resolve) {
var s = document.createElement('script');
s.setAttribute('data-page-js', '');
var src = srcEl.getAttribute('src');
if (src) {
var bust = (src.indexOf('?') >= 0 ? '&' : '?') + '_turbo=' + Date.now();
s.src = src + bust;
s.async = false;
s.onload = function () { resolve(); };
s.onerror = function () { resolve(); };
document.body.appendChild(s);
} else {
s.textContent = srcEl.textContent;
document.body.appendChild(s);
resolve();
}
});
});
}, Promise.resolve());
}
function syncNavActive(doc) {
var nav = document.getElementById('site-nav');
var fetchedNav = doc.getElementById('site-nav');
if (!nav || !fetchedNav) return;
nav.querySelectorAll('a.active').forEach(function (a) { a.classList.remove('active'); });
fetchedNav.querySelectorAll('a[href].active').forEach(function (fa) {
var href = fa.getAttribute('href');
if (!href) return;
var local = nav.querySelector('a[href="' + href + '"]')
|| nav.querySelector('a[href="' + new URL(href, window.location.origin).pathname + '"]');
if (local) local.classList.add('active');
});
}
function setLoading(on) {
var main = document.querySelector('.main');
if (main) main.classList.toggle('nav-loading', on);
}
function applyDocument(doc, url, fromPopstate) {
var main = document.querySelector('.main');
if (!main || !doc.querySelector('.main')) {
window.location.href = url;
return Promise.resolve();
}
window.dispatchEvent(new Event('qihuo:page-leave'));
removePageAssets();
var cssItems = collectPageCss(doc);
var mainHtml = prepareMainHtml(doc);
return applyPageCss(cssItems).then(function () {
main.innerHTML = mainHtml;
document.title = doc.title || document.title;
syncNavActive(doc, url);
return runPageScripts(collectPageScripts(doc));
}).then(function () {
currentUrl = normalizeUrl(url);
if (!fromPopstate) {
history.pushState({ turbo: true }, '', currentUrl);
}
setLoading(false);
window.scrollTo(0, 0);
if (window.qihuoEmitPageLoad) window.qihuoEmitPageLoad();
});
}
function navigateTo(url, opts) {
opts = opts || {};
var target = normalizeUrl(url);
if (target === currentUrl && !opts.force) return Promise.resolve();
if (inflight) inflight.abort();
var ctrl = new AbortController();
inflight = ctrl;
setLoading(true);
return fetchPage(target, ctrl.signal).then(function (doc) {
if (ctrl.signal.aborted) return;
inflight = null;
return applyDocument(doc, target, !!opts.fromPopstate);
}).catch(function () {
if (ctrl.signal.aborted) return;
inflight = null;
setLoading(false);
window.location.href = target;
});
}
function prefetchPage(href) {
var target = normalizeUrl(href);
if (target === currentUrl || pageCache.has(target)) return;
fetchPage(target).catch(function () { /* ignore */ });
}
window.qihuoNavigate = navigateTo;
window.qihuoPrefetchPage = prefetchPage;
document.addEventListener('click', function (e) {
var link = e.target.closest('#site-nav a[href]');
if (!link || !shouldTurboClick(e, link)) return;
e.preventDefault();
var navEl = document.getElementById('site-nav');
if (navEl) {
navEl.querySelectorAll('a.active').forEach(function (a) { a.classList.remove('active'); });
link.classList.add('active');
}
navigateTo(link.href);
}, true);
window.addEventListener('popstate', function () {
var target = normalizeUrl(window.location.href);
if (target === currentUrl) return;
navigateTo(target, { fromPopstate: true, force: true });
});
if (window.qihuoEmitPageLoad) window.qihuoEmitPageLoad();
})();