- Dynamic miller column sizing with flexbox layout - Root column (160px), middle slices (48px), active column (flex) - Auto-scroll browser to rightmost column on navigation - Offline indicator UI and IndexedDB cache integration - /api/documents endpoint for client-side document caching - Breadcrumb navigation in document content area - FTS5 search with SQLite virtual table - Client-side Mermaid and KaTeX rendering via CDN - Deep sample content (advanced-topics/raft-deep-dive) - Border removal (only search button keeps radius) - Full viewport layout with proper borders between sidebar/content
226 lines
6.9 KiB
JavaScript
226 lines
6.9 KiB
JavaScript
(function () {
|
|
const notice = document.querySelector("[data-version-notice]");
|
|
const reload = document.querySelector("[data-version-reload]");
|
|
const offlineNotice = document.querySelector("[data-offline-notice]");
|
|
const documentShell = document.querySelector("[data-document-path][data-document-hash]");
|
|
const browser = document.querySelector(".miller-browser");
|
|
|
|
// Auto-scroll miller browser to show the rightmost (active) column
|
|
function scrollBrowserToRight() {
|
|
if (!browser) return;
|
|
requestAnimationFrame(function () {
|
|
// Only scroll if the rightmost column is not already visible
|
|
var tolerance = 50;
|
|
var maxScroll = browser.scrollWidth - browser.clientWidth;
|
|
if (maxScroll <= 0) return; // everything fits, no scroll needed
|
|
if (browser.scrollLeft >= maxScroll - tolerance) return; // already at right edge
|
|
browser.scrollTo({
|
|
left: browser.scrollWidth,
|
|
behavior: "smooth",
|
|
});
|
|
});
|
|
}
|
|
|
|
scrollBrowserToRight();
|
|
|
|
function updateOfflineUI() {
|
|
if (!offlineNotice) return;
|
|
if (navigator.onLine) {
|
|
offlineNotice.hidden = true;
|
|
} else {
|
|
offlineNotice.hidden = false;
|
|
}
|
|
}
|
|
|
|
window.addEventListener("online", function () {
|
|
updateOfflineUI();
|
|
fetchAndCacheDocuments();
|
|
});
|
|
|
|
window.addEventListener("offline", function () {
|
|
updateOfflineUI();
|
|
restoreBrowserFromCache();
|
|
});
|
|
|
|
updateOfflineUI();
|
|
|
|
// Cache current page content
|
|
if (documentShell && window.MDHubCache) {
|
|
const path = documentShell.getAttribute("data-document-path");
|
|
const title = document.title;
|
|
const html = documentShell.querySelector(".markdown-body")?.innerHTML;
|
|
if (path && html) {
|
|
window.MDHubCache.cacheContent(path, html, title).catch(function () {});
|
|
}
|
|
}
|
|
|
|
// Fetch and cache documents
|
|
function fetchAndCacheDocuments() {
|
|
if (!window.MDHubCache) return;
|
|
fetch("/api/documents")
|
|
.then(function (res) {
|
|
if (!res.ok) throw new Error("fetch failed");
|
|
return res.json();
|
|
})
|
|
.then(function (data) {
|
|
if (data.documents) {
|
|
window.MDHubCache.cacheDocuments(data.documents).catch(function () {});
|
|
}
|
|
})
|
|
.catch(function () {});
|
|
}
|
|
|
|
fetchAndCacheDocuments();
|
|
|
|
// Restore browser from cache when offline
|
|
function restoreBrowserFromCache() {
|
|
if (!browser || !window.MDHubCache) return;
|
|
window.MDHubCache.getCachedDocuments().then(function (docs) {
|
|
if (!docs || docs.length === 0) return;
|
|
// The browser is already rendered server-side; we just keep it.
|
|
// If we wanted to rebuild it from cache, we'd need the full logic.
|
|
// For now, the existing browser HTML is likely cached by the browser itself.
|
|
}).catch(function () {});
|
|
}
|
|
|
|
// Intercept browser navigation when offline
|
|
if (browser) {
|
|
browser.addEventListener("click", function (event) {
|
|
const link = event.target.closest("a");
|
|
if (!link) return;
|
|
if (navigator.onLine) return;
|
|
|
|
const href = link.getAttribute("href");
|
|
if (!href || href.startsWith("http") || href.startsWith("//")) return;
|
|
|
|
// Try to serve from cache for document pages
|
|
if (href.startsWith("/docs/")) {
|
|
event.preventDefault();
|
|
const path = href.replace("/docs/", "");
|
|
window.MDHubCache.getCachedContent(path).then(function (cached) {
|
|
if (cached && cached.html) {
|
|
loadCachedDocument(path, cached.title, cached.html);
|
|
} else {
|
|
window.location.href = href;
|
|
}
|
|
}).catch(function () {
|
|
window.location.href = href;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadCachedDocument(path, title, html) {
|
|
if (!documentShell) {
|
|
window.location.href = "/docs/" + path;
|
|
return;
|
|
}
|
|
|
|
// Update URL without reloading
|
|
history.pushState({ cachedPath: path }, title, "/docs/" + path);
|
|
document.title = title;
|
|
|
|
// Update document shell
|
|
documentShell.setAttribute("data-document-path", path);
|
|
documentShell.setAttribute("data-document-hash", "");
|
|
const titleEl = documentShell.querySelector(".document-meta h1");
|
|
if (titleEl) titleEl.textContent = title;
|
|
const pathEl = documentShell.querySelector(".meta-grid code");
|
|
if (pathEl) pathEl.textContent = path;
|
|
const bodyEl = documentShell.querySelector(".markdown-body");
|
|
if (bodyEl) bodyEl.innerHTML = html;
|
|
|
|
// Re-render math and mermaid
|
|
if (typeof renderMath === "function") renderMath();
|
|
if (typeof renderMermaid === "function") renderMermaid();
|
|
|
|
// Update active state in browser
|
|
updateBrowserActiveState(path);
|
|
}
|
|
|
|
function updateBrowserActiveState(activePath) {
|
|
if (!browser) return;
|
|
browser.querySelectorAll("a.is-active").forEach(function (el) {
|
|
el.classList.remove("is-active");
|
|
});
|
|
browser.querySelectorAll("a").forEach(function (link) {
|
|
const href = link.getAttribute("href");
|
|
if (href === "/docs/" + activePath || href === "/?folder=" + activePath) {
|
|
link.classList.add("is-active");
|
|
}
|
|
});
|
|
}
|
|
|
|
// WebSocket realtime updates
|
|
if (!window.WebSocket) {
|
|
return;
|
|
}
|
|
|
|
const currentPath = documentShell ? documentShell.getAttribute("data-document-path") : null;
|
|
const currentHash = documentShell ? documentShell.getAttribute("data-document-hash") : null;
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
let reconnectTimer = null;
|
|
|
|
function connect() {
|
|
const socket = new WebSocket(protocol + "//" + window.location.host + "/ws");
|
|
|
|
socket.addEventListener("open", function () {
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
});
|
|
|
|
socket.addEventListener("message", function (event) {
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(event.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
if (payload.type !== "document_version" || !payload.data) {
|
|
return;
|
|
}
|
|
|
|
const change = payload.data;
|
|
|
|
if (currentPath && change.path === currentPath && change.hash !== currentHash) {
|
|
if (notice) notice.hidden = false;
|
|
return;
|
|
}
|
|
|
|
if (browser && !documentShell) {
|
|
if (notice) notice.hidden = false;
|
|
}
|
|
});
|
|
|
|
socket.addEventListener("close", function () {
|
|
reconnectTimer = setTimeout(connect, 3000);
|
|
});
|
|
|
|
socket.addEventListener("error", function () {
|
|
socket.close();
|
|
});
|
|
}
|
|
|
|
connect();
|
|
|
|
if (reload) {
|
|
reload.addEventListener("click", function () {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
|
|
// Handle back/forward buttons for cached documents
|
|
window.addEventListener("popstate", function (event) {
|
|
if (event.state && event.state.cachedPath && window.MDHubCache) {
|
|
window.MDHubCache.getCachedContent(event.state.cachedPath).then(function (cached) {
|
|
if (cached && cached.html) {
|
|
loadCachedDocument(event.state.cachedPath, cached.title, cached.html);
|
|
}
|
|
}).catch(function () {});
|
|
}
|
|
});
|
|
})();
|