mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
fix notes scroll issue
This commit is contained in:
parent
a775a9f6d3
commit
c6d6ad7665
1 changed files with 41 additions and 7 deletions
|
|
@ -648,6 +648,13 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const isInternalUpdate = useRef(false)
|
const isInternalUpdate = useRef(false)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
// Read wikiLinks lazily inside the editor config via this ref. wikiLinks changes
|
||||||
|
// identity whenever the workspace directory tree changes (file watcher → new file
|
||||||
|
// list), and it used to be a useEditor() dependency — so any background write to
|
||||||
|
// the workspace destroyed and recreated the entire editor, resetting scroll to the
|
||||||
|
// top. Keeping it off the dep array (and reading the ref at event time) means the
|
||||||
|
// editor instance survives directory changes.
|
||||||
|
const wikiLinksRef = useRef(wikiLinks)
|
||||||
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null)
|
const [activeWikiLink, setActiveWikiLink] = useState<WikiLinkMatch | null>(null)
|
||||||
const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null)
|
const [anchorPosition, setAnchorPosition] = useState<{ left: number; top: number } | null>(null)
|
||||||
const [selectionHighlight, setSelectionHighlight] = useState<SelectionHighlightRange>(null)
|
const [selectionHighlight, setSelectionHighlight] = useState<SelectionHighlightRange>(null)
|
||||||
|
|
@ -670,6 +677,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
|
|
||||||
// Keep ref in sync with state for the plugin to access
|
// Keep ref in sync with state for the plugin to access
|
||||||
selectionHighlightRef.current = selectionHighlight
|
selectionHighlightRef.current = selectionHighlight
|
||||||
|
wikiLinksRef.current = wikiLinks
|
||||||
|
|
||||||
// Memoize the selection highlight extension
|
// Memoize the selection highlight extension
|
||||||
const selectionHighlightExtension = useMemo(
|
const selectionHighlightExtension = useMemo(
|
||||||
|
|
@ -776,11 +784,9 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
TranscriptBlockExtension,
|
TranscriptBlockExtension,
|
||||||
MermaidBlockExtension,
|
MermaidBlockExtension,
|
||||||
WikiLink.configure({
|
WikiLink.configure({
|
||||||
onCreate: wikiLinks?.onCreate
|
onCreate: (path: string) => {
|
||||||
? (path: string) => {
|
void wikiLinksRef.current?.onCreate?.(path)
|
||||||
void wikiLinks.onCreate(path)
|
},
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}),
|
}),
|
||||||
TaskList,
|
TaskList,
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
|
|
@ -912,7 +918,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
if (heading && (!linkedNotePath || isSameNotePath(linkedNotePath, notePath))) {
|
if (heading && (!linkedNotePath || isSameNotePath(linkedNotePath, notePath))) {
|
||||||
return scrollToHeading(_view, heading)
|
return scrollToHeading(_view, heading)
|
||||||
}
|
}
|
||||||
wikiLinks?.onOpen?.(node.attrs.path)
|
wikiLinksRef.current?.onOpen?.(node.attrs.path)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -951,13 +957,15 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// NOTE: wikiLinks is intentionally NOT a dependency — it's read via wikiLinksRef
|
||||||
|
// at event time. Including it rebuilds the whole editor on every directory change
|
||||||
|
// (file watcher), which resets scroll to the top. See wikiLinksRef declaration.
|
||||||
}, [
|
}, [
|
||||||
editorSessionKey,
|
editorSessionKey,
|
||||||
maybeCommitPrimaryHeading,
|
maybeCommitPrimaryHeading,
|
||||||
notePath,
|
notePath,
|
||||||
preventTitleHeadingDemotion,
|
preventTitleHeadingDemotion,
|
||||||
promoteFirstParagraphToTitleHeading,
|
promoteFirstParagraphToTitleHeading,
|
||||||
wikiLinks,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const orderedFiles = useMemo(() => {
|
const orderedFiles = useMemo(() => {
|
||||||
|
|
@ -1203,11 +1211,37 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
// Normalize for comparison (trim trailing whitespace from lines)
|
// Normalize for comparison (trim trailing whitespace from lines)
|
||||||
const normalizeForCompare = (s: string) => s.split('\n').map(line => line.trimEnd()).join('\n').trim()
|
const normalizeForCompare = (s: string) => s.split('\n').map(line => line.trimEnd()).join('\n').trim()
|
||||||
if (normalizeForCompare(currentContent) !== normalizeForCompare(content)) {
|
if (normalizeForCompare(currentContent) !== normalizeForCompare(content)) {
|
||||||
|
// Preserve scroll + selection across an external content sync. setContent()
|
||||||
|
// resets the selection to the top of the doc and ProseMirror scrolls it into
|
||||||
|
// view; without restoring, a background writer touching the open file (graph
|
||||||
|
// builder, live-note runner, version-history commit) yanks the viewport back
|
||||||
|
// to the top repeatedly — making the note impossible to scroll. This editor
|
||||||
|
// instance is bound to a single note path, so the prior scrollTop is always
|
||||||
|
// valid for the reloaded content.
|
||||||
|
const wrapper = wrapperRef.current
|
||||||
|
const prevScrollTop = wrapper?.scrollTop ?? 0
|
||||||
|
const hadFocus = editor.isFocused
|
||||||
|
const { from: prevFrom, to: prevTo } = editor.state.selection
|
||||||
|
|
||||||
isInternalUpdate.current = true
|
isInternalUpdate.current = true
|
||||||
const preprocessed = preprocessMarkdown(content)
|
const preprocessed = preprocessMarkdown(content)
|
||||||
// Treat tab-open content as baseline: do not add hydration to undo history.
|
// Treat tab-open content as baseline: do not add hydration to undo history.
|
||||||
editor.chain().setMeta('addToHistory', false).setContent(preprocessed).run()
|
editor.chain().setMeta('addToHistory', false).setContent(preprocessed).run()
|
||||||
|
|
||||||
|
// Only restore the caret for a focused editor, so we never steal focus or
|
||||||
|
// scroll for a passive viewer. Clamp to the (possibly shorter) new doc.
|
||||||
|
if (hadFocus) {
|
||||||
|
const docSize = editor.state.doc.content.size
|
||||||
|
const from = Math.min(prevFrom, docSize)
|
||||||
|
const to = Math.min(prevTo, docSize)
|
||||||
|
try {
|
||||||
|
editor.chain().setMeta('addToHistory', false).setTextSelection({ from, to }).run()
|
||||||
|
} catch { /* selection no longer valid in the new doc — ignore */ }
|
||||||
|
}
|
||||||
isInternalUpdate.current = false
|
isInternalUpdate.current = false
|
||||||
|
|
||||||
|
// Restore scroll last so it wins over any scrollIntoView triggered above.
|
||||||
|
if (wrapper) wrapper.scrollTop = prevScrollTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editor, content])
|
}, [editor, content])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue