From dfaaff44320a1d18fcb20ffd65391cde39778bfc Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Wed, 30 Oct 2024 15:11:13 +0200 Subject: [PATCH] fix: fix trailing line whitespaces layout shift (#8714) --- .../excalidraw/element/textElement.test.ts | 35 ++++++++++++++++++ packages/excalidraw/element/textElement.ts | 37 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/element/textElement.test.ts b/packages/excalidraw/element/textElement.test.ts index 59727c22ff..6275b762e3 100644 --- a/packages/excalidraw/element/textElement.test.ts +++ b/packages/excalidraw/element/textElement.test.ts @@ -47,6 +47,41 @@ describe("Test wrapText", () => { expect(res).toBe("don't wrap this number\n99,100.99"); }); + it("should trim all trailing whitespaces", () => { + const text = "Hello "; + const maxWidth = 50; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello"); + }); + + it("should trim all but one trailing whitespaces", () => { + const text = "Hello "; + const maxWidth = 60; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello "); + }); + + it("should keep preceding whitespaces and trim all trailing whitespaces", () => { + const text = " Hello World"; + const maxWidth = 90; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(" Hello\nWorld"); + }); + + it("should keep some preceding whitespaces, trim trailing whitespaces, but kep those that fit in the trailing line", () => { + const text = " Hello World "; + const maxWidth = 90; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(" Hello\nWorld "); + }); + + it("should trim keep those whitespace that fit in the trailing line", () => { + const text = "Hello Wo rl d "; + const maxWidth = 100; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello Wo\nrl d "); + }); + it("should support multiple (multi-codepoint) emojis", () => { const text = "πŸ˜€πŸ—ΊπŸ”₯πŸ‘©πŸ½β€πŸ¦°πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦πŸ‡¨πŸ‡Ώ"; const maxWidth = 1; diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index 7618dba804..9d72961b50 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -681,7 +681,7 @@ const wrapLine = ( lines.push(...precedingLines); - // trailing line of the wrapped word might still be joined with next token/s + // trailing line of the wrapped word might -still be joined with next token/s currentLine = trailingLine; currentLineWidth = getLineWidth(trailingLine, font, true); iterator = tokenIterator.next(); @@ -697,12 +697,45 @@ const wrapLine = ( // iterator done, push the trailing line if exists if (currentLine) { - lines.push(currentLine.trimEnd()); + const trailingLine = trimTrailingLine(currentLine, font, maxWidth); + lines.push(trailingLine); } return lines; }; +// similarly to browsers, does not trim all whitespaces, but only those exceeding the maxWidth +const trimTrailingLine = (line: string, font: FontString, maxWidth: number) => { + const shouldTrimWhitespaces = getLineWidth(line, font, true) > maxWidth; + + if (!shouldTrimWhitespaces) { + return line; + } + + // defensively default to `trimeEnd` in case the regex does not match + let [, trimmedLine, whitespaces] = line.match(/^(.+?)(\s+)$/) ?? [ + line, + line.trimEnd(), + "", + ]; + + let trimmedLineWidth = getLineWidth(trimmedLine, font, true); + + for (const whitespace of Array.from(whitespaces)) { + const _charWidth = charWidth.calculate(whitespace, font); + const testLineWidth = trimmedLineWidth + _charWidth; + + if (testLineWidth > maxWidth) { + break; + } + + trimmedLine = trimmedLine + whitespace; + trimmedLineWidth = testLineWidth; + } + + return trimmedLine; +}; + export const wrapText = ( text: string, font: FontString,