From b7597d61594fcdecd0aeea6d3b28ff29a8df127a Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 3 Oct 2026 22:35:28 -0400 Subject: [PATCH] fix(paginator): guard getVisibleRange against not-yet-loaded documents Second member of the 50f56ee guard family: the scroll-anchor tick (#afterScroll) can run before the iframe document is parsed, throwing 'createTreeWalker' on null body. getVisibleRange now returns null for that tick and #afterScroll preserves the previous visibility state. Observed live in the Android app on PalmDOC open. --- package.json | 2 +- paginator.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f3a14b5..f50940f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foliate-js", - "version": "0.5.1", + "version": "0.5.2", "description": "Render e-books in the browser", "repository": { "type": "git", diff --git a/paginator.js b/paginator.js index fa9dbef..f2389eb 100644 --- a/paginator.js +++ b/paginator.js @@ -92,6 +92,10 @@ const getBoundingClientRect = target => { } const getVisibleRange = (doc, start, end, mapRect) => { + // Same teardown race as expand(): a scroll-anchor tick can run + // against a not-yet-parsed iframe document (no body / documentElement + // yet) — return a null range so the caller skips visibility math. + if (!doc || !doc.body || !doc.documentElement) return null // first get all visible nodes const acceptNode = node => { const name = node.localName?.toLowerCase() @@ -1107,6 +1111,9 @@ export class Paginator extends HTMLElement { } #afterScroll(reason) { const range = this.#getVisibleRange() + // Torn-down/not-yet-loaded document tick — preserve the previous + // visibility state instead of anchoring on a null range. + if (!range) return this.#lastVisibleRange = range // don't set new anchor if relocation was to scroll to anchor if (reason !== 'selection' && reason !== 'navigation' && reason !== 'anchor')