Text layers: actually enter edit mode on second click (defer focus + relookup)

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-27 09:04:39 -04:00
1 parent df45b63f45
commit dc31b023d5
1 file changed
+31 -19
+31 -19
View File
@@ -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));
};