351 lines
10 KiB
JavaScript
351 lines
10 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 cachedNotice = document.querySelector("[data-cached-notice]");
|
|
const documentShell = document.querySelector("[data-document-path][data-document-hash]");
|
|
const browser = document.querySelector(".miller-browser");
|
|
const isEditing = Boolean(document.querySelector("[data-editor-form]"));
|
|
|
|
// 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 initColumnPreviewExpansion() {
|
|
if (!browser) {
|
|
return;
|
|
}
|
|
|
|
let hoverTimer = null;
|
|
let expandedColumn = null;
|
|
|
|
function isCompressedColumn(column) {
|
|
return (
|
|
column &&
|
|
column.matches(".miller-column:not(:first-child):not(:last-child)")
|
|
);
|
|
}
|
|
|
|
function collapseColumn(column) {
|
|
if (!column) return;
|
|
column.classList.remove("is-preview-expanded");
|
|
column.style.removeProperty("--expanded-column-width");
|
|
if (expandedColumn === column) {
|
|
expandedColumn = null;
|
|
}
|
|
}
|
|
|
|
function expandColumn(column) {
|
|
if (!isCompressedColumn(column)) return;
|
|
if (expandedColumn && expandedColumn !== column) {
|
|
collapseColumn(expandedColumn);
|
|
}
|
|
|
|
const expandedWidth = Math.min(Math.max(column.scrollWidth + 12, 192), 384);
|
|
column.style.setProperty("--expanded-column-width", expandedWidth + "px");
|
|
column.classList.add("is-preview-expanded");
|
|
expandedColumn = column;
|
|
|
|
requestAnimationFrame(function () {
|
|
const leftEdge = column.offsetLeft;
|
|
const rightEdge = leftEdge + expandedWidth;
|
|
let targetLeft = browser.scrollLeft;
|
|
|
|
if (leftEdge < browser.scrollLeft) {
|
|
targetLeft = leftEdge - 8;
|
|
} else if (rightEdge > browser.scrollLeft + browser.clientWidth) {
|
|
targetLeft = rightEdge - browser.clientWidth + 8;
|
|
}
|
|
|
|
browser.scrollTo({
|
|
left: Math.max(0, targetLeft),
|
|
behavior: "smooth",
|
|
});
|
|
});
|
|
}
|
|
|
|
function scheduleExpansion(column) {
|
|
if (!isCompressedColumn(column)) return;
|
|
clearTimeout(hoverTimer);
|
|
hoverTimer = setTimeout(function () {
|
|
expandColumn(column);
|
|
}, 1000);
|
|
}
|
|
|
|
browser.querySelectorAll(".miller-column").forEach(function (column) {
|
|
column.addEventListener("pointerenter", function () {
|
|
scheduleExpansion(column);
|
|
});
|
|
|
|
column.addEventListener("mouseenter", function () {
|
|
scheduleExpansion(column);
|
|
});
|
|
|
|
column.addEventListener("mouseover", function () {
|
|
if (!isCompressedColumn(column)) return;
|
|
if (expandedColumn === column) return;
|
|
scheduleExpansion(column);
|
|
});
|
|
|
|
column.addEventListener("pointerleave", function () {
|
|
clearTimeout(hoverTimer);
|
|
collapseColumn(column);
|
|
});
|
|
|
|
column.addEventListener("mouseleave", function () {
|
|
clearTimeout(hoverTimer);
|
|
collapseColumn(column);
|
|
});
|
|
});
|
|
}
|
|
|
|
initColumnPreviewExpansion();
|
|
|
|
function updateCachedUI(isCached) {
|
|
if (!cachedNotice) return;
|
|
cachedNotice.hidden = !isCached;
|
|
}
|
|
|
|
function cacheCurrentPage() {
|
|
if (!window.MDHubCache) return;
|
|
|
|
const html = document.documentElement.outerHTML;
|
|
const title = document.title;
|
|
const pagePath = window.MDHubCache.normalizePath(window.location.pathname);
|
|
window.MDHubCache.cachePage(pagePath, html, title).catch(function () {});
|
|
}
|
|
|
|
function registerServiceWorker() {
|
|
if (!("serviceWorker" in navigator)) return;
|
|
window.addEventListener("load", function () {
|
|
navigator.serviceWorker.register("/sw.js").catch(function () {});
|
|
});
|
|
}
|
|
|
|
registerServiceWorker();
|
|
updateCachedUI(Boolean(window.__MDHUB_OFFLINE_CACHED__));
|
|
|
|
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 () {});
|
|
}
|
|
}
|
|
|
|
cacheCurrentPage();
|
|
|
|
// 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;
|
|
|
|
updateCachedUI(true);
|
|
cacheCurrentPage();
|
|
|
|
// 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 (isEditing && currentPath && change.path === currentPath) {
|
|
return;
|
|
}
|
|
|
|
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 () {});
|
|
}
|
|
});
|
|
})();
|