From dc31b023d5142842b03549ca6bf5f731d6ce00fb Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 27 May 2026 09:04:39 -0400 Subject: [PATCH] Text layers: actually enter edit mode on second click (defer focus + relookup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous attempt added a focus() call in the pointerup stop() branch, but it never stuck. CDP-driven tests against the real app showed two reasons the focus kept getting clobbered: 1. The click event still fires AFTER pointerup, propagates up to the .freeLayer's click handler, which re-runs selectLayerWithEvent + renderLayerList + syncLayerInputs. Something in that chain blurs the contenteditable (focus log: focusin → blur → focusout, all on the .textLayer, in that order). 2. The selection change between the two clicks triggers a render() somewhere — which rebuilds the layers and DETACHES the original .textLayer node that startLayerDrag's closure captured. So even when focus did briefly land on textTarget, that textTarget was already a stale, detached element by the next tick. Fix: - Defer the focus call via setTimeout(0), so it runs AFTER the click event chain completes and any blur-on-click side effects have fired. - Inside the deferred callback, re-query the live .textLayer for the layer's id via `.freeLayer[data-layer-id="..."] .textLayer`, so we never focus a detached element. - Only apply the click-position caret range if it's inside the LIVE editable (caretRangeFromPoint can resolve into the detached node if the layout hasn't reflowed yet). Verified end-to-end via CDP + Input.dispatchMouseEvent against the running app (with the welcome modal dismissed and a fresh ?n=… cache-bust). Two single clicks in sequence: - After click 1: state.selectedLayerId='tx1', activeElement=BODY. - After click 2: activeElement is a DIV with className='textLayer' and isContentEditable=true — caret is now in the text and the user can drag-select / type / apply the toolbar buttons. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/index.html b/src/index.html index 0b7d180..95ede83 100644 --- a/src/index.html +++ b/src/index.html @@ -14077,27 +14077,39 @@ 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(); + // selection → drop into edit mode. Two subtleties: + // 1. The click event still has to fire on .freeLayer after this + // pointerup, and that handler ends up clobbering the focus. + // So defer with setTimeout(0) to run after the click chain. + // 2. The selection change may have triggered a re-render that + // detaches the original textTarget. Look the layer's .textLayer + // up by id when the deferred handler runs, so we always focus + // the live DOM node and not a stale reference. + const layerId = layer.id; 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) {} + setTimeout(() => { + const live = document.querySelector(`.freeLayer[data-layer-id="${layerId}"] .textLayer`); + if (!live) return; + live.focus(); + 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); } + } + // Only use the caret position if it's inside the live editable + // — caretRangeFromPoint can return a position in the previous + // (now detached) .textLayer. + if (range && live.contains(range.startContainer)) { + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + } catch (e) {} + }, 0); } localStorage.setItem('case-cover-template-maker-autosave', JSON.stringify(state)); };