fix(reader): PDF selection popover shows reliably on touch, below selection
Chromium's touch selection takeover swallows pointerup in the fx iframe, so the PDF popover's only trigger never fired on real phones — it only appeared when timing happened to deliver the event. Mirror the EPUB path: a selectionchange debounce (400ms settle) opens the popover, with the pointerup path kept for desktop plus the quick-tap word-select guard. Also place the popover BELOW the selection on coarse pointers (+90px clearing the native selection menu and drag handles), matching the reflowable path; desktop keeps above-placement.
This commit is contained in:
+106
-58
@@ -724,7 +724,9 @@ document.addEventListener("alpine:init", () => {
|
||||
// drags, so the popover's pointerup trigger never fires for
|
||||
// them; a selection that has been stable for a moment is the
|
||||
// settled gesture — open the popover for it.
|
||||
if (!this.isFixedLayout)
|
||||
if (this.isFixedLayout && doc.querySelector(".textLayer"))
|
||||
this.schedulePDFSelectionPopover(doc, index);
|
||||
else if (!this.isFixedLayout)
|
||||
this.scheduleSelectionPopover(doc, index);
|
||||
});
|
||||
}
|
||||
@@ -785,65 +787,23 @@ document.addEventListener("alpine:init", () => {
|
||||
// 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 sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
// Denominator: the element whose post-transform screen rect IS
|
||||
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
||||
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
||||
// is dpr× too small and would inflate every fraction (highlight
|
||||
// shifted right/oversized on any dpr != 1 display). The canvas's
|
||||
// rect is in the same transform-inclusive space as the textLayer
|
||||
// span rects, so the dpr scaling cancels exactly.
|
||||
const denom =
|
||||
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
||||
(doc.querySelector("img") as HTMLElement);
|
||||
let dr = denom?.getBoundingClientRect();
|
||||
if (!dr || !dr.width || !dr.height) {
|
||||
const vw = doc.defaultView;
|
||||
dr = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: vw?.innerWidth || 1,
|
||||
height: vw?.innerHeight || 1,
|
||||
} as DOMRect;
|
||||
}
|
||||
const rects: number[][] = [];
|
||||
for (const r of range.getClientRects()) {
|
||||
const x = (r.left - dr.left) / dr.width;
|
||||
const y = (r.top - dr.top) / dr.height;
|
||||
const w = r.width / dr.width;
|
||||
const h = r.height / dr.height;
|
||||
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
||||
}
|
||||
if (!rects.length) return;
|
||||
// Map the first rect to host-space for popover placement.
|
||||
// The canvas's screen rect maps 1:1 onto the host iframe box
|
||||
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
||||
// kept general for the comic img fallback.
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
if (!frame) return;
|
||||
const fr = frame.getBoundingClientRect();
|
||||
const first = range.getBoundingClientRect();
|
||||
const sx = fr.width / dr.width;
|
||||
const sy = fr.height / dr.height;
|
||||
this.pdfSelDoc = doc;
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
||||
y: fr.top + first.top * sy,
|
||||
text,
|
||||
cfi: "",
|
||||
pdfPage: index,
|
||||
pdfRects: rects,
|
||||
});
|
||||
};
|
||||
doc.addEventListener(
|
||||
"pointerup",
|
||||
() => setTimeout(checkPDFSelection, 0),
|
||||
(e: PointerEvent) =>
|
||||
setTimeout(() => {
|
||||
// Chrome on touch selects the word under a quick tap —
|
||||
// that is a page-turn tap, not a selection gesture; clear
|
||||
// it and show nothing (same guard as the reflowable path).
|
||||
if (
|
||||
e.pointerType === "touch" &&
|
||||
this.lastTouchDownT &&
|
||||
Date.now() - this.lastTouchDownT < 300
|
||||
) {
|
||||
doc.getSelection()?.removeAllRanges();
|
||||
return;
|
||||
}
|
||||
this.checkPDFSelectionFor(doc, index);
|
||||
}, 0),
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
@@ -1332,6 +1292,94 @@ document.addEventListener("alpine:init", () => {
|
||||
this.openPopoverForSelection(sel, doc, index);
|
||||
}, 400);
|
||||
},
|
||||
// PDF mirror of scheduleSelectionPopover: the gesture takeover
|
||||
// swallows pointerup for touch selection drags in the fx iframe too,
|
||||
// so a selection that has been stable for a moment is the settled
|
||||
// gesture — open the popover for it.
|
||||
schedulePDFSelectionPopover(doc: any, index: number) {
|
||||
if (this.selPopTimer) clearTimeout(this.selPopTimer);
|
||||
this.selPopTimer = setTimeout(() => {
|
||||
this.selPopTimer = null;
|
||||
// A tap on a painted highlight re-opens the popover in edit mode
|
||||
// via its own path — never clobber it.
|
||||
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
|
||||
return;
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
||||
if (
|
||||
this.selectionPopover.open &&
|
||||
this.selectionPopover.mode === "create"
|
||||
)
|
||||
this.hideSelectionPopover();
|
||||
return;
|
||||
}
|
||||
this.checkPDFSelectionFor(doc, index);
|
||||
}, 400);
|
||||
},
|
||||
checkPDFSelectionFor(doc: any, index: number) {
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
// Denominator: the element whose post-transform screen rect IS
|
||||
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
||||
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
||||
// is dpr× too small and would inflate every fraction (highlight
|
||||
// shifted right/oversized on any dpr != 1 display). The canvas's
|
||||
// rect is in the same transform-inclusive space as the textLayer
|
||||
// span rects, so the dpr scaling cancels exactly.
|
||||
const denom =
|
||||
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
||||
(doc.querySelector("img") as HTMLElement);
|
||||
let dr = denom?.getBoundingClientRect();
|
||||
if (!dr || !dr.width || !dr.height) {
|
||||
const vw = doc.defaultView;
|
||||
dr = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: vw?.innerWidth || 1,
|
||||
height: vw?.innerHeight || 1,
|
||||
} as DOMRect;
|
||||
}
|
||||
const rects: number[][] = [];
|
||||
for (const r of range.getClientRects()) {
|
||||
const x = (r.left - dr.left) / dr.width;
|
||||
const y = (r.top - dr.top) / dr.height;
|
||||
const w = r.width / dr.width;
|
||||
const h = r.height / dr.height;
|
||||
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
||||
}
|
||||
if (!rects.length) return;
|
||||
// Map the first rect to host-space for popover placement.
|
||||
// The canvas's screen rect maps 1:1 onto the host iframe box
|
||||
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
||||
// kept general for the comic img fallback.
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
if (!frame) return;
|
||||
const fr = frame.getBoundingClientRect();
|
||||
const first = range.getBoundingClientRect();
|
||||
const sx = fr.width / dr.width;
|
||||
const sy = fr.height / dr.height;
|
||||
// On touch the browser's own selection menu attaches ABOVE the
|
||||
// selection and its drag handles sit just below it — place ours
|
||||
// below both (same +90 clearance the reflowable path uses). Fine
|
||||
// pointers (desktop) keep the above-the-selection placement.
|
||||
const coarse = window.matchMedia("(pointer: coarse)").matches;
|
||||
const y = coarse
|
||||
? fr.top + first.bottom * sy + 90
|
||||
: fr.top + first.top * sy;
|
||||
this.pdfSelDoc = doc;
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
||||
y,
|
||||
text,
|
||||
cfi: "",
|
||||
pdfPage: index,
|
||||
pdfRects: rects,
|
||||
});
|
||||
},
|
||||
// Trim a touch selection to the visible page. The visible column
|
||||
// occupies content x [start - size, start]; endpoints beyond it are
|
||||
// re-mapped to the page edge on their own line via caretRangeFromPoint.
|
||||
|
||||
Reference in New Issue
Block a user