fix: a text box takes the padding its file declares
The inset ring was a fixed 0.15cm the editor imposed on every box, so a figure frame — which declares 0 — sat 1.5mm right of where LibreOffice draws it and stood 3mm taller, on every figure of the document.
This commit is contained in:
1 parent
615f2af52d
commit
f81109a434
3 files changed
+18
-4
No files matched your search
@@ -21,7 +21,7 @@ export type ShapeKind = 'textbox' | 'roundRect' | 'ellipse';
|
||||
|
||||
// Fixed text inset inside the frame; exported as fo:padding / wps:bodyPr insets.
|
||||
export const TEXTBOX_PADDING_CM = 0.15;
|
||||
const PADDING_PX = (TEXTBOX_PADDING_CM * 96) / 2.54;
|
||||
const paddingPx = (cm: unknown) => ((typeof cm === 'number' ? cm : TEXTBOX_PADDING_CM) * 96) / 2.54;
|
||||
|
||||
// Extra per-axis inset that fits the text area into an ellipse's inscribed
|
||||
// rectangle (half-axis / √2), so text follows the oval instead of the bbox.
|
||||
@@ -39,6 +39,7 @@ export interface TextBoxAttrs {
|
||||
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
|
||||
paddingCm: number; // inset ring around the text (ODF fo:padding)
|
||||
shapeKind: ShapeKind;
|
||||
fillColor: string | null;
|
||||
strokeColor: string | null;
|
||||
@@ -125,6 +126,13 @@ export const TextBox = Node.create({
|
||||
parseHTML: el => (el as HTMLElement).getAttribute('data-wrap-align') || null,
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
// The file's own fo:padding (cm). A figure frame declares 0, and the default
|
||||
// ring would make every such box a little taller and its text a little inset.
|
||||
paddingCm: {
|
||||
default: TEXTBOX_PADDING_CM,
|
||||
parseHTML: el => parseCmAttr((el as HTMLElement).getAttribute('data-padding')) ?? TEXTBOX_PADDING_CM,
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
shapeKind: {
|
||||
default: 'textbox',
|
||||
parseHTML: el => (el as HTMLElement).getAttribute('data-shape') ?? 'textbox',
|
||||
@@ -164,7 +172,7 @@ export const TextBox = Node.create({
|
||||
a.strokeColor ? `border:${a.strokeWidthPt * PX_PER_PT}px solid ${a.strokeColor}` : '',
|
||||
a.shapeKind !== 'textbox' ? `border-radius:${shapeRadius(a.shapeKind)}` : '',
|
||||
a.rotation ? `transform:rotate(${a.rotation}deg)` : '',
|
||||
`padding:${PADDING_PX.toFixed(2)}px`,
|
||||
`padding:${paddingPx(a.paddingCm).toFixed(2)}px`,
|
||||
].filter(Boolean).join(';');
|
||||
return ['div', mergeAttributes(HTMLAttributes, {
|
||||
'data-textbox': '',
|
||||
@@ -175,6 +183,7 @@ export const TextBox = Node.create({
|
||||
...(a.fillColor ? { 'data-fill': a.fillColor } : {}),
|
||||
...(a.strokeColor ? { 'data-stroke': a.strokeColor } : {}),
|
||||
...(a.strokeWidthPt !== 1 ? { 'data-stroke-width': String(a.strokeWidthPt) } : {}),
|
||||
...(a.paddingCm !== TEXTBOX_PADDING_CM ? { 'data-padding': String(a.paddingCm) } : {}),
|
||||
}), 0];
|
||||
},
|
||||
|
||||
@@ -270,7 +279,7 @@ class TextBoxView {
|
||||
|
||||
// Padding lives on the rotor, so the inset ring around the text is frame
|
||||
// (click-to-select) area rather than content.
|
||||
this.rotor.style.padding = `${PADDING_PX.toFixed(2)}px`;
|
||||
this.rotor.style.padding = `${paddingPx(this.attrs().paddingCm).toFixed(2)}px`;
|
||||
this.contentDOM = document.createElement('div');
|
||||
this.contentDOM.className = 'textbox-content';
|
||||
this.rotor.appendChild(this.contentDOM);
|
||||
|
||||
@@ -570,6 +570,7 @@ type TextBoxExport = {
|
||||
wrapOffsetCm: number | null;
|
||||
wrapOffsetYCm: number | null;
|
||||
wrapAlign: string | null;
|
||||
paddingCm: number;
|
||||
shapeKind: ShapeKind;
|
||||
fill: string | null;
|
||||
stroke: string | null;
|
||||
@@ -590,6 +591,7 @@ function textBoxDescriptor(node: TiptapNode): TextBoxExport {
|
||||
wrapOffsetCm: typeof a.wrapOffset === 'number' ? round3(a.wrapOffset) : null,
|
||||
wrapOffsetYCm: typeof a.wrapOffsetY === 'number' ? round3(a.wrapOffsetY) : null,
|
||||
wrapAlign: a.wrapAlign === 'center' ? 'center' : null,
|
||||
paddingCm: typeof a.paddingCm === 'number' ? round3(a.paddingCm) : TEXTBOX_PADDING_CM,
|
||||
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,
|
||||
@@ -3038,7 +3040,7 @@ function textBoxGraphicStyle(box: TextBoxExport, index: number): string {
|
||||
: ' draw:auto-grow-height="false" draw:auto-grow-width="false"';
|
||||
return (
|
||||
`<style:style style:name="TbxFr${index + 1}" style:family="graphic">` +
|
||||
`<style:graphic-properties ${fill} ${stroke} fo:padding="${TEXTBOX_PADDING_CM}cm"` +
|
||||
`<style:graphic-properties ${fill} ${stroke} fo:padding="${box.paddingCm}cm"` +
|
||||
`${grow} draw:textarea-vertical-align="top"${wrap}/>` +
|
||||
`</style:style>`
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { languageFromOdf, NO_LANGUAGE, type DocumentLanguage } from '../storage/
|
||||
import type { HfDoc, HfSet } from '../storage/headerFooter';
|
||||
import type { EmbeddedFont } from '../fonts/embeddedFonts';
|
||||
import { cellPaddingAttr, DEFAULT_CELL_PADDING, type CellPadding } from '../editor/extensions/tableCellPadding';
|
||||
import { TEXTBOX_PADDING_CM } from '../editor/extensions/textBox';
|
||||
import { getSchema } from '@tiptap/core';
|
||||
import type { Schema } from '@tiptap/pm/model';
|
||||
import { hfExtensions } from '../editor/extensions/headerFooter';
|
||||
@@ -348,6 +349,8 @@ function convertTextBoxFrame(frame: Element, textBoxEl: Element, ctx: Ctx): Node
|
||||
// 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';
|
||||
const padCm = lengthToCm(gp['fo:padding']);
|
||||
if (padCm != null && Math.abs(padCm - TEXTBOX_PADDING_CM) > 0.01) attrs.paddingCm = Math.round(padCm * 1000) / 1000;
|
||||
shapeStyleAttrs(gp, attrs, false);
|
||||
return { type: 'textBox', attrs, content: textBoxContent(Array.from(textBoxEl.children), ctx) };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user