Skip to content

Commit 0f0f901

Browse files
committed
fix(mdviewer): cursor jump on formatBlock and edit-mode scroll sync
- Fix cursor jumping to previous line after applying heading format on empty lines: place cursor inside the newly created block element - Block CM scroll-to-line when viewer has focus in edit mode to prevent highlight span creation from displacing the cursor
1 parent 0ef3462 commit 0f0f901

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

src-mdviewer/src/bridge.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -960,12 +960,17 @@ function handleScrollToLine(data) {
960960

961961
// In edit mode, ignore scroll-based sync from CM to prevent feedback
962962
// loops (click in viewer → CM scroll → scroll sync back → viewer jumps).
963-
// Only cursor-based sync (fromScroll=false) should reposition the viewer.
964963
if (fromScroll && getState().editMode) return;
965964

966965
const viewer = document.getElementById("viewer-content");
967966
if (!viewer) return;
968967

968+
// In edit mode, skip CM cursor sync when the viewer has focus — the user
969+
// is actively editing and highlight span creation/removal would displace
970+
// the cursor.
971+
if (getState().editMode && viewer.contains(document.activeElement)) return;
972+
if (!viewer) return;
973+
969974
const elements = viewer.querySelectorAll("[data-source-line]");
970975
let bestEl = null;
971976
let bestLine = -1;

src-mdviewer/src/components/editor.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,33 @@ export function executeFormat(contentEl, command, value) {
402402
case "underline":
403403
document.execCommand(command, false, null);
404404
break;
405-
case "formatBlock":
405+
case "formatBlock": {
406406
document.execCommand("formatBlock", false, value);
407+
// After formatBlock on an empty element, the browser may lose
408+
// cursor position. Find the new block and place cursor inside it.
409+
const sel2 = window.getSelection();
410+
if (sel2 && sel2.rangeCount) {
411+
let block = sel2.anchorNode;
412+
if (block?.nodeType === Node.TEXT_NODE) block = block.parentElement;
413+
// If cursor ended up outside the target block type, find it
414+
const targetTag = value.replace(/[<>/]/g, "").toUpperCase();
415+
if (block && block.tagName !== targetTag) {
416+
// Look for the newly created block near the cursor
417+
const allBlocks = contentEl.querySelectorAll(targetTag);
418+
for (const b of allBlocks) {
419+
if (b.textContent.trim() === "" || b.contains(sel2.anchorNode)) {
420+
const r = document.createRange();
421+
r.setStart(b, 0);
422+
r.collapse(true);
423+
sel2.removeAllRanges();
424+
sel2.addRange(r);
425+
break;
426+
}
427+
}
428+
}
429+
}
407430
break;
431+
}
408432
case "createLink": {
409433
if (value) {
410434
document.execCommand("createLink", false, value);

0 commit comments

Comments
 (0)