bug: Fix page scroll detection and add browser history support

- Use getBoundingClientRect() in the scroll listener so the active-page
  threshold is measured relative to the content container rather than
  the document body, fixing off-by-one page detection.
- Replace history.replaceState with pushState for user-initiated
  navigation (sidebar, prev/next, keyboard) and add a popstate listener
  so the browser back/forward buttons work correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 22:26:48 +00:00
parent 1977a5d966
commit 3c3c5a332f
3 changed files with 633 additions and 603 deletions

View File

@@ -20,7 +20,7 @@ def build_index(title, DIR):
sections = [(n, read_page(n)) for n in page_nums]
sidebar_items = '\n '.join(
f'<li data-page="{n}"><button onclick="goTo({n})">'
f'<li data-page="{n}"><button onclick="goTo({n},false,true)">'
f'<span class="page-num">{n}</span><span>Page {n}</span></button></li>'
for n in page_nums
)
@@ -250,7 +250,7 @@ def build_index(title, DIR):
const indicator = document.getElementById('page-indicator');
const titleEl = document.getElementById('page-title');
function updateNav(idx) {{
function updateNav(idx, push) {{
if (idx === currentIdx && indicator.textContent) return;
currentIdx = idx;
const n = PAGES[idx];
@@ -263,28 +263,29 @@ def build_index(title, DIR):
}});
document.querySelector('#page-list li[data-page="' + n + '"]')
?.scrollIntoView({{ block: 'nearest' }});
history.replaceState(null, '', '#page-' + n);
if (push) history.pushState(null, '', '#page-' + n);
else history.replaceState(null, '', '#page-' + n);
}}
function goTo(n, smooth) {{
function goTo(n, smooth, push) {{
const idx = PAGES.indexOf(n);
if (idx === -1) return;
const sec = document.getElementById('page-' + n);
if (!sec) return;
sec.scrollIntoView({{ behavior: smooth ? 'smooth' : 'instant', block: 'start' }});
updateNav(idx);
updateNav(idx, push);
}}
btnPrev.addEventListener('click', () => {{ if (currentIdx > 0) goTo(PAGES[currentIdx - 1], true); }});
btnNext.addEventListener('click', () => {{ if (currentIdx < total - 1) goTo(PAGES[currentIdx + 1], true); }});
btnPrev.addEventListener('click', () => {{ if (currentIdx > 0) goTo(PAGES[currentIdx - 1], true, true); }});
btnNext.addEventListener('click', () => {{ if (currentIdx < total - 1) goTo(PAGES[currentIdx + 1], true, true); }});
document.addEventListener('keydown', e => {{
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {{ if (currentIdx > 0) goTo(PAGES[currentIdx - 1], true); }}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {{ if (currentIdx < total - 1) goTo(PAGES[currentIdx + 1], true); }}
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {{ if (currentIdx > 0) goTo(PAGES[currentIdx - 1], true, true); }}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {{ if (currentIdx < total - 1) goTo(PAGES[currentIdx + 1], true, true); }}
}});
// Track current page by scroll position
// Track current page by scroll position (replaceState — no new history entry)
content.addEventListener('scroll', () => {{
const containerTop = content.getBoundingClientRect().top;
let found = 0;
@@ -293,13 +294,22 @@ def build_index(title, DIR):
if (sec && sec.getBoundingClientRect().top - containerTop <= 40) found = i;
else break;
}}
if (found !== currentIdx) updateNav(found);
if (found !== currentIdx) updateNav(found, false);
}}, {{ passive: true }});
// Browser back/forward
window.addEventListener('popstate', () => {{
const pm = location.hash.match(/^#page-(\\d+)$/);
if (pm) {{
const n = parseInt(pm[1], 10);
if (PAGES.includes(n)) goTo(n, false, false);
}}
}});
// Initial navigation from URL hash
const m = location.hash.match(/^#page-(\\d+)$/);
const startPage = m ? parseInt(m[1], 10) : PAGES[0];
goTo(PAGES.includes(startPage) ? startPage : PAGES[0], false);
goTo(PAGES.includes(startPage) ? startPage : PAGES[0], false, false);
</script>
</body>
</html>