diff --git a/src/lib/editor/extensions/textBox.ts b/src/lib/editor/extensions/textBox.ts index 58a0200..dfd5137 100644 --- a/src/lib/editor/extensions/textBox.ts +++ b/src/lib/editor/extensions/textBox.ts @@ -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. diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 8366358..6543421 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -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 diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index a41568d..b85471d 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -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) }; }