Skip to content

Commit 41a9dec

Browse files
committed
fix(editor): prevent focus loss when typing in header/footer editors (SD-1993)
Replace unconditional appendChild calls in DomPainter.updateVirtualWindow() with cursor-based DOM reconciliation that skips moves for elements already in the correct position. Add focus save/restore safety net in PresentationEditor.#flushRerenderQueue for edge cases.
1 parent 17117aa commit 41a9dec

2 files changed

Lines changed: 35 additions & 3 deletions

File tree

packages/layout-engine/painters/dom/src/renderer.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,8 +1709,13 @@ export class DomPainter {
17091709
}
17101710
this.mount.appendChild(this.bottomSpacerEl);
17111711

1712-
// Ensure mounted pages are ordered (with gap spacers) before bottom spacer.
1712+
// Ensure mounted pages are ordered (with gap spacers).
1713+
// Use cursor-based reconciliation to skip DOM moves for elements already in
1714+
// the correct position. Moving an element via appendChild/insertBefore triggers
1715+
// a browser blur event on any focused descendant, which breaks header/footer
1716+
// in-place editing where a PM editor lives inside a page element (SD-1993).
17131717
let prevIndex: number | null = null;
1718+
let cursor: ChildNode | null = this.virtualPagesEl.firstChild;
17141719
for (const idx of mounted) {
17151720
if (prevIndex != null && idx > prevIndex + 1) {
17161721
const gap = this.doc!.createElement('div');
@@ -1721,10 +1726,16 @@ export class DomPainter {
17211726
this.topOfIndex(idx) - this.topOfIndex(prevIndex) - this.virtualHeights[prevIndex] - this.virtualGap * 2;
17221727
gap.style.height = `${Math.max(0, Math.floor(gapHeight))}px`;
17231728
this.virtualGapSpacers.push(gap);
1724-
this.virtualPagesEl.appendChild(gap);
1729+
this.virtualPagesEl.insertBefore(gap, cursor);
17251730
}
17261731
const state = this.pageIndexToState.get(idx)!;
1727-
this.virtualPagesEl.appendChild(state.element);
1732+
if (state.element === cursor) {
1733+
// Already in the correct position. Skip the DOM mutation.
1734+
cursor = state.element.nextSibling;
1735+
} else {
1736+
// Out of order. Move to the correct position.
1737+
this.virtualPagesEl.insertBefore(state.element, cursor);
1738+
}
17281739
prevIndex = idx;
17291740
}
17301741

packages/super-editor/src/core/presentation-editor/PresentationEditor.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,13 +3165,34 @@ export class PresentationEditor extends EventEmitter {
31653165
}
31663166
this.#pendingDocChange = false;
31673167
this.#isRerendering = true;
3168+
3169+
// Capture H/F editor focus state before rerender so we can restore it if
3170+
// a DOM mutation (e.g. page re-ordering in updateVirtualWindow) causes the
3171+
// browser to blur the active header/footer editor (SD-1993).
3172+
const sessionMode = this.#headerFooterSession?.session?.mode ?? 'body';
3173+
const activeHfEditor = sessionMode !== 'body' ? this.#headerFooterSession?.activeEditor : null;
3174+
const hadHfFocus = activeHfEditor?.view?.hasFocus?.() ?? false;
3175+
31683176
try {
31693177
await this.#rerender();
31703178
} finally {
31713179
this.#isRerendering = false;
31723180
if (this.#pendingDocChange) {
31733181
this.#scheduleRerender();
31743182
}
3183+
3184+
// Restore focus if the H/F editor lost it during rerender.
3185+
if (hadHfFocus && activeHfEditor?.view) {
3186+
const doc = this.#visibleHost.ownerDocument;
3187+
const editorDom = activeHfEditor.view.dom;
3188+
if (doc && !editorDom.contains(doc.activeElement)) {
3189+
try {
3190+
activeHfEditor.view.focus();
3191+
} catch {
3192+
// Ignore focus errors during recovery
3193+
}
3194+
}
3195+
}
31753196
}
31763197
}
31773198

0 commit comments

Comments
 (0)