diff --git a/src/web/static/app.js b/src/web/static/app.js index 6c13d65..aa7639f 100644 --- a/src/web/static/app.js +++ b/src/web/static/app.js @@ -132,9 +132,19 @@ if (tocPanel) { const tocStatus = document.querySelector("[data-toc-status]"); const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); const largeScreen = window.matchMedia("(min-width: 64rem)"); + const saveScroll = () => { + if (!largeScreen.matches || !tocPanel.dataset.tocIssue) return; + try { + sessionStorage.setItem("toc-scroll", `${tocPanel.dataset.tocIssue}\n${tocPanel.scrollTop}`); + } catch (_) {} + }; + window.addEventListener("pagehide", saveScroll); + tocPanel.addEventListener("click", (event) => { + if (event.target.closest?.("a")) saveScroll(); + }); // Long issues overflow the sidebar; scroll just enough to show where we are. - const revealCurrent = () => { + const revealCurrent = (behavior = "smooth") => { const current = tocPanel.querySelector(".toc-link[aria-current]"); if (!current || tocPanel.scrollHeight <= tocPanel.clientHeight) return; const stickyHeader = tocPanel.querySelector(":scope > div"); @@ -147,7 +157,7 @@ if (tocPanel) { const visibleBottom = tocPanel.scrollTop + tocPanel.clientHeight - revealMargin; if (top >= visibleTop && top + currentRect.height <= visibleBottom) return; const target = Math.max(0, top - Math.max(topMargin, (tocPanel.clientHeight - currentRect.height) / 2)); - tocPanel.scrollTo({ top: target, behavior: reducedMotion.matches ? "auto" : "smooth" }); + tocPanel.scrollTo({ top: target, behavior: reducedMotion.matches ? "auto" : behavior }); }; const sizeOpenPanel = () => { if (tocPanel.dataset.open !== "true" || largeScreen.matches || !tocBar) return; @@ -168,7 +178,7 @@ if (tocPanel) { } }; setOpen(false); - revealCurrent(); + revealCurrent("auto"); if (tocToggle) { tocToggle.addEventListener("click", () => setOpen(tocPanel.dataset.open !== "true")); document.addEventListener("click", (event) => { @@ -196,7 +206,7 @@ if (tocPanel) { }); }; if (currentLink) markPassed(currentLink); - const setCurrent = (link) => { + const setCurrent = (link, behavior = "smooth") => { if (!link || (link === currentLink && link.getAttribute("aria-current") === "location")) return; tocLinks.forEach((candidate) => candidate.removeAttribute("aria-current")); markPassed(link); @@ -216,7 +226,7 @@ if (tocPanel) { delete bar.dataset.live; bar.value = position; }); - revealCurrent(); + revealCurrent(behavior); }; const tocEntries = Array.from(document.querySelectorAll("[data-toc-entry]")).map((entry) => ({ @@ -225,6 +235,7 @@ if (tocPanel) { })).filter(({ link }) => link); if (tocEntries.length) { let queued = false; + let firstPaint = true; const paintCurrent = () => { queued = false; const threshold = (tocBar ? tocBar.offsetHeight : 0) + window.innerHeight / 3; @@ -232,7 +243,8 @@ if (tocPanel) { tocEntries.forEach(({ entry, link }) => { if (entry.getBoundingClientRect().top <= threshold) next = link; }); - setCurrent(next); + setCurrent(next, firstPaint ? "auto" : "smooth"); + firstPaint = false; }; const scheduleCurrent = () => { if (queued) return; diff --git a/src/web/static/theme.js b/src/web/static/theme.js index ccea0b5..bc4f834 100644 --- a/src/web/static/theme.js +++ b/src/web/static/theme.js @@ -31,9 +31,38 @@ } catch (_) {} }); }; + let tocPanel; + let tocRestored = false; + const findToc = (root) => { + if (tocPanel || root.nodeType !== Node.ELEMENT_NODE) return; + tocPanel = root.matches("[data-toc-panel]") ? root : root.querySelector("[data-toc-panel]"); + }; + const restoreToc = () => { + if (!tocPanel || tocRestored) return; + tocRestored = true; + if (!window.matchMedia("(min-width: 64rem)").matches) return; + try { + const saved = sessionStorage.getItem("toc-scroll"); + const separator = saved ? saved.lastIndexOf("\n") : -1; + if (separator < 0 || saved.slice(0, separator) !== tocPanel.dataset.tocIssue) return; + const scrollTop = Number(saved.slice(separator + 1)); + if (Number.isFinite(scrollTop)) tocPanel.scrollTop = scrollTop; + } catch (_) {} + }; const detailsObserver = new MutationObserver((records) => { - records.forEach((record) => record.addedNodes.forEach(restoreDetails)); + const addedNodes = []; + records.forEach((record) => record.addedNodes.forEach((node) => { + addedNodes.push(node); + restoreDetails(node); + findToc(node); + })); + // A following node means the parser has closed the nav and its list is complete. + if (tocPanel && addedNodes.some((node) => !tocPanel.contains(node) + && (tocPanel.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING))) restoreToc(); }); detailsObserver.observe(document.documentElement, { childList: true, subtree: true }); - document.addEventListener("DOMContentLoaded", () => detailsObserver.disconnect(), { once: true }); + document.addEventListener("DOMContentLoaded", () => { + restoreToc(); + detailsObserver.disconnect(); + }, { once: true }); })(); diff --git a/src/web/templates/_toc.html b/src/web/templates/_toc.html index c5fd16a..395016c 100644 --- a/src/web/templates/_toc.html +++ b/src/web/templates/_toc.html @@ -8,7 +8,7 @@ {{ toc.position }} / {{ toc.total }} - + No. {{ toc.issue_number }} ยท {{ toc.short_date }} {% if toc.position > 0 %}Chapter {{ toc.position }} of {{ toc.total }}{% else %}Front page{% endif %}
{% if toc.position > 0 %}Chapter {{ toc.position }} of {{ toc.total }}{% else %}Front page{% endif %}