From df45b63f454a2e58ffb408cc794d38d3e100890f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 27 May 2026 08:50:17 -0400 Subject: [PATCH] Text layers: click already-selected text to enter edit mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous click flow only entered edit mode on a true double-click (event.detail > 1 on the second pointerdown), which is timing- sensitive — slow second clicks fell through to layer-drag and edit mode was hard to reach. Result: users clicked the text and the whole layer kept "selecting" as an object. Three changes to startLayerDrag to fix it: 1. Capture `wasSoleSelected = isLayerSelected(layer.id) && selectedLayerIds().length === 1` at the top of the handler, BEFORE the selection logic runs. This tells us whether the user was clicking on a text layer that was already the lone selection — the standard signal in PowerPoint / Keynote / Google Slides that the user wants to edit the text now. 2. A small dead-zone in the move handler (Math.hypot(dx, dy) < 4 ignores tiny jitter). Without it, a click with a 1-pixel mouse wobble flipped `moved` to true and skipped the edit-mode path. 3. New branch in stop() — if the pointer was released without crossing the dead-zone (moved === false), the click target was inside .textLayer, and wasSoleSelected was true, focus the contenteditable and place the caret at the click position via document.caretRangeFromPoint (with a caretPositionFromPoint fallback for Firefox-style browsers). Net behaviour: - First click on an unselected text layer → selects (drag handles appear, no edit mode). - Second click on the same now-selected text layer → enters edit mode, caret lands where the click was. - Click + drag on a selected text layer → drags normally (moved > 4px, edit-mode branch skipped). - Existing double-click path (event.detail > 1) still works as a belt-and-suspenders fallback for very fast double-clicks. CDP synthesized-event tests couldn't fully drive the pointer- capture path so end-to-end verification needs a real browser; the logic was code-reviewed. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/index.html b/src/index.html index 7dd5311..0b7d180 100644 --- a/src/index.html +++ b/src/index.html @@ -13993,6 +13993,11 @@ if (layer.locked) { setLayerSelection([layer.id], layer.id); render(); return; } const textTarget = event.target.closest('.textLayer'); if (!textTarget) blurEditableFocus(); + // Capture BEFORE we touch selection state: was this text layer + // already the lone-selected layer when the user clicked it? If so, + // a click-without-drag should drop the user into edit mode (the + // standard slide-editor UX). Stashed for the pointerup branch below. + const wasSoleSelected = isLayerSelected(layer.id) && selectedLayerIds().length === 1; const additiveSelect = !!(event.shiftKey || event.ctrlKey || event.metaKey); if (additiveSelect) { event.preventDefault(); @@ -14029,9 +14034,17 @@ el.setPointerCapture(event.pointerId); const move = (moveEvent) => { + // Small dead-zone so a click with a tiny pixel jitter isn't + // interpreted as a drag. Anything inside this radius is ignored; + // the user can still enter text-edit mode via the pointerup branch. + if (!moved) { + const ddx = moveEvent.clientX - startX; + const ddy = moveEvent.clientY - startY; + if (Math.hypot(ddx, ddy) < 4) return; + } if (!snapshotted) { - // Capture the pre-drag state on the first pointermove so a click - // without movement doesn't pollute the undo stack. + // Capture the pre-drag state on the first real pointermove so a + // click without movement doesn't pollute the undo stack. pushHistory(dragLayers.length > 1 ? 'move selected layers' : 'move layer'); snapshotted = true; } @@ -14054,7 +14067,7 @@ syncLayerInputs(); }; - const stop = () => { + const stop = (upEvent) => { document.body.classList.remove('dragging-layer'); el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', stop); @@ -14062,6 +14075,29 @@ if (moved) { el.dataset.dragMoved = '1'; setTimeout(() => { delete el.dataset.dragMoved; }, 0); + } else if (textTarget && wasSoleSelected) { + // Click without drag on a text layer that was already the lone + // selection → drop into edit mode. Focus the contenteditable and + // place the caret at the click position so the user can start + // selecting / typing immediately. This is the standard "select + // once, click again to edit" pattern from PowerPoint / Keynote. + textTarget.focus(); + const cx = (upEvent && typeof upEvent.clientX === 'number') ? upEvent.clientX : startX; + const cy = (upEvent && typeof upEvent.clientY === 'number') ? upEvent.clientY : startY; + try { + let range = null; + if (typeof document.caretRangeFromPoint === 'function') { + range = document.caretRangeFromPoint(cx, cy); + } else if (typeof document.caretPositionFromPoint === 'function') { + const pos = document.caretPositionFromPoint(cx, cy); + if (pos) { range = document.createRange(); range.setStart(pos.offsetNode, pos.offset); range.collapse(true); } + } + if (range) { + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + } catch (e) {} } localStorage.setItem('case-cover-template-maker-autosave', JSON.stringify(state)); };