diff --git a/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts b/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts index ab0c023c5..fa4a78cd1 100644 --- a/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts +++ b/packages/editor/src/extensions/paging/__tests__/scroll-anchor.test.ts @@ -195,6 +195,26 @@ describe("scroll anchor", () => { editor.destroy(); }); + test("does not render pages it is about to scroll away from", () => { + const editor = createEditor(createContainer()); + stubLayout(editor); + const before = Array.from(editor.view.dom.children).map((page) => + (page as HTMLElement).hasAttribute("data-page-placeholder") + ); + + restoreScrollAnchor(editor.view, { blockId: "blk300", offset: 0 }); + + // page 3 is neither an edge, nor next to the caret, nor near the anchor: + // nothing should have rendered it on the way past + expect(before[3]).toBe(true); + expect( + (editor.view.dom.children[3] as HTMLElement).hasAttribute( + "data-page-placeholder" + ) + ).toBe(true); + editor.destroy(); + }); + test("reports failure for a block that is no longer there", () => { const editor = createEditor(createContainer()); stubLayout(editor); diff --git a/packages/editor/src/extensions/paging/anchor.ts b/packages/editor/src/extensions/paging/anchor.ts index 679c27200..43f3d9d12 100644 --- a/packages/editor/src/extensions/paging/anchor.ts +++ b/packages/editor/src/extensions/paging/anchor.ts @@ -22,6 +22,7 @@ import { EditorView } from "@tiptap/pm/view"; import { profiler } from "../../utils/profiler.js"; import { isPage } from "./split.js"; import { + calibrateHeightsNow, findScrollParent, renderViewportNow, viewportKey @@ -113,11 +114,12 @@ export function restoreScrollAnchor( return false; } - // Empty pages start out sized from a guess, and the note shrinks once the - // first real measurements arrive. Doing that first means scrolling against - // the heights the note will actually have, instead of being dragged along - // when it changes size a moment later. - renderViewportNow(view); + // Empty pages start out sized from a guess, and the note changes height once + // the first real measurements arrive. Sizing them first means scrolling + // against the heights the note will actually have, and doing it without + // rendering anything avoids showing pages here that are about to be left + // behind. + calibrateHeightsNow(view); const target = findBlock(view.state.doc, anchor.blockId); if (!target.found) { diff --git a/packages/editor/src/extensions/paging/viewport-plugin.ts b/packages/editor/src/extensions/paging/viewport-plugin.ts index 967b09e29..71fd5a5ed 100644 --- a/packages/editor/src/extensions/paging/viewport-plugin.ts +++ b/packages/editor/src/extensions/paging/viewport-plugin.ts @@ -38,6 +38,16 @@ type PageRange = { from: number; to: number; index: number }; const EMPTY_VISIBLE: Set = new Set(); const pending = new WeakMap void>(); +const calibrations = new WeakMap void>(); + +/** + * Sizes the empty pages from what the note actually looks like, without + * changing which pages are rendered. Used before scrolling somewhere, so the + * note is not still changing height once it gets there. + */ +export function calibrateHeightsNow(view: EditorView): void { + calibrations.get(view)?.(); +} /** * Works out what is on screen right now instead of waiting for the next frame. @@ -500,6 +510,11 @@ export function viewportPlugin(heights: HeightMap): Plugin { }; pending.set(editorView, flush); + calibrations.set(editorView, () => { + updateMetrics(); + measureRenderedPages(); + resizePlaceholders(); + }); window.addEventListener("beforeprint", beforePrint); window.addEventListener("afterprint", afterPrint); @@ -533,6 +548,7 @@ export function viewportPlugin(heights: HeightMap): Plugin { window.removeEventListener("afterprint", afterPrint); printMedia?.removeEventListener?.("change", onPrintMedia); pending.delete(editorView); + calibrations.delete(editorView); window.removeEventListener("resize", schedule); scrollParent?.removeEventListener("scroll", schedule); layout?.disconnect();