fix(reader): PDF highlights never appeared — isPDF read too early + overlay shrunk by pdf.js transform

Two bugs broke the Phase 3b PDF highlight flow end to end:

1. Selection capture never attached: reader.ts read renderer.isPDF
   before view.init() rendered the first spread, but the renderer
   only sets that flag once frames exist (PDF frames carry pdf.js
   onZoom). The stale undefined copy gated the pointerup selection
   listener off, so selecting PDF text did nothing. The listener now
   gates structurally on the loaded document having a .textLayer
   (true for every PDF page, false for comics), and isPDF is re-read
   after init — which also finally makes the Smart|Pan|Text control
   and the saved pointer mode apply on PDFs.

2. Highlights rendered invisibly: the overlay SVG lived inside the
   page iframe, whose <html> pdf.js scales by 1/devicePixelRatio —
   shrinking the overlay into the top-left corner on any dpr != 1
   display. The fork (1c0ebf3) now renders annotation rects
   host-side, inside the frame wrapper element, positioned in
   percentages of the visible page box — immune to the html
   transform, zoom re-renders, comic iframe scaling, and pan/zoom.
This commit is contained in:
2026-08-17 07:47:03 -04:00
parent a05b0167ad
commit eb09a5d939
2 changed files with 13 additions and 5 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"dev": "npm run build:ts:dev && npm run build:css" "dev": "npm run build:ts:dev && npm run build:css"
}, },
"dependencies": { "dependencies": {
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#aba68d8", "@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#1c0ebf3",
"alpinejs": "^3.15.8", "alpinejs": "^3.15.8",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
+12 -4
View File
@@ -576,9 +576,6 @@ document.addEventListener("alpine:init", () => {
this.book.dir = "rtl"; this.book.dir = "rtl";
} }
this.isPDF = (this.renderer as any).isPDF; this.isPDF = (this.renderer as any).isPDF;
if (this.isPDF) {
this.renderer.setAttribute("interaction-mode", this.interactionMode);
}
// Click on a PDF/fixed-layout highlight rect → edit popover. // Click on a PDF/fixed-layout highlight rect → edit popover.
this.renderer.addEventListener( this.renderer.addEventListener(
"show-rect-annotation", "show-rect-annotation",
@@ -671,7 +668,10 @@ document.addEventListener("alpine:init", () => {
); );
} }
// PDF textLayer selection → rect-fraction highlight popover. // PDF textLayer selection → rect-fraction highlight popover.
if (this.isPDF) { // Detected structurally: renderer.isPDF isn't set until the first
// spread renders (during view.init), which is after this listener
// attaches — the stale copy here would always be falsy.
if (this.isFixedLayout && doc.querySelector(".textLayer")) {
const checkPDFSelection = () => { const checkPDFSelection = () => {
const sel = doc.getSelection(); const sel = doc.getSelection();
if (!sel || sel.isCollapsed || !sel.rangeCount) return; if (!sel || sel.isCollapsed || !sel.rangeCount) return;
@@ -816,6 +816,14 @@ document.addEventListener("alpine:init", () => {
} else { } else {
await this.view.init({}) await this.view.init({})
} }
// The renderer only knows it's a PDF once frames exist (they carry
// pdf.js onZoom), i.e. after init has rendered the first spread.
// Read it now and apply the saved pointer mode — this also makes the
// Smart|Pan|Text control appear for PDFs.
this.isPDF = !!this.renderer?.isPDF;
if (this.isPDF) {
this.renderer.setAttribute("interaction-mode", this.interactionMode);
}
this.initTime = Date.now(); this.initTime = Date.now();
this.fetchReadingSpeed(); this.fetchReadingSpeed();
this.refreshAnnotations(); this.refreshAnnotations();