From b134911e7b37a6ba9f6dda4fcae06fde478947d6 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Mon, 10 Aug 2026 19:35:09 +0000 Subject: [PATCH] fix: pagination measures a text box's own spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The atomic leaf a text box becomes carried neither its space above nor its space below, so the fit test ignored both and a page top never dropped the space above the way it does for a paragraph. The box's space rides --space-top, which the page-top decoration zeroes — the rotor centres itself in the padding box and has to follow it. --- src/lib/editor/extensions/pageBreaks.ts | 14 ++++++++++++-- src/lib/editor/extensions/textBox.ts | 10 +++++----- src/styles/editor.css | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/lib/editor/extensions/pageBreaks.ts b/src/lib/editor/extensions/pageBreaks.ts index bdd0347..9f2dcd0 100644 --- a/src/lib/editor/extensions/pageBreaks.ts +++ b/src/lib/editor/extensions/pageBreaks.ts @@ -751,13 +751,22 @@ export const PageBreaks = Extension.create({ // A text box paginates atomically: a line-split spacer can't render // sanely inside a rotated/floated node view. if (child.classList.contains('textbox-node')) { + // Its anchor paragraph's spacing rides the box (textBox.ts): space above + // as padding, which a page top drops, space below as the margin. + const boxCs = getComputedStyle(child); + const spaceAbove = parseFloat(boxCs.getPropertyValue('--space-before')) || 0; + const dropped = spaceAbove > 0.5 && !inTableCell && parseFloat(boxCs.paddingTop) < 0.5 + ? spaceAbove : 0; leaves.push({ el: child, kind: 'atomic', naturalTop: naturalTopOf(child), - naturalHeight: child.offsetHeight, + naturalHeight: child.offsetHeight + dropped, + spaceAfter: inTableCell ? 0 : parseFloat(boxCs.marginBottom) || 0, + spaceAbove, inTableCell, }); + cumulativeDropped += dropped; continue; } // A multi-column section fragment: also atomic for spacer purposes (a @@ -1316,7 +1325,8 @@ export const PageBreaks = Extension.create({ })); } for (const b of pageTopBlocks) { - decoArray.push(Decoration.node(b.from, b.to, { style: 'padding-top:0;margin-top:0' })); + // --space-top: a text box draws its own space above from it (textBox.ts). + decoArray.push(Decoration.node(b.from, b.to, { style: 'padding-top:0;margin-top:0;--space-top:0px' })); } decorations = decoArray.length > 0 diff --git a/src/lib/editor/extensions/textBox.ts b/src/lib/editor/extensions/textBox.ts index 96f032e..1d65ffb 100644 --- a/src/lib/editor/extensions/textBox.ts +++ b/src/lib/editor/extensions/textBox.ts @@ -388,11 +388,11 @@ class TextBoxView { // frame distance already is. const before = this.node.attrs.spaceBefore as number | null; const after = this.node.attrs.spaceAfter as number | null; - // On the rotor too: --space-before is a registered non-inheriting property, and the - // rotor centres itself in the padding box, so it needs the value to correct for it. - for (const el of [d, this.rotor]) { - if (before != null) el.style.setProperty('--space-before', `${before}pt`); - else el.style.removeProperty('--space-before'); + // --space-top is the same value the rotor can inherit (--space-before is registered + // non-inheriting), and it is what pageBreaks.ts zeroes at a page top. + for (const prop of ['--space-before', '--space-top']) { + if (before != null) d.style.setProperty(prop, `${before}pt`); + else d.style.removeProperty(prop); } if (after != null) d.style.marginBottom = `${after}pt`; // Set against the middle or the far end of the column, unless the file placed the diff --git a/src/styles/editor.css b/src/styles/editor.css index fee6d44..3561c9c 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -387,8 +387,8 @@ position: absolute; left: 50%; /* Centred in the padding box, so half the space above has to come back to keep the - frame itself below it. */ - top: calc(50% + var(--space-before) / 2); + frame itself below it — the *applied* space, which a page top drops. */ + top: calc(50% + var(--space-top, 0px) / 2); box-sizing: border-box; transform: translate(-50%, -50%); }