From 5179d0e49e7c309fca9ad6b93cdafaba4c95d03f Mon Sep 17 00:00:00 2001 From: Local Dev Date: Tue, 22 Sep 2026 21:33:48 +0200 Subject: [PATCH] fix(pdf-editor): a shape can be resized the moment it is drawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drawing a rectangle left it selected with its handles showing, and then refused to let you touch them. The handles were only live under the select tool, so sizing the shape you were still looking at meant a trip to the toolbar and back — for a gesture the editor had already drawn the grips for. Handles are now grabbable whatever tool is armed. They cannot be confused with drawing: a handle is a nine-pixel square that only exists while something is selected, nobody lands on one by accident, and Escape drops the selection if the space is wanted back for drawing. The original gate was protecting against a collision that does not really happen, at the cost of one that does. Making them universal opened a trap in the release path, fixed here too: the text-markup tools return early from onUp to commit a text selection, which would have stranded a resize half-done — applied to the live mark, never journalled, with the drag still set so the next press behaved oddly. A handle drag is now finished first, whatever tool is armed. --- bundled-addons/pdf-editor/editor.css | 10 +++-- bundled-addons/pdf-editor/lib/tools.js | 57 ++++++++++++++++---------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/bundled-addons/pdf-editor/editor.css b/bundled-addons/pdf-editor/editor.css index a6bb2d9c..8e65d2fb 100644 --- a/bundled-addons/pdf-editor/editor.css +++ b/bundled-addons/pdf-editor/editor.css @@ -260,10 +260,12 @@ body[data-dragging="1"] .selbar, .btn.styl.active { background: rgb(from var(--acid) r g b / .16); border-color: rgb(from var(--acid) r g b / .7); color: var(--acid); } -/* Resize handles. Only grabbable with the select tool armed, so a drawing - drag near a selected mark is never stolen by one. */ -body[data-mode="select"] .smOverlay .handle { pointer-events: all; } -.smOverlay .handle { pointer-events: none; } +/* Resize handles stay grabbable whatever tool is armed, so a shape can be + sized the moment it is drawn without a detour through the select tool. + Safe to leave live because a handle is a 9 px square that exists only while + something is selected — aiming at one is not something you do by accident, + and Escape clears the selection if you want the space back for drawing. */ +.smOverlay .handle { pointer-events: all; } /* The invisible grab area under a thin stroke. `stroke` rather than `visibleStroke`: the copy is transparent on purpose. */ diff --git a/bundled-addons/pdf-editor/lib/tools.js b/bundled-addons/pdf-editor/lib/tools.js index f78221fd..5125d4c0 100644 --- a/bundled-addons/pdf-editor/lib/tools.js +++ b/bundled-addons/pdf-editor/lib/tools.js @@ -135,26 +135,31 @@ export class Tools { onDown(e) { if (e.button !== 0) return; - // A resize handle is checked before anything else: it sits outside the - // mark's own bounds, so the page under it may not even be the page the - // mark is on. - if (this.tool === "select") { - const handle = e.target?.closest?.(".handle")?.dataset?.handle; - if (handle && this.selected) { - const a = this.model.annot(this.selected); - const ov = a && this.strip.overlayFor(a.page); - if (a && ov?.viewport) { - const view = this.strip.views.get(a.page); - const r = view.pv.div.getBoundingClientRect(); - const [hx, hy] = ov.toPdf(e.clientX - r.left, e.clientY - r.top); - this.drag = { - kind: "resize", uid: a.page, id: a.id, handle, - x0: hx, y0: hy, orig: structuredClone(a), from: boundsOf(a), - cx: e.clientX, cy: e.clientY, moved: false, - }; - e.preventDefault(); - return; - } + // A resize handle is checked before anything else, and whatever tool is + // armed. Two reasons it comes first: it sits outside the mark's own + // bounds, so the page under it may not even be the page the mark is on; + // and a shape is selected the instant it is drawn, so grabbing a corner + // has to work right then, without a detour through the select tool to + // resize the thing you are still looking at. + // + // It cannot be confused with drawing. A handle is a 9 px square that + // exists only while something is selected; nobody aims at one by + // accident, and Escape drops the selection if the space is wanted back. + const handle = e.target?.closest?.(".handle")?.dataset?.handle; + if (handle && this.selected) { + const a = this.model.annot(this.selected); + const ov = a && this.strip.overlayFor(a.page); + if (a && ov?.viewport) { + const view = this.strip.views.get(a.page); + const r = view.pv.div.getBoundingClientRect(); + const [hx, hy] = ov.toPdf(e.clientX - r.left, e.clientY - r.top); + this.drag = { + kind: "resize", uid: a.page, id: a.id, handle, + x0: hx, y0: hy, orig: structuredClone(a), from: boundsOf(a), + cx: e.clientX, cy: e.clientY, moved: false, + }; + e.preventDefault(); + return; } } @@ -255,8 +260,16 @@ export class Tools { } onUp(e) { - if (TEXT_MARKUP.has(this.tool)) { this._commitTextMarkup(); return; } - const d = this.drag; + // A handle drag is finished here whatever tool is armed. Without this the + // text-markup tools would return on the line below and strand a resize + // half-done: applied to the live mark, never journalled, and with + // this.drag still set so the next press behaves oddly. + const inFlight = this.drag; + if (!(inFlight && inFlight.kind === "resize") && TEXT_MARKUP.has(this.tool)) { + this._commitTextMarkup(); + return; + } + const d = inFlight; this.drag = null; if (!d) return; this.strip.setPreview(null, null);