fix: use canvas measureText to calculate width in measureText (#6030)
* fix: use canvas measureText to calculate width in measureText * calculate multiline width correctly using canvas measure text and rename functions * set correct width when pasting in bound container * take existing value + new pasted * remove debugger :p * fix snaps
This commit is contained in:
parent
2595e0de82
commit
af3b93c410
@ -157,7 +157,7 @@ describe("Test measureText", () => {
|
|||||||
|
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
expect(res.container).toMatchInlineSnapshot(`
|
||||||
<div
|
<div
|
||||||
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 111px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
||||||
|
@ -271,9 +271,12 @@ export const measureText = (
|
|||||||
container.style.whiteSpace = "pre";
|
container.style.whiteSpace = "pre";
|
||||||
container.style.font = font;
|
container.style.font = font;
|
||||||
container.style.minHeight = "1em";
|
container.style.minHeight = "1em";
|
||||||
|
const textWidth = getTextWidth(text, font);
|
||||||
|
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
const lineHeight = getApproxLineHeight(font);
|
const lineHeight = getApproxLineHeight(font);
|
||||||
container.style.width = `${String(maxWidth + 1)}px`;
|
container.style.width = `${String(Math.min(textWidth, maxWidth) + 1)}px`;
|
||||||
|
|
||||||
container.style.overflow = "hidden";
|
container.style.overflow = "hidden";
|
||||||
container.style.wordBreak = "break-word";
|
container.style.wordBreak = "break-word";
|
||||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
container.style.lineHeight = `${String(lineHeight)}px`;
|
||||||
@ -291,7 +294,10 @@ export const measureText = (
|
|||||||
// 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;
|
||||||
// Since span adds 1px extra width to the container
|
// Since span adds 1px extra width to the container
|
||||||
const width = container.offsetWidth - 1;
|
let width = container.offsetWidth;
|
||||||
|
if (maxWidth && textWidth > maxWidth) {
|
||||||
|
width = width - 1;
|
||||||
|
}
|
||||||
const height = container.offsetHeight;
|
const height = container.offsetHeight;
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
@ -312,7 +318,7 @@ export const getApproxLineHeight = (font: FontString) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
const getTextWidth = (text: string, font: FontString) => {
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
@ -330,10 +336,18 @@ const getTextWidth = (text: string, font: FontString) => {
|
|||||||
return metrics.width;
|
return metrics.width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let width = 0;
|
||||||
|
lines.forEach((line) => {
|
||||||
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
|
});
|
||||||
|
return width;
|
||||||
|
};
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getTextWidth(" ", font);
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
const push = (str: string) => {
|
const push = (str: string) => {
|
||||||
if (str.trim()) {
|
if (str.trim()) {
|
||||||
@ -351,7 +365,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < words.length) {
|
while (index < words.length) {
|
||||||
const currentWordWidth = getTextWidth(words[index], font);
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
// Start breaking longer words exceeding max width
|
||||||
if (currentWordWidth >= maxWidth) {
|
if (currentWordWidth >= maxWidth) {
|
||||||
@ -400,7 +414,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
// 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 = getTextWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
@ -448,7 +462,7 @@ export const charWidth = (() => {
|
|||||||
cachedCharWidth[font] = [];
|
cachedCharWidth[font] = [];
|
||||||
}
|
}
|
||||||
if (!cachedCharWidth[font][ascii]) {
|
if (!cachedCharWidth[font][ascii]) {
|
||||||
const width = getTextWidth(char, font);
|
const width = getLineWidth(char, font);
|
||||||
cachedCharWidth[font][ascii] = width;
|
cachedCharWidth[font][ascii] = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +522,7 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|||||||
while (widthTillNow <= width) {
|
while (widthTillNow <= width) {
|
||||||
const batch = dummyText.substr(index, index + batchLength);
|
const batch = dummyText.substr(index, index + batchLength);
|
||||||
str += batch;
|
str += batch;
|
||||||
widthTillNow += getTextWidth(str, font);
|
widthTillNow += getLineWidth(str, font);
|
||||||
if (index === dummyText.length - 1) {
|
if (index === dummyText.length - 1) {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
@ -517,7 +531,7 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|||||||
|
|
||||||
while (widthTillNow > width) {
|
while (widthTillNow > width) {
|
||||||
str = str.substr(0, str.length - 1);
|
str = str.substr(0, str.length - 1);
|
||||||
widthTillNow = getTextWidth(str, font);
|
widthTillNow = getLineWidth(str, font);
|
||||||
}
|
}
|
||||||
return str.length;
|
return str.length;
|
||||||
};
|
};
|
||||||
|
@ -862,7 +862,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
110.5,
|
110,
|
||||||
17,
|
17,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@ -910,7 +910,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
426,
|
425,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@ -1026,7 +1026,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(85);
|
expect(rectangle.y).toBe(85);
|
||||||
expect(text.x).toBe(90.5);
|
expect(text.x).toBe(90);
|
||||||
expect(text.y).toBe(90);
|
expect(text.y).toBe(90);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
|
getTextWidth,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
@ -39,6 +40,7 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { parseClipboard } from "../clipboard";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -348,6 +350,32 @@ export const textWysiwyg = ({
|
|||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
|
editable.onpaste = async (event) => {
|
||||||
|
const clipboardData = await parseClipboard(event, true);
|
||||||
|
if (!clipboardData.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = normalizeText(clipboardData.text);
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const container = getContainerElement(element);
|
||||||
|
|
||||||
|
const font = getFontString({
|
||||||
|
fontSize: app.state.currentItemFontSize,
|
||||||
|
fontFamily: app.state.currentItemFontFamily,
|
||||||
|
});
|
||||||
|
if (container) {
|
||||||
|
const wrappedText = wrapText(
|
||||||
|
`${editable.value}${data}`,
|
||||||
|
font,
|
||||||
|
getMaxContainerWidth(container),
|
||||||
|
);
|
||||||
|
const width = getTextWidth(wrappedText, font);
|
||||||
|
editable.style.width = `${width}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
editable.oninput = () => {
|
editable.oninput = () => {
|
||||||
const updatedTextElement = Scene.getScene(element)?.getElement(
|
const updatedTextElement = Scene.getScene(element)?.getElement(
|
||||||
id,
|
id,
|
||||||
|
@ -312,7 +312,7 @@ Object {
|
|||||||
"versionNonce": 0,
|
"versionNonce": 0,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0.5,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1027,7 +1027,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(getBoundTextElementPosition(container, textElement))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 387.5,
|
"x": 387,
|
||||||
"y": 70,
|
"y": 70,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1086,7 +1086,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(getBoundTextElementPosition(container, textElement))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"x": 190.5,
|
"x": 190,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user