web: stop the pixel scroll fallback fighting the block anchor

This commit is contained in:
Ammar Ahmed
2026-08-25 22:44:19 +05:00
parent 6f6b93af31
commit 78cdc390b3
3 changed files with 31 additions and 3 deletions

View File

@@ -950,6 +950,16 @@ function isFile(e: DragEvent) {
);
}
/** The editor is not always registered yet when a note first loads. */
function retryScrollAnchor(sessionId: string, anchor: ScrollAnchor, tries = 5) {
if (tries <= 0) return;
requestAnimationFrame(() => {
const editor = useEditorManager.getState().getEditor(sessionId)?.editor;
if (editor?.restoreScrollAnchor(anchor)) return;
retryScrollAnchor(sessionId, anchor, tries - 1);
});
}
function restoreScrollPosition(session: EditorSession, editor?: IEditor) {
if (session?.activeBlockId) return scrollIntoViewById(session.activeBlockId);
@@ -957,11 +967,20 @@ function restoreScrollPosition(session: EditorSession, editor?: IEditor) {
// against a fully rendered document, and a paged one only knows estimated
// heights until it renders. The editor reveals the page holding the block
// before scrolling, so the position it lands on is the real one.
//
// Once there is an anchor the pixel path must not run at all, not even as a
// fallback: it scrolls to an offset that means something else now, and its
// ResizeObserver fires again every time a page renders and changes the
// document's height.
const anchor = Config.get<ScrollAnchor | null>(
`${session.id}:scroll-anchor`,
null
);
if (anchor && editor?.restoreScrollAnchor(anchor)) return;
if (anchor) {
if (editor?.restoreScrollAnchor(anchor)) return;
retryScrollAnchor(session.id, anchor);
return;
}
const scrollContainer = document.getElementById(`editorScroll_${session.id}`);
const scrollPosition = Config.get(`${session.id}:scroll-position`, 0);

View File

@@ -208,6 +208,12 @@ function TipTap(props: TipTapProps) {
} = props;
const autoSave = useRef(true);
useEffect(() => {
profiler.count("editor.mounts");
profiler.event("editor.mount", { id });
return () => profiler.count("editor.unmounts");
}, []);
const { toolbarConfig } = useToolbarConfig();
const features = useAreFeaturesAvailable([
"callout",

View File

@@ -57,7 +57,8 @@ export const useEditor = (
// The Editor constructor already rendered a view into the same element,
// so replacing it on the first run means rendering the whole document
// twice. Only later runs (changed options) need a fresh view.
if (isFirstRun.current) {
const firstRun = isFirstRun.current;
if (firstRun) {
isFirstRun.current = false;
} else {
profiler.time("editor.destroyView", () => destroyView(editor.view));
@@ -70,7 +71,9 @@ export const useEditor = (
});
}
if (oldIsFocused && !editor.isFocused) editor.commands.focus();
options.onCreate?.({ editor: editor });
// The Editor constructor emits `create` for the view it made, so calling
// the option here as well would run every consumer twice on load.
if (!firstRun) options.onCreate?.({ editor: editor });
const { searchTerm, ...searchOptions } = useEditorSearchStore.getState();
if (!searchOptions.isSearching) {