feat: add line height attribute to text element (#6360)
* feat: add line height attribute to text element * lint * update line height when redrawing text bounding box * fix tests * retain line height when pasting styles * fix test * create a util for calculating ling height using old algo * update line height when resizing multiple text elements * make line height backward compatible * udpate line height for older element when font size updated * remove logs * Add specs * lint * review fixes * simplify by changing `lineHeight` from px to unitless * make param non-optional * update comment * fix: jumping text due to font size being calculated incorrectly * update line height when font family is updated * lint * Add spec * more specs * rename to getDefaultLineHeight * fix getting lineHeight for potentially undefined fontFamily * reduce duplication * fix fallback * refactor and comment tweaks * fix --------- Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
ac4c8b3ca7
commit
83383977f5
@ -45,6 +45,7 @@ export const actionUnbindText = register({
|
|||||||
const { width, height } = measureText(
|
const { width, height } = measureText(
|
||||||
boundTextElement.originalText,
|
boundTextElement.originalText,
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||||
element.id,
|
element.id,
|
||||||
|
@ -54,6 +54,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
@ -637,6 +638,7 @@ export const actionChangeFontFamily = register({
|
|||||||
oldElement,
|
oldElement,
|
||||||
{
|
{
|
||||||
fontFamily: value,
|
fontFamily: value,
|
||||||
|
lineHeight: getDefaultLineHeight(value),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||||
|
@ -12,7 +12,10 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getBoundTextElement } from "../element/textElement";
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
@ -92,12 +95,18 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isTextElement(newElement)) {
|
if (isTextElement(newElement)) {
|
||||||
|
const fontSize =
|
||||||
|
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
|
const fontFamily =
|
||||||
|
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
|
fontSize,
|
||||||
fontFamily:
|
fontFamily,
|
||||||
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
textAlign:
|
textAlign:
|
||||||
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
|
lineHeight:
|
||||||
|
elementStylesToCopyFrom.lineHeight ||
|
||||||
|
getDefaultLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
if (newElement.containerId) {
|
if (newElement.containerId) {
|
||||||
|
@ -260,13 +260,14 @@ import throttle from "lodash.throttle";
|
|||||||
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxLineHeight,
|
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
isMeasureTextSupported,
|
isMeasureTextSupported,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
@ -1731,12 +1732,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(acc: ExcalidrawTextElement[], line, idx) => {
|
(acc: ExcalidrawTextElement[], line, idx) => {
|
||||||
const text = line.trim();
|
const text = line.trim();
|
||||||
|
|
||||||
|
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
||||||
if (text.length) {
|
if (text.length) {
|
||||||
const element = newTextElement({
|
const element = newTextElement({
|
||||||
...textElementProps,
|
...textElementProps,
|
||||||
x,
|
x,
|
||||||
y: currentY,
|
y: currentY,
|
||||||
text,
|
text,
|
||||||
|
lineHeight,
|
||||||
});
|
});
|
||||||
acc.push(element);
|
acc.push(element);
|
||||||
currentY += element.height + LINE_GAP;
|
currentY += element.height + LINE_GAP;
|
||||||
@ -1745,14 +1748,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// add paragraph only if previous line was not empty, IOW don't add
|
// add paragraph only if previous line was not empty, IOW don't add
|
||||||
// more than one empty line
|
// more than one empty line
|
||||||
if (prevLine) {
|
if (prevLine) {
|
||||||
const defaultLineHeight = getApproxLineHeight(
|
currentY +=
|
||||||
getFontString({
|
getLineHeightInPx(textElementProps.fontSize, lineHeight) +
|
||||||
fontSize: textElementProps.fontSize,
|
LINE_GAP;
|
||||||
fontFamily: textElementProps.fontFamily,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
currentY += defaultLineHeight + LINE_GAP;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2607,6 +2605,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
|
||||||
|
|
||||||
|
const lineHeight =
|
||||||
|
existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
|
||||||
|
const fontSize = this.state.currentItemFontSize;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!existingTextElement &&
|
!existingTextElement &&
|
||||||
shouldBindToContainer &&
|
shouldBindToContainer &&
|
||||||
@ -2614,11 +2619,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isArrowElement(container)
|
!isArrowElement(container)
|
||||||
) {
|
) {
|
||||||
const fontString = {
|
const fontString = {
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily,
|
||||||
};
|
};
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(fontString));
|
const minWidth = getApproxMinLineWidth(
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(fontString));
|
getFontString(fontString),
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
|
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
const newHeight = Math.max(containerDims.height, minHeight);
|
const newHeight = Math.max(containerDims.height, minHeight);
|
||||||
const newWidth = Math.max(containerDims.width, minWidth);
|
const newWidth = Math.max(containerDims.width, minWidth);
|
||||||
@ -2652,8 +2660,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
roundness: null,
|
roundness: null,
|
||||||
text: "",
|
text: "",
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily,
|
||||||
textAlign: parentCenterPosition
|
textAlign: parentCenterPosition
|
||||||
? "center"
|
? "center"
|
||||||
: this.state.currentItemTextAlign,
|
: this.state.currentItemTextAlign,
|
||||||
@ -2663,6 +2671,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
locked: false,
|
locked: false,
|
||||||
|
lineHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingTextElement && shouldBindToContainer && container) {
|
if (!existingTextElement && shouldBindToContainer && container) {
|
||||||
|
@ -35,6 +35,7 @@ import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
|||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
|
import { detectLineHeight, getDefaultLineHeight } from "../element/textElement";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@ -165,17 +166,32 @@ const restoreElement = (
|
|||||||
const [fontPx, _fontFamily]: [string, string] = (
|
const [fontPx, _fontFamily]: [string, string] = (
|
||||||
element as any
|
element as any
|
||||||
).font.split(" ");
|
).font.split(" ");
|
||||||
fontSize = parseInt(fontPx, 10);
|
fontSize = parseFloat(fontPx);
|
||||||
fontFamily = getFontFamilyByName(_fontFamily);
|
fontFamily = getFontFamilyByName(_fontFamily);
|
||||||
}
|
}
|
||||||
|
const text = element.text ?? "";
|
||||||
|
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
text: element.text ?? "",
|
text,
|
||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
originalText: element.originalText || element.text,
|
originalText: element.originalText || text,
|
||||||
|
// line-height might not be specified either when creating elements
|
||||||
|
// programmatically, or when importing old diagrams.
|
||||||
|
// For the latter we want to detect the original line height which
|
||||||
|
// will likely differ from our per-font fixed line height we now use,
|
||||||
|
// to maintain backward compatibility.
|
||||||
|
lineHeight:
|
||||||
|
element.lineHeight ||
|
||||||
|
(element.height
|
||||||
|
? // detect line-height from current element height and font-size
|
||||||
|
detectLineHeight(element)
|
||||||
|
: // no element height likely means programmatic use, so default
|
||||||
|
// to a fixed line height
|
||||||
|
getDefaultLineHeight(element.fontFamily)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (refreshDimensions) {
|
if (refreshDimensions) {
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
@ -137,10 +138,12 @@ export const newTextElement = (
|
|||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"];
|
containerId?: ExcalidrawTextContainer["id"];
|
||||||
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
|
const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureText(text, getFontString(opts));
|
const metrics = measureText(text, getFontString(opts), lineHeight);
|
||||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||||
const textElement = newElementWith(
|
const textElement = newElementWith(
|
||||||
{
|
{
|
||||||
@ -156,6 +159,7 @@ export const newTextElement = (
|
|||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
|
lineHeight,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -176,6 +180,7 @@ const getAdjustedDimensions = (
|
|||||||
const { width: nextWidth, height: nextHeight } = measureText(
|
const { width: nextWidth, height: nextHeight } = measureText(
|
||||||
nextText,
|
nextText,
|
||||||
getFontString(element),
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
@ -185,7 +190,11 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(element.text, getFontString(element));
|
const prevMetrics = measureText(
|
||||||
|
element.text,
|
||||||
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
|
@ -39,13 +39,13 @@ import {
|
|||||||
import { Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
|
getApproxMinLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
@ -360,7 +360,7 @@ export const resizeSingleElement = (
|
|||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFontSize: number | null = null;
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
@ -410,9 +410,7 @@ export const resizeSingleElement = (
|
|||||||
boundTextElement.id,
|
boundTextElement.id,
|
||||||
) as typeof boundTextElement | undefined;
|
) as typeof boundTextElement | undefined;
|
||||||
if (stateOfBoundTextElementAtResize) {
|
if (stateOfBoundTextElementAtResize) {
|
||||||
boundTextFont = {
|
boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
|
||||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (shouldMaintainAspectRatio) {
|
if (shouldMaintainAspectRatio) {
|
||||||
const updatedElement = {
|
const updatedElement = {
|
||||||
@ -428,12 +426,16 @@ export const resizeSingleElement = (
|
|||||||
if (nextFontSize === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boundTextFont = {
|
boundTextFontSize = nextFontSize;
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
const minWidth = getApproxMinLineWidth(
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
getFontString(boundTextElement),
|
||||||
|
boundTextElement.lineHeight,
|
||||||
|
);
|
||||||
|
const minHeight = getApproxMinLineHeight(
|
||||||
|
boundTextElement.fontSize,
|
||||||
|
boundTextElement.lineHeight,
|
||||||
|
);
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||||
}
|
}
|
||||||
@ -566,8 +568,10 @@ export const resizeSingleElement = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(element, resizedElement);
|
mutateElement(element, resizedElement);
|
||||||
if (boundTextElement && boundTextFont) {
|
if (boundTextElement && boundTextFontSize != null) {
|
||||||
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
mutateElement(boundTextElement, {
|
||||||
|
fontSize: boundTextFontSize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(element, transformHandleDirection);
|
handleBindTextResize(element, transformHandleDirection);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
@ -6,6 +6,9 @@ import {
|
|||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
detectLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
|
getDefaultLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
@ -40,9 +43,7 @@ describe("Test wrapText", () => {
|
|||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello \nwhats \nup`,
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
@ -64,8 +65,7 @@ p`,
|
|||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
|
|
||||||
width: 140,
|
width: 140,
|
||||||
res: `Hello whats
|
res: `Hello whats \nup`,
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "fit the container",
|
desc: "fit the container",
|
||||||
@ -95,9 +95,7 @@ whats up`;
|
|||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello\nwhats \nup`,
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break all characters when width of each character is less than container width",
|
desc: "break all characters when width of each character is less than container width",
|
||||||
@ -143,11 +141,7 @@ whats up`,
|
|||||||
{
|
{
|
||||||
desc: "fit characters of long string as per container width",
|
desc: "fit characters of long string as per container width",
|
||||||
width: 170,
|
width: 170,
|
||||||
res: `hellolongtextth
|
res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
|
||||||
isiswhatsupwith
|
|
||||||
youIamtypingggg
|
|
||||||
gandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -166,8 +160,7 @@ now`,
|
|||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
|
|
||||||
width: 600,
|
width: 600,
|
||||||
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
|
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
|
||||||
break it now`,
|
|
||||||
},
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
@ -181,8 +174,7 @@ break it now`,
|
|||||||
const text = "Hello Excalidraw";
|
const text = "Hello Excalidraw";
|
||||||
// Length of "Excalidraw" is 100 and exacty equal to max width
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
||||||
const res = wrapText(text, font, 100);
|
const res = wrapText(text, font, 100);
|
||||||
expect(res).toEqual(`Hello
|
expect(res).toEqual(`Hello \nExcalidraw`);
|
||||||
Excalidraw`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the text as is if max width is invalid", () => {
|
it("should return the text as is if max width is invalid", () => {
|
||||||
@ -312,3 +304,35 @@ describe("Test measureText", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const textElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 1,
|
||||||
|
height: 175,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test detectLineHeight", () => {
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(detectLineHeight(textElement)).toBe(1.25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getLineHeightInPx", () => {
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(
|
||||||
|
getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
|
||||||
|
).toBe(25);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getDefaultLineHeight", () => {
|
||||||
|
it("should return line height using default font family when not passed", () => {
|
||||||
|
//@ts-ignore
|
||||||
|
expect(getDefaultLineHeight()).toBe(1.25);
|
||||||
|
});
|
||||||
|
it("should return correct line height", () => {
|
||||||
|
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_FAMILY,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
@ -41,12 +43,15 @@ export const normalizeText = (text: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const splitIntoLines = (text: string) => {
|
||||||
|
return normalizeText(text).split("\n");
|
||||||
|
};
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
const boundTextUpdates = {
|
const boundTextUpdates = {
|
||||||
x: textElement.x,
|
x: textElement.x,
|
||||||
y: textElement.y,
|
y: textElement.y,
|
||||||
@ -68,6 +73,7 @@ export const redrawTextBoundingBox = (
|
|||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
@ -185,7 +191,11 @@ export const handleBindTextResize = (
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = measureText(text, getFontString(textElement));
|
const dimensions = measureText(
|
||||||
|
text,
|
||||||
|
getFontString(textElement),
|
||||||
|
textElement.lineHeight,
|
||||||
|
);
|
||||||
nextHeight = dimensions.height;
|
nextHeight = dimensions.height;
|
||||||
nextWidth = dimensions.width;
|
nextWidth = dimensions.width;
|
||||||
}
|
}
|
||||||
@ -261,32 +271,52 @@ const computeBoundTextPosition = (
|
|||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||||
|
|
||||||
export const measureText = (text: string, font: FontString) => {
|
export const measureText = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, font);
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
const width = getTextWidth(text, font);
|
||||||
|
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
/**
|
||||||
const cacheApproxLineHeight: { [key: FontString]: number } = {};
|
* To get unitless line-height (if unknown) we can calculate it by dividing
|
||||||
|
* height-per-line by fontSize.
|
||||||
|
*/
|
||||||
|
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
|
||||||
|
const lineCount = splitIntoLines(textElement.text).length;
|
||||||
|
return (textElement.height /
|
||||||
|
lineCount /
|
||||||
|
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
||||||
|
|
||||||
export const getApproxLineHeight = (font: FontString) => {
|
/**
|
||||||
if (cacheApproxLineHeight[font]) {
|
* We calculate the line height from the font size and the unitless line height,
|
||||||
return cacheApproxLineHeight[font];
|
* aligning with the W3C spec.
|
||||||
}
|
*/
|
||||||
const fontSize = parseInt(font);
|
export const getLineHeightInPx = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return fontSize * lineHeight;
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate line height relative to font size
|
// FIXME rename to getApproxMinContainerHeight
|
||||||
cacheApproxLineHeight[font] = fontSize * 1.2;
|
export const getApproxMinLineHeight = (
|
||||||
return cacheApproxLineHeight[font];
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
@ -309,7 +339,7 @@ const getLineWidth = (text: string, font: FontString) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = splitIntoLines(text);
|
||||||
let width = 0;
|
let width = 0;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
@ -317,10 +347,13 @@ export const getTextWidth = (text: string, font: FontString) => {
|
|||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextHeight = (text: string, font: FontString) => {
|
export const getTextHeight = (
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
text: string,
|
||||||
const lineHeight = getApproxLineHeight(font);
|
fontSize: number,
|
||||||
return lineHeight * lines.length;
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const lineCount = splitIntoLines(text).length;
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
@ -468,21 +501,23 @@ export const charWidth = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
|
|
||||||
|
// FIXME rename to getApproxMinContainerWidth
|
||||||
|
export const getApproxMinLineWidth = (
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
if (maxCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||||
BOUND_TEXT_PADDING * 2
|
BOUND_TEXT_PADDING * 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApproxMinLineHeight = (font: FontString) => {
|
|
||||||
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinCharWidth = (font: FontString) => {
|
export const getMinCharWidth = (font: FontString) => {
|
||||||
const cache = charWidth.getCache(font);
|
const cache = charWidth.getCache(font);
|
||||||
if (!cache) {
|
if (!cache) {
|
||||||
@ -828,3 +863,32 @@ export const isMeasureTextSupported = () => {
|
|||||||
);
|
);
|
||||||
return width > 0;
|
return width > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unitless line height
|
||||||
|
*
|
||||||
|
* In previous versions we used `normal` line height, which browsers interpret
|
||||||
|
* differently, and based on font-family and font-size.
|
||||||
|
*
|
||||||
|
* To make line heights consistent across browsers we hardcode the values for
|
||||||
|
* each of our fonts based on most common average line-heights.
|
||||||
|
* See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
|
||||||
|
* where the values come from.
|
||||||
|
*/
|
||||||
|
const DEFAULT_LINE_HEIGHT = {
|
||||||
|
// ~1.25 is the average for Virgil in WebKit and Blink.
|
||||||
|
// Gecko (FF) uses ~1.28.
|
||||||
|
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
// ~1.15 is the average for Virgil in WebKit and Blink.
|
||||||
|
// Gecko if all over the place.
|
||||||
|
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
// ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
|
||||||
|
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
||||||
|
if (fontFamily) {
|
||||||
|
return DEFAULT_LINE_HEIGHT[fontFamily];
|
||||||
|
}
|
||||||
|
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
||||||
|
};
|
||||||
|
@ -783,7 +783,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(25);
|
expect(text.x).toBe(25);
|
||||||
expect(text.height).toBe(48);
|
expect(text.height).toBe(50);
|
||||||
expect(text.width).toBe(60);
|
expect(text.width).toBe(60);
|
||||||
|
|
||||||
// Edit and text by removing second line and it should
|
// Edit and text by removing second line and it should
|
||||||
@ -810,7 +810,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
expect(text.text).toBe("Hello");
|
||||||
expect(text.originalText).toBe("Hello");
|
expect(text.originalText).toBe("Hello");
|
||||||
expect(text.height).toBe(24);
|
expect(text.height).toBe(25);
|
||||||
expect(text.width).toBe(50);
|
expect(text.width).toBe(50);
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
@ -903,7 +903,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
85,
|
85,
|
||||||
5,
|
4.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -929,7 +929,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -1067,9 +1067,9 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(-35);
|
expect(rectangle.y).toBe(-40);
|
||||||
expect(text.x).toBe(85);
|
expect(text.x).toBe(85);
|
||||||
expect(text.y).toBe(-30);
|
expect(text.y).toBe(-35);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
@ -1112,7 +1112,7 @@ describe("textWysiwyg", () => {
|
|||||||
target: { value: "Online whiteboard collaboration made easy" },
|
target: { value: "Online whiteboard collaboration made easy" },
|
||||||
});
|
});
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(178);
|
expect(rectangle.height).toBe(185);
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -1186,6 +1186,41 @@ describe("textWysiwyg", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update line height when font family updated", async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
editor.blur();
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.25);
|
||||||
|
|
||||||
|
mouse.select(rectangle);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
|
).toEqual(FONT_FAMILY.Cascadia);
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.2);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle(/normal/i));
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
|
).toEqual(FONT_FAMILY.Helvetica);
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
|
).toEqual(1.15);
|
||||||
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
let editor: HTMLTextAreaElement;
|
let editor: HTMLTextAreaElement;
|
||||||
|
|
||||||
@ -1245,7 +1280,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1257,7 +1292,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
30,
|
30,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1269,7 +1304,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
45,
|
45,
|
||||||
45.5,
|
45,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1281,7 +1316,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1292,7 +1327,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
30,
|
30,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1303,7 +1338,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
45,
|
45,
|
||||||
66,
|
65,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1333,7 +1368,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(textElement.width).toBe(600);
|
expect(textElement.width).toBe(600);
|
||||||
expect(textElement.height).toBe(24);
|
expect(textElement.height).toBe(25);
|
||||||
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
||||||
expect((textElement as ExcalidrawTextElement).text).toBe(
|
expect((textElement as ExcalidrawTextElement).text).toBe(
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
@ -1365,7 +1400,7 @@ describe("textWysiwyg", () => {
|
|||||||
],
|
],
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
height: 34,
|
height: 35,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
link: null,
|
link: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
|
@ -22,7 +22,6 @@ import {
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
@ -150,9 +149,7 @@ export const textWysiwyg = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { textAlign, verticalAlign } = updatedTextElement;
|
const { textAlign, verticalAlign } = updatedTextElement;
|
||||||
const approxLineHeight = getApproxLineHeight(
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
);
|
|
||||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||||
let coordX = updatedTextElement.x;
|
let coordX = updatedTextElement.x;
|
||||||
let coordY = updatedTextElement.y;
|
let coordY = updatedTextElement.y;
|
||||||
@ -213,7 +210,7 @@ export const textWysiwyg = ({
|
|||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
||||||
const diff = Math.min(
|
const diff = Math.min(
|
||||||
textElementHeight - maxHeight,
|
textElementHeight - maxHeight,
|
||||||
approxLineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: containerDims.height + diff });
|
mutateElement(container, { height: containerDims.height + diff });
|
||||||
return;
|
return;
|
||||||
@ -226,7 +223,7 @@ export const textWysiwyg = ({
|
|||||||
) {
|
) {
|
||||||
const diff = Math.min(
|
const diff = Math.min(
|
||||||
maxHeight - textElementHeight,
|
maxHeight - textElementHeight,
|
||||||
approxLineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: containerDims.height - diff });
|
mutateElement(container, { height: containerDims.height - diff });
|
||||||
}
|
}
|
||||||
@ -266,10 +263,6 @@ export const textWysiwyg = ({
|
|||||||
editable.selectionEnd = editable.value.length - diff;
|
editable.selectionEnd = editable.value.length - diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = updatedTextElement.originalText.split("\n");
|
|
||||||
const lineHeight = updatedTextElement.containerId
|
|
||||||
? approxLineHeight
|
|
||||||
: updatedTextElement.height / lines.length;
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
@ -282,7 +275,7 @@ export const textWysiwyg = ({
|
|||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: element.lineHeight,
|
||||||
width: `${textElementWidth}px`,
|
width: `${textElementWidth}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
@ -388,7 +381,11 @@ export const textWysiwyg = ({
|
|||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getMaxContainerWidth(container!),
|
||||||
);
|
);
|
||||||
const { width, height } = measureText(wrappedText, font);
|
const { width, height } = measureText(
|
||||||
|
wrappedText,
|
||||||
|
font,
|
||||||
|
updatedTextElement.lineHeight,
|
||||||
|
);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
editable.style.height = `${height}px`;
|
editable.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
|
@ -135,6 +135,11 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId: ExcalidrawGenericElement["id"] | null;
|
containerId: ExcalidrawGenericElement["id"] | null;
|
||||||
originalText: string;
|
originalText: string;
|
||||||
|
/**
|
||||||
|
* Unitless line height (aligned to W3C). To get line height in px, multiply
|
||||||
|
* with font size (using `getLineHeightInPx` helper).
|
||||||
|
*/
|
||||||
|
lineHeight: number & { _brand: "unitlessLineHeight" };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawBindableElement =
|
export type ExcalidrawBindableElement =
|
||||||
|
@ -40,10 +40,10 @@ import {
|
|||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
getLineHeightInPx,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
@ -279,9 +279,7 @@ const drawElementOnCanvas = (
|
|||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
const lineHeight = element.containerId
|
|
||||||
? getApproxLineHeight(getFontString(element))
|
|
||||||
: element.height / lines.length;
|
|
||||||
const horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@ -290,11 +288,16 @@ const drawElementOnCanvas = (
|
|||||||
: 0;
|
: 0;
|
||||||
context.textBaseline = "bottom";
|
context.textBaseline = "bottom";
|
||||||
|
|
||||||
|
const lineHeightPx = getLineHeightInPx(
|
||||||
|
element.fontSize,
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
context.fillText(
|
context.fillText(
|
||||||
lines[index],
|
lines[index],
|
||||||
horizontalOffset,
|
horizontalOffset,
|
||||||
(index + 1) * lineHeight,
|
(index + 1) * lineHeightPx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -1313,7 +1316,10 @@ export const renderElementToSvg = (
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
const lineHeight = element.height / lines.length;
|
const lineHeightPx = getLineHeightInPx(
|
||||||
|
element.fontSize,
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
const horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@ -1331,7 +1337,7 @@ export const renderElementToSvg = (
|
|||||||
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
|
||||||
text.textContent = lines[i];
|
text.textContent = lines[i];
|
||||||
text.setAttribute("x", `${horizontalOffset}`);
|
text.setAttribute("x", `${horizontalOffset}`);
|
||||||
text.setAttribute("y", `${i * lineHeight}`);
|
text.setAttribute("y", `${i * lineHeightPx}`);
|
||||||
text.setAttribute("font-family", getFontFamilyString(element));
|
text.setAttribute("font-family", getFontFamilyString(element));
|
||||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
text.setAttribute("fill", element.strokeColor);
|
text.setAttribute("fill", element.strokeColor);
|
||||||
|
@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
|
@ -3,8 +3,10 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
|
|||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getApproxLineHeight } from "../element/textElement";
|
import {
|
||||||
import { getFontString } from "../utils";
|
getDefaultLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
|
} from "../element/textElement";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
|
|
||||||
@ -118,12 +120,10 @@ describe("paste text as single lines", () => {
|
|||||||
|
|
||||||
it("should space items correctly", async () => {
|
it("should space items correctly", async () => {
|
||||||
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
|
||||||
const lineHeight =
|
const lineHeightPx =
|
||||||
getApproxLineHeight(
|
getLineHeightInPx(
|
||||||
getFontString({
|
h.app.state.currentItemFontSize,
|
||||||
fontSize: h.app.state.currentItemFontSize,
|
getDefaultLineHeight(h.state.currentItemFontFamily),
|
||||||
fontFamily: h.app.state.currentItemFontFamily,
|
|
||||||
}),
|
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -135,19 +135,17 @@ describe("paste text as single lines", () => {
|
|||||||
for (let i = 1; i < h.elements.length; i++) {
|
for (let i = 1; i < h.elements.length; i++) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [fx, elY] = getElementBounds(h.elements[i]);
|
const [fx, elY] = getElementBounds(h.elements[i]);
|
||||||
expect(elY).toEqual(firstElY + lineHeight * i);
|
expect(elY).toEqual(firstElY + lineHeightPx * i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should leave a space for blank new lines", async () => {
|
it("should leave a space for blank new lines", async () => {
|
||||||
const text = "hkhkjhki\n\njgkjhffjh";
|
const text = "hkhkjhki\n\njgkjhffjh";
|
||||||
const lineHeight =
|
const lineHeightPx =
|
||||||
getApproxLineHeight(
|
getLineHeightInPx(
|
||||||
getFontString({
|
h.app.state.currentItemFontSize,
|
||||||
fontSize: h.app.state.currentItemFontSize,
|
getDefaultLineHeight(h.state.currentItemFontFamily),
|
||||||
fontFamily: h.app.state.currentItemFontFamily,
|
|
||||||
}),
|
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
@ -158,7 +156,7 @@ describe("paste text as single lines", () => {
|
|||||||
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
const [fx, firstElY] = getElementBounds(h.elements[0]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
const [lx, lastElY] = getElementBounds(h.elements[1]);
|
||||||
expect(lastElY).toEqual(firstElY + lineHeight * 2);
|
expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -224,7 +222,7 @@ describe("Paste bound text container", () => {
|
|||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
expect(container.height).toBe(354);
|
expect(container.height).toBe(368);
|
||||||
expect(container.width).toBe(166);
|
expect(container.width).toBe(166);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -247,7 +245,7 @@ describe("Paste bound text container", () => {
|
|||||||
await sleep(1);
|
await sleep(1);
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
expect(container.height).toBe(740);
|
expect(container.height).toBe(770);
|
||||||
expect(container.width).toBe(166);
|
expect(container.width).toBe(166);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -291,6 +291,7 @@ Object {
|
|||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -312,7 +313,7 @@ Object {
|
|||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -8.4,
|
"y": -8.75,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -329,6 +330,7 @@ Object {
|
|||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id-text01",
|
"id": "id-text01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
"lineHeight": 1.25,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
@ -181,11 +181,13 @@ export class API {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "text":
|
case "text":
|
||||||
|
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
|
||||||
|
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
|
||||||
element = newTextElement({
|
element = newTextElement({
|
||||||
...base,
|
...base,
|
||||||
text: rest.text || "test",
|
text: rest.text || "test",
|
||||||
fontSize: rest.fontSize ?? appState.currentItemFontSize,
|
fontSize,
|
||||||
fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
|
fontFamily,
|
||||||
textAlign: rest.textAlign ?? appState.currentItemTextAlign,
|
textAlign: rest.textAlign ?? appState.currentItemTextAlign,
|
||||||
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: rest.containerId ?? undefined,
|
containerId: rest.containerId ?? undefined,
|
||||||
|
@ -1031,7 +1031,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"height": 128,
|
"height": 130,
|
||||||
"width": 367,
|
"width": 367,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1040,7 +1040,7 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 272,
|
"x": 272,
|
||||||
"y": 46,
|
"y": 45,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
@ -1052,11 +1052,11 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
20,
|
20,
|
||||||
36,
|
35,
|
||||||
502,
|
502,
|
||||||
94,
|
95,
|
||||||
205.9061448421403,
|
205.9061448421403,
|
||||||
53,
|
52.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1090,7 +1090,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect({ width: container.width, height: container.height })
|
expect({ width: container.width, height: container.height })
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"height": 128,
|
"height": 130,
|
||||||
"width": 340,
|
"width": 340,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1099,7 +1099,7 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": -4,
|
"y": -5,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(textElement.text).toMatchInlineSnapshot(`
|
expect(textElement.text).toMatchInlineSnapshot(`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user