fix: a figure frame stays in the middle of the column

An as-char text box takes its centring from the paragraph it is anchored
in, and the importer lifts it out of that paragraph — so every figure in
a fixture sat at the left margin. Carry style:horizontal-pos on the
box (an image needs none: with no side to float to it centres anyway).
This commit is contained in:
Steffen Becker committed 2026-08-10 10:51:14 +00:00
1 parent c26aac1bee
commit 615f2af52d
3 files changed
+23 -2

No files matched your search

+13
View File
@@ -38,6 +38,7 @@ export interface TextBoxAttrs {
wrap: WrapMode;
wrapOffset: number | null; // cm from the text column's left edge
wrapOffsetY: number | null; // cm below the anchor paragraph
wrapAlign: string | null; // 'center' = set against the middle of the column
shapeKind: ShapeKind;
fillColor: string | null;
strokeColor: string | null;
@@ -117,6 +118,13 @@ export const TextBox = Node.create({
parseHTML: el => parseCmAttr((el as HTMLElement).getAttribute('data-wrap-offset-y')),
renderHTML: () => ({}),
},
// 'center' = set against the middle of the column (ODF style:horizontal-pos): a
// figure frame keeps it after the importer lifts it out of its anchor paragraph.
wrapAlign: {
default: null,
parseHTML: el => (el as HTMLElement).getAttribute('data-wrap-align') || null,
renderHTML: () => ({}),
},
shapeKind: {
default: 'textbox',
parseHTML: el => (el as HTMLElement).getAttribute('data-shape') ?? 'textbox',
@@ -366,6 +374,11 @@ class TextBoxView {
} else if (a.wrap === 'topBottom') {
d.style.clear = 'both';
}
// Set against the middle of the column, unless the file placed it by coordinate.
if (a.wrapAlign === 'center' && a.wrapOffset == null && a.wrap !== 'left' && a.wrap !== 'right') {
d.style.marginLeft = 'auto';
d.style.marginRight = 'auto';
}
}
// The resize/rotate handles and the badge — always ours, never ProseMirror's.
+6 -2
View File
@@ -569,6 +569,7 @@ type TextBoxExport = {
wrap: WrapMode;
wrapOffsetCm: number | null;
wrapOffsetYCm: number | null;
wrapAlign: string | null;
shapeKind: ShapeKind;
fill: string | null;
stroke: string | null;
@@ -588,6 +589,7 @@ function textBoxDescriptor(node: TiptapNode): TextBoxExport {
wrap: wrapAttr === 'left' || wrapAttr === 'right' || wrapAttr === 'topBottom' ? wrapAttr : 'inline',
wrapOffsetCm: typeof a.wrapOffset === 'number' ? round3(a.wrapOffset) : null,
wrapOffsetYCm: typeof a.wrapOffsetY === 'number' ? round3(a.wrapOffsetY) : null,
wrapAlign: a.wrapAlign === 'center' ? 'center' : null,
shapeKind: kind === 'roundRect' || kind === 'ellipse' ? kind : 'textbox',
fill: typeof a.fillColor === 'string' && a.fillColor ? a.fillColor : null,
stroke: typeof a.strokeColor === 'string' && a.strokeColor ? a.strokeColor : null,
@@ -3022,9 +3024,11 @@ function textBoxGraphicStyle(box: TextBoxExport, index: number): string {
const stroke = box.stroke
? `draw:stroke="solid" svg:stroke-color="${normalizeColor(box.stroke) ?? '#000000'}" svg:stroke-width="${r3(box.strokeWidthPt)}pt"`
: 'draw:stroke="none"';
// An as-char frame keeps only its horizontal-pos: that is what centres a figure
// frame, and the anchor paragraph this export mints carries no alignment.
const wrap = box.wrap === 'inline'
? ''
: ` ${imageWrapProps(box.wrap, box.wrapOffsetCm)} style:number-wrapped-paragraphs="no-limit"` +
? (box.wrapAlign === 'center' ? ' style:horizontal-pos="center" style:horizontal-rel="paragraph-content"' : '')
: ` ${imageWrapProps(box.wrap, box.wrapOffsetCm, box.wrapAlign)} style:number-wrapped-paragraphs="no-limit"` +
` style:horizontal-rel="paragraph-content"` +
` style:vertical-pos="${box.wrapOffsetYCm != null ? 'from-top' : 'top'}" style:vertical-rel="paragraph"`;
// auto-grow only for plain text boxes; a custom-shape needs both explicitly
+4
View File
@@ -344,6 +344,10 @@ function convertTextBoxFrame(frame: Element, textBoxEl: Element, ctx: Ctx): Node
if (hCm != null) attrs.height = Math.round(cmToPx(hCm));
const gp = ctx.resolver.graphicProps(frame.getAttributeNS(NS.draw, 'style-name'));
applyFrameRotationAndWrap(frame, attrs, gp);
// A box is a block of its own here, so the centring of a figure frame — which its
// as-char anchor paragraph would otherwise give it — has to ride the box itself.
// An image needs none: with no side to float to it is centred anyway.
if (gp['style:horizontal-pos'] === 'center' && attrs.wrapOffset == null) attrs.wrapAlign = 'center';
shapeStyleAttrs(gp, attrs, false);
return { type: 'textBox', attrs, content: textBoxContent(Array.from(textBoxEl.children), ctx) };
}