Skip to content

Commit a901cd8

Browse files
committed
fix(mdviewer): scroll sync feedback loop, theme reflow, lang-picker scroll
- Fix CM→viewer scroll sync in edit mode: use _scrollFromViewer flag to block only feedback loops, not all scroll-based sync - Skip highlight (not scroll) when viewer has focus to prevent cursor displacement while still allowing scroll restoration - Prevent theme reflow on reload by skipping if already applied - Hide lang-picker on scroll
1 parent b4a96ac commit a901cd8

2 files changed

Lines changed: 19 additions & 10 deletions

File tree

src-mdviewer/src/bridge.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let _syncId = 0;
1414
let _lastReceivedSyncId = -1;
1515
let _suppressContentChange = false;
1616
let _scrollFromCM = false;
17+
let _scrollFromViewer = false;
1718
let _baseURL = "";
1819
let _cursorPosBeforeEdit = null; // cursor position before current edit batch
1920
let _cursorPosDirty = false; // true after content changes, reset when emitted
@@ -426,6 +427,8 @@ export function initBridge() {
426427
const sourceLine = _getSourceLineFromElement(e.target);
427428
if (getState().editMode) {
428429
if (sourceLine != null) {
430+
_scrollFromViewer = true;
431+
setTimeout(() => { _scrollFromViewer = false; }, 500);
429432
sendToParent("mdviewrScrollSync", { sourceLine });
430433
}
431434
return;
@@ -798,10 +801,11 @@ function handleReloadFile(data) {
798801
// --- Theme, edit mode, locale ---
799802

800803
function handleSetTheme(data) {
801-
const { theme } = data;
802804
// Force light theme for a paper-like appearance regardless of editor theme.
803805
// The theme infrastructure is preserved for future use.
804806
const appliedTheme = "light";
807+
// Skip if already applied to avoid reflows that can reset scroll position
808+
if (document.documentElement.getAttribute("data-theme") === appliedTheme) return;
805809
document.documentElement.setAttribute("data-theme", appliedTheme);
806810
document.documentElement.style.colorScheme = "light";
807811
setState({ theme: appliedTheme });
@@ -975,18 +979,14 @@ function handleScrollToLine(data) {
975979
const { line, fromScroll, tableCol } = data;
976980
if (line == null) return;
977981

978-
// In edit mode, ignore scroll-based sync from CM to prevent feedback
979-
// loops (click in viewer → CM scroll → scroll sync back → viewer jumps).
980-
if (fromScroll && getState().editMode) return;
982+
// In edit mode, ignore scroll-based sync that originated from the viewer
983+
// itself (feedback loop: viewer click → CM scroll → scroll sync back).
984+
if (fromScroll && getState().editMode && _scrollFromViewer) return;
981985

982986
const viewer = document.getElementById("viewer-content");
983987
if (!viewer) return;
984988

985-
// In edit mode, skip CM cursor sync when the viewer has focus — the user
986-
// is actively editing and highlight span creation/removal would displace
987-
// the cursor.
988-
if (getState().editMode && viewer.contains(document.activeElement)) return;
989-
if (!viewer) return;
989+
const skipHighlight = getState().editMode && viewer.contains(document.activeElement);
990990

991991
const elements = viewer.querySelectorAll("[data-source-line]");
992992
let bestEl = null;
@@ -1089,7 +1089,8 @@ function handleScrollToLine(data) {
10891089
setTimeout(() => { _scrollFromCM = false; }, 200);
10901090

10911091
// Persistent highlight on the element corresponding to the CM cursor.
1092-
// Only show when CM has focus (not when viewer has focus).
1092+
// Skip highlight when viewer has focus to avoid cursor displacement.
1093+
if (skipHighlight) return;
10931094
_removeCursorHighlight(viewer);
10941095

10951096
// For <br> paragraphs, wrap only the specific line's content in a

src-mdviewer/src/components/lang-picker.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,12 @@ export function isLangPickerDropdownOpen() {
329329
return dropdownOpen;
330330
}
331331

332+
function onScroll() {
333+
if (picker && picker.classList.contains("visible")) {
334+
hide();
335+
}
336+
}
337+
332338
export function initLangPicker(editorEl) {
333339
contentEl = editorEl;
334340
buildPicker();
@@ -338,6 +344,7 @@ export function initLangPicker(editorEl) {
338344

339345
document.addEventListener("selectionchange", updatePosition);
340346
contentEl.addEventListener("mouseup", updatePosition);
347+
document.addEventListener("scroll", onScroll, true);
341348
contentEl.addEventListener("keyup", updatePosition);
342349
document.addEventListener("mousedown", onDocumentMousedown);
343350
}
@@ -351,6 +358,7 @@ export function destroyLangPicker() {
351358
}
352359
document.removeEventListener("selectionchange", updatePosition);
353360
document.removeEventListener("mousedown", onDocumentMousedown);
361+
document.removeEventListener("scroll", onScroll, true);
354362
if (picker) picker.innerHTML = "";
355363
contentEl = null;
356364
currentPre = null;

0 commit comments

Comments
 (0)