mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Compare commits
10 Commits
zsviczian-
...
aakansha-n
Author | SHA1 | Date | |
---|---|---|---|
4ed3a2e7be | |||
faec098e30 | |||
65e849804d | |||
ae7ff76126 | |||
7c0f783cbc | |||
fd379c2897 | |||
97929c07d6 | |||
ba22646c22 | |||
c21fecde40 | |||
caf0a904db |
@ -20,15 +20,13 @@ import {
|
|||||||
isTestEnv,
|
isTestEnv,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
|
import { bumpVersion, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getBoundTextElementOffset,
|
|
||||||
getContainerDims,
|
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
@ -44,7 +42,6 @@ import {
|
|||||||
DEFAULT_VERTICAL_ALIGN,
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
@ -211,8 +208,6 @@ const getAdjustedDimensions = (
|
|||||||
height: number;
|
height: number;
|
||||||
baseline: number;
|
baseline: number;
|
||||||
} => {
|
} => {
|
||||||
const container = getContainerElement(element);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
@ -268,27 +263,6 @@ const getAdjustedDimensions = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure container dimensions are set properly when
|
|
||||||
// text editor overflows beyond viewport dimensions
|
|
||||||
if (container) {
|
|
||||||
const boundTextElementPadding = getBoundTextElementOffset(element);
|
|
||||||
|
|
||||||
const containerDims = getContainerDims(container);
|
|
||||||
let height = containerDims.height;
|
|
||||||
let width = containerDims.width;
|
|
||||||
if (nextHeight > height - boundTextElementPadding * 2) {
|
|
||||||
height = nextHeight + boundTextElementPadding * 2;
|
|
||||||
}
|
|
||||||
if (nextWidth > width - boundTextElementPadding * 2) {
|
|
||||||
width = nextWidth + boundTextElementPadding * 2;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!isArrowElement(container) &&
|
|
||||||
(height !== containerDims.height || width !== containerDims.width)
|
|
||||||
) {
|
|
||||||
mutateElement(container, { height, width });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
|
@ -49,18 +49,7 @@ describe("Test wrapText", () => {
|
|||||||
{
|
{
|
||||||
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",
|
||||||
width: 25,
|
width: 25,
|
||||||
res: `H
|
res: `H\ne\nl\nl\no \nw\nh\na\nt\ns \nu\np`,
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
@ -90,8 +79,7 @@ up`,
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
describe("When text contain new lines", () => {
|
||||||
const text = `Hello
|
const text = "Hello\nwhats up";
|
||||||
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",
|
||||||
@ -101,18 +89,7 @@ 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",
|
||||||
width: 25,
|
width: 25,
|
||||||
res: `H
|
res: `H\ne\nl\nl\no\nw\nh\na\nt\ns \nu\np`,
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
@ -149,13 +126,7 @@ whats up`,
|
|||||||
desc: "fit characters of long string as per container width and break words as per the width",
|
desc: "fit characters of long string as per container width and break words as per the width",
|
||||||
|
|
||||||
width: 130,
|
width: 130,
|
||||||
res: `hellolongte
|
res: `hellolongte\nxtthisiswha\ntsupwithyou\nIamtypinggg\nggandtyping\ngg break it \nnow`,
|
||||||
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",
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
@ -190,7 +161,7 @@ now`,
|
|||||||
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
||||||
const res = wrapText(text, font, 110);
|
const res = wrapText(text, font, 110);
|
||||||
expect(res).toBe(
|
expect(res).toBe(
|
||||||
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
|
`Wikipedia \nis hosted \nby \nWikimedia- \nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts \na range-of \nother \nprojects`,
|
||||||
);
|
);
|
||||||
|
|
||||||
text = "Hello thereusing-now";
|
text = "Hello thereusing-now";
|
||||||
|
@ -76,6 +76,7 @@ export const redrawTextBoundingBox = (
|
|||||||
boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
|
maxWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
@ -195,6 +196,7 @@ export const handleBindTextResize = (
|
|||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
|
maxWidth,
|
||||||
);
|
);
|
||||||
nextHeight = metrics.height;
|
nextHeight = metrics.height;
|
||||||
nextWidth = metrics.width;
|
nextWidth = metrics.width;
|
||||||
@ -283,6 +285,7 @@ export const measureText = (
|
|||||||
text: string,
|
text: string,
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
maxWidth?: number | null,
|
||||||
) => {
|
) => {
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@ -292,7 +295,14 @@ export const measureText = (
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
const fontSize = parseFloat(font);
|
const fontSize = parseFloat(font);
|
||||||
const height = getTextHeight(text, fontSize, lineHeight);
|
const height = getTextHeight(text, fontSize, lineHeight);
|
||||||
const width = getTextWidth(text, font);
|
let width = getTextWidth(text, font);
|
||||||
|
// Since we now preserve trailing whitespaces so if the text has
|
||||||
|
// trailing whitespaces, it will be considered in the width and thus width
|
||||||
|
// computed might be much higher than the allowed max width
|
||||||
|
// by the container hence making sure the width never goes beyond the max width.
|
||||||
|
if (maxWidth) {
|
||||||
|
width = Math.min(width, maxWidth);
|
||||||
|
}
|
||||||
const baseline = measureBaseline(text, font, lineHeight);
|
const baseline = measureBaseline(text, font, lineHeight);
|
||||||
return { width, height, baseline };
|
return { width, height, baseline };
|
||||||
};
|
};
|
||||||
@ -380,7 +390,7 @@ export const getApproxMinLineHeight = (
|
|||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
export const getLineWidth = (text: string, font: FontString) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
@ -440,10 +450,8 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
|
||||||
|
|
||||||
let currentLine = "";
|
let currentLine = "";
|
||||||
let currentLineWidthTillNow = 0;
|
let currentLineWidthTillNow = 0;
|
||||||
@ -459,7 +467,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
};
|
};
|
||||||
originalLines.forEach((originalLine) => {
|
originalLines.forEach((originalLine) => {
|
||||||
const currentLineWidth = getTextWidth(originalLine, font);
|
const currentLineWidth = getLineWidth(originalLine, font);
|
||||||
|
|
||||||
// Push the line if its <= maxWidth
|
// Push the line if its <= maxWidth
|
||||||
if (currentLineWidth <= maxWidth) {
|
if (currentLineWidth <= maxWidth) {
|
||||||
@ -507,23 +515,25 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
resetParams();
|
resetParams();
|
||||||
// space needs to be appended before next word
|
// space needs to be appended before next word
|
||||||
// as currentLine contains chars which couldn't be appended
|
// as currentLine contains chars which couldn't be appended
|
||||||
// to previous line unless the line ends with hyphen to sync
|
// to previous line unless the line ends with hyphen to sync
|
||||||
// with css word-wrap
|
// with css word-wrap
|
||||||
} else if (!currentLine.endsWith("-")) {
|
} else if (!currentLine.endsWith("-") && index < words.length) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
currentLineWidthTillNow += spaceWidth;
|
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
} else {
|
} else {
|
||||||
// Start appending words in a line till max width reached
|
// Start appending words in a line till max width reached
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(
|
||||||
|
`${currentLine + word}`.trimEnd(),
|
||||||
|
font,
|
||||||
|
);
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
@ -531,24 +541,20 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
index++;
|
|
||||||
|
|
||||||
// if word ends with "-" then we don't need to add space
|
// if word ends with "-" then we don't need to add space
|
||||||
// to sync with css word-wrap
|
// to sync with css word-wrap
|
||||||
const shouldAppendSpace = !word.endsWith("-");
|
const shouldAppendSpace = !word.endsWith("-");
|
||||||
currentLine += word;
|
currentLine += word;
|
||||||
|
|
||||||
if (shouldAppendSpace) {
|
if (shouldAppendSpace && index < words.length) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
}
|
}
|
||||||
|
index++;
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
// Push the word if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
if (shouldAppendSpace) {
|
lines.push(currentLine);
|
||||||
lines.push(currentLine.slice(0, -1));
|
|
||||||
} else {
|
|
||||||
lines.push(currentLine);
|
|
||||||
}
|
|
||||||
resetParams();
|
resetParams();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -971,3 +977,22 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
|||||||
}
|
}
|
||||||
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSpacesOffsetForLine = (
|
||||||
|
element: ExcalidrawTextElement,
|
||||||
|
line: string,
|
||||||
|
font: FontString,
|
||||||
|
) => {
|
||||||
|
const container = getContainerElement(element);
|
||||||
|
const trailingSpacesWidth =
|
||||||
|
getLineWidth(line, font) - getLineWidth(line.trimEnd(), font);
|
||||||
|
const maxWidth = container ? getBoundTextMaxWidth(container) : element.width;
|
||||||
|
const availableWidth = maxWidth - getLineWidth(line.trimEnd(), font);
|
||||||
|
let spacesOffset = 0;
|
||||||
|
if (element.textAlign === TEXT_ALIGN.CENTER) {
|
||||||
|
spacesOffset = -Math.min(trailingSpacesWidth / 2, availableWidth / 2);
|
||||||
|
} else if (element.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
|
spacesOffset = -Math.min(availableWidth, trailingSpacesWidth);
|
||||||
|
}
|
||||||
|
return spacesOffset;
|
||||||
|
};
|
||||||
|
@ -1213,32 +1213,46 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
const originalRectHeight = rectangle.height;
|
const container = API.createElement({
|
||||||
expect(rectangle.height).toBe(originalRectHeight);
|
type: "rectangle",
|
||||||
|
height: 75,
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
width: 90,
|
||||||
const editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: { value: "Online whiteboard collaboration made easy" },
|
|
||||||
});
|
});
|
||||||
editor.blur();
|
const originalRectHeight = container.height;
|
||||||
expect(rectangle.height).toBe(185);
|
expect(container.height).toBe(originalRectHeight);
|
||||||
mouse.select(rectangle);
|
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Online whiteboard collaboration made easy",
|
||||||
|
});
|
||||||
|
h.elements = [container, text];
|
||||||
|
API.setSelectedElements([container, text]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
});
|
});
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
let contextMenu = document.querySelector(".context-menu");
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
||||||
|
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
||||||
|
);
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 20,
|
||||||
|
clientY: 30,
|
||||||
|
});
|
||||||
|
contextMenu = document.querySelector(".context-menu");
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
|
||||||
expect(h.elements[0].boundElements).toEqual([]);
|
expect(h.elements[0].boundElements).toEqual([]);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
expect(getOriginalContainerHeightFromCache(container.id)).toBe(null);
|
||||||
|
|
||||||
expect(rectangle.height).toBe(originalRectHeight);
|
expect(container.height).toBe(originalRectHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset the container height cache when resizing", async () => {
|
it("should reset the container height cache when resizing", async () => {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
|
import { CLASSES, TEXT_ALIGN, isSafari } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -23,11 +23,9 @@ 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,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@ -36,6 +34,7 @@ import {
|
|||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
|
computeBoundTextPosition,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@ -162,7 +161,7 @@ export const textWysiwyg = ({
|
|||||||
let textElementWidth = updatedTextElement.width;
|
let textElementWidth = updatedTextElement.width;
|
||||||
// Set to element height by default since that's
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let textElementHeight = updatedTextElement.height;
|
const textElementHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
@ -179,15 +178,6 @@ 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));
|
|
||||||
if (editorHeight > 0) {
|
|
||||||
textElementHeight = editorHeight;
|
|
||||||
}
|
|
||||||
if (propertiesUpdated) {
|
|
||||||
// update height of the editor after properties updated
|
|
||||||
textElementHeight = updatedTextElement.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let originalContainerData;
|
let originalContainerData;
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
@ -206,6 +196,8 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getBoundTextMaxWidth(container);
|
maxWidth = getBoundTextMaxWidth(container);
|
||||||
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
|
|
||||||
maxHeight = getBoundTextMaxHeight(
|
maxHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
@ -232,25 +224,24 @@ export const textWysiwyg = ({
|
|||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: targetContainerHeight });
|
mutateElement(container, { height: targetContainerHeight });
|
||||||
}
|
} 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);
|
let spacesOffset = 0;
|
||||||
|
if (updatedTextElement.textAlign === TEXT_ALIGN.CENTER) {
|
||||||
|
spacesOffset = Math.max(0, updatedTextElement.width / 2 - maxWidth / 2);
|
||||||
|
} else if (updatedTextElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
|
spacesOffset = Math.max(0, updatedTextElement.width - maxWidth);
|
||||||
|
}
|
||||||
|
const [viewportX, viewportY] = getViewportCoords(
|
||||||
|
coordX + spacesOffset,
|
||||||
|
coordY,
|
||||||
|
);
|
||||||
const initialSelectionStart = editable.selectionStart;
|
const initialSelectionStart = editable.selectionStart;
|
||||||
const initialSelectionEnd = editable.selectionEnd;
|
const initialSelectionEnd = editable.selectionEnd;
|
||||||
const initialLength = editable.value.length;
|
const initialLength = editable.value.length;
|
||||||
@ -382,31 +373,17 @@ export const textWysiwyg = ({
|
|||||||
font,
|
font,
|
||||||
getBoundTextMaxWidth(container),
|
getBoundTextMaxWidth(container),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const { width } = measureText(
|
||||||
|
wrappedText,
|
||||||
|
font,
|
||||||
|
element.lineHeight,
|
||||||
|
getBoundTextMaxWidth(container),
|
||||||
|
);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
editable.oninput = () => {
|
editable.oninput = () => {
|
||||||
const updatedTextElement = Scene.getScene(element)?.getElement(
|
|
||||||
id,
|
|
||||||
) as ExcalidrawTextElement;
|
|
||||||
const font = getFontString(updatedTextElement);
|
|
||||||
if (isBoundToContainer(element)) {
|
|
||||||
const container = getContainerElement(element);
|
|
||||||
const wrappedText = wrapText(
|
|
||||||
normalizeText(editable.value),
|
|
||||||
font,
|
|
||||||
getBoundTextMaxWidth(container!),
|
|
||||||
);
|
|
||||||
const { width, height } = measureText(
|
|
||||||
wrappedText,
|
|
||||||
font,
|
|
||||||
updatedTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
editable.style.width = `${width}px`;
|
|
||||||
editable.style.height = `${height}px`;
|
|
||||||
}
|
|
||||||
onChange(normalizeText(editable.value));
|
onChange(normalizeText(editable.value));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import {
|
|||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
|
getSpacesOffsetForLine,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@ -319,13 +320,13 @@ const drawElementOnCanvas = (
|
|||||||
}
|
}
|
||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
context.save();
|
context.save();
|
||||||
context.font = getFontString(element);
|
const font = getFontString(element);
|
||||||
|
context.font = font;
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle = element.strokeColor;
|
||||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
// 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
|
||||||
@ -336,11 +337,17 @@ const drawElementOnCanvas = (
|
|||||||
element.fontSize,
|
element.fontSize,
|
||||||
element.lineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
const verticalOffset = element.height - element.baseline;
|
const verticalOffset = element.height - element.baseline;
|
||||||
for (let index = 0; index < lines.length; index++) {
|
for (let index = 0; index < lines.length; index++) {
|
||||||
context.fillText(
|
const spacesOffset = getSpacesOffsetForLine(
|
||||||
|
element,
|
||||||
lines[index],
|
lines[index],
|
||||||
horizontalOffset,
|
font,
|
||||||
|
);
|
||||||
|
context.fillText(
|
||||||
|
lines[index].trimEnd(),
|
||||||
|
horizontalOffset + spacesOffset,
|
||||||
(index + 1) * lineHeightPx - verticalOffset,
|
(index + 1) * lineHeightPx - verticalOffset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1151,7 +1151,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"Online whiteboard collaboration
|
"Online whiteboard collaboration
|
||||||
made easy"
|
made easy"
|
||||||
`);
|
`);
|
||||||
const handleBindTextResizeSpy = jest.spyOn(
|
const handleBindTextResizeSpy = jest.spyOn(
|
||||||
|
Reference in New Issue
Block a user