Compare commits
19 Commits
master
...
aakansha-r
Author | SHA1 | Date | |
---|---|---|---|
|
993294ac08 | ||
|
f584416c9a | ||
|
87b0c7a679 | ||
|
ee8fff8e8b | ||
|
b799490ece | ||
|
fd18896293 | ||
|
e900cb0b64 | ||
|
54bf3d9092 | ||
|
15f19835fe | ||
|
96c4cff805 | ||
|
1ac580136d | ||
|
8c89fdfa51 | ||
|
0e54994187 | ||
|
91f6e87317 | ||
|
a05db6864e | ||
|
eacee9a158 | ||
|
7722de4ef2 | ||
|
0a295e523b | ||
|
60deddb0e2 |
@ -4,9 +4,9 @@ import { mutateElement } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import { measureText } from "../element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
|
@ -54,8 +54,8 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import { getDefaultLineHeight } from "../element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
@ -12,16 +12,14 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
getBoundTextElement,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
getDefaultRoundnessTypeForElement,
|
getDefaultRoundnessTypeForElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { getDefaultLineHeight } from "../element/textMeasurements";
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
|
@ -260,18 +260,20 @@ import throttle from "lodash.throttle";
|
|||||||
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
|
||||||
getLineHeightInPx,
|
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
isMeasureTextSupported,
|
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import {
|
||||||
|
getApproxMinContainerHeight,
|
||||||
|
getApproxMinContainerWidth,
|
||||||
|
isMeasureTextSupported,
|
||||||
|
getLineHeightInPx,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "../element/textMeasurements";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
normalizeLink,
|
normalizeLink,
|
||||||
@ -2622,11 +2624,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
};
|
};
|
||||||
const minWidth = getApproxMinLineWidth(
|
const minWidth = getApproxMinContainerWidth(
|
||||||
getFontString(fontString),
|
getFontString(fontString),
|
||||||
lineHeight,
|
lineHeight,
|
||||||
);
|
);
|
||||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
const minHeight = getApproxMinContainerHeight(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);
|
||||||
|
@ -35,7 +35,10 @@ 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";
|
import {
|
||||||
|
detectLineHeight,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "../element/textMeasurements";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -25,15 +25,17 @@ import {
|
|||||||
getBoundTextElementOffset,
|
getBoundTextElementOffset,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
getBoundTextMaxWidth,
|
||||||
getMaxContainerWidth,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
import {
|
||||||
|
measureText,
|
||||||
|
wrapText,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
} from "./textMeasurements";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@ -270,7 +272,7 @@ export const refreshTextDimensions = (
|
|||||||
text = wrapText(
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
getMaxContainerWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
|
@ -39,15 +39,16 @@ import {
|
|||||||
import { Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
getApproxMinLineHeight,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
import {
|
||||||
|
getApproxMinContainerHeight,
|
||||||
|
getApproxMinContainerWidth,
|
||||||
|
} from "./textMeasurements";
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
return angle - 2 * Math.PI;
|
return angle - 2 * Math.PI;
|
||||||
@ -201,7 +202,7 @@ const measureFontSizeFromWidth = (
|
|||||||
if (hasContainer) {
|
if (hasContainer) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
if (container) {
|
if (container) {
|
||||||
width = getMaxContainerWidth(container);
|
width = getBoundTextMaxWidth(container);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextFontSize = element.fontSize * (nextWidth / width);
|
const nextFontSize = element.fontSize * (nextWidth / width);
|
||||||
@ -421,18 +422,18 @@ export const resizeSingleElement = (
|
|||||||
|
|
||||||
const nextFontSize = measureFontSizeFromWidth(
|
const nextFontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
getMaxContainerWidth(updatedElement),
|
getBoundTextMaxWidth(updatedElement),
|
||||||
);
|
);
|
||||||
if (nextFontSize === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boundTextFontSize = nextFontSize;
|
boundTextFontSize = nextFontSize;
|
||||||
} else {
|
} else {
|
||||||
const minWidth = getApproxMinLineWidth(
|
const minWidth = getApproxMinContainerWidth(
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
const minHeight = getApproxMinLineHeight(
|
const minHeight = getApproxMinContainerHeight(
|
||||||
boundTextElement.fontSize,
|
boundTextElement.fontSize,
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
@ -698,7 +699,7 @@ const resizeMultipleElements = (
|
|||||||
const fontSize = measureFontSizeFromWidth(
|
const fontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||||
boundTextElement
|
boundTextElement
|
||||||
? getMaxContainerWidth(updatedElement)
|
? getBoundTextMaxWidth(updatedElement)
|
||||||
: updatedElement.width,
|
: updatedElement.width,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,189 +1,11 @@
|
|||||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getMaxContainerWidth,
|
getBoundTextMaxWidth,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxHeight,
|
||||||
wrapText,
|
|
||||||
detectLineHeight,
|
|
||||||
getLineHeightInPx,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { ExcalidrawTextElementWithContainer } from "./types";
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
|
||||||
|
|
||||||
it("shouldn't add new lines for trailing spaces", () => {
|
|
||||||
const text = "Hello whats up ";
|
|
||||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with emojis", () => {
|
|
||||||
const text = "😀";
|
|
||||||
const maxWidth = 1;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe("😀");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the text correctly when max width reached", () => {
|
|
||||||
const text = "Hello😀";
|
|
||||||
const maxWidth = 10;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
|
||||||
const text = "Hello whats up";
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 80,
|
|
||||||
res: `Hello \nwhats \nup`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 140,
|
|
||||||
res: `Hello whats \nup`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: "Hello whats up",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should push the word if its equal to max width",
|
|
||||||
width: 60,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
|
||||||
const text = `Hello
|
|
||||||
whats up`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 80,
|
|
||||||
res: `Hello\nwhats \nup`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 150,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should respect new lines and ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text is long", () => {
|
|
||||||
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width",
|
|
||||||
width: 170,
|
|
||||||
res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width and break words as per the width",
|
|
||||||
|
|
||||||
width: 130,
|
|
||||||
res: `hellolongte
|
|
||||||
xtthisiswha
|
|
||||||
tsupwithyou
|
|
||||||
Iamtypinggg
|
|
||||||
ggandtyping
|
|
||||||
gg break it
|
|
||||||
now`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
|
||||||
|
|
||||||
width: 600,
|
|
||||||
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
|
||||||
const text = "Hello Excalidraw";
|
|
||||||
// Length of "Excalidraw" is 100 and exacty equal to max width
|
|
||||||
const res = wrapText(text, font, 100);
|
|
||||||
expect(res).toEqual(`Hello \nExcalidraw`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the text as is if max width is invalid", () => {
|
|
||||||
const text = "Hello Excalidraw";
|
|
||||||
expect(wrapText(text, font, NaN)).toEqual(text);
|
|
||||||
expect(wrapText(text, font, -1)).toEqual(text);
|
|
||||||
expect(wrapText(text, font, Infinity)).toEqual(text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test measureText", () => {
|
||||||
describe("Test getContainerCoords", () => {
|
describe("Test getContainerCoords", () => {
|
||||||
@ -260,7 +82,7 @@ describe("Test measureText", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test getMaxContainerWidth", () => {
|
describe("Test getBoundTextMaxWidth", () => {
|
||||||
const params = {
|
const params = {
|
||||||
width: 178,
|
width: 178,
|
||||||
height: 194,
|
height: 194,
|
||||||
@ -268,71 +90,84 @@ describe("Test measureText", () => {
|
|||||||
|
|
||||||
it("should return max width when container is rectangle", () => {
|
it("should return max width when container is rectangle", () => {
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(168);
|
expect(getBoundTextMaxWidth(container)).toBe(168);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is ellipse", () => {
|
it("should return max width when container is ellipse", () => {
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(116);
|
expect(getBoundTextMaxWidth(container)).toBe(116);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max width when container is diamond", () => {
|
it("should return max width when container is diamond", () => {
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
expect(getMaxContainerWidth(container)).toBe(79);
|
expect(getBoundTextMaxWidth(container)).toBe(79);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is arrow", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getBoundTextMaxWidth(container)).toBe(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test getMaxContainerHeight", () => {
|
describe("Test getBoundTextMaxHeight", () => {
|
||||||
const params = {
|
const params = {
|
||||||
width: 178,
|
width: 178,
|
||||||
height: 194,
|
height: 194,
|
||||||
|
id: "container-id",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const boundTextElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text-id",
|
||||||
|
x: 560.51171875,
|
||||||
|
y: 202.033203125,
|
||||||
|
width: 154,
|
||||||
|
height: 175,
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 1,
|
||||||
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
||||||
|
textAlign: "center",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
containerId: params.id,
|
||||||
|
}) as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
it("should return max height when container is rectangle", () => {
|
it("should return max height when container is rectangle", () => {
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(184);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max height when container is ellipse", () => {
|
it("should return max height when container is ellipse", () => {
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(127);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return max height when container is diamond", () => {
|
it("should return max height when container is diamond", () => {
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
expect(getMaxContainerHeight(container)).toBe(87);
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const textElement = API.createElement({
|
it("should return max height when container is arrow", () => {
|
||||||
type: "text",
|
const container = API.createElement({
|
||||||
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
type: "arrow",
|
||||||
fontSize: 20,
|
...params,
|
||||||
fontFamily: 1,
|
});
|
||||||
height: 175,
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test detectLineHeight", () => {
|
it("should return max height when container is arrow and height is less than threshold", () => {
|
||||||
it("should return correct line height", () => {
|
const container = API.createElement({
|
||||||
expect(detectLineHeight(textElement)).toBe(1.25);
|
type: "arrow",
|
||||||
});
|
...params,
|
||||||
|
height: 70,
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test getLineHeightInPx", () => {
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
|
||||||
it("should return correct line height", () => {
|
boundTextElement.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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,13 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontFamilyValues,
|
|
||||||
FontString,
|
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
DEFAULT_FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
FONT_FAMILY,
|
|
||||||
TEXT_ALIGN,
|
|
||||||
VERTICAL_ALIGN,
|
|
||||||
} from "../constants";
|
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
@ -32,6 +23,7 @@ import {
|
|||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
import { ExtractSetType } from "../utility-types";
|
import { ExtractSetType } from "../utility-types";
|
||||||
|
import { measureText, wrapText } from "./textMeasurements";
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@ -43,10 +35,6 @@ 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,
|
||||||
@ -60,16 +48,15 @@ export const redrawTextBoundingBox = (
|
|||||||
height: textElement.height,
|
height: textElement.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
boundTextUpdates.text = textElement.text;
|
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
@ -80,27 +67,22 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.height = metrics.height;
|
boundTextUpdates.height = metrics.height;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const centerX = textElement.x + textElement.width / 2;
|
|
||||||
const centerY = textElement.y + textElement.height / 2;
|
|
||||||
const diffWidth = metrics.width - textElement.width;
|
|
||||||
const diffHeight = metrics.height - textElement.height;
|
|
||||||
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
|
|
||||||
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
|
|
||||||
} else {
|
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
let maxContainerHeight = getMaxContainerHeight(container);
|
const maxContainerHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
let nextHeight = containerDims.height;
|
let nextHeight = containerDims.height;
|
||||||
|
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (metrics.height > maxContainerHeight) {
|
||||||
nextHeight = computeContainerDimensionForBoundText(
|
nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
maxContainerHeight = getMaxContainerHeight(container);
|
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
@ -109,7 +91,6 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.x = x;
|
boundTextUpdates.x = x;
|
||||||
boundTextUpdates.y = y;
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(textElement, boundTextUpdates);
|
mutateElement(textElement, boundTextUpdates);
|
||||||
};
|
};
|
||||||
@ -180,8 +161,11 @@ export const handleBindTextResize = (
|
|||||||
let nextHeight = textElement.height;
|
let nextHeight = textElement.height;
|
||||||
let nextWidth = textElement.width;
|
let nextWidth = textElement.width;
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
const maxWidth = getMaxContainerWidth(container);
|
const maxWidth = getBoundTextMaxWidth(container);
|
||||||
const maxHeight = getMaxContainerHeight(container);
|
const maxHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
let containerHeight = containerDims.height;
|
let containerHeight = containerDims.height;
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
if (text) {
|
if (text) {
|
||||||
@ -239,16 +223,22 @@ export const handleBindTextResize = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeBoundTextPosition = (
|
export const computeBoundTextPosition = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const containerCoords = getContainerCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
const maxContainerHeight = getMaxContainerHeight(container);
|
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
||||||
const maxContainerWidth = getMaxContainerWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||||
|
|
||||||
let x;
|
let x;
|
||||||
let y;
|
let y;
|
||||||
|
if (isArrowElement(container)) {
|
||||||
|
return LinearElementEditor.getBoundTextElementPosition(
|
||||||
|
container,
|
||||||
|
boundTextElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
y = containerCoords.y;
|
y = containerCoords.y;
|
||||||
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
@ -269,298 +259,6 @@ const computeBoundTextPosition = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
|
||||||
|
|
||||||
export const measureText = (
|
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
text = text
|
|
||||||
.split("\n")
|
|
||||||
// replace empty lines with single space because leading/trailing empty
|
|
||||||
// lines would be stripped from computation
|
|
||||||
.map((x) => x || " ")
|
|
||||||
.join("\n");
|
|
||||||
const fontSize = parseFloat(font);
|
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
|
||||||
const width = getTextWidth(text, font);
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We calculate the line height from the font size and the unitless line height,
|
|
||||||
* aligning with the W3C spec.
|
|
||||||
*/
|
|
||||||
export const getLineHeightInPx = (
|
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return fontSize * lineHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME rename to getApproxMinContainerHeight
|
|
||||||
export const getApproxMinLineHeight = (
|
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
|
||||||
if (!canvas) {
|
|
||||||
canvas = document.createElement("canvas");
|
|
||||||
}
|
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
|
||||||
canvas2dContext.font = font;
|
|
||||||
const width = canvas2dContext.measureText(text).width;
|
|
||||||
|
|
||||||
// since in test env the canvas measureText algo
|
|
||||||
// doesn't measure text and instead just returns number of
|
|
||||||
// characters hence we assume that each letteris 10px
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return width * 10;
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
|
||||||
const lines = splitIntoLines(text);
|
|
||||||
let width = 0;
|
|
||||||
lines.forEach((line) => {
|
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
|
||||||
});
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextHeight = (
|
|
||||||
text: string,
|
|
||||||
fontSize: number,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
const lineCount = splitIntoLines(text).length;
|
|
||||||
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|
||||||
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
|
||||||
// computation, we need to make sure we don't continue as we'll end up
|
|
||||||
// in an infinite loop
|
|
||||||
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
|
||||||
const originalLines = text.split("\n");
|
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
|
||||||
|
|
||||||
let currentLine = "";
|
|
||||||
let currentLineWidthTillNow = 0;
|
|
||||||
|
|
||||||
const push = (str: string) => {
|
|
||||||
if (str.trim()) {
|
|
||||||
lines.push(str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetParams = () => {
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
originalLines.forEach((originalLine) => {
|
|
||||||
const currentLineWidth = getTextWidth(originalLine, font);
|
|
||||||
|
|
||||||
//Push the line if its <= maxWidth
|
|
||||||
if (currentLineWidth <= maxWidth) {
|
|
||||||
lines.push(originalLine);
|
|
||||||
return; // continue
|
|
||||||
}
|
|
||||||
const words = originalLine.split(" ");
|
|
||||||
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
while (index < words.length) {
|
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
|
||||||
|
|
||||||
// This will only happen when single word takes entire width
|
|
||||||
if (currentWordWidth === maxWidth) {
|
|
||||||
push(words[index]);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
|
||||||
else if (currentWordWidth > maxWidth) {
|
|
||||||
// push current line since the current word exceeds the max width
|
|
||||||
// so will be appended in next line
|
|
||||||
push(currentLine);
|
|
||||||
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
while (words[index].length > 0) {
|
|
||||||
const currentChar = String.fromCodePoint(
|
|
||||||
words[index].codePointAt(0)!,
|
|
||||||
);
|
|
||||||
const width = charWidth.calculate(currentChar, font);
|
|
||||||
currentLineWidthTillNow += width;
|
|
||||||
words[index] = words[index].slice(currentChar.length);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
currentLine = currentChar;
|
|
||||||
currentLineWidthTillNow = width;
|
|
||||||
} else {
|
|
||||||
currentLine += currentChar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// push current line if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
resetParams();
|
|
||||||
} else {
|
|
||||||
// space needs to be appended before next word
|
|
||||||
// as currentLine contains chars which couldn't be appended
|
|
||||||
// to previous line
|
|
||||||
currentLine += " ";
|
|
||||||
currentLineWidthTillNow += spaceWidth;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
// Start appending words in a line till max width reached
|
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
|
||||||
const word = words[index];
|
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
currentLine += `${word} `;
|
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
const word = currentLine.slice(0, -1);
|
|
||||||
push(word);
|
|
||||||
resetParams();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLine.slice(-1) === " ") {
|
|
||||||
// only remove last trailing space which we have added when joining words
|
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
push(currentLine);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return lines.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const charWidth = (() => {
|
|
||||||
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
|
||||||
|
|
||||||
const calculate = (char: string, font: FontString) => {
|
|
||||||
const ascii = char.charCodeAt(0);
|
|
||||||
if (!cachedCharWidth[font]) {
|
|
||||||
cachedCharWidth[font] = [];
|
|
||||||
}
|
|
||||||
if (!cachedCharWidth[font][ascii]) {
|
|
||||||
const width = getLineWidth(char, font);
|
|
||||||
cachedCharWidth[font][ascii] = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedCharWidth[font][ascii];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCache = (font: FontString) => {
|
|
||||||
return cachedCharWidth[font];
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
calculate,
|
|
||||||
getCache,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|
||||||
|
|
||||||
// FIXME rename to getApproxMinContainerWidth
|
|
||||||
export const getApproxMinLineWidth = (
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
|
||||||
if (maxCharWidth === 0) {
|
|
||||||
return (
|
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
|
||||||
BOUND_TEXT_PADDING * 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinCharWidth = (font: FontString) => {
|
|
||||||
const cache = charWidth.getCache(font);
|
|
||||||
if (!cache) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
|
||||||
|
|
||||||
return Math.min(...cacheWithOutEmpty);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMaxCharWidth = (font: FontString) => {
|
|
||||||
const cache = charWidth.getCache(font);
|
|
||||||
if (!cache) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
|
||||||
return Math.max(...cacheWithOutEmpty);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|
||||||
// Generally lower case is used so converting to lower case
|
|
||||||
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
|
||||||
const batchLength = 6;
|
|
||||||
let index = 0;
|
|
||||||
let widthTillNow = 0;
|
|
||||||
let str = "";
|
|
||||||
while (widthTillNow <= width) {
|
|
||||||
const batch = dummyText.substr(index, index + batchLength);
|
|
||||||
str += batch;
|
|
||||||
widthTillNow += getLineWidth(str, font);
|
|
||||||
if (index === dummyText.length - 1) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
index = index + batchLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (widthTillNow > width) {
|
|
||||||
str = str.substr(0, str.length - 1);
|
|
||||||
widthTillNow = getLineWidth(str, font);
|
|
||||||
}
|
|
||||||
return str.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.length
|
return container?.boundElements?.length
|
||||||
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
||||||
@ -686,18 +384,6 @@ export const getBoundTextElementOffset = (
|
|||||||
return BOUND_TEXT_PADDING;
|
return BOUND_TEXT_PADDING;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElementPosition = (
|
|
||||||
container: ExcalidrawElement,
|
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
|
||||||
) => {
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
return LinearElementEditor.getBoundTextElementPosition(
|
|
||||||
container,
|
|
||||||
boundTextElement,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shouldAllowVerticalAlign = (
|
export const shouldAllowVerticalAlign = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
@ -798,18 +484,10 @@ export const computeContainerDimensionForBoundText = (
|
|||||||
return dimension + padding;
|
return dimension + padding;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
|
||||||
const width = getContainerDims(container).width;
|
const width = getContainerDims(container).width;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
return width - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerWidth <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.width;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return containerWidth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.type === "ellipse") {
|
if (container.type === "ellipse") {
|
||||||
@ -826,16 +504,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
|||||||
return width - BOUND_TEXT_PADDING * 2;
|
return width - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
export const getBoundTextMaxHeight = (
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
|
) => {
|
||||||
const height = getContainerDims(container).height;
|
const height = getContainerDims(container).height;
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerHeight <= 0) {
|
if (containerHeight <= 0) {
|
||||||
const boundText = getBoundTextElement(container);
|
return boundTextElement.height;
|
||||||
if (boundText) {
|
|
||||||
return boundText.height;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@ -852,43 +529,3 @@ export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
|||||||
}
|
}
|
||||||
return height - BOUND_TEXT_PADDING * 2;
|
return height - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMeasureTextSupported = () => {
|
|
||||||
const width = getTextWidth(
|
|
||||||
DUMMY_TEXT,
|
|
||||||
getFontString({
|
|
||||||
fontSize: DEFAULT_FONT_SIZE,
|
|
||||||
fontFamily: DEFAULT_FONT_FAMILY,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
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];
|
|
||||||
};
|
|
||||||
|
213
src/element/textMeasurements.test.ts
Normal file
213
src/element/textMeasurements.test.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import {
|
||||||
|
detectLineHeight,
|
||||||
|
getDefaultLineHeight,
|
||||||
|
getLineHeightInPx,
|
||||||
|
wrapText,
|
||||||
|
} from "./textMeasurements";
|
||||||
|
import { FontString } from "./types";
|
||||||
|
|
||||||
|
describe("Test wrapText", () => {
|
||||||
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||||
|
|
||||||
|
it("shouldn't add new lines for trailing spaces", () => {
|
||||||
|
const text = "Hello whats up ";
|
||||||
|
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with emojis", () => {
|
||||||
|
const text = "😀";
|
||||||
|
const maxWidth = 1;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("😀");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the text correctly when max width reached", () => {
|
||||||
|
const text = "Hello😀";
|
||||||
|
const maxWidth = 10;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text doesn't contain new lines", () => {
|
||||||
|
const text = "Hello whats up";
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "break all words when width of each word is less than container width",
|
||||||
|
width: 80,
|
||||||
|
res: `Hello \nwhats \nup`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break all characters when width of each character is less than container width",
|
||||||
|
width: 25,
|
||||||
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break words as per the width",
|
||||||
|
|
||||||
|
width: 140,
|
||||||
|
res: `Hello whats \nup`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the container",
|
||||||
|
|
||||||
|
width: 250,
|
||||||
|
res: "Hello whats up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should push the word if its equal to max width",
|
||||||
|
width: 60,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text contain new lines", () => {
|
||||||
|
const text = `Hello
|
||||||
|
whats up`;
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "break all words when width of each word is less than container width",
|
||||||
|
width: 80,
|
||||||
|
res: `Hello\nwhats \nup`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break all characters when width of each character is less than container width",
|
||||||
|
width: 25,
|
||||||
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break words as per the width",
|
||||||
|
|
||||||
|
width: 150,
|
||||||
|
res: `Hello
|
||||||
|
whats up`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the container",
|
||||||
|
|
||||||
|
width: 250,
|
||||||
|
res: `Hello
|
||||||
|
whats up`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should respect new lines and ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text is long", () => {
|
||||||
|
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "fit characters of long string as per container width",
|
||||||
|
width: 170,
|
||||||
|
res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
desc: "fit characters of long string as per container width and break words as per the width",
|
||||||
|
|
||||||
|
width: 130,
|
||||||
|
res: `hellolongte
|
||||||
|
xtthisiswha
|
||||||
|
tsupwithyou
|
||||||
|
Iamtypinggg
|
||||||
|
ggandtyping
|
||||||
|
gg break it
|
||||||
|
now`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
|
|
||||||
|
width: 600,
|
||||||
|
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
||||||
|
const text = "Hello Excalidraw";
|
||||||
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
||||||
|
const res = wrapText(text, font, 100);
|
||||||
|
expect(res).toEqual(`Hello \nExcalidraw`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the text as is if max width is invalid", () => {
|
||||||
|
const text = "Hello Excalidraw";
|
||||||
|
expect(wrapText(text, font, NaN)).toEqual(text);
|
||||||
|
expect(wrapText(text, font, -1)).toEqual(text);
|
||||||
|
expect(wrapText(text, font, Infinity)).toEqual(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
339
src/element/textMeasurements.ts
Normal file
339
src/element/textMeasurements.ts
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_FAMILY,
|
||||||
|
} from "../constants";
|
||||||
|
import { getFontString, isTestEnv } from "../utils";
|
||||||
|
import { normalizeText } from "./textElement";
|
||||||
|
import { ExcalidrawTextElement, FontFamilyValues, FontString } from "./types";
|
||||||
|
|
||||||
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
|
// since in test env the canvas measureText algo
|
||||||
|
// doesn't measure text and instead just returns number of
|
||||||
|
// characters hence we assume that each letter is 10px
|
||||||
|
const DUMMY_CHAR_WIDTH = 10;
|
||||||
|
|
||||||
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
|
if (!canvas) {
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
|
}
|
||||||
|
const canvas2dContext = canvas.getContext("2d")!;
|
||||||
|
canvas2dContext.font = font;
|
||||||
|
const width = canvas2dContext.measureText(text).width;
|
||||||
|
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return width * DUMMY_CHAR_WIDTH;
|
||||||
|
}
|
||||||
|
/* istanbul ignore next */
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
|
const lines = splitIntoLines(text);
|
||||||
|
let width = 0;
|
||||||
|
lines.forEach((line) => {
|
||||||
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
|
});
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextHeight = (
|
||||||
|
text: string,
|
||||||
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const lineCount = splitIntoLines(text).length;
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitIntoLines = (text: string) => {
|
||||||
|
return normalizeText(text).split("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureText = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
text = text
|
||||||
|
.split("\n")
|
||||||
|
// replace empty lines with single space because leading/trailing empty
|
||||||
|
// lines would be stripped from computation
|
||||||
|
.map((x) => x || " ")
|
||||||
|
.join("\n");
|
||||||
|
const fontSize = parseFloat(font);
|
||||||
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
|
const width = getTextWidth(text, font);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApproxMinContainerWidth = (
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
|
if (maxCharWidth === 0) {
|
||||||
|
return (
|
||||||
|
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||||
|
BOUND_TEXT_PADDING * 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApproxMinContainerHeight = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const charWidth = (() => {
|
||||||
|
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
||||||
|
|
||||||
|
const calculate = (char: string, font: FontString) => {
|
||||||
|
const ascii = char.charCodeAt(0);
|
||||||
|
if (!cachedCharWidth[font]) {
|
||||||
|
cachedCharWidth[font] = [];
|
||||||
|
}
|
||||||
|
if (!cachedCharWidth[font][ascii]) {
|
||||||
|
const width = getLineWidth(char, font);
|
||||||
|
cachedCharWidth[font][ascii] = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedCharWidth[font][ascii];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCache = (font: FontString) => {
|
||||||
|
return cachedCharWidth[font];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
calculate,
|
||||||
|
getCache,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const getMaxCharWidth = (font: FontString) => {
|
||||||
|
const cache = charWidth.getCache(font);
|
||||||
|
if (!cache) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
||||||
|
return Math.max(...cacheWithOutEmpty);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** this is not used currently but might be useful
|
||||||
|
* in future hence keeping it
|
||||||
|
*/
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
||||||
|
// Generally lower case is used so converting to lower case
|
||||||
|
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
||||||
|
const batchLength = 6;
|
||||||
|
let index = 0;
|
||||||
|
let widthTillNow = 0;
|
||||||
|
let str = "";
|
||||||
|
while (widthTillNow <= width) {
|
||||||
|
const batch = dummyText.substr(index, index + batchLength);
|
||||||
|
str += batch;
|
||||||
|
widthTillNow += getLineWidth(str, font);
|
||||||
|
if (index === dummyText.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
index = index + batchLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (widthTillNow > width) {
|
||||||
|
str = str.substr(0, str.length - 1);
|
||||||
|
widthTillNow = getLineWidth(str, font);
|
||||||
|
}
|
||||||
|
return str.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
|
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
||||||
|
// computation, we need to make sure we don't continue as we'll end up
|
||||||
|
// in an infinite loop
|
||||||
|
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: Array<string> = [];
|
||||||
|
const originalLines = text.split("\n");
|
||||||
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
|
let currentLine = "";
|
||||||
|
let currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
|
const push = (str: string) => {
|
||||||
|
if (str.trim()) {
|
||||||
|
lines.push(str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetParams = () => {
|
||||||
|
currentLine = "";
|
||||||
|
currentLineWidthTillNow = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
originalLines.forEach((originalLine) => {
|
||||||
|
const currentLineWidth = getTextWidth(originalLine, font);
|
||||||
|
|
||||||
|
//Push the line if its <= maxWidth
|
||||||
|
if (currentLineWidth <= maxWidth) {
|
||||||
|
lines.push(originalLine);
|
||||||
|
return; // continue
|
||||||
|
}
|
||||||
|
const words = originalLine.split(" ");
|
||||||
|
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < words.length) {
|
||||||
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
|
||||||
|
// This will only happen when single word takes entire width
|
||||||
|
if (currentWordWidth === maxWidth) {
|
||||||
|
push(words[index]);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start breaking longer words exceeding max width
|
||||||
|
else if (currentWordWidth > maxWidth) {
|
||||||
|
// push current line since the current word exceeds the max width
|
||||||
|
// so will be appended in next line
|
||||||
|
push(currentLine);
|
||||||
|
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
while (words[index].length > 0) {
|
||||||
|
const currentChar = String.fromCodePoint(
|
||||||
|
words[index].codePointAt(0)!,
|
||||||
|
);
|
||||||
|
const width = charWidth.calculate(currentChar, font);
|
||||||
|
currentLineWidthTillNow += width;
|
||||||
|
words[index] = words[index].slice(currentChar.length);
|
||||||
|
|
||||||
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
currentLine = currentChar;
|
||||||
|
currentLineWidthTillNow = width;
|
||||||
|
} else {
|
||||||
|
currentLine += currentChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push current line if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
resetParams();
|
||||||
|
} else {
|
||||||
|
// space needs to be appended before next word
|
||||||
|
// as currentLine contains chars which couldn't be appended
|
||||||
|
// to previous line
|
||||||
|
currentLine += " ";
|
||||||
|
currentLineWidthTillNow += spaceWidth;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
// Start appending words in a line till max width reached
|
||||||
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
|
const word = words[index];
|
||||||
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
currentLine += `${word} `;
|
||||||
|
|
||||||
|
// Push the word if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
|
const word = currentLine.slice(0, -1);
|
||||||
|
push(word);
|
||||||
|
resetParams();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine.slice(-1) === " ") {
|
||||||
|
// only remove last trailing space which we have added when joining words
|
||||||
|
currentLine = currentLine.slice(0, -1);
|
||||||
|
push(currentLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMeasureTextSupported = () => {
|
||||||
|
const width = getTextWidth(
|
||||||
|
DUMMY_TEXT,
|
||||||
|
getFontString({
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return width > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We calculate the line height from the font size and the unitless line height,
|
||||||
|
* aligning with the W3C spec.
|
||||||
|
*/
|
||||||
|
export const getLineHeightInPx = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return fontSize * lineHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Helvetica in WebKit and Blink.
|
||||||
|
// Gecko if all over the place.
|
||||||
|
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
|
||||||
|
// ~1.2 is the average for Cascadia 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];
|
||||||
|
};
|
@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -23,17 +23,14 @@ import { AppState } from "../types";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerCoords,
|
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
getBoundTextMaxHeight,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxWidth,
|
||||||
getMaxContainerWidth,
|
computeBoundTextPosition,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@ -43,6 +40,12 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
import {
|
||||||
|
getTextWidth,
|
||||||
|
measureText,
|
||||||
|
wrapText,
|
||||||
|
getTextHeight,
|
||||||
|
} from "./textMeasurements";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -177,15 +180,12 @@ export const textWysiwyg = ({
|
|||||||
editable,
|
editable,
|
||||||
);
|
);
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
// using editor.style.height to get the accurate height of text editor
|
|
||||||
const editorHeight = Number(editable.style.height.slice(0, -2));
|
textElementHeight = getTextHeight(
|
||||||
if (editorHeight > 0) {
|
updatedTextElement.text,
|
||||||
textElementHeight = editorHeight;
|
updatedTextElement.fontSize,
|
||||||
}
|
updatedTextElement.lineHeight,
|
||||||
if (propertiesUpdated) {
|
);
|
||||||
// update height of the editor after properties updated
|
|
||||||
textElementHeight = updatedTextElement.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let originalContainerData;
|
let originalContainerData;
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
@ -203,8 +203,11 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
maxHeight = getMaxContainerHeight(container);
|
maxHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
||||||
@ -226,22 +229,12 @@ export const textWysiwyg = ({
|
|||||||
element.lineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: containerDims.height - diff });
|
mutateElement(container, { height: containerDims.height - diff });
|
||||||
}
|
} else {
|
||||||
// Start pushing text upward until a diff of 30px (padding)
|
const { y } = computeBoundTextPosition(
|
||||||
// is reached
|
container,
|
||||||
else {
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
const containerCoords = getContainerCoords(container);
|
);
|
||||||
|
coordY = y;
|
||||||
// vertically center align the text
|
|
||||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
|
||||||
if (!isArrowElement(container)) {
|
|
||||||
coordY =
|
|
||||||
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
|
||||||
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
@ -362,7 +355,7 @@ export const textWysiwyg = ({
|
|||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
`${editable.value}${data}`,
|
`${editable.value}${data}`,
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const width = getTextWidth(wrappedText, font);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
@ -379,7 +372,7 @@ export const textWysiwyg = ({
|
|||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
normalizeText(editable.value),
|
normalizeText(editable.value),
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getBoundTextMaxWidth(container!),
|
||||||
);
|
);
|
||||||
const { width, height } = measureText(
|
const { width, height } = measureText(
|
||||||
wrappedText,
|
wrappedText,
|
||||||
|
@ -43,11 +43,11 @@ import {
|
|||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
getBoundTextMaxHeight,
|
||||||
getMaxContainerHeight,
|
getBoundTextMaxWidth,
|
||||||
getMaxContainerWidth,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { getLineHeightInPx } from "../element/textMeasurements";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@ -279,7 +279,6 @@ 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 horizontalOffset =
|
const horizontalOffset =
|
||||||
element.textAlign === "center"
|
element.textAlign === "center"
|
||||||
? element.width / 2
|
? element.width / 2
|
||||||
@ -823,14 +822,17 @@ const drawElementFromCanvas = (
|
|||||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
|
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
|
||||||
hasBoundTextElement(element)
|
hasBoundTextElement(element)
|
||||||
) {
|
) {
|
||||||
|
const textElement = getBoundTextElement(
|
||||||
|
element,
|
||||||
|
) as ExcalidrawTextElementWithContainer;
|
||||||
const coords = getContainerCoords(element);
|
const coords = getContainerCoords(element);
|
||||||
context.strokeStyle = "#c92a2a";
|
context.strokeStyle = "#c92a2a";
|
||||||
context.lineWidth = 3;
|
context.lineWidth = 3;
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
||||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
||||||
getMaxContainerWidth(element) * window.devicePixelRatio,
|
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||||
getMaxContainerHeight(element) * window.devicePixelRatio,
|
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@ 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 { getElementBounds } from "../element";
|
||||||
|
import { NormalizedZoomValue } from "../types";
|
||||||
import {
|
import {
|
||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
} from "../element/textElement";
|
} from "../element/textMeasurements";
|
||||||
import { getElementBounds } from "../element";
|
|
||||||
import { NormalizedZoomValue } from "../types";
|
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -17,11 +17,8 @@ import { KEYS } from "../keys";
|
|||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { queryByTestId, queryByText } from "@testing-library/react";
|
import { queryByTestId, queryByText } from "@testing-library/react";
|
||||||
import { resize, rotate } from "./utils";
|
import { resize, rotate } from "./utils";
|
||||||
import {
|
import { getBoundTextMaxWidth } from "../element/textElement";
|
||||||
getBoundTextElementPosition,
|
import { wrapText } from "../element/textMeasurements";
|
||||||
wrapText,
|
|
||||||
getMaxContainerWidth,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import * as textElementUtils from "../element/textElement";
|
import * as textElementUtils from "../element/textElement";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
|
|
||||||
@ -729,7 +726,7 @@ describe("Test Linear Elements", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
text: wrapText(text, font, getMaxContainerWidth(container)),
|
text: wrapText(text, font, getBoundTextMaxWidth(container)),
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 20,
|
height: 20,
|
||||||
@ -937,8 +934,9 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
expect(container.angle).toBe(0);
|
expect(container.angle).toBe(0);
|
||||||
expect(textElement.angle).toBe(0);
|
expect(textElement.angle).toBe(0);
|
||||||
expect(getBoundTextElementPosition(arrow, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(arrow, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
@ -964,8 +962,9 @@ describe("Test Linear Elements", () => {
|
|||||||
rotate(container, -35, 55);
|
rotate(container, -35, 55);
|
||||||
expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
|
expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
|
||||||
expect(textElement.angle).toBe(0);
|
expect(textElement.angle).toBe(0);
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(container, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 21.73926141863671,
|
"x": 21.73926141863671,
|
||||||
"y": 73.31003398390868,
|
"y": 73.31003398390868,
|
||||||
@ -1002,8 +1001,9 @@ describe("Test Linear Elements", () => {
|
|||||||
);
|
);
|
||||||
expect(container.width).toBe(70);
|
expect(container.width).toBe(70);
|
||||||
expect(container.height).toBe(50);
|
expect(container.height).toBe(50);
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(container, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
@ -1036,8 +1036,9 @@ describe("Test Linear Elements", () => {
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(container, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 272,
|
"x": 272,
|
||||||
"y": 45,
|
"y": 45,
|
||||||
@ -1070,8 +1071,9 @@ describe("Test Linear Elements", () => {
|
|||||||
arrow,
|
arrow,
|
||||||
);
|
);
|
||||||
expect(container.width).toBe(40);
|
expect(container.width).toBe(40);
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(container, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 25,
|
"x": 25,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -1095,8 +1097,9 @@ describe("Test Linear Elements", () => {
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
LinearElementEditor.getBoundTextElementPosition(container, textElement),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 75,
|
"x": 75,
|
||||||
"y": -5,
|
"y": -5,
|
||||||
@ -1149,7 +1152,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(rect.x).toBe(400);
|
expect(rect.x).toBe(400);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard collaboration
|
"Online whiteboard collaboration
|
||||||
made easy"
|
made easy"
|
||||||
@ -1172,7 +1175,7 @@ describe("Test Linear Elements", () => {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
|
Loading…
x
Reference in New Issue
Block a user