From 93f020f2b569b013a846e034e0c688db88a6c348 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 11 Oct 2026 11:21:51 -0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20PDF=20adjust=20handles=20=E2=80=94?= =?UTF-8?q?=20create=20once,=20reposition=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- web/src/reader/reader.ts | 75 ++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index fec3af81..ba2702f6 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -1737,47 +1737,54 @@ document.addEventListener("alpine:init", () => { if (!first || !last) return; // Start dot at the first rect's LEFT-MIDDLE, end dot at the last // 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: "end", fx: last[0] + last[2], fy: last[1] + last[3] / 2 }, ]; 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( + `.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 dot = document.createElement("div"); - dot.className = "adjust-handle"; - dot.dataset.which = pt.which; dot.style.left = `${host.x - 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