mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			aakansha-n
			...
			arnost/png
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b3d95d9307 | ||
| 
						 | 
					671ed94d74 | ||
| 
						 | 
					d5ac76d4ea | ||
| 
						 | 
					2b19d53549 | ||
| 
						 | 
					b4abfad638 | ||
| 
						 | 
					a39640ead1 | ||
| 
						 | 
					84bd9bd4ff | 
@@ -53,6 +53,7 @@
 | 
			
		||||
    "sass": "1.51.0",
 | 
			
		||||
    "socket.io-client": "2.3.1",
 | 
			
		||||
    "tunnel-rat": "0.1.2",
 | 
			
		||||
    "upng-js": "2.1.0",
 | 
			
		||||
    "workbox-background-sync": "^6.5.4",
 | 
			
		||||
    "workbox-broadcast-update": "^6.5.4",
 | 
			
		||||
    "workbox-cacheable-response": "^6.5.4",
 | 
			
		||||
@@ -78,6 +79,7 @@
 | 
			
		||||
    "@types/react-dom": "18.0.6",
 | 
			
		||||
    "@types/resize-observer-browser": "0.1.7",
 | 
			
		||||
    "@types/socket.io-client": "1.4.36",
 | 
			
		||||
    "@types/upng-js": "2.1.2",
 | 
			
		||||
    "chai": "4.3.6",
 | 
			
		||||
    "dotenv": "16.0.1",
 | 
			
		||||
    "eslint-config-prettier": "8.5.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -825,6 +825,14 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
        if (typeof this.props.name !== "undefined") {
 | 
			
		||||
          name = this.props.name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        editingElement =
 | 
			
		||||
          editingElement || actionResult.appState?.editingElement || null;
 | 
			
		||||
 | 
			
		||||
        if (editingElement?.isDeleted) {
 | 
			
		||||
          editingElement = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setState(
 | 
			
		||||
          (state) => {
 | 
			
		||||
            // using Object.assign instead of spread to fool TS 4.2.2+ into
 | 
			
		||||
@@ -835,8 +843,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
              // or programmatically from the host, so it will need to be
 | 
			
		||||
              // rewritten later
 | 
			
		||||
              contextMenu: null,
 | 
			
		||||
              editingElement:
 | 
			
		||||
                editingElement || actionResult.appState?.editingElement || null,
 | 
			
		||||
              editingElement,
 | 
			
		||||
              viewModeEnabled,
 | 
			
		||||
              zenModeEnabled,
 | 
			
		||||
              gridSize,
 | 
			
		||||
@@ -1347,6 +1354,12 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // failsafe in case the state is being updated in incorrect order resulting
 | 
			
		||||
    // in the editingElement being now a deleted element
 | 
			
		||||
    if (this.state.editingElement?.isDeleted) {
 | 
			
		||||
      this.setState({ editingElement: null });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      this.state.selectedLinearElement &&
 | 
			
		||||
      !this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
 | 
			
		||||
@@ -4142,12 +4155,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (pointerDownState.resize.handleType) {
 | 
			
		||||
        setCursor(
 | 
			
		||||
          this.canvas,
 | 
			
		||||
          getCursorForResizingElement({
 | 
			
		||||
            transformHandleType: pointerDownState.resize.handleType,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
        pointerDownState.resize.isResizing = true;
 | 
			
		||||
        pointerDownState.resize.offset = tupleToCoors(
 | 
			
		||||
          getResizeOffsetXY(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { isTransparent, isWritableElement } from "../../utils";
 | 
			
		||||
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
 | 
			
		||||
import { ExcalidrawElement } from "../../element/types";
 | 
			
		||||
import { AppState } from "../../types";
 | 
			
		||||
import { TopPicks } from "./TopPicks";
 | 
			
		||||
@@ -121,11 +121,14 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        onCloseAutoFocus={(e) => {
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
          e.stopPropagation();
 | 
			
		||||
          // prevents focusing the trigger
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
 | 
			
		||||
          // return focus to excalidraw container
 | 
			
		||||
          if (container) {
 | 
			
		||||
          // return focus to excalidraw container unless
 | 
			
		||||
          // user focuses an interactive element, such as a button, or
 | 
			
		||||
          // enters the text editor by clicking on canvas with the text tool
 | 
			
		||||
          if (container && !isInteractive(document.activeElement)) {
 | 
			
		||||
            container.focus();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
 | 
			
		||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
 | 
			
		||||
import { restore, restoreLibraryItems } from "./restore";
 | 
			
		||||
import { ImportedLibraryData } from "./types";
 | 
			
		||||
import UPNG from "upng-js";
 | 
			
		||||
 | 
			
		||||
const parseFileContents = async (blob: Blob | File) => {
 | 
			
		||||
  let contents: string;
 | 
			
		||||
@@ -210,9 +211,7 @@ export const loadLibraryFromBlob = async (
 | 
			
		||||
  return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const canvasToBlob = async (
 | 
			
		||||
  canvas: HTMLCanvasElement,
 | 
			
		||||
): Promise<Blob> => {
 | 
			
		||||
const _canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    try {
 | 
			
		||||
      canvas.toBlob((blob) => {
 | 
			
		||||
@@ -232,6 +231,31 @@ export const canvasToBlob = async (
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const canvasToBlob = async (
 | 
			
		||||
  canvas: HTMLCanvasElement,
 | 
			
		||||
): Promise<Blob> => {
 | 
			
		||||
  const ctx = canvas.getContext("2d");
 | 
			
		||||
 | 
			
		||||
  if (!ctx) {
 | 
			
		||||
    throw new Error("No canvas context");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.time("getImageData");
 | 
			
		||||
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
 | 
			
		||||
  console.timeEnd("getImageData");
 | 
			
		||||
 | 
			
		||||
  console.time("encode");
 | 
			
		||||
  const pngData = UPNG.encode(
 | 
			
		||||
    [imageData.buffer],
 | 
			
		||||
    canvas.width,
 | 
			
		||||
    canvas.height,
 | 
			
		||||
    0,
 | 
			
		||||
  );
 | 
			
		||||
  console.timeEnd("encode");
 | 
			
		||||
 | 
			
		||||
  return new Blob([pngData], { type: "image/png" });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** generates SHA-1 digest from supplied file (if not supported, falls back
 | 
			
		||||
    to a 40-char base64 random id) */
 | 
			
		||||
export const generateIdFromFile = async (file: File): Promise<FileId> => {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,9 @@ export const exportCanvas = async (
 | 
			
		||||
  document.body.appendChild(tempCanvas);
 | 
			
		||||
 | 
			
		||||
  if (type === "png") {
 | 
			
		||||
    console.time("export png");
 | 
			
		||||
    let blob = await canvasToBlob(tempCanvas);
 | 
			
		||||
    console.timeEnd("export png");
 | 
			
		||||
    tempCanvas.remove();
 | 
			
		||||
    if (appState.exportEmbedScene) {
 | 
			
		||||
      blob = await (
 | 
			
		||||
 
 | 
			
		||||
@@ -890,26 +890,34 @@ const rotateMultipleElements = (
 | 
			
		||||
      centerY,
 | 
			
		||||
      centerAngle + origAngle - element.angle,
 | 
			
		||||
    );
 | 
			
		||||
    mutateElement(element, {
 | 
			
		||||
      x: element.x + (rotatedCX - cx),
 | 
			
		||||
      y: element.y + (rotatedCY - cy),
 | 
			
		||||
      angle: normalizeAngle(centerAngle + origAngle),
 | 
			
		||||
    });
 | 
			
		||||
    const boundTextElementId = getBoundTextElementId(element);
 | 
			
		||||
    if (boundTextElementId) {
 | 
			
		||||
      const textElement =
 | 
			
		||||
        Scene.getScene(element)?.getElement<ExcalidrawTextElementWithContainer>(
 | 
			
		||||
          boundTextElementId,
 | 
			
		||||
        );
 | 
			
		||||
      if (textElement && !isArrowElement(element)) {
 | 
			
		||||
        mutateElement(textElement, {
 | 
			
		||||
          x: textElement.x + (rotatedCX - cx),
 | 
			
		||||
          y: textElement.y + (rotatedCY - cy),
 | 
			
		||||
 | 
			
		||||
    mutateElement(
 | 
			
		||||
      element,
 | 
			
		||||
      {
 | 
			
		||||
        x: element.x + (rotatedCX - cx),
 | 
			
		||||
        y: element.y + (rotatedCY - cy),
 | 
			
		||||
        angle: normalizeAngle(centerAngle + origAngle),
 | 
			
		||||
      },
 | 
			
		||||
      false,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    updateBoundElements(element, { simultaneouslyUpdated: elements });
 | 
			
		||||
 | 
			
		||||
    const boundText = getBoundTextElement(element);
 | 
			
		||||
    if (boundText && !isArrowElement(element)) {
 | 
			
		||||
      mutateElement(
 | 
			
		||||
        boundText,
 | 
			
		||||
        {
 | 
			
		||||
          x: boundText.x + (rotatedCX - cx),
 | 
			
		||||
          y: boundText.y + (rotatedCY - cy),
 | 
			
		||||
          angle: normalizeAngle(centerAngle + origAngle),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
        },
 | 
			
		||||
        false,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Scene.getScene(elements[0])?.informMutation();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getResizeOffsetXY = (
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,18 @@ describe("Test wrapText", () => {
 | 
			
		||||
      {
 | 
			
		||||
        desc: "break all characters when width of each character is less than container width",
 | 
			
		||||
        width: 25,
 | 
			
		||||
        res: `H\ne\nl\nl\no \nw\nh\na\nt\ns \nu\np`,
 | 
			
		||||
        res: `H
 | 
			
		||||
e
 | 
			
		||||
l
 | 
			
		||||
l
 | 
			
		||||
o
 | 
			
		||||
w
 | 
			
		||||
h
 | 
			
		||||
a
 | 
			
		||||
t
 | 
			
		||||
s
 | 
			
		||||
u
 | 
			
		||||
p`,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        desc: "break words as per the width",
 | 
			
		||||
@@ -79,7 +90,8 @@ up`,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("When text contain new lines", () => {
 | 
			
		||||
    const text = "Hello\nwhats up";
 | 
			
		||||
    const text = `Hello
 | 
			
		||||
whats up`;
 | 
			
		||||
    [
 | 
			
		||||
      {
 | 
			
		||||
        desc: "break all words when width of each word is less than container width",
 | 
			
		||||
@@ -89,7 +101,18 @@ up`,
 | 
			
		||||
      {
 | 
			
		||||
        desc: "break all characters when width of each character is less than container width",
 | 
			
		||||
        width: 25,
 | 
			
		||||
        res: `H\ne\nl\nl\no\nw\nh\na\nt\ns \nu\np`,
 | 
			
		||||
        res: `H
 | 
			
		||||
e
 | 
			
		||||
l
 | 
			
		||||
l
 | 
			
		||||
o
 | 
			
		||||
w
 | 
			
		||||
h
 | 
			
		||||
a
 | 
			
		||||
t
 | 
			
		||||
s
 | 
			
		||||
u
 | 
			
		||||
p`,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        desc: "break words as per the width",
 | 
			
		||||
@@ -126,7 +149,13 @@ whats up`,
 | 
			
		||||
        desc: "fit characters of long string as per container width and break words as per the width",
 | 
			
		||||
 | 
			
		||||
        width: 130,
 | 
			
		||||
        res: `hellolongte\nxtthisiswha\ntsupwithyou\nIamtypinggg\nggandtyping\ngg break it \nnow`,
 | 
			
		||||
        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",
 | 
			
		||||
@@ -161,7 +190,7 @@ whats up`,
 | 
			
		||||
      "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
 | 
			
		||||
    const res = wrapText(text, font, 110);
 | 
			
		||||
    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";
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,6 @@ export const redrawTextBoundingBox = (
 | 
			
		||||
    boundTextUpdates.text,
 | 
			
		||||
    getFontString(textElement),
 | 
			
		||||
    textElement.lineHeight,
 | 
			
		||||
    maxWidth,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  boundTextUpdates.width = metrics.width;
 | 
			
		||||
@@ -196,7 +195,6 @@ export const handleBindTextResize = (
 | 
			
		||||
        text,
 | 
			
		||||
        getFontString(textElement),
 | 
			
		||||
        textElement.lineHeight,
 | 
			
		||||
        maxWidth,
 | 
			
		||||
      );
 | 
			
		||||
      nextHeight = metrics.height;
 | 
			
		||||
      nextWidth = metrics.width;
 | 
			
		||||
@@ -285,7 +283,6 @@ export const measureText = (
 | 
			
		||||
  text: string,
 | 
			
		||||
  font: FontString,
 | 
			
		||||
  lineHeight: ExcalidrawTextElement["lineHeight"],
 | 
			
		||||
  maxWidth?: number | null,
 | 
			
		||||
) => {
 | 
			
		||||
  text = text
 | 
			
		||||
    .split("\n")
 | 
			
		||||
@@ -295,14 +292,7 @@ export const measureText = (
 | 
			
		||||
    .join("\n");
 | 
			
		||||
  const fontSize = parseFloat(font);
 | 
			
		||||
  const height = getTextHeight(text, fontSize, lineHeight);
 | 
			
		||||
  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 width = getTextWidth(text, font);
 | 
			
		||||
  const baseline = measureBaseline(text, font, lineHeight);
 | 
			
		||||
  return { width, height, baseline };
 | 
			
		||||
};
 | 
			
		||||
@@ -390,7 +380,7 @@ export const getApproxMinLineHeight = (
 | 
			
		||||
 | 
			
		||||
let canvas: HTMLCanvasElement | undefined;
 | 
			
		||||
 | 
			
		||||
export const getLineWidth = (text: string, font: FontString) => {
 | 
			
		||||
const getLineWidth = (text: string, font: FontString) => {
 | 
			
		||||
  if (!canvas) {
 | 
			
		||||
    canvas = document.createElement("canvas");
 | 
			
		||||
  }
 | 
			
		||||
@@ -450,8 +440,10 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
 | 
			
		||||
  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;
 | 
			
		||||
@@ -467,7 +459,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
 | 
			
		||||
    currentLineWidthTillNow = 0;
 | 
			
		||||
  };
 | 
			
		||||
  originalLines.forEach((originalLine) => {
 | 
			
		||||
    const currentLineWidth = getLineWidth(originalLine, font);
 | 
			
		||||
    const currentLineWidth = getTextWidth(originalLine, font);
 | 
			
		||||
 | 
			
		||||
    // Push the line if its <= maxWidth
 | 
			
		||||
    if (currentLineWidth <= maxWidth) {
 | 
			
		||||
@@ -515,25 +507,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // push current line if appending space exceeds max width
 | 
			
		||||
        if (currentLineWidthTillNow >= maxWidth) {
 | 
			
		||||
        if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
 | 
			
		||||
          push(currentLine);
 | 
			
		||||
          resetParams();
 | 
			
		||||
          // space needs to be appended before next word
 | 
			
		||||
          // as currentLine contains chars which couldn't be appended
 | 
			
		||||
          // to previous line unless the line ends with hyphen to sync
 | 
			
		||||
          // with css word-wrap
 | 
			
		||||
        } else if (!currentLine.endsWith("-") && index < words.length) {
 | 
			
		||||
        } else if (!currentLine.endsWith("-")) {
 | 
			
		||||
          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}`.trimEnd(),
 | 
			
		||||
            font,
 | 
			
		||||
          );
 | 
			
		||||
          currentLineWidthTillNow = getLineWidth(currentLine + word, font);
 | 
			
		||||
 | 
			
		||||
          if (currentLineWidthTillNow > maxWidth) {
 | 
			
		||||
            push(currentLine);
 | 
			
		||||
@@ -541,20 +531,24 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          index++;
 | 
			
		||||
 | 
			
		||||
          // if word ends with "-" then we don't need to add space
 | 
			
		||||
          // to sync with css word-wrap
 | 
			
		||||
          const shouldAppendSpace = !word.endsWith("-");
 | 
			
		||||
          currentLine += word;
 | 
			
		||||
 | 
			
		||||
          if (shouldAppendSpace && index < words.length) {
 | 
			
		||||
          if (shouldAppendSpace) {
 | 
			
		||||
            currentLine += " ";
 | 
			
		||||
          }
 | 
			
		||||
          index++;
 | 
			
		||||
 | 
			
		||||
          // Push the word if appending space exceeds max width
 | 
			
		||||
          if (currentLineWidthTillNow >= maxWidth) {
 | 
			
		||||
            lines.push(currentLine);
 | 
			
		||||
          if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
 | 
			
		||||
            if (shouldAppendSpace) {
 | 
			
		||||
              lines.push(currentLine.slice(0, -1));
 | 
			
		||||
            } else {
 | 
			
		||||
              lines.push(currentLine);
 | 
			
		||||
            }
 | 
			
		||||
            resetParams();
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
@@ -977,22 +971,3 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
 | 
			
		||||
  }
 | 
			
		||||
  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;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,17 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 | 
			
		||||
const tab = "    ";
 | 
			
		||||
const mouse = new Pointer("mouse");
 | 
			
		||||
 | 
			
		||||
const getTextEditor = () => {
 | 
			
		||||
  return document.querySelector(
 | 
			
		||||
    ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
  ) as HTMLTextAreaElement;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
 | 
			
		||||
  fireEvent.change(editor, { target: { value } });
 | 
			
		||||
  editor.dispatchEvent(new Event("input"));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("textWysiwyg", () => {
 | 
			
		||||
  describe("start text editing", () => {
 | 
			
		||||
    const { h } = window;
 | 
			
		||||
@@ -190,9 +201,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      mouse.clickAt(text.x + 50, text.y + 50);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      expect(editor).not.toBe(null);
 | 
			
		||||
      expect(h.state.editingElement?.id).toBe(text.id);
 | 
			
		||||
@@ -214,9 +223,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      mouse.doubleClickAt(text.x + 50, text.y + 50);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      expect(editor).not.toBe(null);
 | 
			
		||||
      expect(h.state.editingElement?.id).toBe(text.id);
 | 
			
		||||
@@ -243,9 +250,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      textElement = UI.createElement("text");
 | 
			
		||||
 | 
			
		||||
      mouse.clickOn(textElement);
 | 
			
		||||
      textarea = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      )!;
 | 
			
		||||
      textarea = getTextEditor();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterAll(() => {
 | 
			
		||||
@@ -455,17 +460,11 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      UI.clickTool("text");
 | 
			
		||||
      mouse.clickAt(750, 300);
 | 
			
		||||
 | 
			
		||||
      textarea = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      )!;
 | 
			
		||||
      fireEvent.change(textarea, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value:
 | 
			
		||||
            "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      textarea.dispatchEvent(new Event("input"));
 | 
			
		||||
      textarea = getTextEditor();
 | 
			
		||||
      updateTextEditor(
 | 
			
		||||
        textarea,
 | 
			
		||||
        "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
 | 
			
		||||
      );
 | 
			
		||||
      await new Promise((cb) => setTimeout(cb, 0));
 | 
			
		||||
      textarea.blur();
 | 
			
		||||
      expect(textarea.style.width).toBe("792px");
 | 
			
		||||
@@ -513,11 +512,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
        { id: text.id, type: "text" },
 | 
			
		||||
      ]);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -543,11 +540,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(text.angle).toBe(rectangle.angle);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -572,9 +567,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      API.setSelectedElements([diamond]);
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      const value = new Array(1000).fill("1").join("\n");
 | 
			
		||||
@@ -587,9 +580,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(diamond.height).toBe(50020);
 | 
			
		||||
 | 
			
		||||
      // Clearing text to simulate height decrease
 | 
			
		||||
      expect(() =>
 | 
			
		||||
        fireEvent.input(editor, { target: { value: "" } }),
 | 
			
		||||
      ).not.toThrow();
 | 
			
		||||
      expect(() => updateTextEditor(editor, "")).not.toThrow();
 | 
			
		||||
 | 
			
		||||
      expect(diamond.height).toBe(70);
 | 
			
		||||
    });
 | 
			
		||||
@@ -611,9 +602,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(text.type).toBe("text");
 | 
			
		||||
      expect(text.containerId).toBe(null);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      let editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      let editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
 | 
			
		||||
@@ -628,11 +617,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(text.containerId).toBe(rectangle.id);
 | 
			
		||||
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
 | 
			
		||||
@@ -652,13 +639,11 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
			
		||||
      expect(text.type).toBe("text");
 | 
			
		||||
      expect(text.containerId).toBe(rectangle.id);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(rectangle.boundElements).toStrictEqual([
 | 
			
		||||
        { id: text.id, type: "text" },
 | 
			
		||||
@@ -689,11 +674,8 @@ describe("textWysiwyg", () => {
 | 
			
		||||
        { id: text.id, type: "text" },
 | 
			
		||||
      ]);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -717,17 +699,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
        freedraw.y + freedraw.height / 2,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value: "Hello World!",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
 | 
			
		||||
      editor.dispatchEvent(new Event("input"));
 | 
			
		||||
 | 
			
		||||
      expect(freedraw.boundElements).toBe(null);
 | 
			
		||||
      expect(h.elements[1].type).toBe("text");
 | 
			
		||||
@@ -759,11 +733,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(text.type).toBe("text");
 | 
			
		||||
      expect(text.containerId).toBe(null);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -776,17 +748,12 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      UI.clickTool("text");
 | 
			
		||||
      mouse.clickAt(20, 30);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value: "Excalidraw is an opensource virtual collaborative whiteboard",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      editor.dispatchEvent(new Event("input"));
 | 
			
		||||
      updateTextEditor(
 | 
			
		||||
        editor,
 | 
			
		||||
        "Excalidraw is an opensource virtual collaborative whiteboard",
 | 
			
		||||
      );
 | 
			
		||||
      await new Promise((cb) => setTimeout(cb, 0));
 | 
			
		||||
      expect(h.elements.length).toBe(2);
 | 
			
		||||
      expect(h.elements[1].type).toBe("text");
 | 
			
		||||
@@ -826,12 +793,10 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      mouse.down();
 | 
			
		||||
 | 
			
		||||
      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
			
		||||
      let editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      let editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
 | 
			
		||||
      UI.clickTool("text");
 | 
			
		||||
@@ -841,9 +806,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
        rectangle.y + rectangle.height / 2,
 | 
			
		||||
      );
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      editor.select();
 | 
			
		||||
      fireEvent.click(screen.getByTitle(/code/i));
 | 
			
		||||
@@ -876,17 +839,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      Keyboard.keyDown(KEYS.ENTER);
 | 
			
		||||
      let text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
			
		||||
      let editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      let editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value: "Hello World!",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      editor.dispatchEvent(new Event("input"));
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((cb) => setTimeout(cb, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -905,17 +860,8 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      mouse.select(rectangle);
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value: "Hello",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      editor.dispatchEvent(new Event("input"));
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
 | 
			
		||||
@@ -943,13 +889,11 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
			
		||||
      expect(text.containerId).toBe(rectangle.id);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(rectangle.boundElements).toStrictEqual([
 | 
			
		||||
        { id: text.id, type: "text" },
 | 
			
		||||
@@ -982,11 +926,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      // Bind first text
 | 
			
		||||
      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
			
		||||
      expect(text.containerId).toBe(rectangle.id);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(rectangle.boundElements).toStrictEqual([
 | 
			
		||||
        { id: text.id, type: "text" },
 | 
			
		||||
@@ -1005,11 +947,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
    it("should respect text alignment when resizing", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      let editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      let editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
 | 
			
		||||
      // should center align horizontally and vertically by default
 | 
			
		||||
@@ -1024,9 +964,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      mouse.select(rectangle);
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      editor.select();
 | 
			
		||||
 | 
			
		||||
@@ -1049,9 +987,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      mouse.select(rectangle);
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      editor.select();
 | 
			
		||||
 | 
			
		||||
@@ -1089,11 +1025,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(text.type).toBe("text");
 | 
			
		||||
      expect(text.containerId).toBe(rectangle.id);
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -1106,11 +1040,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
    it("should scale font size correctly when resizing using shift", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      const textElement = h.elements[1] as ExcalidrawTextElement;
 | 
			
		||||
      expect(rectangle.width).toBe(90);
 | 
			
		||||
@@ -1128,11 +1060,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
    it("should bind text correctly when container duplicated with alt-drag", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(h.elements.length).toBe(2);
 | 
			
		||||
 | 
			
		||||
@@ -1162,11 +1092,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
    it("undo should work", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(rectangle.boundElements).toStrictEqual([
 | 
			
		||||
        { id: h.elements[1].id, type: "text" },
 | 
			
		||||
@@ -1201,12 +1129,10 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
    it("should not allow bound text with only whitespaces", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "   " } });
 | 
			
		||||
      updateTextEditor(editor, "   ");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(rectangle.boundElements).toStrictEqual([]);
 | 
			
		||||
      expect(h.elements[1].isDeleted).toBe(true);
 | 
			
		||||
@@ -1225,9 +1151,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
        type: "text",
 | 
			
		||||
        text: "Online whiteboard collaboration made easy",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      h.elements = [container, text];
 | 
			
		||||
      API.setSelectedElements([container, text]);
 | 
			
		||||
 | 
			
		||||
      fireEvent.contextMenu(GlobalTestState.canvas, {
 | 
			
		||||
        button: 2,
 | 
			
		||||
        clientX: 20,
 | 
			
		||||
@@ -1258,11 +1184,9 @@ describe("textWysiwyg", () => {
 | 
			
		||||
    it("should reset the container height cache when resizing", async () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 | 
			
		||||
      let editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      let editor = getTextEditor();
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
      updateTextEditor(editor, "Hello");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
 | 
			
		||||
      resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
 | 
			
		||||
@@ -1272,9 +1196,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      mouse.select(rectangle);
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
 | 
			
		||||
      editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      editor.blur();
 | 
			
		||||
@@ -1287,12 +1209,8 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
 | 
			
		||||
      mouse.select(rectangle);
 | 
			
		||||
@@ -1316,12 +1234,8 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
      expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 | 
			
		||||
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      fireEvent.change(editor, { target: { value: "Hello World!" } });
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
 | 
			
		||||
@@ -1352,17 +1266,12 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
        editor = document.querySelector(
 | 
			
		||||
          ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
        ) as HTMLTextAreaElement;
 | 
			
		||||
        await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
        fireEvent.change(editor, { target: { value: "Hello" } });
 | 
			
		||||
        editor = getTextEditor();
 | 
			
		||||
        updateTextEditor(editor, "Hello");
 | 
			
		||||
        editor.blur();
 | 
			
		||||
        mouse.select(rectangle);
 | 
			
		||||
        Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
        editor = document.querySelector(
 | 
			
		||||
          ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
        ) as HTMLTextAreaElement;
 | 
			
		||||
        editor = getTextEditor();
 | 
			
		||||
        editor.select();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@@ -1473,17 +1382,12 @@ describe("textWysiwyg", () => {
 | 
			
		||||
    it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
 | 
			
		||||
      UI.clickTool("text");
 | 
			
		||||
      mouse.clickAt(20, 30);
 | 
			
		||||
      const editor = document.querySelector(
 | 
			
		||||
        ".excalidraw-textEditorContainer > textarea",
 | 
			
		||||
      ) as HTMLTextAreaElement;
 | 
			
		||||
      const editor = getTextEditor();
 | 
			
		||||
 | 
			
		||||
      fireEvent.change(editor, {
 | 
			
		||||
        target: {
 | 
			
		||||
          value: "Excalidraw is an opensource virtual collaborative whiteboard",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      editor.dispatchEvent(new Event("input"));
 | 
			
		||||
      updateTextEditor(
 | 
			
		||||
        editor,
 | 
			
		||||
        "Excalidraw is an opensource virtual collaborative whiteboard",
 | 
			
		||||
      );
 | 
			
		||||
      await new Promise((cb) => setTimeout(cb, 0));
 | 
			
		||||
 | 
			
		||||
      editor.select();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import {
 | 
			
		||||
  isBoundToContainer,
 | 
			
		||||
  isTextElement,
 | 
			
		||||
} from "./typeChecks";
 | 
			
		||||
import { CLASSES, TEXT_ALIGN, isSafari } from "../constants";
 | 
			
		||||
import { CLASSES, isSafari } from "../constants";
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
@@ -26,7 +26,7 @@ import {
 | 
			
		||||
  getContainerDims,
 | 
			
		||||
  getContainerElement,
 | 
			
		||||
  getTextElementAngle,
 | 
			
		||||
  measureText,
 | 
			
		||||
  getTextWidth,
 | 
			
		||||
  normalizeText,
 | 
			
		||||
  redrawTextBoundingBox,
 | 
			
		||||
  wrapText,
 | 
			
		||||
@@ -196,8 +196,6 @@ export const textWysiwyg = ({
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        maxWidth = getBoundTextMaxWidth(container);
 | 
			
		||||
        textElementWidth = Math.min(textElementWidth, maxWidth);
 | 
			
		||||
 | 
			
		||||
        maxHeight = getBoundTextMaxHeight(
 | 
			
		||||
          container,
 | 
			
		||||
          updatedTextElement as ExcalidrawTextElementWithContainer,
 | 
			
		||||
@@ -232,16 +230,7 @@ export const textWysiwyg = ({
 | 
			
		||||
          coordY = y;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      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 [viewportX, viewportY] = getViewportCoords(coordX, coordY);
 | 
			
		||||
      const initialSelectionStart = editable.selectionStart;
 | 
			
		||||
      const initialSelectionEnd = editable.selectionEnd;
 | 
			
		||||
      const initialLength = editable.value.length;
 | 
			
		||||
@@ -373,12 +362,7 @@ export const textWysiwyg = ({
 | 
			
		||||
          font,
 | 
			
		||||
          getBoundTextMaxWidth(container),
 | 
			
		||||
        );
 | 
			
		||||
        const { width } = measureText(
 | 
			
		||||
          wrappedText,
 | 
			
		||||
          font,
 | 
			
		||||
          element.lineHeight,
 | 
			
		||||
          getBoundTextMaxWidth(container),
 | 
			
		||||
        );
 | 
			
		||||
        const width = getTextWidth(wrappedText, font);
 | 
			
		||||
        editable.style.width = `${width}px`;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,6 @@ import {
 | 
			
		||||
  getLineHeightInPx,
 | 
			
		||||
  getBoundTextMaxHeight,
 | 
			
		||||
  getBoundTextMaxWidth,
 | 
			
		||||
  getSpacesOffsetForLine,
 | 
			
		||||
} from "../element/textElement";
 | 
			
		||||
import { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
 | 
			
		||||
@@ -320,13 +319,13 @@ const drawElementOnCanvas = (
 | 
			
		||||
        }
 | 
			
		||||
        context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
 | 
			
		||||
        context.save();
 | 
			
		||||
        const font = getFontString(element);
 | 
			
		||||
        context.font = font;
 | 
			
		||||
        context.font = getFontString(element);
 | 
			
		||||
        context.fillStyle = element.strokeColor;
 | 
			
		||||
        context.textAlign = element.textAlign as CanvasTextAlign;
 | 
			
		||||
 | 
			
		||||
        // Canvas does not support multiline text by default
 | 
			
		||||
        const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
 | 
			
		||||
 | 
			
		||||
        const horizontalOffset =
 | 
			
		||||
          element.textAlign === "center"
 | 
			
		||||
            ? element.width / 2
 | 
			
		||||
@@ -337,17 +336,11 @@ const drawElementOnCanvas = (
 | 
			
		||||
          element.fontSize,
 | 
			
		||||
          element.lineHeight,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const verticalOffset = element.height - element.baseline;
 | 
			
		||||
        for (let index = 0; index < lines.length; index++) {
 | 
			
		||||
          const spacesOffset = getSpacesOffsetForLine(
 | 
			
		||||
            element,
 | 
			
		||||
            lines[index],
 | 
			
		||||
            font,
 | 
			
		||||
          );
 | 
			
		||||
          context.fillText(
 | 
			
		||||
            lines[index].trimEnd(),
 | 
			
		||||
            horizontalOffset + spacesOffset,
 | 
			
		||||
            lines[index],
 | 
			
		||||
            horizontalOffset,
 | 
			
		||||
            (index + 1) * lineHeightPx - verticalOffset,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1151,7 +1151,7 @@ describe("Test Linear Elements", () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
 | 
			
		||||
      ).toMatchInlineSnapshot(`
 | 
			
		||||
        "Online whiteboard collaboration 
 | 
			
		||||
        "Online whiteboard collaboration
 | 
			
		||||
        made easy"
 | 
			
		||||
      `);
 | 
			
		||||
      const handleBindTextResizeSpy = jest.spyOn(
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,13 @@ export const isInputLike = (
 | 
			
		||||
  target instanceof HTMLTextAreaElement ||
 | 
			
		||||
  target instanceof HTMLSelectElement;
 | 
			
		||||
 | 
			
		||||
export const isInteractive = (target: Element | EventTarget | null) => {
 | 
			
		||||
  return (
 | 
			
		||||
    isInputLike(target) ||
 | 
			
		||||
    (target instanceof Element && !!target.closest("label, button"))
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isWritableElement = (
 | 
			
		||||
  target: Element | EventTarget | null,
 | 
			
		||||
): target is
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -2851,6 +2851,11 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
 | 
			
		||||
  integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
 | 
			
		||||
 | 
			
		||||
"@types/upng-js@2.1.2":
 | 
			
		||||
  version "2.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/upng-js/-/upng-js-2.1.2.tgz#4680f700c3003eaf958b92418c6e19521bb1e034"
 | 
			
		||||
  integrity sha512-sxCnLDEmGFDcnrdSMml3/mDSbfSAvt86Ld32J6Ac75Og7GzzmwHbUnJNTue74tfQC/3PPD/W0jG2dQuF9v6UOg==
 | 
			
		||||
 | 
			
		||||
"@types/ws@^8.5.1":
 | 
			
		||||
  version "8.5.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
 | 
			
		||||
@@ -8034,7 +8039,7 @@ p-try@^2.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
 | 
			
		||||
  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 | 
			
		||||
 | 
			
		||||
pako@1.0.11:
 | 
			
		||||
pako@1.0.11, pako@^1.0.5:
 | 
			
		||||
  version "1.0.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
 | 
			
		||||
  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
 | 
			
		||||
@@ -10557,6 +10562,13 @@ update-browserslist-db@^1.0.10:
 | 
			
		||||
    escalade "^3.1.1"
 | 
			
		||||
    picocolors "^1.0.0"
 | 
			
		||||
 | 
			
		||||
upng-js@2.1.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/upng-js/-/upng-js-2.1.0.tgz#7176e73973db361ca95d0fa14f958385db6b9dd2"
 | 
			
		||||
  integrity sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    pako "^1.0.5"
 | 
			
		||||
 | 
			
		||||
uri-js@^4.2.2:
 | 
			
		||||
  version "4.4.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user