From 56726638ec1fc8c61994bf89fa42529e64130568 Mon Sep 17 00:00:00 2001 From: Scott Latz Date: Mon, 8 Jun 2026 08:31:52 -0500 Subject: [PATCH 1/2] Fix preview pane scrolling to bottom on checkbox click When clicking a task-list checkbox in preview mode, dispatching the source edit could trigger scroll-sync via the editor's scrollDOM and jump the preview to the editor's cursor position (often the bottom of the document). Save and restore the preview pane's scroll position around the dispatch to prevent this. Co-Authored-By: Claude Sonnet 4.6 --- src/view.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/view.ts b/src/view.ts index cf5cb16..4e9f9f0 100644 --- a/src/view.ts +++ b/src/view.ts @@ -377,10 +377,17 @@ function handleTaskItemToggle(event: MouseEvent) { // Let the native toggle stand for instant feedback; just sync the source const from = lineRange.from + toggle.offset; + + // Dispatch can trigger scroll-sync via the editor's scrollDOM; preserve preview position. + const savedScrollTop = previewPane.scrollTop; + MarkEdit.editorView.dispatch({ changes: { from, to: from + 1, insert: toggle.replacement }, annotations: silentChange.of(true), }); + + previewPane.scrollTop = savedScrollTop; + requestAnimationFrame(() => { previewPane.scrollTop = savedScrollTop; }); } const states: { From 9e7212fc79d8469975fb9fae7fe37b817b3339b9 Mon Sep 17 00:00:00 2001 From: Scott Latz Date: Mon, 8 Jun 2026 08:43:43 -0500 Subject: [PATCH 2/2] Fix preview pane scrolling to bottom on checkbox click When clicking a task-list checkbox in preview mode, the editor's scrollDOM could emit a scrollend event that triggered syncScrollProgress and jumped the preview to the editor's cursor position (often the bottom of the document). Instead of saving/restoring scrollTop (which risks overriding user momentum scrolling from a delayed rAF callback), suppress the scroll-sync listener for the duration of the dispatch and re-enable it on the next animation frame. The preview position is never touched, so user scroll state is fully preserved. Co-Authored-By: Claude Sonnet 4.6 --- src/scroll.ts | 20 ++++++++++++++++++-- src/view.ts | 12 +++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/scroll.ts b/src/scroll.ts index 1a56bb7..62fa8eb 100644 --- a/src/scroll.ts +++ b/src/scroll.ts @@ -8,7 +8,11 @@ export function startObserving(sourcePane: HTMLElement, targetPane: HTMLElement) } if ('onscrollend' in window) { - sourcePane.addEventListener('scrollend', () => syncScrollProgress(sourcePane, targetPane)); + sourcePane.addEventListener('scrollend', () => { + if (!states.syncSuppressed) { + syncScrollProgress(sourcePane, targetPane); + } + }); } else { sourcePane.addEventListener('scroll', () => { if (states.scrollUpdater !== undefined) { @@ -16,12 +20,22 @@ export function startObserving(sourcePane: HTMLElement, targetPane: HTMLElement) } states.scrollUpdater = setTimeout(() => { - syncScrollProgress(sourcePane, targetPane); + if (!states.syncSuppressed) { + syncScrollProgress(sourcePane, targetPane); + } }, 100); }); } } +export function suppressScrollSync() { + states.syncSuppressed = true; +} + +export function resumeScrollSync() { + states.syncSuppressed = false; +} + export function syncScrollProgress(sourcePane: HTMLElement, targetPane: HTMLElement, animated = true) { const { line, progress } = getScrollProgress(sourcePane); scrollToProgress(targetPane, line, progress, animated); @@ -136,6 +150,8 @@ function clampProgressValue(value: number) { const states: { scrollUpdater: ReturnType | undefined; + syncSuppressed: boolean; } = { scrollUpdater: undefined, + syncSuppressed: false, }; diff --git a/src/view.ts b/src/view.ts index 4e9f9f0..8dfe5e6 100644 --- a/src/view.ts +++ b/src/view.ts @@ -5,7 +5,7 @@ import { renderMarkdown, renderMermaid, renderKatex, handlePostRender, applyStyl import { replaceImageURLs } from './features/image'; import { hidePreviewButtons, previewModes } from './support/settings'; import { localized } from './shared/strings'; -import { syncScrollProgress } from './scroll'; +import { syncScrollProgress, suppressScrollSync, resumeScrollSync } from './scroll'; import { resolveTaskToggle } from './features/task'; import { ClassNames, CacheKeys } from './shared/const'; @@ -378,16 +378,14 @@ function handleTaskItemToggle(event: MouseEvent) { // Let the native toggle stand for instant feedback; just sync the source const from = lineRange.from + toggle.offset; - // Dispatch can trigger scroll-sync via the editor's scrollDOM; preserve preview position. - const savedScrollTop = previewPane.scrollTop; - + // Suppress scroll-sync for this dispatch: the editor's scrollDOM can emit a + // scrollend event that would jump the preview to the editor's cursor line. + suppressScrollSync(); MarkEdit.editorView.dispatch({ changes: { from, to: from + 1, insert: toggle.replacement }, annotations: silentChange.of(true), }); - - previewPane.scrollTop = savedScrollTop; - requestAnimationFrame(() => { previewPane.scrollTop = savedScrollTop; }); + requestAnimationFrame(resumeScrollSync); } const states: {