fix: set the dimensions of bound text correctly (#5710)
* fix: set the dimensions of bound text correctly * use original Text when wrapping * fix text align * fix specs * fix * newline
This commit is contained in:
parent
8636ef1017
commit
4cb6f09559
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,4 +25,3 @@ src/packages/excalidraw/types
|
|||||||
src/packages/excalidraw/example/public/bundle.js
|
src/packages/excalidraw/example/public/bundle.js
|
||||||
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
||||||
src/packages/excalidraw/example/public/excalidraw.development.js
|
src/packages/excalidraw/example/public/excalidraw.development.js
|
||||||
|
|
||||||
|
@ -201,6 +201,12 @@ export const VERTICAL_ALIGN = {
|
|||||||
BOTTOM: "bottom",
|
BOTTOM: "bottom",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TEXT_ALIGN = {
|
||||||
|
LEFT: "left",
|
||||||
|
CENTER: "center",
|
||||||
|
RIGHT: "right",
|
||||||
|
};
|
||||||
|
|
||||||
export const ELEMENT_READY_TO_ERASE_OPACITY = 20;
|
export const ELEMENT_READY_TO_ERASE_OPACITY = 20;
|
||||||
|
|
||||||
export const COOKIES = {
|
export const COOKIES = {
|
||||||
|
@ -252,8 +252,16 @@ const getAdjustedDimensions = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||||
|
return getContainerDims(container).width - BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||||
|
return getContainerDims(container).height - BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateTextElement = (
|
export const updateTextElement = (
|
||||||
element: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
{
|
{
|
||||||
text,
|
text,
|
||||||
isDeleted,
|
isDeleted,
|
||||||
@ -264,16 +272,19 @@ export const updateTextElement = (
|
|||||||
originalText: string;
|
originalText: string;
|
||||||
},
|
},
|
||||||
): ExcalidrawTextElement => {
|
): ExcalidrawTextElement => {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(textElement);
|
||||||
if (container) {
|
if (container) {
|
||||||
const containerDims = getContainerDims(container);
|
text = wrapText(
|
||||||
text = wrapText(text, getFontString(element), containerDims.width);
|
originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
getMaxContainerWidth(container),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(element, text);
|
const dimensions = getAdjustedDimensions(textElement, text);
|
||||||
return newElementWith(element, {
|
return newElementWith(textElement, {
|
||||||
text,
|
text,
|
||||||
originalText,
|
originalText,
|
||||||
isDeleted: isDeleted ?? element.isDeleted,
|
isDeleted: isDeleted ?? textElement.isDeleted,
|
||||||
...dimensions,
|
...dimensions,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { BOUND_TEXT_PADDING } from "../constants";
|
||||||
import { wrapText } from "./textElement";
|
import { wrapText } from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ up`,
|
|||||||
},
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
const res = wrapText(text, font, data.width);
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
expect(res).toEqual(data.res);
|
expect(res).toEqual(data.res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -93,7 +94,7 @@ whats up`,
|
|||||||
},
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should respect new lines and ${data.desc}`, () => {
|
it(`should respect new lines and ${data.desc}`, () => {
|
||||||
const res = wrapText(text, font, data.width);
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
expect(res).toEqual(data.res);
|
expect(res).toEqual(data.res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -132,7 +133,7 @@ break it now`,
|
|||||||
},
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
const res = wrapText(text, font, data.width);
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
expect(res).toEqual(data.res);
|
expect(res).toEqual(data.res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,42 +7,41 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
import { BOUND_TEXT_PADDING, 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 ".";
|
||||||
|
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
element: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
let text = element.text;
|
let text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
const containerDims = getContainerDims(container);
|
maxWidth = getMaxContainerWidth(container);
|
||||||
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
element.originalText,
|
textElement.originalText,
|
||||||
getFontString(element),
|
getFontString(textElement),
|
||||||
containerDims.width,
|
getMaxContainerWidth(container),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
element.originalText,
|
textElement.originalText,
|
||||||
getFontString(element),
|
getFontString(textElement),
|
||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
let coordY = element.y;
|
let coordY = textElement.y;
|
||||||
let coordX = element.x;
|
let coordX = textElement.x;
|
||||||
// Resize container and vertically center align the text
|
// Resize container and vertically center align the text
|
||||||
if (container) {
|
if (container) {
|
||||||
const containerDims = getContainerDims(container);
|
const containerDims = getContainerDims(container);
|
||||||
let nextHeight = containerDims.height;
|
let nextHeight = containerDims.height;
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
if (element.verticalAlign === VERTICAL_ALIGN.TOP) {
|
|
||||||
coordY = container.y + BOUND_TEXT_PADDING;
|
coordY = container.y + BOUND_TEXT_PADDING;
|
||||||
} else if (element.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
coordY =
|
coordY =
|
||||||
container.y +
|
container.y +
|
||||||
containerDims.height -
|
containerDims.height -
|
||||||
@ -50,14 +49,25 @@ export const redrawTextBoundingBox = (
|
|||||||
BOUND_TEXT_PADDING;
|
BOUND_TEXT_PADDING;
|
||||||
} else {
|
} else {
|
||||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
||||||
if (metrics.height > containerDims.height - BOUND_TEXT_PADDING * 2) {
|
if (metrics.height > getMaxContainerHeight(container)) {
|
||||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||||
|
coordX = container.x + BOUND_TEXT_PADDING;
|
||||||
|
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
|
coordX =
|
||||||
|
container.x + containerDims.width - metrics.width - BOUND_TEXT_PADDING;
|
||||||
|
} else {
|
||||||
|
coordX = container.x + container.width / 2 - metrics.width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
}
|
}
|
||||||
mutateElement(element, {
|
|
||||||
|
mutateElement(textElement, {
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
baseline: metrics.baseline,
|
baseline: metrics.baseline,
|
||||||
@ -118,6 +128,7 @@ export const handleBindTextResize = (
|
|||||||
}
|
}
|
||||||
let text = textElement.text;
|
let text = textElement.text;
|
||||||
let nextHeight = textElement.height;
|
let nextHeight = textElement.height;
|
||||||
|
let nextWidth = textElement.width;
|
||||||
let containerHeight = element.height;
|
let containerHeight = element.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
@ -125,7 +136,7 @@ export const handleBindTextResize = (
|
|||||||
text = wrapText(
|
text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
element.width,
|
getMaxContainerWidth(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +146,7 @@ export const handleBindTextResize = (
|
|||||||
element.width,
|
element.width,
|
||||||
);
|
);
|
||||||
nextHeight = dimensions.height;
|
nextHeight = dimensions.height;
|
||||||
|
nextWidth = dimensions.width;
|
||||||
nextBaseLine = dimensions.baseline;
|
nextBaseLine = dimensions.baseline;
|
||||||
}
|
}
|
||||||
// increase height in case text element height exceeds
|
// increase height in case text element height exceeds
|
||||||
@ -162,13 +174,12 @@ export const handleBindTextResize = (
|
|||||||
} else {
|
} else {
|
||||||
updatedY = element.y + element.height / 2 - nextHeight / 2;
|
updatedY = element.y + element.height / 2 - nextHeight / 2;
|
||||||
}
|
}
|
||||||
|
const updatedX = element.x + element.width / 2 - nextWidth / 2;
|
||||||
mutateElement(textElement, {
|
mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
// preserve padding and set width correctly
|
width: nextWidth,
|
||||||
width: element.width - BOUND_TEXT_PADDING * 2,
|
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
x: element.x + BOUND_TEXT_PADDING,
|
x: updatedX,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
baseline: nextBaseLine,
|
baseline: nextBaseLine,
|
||||||
});
|
});
|
||||||
@ -195,7 +206,6 @@ export const measureText = (
|
|||||||
container.style.minHeight = "1em";
|
container.style.minHeight = "1em";
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
const lineHeight = getApproxLineHeight(font);
|
const lineHeight = getApproxLineHeight(font);
|
||||||
container.style.width = `${String(maxWidth)}px`;
|
|
||||||
container.style.maxWidth = `${String(maxWidth)}px`;
|
container.style.maxWidth = `${String(maxWidth)}px`;
|
||||||
container.style.overflow = "hidden";
|
container.style.overflow = "hidden";
|
||||||
container.style.wordBreak = "break-word";
|
container.style.wordBreak = "break-word";
|
||||||
@ -213,7 +223,8 @@ export const measureText = (
|
|||||||
container.appendChild(span);
|
container.appendChild(span);
|
||||||
// Baseline is important for positioning text on canvas
|
// Baseline is important for positioning text on canvas
|
||||||
const baseline = span.offsetTop + span.offsetHeight;
|
const baseline = span.offsetTop + span.offsetHeight;
|
||||||
const width = container.offsetWidth;
|
// Since span adds 1px extra width to the container
|
||||||
|
const width = container.offsetWidth + 1;
|
||||||
|
|
||||||
const height = container.offsetHeight;
|
const height = container.offsetHeight;
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
@ -251,13 +262,7 @@ const getTextWidth = (text: string, font: FontString) => {
|
|||||||
return metrics.width;
|
return metrics.width;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wrapText = (
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
containerWidth: number,
|
|
||||||
) => {
|
|
||||||
const maxWidth = containerWidth - BOUND_TEXT_PADDING * 2;
|
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getTextWidth(" ", font);
|
const spaceWidth = getTextWidth(" ", font);
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from "../actions/actionProperties";
|
} from "../actions/actionProperties";
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
|
import { getMaxContainerWidth } from "./newElement";
|
||||||
|
|
||||||
const normalizeText = (text: string) => {
|
const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@ -114,13 +115,13 @@ export const textWysiwyg = ({
|
|||||||
getFontString(updatedTextElement),
|
getFontString(updatedTextElement),
|
||||||
);
|
);
|
||||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||||
let coordX = updatedTextElement.x;
|
const coordX = updatedTextElement.x;
|
||||||
let coordY = updatedTextElement.y;
|
let coordY = updatedTextElement.y;
|
||||||
const container = getContainerElement(updatedTextElement);
|
const container = getContainerElement(updatedTextElement);
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
let width = updatedTextElement.width;
|
const width = 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 height = updatedTextElement.height;
|
let height = updatedTextElement.height;
|
||||||
@ -146,10 +147,6 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
||||||
maxHeight = containerDims.height - BOUND_TEXT_PADDING * 2;
|
maxHeight = containerDims.height - BOUND_TEXT_PADDING * 2;
|
||||||
width = maxWidth;
|
|
||||||
// The coordinates of text box set a distance of
|
|
||||||
// 5px to preserve padding
|
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (height > maxHeight) {
|
if (height > maxHeight) {
|
||||||
const diff = Math.min(height - maxHeight, approxLineHeight);
|
const diff = Math.min(height - maxHeight, approxLineHeight);
|
||||||
@ -212,7 +209,7 @@ export const textWysiwyg = ({
|
|||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: `${lineHeight}px`,
|
||||||
width: `${width}px`,
|
width: `${Math.min(width, maxWidth)}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
@ -229,7 +226,6 @@ export const textWysiwyg = ({
|
|||||||
color: updatedTextElement.strokeColor,
|
color: updatedTextElement.strokeColor,
|
||||||
opacity: updatedTextElement.opacity / 100,
|
opacity: updatedTextElement.opacity / 100,
|
||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
maxWidth: `${maxWidth}px`,
|
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
// For some reason updating font attribute doesn't set font family
|
// For some reason updating font attribute doesn't set font family
|
||||||
@ -301,13 +297,14 @@ export const textWysiwyg = ({
|
|||||||
// doubles the height as soon as user starts typing
|
// doubles the height as soon as user starts typing
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
if (isBoundToContainer(element) && lines > 1) {
|
||||||
let height = "auto";
|
let height = "auto";
|
||||||
|
editable.style.height = "0px";
|
||||||
|
let heightSet = false;
|
||||||
if (lines === 2) {
|
if (lines === 2) {
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
const actualLineCount = wrapText(
|
const actualLineCount = wrapText(
|
||||||
editable.value,
|
editable.value,
|
||||||
font,
|
font,
|
||||||
container!.width,
|
getMaxContainerWidth(container!),
|
||||||
).split("\n").length;
|
).split("\n").length;
|
||||||
// This is browser behaviour when setting height to "auto"
|
// This is browser behaviour when setting height to "auto"
|
||||||
// It sets the height needed for 2 lines even if actual
|
// It sets the height needed for 2 lines even if actual
|
||||||
@ -316,10 +313,13 @@ export const textWysiwyg = ({
|
|||||||
// so single line aligns vertically when deleting
|
// so single line aligns vertically when deleting
|
||||||
if (actualLineCount === 1) {
|
if (actualLineCount === 1) {
|
||||||
height = `${editable.scrollHeight / 2}px`;
|
height = `${editable.scrollHeight / 2}px`;
|
||||||
|
editable.style.height = height;
|
||||||
|
heightSet = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editable.style.height = height;
|
if (!heightSet) {
|
||||||
editable.style.height = `${editable.scrollHeight}px`;
|
editable.style.height = `${editable.scrollHeight}px`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onChange(normalizeText(editable.value));
|
onChange(normalizeText(editable.value));
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { FONT_FAMILY, THEME, VERTICAL_ALIGN } from "../constants";
|
import { FONT_FAMILY, TEXT_ALIGN, THEME, VERTICAL_ALIGN } from "../constants";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
@ -11,7 +11,7 @@ export type GroupId = string;
|
|||||||
export type PointerType = "mouse" | "pen" | "touch";
|
export type PointerType = "mouse" | "pen" | "touch";
|
||||||
export type StrokeSharpness = "round" | "sharp";
|
export type StrokeSharpness = "round" | "sharp";
|
||||||
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||||
export type TextAlign = "left" | "center" | "right";
|
export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
|
||||||
|
|
||||||
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
||||||
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
||||||
|
@ -296,7 +296,7 @@ Object {
|
|||||||
"versionNonce": 0,
|
"versionNonce": 0,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": -0.5,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user