diff --git a/docs/architecture/pagination.md b/docs/architecture/pagination.md index fb90d17..7b2531e 100644 --- a/docs/architecture/pagination.md +++ b/docs/architecture/pagination.md @@ -149,7 +149,8 @@ Opening a 460-page document ran five rounds of that where four answer everything **The fields of one answer read together.** Every field that resolves against the settled layout — each index's page numbers, every cross-reference — registers with `scheduleFieldRound(view, owner, read)` (`pageBreaks.ts`) instead of scheduling a frame of -its own. The round runs every reader's measurement first and only then their writes, since +its own. The round reads the page grid once and hands it to every reader — they all resolve +against the same one — and runs every reader's measurement first and only then their writes, since a write between two measurements makes the browser lay the whole document out again; a write may return one more phase (the index reads back where its rows now fall), run after every other write. Their cached results — the index `entries`, a reference's `text` — ride diff --git a/src/lib/editor/extensions/crossReference.ts b/src/lib/editor/extensions/crossReference.ts index 38b56c2..3a9269c 100644 --- a/src/lib/editor/extensions/crossReference.ts +++ b/src/lib/editor/extensions/crossReference.ts @@ -3,7 +3,7 @@ import type { Editor } from '@tiptap/core'; import type { Node as PMNode } from '@tiptap/pm/model'; import type { EditorView } from '@tiptap/pm/view'; import { bookmarks, findBookmark, type BookmarkRef } from './bookmark'; -import { readVerticalMargins, pageOfElement, scheduleFieldRound, type FieldWrite, type PageGrid } from './pageBreaks'; +import { pageOfElement, scheduleFieldRound, type FieldWrite, type PageGrid, type VMargins } from './pageBreaks'; // A cross-reference: an inline atom showing either the text of a bookmark or the page it // sits on, kept live by the node view the way the TOC keeps its page numbers. Round-trips @@ -119,10 +119,10 @@ class CrossRefBatch { schedule(): void { if (this.editor.isDestroyed) return; - scheduleFieldRound(this.editor.view, this, () => this.measure()); + scheduleFieldRound(this.editor.view, this, (vm) => this.measure(vm)); } - private measure(): FieldWrite | void { + private measure(vm: VMargins): FieldWrite | void { const { editor } = this; if (editor.isDestroyed) return; const view = editor.view; @@ -132,8 +132,7 @@ class CrossRefBatch { } const targets = new Map(); for (const b of bookmarks(editor.state.doc)) if (!targets.has(b.name)) targets.set(b.name, b); - let grid: PageGrid | null = null; - const gridOf = () => (grid ??= readVerticalMargins(view.dom as HTMLElement).grid); + const gridOf = () => vm.grid; const jobs: { ref: CrossRefView; node: PMNode; pos: number; text: string }[] = []; for (const ref of this.views) { const pos = ref.pos(); diff --git a/src/lib/editor/extensions/pageBreaks.ts b/src/lib/editor/extensions/pageBreaks.ts index 0301d03..e7ba97e 100644 --- a/src/lib/editor/extensions/pageBreaks.ts +++ b/src/lib/editor/extensions/pageBreaks.ts @@ -333,7 +333,7 @@ export const FORCE_PAGE_RECALC = 'forcePageBreakRecalc'; // It measures, and what it returns writes: its own DOM, and its cached result on the // round's transaction. A write may return one more phase, run after every other write. export type FieldWrite = (tr: Transaction) => FieldWrite | void; -export type FieldRead = () => FieldWrite | void; +export type FieldRead = (vm: VMargins) => FieldWrite | void; type FieldRound = { jobs: Map; raf: number | null }; const fieldRounds = new WeakMap(); @@ -352,7 +352,10 @@ function runFieldRound(view: EditorView): void { const round = fieldRounds.get(view); if (!round) return; round.raf = null; - let phase = Array.from(round.jobs.values(), (read) => read()); + // One reading of the page grid for the round: getComputedStyle is a style pass over + // the whole document, and every field resolves its pages against the same grid. + const vm = readVerticalMargins(view.dom as HTMLElement); + let phase = Array.from(round.jobs.values(), (read) => read(vm)); round.jobs.clear(); const tr = view.state.tr.setMeta('addToHistory', false); // Bounded: measure, write, and the one read-back a write can ask for. diff --git a/src/lib/editor/extensions/tableOfContents.ts b/src/lib/editor/extensions/tableOfContents.ts index 6c112cf..a074487 100644 --- a/src/lib/editor/extensions/tableOfContents.ts +++ b/src/lib/editor/extensions/tableOfContents.ts @@ -10,7 +10,7 @@ import { indexEntries, indexRows } from './indexEntry'; import { bibliographyEntries, bibliographyRows } from './bibliographyEntry'; import { isCitationStyle, type CitationStyle } from '../../utils/citationStyle'; import type { Transaction } from '@tiptap/pm/state'; -import { readVerticalMargins, pageOfElement, topInEditor, scheduleFieldRound, FORCE_PAGE_RECALC, type FieldWrite, type PageGrid } from './pageBreaks'; +import { pageOfElement, topInEditor, scheduleFieldRound, FORCE_PAGE_RECALC, type FieldWrite, type PageGrid, type VMargins } from './pageBreaks'; // A generated index: a block atom listing every source with its live page number — the // headings for a table of contents, the captions of one category for a list of figures @@ -232,7 +232,7 @@ class TocView { private schedule(): void { if (this.editor.isDestroyed) return; - scheduleFieldRound(this.editor.view, this, () => this.measure()); + scheduleFieldRound(this.editor.view, this, (vm) => this.measure(vm)); } // What the index lists, in document order: the headings down to its own level, or — @@ -295,19 +295,15 @@ class TocView { return out; } - private grid(): PageGrid { - return readVerticalMargins(this.editor.view.dom as HTMLElement).grid; - } - private pageOf(pos: number, grid: PageGrid): number { const el = this.editor.view.nodeDOM(pos) as HTMLElement | null; if (!el || el.nodeType !== 1) return 1; return pageOfElement(this.editor.view, el, grid); } - private measure(): FieldWrite | void { + private measure(vm: VMargins): FieldWrite | void { if (this.editor.isDestroyed || !this.dom.isConnected) return; - const grid = this.grid(); + const grid = vm.grid; let heads = this.sources(); let entries: TocEntry[]; if (indexKindOf(this.node()?.attrs?.index) === 'alphabetical') { @@ -335,18 +331,17 @@ class TocView { } // The rows carry their page numbers now, so where they fall is read once every // field of the round has written. - return (last) => this.paginate(last); + return (last) => this.paginate(last, vm); }; } // The index is a block atom: it has no inner document positions for pagination to put // a spacer at, so one longer than a page breaks itself — the row that would cross the // boundary takes the gap to the next page's content top as its margin. - private paginate(tr: Transaction): void { + private paginate(tr: Transaction, vm: VMargins): void { const rows = Array.from(this.dom.querySelectorAll('.toc-entry')); if (!rows.length) return; const view = this.editor.view; - const vm = readVerticalMargins(view.dom as HTMLElement); for (const row of rows) row.style.marginTop = ''; // Read every natural top and height first: applying a gap moves each row below it, // and one reflow for the whole index beats one per row.