fix(web): PDF adjust handles — create once, reposition only

User report (desktop): the endpoint handles wouldn't move.
renderPdfAdjustHandles rebuilt the handle layer via innerHTML="" on
every preview frame — the first drag move triggered a repaint that
DESTROYED the dot being dragged. A removed element loses pointer
capture, so real mouse/touch drags died silently after one pixel; the
pointerup never reached the detached dot either, so nothing saved and
the session preview reverted — the same mechanism behind the earlier
"resets to the original selection" report. CDP tests masked it because
synthetic events dispatched directly at the detached node still ran its
listeners.

The two dots are now created once per session and every later call only
repositions them (style updates move nothing the listeners live on).
Grip-era code had this reuse pattern; the endpoint rewrite regressed it.

Real-touch verified on the emulator (adb-injected drag): the handle
tracks, the release saves a multi-line extent with clean recomputed
text (3 rects, PUT applied).
This commit is contained in:
John O'Keefe committed 2026-10-11 11:21:51 -04:00
1 parent a3ecac3507
commit 93f020f2b5
1 file changed
+41 -34
+41 -34
View File
@@ -1737,47 +1737,54 @@ document.addEventListener("alpine:init", () => {
if (!first || !last) return; if (!first || !last) return;
// Start dot at the first rect's LEFT-MIDDLE, end dot at the last // Start dot at the first rect's LEFT-MIDDLE, end dot at the last
// rect's RIGHT-MIDDLE — the range boundary points. // rect's RIGHT-MIDDLE — the range boundary points.
const pts: Array<{ which: string; fx: number; fy: number }> = [ const pts: Array<{ which: "start" | "end"; fx: number; fy: number }> = [
{ which: "start", fx: first[0], fy: first[1] + first[3] / 2 }, { which: "start", fx: first[0], fy: first[1] + first[3] / 2 },
{ which: "end", fx: last[0] + last[2], fy: last[1] + last[3] / 2 }, { which: "end", fx: last[0] + last[2], fy: last[1] + last[3] / 2 },
]; ];
for (const pt of pts) { for (const pt of pts) {
// Create each dot ONCE per session; every later call only
// REPOSITIONS the existing element. An innerHTML rebuild here
// destroys the dot mid-drag — it loses pointer capture and the
// drag dies silently ("handles not moving", user report).
let dot = layer.querySelector<HTMLDivElement>(
`.adjust-handle[data-which="${pt.which}"]`,
);
if (!dot) {
dot = document.createElement("div");
dot.className = "adjust-handle";
dot.dataset.which = pt.which;
const which = pt.which;
// Pointer events (mouse + touch + pen); the page's own touch
// handlers are not engaged because the dots live in the HOST
// document, above the iframe.
dot.addEventListener("pointerdown", (e) => {
e.preventDefault();
try {
dot!.setPointerCapture(e.pointerId);
} catch {}
dot!.dataset.dragging = "1";
});
dot.addEventListener("pointermove", (e) => {
if (!dot!.dataset.dragging) return;
this.pdfAdjustHandleMove(which, e.clientX, e.clientY);
});
dot.addEventListener("pointerup", (e) => {
if (!dot!.dataset.dragging) return;
delete dot!.dataset.dragging;
try {
dot!.releasePointerCapture?.(e.pointerId);
} catch {}
this.pdfAdjustRelease();
});
dot.addEventListener("pointercancel", () => {
if (!dot!.dataset.dragging) return;
delete dot!.dataset.dragging;
});
layer.appendChild(dot);
}
const host = map.toHost(pt.fx, pt.fy); const host = map.toHost(pt.fx, pt.fy);
const dot = document.createElement("div");
dot.className = "adjust-handle";
dot.dataset.which = pt.which;
dot.style.left = `${host.x - 13}px`; dot.style.left = `${host.x - 13}px`;
dot.style.top = `${host.y - 13}px`; dot.style.top = `${host.y - 13}px`;
// Pointer events (mouse + touch + pen); the page's own touch
// handlers are not engaged because the dots live in the HOST
// document, above the iframe.
dot.addEventListener("pointerdown", (e) => {
e.preventDefault();
try {
dot.setPointerCapture(e.pointerId);
} catch {}
dot.dataset.dragging = "1";
});
dot.addEventListener("pointermove", (e) => {
if (!dot.dataset.dragging) return;
this.pdfAdjustHandleMove(
pt.which,
e.clientX,
e.clientY,
);
});
dot.addEventListener("pointerup", (e) => {
if (!dot.dataset.dragging) return;
delete dot.dataset.dragging;
try {
dot.releasePointerCapture?.(e.pointerId);
} catch {}
this.pdfAdjustRelease();
});
dot.addEventListener("pointercancel", () => {
delete dot.dataset.dragging;
});
layer.appendChild(dot);
} }
}, },
/** Endpoint drag: the finger (host px) re-anchors the grabbed end at /** Endpoint drag: the finger (host px) re-anchors the grabbed end at