Browse Source

Merge remote-tracking branch 'origin/master' into aakansha-refact

Aakansha Doshi 2 years ago
parent
commit
b799490ece

+ 3 - 3
dev-docs/yarn.lock

@@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
 
 webpack@^5.73.0:
 webpack@^5.73.0:
-  version "5.74.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
-  integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
+  version "5.76.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
+  integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
   dependencies:
   dependencies:
     "@types/eslint-scope" "^3.7.3"
     "@types/eslint-scope" "^3.7.3"
     "@types/estree" "^0.0.51"
     "@types/estree" "^0.0.51"

+ 16 - 7
src/actions/actionBoundText.tsx

@@ -45,6 +45,7 @@ export const actionUnbindText = register({
         const { width, height } = measureText(
         const { width, height } = measureText(
           boundTextElement.originalText,
           boundTextElement.originalText,
           getFontString(boundTextElement),
           getFontString(boundTextElement),
+          boundTextElement.lineHeight,
         );
         );
         const originalContainerHeight = getOriginalContainerHeightFromCache(
         const originalContainerHeight = getOriginalContainerHeightFromCache(
           element.id,
           element.id,
@@ -239,15 +240,23 @@ export const actionCreateContainerFromText = register({
           linearElementIds.includes(ele.id),
           linearElementIds.includes(ele.id),
         ) as ExcalidrawLinearElement[];
         ) as ExcalidrawLinearElement[];
         linearElements.forEach((ele) => {
         linearElements.forEach((ele) => {
-          let startBinding = null;
-          let endBinding = null;
-          if (ele.startBinding) {
-            startBinding = { ...ele.startBinding, elementId: container.id };
+          let startBinding = ele.startBinding;
+          let endBinding = ele.endBinding;
+
+          if (startBinding?.elementId === textElement.id) {
+            startBinding = {
+              ...startBinding,
+              elementId: container.id,
+            };
+          }
+
+          if (endBinding?.elementId === textElement.id) {
+            endBinding = { ...endBinding, elementId: container.id };
           }
           }
-          if (ele.endBinding) {
-            endBinding = { ...ele.endBinding, elementId: container.id };
+
+          if (startBinding || endBinding) {
+            mutateElement(ele, { startBinding, endBinding });
           }
           }
-          mutateElement(ele, { startBinding, endBinding });
         });
         });
       }
       }
 
 

+ 2 - 0
src/actions/actionProperties.tsx

@@ -55,6 +55,7 @@ import {
   getBoundTextElement,
   getBoundTextElement,
   getContainerElement,
   getContainerElement,
 } from "../element/textElement";
 } from "../element/textElement";
+import { getDefaultLineHeight } from "../element/textMeasurements";
 import {
 import {
   isBoundToContainer,
   isBoundToContainer,
   isLinearElement,
   isLinearElement,
@@ -637,6 +638,7 @@ export const actionChangeFontFamily = register({
               oldElement,
               oldElement,
               {
               {
                 fontFamily: value,
                 fontFamily: value,
+                lineHeight: getDefaultLineHeight(value),
               },
               },
             );
             );
             redrawTextBoundingBox(newElement, getContainerElement(oldElement));
             redrawTextBoundingBox(newElement, getContainerElement(oldElement));

+ 10 - 3
src/actions/actionStyles.ts

@@ -19,6 +19,7 @@ import {
   getDefaultRoundnessTypeForElement,
   getDefaultRoundnessTypeForElement,
 } from "../element/typeChecks";
 } from "../element/typeChecks";
 import { getSelectedElements } from "../scene";
 import { getSelectedElements } from "../scene";
+import { getDefaultLineHeight } from "../element/textMeasurements";
 
 
 // `copiedStyles` is exported only for tests.
 // `copiedStyles` is exported only for tests.
 export let copiedStyles: string = "{}";
 export let copiedStyles: string = "{}";
@@ -92,12 +93,18 @@ export const actionPasteStyles = register({
           });
           });
 
 
           if (isTextElement(newElement)) {
           if (isTextElement(newElement)) {
+            const fontSize =
+              elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
+            const fontFamily =
+              elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
             newElement = newElementWith(newElement, {
             newElement = newElementWith(newElement, {
-              fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
-              fontFamily:
-                elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
+              fontSize,
+              fontFamily,
               textAlign:
               textAlign:
                 elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
                 elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
+              lineHeight:
+                elementStylesToCopyFrom.lineHeight ||
+                getDefaultLineHeight(fontFamily),
             });
             });
             let container = null;
             let container = null;
             if (newElement.containerId) {
             if (newElement.containerId) {

+ 24 - 15
src/components/App.tsx

@@ -268,10 +268,11 @@ import {
   isValidTextContainer,
   isValidTextContainer,
 } from "../element/textElement";
 } from "../element/textElement";
 import {
 import {
-  getLineHeight,
   getApproxMinLineHeight,
   getApproxMinLineHeight,
   getApproxMinLineWidth,
   getApproxMinLineWidth,
   isMeasureTextSupported,
   isMeasureTextSupported,
+  getLineHeightInPx,
+  getDefaultLineHeight,
 } from "../element/textMeasurements";
 } from "../element/textMeasurements";
 import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
 import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
 import {
 import {
@@ -1733,12 +1734,14 @@ class App extends React.Component<AppProps, AppState> {
       (acc: ExcalidrawTextElement[], line, idx) => {
       (acc: ExcalidrawTextElement[], line, idx) => {
         const text = line.trim();
         const text = line.trim();
 
 
+        const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
         if (text.length) {
         if (text.length) {
           const element = newTextElement({
           const element = newTextElement({
             ...textElementProps,
             ...textElementProps,
             x,
             x,
             y: currentY,
             y: currentY,
             text,
             text,
+            lineHeight,
           });
           });
           acc.push(element);
           acc.push(element);
           currentY += element.height + LINE_GAP;
           currentY += element.height + LINE_GAP;
@@ -1747,14 +1750,9 @@ class App extends React.Component<AppProps, AppState> {
           // add paragraph only if previous line was not empty, IOW don't add
           // add paragraph only if previous line was not empty, IOW don't add
           // more than one empty line
           // more than one empty line
           if (prevLine) {
           if (prevLine) {
-            const defaultLineHeight = getLineHeight(
-              getFontString({
-                fontSize: textElementProps.fontSize,
-                fontFamily: textElementProps.fontFamily,
-              }),
-            );
-
-            currentY += defaultLineHeight + LINE_GAP;
+            currentY +=
+              getLineHeightInPx(textElementProps.fontSize, lineHeight) +
+              LINE_GAP;
           }
           }
         }
         }
 
 
@@ -2609,6 +2607,13 @@ class App extends React.Component<AppProps, AppState> {
       existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
       existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
     }
     }
 
 
+    const fontFamily =
+      existingTextElement?.fontFamily || this.state.currentItemFontFamily;
+
+    const lineHeight =
+      existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
+    const fontSize = this.state.currentItemFontSize;
+
     if (
     if (
       !existingTextElement &&
       !existingTextElement &&
       shouldBindToContainer &&
       shouldBindToContainer &&
@@ -2616,11 +2621,14 @@ class App extends React.Component<AppProps, AppState> {
       !isArrowElement(container)
       !isArrowElement(container)
     ) {
     ) {
       const fontString = {
       const fontString = {
-        fontSize: this.state.currentItemFontSize,
-        fontFamily: this.state.currentItemFontFamily,
+        fontSize,
+        fontFamily,
       };
       };
-      const minWidth = getApproxMinLineWidth(getFontString(fontString));
-      const minHeight = getApproxMinLineHeight(getFontString(fontString));
+      const minWidth = getApproxMinLineWidth(
+        getFontString(fontString),
+        lineHeight,
+      );
+      const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
       const containerDims = getContainerDims(container);
       const containerDims = getContainerDims(container);
       const newHeight = Math.max(containerDims.height, minHeight);
       const newHeight = Math.max(containerDims.height, minHeight);
       const newWidth = Math.max(containerDims.width, minWidth);
       const newWidth = Math.max(containerDims.width, minWidth);
@@ -2654,8 +2662,8 @@ class App extends React.Component<AppProps, AppState> {
           opacity: this.state.currentItemOpacity,
           opacity: this.state.currentItemOpacity,
           roundness: null,
           roundness: null,
           text: "",
           text: "",
-          fontSize: this.state.currentItemFontSize,
-          fontFamily: this.state.currentItemFontFamily,
+          fontSize,
+          fontFamily,
           textAlign: parentCenterPosition
           textAlign: parentCenterPosition
             ? "center"
             ? "center"
             : this.state.currentItemTextAlign,
             : this.state.currentItemTextAlign,
@@ -2665,6 +2673,7 @@ class App extends React.Component<AppProps, AppState> {
           containerId: shouldBindToContainer ? container?.id : undefined,
           containerId: shouldBindToContainer ? container?.id : undefined,
           groupIds: container?.groupIds ?? [],
           groupIds: container?.groupIds ?? [],
           locked: false,
           locked: false,
+          lineHeight,
         });
         });
 
 
     if (!existingTextElement && shouldBindToContainer && container) {
     if (!existingTextElement && shouldBindToContainer && container) {

+ 3 - 1
src/data/index.ts

@@ -89,7 +89,9 @@ export const exportCanvas = async (
     return await fileSave(blob, {
     return await fileSave(blob, {
       description: "Export to PNG",
       description: "Export to PNG",
       name,
       name,
-      extension: appState.exportEmbedScene ? "excalidraw.png" : "png",
+      // FIXME reintroduce `excalidraw.png` when most people upgrade away
+      // from 111.0.5563.64 (arm64), see #6349
+      extension: /* appState.exportEmbedScene ? "excalidraw.png" : */ "png",
       fileHandle,
       fileHandle,
     });
     });
   } else if (type === "clipboard") {
   } else if (type === "clipboard") {

+ 22 - 3
src/data/restore.ts

@@ -35,6 +35,10 @@ import { getUpdatedTimestamp, updateActiveTool } from "../utils";
 import { arrayToMap } from "../utils";
 import { arrayToMap } from "../utils";
 import oc from "open-color";
 import oc from "open-color";
 import { MarkOptional, Mutable } from "../utility-types";
 import { MarkOptional, Mutable } from "../utility-types";
+import {
+  detectLineHeight,
+  getDefaultLineHeight,
+} from "../element/textMeasurements";
 
 
 type RestoredAppState = Omit<
 type RestoredAppState = Omit<
   AppState,
   AppState,
@@ -165,17 +169,32 @@ const restoreElement = (
         const [fontPx, _fontFamily]: [string, string] = (
         const [fontPx, _fontFamily]: [string, string] = (
           element as any
           element as any
         ).font.split(" ");
         ).font.split(" ");
-        fontSize = parseInt(fontPx, 10);
+        fontSize = parseFloat(fontPx);
         fontFamily = getFontFamilyByName(_fontFamily);
         fontFamily = getFontFamilyByName(_fontFamily);
       }
       }
+      const text = element.text ?? "";
+
       element = restoreElementWithProperties(element, {
       element = restoreElementWithProperties(element, {
         fontSize,
         fontSize,
         fontFamily,
         fontFamily,
-        text: element.text ?? "",
+        text,
         textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
         textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
         verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
         verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
         containerId: element.containerId ?? null,
         containerId: element.containerId ?? null,
-        originalText: element.originalText || element.text,
+        originalText: element.originalText || text,
+        // line-height might not be specified either when creating elements
+        // programmatically, or when importing old diagrams.
+        // For the latter we want to detect the original line height which
+        // will likely differ from our per-font fixed line height we now use,
+        // to maintain backward compatibility.
+        lineHeight:
+          element.lineHeight ||
+          (element.height
+            ? // detect line-height from current element height and font-size
+              detectLineHeight(element)
+            : // no element height likely means programmatic use, so default
+              // to a fixed line height
+              getDefaultLineHeight(element.fontFamily)),
       });
       });
 
 
       if (refreshDimensions) {
       if (refreshDimensions) {

+ 6 - 1
src/element/collision.ts

@@ -786,7 +786,12 @@ export const findFocusPointForEllipse = (
       orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
       orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
     squares;
     squares;
 
 
-  const n = (-m * px - 1) / py;
+  let n = (-m * px - 1) / py;
+
+  if (n === 0) {
+    // if zero {-0, 0}, fall back to a same-sign value in the similar range
+    n = (Object.is(n, -0) ? -1 : 1) * 0.01;
+  }
 
 
   const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
   const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
   return GA.point(x, (-m * x - 1) / n);
   return GA.point(x, (-m * x - 1) / n);

+ 15 - 3
src/element/newElement.ts

@@ -31,7 +31,11 @@ import {
 import { VERTICAL_ALIGN } from "../constants";
 import { VERTICAL_ALIGN } from "../constants";
 import { isArrowElement } from "./typeChecks";
 import { isArrowElement } from "./typeChecks";
 import { MarkOptional, Merge, Mutable } from "../utility-types";
 import { MarkOptional, Merge, Mutable } from "../utility-types";
-import { measureText, wrapText } from "./textMeasurements";
+import {
+  measureText,
+  wrapText,
+  getDefaultLineHeight,
+} from "./textMeasurements";
 
 
 type ElementConstructorOpts = MarkOptional<
 type ElementConstructorOpts = MarkOptional<
   Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
   Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -136,10 +140,12 @@ export const newTextElement = (
     textAlign: TextAlign;
     textAlign: TextAlign;
     verticalAlign: VerticalAlign;
     verticalAlign: VerticalAlign;
     containerId?: ExcalidrawTextContainer["id"];
     containerId?: ExcalidrawTextContainer["id"];
+    lineHeight?: ExcalidrawTextElement["lineHeight"];
   } & ElementConstructorOpts,
   } & ElementConstructorOpts,
 ): NonDeleted<ExcalidrawTextElement> => {
 ): NonDeleted<ExcalidrawTextElement> => {
+  const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily);
   const text = normalizeText(opts.text);
   const text = normalizeText(opts.text);
-  const metrics = measureText(text, getFontString(opts));
+  const metrics = measureText(text, getFontString(opts), lineHeight);
   const offsets = getTextElementPositionOffsets(opts, metrics);
   const offsets = getTextElementPositionOffsets(opts, metrics);
   const textElement = newElementWith(
   const textElement = newElementWith(
     {
     {
@@ -155,6 +161,7 @@ export const newTextElement = (
       height: metrics.height,
       height: metrics.height,
       containerId: opts.containerId || null,
       containerId: opts.containerId || null,
       originalText: text,
       originalText: text,
+      lineHeight,
     },
     },
     {},
     {},
   );
   );
@@ -175,6 +182,7 @@ const getAdjustedDimensions = (
   const { width: nextWidth, height: nextHeight } = measureText(
   const { width: nextWidth, height: nextHeight } = measureText(
     nextText,
     nextText,
     getFontString(element),
     getFontString(element),
+    element.lineHeight,
   );
   );
   const { textAlign, verticalAlign } = element;
   const { textAlign, verticalAlign } = element;
   let x: number;
   let x: number;
@@ -184,7 +192,11 @@ const getAdjustedDimensions = (
     verticalAlign === VERTICAL_ALIGN.MIDDLE &&
     verticalAlign === VERTICAL_ALIGN.MIDDLE &&
     !element.containerId
     !element.containerId
   ) {
   ) {
-    const prevMetrics = measureText(element.text, getFontString(element));
+    const prevMetrics = measureText(
+      element.text,
+      getFontString(element),
+      element.lineHeight,
+    );
     const offsets = getTextElementPositionOffsets(element, {
     const offsets = getTextElementPositionOffsets(element, {
       width: nextWidth - prevMetrics.width,
       width: nextWidth - prevMetrics.width,
       height: nextHeight - prevMetrics.height,
       height: nextHeight - prevMetrics.height,

+ 15 - 11
src/element/resizeElements.ts

@@ -361,7 +361,7 @@ export const resizeSingleElement = (
   let scaleX = atStartBoundsWidth / boundsCurrentWidth;
   let scaleX = atStartBoundsWidth / boundsCurrentWidth;
   let scaleY = atStartBoundsHeight / boundsCurrentHeight;
   let scaleY = atStartBoundsHeight / boundsCurrentHeight;
 
 
-  let boundTextFont: { fontSize?: number } = {};
+  let boundTextFontSize: number | null = null;
   const boundTextElement = getBoundTextElement(element);
   const boundTextElement = getBoundTextElement(element);
 
 
   if (transformHandleDirection.includes("e")) {
   if (transformHandleDirection.includes("e")) {
@@ -411,9 +411,7 @@ export const resizeSingleElement = (
       boundTextElement.id,
       boundTextElement.id,
     ) as typeof boundTextElement | undefined;
     ) as typeof boundTextElement | undefined;
     if (stateOfBoundTextElementAtResize) {
     if (stateOfBoundTextElementAtResize) {
-      boundTextFont = {
-        fontSize: stateOfBoundTextElementAtResize.fontSize,
-      };
+      boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
     }
     }
     if (shouldMaintainAspectRatio) {
     if (shouldMaintainAspectRatio) {
       const updatedElement = {
       const updatedElement = {
@@ -429,12 +427,16 @@ export const resizeSingleElement = (
       if (nextFontSize === null) {
       if (nextFontSize === null) {
         return;
         return;
       }
       }
-      boundTextFont = {
-        fontSize: nextFontSize,
-      };
+      boundTextFontSize = nextFontSize;
     } else {
     } else {
-      const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
-      const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
+      const minWidth = getApproxMinLineWidth(
+        getFontString(boundTextElement),
+        boundTextElement.lineHeight,
+      );
+      const minHeight = getApproxMinLineHeight(
+        boundTextElement.fontSize,
+        boundTextElement.lineHeight,
+      );
       eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
       eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
       eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
       eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
     }
     }
@@ -567,8 +569,10 @@ export const resizeSingleElement = (
     });
     });
 
 
     mutateElement(element, resizedElement);
     mutateElement(element, resizedElement);
-    if (boundTextElement && boundTextFont) {
-      mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
+    if (boundTextElement && boundTextFontSize != null) {
+      mutateElement(boundTextElement, {
+        fontSize: boundTextFontSize,
+      });
     }
     }
     handleBindTextResize(element, transformHandleDirection);
     handleBindTextResize(element, transformHandleDirection);
   }
   }

+ 6 - 2
src/element/textElement.ts

@@ -40,7 +40,6 @@ export const redrawTextBoundingBox = (
   container: ExcalidrawElement | null,
   container: ExcalidrawElement | null,
 ) => {
 ) => {
   let maxWidth = undefined;
   let maxWidth = undefined;
-
   const boundTextUpdates = {
   const boundTextUpdates = {
     x: textElement.x,
     x: textElement.x,
     y: textElement.y,
     y: textElement.y,
@@ -61,6 +60,7 @@ export const redrawTextBoundingBox = (
   const metrics = measureText(
   const metrics = measureText(
     boundTextUpdates.text,
     boundTextUpdates.text,
     getFontString(textElement),
     getFontString(textElement),
+    textElement.lineHeight,
   );
   );
 
 
   boundTextUpdates.width = metrics.width;
   boundTextUpdates.width = metrics.width;
@@ -175,7 +175,11 @@ export const handleBindTextResize = (
           maxWidth,
           maxWidth,
         );
         );
       }
       }
-      const dimensions = measureText(text, getFontString(textElement));
+      const dimensions = measureText(
+        text,
+        getFontString(textElement),
+        textElement.lineHeight,
+      );
       nextHeight = dimensions.height;
       nextHeight = dimensions.height;
       nextWidth = dimensions.width;
       nextWidth = dimensions.width;
     }
     }

+ 52 - 19
src/element/textMeasurements.test.ts

@@ -1,5 +1,11 @@
-import { BOUND_TEXT_PADDING } from "../constants";
-import { wrapText } from "./textMeasurements";
+import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
+import { API } from "../tests/helpers/api";
+import {
+  detectLineHeight,
+  getDefaultLineHeight,
+  getLineHeightInPx,
+  wrapText,
+} from "./textMeasurements";
 import { FontString } from "./types";
 import { FontString } from "./types";
 
 
 describe("Test wrapText", () => {
 describe("Test wrapText", () => {
@@ -33,9 +39,7 @@ describe("Test wrapText", () => {
       {
       {
         desc: "break all words when width of each word is less than container width",
         desc: "break all words when width of each word is less than container width",
         width: 80,
         width: 80,
-        res: `Hello 
-whats 
-up`,
+        res: `Hello \nwhats \nup`,
       },
       },
       {
       {
         desc: "break all characters when width of each character is less than container width",
         desc: "break all characters when width of each character is less than container width",
@@ -57,8 +61,7 @@ p`,
         desc: "break words as per the width",
         desc: "break words as per the width",
 
 
         width: 140,
         width: 140,
-        res: `Hello whats 
-up`,
+        res: `Hello whats \nup`,
       },
       },
       {
       {
         desc: "fit the container",
         desc: "fit the container",
@@ -88,9 +91,7 @@ whats up`;
       {
       {
         desc: "break all words when width of each word is less than container width",
         desc: "break all words when width of each word is less than container width",
         width: 80,
         width: 80,
-        res: `Hello
-whats 
-up`,
+        res: `Hello\nwhats \nup`,
       },
       },
       {
       {
         desc: "break all characters when width of each character is less than container width",
         desc: "break all characters when width of each character is less than container width",
@@ -136,11 +137,7 @@ whats up`,
       {
       {
         desc: "fit characters of long string as per container width",
         desc: "fit characters of long string as per container width",
         width: 170,
         width: 170,
-        res: `hellolongtextth
-isiswhatsupwith
-youIamtypingggg
-gandtypinggg 
-break it now`,
+        res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
       },
       },
 
 
       {
       {
@@ -159,8 +156,7 @@ now`,
         desc: "fit the long text when container width is greater than text length and move the rest to next line",
         desc: "fit the long text when container width is greater than text length and move the rest to next line",
 
 
         width: 600,
         width: 600,
-        res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg 
-break it now`,
+        res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
       },
       },
     ].forEach((data) => {
     ].forEach((data) => {
       it(`should ${data.desc}`, () => {
       it(`should ${data.desc}`, () => {
@@ -174,7 +170,44 @@ break it now`,
     const text = "Hello Excalidraw";
     const text = "Hello Excalidraw";
     // Length of "Excalidraw" is 100 and exacty equal to max width
     // Length of "Excalidraw" is 100 and exacty equal to max width
     const res = wrapText(text, font, 100);
     const res = wrapText(text, font, 100);
-    expect(res).toEqual(`Hello 
-Excalidraw`);
+    expect(res).toEqual(`Hello \nExcalidraw`);
+  });
+
+  it("should return the text as is if max width is invalid", () => {
+    const text = "Hello Excalidraw";
+    expect(wrapText(text, font, NaN)).toEqual(text);
+    expect(wrapText(text, font, -1)).toEqual(text);
+    expect(wrapText(text, font, Infinity)).toEqual(text);
+  });
+});
+const textElement = API.createElement({
+  type: "text",
+  text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
+  fontSize: 20,
+  fontFamily: 1,
+  height: 175,
+});
+
+describe("Test detectLineHeight", () => {
+  it("should return correct line height", () => {
+    expect(detectLineHeight(textElement)).toBe(1.25);
+  });
+});
+
+describe("Test getLineHeightInPx", () => {
+  it("should return correct line height", () => {
+    expect(
+      getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
+    ).toBe(25);
+  });
+});
+
+describe("Test getDefaultLineHeight", () => {
+  it("should return line height using default font family when not passed", () => {
+    //@ts-ignore
+    expect(getDefaultLineHeight()).toBe(1.25);
+  });
+  it("should return correct line height", () => {
+    expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
   });
   });
 });
 });

+ 91 - 15
src/element/textMeasurements.ts

@@ -2,9 +2,11 @@ import {
   BOUND_TEXT_PADDING,
   BOUND_TEXT_PADDING,
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
   DEFAULT_FONT_SIZE,
+  FONT_FAMILY,
 } from "../constants";
 } from "../constants";
 import { getFontString, isTestEnv } from "../utils";
 import { getFontString, isTestEnv } from "../utils";
-import { FontString } from "./types";
+import { normalizeText } from "./textElement";
+import { ExcalidrawTextElement, FontFamilyValues, FontString } from "./types";
 
 
 const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
 const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
 const cacheLineHeight: { [key: FontString]: number } = {};
 const cacheLineHeight: { [key: FontString]: number } = {};
@@ -35,16 +37,14 @@ const getLineWidth = (text: string, font: FontString) => {
   canvas2dContext.font = font;
   canvas2dContext.font = font;
   const width = canvas2dContext.measureText(text).width;
   const width = canvas2dContext.measureText(text).width;
 
 
-  /* istanbul ignore else */
   if (isTestEnv()) {
   if (isTestEnv()) {
     return width * DUMMY_CHAR_WIDTH;
     return width * DUMMY_CHAR_WIDTH;
   }
   }
-  /* istanbul ignore next */
   return width;
   return width;
 };
 };
 
 
 export const getTextWidth = (text: string, font: FontString) => {
 export const getTextWidth = (text: string, font: FontString) => {
-  const lines = text.replace(/\r\n?/g, "\n").split("\n");
+  const lines = splitIntoLines(text);
   let width = 0;
   let width = 0;
   lines.forEach((line) => {
   lines.forEach((line) => {
     width = Math.max(width, getLineWidth(line, font));
     width = Math.max(width, getLineWidth(line, font));
@@ -52,39 +52,57 @@ export const getTextWidth = (text: string, font: FontString) => {
   return width;
   return width;
 };
 };
 
 
-export const getTextHeight = (text: string, font: FontString) => {
-  const lines = text.replace(/\r\n?/g, "\n").split("\n");
-  const lineHeight = getLineHeight(font);
-  return lineHeight * lines.length;
+export const getTextHeight = (
+  text: string,
+  fontSize: number,
+  lineHeight: ExcalidrawTextElement["lineHeight"],
+) => {
+  const lineCount = splitIntoLines(text).length;
+  return getLineHeightInPx(fontSize, lineHeight) * lineCount;
 };
 };
 
 
-export const measureText = (text: string, font: FontString) => {
+export const splitIntoLines = (text: string) => {
+  return normalizeText(text).split("\n");
+};
+
+export const measureText = (
+  text: string,
+  font: FontString,
+  lineHeight: ExcalidrawTextElement["lineHeight"],
+) => {
   text = text
   text = text
     .split("\n")
     .split("\n")
     // replace empty lines with single space because leading/trailing empty
     // replace empty lines with single space because leading/trailing empty
     // lines would be stripped from computation
     // lines would be stripped from computation
     .map((x) => x || " ")
     .map((x) => x || " ")
     .join("\n");
     .join("\n");
-
-  const height = getTextHeight(text, font);
+  const fontSize = parseFloat(font);
+  const height = getTextHeight(text, fontSize, lineHeight);
   const width = getTextWidth(text, font);
   const width = getTextWidth(text, font);
 
 
   return { width, height };
   return { width, height };
 };
 };
 
 
-export const getApproxMinLineWidth = (font: FontString) => {
+export const getApproxMinLineWidth = (
+  font: FontString,
+  lineHeight: ExcalidrawTextElement["lineHeight"],
+) => {
   const maxCharWidth = getMaxCharWidth(font);
   const maxCharWidth = getMaxCharWidth(font);
   if (maxCharWidth === 0) {
   if (maxCharWidth === 0) {
     return (
     return (
-      measureText(DUMMY_TEXT.split("").join("\n"), font).width +
+      measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
       BOUND_TEXT_PADDING * 2
       BOUND_TEXT_PADDING * 2
     );
     );
   }
   }
   return maxCharWidth + BOUND_TEXT_PADDING * 2;
   return maxCharWidth + BOUND_TEXT_PADDING * 2;
 };
 };
 
 
-export const getApproxMinLineHeight = (font: FontString) => {
-  return getLineHeight(font) + BOUND_TEXT_PADDING * 2;
+// FIXME rename to getApproxMinContainerHeight
+export const getApproxMinLineHeight = (
+  fontSize: ExcalidrawTextElement["fontSize"],
+  lineHeight: ExcalidrawTextElement["lineHeight"],
+) => {
+  return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
 };
 };
 
 
 export const charWidth = (() => {
 export const charWidth = (() => {
@@ -150,6 +168,13 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
 };
 };
 
 
 export const wrapText = (text: string, font: FontString, maxWidth: number) => {
 export const wrapText = (text: string, font: FontString, maxWidth: number) => {
+  // if maxWidth is not finite or NaN which can happen in case of bugs in
+  // computation, we need to make sure we don't continue as we'll end up
+  // in an infinite loop
+  if (!Number.isFinite(maxWidth) || maxWidth < 0) {
+    return text;
+  }
+
   const lines: Array<string> = [];
   const lines: Array<string> = [];
   const originalLines = text.split("\n");
   const originalLines = text.split("\n");
   const spaceWidth = getLineWidth(" ", font);
   const spaceWidth = getLineWidth(" ", font);
@@ -272,3 +297,54 @@ export const isMeasureTextSupported = () => {
   );
   );
   return width > 0;
   return width > 0;
 };
 };
+
+/**
+ * We calculate the line height from the font size and the unitless line height,
+ * aligning with the W3C spec.
+ */
+export const getLineHeightInPx = (
+  fontSize: ExcalidrawTextElement["fontSize"],
+  lineHeight: ExcalidrawTextElement["lineHeight"],
+) => {
+  return fontSize * lineHeight;
+};
+
+/**
+ * To get unitless line-height (if unknown) we can calculate it by dividing
+ * height-per-line by fontSize.
+ */
+export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
+  const lineCount = splitIntoLines(textElement.text).length;
+  return (textElement.height /
+    lineCount /
+    textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
+};
+
+/**
+ * Unitless line height
+ *
+ * In previous versions we used `normal` line height, which browsers interpret
+ * differently, and based on font-family and font-size.
+ *
+ * To make line heights consistent across browsers we hardcode the values for
+ * each of our fonts based on most common average line-heights.
+ * See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
+ * where the values come from.
+ */
+const DEFAULT_LINE_HEIGHT = {
+  // ~1.25 is the average for Virgil in WebKit and Blink.
+  // Gecko (FF) uses ~1.28.
+  [FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
+  // ~1.15 is the average for Virgil in WebKit and Blink.
+  // Gecko if all over the place.
+  [FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
+  // ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
+  [FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
+};
+
+export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
+  if (fontFamily) {
+    return DEFAULT_LINE_HEIGHT[fontFamily];
+  }
+  return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
+};

+ 50 - 15
src/element/textWysiwyg.test.tsx

@@ -783,7 +783,7 @@ describe("textWysiwyg", () => {
         rectangle.y + h.elements[0].height / 2 - text.height / 2,
         rectangle.y + h.elements[0].height / 2 - text.height / 2,
       );
       );
       expect(text.x).toBe(25);
       expect(text.x).toBe(25);
-      expect(text.height).toBe(48);
+      expect(text.height).toBe(50);
       expect(text.width).toBe(60);
       expect(text.width).toBe(60);
 
 
       // Edit and text by removing second line and it should
       // Edit and text by removing second line and it should
@@ -810,7 +810,7 @@ describe("textWysiwyg", () => {
 
 
       expect(text.text).toBe("Hello");
       expect(text.text).toBe("Hello");
       expect(text.originalText).toBe("Hello");
       expect(text.originalText).toBe("Hello");
-      expect(text.height).toBe(24);
+      expect(text.height).toBe(25);
       expect(text.width).toBe(50);
       expect(text.width).toBe(50);
       expect(text.y).toBe(
       expect(text.y).toBe(
         rectangle.y + h.elements[0].height / 2 - text.height / 2,
         rectangle.y + h.elements[0].height / 2 - text.height / 2,
@@ -903,7 +903,7 @@ describe("textWysiwyg", () => {
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         Array [
         Array [
           85,
           85,
-          5,
+          4.5,
         ]
         ]
       `);
       `);
 
 
@@ -929,7 +929,7 @@ describe("textWysiwyg", () => {
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         Array [
         Array [
           15,
           15,
-          66,
+          65,
         ]
         ]
       `);
       `);
 
 
@@ -1067,9 +1067,9 @@ describe("textWysiwyg", () => {
       mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
       mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
       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(-35);
+      expect(rectangle.y).toBe(-40);
       expect(text.x).toBe(85);
       expect(text.x).toBe(85);
-      expect(text.y).toBe(-30);
+      expect(text.y).toBe(-35);
 
 
       Keyboard.withModifierKeys({ ctrl: true }, () => {
       Keyboard.withModifierKeys({ ctrl: true }, () => {
         Keyboard.keyPress(KEYS.Z);
         Keyboard.keyPress(KEYS.Z);
@@ -1112,7 +1112,7 @@ describe("textWysiwyg", () => {
         target: { value: "Online whiteboard collaboration made easy" },
         target: { value: "Online whiteboard collaboration made easy" },
       });
       });
       editor.blur();
       editor.blur();
-      expect(rectangle.height).toBe(178);
+      expect(rectangle.height).toBe(185);
       mouse.select(rectangle);
       mouse.select(rectangle);
       fireEvent.contextMenu(GlobalTestState.canvas, {
       fireEvent.contextMenu(GlobalTestState.canvas, {
         button: 2,
         button: 2,
@@ -1186,6 +1186,41 @@ describe("textWysiwyg", () => {
       );
       );
     });
     });
 
 
+    it("should update line height when font family updated", async () => {
+      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!" } });
+      editor.blur();
+      expect(
+        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
+      ).toEqual(1.25);
+
+      mouse.select(rectangle);
+      Keyboard.keyPress(KEYS.ENTER);
+
+      fireEvent.click(screen.getByTitle(/code/i));
+      expect(
+        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
+      ).toEqual(FONT_FAMILY.Cascadia);
+      expect(
+        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
+      ).toEqual(1.2);
+
+      fireEvent.click(screen.getByTitle(/normal/i));
+      expect(
+        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
+      ).toEqual(FONT_FAMILY.Helvetica);
+      expect(
+        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
+      ).toEqual(1.15);
+    });
+
     describe("should align correctly", () => {
     describe("should align correctly", () => {
       let editor: HTMLTextAreaElement;
       let editor: HTMLTextAreaElement;
 
 
@@ -1245,7 +1280,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             15,
             15,
-            45.5,
+            45,
           ]
           ]
         `);
         `);
       });
       });
@@ -1257,7 +1292,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             30,
             30,
-            45.5,
+            45,
           ]
           ]
         `);
         `);
       });
       });
@@ -1269,7 +1304,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             45,
             45,
-            45.5,
+            45,
           ]
           ]
         `);
         `);
       });
       });
@@ -1281,7 +1316,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             15,
             15,
-            66,
+            65,
           ]
           ]
         `);
         `);
       });
       });
@@ -1292,7 +1327,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             30,
             30,
-            66,
+            65,
           ]
           ]
         `);
         `);
       });
       });
@@ -1303,7 +1338,7 @@ describe("textWysiwyg", () => {
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
           Array [
           Array [
             45,
             45,
-            66,
+            65,
           ]
           ]
         `);
         `);
       });
       });
@@ -1333,7 +1368,7 @@ describe("textWysiwyg", () => {
 
 
       const textElement = h.elements[1] as ExcalidrawTextElement;
       const textElement = h.elements[1] as ExcalidrawTextElement;
       expect(textElement.width).toBe(600);
       expect(textElement.width).toBe(600);
-      expect(textElement.height).toBe(24);
+      expect(textElement.height).toBe(25);
       expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
       expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
       expect((textElement as ExcalidrawTextElement).text).toBe(
       expect((textElement as ExcalidrawTextElement).text).toBe(
         "Excalidraw is an opensource virtual collaborative whiteboard",
         "Excalidraw is an opensource virtual collaborative whiteboard",
@@ -1365,7 +1400,7 @@ describe("textWysiwyg", () => {
           ],
           ],
           fillStyle: "hachure",
           fillStyle: "hachure",
           groupIds: [],
           groupIds: [],
-          height: 34,
+          height: 35,
           isDeleted: false,
           isDeleted: false,
           link: null,
           link: null,
           locked: false,
           locked: false,

+ 16 - 7
src/element/textWysiwyg.tsx

@@ -41,7 +41,6 @@ import App from "../components/App";
 import { LinearElementEditor } from "./linearElementEditor";
 import { LinearElementEditor } from "./linearElementEditor";
 import { parseClipboard } from "../clipboard";
 import { parseClipboard } from "../clipboard";
 import {
 import {
-  getLineHeight,
   getTextWidth,
   getTextWidth,
   measureText,
   measureText,
   wrapText,
   wrapText,
@@ -153,7 +152,6 @@ export const textWysiwyg = ({
       return;
       return;
     }
     }
     const { textAlign, verticalAlign } = updatedTextElement;
     const { textAlign, verticalAlign } = updatedTextElement;
-    const lineHeight = getLineHeight(getFontString(updatedTextElement));
 
 
     if (updatedTextElement && isTextElement(updatedTextElement)) {
     if (updatedTextElement && isTextElement(updatedTextElement)) {
       let coordX = updatedTextElement.x;
       let coordX = updatedTextElement.x;
@@ -185,7 +183,8 @@ export const textWysiwyg = ({
 
 
         textElementHeight = getTextHeight(
         textElementHeight = getTextHeight(
           updatedTextElement.text,
           updatedTextElement.text,
-          getFontString(updatedTextElement),
+          updatedTextElement.fontSize,
+          updatedTextElement.lineHeight,
         );
         );
 
 
         let originalContainerData;
         let originalContainerData;
@@ -212,7 +211,10 @@ export const textWysiwyg = ({
 
 
         // autogrow container height if text exceeds
         // autogrow container height if text exceeds
         if (!isArrowElement(container) && textElementHeight > maxHeight) {
         if (!isArrowElement(container) && textElementHeight > maxHeight) {
-          const diff = Math.min(textElementHeight - maxHeight, lineHeight);
+          const diff = Math.min(
+            textElementHeight - maxHeight,
+            element.lineHeight,
+          );
           mutateElement(container, { height: containerDims.height + diff });
           mutateElement(container, { height: containerDims.height + diff });
           return;
           return;
         } else if (
         } else if (
@@ -222,7 +224,10 @@ export const textWysiwyg = ({
           containerDims.height > originalContainerData.height &&
           containerDims.height > originalContainerData.height &&
           textElementHeight < maxHeight
           textElementHeight < maxHeight
         ) {
         ) {
-          const diff = Math.min(maxHeight - textElementHeight, lineHeight);
+          const diff = Math.min(
+            maxHeight - textElementHeight,
+            element.lineHeight,
+          );
           mutateElement(container, { height: containerDims.height - diff });
           mutateElement(container, { height: containerDims.height - diff });
         } else {
         } else {
           const { y } = computeBoundTextPosition(
           const { y } = computeBoundTextPosition(
@@ -263,7 +268,7 @@ export const textWysiwyg = ({
       Object.assign(editable.style, {
       Object.assign(editable.style, {
         font: getFontString(updatedTextElement),
         font: getFontString(updatedTextElement),
         // must be defined *after* font ¯\_(ツ)_/¯
         // must be defined *after* font ¯\_(ツ)_/¯
-        lineHeight: `${lineHeight}px`,
+        lineHeight: element.lineHeight,
         width: `${textElementWidth}px`,
         width: `${textElementWidth}px`,
         height: `${textElementHeight}px`,
         height: `${textElementHeight}px`,
         left: `${viewportX}px`,
         left: `${viewportX}px`,
@@ -369,7 +374,11 @@ export const textWysiwyg = ({
           font,
           font,
           getBoundTextMaxWidth(container!),
           getBoundTextMaxWidth(container!),
         );
         );
-        const { width, height } = measureText(wrappedText, font);
+        const { width, height } = measureText(
+          wrappedText,
+          font,
+          updatedTextElement.lineHeight,
+        );
         editable.style.width = `${width}px`;
         editable.style.width = `${width}px`;
         editable.style.height = `${height}px`;
         editable.style.height = `${height}px`;
       }
       }

+ 5 - 0
src/element/types.ts

@@ -135,6 +135,11 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
     verticalAlign: VerticalAlign;
     verticalAlign: VerticalAlign;
     containerId: ExcalidrawGenericElement["id"] | null;
     containerId: ExcalidrawGenericElement["id"] | null;
     originalText: string;
     originalText: string;
+    /**
+     * Unitless line height (aligned to W3C). To get line height in px, multiply
+     *  with font size (using `getLineHeightInPx` helper).
+     */
+    lineHeight: number & { _brand: "unitlessLineHeight" };
   }>;
   }>;
 
 
 export type ExcalidrawBindableElement =
 export type ExcalidrawBindableElement =

+ 1 - 1
src/packages/excalidraw/package.json

@@ -64,7 +64,7 @@
     "terser-webpack-plugin": "5.3.3",
     "terser-webpack-plugin": "5.3.3",
     "ts-loader": "9.3.1",
     "ts-loader": "9.3.1",
     "typescript": "4.7.4",
     "typescript": "4.7.4",
-    "webpack": "5.73.0",
+    "webpack": "5.76.0",
     "webpack-bundle-analyzer": "4.5.0",
     "webpack-bundle-analyzer": "4.5.0",
     "webpack-cli": "4.10.0",
     "webpack-cli": "4.10.0",
     "webpack-dev-server": "4.9.3",
     "webpack-dev-server": "4.9.3",

+ 19 - 19
src/packages/excalidraw/yarn.lock

@@ -1393,10 +1393,10 @@ acorn-walk@^8.0.0:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
   integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
   integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
 
 
-acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
-  version "8.7.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
-  integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
+acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
+  version "8.8.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
 
 
 ajv-formats@^2.1.1:
 ajv-formats@^2.1.1:
   version "2.1.1"
   version "2.1.1"
@@ -2068,10 +2068,10 @@ encodeurl@~1.0.2:
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
 
-enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
-  version "5.10.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
-  integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
+  version "5.12.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
+  integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
   dependencies:
   dependencies:
     graceful-fs "^4.2.4"
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
     tapable "^2.2.0"
@@ -3751,10 +3751,10 @@ vary@~1.1.2:
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
 
-watchpack@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
-  integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
+watchpack@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
   dependencies:
   dependencies:
     glob-to-regexp "^0.4.1"
     glob-to-regexp "^0.4.1"
     graceful-fs "^4.1.2"
     graceful-fs "^4.1.2"
@@ -3858,21 +3858,21 @@ webpack-sources@^3.2.3:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
 
[email protected]3.0:
-  version "5.73.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
-  integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
[email protected]6.0:
+  version "5.76.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
+  integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
   dependencies:
   dependencies:
     "@types/eslint-scope" "^3.7.3"
     "@types/eslint-scope" "^3.7.3"
     "@types/estree" "^0.0.51"
     "@types/estree" "^0.0.51"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
-    acorn "^8.4.1"
+    acorn "^8.7.1"
     acorn-import-assertions "^1.7.6"
     acorn-import-assertions "^1.7.6"
     browserslist "^4.14.5"
     browserslist "^4.14.5"
     chrome-trace-event "^1.0.2"
     chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.9.3"
+    enhanced-resolve "^5.10.0"
     es-module-lexer "^0.9.0"
     es-module-lexer "^0.9.0"
     eslint-scope "5.1.1"
     eslint-scope "5.1.1"
     events "^3.2.0"
     events "^3.2.0"
@@ -3885,7 +3885,7 @@ [email protected]:
     schema-utils "^3.1.0"
     schema-utils "^3.1.0"
     tapable "^2.1.1"
     tapable "^2.1.1"
     terser-webpack-plugin "^5.1.3"
     terser-webpack-plugin "^5.1.3"
-    watchpack "^2.3.1"
+    watchpack "^2.4.0"
     webpack-sources "^3.2.3"
     webpack-sources "^3.2.3"
 
 
 websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
 websocket-driver@>=0.5.1, websocket-driver@^0.7.4:

+ 1 - 1
src/packages/utils/package.json

@@ -48,7 +48,7 @@
     "file-loader": "6.2.0",
     "file-loader": "6.2.0",
     "sass-loader": "13.0.2",
     "sass-loader": "13.0.2",
     "ts-loader": "9.3.1",
     "ts-loader": "9.3.1",
-    "webpack": "5.73.0",
+    "webpack": "5.76.0",
     "webpack-bundle-analyzer": "4.5.0",
     "webpack-bundle-analyzer": "4.5.0",
     "webpack-cli": "4.10.0"
     "webpack-cli": "4.10.0"
   },
   },

+ 20 - 46
src/packages/utils/yarn.lock

@@ -1187,10 +1187,10 @@ acorn-walk@^8.0.0:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
   integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
   integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
 
 
-acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
-  version "8.7.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
-  integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
+acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
+  version "8.8.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
 
 
 ajv-keywords@^3.5.2:
 ajv-keywords@^3.5.2:
   version "3.5.2"
   version "3.5.2"
@@ -1383,18 +1383,7 @@ braces@^3.0.1:
   dependencies:
   dependencies:
     fill-range "^7.0.1"
     fill-range "^7.0.1"
 
 
-browserslist@^4.14.5:
-  version "4.19.3"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.3.tgz#29b7caad327ecf2859485f696f9604214bedd383"
-  integrity sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==
-  dependencies:
-    caniuse-lite "^1.0.30001312"
-    electron-to-chromium "^1.4.71"
-    escalade "^3.1.1"
-    node-releases "^2.0.2"
-    picocolors "^1.0.0"
-
-browserslist@^4.20.2, browserslist@^4.21.2:
+browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.21.2:
   version "4.21.2"
   version "4.21.2"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
   integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
   integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
@@ -1417,11 +1406,6 @@ call-bind@^1.0.0:
     function-bind "^1.1.1"
     function-bind "^1.1.1"
     get-intrinsic "^1.0.2"
     get-intrinsic "^1.0.2"
 
 
-caniuse-lite@^1.0.30001312:
-  version "1.0.30001312"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
-  integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
-
 caniuse-lite@^1.0.30001366:
 caniuse-lite@^1.0.30001366:
   version "1.0.30001367"
   version "1.0.30001367"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
@@ -1601,20 +1585,15 @@ electron-to-chromium@^1.4.188:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
   integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
   integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
 
 
-electron-to-chromium@^1.4.71:
-  version "1.4.75"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.75.tgz#d1ad9bb46f2f1bf432118c2be21d27ffeae82fdd"
-  integrity sha512-LxgUNeu3BVU7sXaKjUDD9xivocQLxFtq6wgERrutdY/yIOps3ODOZExK1jg8DTEg4U8TUCb5MLGeWFOYuxjF3Q==
-
 emojis-list@^3.0.0:
 emojis-list@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
   integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
 
 
-enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
-  version "5.9.3"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
-  integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
+  version "5.12.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
+  integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
   dependencies:
   dependencies:
     graceful-fs "^4.2.4"
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
     tapable "^2.2.0"
@@ -2011,11 +1990,6 @@ neo-async@^2.6.2:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
   integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
   integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
 
 
-node-releases@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
-  integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
-
 node-releases@^2.0.6:
 node-releases@^2.0.6:
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
@@ -2494,10 +2468,10 @@ util-deprecate@^1.0.2:
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
 
-watchpack@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
-  integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
+watchpack@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
   dependencies:
   dependencies:
     glob-to-regexp "^0.4.1"
     glob-to-regexp "^0.4.1"
     graceful-fs "^4.1.2"
     graceful-fs "^4.1.2"
@@ -2548,21 +2522,21 @@ webpack-sources@^3.2.3:
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
   integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
 
[email protected]3.0:
-  version "5.73.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
-  integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
[email protected]6.0:
+  version "5.76.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
+  integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
   dependencies:
   dependencies:
     "@types/eslint-scope" "^3.7.3"
     "@types/eslint-scope" "^3.7.3"
     "@types/estree" "^0.0.51"
     "@types/estree" "^0.0.51"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-edit" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
     "@webassemblyjs/wasm-parser" "1.11.1"
-    acorn "^8.4.1"
+    acorn "^8.7.1"
     acorn-import-assertions "^1.7.6"
     acorn-import-assertions "^1.7.6"
     browserslist "^4.14.5"
     browserslist "^4.14.5"
     chrome-trace-event "^1.0.2"
     chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.9.3"
+    enhanced-resolve "^5.10.0"
     es-module-lexer "^0.9.0"
     es-module-lexer "^0.9.0"
     eslint-scope "5.1.1"
     eslint-scope "5.1.1"
     events "^3.2.0"
     events "^3.2.0"
@@ -2575,7 +2549,7 @@ [email protected]:
     schema-utils "^3.1.0"
     schema-utils "^3.1.0"
     tapable "^2.1.1"
     tapable "^2.1.1"
     terser-webpack-plugin "^5.1.3"
     terser-webpack-plugin "^5.1.3"
-    watchpack "^2.3.1"
+    watchpack "^2.4.0"
     webpack-sources "^3.2.3"
     webpack-sources "^3.2.3"
 
 
 which@^2.0.1:
 which@^2.0.1:

+ 12 - 7
src/renderer/renderElement.ts

@@ -47,7 +47,7 @@ import {
   getBoundTextMaxWidth,
   getBoundTextMaxWidth,
 } from "../element/textElement";
 } from "../element/textElement";
 import { LinearElementEditor } from "../element/linearElementEditor";
 import { LinearElementEditor } from "../element/linearElementEditor";
-import { getLineHeight } from "../element/textMeasurements";
+import { getLineHeightInPx } from "../element/textMeasurements";
 
 
 // using a stronger invert (100% vs our regular 93%) and saturate
 // using a stronger invert (100% vs our regular 93%) and saturate
 // as a temp hack to make images in dark theme look closer to original
 // as a temp hack to make images in dark theme look closer to original
@@ -279,9 +279,6 @@ const drawElementOnCanvas = (
 
 
         // Canvas does not support multiline text by default
         // Canvas does not support multiline text by default
         const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
         const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
-        const lineHeight = element.containerId
-          ? getLineHeight(getFontString(element))
-          : element.height / lines.length;
         const horizontalOffset =
         const horizontalOffset =
           element.textAlign === "center"
           element.textAlign === "center"
             ? element.width / 2
             ? element.width / 2
@@ -290,11 +287,16 @@ const drawElementOnCanvas = (
             : 0;
             : 0;
         context.textBaseline = "bottom";
         context.textBaseline = "bottom";
 
 
+        const lineHeightPx = getLineHeightInPx(
+          element.fontSize,
+          element.lineHeight,
+        );
+
         for (let index = 0; index < lines.length; index++) {
         for (let index = 0; index < lines.length; index++) {
           context.fillText(
           context.fillText(
             lines[index],
             lines[index],
             horizontalOffset,
             horizontalOffset,
-            (index + 1) * lineHeight,
+            (index + 1) * lineHeightPx,
           );
           );
         }
         }
         context.restore();
         context.restore();
@@ -1316,7 +1318,10 @@ export const renderElementToSvg = (
           }) rotate(${degree} ${cx} ${cy})`,
           }) rotate(${degree} ${cx} ${cy})`,
         );
         );
         const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
         const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
-        const lineHeight = element.height / lines.length;
+        const lineHeightPx = getLineHeightInPx(
+          element.fontSize,
+          element.lineHeight,
+        );
         const horizontalOffset =
         const horizontalOffset =
           element.textAlign === "center"
           element.textAlign === "center"
             ? element.width / 2
             ? element.width / 2
@@ -1334,7 +1339,7 @@ export const renderElementToSvg = (
           const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
           const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
           text.textContent = lines[i];
           text.textContent = lines[i];
           text.setAttribute("x", `${horizontalOffset}`);
           text.setAttribute("x", `${horizontalOffset}`);
-          text.setAttribute("y", `${i * lineHeight}`);
+          text.setAttribute("y", `${i * lineHeightPx}`);
           text.setAttribute("font-family", getFontFamilyString(element));
           text.setAttribute("font-family", getFontFamilyString(element));
           text.setAttribute("font-size", `${element.fontSize}px`);
           text.setAttribute("font-size", `${element.fontSize}px`);
           text.setAttribute("fill", element.strokeColor);
           text.setAttribute("fill", element.strokeColor);

+ 1 - 1
src/tests/__snapshots__/linearElementEditor.test.tsx.snap

@@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
   class="excalidraw-wysiwyg"
   class="excalidraw-wysiwyg"
   data-type="wysiwyg"
   data-type="wysiwyg"
   dir="auto"
   dir="auto"
-  style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
+  style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
   tabindex="0"
   tabindex="0"
   wrap="off"
   wrap="off"
 />
 />

+ 100 - 0
src/tests/binding.test.tsx

@@ -4,6 +4,7 @@ import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { getTransformHandles } from "../element/transformHandles";
 import { getTransformHandles } from "../element/transformHandles";
 import { API } from "./helpers/api";
 import { API } from "./helpers/api";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
+import { actionCreateContainerFromText } from "../actions/actionBoundText";
 
 
 const { h } = window;
 const { h } = window;
 
 
@@ -209,4 +210,103 @@ describe("element binding", () => {
     ).toBe(null);
     ).toBe(null);
     expect(arrow.endBinding?.elementId).toBe(text.id);
     expect(arrow.endBinding?.elementId).toBe(text.id);
   });
   });
+
+  it("should update binding when text containerized", async () => {
+    const rectangle1 = API.createElement({
+      type: "rectangle",
+      id: "rectangle1",
+      width: 100,
+      height: 100,
+      boundElements: [
+        { id: "arrow1", type: "arrow" },
+        { id: "arrow2", type: "arrow" },
+      ],
+    });
+
+    const arrow1 = API.createElement({
+      type: "arrow",
+      id: "arrow1",
+      points: [
+        [0, 0],
+        [0, -87.45777932247563],
+      ],
+      startBinding: {
+        elementId: "rectangle1",
+        focus: 0.2,
+        gap: 7,
+      },
+      endBinding: {
+        elementId: "text1",
+        focus: 0.2,
+        gap: 7,
+      },
+    });
+
+    const arrow2 = API.createElement({
+      type: "arrow",
+      id: "arrow2",
+      points: [
+        [0, 0],
+        [0, -87.45777932247563],
+      ],
+      startBinding: {
+        elementId: "text1",
+        focus: 0.2,
+        gap: 7,
+      },
+      endBinding: {
+        elementId: "rectangle1",
+        focus: 0.2,
+        gap: 7,
+      },
+    });
+
+    const text1 = API.createElement({
+      type: "text",
+      id: "text1",
+      text: "ola",
+      boundElements: [
+        { id: "arrow1", type: "arrow" },
+        { id: "arrow2", type: "arrow" },
+      ],
+    });
+
+    h.elements = [rectangle1, arrow1, arrow2, text1];
+
+    API.setSelectedElements([text1]);
+
+    expect(h.state.selectedElementIds[text1.id]).toBe(true);
+
+    h.app.actionManager.executeAction(actionCreateContainerFromText);
+
+    // new text container will be placed before the text element
+    const container = h.elements.at(-2)!;
+
+    expect(container.type).toBe("rectangle");
+    expect(container.id).not.toBe(rectangle1.id);
+
+    expect(container).toEqual(
+      expect.objectContaining({
+        boundElements: expect.arrayContaining([
+          {
+            type: "text",
+            id: text1.id,
+          },
+          {
+            type: "arrow",
+            id: arrow1.id,
+          },
+          {
+            type: "arrow",
+            id: arrow2.id,
+          },
+        ]),
+      }),
+    );
+
+    expect(arrow1.startBinding?.elementId).toBe(rectangle1.id);
+    expect(arrow1.endBinding?.elementId).toBe(container.id);
+    expect(arrow2.startBinding?.elementId).toBe(container.id);
+    expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
+  });
 });
 });

+ 17 - 18
src/tests/clipboard.test.tsx

@@ -3,10 +3,13 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
 import { Pointer, Keyboard } from "./helpers/ui";
 import { Pointer, Keyboard } from "./helpers/ui";
 import ExcalidrawApp from "../excalidraw-app";
 import ExcalidrawApp from "../excalidraw-app";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
-import { getLineHeight } from "../element/textMeasurements";
-import { getFontString } from "../utils";
+
 import { getElementBounds } from "../element";
 import { getElementBounds } from "../element";
 import { NormalizedZoomValue } from "../types";
 import { NormalizedZoomValue } from "../types";
+import {
+  getDefaultLineHeight,
+  getLineHeightInPx,
+} from "../element/textMeasurements";
 
 
 const { h } = window;
 const { h } = window;
 
 
@@ -118,12 +121,10 @@ describe("paste text as single lines", () => {
 
 
   it("should space items correctly", async () => {
   it("should space items correctly", async () => {
     const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
     const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
-    const lineHeight =
-      getLineHeight(
-        getFontString({
-          fontSize: h.app.state.currentItemFontSize,
-          fontFamily: h.app.state.currentItemFontFamily,
-        }),
+    const lineHeightPx =
+      getLineHeightInPx(
+        h.app.state.currentItemFontSize,
+        getDefaultLineHeight(h.state.currentItemFontFamily),
       ) +
       ) +
       10 / h.app.state.zoom.value;
       10 / h.app.state.zoom.value;
     mouse.moveTo(100, 100);
     mouse.moveTo(100, 100);
@@ -135,19 +136,17 @@ describe("paste text as single lines", () => {
       for (let i = 1; i < h.elements.length; i++) {
       for (let i = 1; i < h.elements.length; i++) {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         const [fx, elY] = getElementBounds(h.elements[i]);
         const [fx, elY] = getElementBounds(h.elements[i]);
-        expect(elY).toEqual(firstElY + lineHeight * i);
+        expect(elY).toEqual(firstElY + lineHeightPx * i);
       }
       }
     });
     });
   });
   });
 
 
   it("should leave a space for blank new lines", async () => {
   it("should leave a space for blank new lines", async () => {
     const text = "hkhkjhki\n\njgkjhffjh";
     const text = "hkhkjhki\n\njgkjhffjh";
-    const lineHeight =
-      getLineHeight(
-        getFontString({
-          fontSize: h.app.state.currentItemFontSize,
-          fontFamily: h.app.state.currentItemFontFamily,
-        }),
+    const lineHeightPx =
+      getLineHeightInPx(
+        h.app.state.currentItemFontSize,
+        getDefaultLineHeight(h.state.currentItemFontFamily),
       ) +
       ) +
       10 / h.app.state.zoom.value;
       10 / h.app.state.zoom.value;
     mouse.moveTo(100, 100);
     mouse.moveTo(100, 100);
@@ -158,7 +157,7 @@ describe("paste text as single lines", () => {
       const [fx, firstElY] = getElementBounds(h.elements[0]);
       const [fx, firstElY] = getElementBounds(h.elements[0]);
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       const [lx, lastElY] = getElementBounds(h.elements[1]);
       const [lx, lastElY] = getElementBounds(h.elements[1]);
-      expect(lastElY).toEqual(firstElY + lineHeight * 2);
+      expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
     });
     });
   });
   });
 });
 });
@@ -224,7 +223,7 @@ describe("Paste bound text container", () => {
       await sleep(1);
       await sleep(1);
       expect(h.elements.length).toEqual(2);
       expect(h.elements.length).toEqual(2);
       const container = h.elements[0];
       const container = h.elements[0];
-      expect(container.height).toBe(354);
+      expect(container.height).toBe(368);
       expect(container.width).toBe(166);
       expect(container.width).toBe(166);
     });
     });
   });
   });
@@ -247,7 +246,7 @@ describe("Paste bound text container", () => {
       await sleep(1);
       await sleep(1);
       expect(h.elements.length).toEqual(2);
       expect(h.elements.length).toEqual(2);
       const container = h.elements[0];
       const container = h.elements[0];
-      expect(container.height).toBe(740);
+      expect(container.height).toBe(770);
       expect(container.width).toBe(166);
       expect(container.width).toBe(166);
     });
     });
   });
   });

+ 3 - 1
src/tests/data/__snapshots__/restore.test.ts.snap

@@ -291,6 +291,7 @@ Object {
   "height": 100,
   "height": 100,
   "id": "id-text01",
   "id": "id-text01",
   "isDeleted": false,
   "isDeleted": false,
+  "lineHeight": 1.25,
   "link": null,
   "link": null,
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
@@ -312,7 +313,7 @@ Object {
   "verticalAlign": "middle",
   "verticalAlign": "middle",
   "width": 100,
   "width": 100,
   "x": -20,
   "x": -20,
-  "y": -8.4,
+  "y": -8.75,
 }
 }
 `;
 `;
 
 
@@ -329,6 +330,7 @@ Object {
   "height": 100,
   "height": 100,
   "id": "id-text01",
   "id": "id-text01",
   "isDeleted": false,
   "isDeleted": false,
+  "lineHeight": 1.25,
   "link": null,
   "link": null,
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,

+ 11 - 2
src/tests/helpers/api.ts

@@ -111,6 +111,9 @@ export class API {
     fileId?: T extends "image" ? string : never;
     fileId?: T extends "image" ? string : never;
     scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
     scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
     status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
     status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
+    startBinding?: T extends "arrow"
+      ? ExcalidrawLinearElement["startBinding"]
+      : never;
     endBinding?: T extends "arrow"
     endBinding?: T extends "arrow"
       ? ExcalidrawLinearElement["endBinding"]
       ? ExcalidrawLinearElement["endBinding"]
       : never;
       : never;
@@ -178,11 +181,13 @@ export class API {
         });
         });
         break;
         break;
       case "text":
       case "text":
+        const fontSize = rest.fontSize ?? appState.currentItemFontSize;
+        const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
         element = newTextElement({
         element = newTextElement({
           ...base,
           ...base,
           text: rest.text || "test",
           text: rest.text || "test",
-          fontSize: rest.fontSize ?? appState.currentItemFontSize,
-          fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
+          fontSize,
+          fontFamily,
           textAlign: rest.textAlign ?? appState.currentItemTextAlign,
           textAlign: rest.textAlign ?? appState.currentItemTextAlign,
           verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
           verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
           containerId: rest.containerId ?? undefined,
           containerId: rest.containerId ?? undefined,
@@ -221,6 +226,10 @@ export class API {
         });
         });
         break;
         break;
     }
     }
+    if (element.type === "arrow") {
+      element.startBinding = rest.startBinding ?? null;
+      element.endBinding = rest.endBinding ?? null;
+    }
     if (id) {
     if (id) {
       element.id = id;
       element.id = id;
     }
     }

+ 7 - 7
src/tests/linearElementEditor.test.tsx

@@ -1031,7 +1031,7 @@ describe("Test Linear Elements", () => {
       expect({ width: container.width, height: container.height })
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
         .toMatchInlineSnapshot(`
         Object {
         Object {
-          "height": 128,
+          "height": 130,
           "width": 367,
           "width": 367,
         }
         }
       `);
       `);
@@ -1041,7 +1041,7 @@ describe("Test Linear Elements", () => {
       ).toMatchInlineSnapshot(`
       ).toMatchInlineSnapshot(`
         Object {
         Object {
           "x": 272,
           "x": 272,
-          "y": 46,
+          "y": 45,
         }
         }
       `);
       `);
       expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
       expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
@@ -1053,11 +1053,11 @@ describe("Test Linear Elements", () => {
         .toMatchInlineSnapshot(`
         .toMatchInlineSnapshot(`
         Array [
         Array [
           20,
           20,
-          36,
+          35,
           502,
           502,
-          94,
+          95,
           205.9061448421403,
           205.9061448421403,
-          53,
+          52.5,
         ]
         ]
       `);
       `);
     });
     });
@@ -1092,7 +1092,7 @@ describe("Test Linear Elements", () => {
       expect({ width: container.width, height: container.height })
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
         .toMatchInlineSnapshot(`
         Object {
         Object {
-          "height": 128,
+          "height": 130,
           "width": 340,
           "width": 340,
         }
         }
       `);
       `);
@@ -1102,7 +1102,7 @@ describe("Test Linear Elements", () => {
       ).toMatchInlineSnapshot(`
       ).toMatchInlineSnapshot(`
         Object {
         Object {
           "x": 75,
           "x": 75,
-          "y": -4,
+          "y": -5,
         }
         }
       `);
       `);
       expect(textElement.text).toMatchInlineSnapshot(`
       expect(textElement.text).toMatchInlineSnapshot(`