mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 04:44:31 +01:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			dwelle/ref
			...
			feat-add-e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					48c1f93e3e | ||
| 
						 | 
					3dd446dc15 | ||
| 
						 | 
					56c21529db | ||
| 
						 | 
					a13aed92f2 | ||
| 
						 | 
					134df7bfbb | ||
| 
						 | 
					5191cdbe26 | ||
| 
						 | 
					27fd150a20 | ||
| 
						 | 
					188921c247 | 
@@ -10,13 +10,13 @@ import { FONT_FAMILY } from "@excalidraw/excalidraw";
 | 
			
		||||
 | 
			
		||||
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
 | 
			
		||||
 | 
			
		||||
| Font Family  | Description                                 |
 | 
			
		||||
| ------------ | ------------------------------------------- |
 | 
			
		||||
| `HAND_DRAWN` | The handwritten font (by default, `Virgil`) |
 | 
			
		||||
| `NORMAL`     | The regular font (by default, `Helvetica`)  |
 | 
			
		||||
| `CODE`       | The code font (by default, `Cascadia`)      |
 | 
			
		||||
| Font Family | Description            |
 | 
			
		||||
| ----------- | ---------------------- |
 | 
			
		||||
| `Virgil`    | The `handwritten` font |
 | 
			
		||||
| `Helvetica` | The `Normal` Font      |
 | 
			
		||||
| `Cascadia`  | The `Code` Font        |
 | 
			
		||||
 | 
			
		||||
Defaults to `HAND_DRAWN` unless passed in `initialData.appState.currentItemFontFamily`.
 | 
			
		||||
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
 | 
			
		||||
 | 
			
		||||
### THEME
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								dev-docs/docs/codebase/json-schema.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								dev-docs/docs/codebase/json-schema.mdx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
# JSON Schema
 | 
			
		||||
 | 
			
		||||
The Excalidraw data format uses plaintext JSON.
 | 
			
		||||
 | 
			
		||||
## Excalidraw files
 | 
			
		||||
 | 
			
		||||
When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) is using the below format.
 | 
			
		||||
 | 
			
		||||
### Attributes
 | 
			
		||||
 | 
			
		||||
| Attribute | Description | Value |
 | 
			
		||||
| --- | --- | --- |
 | 
			
		||||
| `type` | The type of the Excalidraw schema | `"excalidraw"` |
 | 
			
		||||
| `version` | The version of the Excalidraw schema | number |
 | 
			
		||||
| `source` | The source URL of the Excalidraw application | `"https://excalidraw.com"` |
 | 
			
		||||
| `elements` | An array of objects representing excalidraw elements on canvas | Array containing excalidraw element objects |
 | 
			
		||||
| `appState` | Additional application state/configuration | Object containing application state properties |
 | 
			
		||||
| `files` | Data for excalidraw `image` elements | Object containing image data |
 | 
			
		||||
 | 
			
		||||
### JSON Schema example
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  // schema information
 | 
			
		||||
  "type": "excalidraw",
 | 
			
		||||
  "version": 2,
 | 
			
		||||
  "source": "https://excalidraw.com",
 | 
			
		||||
 | 
			
		||||
  // elements on canvas
 | 
			
		||||
  "elements": [
 | 
			
		||||
    // example element
 | 
			
		||||
    {
 | 
			
		||||
      "id": "pologsyG-tAraPgiN9xP9b",
 | 
			
		||||
      "type": "rectangle",
 | 
			
		||||
      "x": 928,
 | 
			
		||||
      "y": 319,
 | 
			
		||||
      "width": 134,
 | 
			
		||||
      "height": 90
 | 
			
		||||
      /* ...other element properties */
 | 
			
		||||
    }
 | 
			
		||||
    /* other elements */
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  // editor state (canvas config, preferences, ...)
 | 
			
		||||
  "appState": {
 | 
			
		||||
    "gridSize": null,
 | 
			
		||||
    "viewBackgroundColor": "#ffffff"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // files data for "image" elements, using format `{ [fileId]: fileData }`
 | 
			
		||||
  "files": {
 | 
			
		||||
    // example of an image data object
 | 
			
		||||
    "3cebd7720911620a3938ce77243696149da03861": {
 | 
			
		||||
      "mimeType": "image/png",
 | 
			
		||||
      "id": "3cebd7720911620a3938c.77243626149da03861",
 | 
			
		||||
      "dataURL": "data:image/png;base64,iVBORWOKGgoAAAANSUhEUgA=",
 | 
			
		||||
      "created": 1690295874454,
 | 
			
		||||
      "lastRetrieved": 1690295874454
 | 
			
		||||
    }
 | 
			
		||||
    /* ...other image data objects */
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Excalidraw clipboard format
 | 
			
		||||
 | 
			
		||||
When copying selected excalidraw elements to clipboard, the JSON schema is similar to `.excalidraw` format, except it differs in attributes.
 | 
			
		||||
 | 
			
		||||
### Attributes
 | 
			
		||||
 | 
			
		||||
| Attribute | Description | Example Value |
 | 
			
		||||
| --- | --- | --- |
 | 
			
		||||
| `type` | The type of the Excalidraw document. | "excalidraw/clipboard" |
 | 
			
		||||
| `elements` | An array of objects representing excalidraw elements on canvas. | Array containing excalidraw element objects (see example below) |
 | 
			
		||||
| `files` | Data for excalidraw `image` elements. | Object containing image data |
 | 
			
		||||
@@ -23,7 +23,6 @@ const sidebars = {
 | 
			
		||||
      },
 | 
			
		||||
      items: ["introduction/development", "introduction/contributing"],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      type: "category",
 | 
			
		||||
      label: "@excalidraw/excalidraw",
 | 
			
		||||
@@ -92,6 +91,11 @@ const sidebars = {
 | 
			
		||||
        "@excalidraw/excalidraw/development",
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: "category",
 | 
			
		||||
      label: "Codebase",
 | 
			
		||||
      items: ["codebase/json-schema"],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ import {
 | 
			
		||||
  computeBoundTextPosition,
 | 
			
		||||
  computeContainerDimensionForBoundText,
 | 
			
		||||
  getBoundTextElement,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  measureText,
 | 
			
		||||
  redrawTextBoundingBox,
 | 
			
		||||
} from "../element/textElement";
 | 
			
		||||
@@ -32,6 +31,7 @@ import {
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import { Mutable } from "../utility-types";
 | 
			
		||||
import { getFontString } from "../utils";
 | 
			
		||||
import { register } from "./register";
 | 
			
		||||
 | 
			
		||||
export const actionUnbindText = register({
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawLinearElement,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  FontFamilyId,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  TextAlign,
 | 
			
		||||
  VerticalAlign,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
@@ -689,22 +689,22 @@ export const actionChangeFontFamily = register({
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => {
 | 
			
		||||
    const options: {
 | 
			
		||||
      value: FontFamilyId;
 | 
			
		||||
      value: FontFamilyValues;
 | 
			
		||||
      text: string;
 | 
			
		||||
      icon: JSX.Element;
 | 
			
		||||
    }[] = [
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
 | 
			
		||||
        value: FONT_FAMILY.Virgil,
 | 
			
		||||
        text: t("labels.handDrawn"),
 | 
			
		||||
        icon: FreedrawIcon,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.NORMAL.fontFamilyId,
 | 
			
		||||
        value: FONT_FAMILY.Helvetica,
 | 
			
		||||
        text: t("labels.normal"),
 | 
			
		||||
        icon: FontFamilyNormalIcon,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.CODE.fontFamilyId,
 | 
			
		||||
        value: FONT_FAMILY.Cascadia,
 | 
			
		||||
        text: t("labels.code"),
 | 
			
		||||
        icon: FontFamilyCodeIcon,
 | 
			
		||||
      },
 | 
			
		||||
@@ -713,7 +713,7 @@ export const actionChangeFontFamily = register({
 | 
			
		||||
    return (
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>{t("labels.fontFamily")}</legend>
 | 
			
		||||
        <ButtonIconSelect<FontFamilyId | false>
 | 
			
		||||
        <ButtonIconSelect<FontFamilyValues | false>
 | 
			
		||||
          group="font-family"
 | 
			
		||||
          options={options}
 | 
			
		||||
          value={getFormValue(
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,7 @@ import {
 | 
			
		||||
import {
 | 
			
		||||
  debounce,
 | 
			
		||||
  distance,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getNearestScrollableContainer,
 | 
			
		||||
  isInputLike,
 | 
			
		||||
  isToolIcon,
 | 
			
		||||
@@ -297,7 +298,6 @@ import {
 | 
			
		||||
  getContainerCenter,
 | 
			
		||||
  getContainerElement,
 | 
			
		||||
  getDefaultLineHeight,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getLineHeightInPx,
 | 
			
		||||
  getTextBindableContainerAtPosition,
 | 
			
		||||
  isMeasureTextSupported,
 | 
			
		||||
@@ -6501,7 +6501,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            nextElements = updateFrameMembershipOfSelectedElements(
 | 
			
		||||
              this.scene.getElementsIncludingDeleted(),
 | 
			
		||||
              nextElements,
 | 
			
		||||
              this.state,
 | 
			
		||||
              this,
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -37,10 +37,25 @@ const StaticCanvas = (props: StaticCanvasProps) => {
 | 
			
		||||
      canvas.classList.add("excalidraw__canvas", "static");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    canvas.style.width = `${props.appState.width}px`;
 | 
			
		||||
    canvas.style.height = `${props.appState.height}px`;
 | 
			
		||||
    canvas.width = props.appState.width * props.scale;
 | 
			
		||||
    canvas.height = props.appState.height * props.scale;
 | 
			
		||||
    const widthString = `${props.appState.width}px`;
 | 
			
		||||
    const heightString = `${props.appState.height}px`;
 | 
			
		||||
    if (canvas.style.width !== widthString) {
 | 
			
		||||
      canvas.style.width = widthString;
 | 
			
		||||
    }
 | 
			
		||||
    if (canvas.style.height !== heightString) {
 | 
			
		||||
      canvas.style.height = heightString;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scaledWidth = props.appState.width * props.scale;
 | 
			
		||||
    const scaledHeight = props.appState.height * props.scale;
 | 
			
		||||
    // setting width/height resets the canvas even if dimensions not changed,
 | 
			
		||||
    // which would cause flicker when we skip frame (due to throttling)
 | 
			
		||||
    if (canvas.width !== scaledWidth) {
 | 
			
		||||
      canvas.width = scaledWidth;
 | 
			
		||||
    }
 | 
			
		||||
    if (canvas.height !== scaledHeight) {
 | 
			
		||||
      canvas.height = scaledHeight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderStaticScene(
 | 
			
		||||
      {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import cssVariables from "./css/variables.module.scss";
 | 
			
		||||
import { AppProps } from "./types";
 | 
			
		||||
import { ExcalidrawElement, FontFamilyId } from "./element/types";
 | 
			
		||||
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
 | 
			
		||||
import { COLOR_PALETTE } from "./colors";
 | 
			
		||||
 | 
			
		||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
 | 
			
		||||
@@ -94,19 +94,10 @@ export const CLASSES = {
 | 
			
		||||
 | 
			
		||||
// 1-based in case we ever do `if(element.fontFamily)`
 | 
			
		||||
export const FONT_FAMILY = {
 | 
			
		||||
  HAND_DRAWN: {
 | 
			
		||||
    fontFamilyId: 1,
 | 
			
		||||
    fontFamily: "Virgil",
 | 
			
		||||
  },
 | 
			
		||||
  NORMAL: {
 | 
			
		||||
    fontFamilyId: 2,
 | 
			
		||||
    fontFamily: "Helvetica",
 | 
			
		||||
  },
 | 
			
		||||
  CODE: {
 | 
			
		||||
    fontFamilyId: 3,
 | 
			
		||||
    fontFamily: "Cascadia",
 | 
			
		||||
  },
 | 
			
		||||
} as const;
 | 
			
		||||
  Virgil: 1,
 | 
			
		||||
  Helvetica: 2,
 | 
			
		||||
  Cascadia: 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const THEME = {
 | 
			
		||||
  LIGHT: "light",
 | 
			
		||||
@@ -128,8 +119,7 @@ export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
 | 
			
		||||
 | 
			
		||||
export const MIN_FONT_SIZE = 1;
 | 
			
		||||
export const DEFAULT_FONT_SIZE = 20;
 | 
			
		||||
export const DEFAULT_FONT_FAMILY: FontFamilyId =
 | 
			
		||||
  FONT_FAMILY.HAND_DRAWN.fontFamilyId;
 | 
			
		||||
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
 | 
			
		||||
export const DEFAULT_TEXT_ALIGN = "left";
 | 
			
		||||
export const DEFAULT_VERTICAL_ALIGN = "top";
 | 
			
		||||
export const DEFAULT_VERSION = "{version}";
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawSelectionElement,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  PointBinding,
 | 
			
		||||
  StrokeRoundness,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
@@ -21,9 +22,11 @@ import {
 | 
			
		||||
import { isTextElement, isUsingAdaptiveRadius } from "../element/typeChecks";
 | 
			
		||||
import { randomId } from "../random";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_FONT_FAMILY,
 | 
			
		||||
  DEFAULT_TEXT_ALIGN,
 | 
			
		||||
  DEFAULT_VERTICAL_ALIGN,
 | 
			
		||||
  PRECEDING_ELEMENT_KEY,
 | 
			
		||||
  FONT_FAMILY,
 | 
			
		||||
  ROUNDNESS,
 | 
			
		||||
  DEFAULT_SIDEBAR,
 | 
			
		||||
  DEFAULT_ELEMENT_PROPS,
 | 
			
		||||
@@ -31,14 +34,12 @@ import {
 | 
			
		||||
import { getDefaultAppState } from "../appState";
 | 
			
		||||
import { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
import { bumpVersion } from "../element/mutateElement";
 | 
			
		||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
 | 
			
		||||
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
 | 
			
		||||
import { arrayToMap } from "../utils";
 | 
			
		||||
import { MarkOptional, Mutable } from "../utility-types";
 | 
			
		||||
import {
 | 
			
		||||
  detectLineHeight,
 | 
			
		||||
  getDefaultLineHeight,
 | 
			
		||||
  getFontFamilyIdByName,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  measureBaseline,
 | 
			
		||||
} from "../element/textElement";
 | 
			
		||||
import { normalizeLink } from "./url";
 | 
			
		||||
@@ -74,6 +75,15 @@ export type RestoredDataState = {
 | 
			
		||||
  files: BinaryFiles;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
 | 
			
		||||
  if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) {
 | 
			
		||||
    return FONT_FAMILY[
 | 
			
		||||
      fontFamilyName as keyof typeof FONT_FAMILY
 | 
			
		||||
    ] as FontFamilyValues;
 | 
			
		||||
  }
 | 
			
		||||
  return DEFAULT_FONT_FAMILY;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const repairBinding = (binding: PointBinding | null) => {
 | 
			
		||||
  if (!binding) {
 | 
			
		||||
    return null;
 | 
			
		||||
@@ -82,7 +92,8 @@ const repairBinding = (binding: PointBinding | null) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const restoreElementWithProperties = <
 | 
			
		||||
  T extends Required<Omit<ExcalidrawElement, "customData">> & {
 | 
			
		||||
  T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
 | 
			
		||||
    subtype?: ExcalidrawElement["subtype"];
 | 
			
		||||
    customData?: ExcalidrawElement["customData"];
 | 
			
		||||
    /** @deprecated */
 | 
			
		||||
    boundElementIds?: readonly ExcalidrawElement["id"][];
 | 
			
		||||
@@ -148,6 +159,9 @@ const restoreElementWithProperties = <
 | 
			
		||||
    locked: element.locked ?? false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if ("subtype" in element) {
 | 
			
		||||
    base.subtype = element.subtype;
 | 
			
		||||
  }
 | 
			
		||||
  if ("customData" in element) {
 | 
			
		||||
    base.customData = element.customData;
 | 
			
		||||
  }
 | 
			
		||||
@@ -176,7 +190,7 @@ const restoreElement = (
 | 
			
		||||
          element as any
 | 
			
		||||
        ).font.split(" ");
 | 
			
		||||
        fontSize = parseFloat(fontPx);
 | 
			
		||||
        fontFamily = getFontFamilyIdByName(_fontFamily);
 | 
			
		||||
        fontFamily = getFontFamilyByName(_fontFamily);
 | 
			
		||||
      }
 | 
			
		||||
      const text = element.text ?? "";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ import {
 | 
			
		||||
} from "../element/newElement";
 | 
			
		||||
import {
 | 
			
		||||
  getDefaultLineHeight,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  measureText,
 | 
			
		||||
  normalizeText,
 | 
			
		||||
} from "../element/textElement";
 | 
			
		||||
@@ -34,12 +33,12 @@ import {
 | 
			
		||||
  ExcalidrawSelectionElement,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  FileId,
 | 
			
		||||
  FontFamilyId,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  TextAlign,
 | 
			
		||||
  VerticalAlign,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { MarkOptional } from "../utility-types";
 | 
			
		||||
import { assertNever } from "../utils";
 | 
			
		||||
import { assertNever, getFontString } from "../utils";
 | 
			
		||||
 | 
			
		||||
export type ValidLinearElement = {
 | 
			
		||||
  type: "arrow" | "line";
 | 
			
		||||
@@ -48,7 +47,7 @@ export type ValidLinearElement = {
 | 
			
		||||
  label?: {
 | 
			
		||||
    text: string;
 | 
			
		||||
    fontSize?: number;
 | 
			
		||||
    fontFamily?: FontFamilyId;
 | 
			
		||||
    fontFamily?: FontFamilyValues;
 | 
			
		||||
    textAlign?: TextAlign;
 | 
			
		||||
    verticalAlign?: VerticalAlign;
 | 
			
		||||
  } & MarkOptional<ElementConstructorOpts, "x" | "y">;
 | 
			
		||||
@@ -125,7 +124,7 @@ export type ValidContainer =
 | 
			
		||||
      label?: {
 | 
			
		||||
        text: string;
 | 
			
		||||
        fontSize?: number;
 | 
			
		||||
        fontFamily?: FontFamilyId;
 | 
			
		||||
        fontFamily?: FontFamilyValues;
 | 
			
		||||
        textAlign?: TextAlign;
 | 
			
		||||
        verticalAlign?: VerticalAlign;
 | 
			
		||||
      } & MarkOptional<ElementConstructorOpts, "x" | "y">;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ import { register } from "../actions/register";
 | 
			
		||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { ExcalidrawProps } from "../types";
 | 
			
		||||
import { setCursorForShape, updateActiveTool } from "../utils";
 | 
			
		||||
import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
 | 
			
		||||
import { newTextElement } from "./newElement";
 | 
			
		||||
import { getContainerElement, getFontString, wrapText } from "./textElement";
 | 
			
		||||
import { getContainerElement, wrapText } from "./textElement";
 | 
			
		||||
import { isEmbeddableElement } from "./typeChecks";
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
@@ -218,7 +218,7 @@ export const createPlaceholderEmbeddableLabel = (
 | 
			
		||||
    Math.min(element.width / 2, element.width / text.length),
 | 
			
		||||
    element.width / 30,
 | 
			
		||||
  );
 | 
			
		||||
  const fontFamily = FONT_FAMILY.NORMAL.fontFamilyId;
 | 
			
		||||
  const fontFamily = FONT_FAMILY.Helvetica;
 | 
			
		||||
 | 
			
		||||
  const fontString = getFontString({
 | 
			
		||||
    fontSize,
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,7 @@ describe("duplicating single elements", () => {
 | 
			
		||||
      opacity: 100,
 | 
			
		||||
      text: "hello",
 | 
			
		||||
      fontSize: 20,
 | 
			
		||||
      fontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
 | 
			
		||||
      fontFamily: FONT_FAMILY.Virgil,
 | 
			
		||||
      textAlign: "left",
 | 
			
		||||
      verticalAlign: "top",
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,17 @@ import {
 | 
			
		||||
  VerticalAlign,
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  FontFamilyId,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  ExcalidrawTextContainer,
 | 
			
		||||
  ExcalidrawFrameElement,
 | 
			
		||||
  ExcalidrawEmbeddableElement,
 | 
			
		||||
} from "../element/types";
 | 
			
		||||
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
 | 
			
		||||
import {
 | 
			
		||||
  arrayToMap,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getUpdatedTimestamp,
 | 
			
		||||
  isTestEnv,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
import { randomInteger, randomId } from "../random";
 | 
			
		||||
import { bumpVersion, newElementWith } from "./mutateElement";
 | 
			
		||||
import { getNewGroupIdsForDuplication } from "../groups";
 | 
			
		||||
@@ -30,7 +35,6 @@ import {
 | 
			
		||||
  wrapText,
 | 
			
		||||
  getBoundTextMaxWidth,
 | 
			
		||||
  getDefaultLineHeight,
 | 
			
		||||
  getFontString,
 | 
			
		||||
} from "./textElement";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_ELEMENT_PROPS,
 | 
			
		||||
@@ -180,7 +184,7 @@ export const newTextElement = (
 | 
			
		||||
  opts: {
 | 
			
		||||
    text: string;
 | 
			
		||||
    fontSize?: number;
 | 
			
		||||
    fontFamily?: FontFamilyId;
 | 
			
		||||
    fontFamily?: FontFamilyValues;
 | 
			
		||||
    textAlign?: TextAlign;
 | 
			
		||||
    verticalAlign?: VerticalAlign;
 | 
			
		||||
    containerId?: ExcalidrawTextContainer["id"] | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ import {
 | 
			
		||||
  isTextElement,
 | 
			
		||||
} from "./typeChecks";
 | 
			
		||||
import { mutateElement } from "./mutateElement";
 | 
			
		||||
import { getFontString } from "../utils";
 | 
			
		||||
import { updateBoundElements } from "./binding";
 | 
			
		||||
import {
 | 
			
		||||
  TransformHandleType,
 | 
			
		||||
@@ -52,7 +53,6 @@ import {
 | 
			
		||||
  getApproxMinLineHeight,
 | 
			
		||||
  measureText,
 | 
			
		||||
  getBoundTextMaxHeight,
 | 
			
		||||
  getFontString,
 | 
			
		||||
} from "./textElement";
 | 
			
		||||
import { LinearElementEditor } from "./linearElementEditor";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -427,6 +427,6 @@ describe("Test getDefaultLineHeight", () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should return correct line height", () => {
 | 
			
		||||
    expect(getDefaultLineHeight(FONT_FAMILY.CODE.fontFamilyId)).toBe(1.2);
 | 
			
		||||
    expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { arrayToMap, isTestEnv } from "../utils";
 | 
			
		||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
 | 
			
		||||
import {
 | 
			
		||||
  ExcalidrawElement,
 | 
			
		||||
  ExcalidrawTextContainer,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
  ExcalidrawTextElementWithContainer,
 | 
			
		||||
  FontFamilyId,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  FontString,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
} from "./types";
 | 
			
		||||
@@ -19,7 +19,6 @@ import {
 | 
			
		||||
  isSafari,
 | 
			
		||||
  TEXT_ALIGN,
 | 
			
		||||
  VERTICAL_ALIGN,
 | 
			
		||||
  WINDOWS_EMOJI_FALLBACK_FONT,
 | 
			
		||||
} from "../constants";
 | 
			
		||||
import { MaybeTransformHandleType } from "./transformHandles";
 | 
			
		||||
import Scene from "../scene/Scene";
 | 
			
		||||
@@ -968,57 +967,17 @@ export const isMeasureTextSupported = () => {
 | 
			
		||||
const DEFAULT_LINE_HEIGHT = {
 | 
			
		||||
  // ~1.25 is the average for Virgil in WebKit and Blink.
 | 
			
		||||
  // Gecko (FF) uses ~1.28.
 | 
			
		||||
  [FONT_FAMILY.HAND_DRAWN.fontFamilyId]:
 | 
			
		||||
    1.25 as ExcalidrawTextElement["lineHeight"],
 | 
			
		||||
  [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.NORMAL.fontFamilyId]:
 | 
			
		||||
    1.15 as ExcalidrawTextElement["lineHeight"],
 | 
			
		||||
  [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.CODE.fontFamilyId]: 1.2 as ExcalidrawTextElement["lineHeight"],
 | 
			
		||||
  [FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getDefaultLineHeight = (fontId: number) => {
 | 
			
		||||
  if (fontId in DEFAULT_LINE_HEIGHT) {
 | 
			
		||||
    return (
 | 
			
		||||
      DEFAULT_LINE_HEIGHT as Record<number, ExcalidrawTextElement["lineHeight"]>
 | 
			
		||||
    )[fontId];
 | 
			
		||||
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
 | 
			
		||||
  if (fontFamily in DEFAULT_LINE_HEIGHT) {
 | 
			
		||||
    return DEFAULT_LINE_HEIGHT[fontFamily];
 | 
			
		||||
  }
 | 
			
		||||
  return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getFontFamilyIdByName = (fontFamilyName: string): FontFamilyId => {
 | 
			
		||||
  for (const key in FONT_FAMILY) {
 | 
			
		||||
    const font = FONT_FAMILY[key as keyof typeof FONT_FAMILY];
 | 
			
		||||
    if (font.fontFamily === fontFamilyName) {
 | 
			
		||||
      return font.fontFamilyId;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return DEFAULT_FONT_FAMILY;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getFontFamilyString = ({
 | 
			
		||||
  fontFamily,
 | 
			
		||||
}: {
 | 
			
		||||
  fontFamily: FontFamilyId;
 | 
			
		||||
}) => {
 | 
			
		||||
  for (const key in FONT_FAMILY) {
 | 
			
		||||
    const font = FONT_FAMILY[key as keyof typeof FONT_FAMILY];
 | 
			
		||||
    if (font.fontFamilyId === fontFamily) {
 | 
			
		||||
      return `${font.fontFamily}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return WINDOWS_EMOJI_FALLBACK_FONT;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** returns fontSize+fontFamily string for assignment to DOM elements */
 | 
			
		||||
export const getFontString = ({
 | 
			
		||||
  fontSize,
 | 
			
		||||
  fontFamily,
 | 
			
		||||
}: {
 | 
			
		||||
  fontSize: number;
 | 
			
		||||
  fontFamily: FontFamilyId;
 | 
			
		||||
}) => {
 | 
			
		||||
  return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -798,7 +798,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
      updateTextEditor(editor, "Hello World!");
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(text.fontFamily).toEqual(FONT_FAMILY.HAND_DRAWN.fontFamilyId);
 | 
			
		||||
      expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
 | 
			
		||||
      UI.clickTool("text");
 | 
			
		||||
 | 
			
		||||
      mouse.clickAt(
 | 
			
		||||
@@ -815,7 +815,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      editor.blur();
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.CODE.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Cascadia);
 | 
			
		||||
 | 
			
		||||
      //undo
 | 
			
		||||
      Keyboard.withModifierKeys({ ctrl: true }, () => {
 | 
			
		||||
@@ -823,7 +823,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      });
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.HAND_DRAWN.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Virgil);
 | 
			
		||||
 | 
			
		||||
      //redo
 | 
			
		||||
      Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
 | 
			
		||||
@@ -831,7 +831,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      });
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.CODE.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Cascadia);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should wrap text and vertcially center align once text submitted", async () => {
 | 
			
		||||
@@ -1220,7 +1220,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.CODE.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Cascadia);
 | 
			
		||||
      expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
 | 
			
		||||
 | 
			
		||||
      fireEvent.click(screen.getByTitle(/Very large/i));
 | 
			
		||||
@@ -1247,7 +1247,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      fireEvent.click(screen.getByTitle(/code/i));
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.CODE.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Cascadia);
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
 | 
			
		||||
      ).toEqual(1.2);
 | 
			
		||||
@@ -1255,7 +1255,7 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      fireEvent.click(screen.getByTitle(/normal/i));
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
 | 
			
		||||
      ).toEqual(FONT_FAMILY.NORMAL.fontFamilyId);
 | 
			
		||||
      ).toEqual(FONT_FAMILY.Helvetica);
 | 
			
		||||
      expect(
 | 
			
		||||
        (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
 | 
			
		||||
      ).toEqual(1.15);
 | 
			
		||||
@@ -1509,4 +1509,30 @@ describe("textWysiwyg", () => {
 | 
			
		||||
      expect(text.text).toBe("Excalidraw");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should bump the version of labelled arrow when label updated", async () => {
 | 
			
		||||
    await render(<ExcalidrawApp />);
 | 
			
		||||
    const arrow = UI.createElement("arrow", {
 | 
			
		||||
      width: 300,
 | 
			
		||||
      height: 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    mouse.select(arrow);
 | 
			
		||||
    Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
    let editor = getTextEditor();
 | 
			
		||||
    await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
    updateTextEditor(editor, "Hello");
 | 
			
		||||
    editor.blur();
 | 
			
		||||
 | 
			
		||||
    const { version } = arrow;
 | 
			
		||||
 | 
			
		||||
    mouse.select(arrow);
 | 
			
		||||
    Keyboard.keyPress(KEYS.ENTER);
 | 
			
		||||
    editor = getTextEditor();
 | 
			
		||||
    await new Promise((r) => setTimeout(r, 0));
 | 
			
		||||
    updateTextEditor(editor, "Hello\nworld!");
 | 
			
		||||
    editor.blur();
 | 
			
		||||
 | 
			
		||||
    expect(arrow.version).toEqual(version + 1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,10 @@
 | 
			
		||||
import { CODES, KEYS } from "../keys";
 | 
			
		||||
import { isWritableElement, isTestEnv } from "../utils";
 | 
			
		||||
import {
 | 
			
		||||
  isWritableElement,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getFontFamilyString,
 | 
			
		||||
  isTestEnv,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
import Scene from "../scene/Scene";
 | 
			
		||||
import {
 | 
			
		||||
  isArrowElement,
 | 
			
		||||
@@ -15,7 +20,7 @@ import {
 | 
			
		||||
  ExcalidrawTextContainer,
 | 
			
		||||
} from "./types";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import { mutateElement } from "./mutateElement";
 | 
			
		||||
import { bumpVersion, mutateElement } from "./mutateElement";
 | 
			
		||||
import {
 | 
			
		||||
  getBoundTextElementId,
 | 
			
		||||
  getContainerElement,
 | 
			
		||||
@@ -29,8 +34,6 @@ import {
 | 
			
		||||
  computeContainerDimensionForBoundText,
 | 
			
		||||
  detectLineHeight,
 | 
			
		||||
  computeBoundTextPosition,
 | 
			
		||||
  getFontString,
 | 
			
		||||
  getFontFamilyString,
 | 
			
		||||
} from "./textElement";
 | 
			
		||||
import {
 | 
			
		||||
  actionDecreaseFontSize,
 | 
			
		||||
@@ -538,6 +541,9 @@ export const textWysiwyg = ({
 | 
			
		||||
              id: element.id,
 | 
			
		||||
            }),
 | 
			
		||||
          });
 | 
			
		||||
        } else if (isArrowElement(container)) {
 | 
			
		||||
          // updating an arrow label may change bounds, prevent stale cache:
 | 
			
		||||
          bumpVersion(container);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        mutateElement(container, {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ import { MarkNonNullable, ValueOf } from "../utility-types";
 | 
			
		||||
 | 
			
		||||
export type ChartType = "bar" | "line";
 | 
			
		||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
 | 
			
		||||
export type FontFamilyId =
 | 
			
		||||
  typeof FONT_FAMILY[keyof typeof FONT_FAMILY]["fontFamilyId"];
 | 
			
		||||
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
 | 
			
		||||
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
 | 
			
		||||
export type Theme = typeof THEME[keyof typeof THEME];
 | 
			
		||||
export type FontString = string & { _brand: "fontString" };
 | 
			
		||||
export type GroupId = string;
 | 
			
		||||
@@ -65,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
 | 
			
		||||
  updated: number;
 | 
			
		||||
  link: string | null;
 | 
			
		||||
  locked: boolean;
 | 
			
		||||
  subtype?: string;
 | 
			
		||||
  customData?: Record<string, any>;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
@@ -150,7 +151,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
 | 
			
		||||
  Readonly<{
 | 
			
		||||
    type: "text";
 | 
			
		||||
    fontSize: number;
 | 
			
		||||
    fontFamily: FontFamilyId;
 | 
			
		||||
    fontFamily: FontFamilyValues;
 | 
			
		||||
    text: string;
 | 
			
		||||
    baseline: number;
 | 
			
		||||
    textAlign: TextAlign;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import { ExcalidrawElement } from "./element/types";
 | 
			
		||||
import {
 | 
			
		||||
  convertToExcalidrawElements,
 | 
			
		||||
  Excalidraw,
 | 
			
		||||
} from "./packages/excalidraw/index";
 | 
			
		||||
import { API } from "./tests/helpers/api";
 | 
			
		||||
import { Pointer } from "./tests/helpers/ui";
 | 
			
		||||
import { Keyboard, Pointer } from "./tests/helpers/ui";
 | 
			
		||||
import { render } from "./tests/test-utils";
 | 
			
		||||
 | 
			
		||||
const { h } = window;
 | 
			
		||||
@@ -28,83 +29,301 @@ describe("adding elements to frames", () => {
 | 
			
		||||
    }, []);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  describe("resizing frame over elements", () => {
 | 
			
		||||
    const testElements = async (
 | 
			
		||||
      containerType: "arrow" | "rectangle",
 | 
			
		||||
      initialOrder: ElementType[],
 | 
			
		||||
      expectedOrder: ElementType[],
 | 
			
		||||
    ) => {
 | 
			
		||||
      await render(<Excalidraw />);
 | 
			
		||||
  function resizeFrameOverElement(
 | 
			
		||||
    frame: ExcalidrawElement,
 | 
			
		||||
    element: ExcalidrawElement,
 | 
			
		||||
  ) {
 | 
			
		||||
    mouse.clickAt(0, 0);
 | 
			
		||||
    mouse.downAt(frame.x + frame.width, frame.y + frame.height);
 | 
			
		||||
    mouse.moveTo(
 | 
			
		||||
      element.x + element.width + 50,
 | 
			
		||||
      element.y + element.height + 50,
 | 
			
		||||
    );
 | 
			
		||||
    mouse.up();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      const frame = API.createElement({ type: "frame", x: 0, y: 0 });
 | 
			
		||||
  function dragElementIntoFrame(
 | 
			
		||||
    frame: ExcalidrawElement,
 | 
			
		||||
    element: ExcalidrawElement,
 | 
			
		||||
  ) {
 | 
			
		||||
    mouse.clickAt(element.x, element.y);
 | 
			
		||||
    mouse.downAt(element.x + element.width / 2, element.y + element.height / 2);
 | 
			
		||||
    mouse.moveTo(frame.x + frame.width / 2, frame.y + frame.height / 2);
 | 
			
		||||
    mouse.up();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      h.elements = reorderElements(
 | 
			
		||||
        [
 | 
			
		||||
          frame,
 | 
			
		||||
          ...convertToExcalidrawElements([
 | 
			
		||||
            {
 | 
			
		||||
              type: containerType,
 | 
			
		||||
              x: 100,
 | 
			
		||||
              y: 100,
 | 
			
		||||
              height: 10,
 | 
			
		||||
              label: { text: "xx" },
 | 
			
		||||
            },
 | 
			
		||||
          ]),
 | 
			
		||||
        ],
 | 
			
		||||
        initialOrder,
 | 
			
		||||
      );
 | 
			
		||||
  function selectElementAndDuplicate(
 | 
			
		||||
    element: ExcalidrawElement,
 | 
			
		||||
    moveTo: [number, number] = [element.x + 25, element.y + 25],
 | 
			
		||||
  ) {
 | 
			
		||||
    const [x, y] = [
 | 
			
		||||
      element.x + element.width / 2,
 | 
			
		||||
      element.y + element.height / 2,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
      assertOrder(h.elements, initialOrder);
 | 
			
		||||
 | 
			
		||||
      expect(h.elements[1].frameId).toBe(null);
 | 
			
		||||
      expect(h.elements[2].frameId).toBe(null);
 | 
			
		||||
 | 
			
		||||
      const container = h.elements[1];
 | 
			
		||||
 | 
			
		||||
      mouse.clickAt(0, 0);
 | 
			
		||||
      mouse.downAt(frame.x + frame.width, frame.y + frame.height);
 | 
			
		||||
      mouse.moveTo(
 | 
			
		||||
        container.x + container.width + 100,
 | 
			
		||||
        container.y + container.height + 100,
 | 
			
		||||
      );
 | 
			
		||||
    Keyboard.withModifierKeys({ alt: true }, () => {
 | 
			
		||||
      mouse.downAt(x, y);
 | 
			
		||||
      mouse.moveTo(moveTo[0], moveTo[1]);
 | 
			
		||||
      mouse.up();
 | 
			
		||||
      assertOrder(h.elements, expectedOrder);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      expect(h.elements[0].frameId).toBe(frame.id);
 | 
			
		||||
      expect(h.elements[1].frameId).toBe(frame.id);
 | 
			
		||||
    };
 | 
			
		||||
  function expectEqualIds(expected: ExcalidrawElement[]) {
 | 
			
		||||
    expect(h.elements.map((x) => x.id)).toEqual(expected.map((x) => x.id));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    it("resizing over text containers / labelled arrows", async () => {
 | 
			
		||||
      await testElements(
 | 
			
		||||
  let frame: ExcalidrawElement;
 | 
			
		||||
  let rect1: ExcalidrawElement;
 | 
			
		||||
  let rect2: ExcalidrawElement;
 | 
			
		||||
  let rect3: ExcalidrawElement;
 | 
			
		||||
  let rect4: ExcalidrawElement;
 | 
			
		||||
  let text: ExcalidrawElement;
 | 
			
		||||
  let arrow: ExcalidrawElement;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await render(<Excalidraw />);
 | 
			
		||||
 | 
			
		||||
    frame = API.createElement({ id: "id0", type: "frame", x: 0, width: 150 });
 | 
			
		||||
    rect1 = API.createElement({
 | 
			
		||||
      id: "id1",
 | 
			
		||||
      type: "rectangle",
 | 
			
		||||
      x: -1000,
 | 
			
		||||
    });
 | 
			
		||||
    rect2 = API.createElement({
 | 
			
		||||
      id: "id2",
 | 
			
		||||
      type: "rectangle",
 | 
			
		||||
      x: 200,
 | 
			
		||||
      width: 50,
 | 
			
		||||
    });
 | 
			
		||||
    rect3 = API.createElement({
 | 
			
		||||
      id: "id3",
 | 
			
		||||
      type: "rectangle",
 | 
			
		||||
      x: 400,
 | 
			
		||||
      width: 50,
 | 
			
		||||
    });
 | 
			
		||||
    rect4 = API.createElement({
 | 
			
		||||
      id: "id4",
 | 
			
		||||
      type: "rectangle",
 | 
			
		||||
      x: 1000,
 | 
			
		||||
      width: 50,
 | 
			
		||||
    });
 | 
			
		||||
    text = API.createElement({
 | 
			
		||||
      id: "id5",
 | 
			
		||||
      type: "text",
 | 
			
		||||
      x: 100,
 | 
			
		||||
    });
 | 
			
		||||
    arrow = API.createElement({
 | 
			
		||||
      id: "id6",
 | 
			
		||||
      type: "arrow",
 | 
			
		||||
      x: 100,
 | 
			
		||||
      boundElements: [{ id: text.id, type: "text" }],
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const commonTestCases = async (
 | 
			
		||||
    func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
 | 
			
		||||
  ) => {
 | 
			
		||||
    describe("when frame is in a layer below", async () => {
 | 
			
		||||
      it("should add an element", async () => {
 | 
			
		||||
        h.elements = [frame, rect2];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
 | 
			
		||||
        expect(h.elements[0].frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements", async () => {
 | 
			
		||||
        h.elements = [frame, rect2, rect3];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, rect3, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other other elements in between", async () => {
 | 
			
		||||
        h.elements = [frame, rect1, rect2, rect4, rect3];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, rect3, frame, rect1, rect4]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other elements in between and the order is reversed", async () => {
 | 
			
		||||
        h.elements = [frame, rect3, rect4, rect2, rect1];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, rect3, frame, rect4, rect1]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("when frame is in a layer above", async () => {
 | 
			
		||||
      it("should add an element", async () => {
 | 
			
		||||
        h.elements = [rect2, frame];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
 | 
			
		||||
        expect(h.elements[0].frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements", async () => {
 | 
			
		||||
        h.elements = [rect2, rect3, frame];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect3, rect2, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other other elements in between", async () => {
 | 
			
		||||
        h.elements = [rect1, rect2, rect4, rect3, frame];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect1, rect4, rect3, rect2, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other elements in between and the order is reversed", async () => {
 | 
			
		||||
        h.elements = [rect3, rect4, rect2, rect1, frame];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect4, rect1, rect3, rect2, frame]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("when frame is in an inner layer", async () => {
 | 
			
		||||
      it("should add elements", async () => {
 | 
			
		||||
        h.elements = [rect2, frame, rect3];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect2, rect3, frame]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other other elements in between", async () => {
 | 
			
		||||
        h.elements = [rect2, rect1, frame, rect4, rect3];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect1, rect2, rect3, frame, rect4]);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("should add elements when there are other elements in between and the order is reversed", async () => {
 | 
			
		||||
        h.elements = [rect3, rect4, frame, rect2, rect1];
 | 
			
		||||
 | 
			
		||||
        func(frame, rect2);
 | 
			
		||||
        func(frame, rect3);
 | 
			
		||||
 | 
			
		||||
        expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
        expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
        expectEqualIds([rect4, rect3, rect2, frame, rect1]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const resizingTest = async (
 | 
			
		||||
    containerType: "arrow" | "rectangle",
 | 
			
		||||
    initialOrder: ElementType[],
 | 
			
		||||
    expectedOrder: ElementType[],
 | 
			
		||||
  ) => {
 | 
			
		||||
    await render(<Excalidraw />);
 | 
			
		||||
 | 
			
		||||
    const frame = API.createElement({ type: "frame", x: 0, y: 0 });
 | 
			
		||||
 | 
			
		||||
    h.elements = reorderElements(
 | 
			
		||||
      [
 | 
			
		||||
        frame,
 | 
			
		||||
        ...convertToExcalidrawElements([
 | 
			
		||||
          {
 | 
			
		||||
            type: containerType,
 | 
			
		||||
            x: 100,
 | 
			
		||||
            y: 100,
 | 
			
		||||
            height: 10,
 | 
			
		||||
            label: { text: "xx" },
 | 
			
		||||
          },
 | 
			
		||||
        ]),
 | 
			
		||||
      ],
 | 
			
		||||
      initialOrder,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    assertOrder(h.elements, initialOrder);
 | 
			
		||||
 | 
			
		||||
    expect(h.elements[1].frameId).toBe(null);
 | 
			
		||||
    expect(h.elements[2].frameId).toBe(null);
 | 
			
		||||
 | 
			
		||||
    const container = h.elements[1];
 | 
			
		||||
 | 
			
		||||
    resizeFrameOverElement(frame, container);
 | 
			
		||||
    assertOrder(h.elements, expectedOrder);
 | 
			
		||||
 | 
			
		||||
    expect(h.elements[0].frameId).toBe(frame.id);
 | 
			
		||||
    expect(h.elements[1].frameId).toBe(frame.id);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  describe("resizing frame over elements", async () => {
 | 
			
		||||
    await commonTestCases(resizeFrameOverElement);
 | 
			
		||||
 | 
			
		||||
    it("resizing over text containers and labelled arrows", async () => {
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "rectangle",
 | 
			
		||||
        ["frame", "rectangle", "text"],
 | 
			
		||||
        ["rectangle", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
      await testElements(
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "rectangle",
 | 
			
		||||
        ["frame", "text", "rectangle"],
 | 
			
		||||
        ["rectangle", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
      await testElements(
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "rectangle",
 | 
			
		||||
        ["rectangle", "text", "frame"],
 | 
			
		||||
        ["rectangle", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
      await testElements(
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "rectangle",
 | 
			
		||||
        ["text", "rectangle", "frame"],
 | 
			
		||||
        ["text", "rectangle", "frame"],
 | 
			
		||||
        ["rectangle", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      await testElements(
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "arrow",
 | 
			
		||||
        ["frame", "arrow", "text"],
 | 
			
		||||
        ["arrow", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
      await testElements(
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "arrow",
 | 
			
		||||
        ["text", "arrow", "frame"],
 | 
			
		||||
        ["text", "arrow", "frame"],
 | 
			
		||||
        ["arrow", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
      await resizingTest(
 | 
			
		||||
        "arrow",
 | 
			
		||||
        ["frame", "arrow", "text"],
 | 
			
		||||
        ["arrow", "text", "frame"],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // FIXME failing in tests (it fails to add elements to frame for some
 | 
			
		||||
@@ -118,11 +337,104 @@ describe("adding elements to frames", () => {
 | 
			
		||||
      //   ["arrow", "text", "frame"],
 | 
			
		||||
      //   ["arrow", "text", "frame"],
 | 
			
		||||
      // );
 | 
			
		||||
      // await testElements(
 | 
			
		||||
      //   "arrow",
 | 
			
		||||
      //   ["frame", "text", "arrow"],
 | 
			
		||||
      //   ["text", "arrow", "frame"],
 | 
			
		||||
      // );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should add arrow bound with text when frame is in a layer below", async () => {
 | 
			
		||||
      h.elements = [frame, arrow, text];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, arrow);
 | 
			
		||||
 | 
			
		||||
      expect(arrow.frameId).toBe(frame.id);
 | 
			
		||||
      expect(text.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([arrow, text, frame]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should add arrow bound with text when frame is in a layer above", async () => {
 | 
			
		||||
      h.elements = [arrow, text, frame];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, arrow);
 | 
			
		||||
 | 
			
		||||
      expect(arrow.frameId).toBe(frame.id);
 | 
			
		||||
      expect(text.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([arrow, text, frame]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should add arrow bound with text when frame is in an inner layer", async () => {
 | 
			
		||||
      h.elements = [arrow, frame, text];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, arrow);
 | 
			
		||||
 | 
			
		||||
      expect(arrow.frameId).toBe(frame.id);
 | 
			
		||||
      expect(text.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([arrow, text, frame]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("resizing frame over elements but downwards", async () => {
 | 
			
		||||
    it("should add elements when frame is in a layer below", async () => {
 | 
			
		||||
      h.elements = [frame, rect1, rect2, rect3, rect4];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, rect4);
 | 
			
		||||
      resizeFrameOverElement(frame, rect3);
 | 
			
		||||
 | 
			
		||||
      expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
      expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([rect2, rect3, frame, rect4, rect1]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should add elements when frame is in a layer above", async () => {
 | 
			
		||||
      h.elements = [rect1, rect2, rect3, rect4, frame];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, rect4);
 | 
			
		||||
      resizeFrameOverElement(frame, rect3);
 | 
			
		||||
 | 
			
		||||
      expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
      expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([rect1, rect2, rect3, frame, rect4]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should add elements when frame is in an inner layer", async () => {
 | 
			
		||||
      h.elements = [rect1, rect2, frame, rect3, rect4];
 | 
			
		||||
 | 
			
		||||
      resizeFrameOverElement(frame, rect4);
 | 
			
		||||
      resizeFrameOverElement(frame, rect3);
 | 
			
		||||
 | 
			
		||||
      expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
      expect(rect3.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([rect1, rect2, rect3, frame, rect4]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("dragging elements into the frame", async () => {
 | 
			
		||||
    await commonTestCases(dragElementIntoFrame);
 | 
			
		||||
 | 
			
		||||
    it("should drag element inside, duplicate it and keep it in frame", () => {
 | 
			
		||||
      h.elements = [frame, rect2];
 | 
			
		||||
 | 
			
		||||
      dragElementIntoFrame(frame, rect2);
 | 
			
		||||
 | 
			
		||||
      const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
 | 
			
		||||
 | 
			
		||||
      selectElementAndDuplicate(rect2);
 | 
			
		||||
 | 
			
		||||
      expect(rect2_copy.frameId).toBe(frame.id);
 | 
			
		||||
      expect(rect2.frameId).toBe(frame.id);
 | 
			
		||||
      expectEqualIds([rect2_copy, rect2, frame]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should drag element inside, duplicate it and remove it from frame", () => {
 | 
			
		||||
      h.elements = [frame, rect2];
 | 
			
		||||
 | 
			
		||||
      dragElementIntoFrame(frame, rect2);
 | 
			
		||||
 | 
			
		||||
      const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
 | 
			
		||||
 | 
			
		||||
      // move the rect2 outside the frame
 | 
			
		||||
      selectElementAndDuplicate(rect2, [-1000, -1000]);
 | 
			
		||||
 | 
			
		||||
      expect(rect2_copy.frameId).toBe(frame.id);
 | 
			
		||||
      expect(rect2.frameId).toBe(null);
 | 
			
		||||
      expectEqualIds([rect2_copy, frame, rect2]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								src/frame.ts
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								src/frame.ts
									
									
									
									
									
								
							@@ -468,14 +468,39 @@ export const addElementsToFrame = (
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let nextElements = allElements.slice();
 | 
			
		||||
  const allElementsIndex = allElements.reduce(
 | 
			
		||||
    (acc: Record<string, number>, element, index) => {
 | 
			
		||||
      acc[element.id] = index;
 | 
			
		||||
      return acc;
 | 
			
		||||
    },
 | 
			
		||||
    {},
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const frameIndex = allElementsIndex[frame.id];
 | 
			
		||||
  // need to be calculated before the mutation below occurs
 | 
			
		||||
  const leftFrameBoundaryIndex = findIndex(
 | 
			
		||||
    allElements,
 | 
			
		||||
    (e) => e.frameId === frame.id,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const existingFrameChildren = allElements.filter(
 | 
			
		||||
    (element) => element.frameId === frame.id,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const addedFrameChildren_left: ExcalidrawElement[] = [];
 | 
			
		||||
  const addedFrameChildren_right: ExcalidrawElement[] = [];
 | 
			
		||||
 | 
			
		||||
  const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
 | 
			
		||||
  for (const element of omitGroupsContainingFrames(
 | 
			
		||||
    allElements,
 | 
			
		||||
    _elementsToAdd,
 | 
			
		||||
  )) {
 | 
			
		||||
    if (element.frameId !== frame.id && !isFrameElement(element)) {
 | 
			
		||||
      if (allElementsIndex[element.id] > frameIndex) {
 | 
			
		||||
        addedFrameChildren_right.push(element);
 | 
			
		||||
      } else {
 | 
			
		||||
        addedFrameChildren_left.push(element);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      mutateElement(
 | 
			
		||||
        element,
 | 
			
		||||
        {
 | 
			
		||||
@@ -483,28 +508,35 @@ export const addElementsToFrame = (
 | 
			
		||||
        },
 | 
			
		||||
        false,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
 | 
			
		||||
      const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
 | 
			
		||||
 | 
			
		||||
      if (elementIndex < frameBoundary) {
 | 
			
		||||
        nextElements = [
 | 
			
		||||
          ...nextElements.slice(0, elementIndex),
 | 
			
		||||
          ...nextElements.slice(elementIndex + 1, frameBoundary),
 | 
			
		||||
          element,
 | 
			
		||||
          ...nextElements.slice(frameBoundary),
 | 
			
		||||
        ];
 | 
			
		||||
      } else if (elementIndex > frameIndex) {
 | 
			
		||||
        nextElements = [
 | 
			
		||||
          ...nextElements.slice(0, frameIndex),
 | 
			
		||||
          element,
 | 
			
		||||
          ...nextElements.slice(frameIndex, elementIndex),
 | 
			
		||||
          ...nextElements.slice(elementIndex + 1),
 | 
			
		||||
        ];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const frameElement = allElements[frameIndex];
 | 
			
		||||
  const nextFrameChildren = addedFrameChildren_left
 | 
			
		||||
    .concat(existingFrameChildren)
 | 
			
		||||
    .concat(addedFrameChildren_right);
 | 
			
		||||
 | 
			
		||||
  const nextFrameChildrenMap = nextFrameChildren.reduce(
 | 
			
		||||
    (acc: Record<string, boolean>, element) => {
 | 
			
		||||
      acc[element.id] = true;
 | 
			
		||||
      return acc;
 | 
			
		||||
    },
 | 
			
		||||
    {},
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const nextOtherElements_left = allElements
 | 
			
		||||
    .slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
 | 
			
		||||
    .filter((element) => !nextFrameChildrenMap[element.id]);
 | 
			
		||||
 | 
			
		||||
  const nextOtherElement_right = allElements
 | 
			
		||||
    .slice(frameIndex + 1)
 | 
			
		||||
    .filter((element) => !nextFrameChildrenMap[element.id]);
 | 
			
		||||
 | 
			
		||||
  const nextElements = nextOtherElements_left
 | 
			
		||||
    .concat(nextFrameChildren)
 | 
			
		||||
    .concat([frameElement])
 | 
			
		||||
    .concat(nextOtherElement_right);
 | 
			
		||||
 | 
			
		||||
  return nextElements;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -518,6 +550,7 @@ export const removeElementsFromFrame = (
 | 
			
		||||
  for (const element of elementsToRemove) {
 | 
			
		||||
    if (element.frameId) {
 | 
			
		||||
      _elementsToRemove.push(element);
 | 
			
		||||
 | 
			
		||||
      const boundTextElement = getBoundTextElement(element);
 | 
			
		||||
      if (boundTextElement) {
 | 
			
		||||
        _elementsToRemove.push(boundTextElement);
 | 
			
		||||
@@ -566,7 +599,7 @@ export const replaceAllElementsInFrame = (
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** does not mutate elements, but return new ones */
 | 
			
		||||
/** does not mutate elements, but returns new ones */
 | 
			
		||||
export const updateFrameMembershipOfSelectedElements = (
 | 
			
		||||
  allElements: ExcalidrawElementsIncludingDeleted,
 | 
			
		||||
  appState: AppState,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { ExcalidrawElementSkeleton } from "../../../data/transform";
 | 
			
		||||
import { FileId } from "../../../element/types";
 | 
			
		||||
import { FONT_FAMILY } from "../entry";
 | 
			
		||||
 | 
			
		||||
const elements: ExcalidrawElementSkeleton[] = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -40,10 +39,7 @@ const elements: ExcalidrawElementSkeleton[] = [
 | 
			
		||||
];
 | 
			
		||||
export default {
 | 
			
		||||
  elements,
 | 
			
		||||
  appState: {
 | 
			
		||||
    viewBackgroundColor: "#AFEEEE",
 | 
			
		||||
    currentItemFontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
 | 
			
		||||
  },
 | 
			
		||||
  appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
 | 
			
		||||
  scrollToContent: true,
 | 
			
		||||
  libraryItems: [
 | 
			
		||||
    [
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@
 | 
			
		||||
    "@babel/preset-env": "7.18.6",
 | 
			
		||||
    "@babel/preset-react": "7.18.6",
 | 
			
		||||
    "@babel/preset-typescript": "7.18.6",
 | 
			
		||||
    "@size-limit/preset-big-lib": "8.2.6",
 | 
			
		||||
    "@size-limit/preset-big-lib": "9.0.0",
 | 
			
		||||
    "autoprefixer": "10.4.7",
 | 
			
		||||
    "babel-loader": "8.2.5",
 | 
			
		||||
    "babel-plugin-transform-class-properties": "6.24.1",
 | 
			
		||||
@@ -63,7 +63,7 @@
 | 
			
		||||
    "mini-css-extract-plugin": "2.6.1",
 | 
			
		||||
    "postcss-loader": "7.0.1",
 | 
			
		||||
    "sass-loader": "13.0.2",
 | 
			
		||||
    "size-limit": "8.2.4",
 | 
			
		||||
    "size-limit": "9.0.0",
 | 
			
		||||
    "style-loader": "3.3.3",
 | 
			
		||||
    "terser-webpack-plugin": "5.3.3",
 | 
			
		||||
    "ts-loader": "9.3.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1112,38 +1112,37 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    debug "^4.1.1"
 | 
			
		||||
 | 
			
		||||
"@size-limit/file@8.2.6":
 | 
			
		||||
  version "8.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-8.2.6.tgz#0e17045a0fa8009fc787c85e3c09f611316f908c"
 | 
			
		||||
  integrity sha512-B7ayjxiJsbtXdIIWazJkB5gezi5WBMecdHTFPMDhI3NwEML1RVvUjAkrb1mPAAkIpt2LVHPnhdCUHjqDdjugwg==
 | 
			
		||||
"@size-limit/file@9.0.0":
 | 
			
		||||
  version "9.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-9.0.0.tgz#eed5415f5bcc8407979e47ffa49ffaf12d2d2378"
 | 
			
		||||
  integrity sha512-oM2UaH2FRq4q22k+R+P6xCpzET10T94LFdSjb9svVu/vOD7NaB9LGcG6se8TW1BExXiyXO4GEhLsBt3uMKM3qA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    semver "7.5.3"
 | 
			
		||||
    semver "7.5.4"
 | 
			
		||||
 | 
			
		||||
"@size-limit/preset-big-lib@8.2.6":
 | 
			
		||||
  version "8.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-8.2.6.tgz#fbff51e7a03fc36b6b3d9103cbe5b3909e35a83e"
 | 
			
		||||
  integrity sha512-63a+yos0QNMVCfx1OWnxBrdQVTlBVGzW5fDXwpWq/hKfP3B89XXHYGeL2Z2f8IXSVeGkAHXnDcTZyIPRaXffVg==
 | 
			
		||||
"@size-limit/preset-big-lib@9.0.0":
 | 
			
		||||
  version "9.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-9.0.0.tgz#ddcf30e7646b66ecc0f8a1a6498a5eda6d82876d"
 | 
			
		||||
  integrity sha512-wc+VNLXjn0z11s1IWevo8+utP7uZGPVDNNe5cNyMFYHv7/pwJtgsd8w2onEkbK1h8x1oJfWlcqFNKAnvD1Bylw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@size-limit/file" "8.2.6"
 | 
			
		||||
    "@size-limit/time" "8.2.6"
 | 
			
		||||
    "@size-limit/webpack" "8.2.6"
 | 
			
		||||
    size-limit "8.2.6"
 | 
			
		||||
    "@size-limit/file" "9.0.0"
 | 
			
		||||
    "@size-limit/time" "9.0.0"
 | 
			
		||||
    "@size-limit/webpack" "9.0.0"
 | 
			
		||||
    size-limit "9.0.0"
 | 
			
		||||
 | 
			
		||||
"@size-limit/time@8.2.6":
 | 
			
		||||
  version "8.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-8.2.6.tgz#5d1912bcfc6437f6f59804737ad0538b25c207ed"
 | 
			
		||||
  integrity sha512-fUEPvz7Uq6+oUQxSYbNlJt3tTgQBl1VY21USi/B7ebdnVKLnUx1JyPI9v7imN6XEkB2VpJtnYgjFeLgNrirzMA==
 | 
			
		||||
"@size-limit/time@9.0.0":
 | 
			
		||||
  version "9.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-9.0.0.tgz#44ba75b3cba30736b133dbb3fd740f894a642c87"
 | 
			
		||||
  integrity sha512-//Yba5fRkYqpBZ6MFtjDTSjCpQonDMqkwofpe0G1hMd/5l/3PZXVLDCAU2BW3nQFqTkpeyytFG6Y3jxUqSddiw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    estimo "^2.3.6"
 | 
			
		||||
    react "^17.0.2"
 | 
			
		||||
 | 
			
		||||
"@size-limit/webpack@8.2.6":
 | 
			
		||||
  version "8.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-8.2.6.tgz#3a3c98293b80f7c5fb6e8499199ae6f94f05b463"
 | 
			
		||||
  integrity sha512-y2sB66m5sJxIjZ8SEAzpWbiw3/+bnQHDHfk9cSbV5ChKklq02AlYg8BS5KxGWmMpdyUo4TzpjSCP9oEudY+hxQ==
 | 
			
		||||
"@size-limit/webpack@9.0.0":
 | 
			
		||||
  version "9.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490"
 | 
			
		||||
  integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    nanoid "^3.3.6"
 | 
			
		||||
    webpack "^5.88.0"
 | 
			
		||||
    webpack "^5.88.2"
 | 
			
		||||
 | 
			
		||||
"@types/body-parser@*":
 | 
			
		||||
  version "1.19.2"
 | 
			
		||||
@@ -1694,9 +1693,9 @@ ansi-styles@^4.1.0:
 | 
			
		||||
    color-convert "^2.0.1"
 | 
			
		||||
 | 
			
		||||
anymatch@~3.1.2:
 | 
			
		||||
  version "3.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
 | 
			
		||||
  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
 | 
			
		||||
  version "3.1.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
 | 
			
		||||
  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    normalize-path "^3.0.0"
 | 
			
		||||
    picomatch "^2.0.4"
 | 
			
		||||
@@ -2553,9 +2552,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
 | 
			
		||||
  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 | 
			
		||||
 | 
			
		||||
fast-glob@^3.2.9:
 | 
			
		||||
  version "3.2.12"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
 | 
			
		||||
  integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
 | 
			
		||||
  version "3.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
 | 
			
		||||
  integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@nodelib/fs.stat" "^2.0.2"
 | 
			
		||||
    "@nodelib/fs.walk" "^1.2.3"
 | 
			
		||||
@@ -2672,9 +2671,9 @@ fs.realpath@^1.0.0:
 | 
			
		||||
  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 | 
			
		||||
 | 
			
		||||
fsevents@~2.3.2:
 | 
			
		||||
  version "2.3.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
 | 
			
		||||
  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
 | 
			
		||||
  version "2.3.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
 | 
			
		||||
  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
 | 
			
		||||
 | 
			
		||||
function-bind@^1.1.1:
 | 
			
		||||
  version "1.1.1"
 | 
			
		||||
@@ -2993,7 +2992,7 @@ is-docker@^2.0.0, is-docker@^2.1.1:
 | 
			
		||||
is-extglob@^2.1.1:
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
 | 
			
		||||
  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 | 
			
		||||
  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
 | 
			
		||||
 | 
			
		||||
is-glob@^4.0.1, is-glob@~4.0.1:
 | 
			
		||||
  version "4.0.3"
 | 
			
		||||
@@ -3105,7 +3104,7 @@ klona@^2.0.4, klona@^2.0.5:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
 | 
			
		||||
  integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
 | 
			
		||||
 | 
			
		||||
lilconfig@^2.0.6, lilconfig@^2.1.0:
 | 
			
		||||
lilconfig@^2.1.0:
 | 
			
		||||
  version "2.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
 | 
			
		||||
  integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
 | 
			
		||||
@@ -3146,7 +3145,7 @@ lodash@^4.17.20, lodash@^4.17.4:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
 | 
			
		||||
  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 | 
			
		||||
 | 
			
		||||
loose-envify@^1.0.0, loose-envify@^1.1.0:
 | 
			
		||||
loose-envify@^1.0.0:
 | 
			
		||||
  version "1.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
 | 
			
		||||
  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
 | 
			
		||||
@@ -3360,11 +3359,6 @@ npm-run-path@^4.0.1:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    path-key "^3.0.0"
 | 
			
		||||
 | 
			
		||||
object-assign@^4.1.1:
 | 
			
		||||
  version "4.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 | 
			
		||||
  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
 | 
			
		||||
 | 
			
		||||
object-inspect@^1.9.0:
 | 
			
		||||
  version "1.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
 | 
			
		||||
@@ -3519,16 +3513,16 @@ picocolors@^1.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
 | 
			
		||||
  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
 | 
			
		||||
 | 
			
		||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
 | 
			
		||||
  version "2.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
 | 
			
		||||
  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
 | 
			
		||||
 | 
			
		||||
picomatch@^2.3.1:
 | 
			
		||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
 | 
			
		||||
  version "2.3.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
 | 
			
		||||
  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 | 
			
		||||
 | 
			
		||||
picomatch@^2.2.3:
 | 
			
		||||
  version "2.3.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
 | 
			
		||||
  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
 | 
			
		||||
 | 
			
		||||
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
 | 
			
		||||
  version "4.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
 | 
			
		||||
@@ -3685,14 +3679,6 @@ raw-body@2.5.1:
 | 
			
		||||
    iconv-lite "0.4.24"
 | 
			
		||||
    unpipe "1.0.0"
 | 
			
		||||
 | 
			
		||||
react@^17.0.2:
 | 
			
		||||
  version "17.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
 | 
			
		||||
  integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    loose-envify "^1.1.0"
 | 
			
		||||
    object-assign "^4.1.1"
 | 
			
		||||
 | 
			
		||||
readable-stream@^2.0.1:
 | 
			
		||||
  version "2.3.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
 | 
			
		||||
@@ -3927,10 +3913,10 @@ semver@7.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
 | 
			
		||||
  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
 | 
			
		||||
 | 
			
		||||
semver@7.5.3:
 | 
			
		||||
  version "7.5.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
 | 
			
		||||
  integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
 | 
			
		||||
semver@7.5.4, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
 | 
			
		||||
  version "7.5.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
 | 
			
		||||
  integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    lru-cache "^6.0.0"
 | 
			
		||||
 | 
			
		||||
@@ -3939,13 +3925,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
 | 
			
		||||
  integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 | 
			
		||||
 | 
			
		||||
semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
 | 
			
		||||
  version "7.5.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
 | 
			
		||||
  integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    lru-cache "^6.0.0"
 | 
			
		||||
 | 
			
		||||
send@0.18.0:
 | 
			
		||||
  version "0.18.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
 | 
			
		||||
@@ -4054,22 +4033,10 @@ sirv@^1.0.7:
 | 
			
		||||
    mime "^2.3.1"
 | 
			
		||||
    totalist "^1.0.0"
 | 
			
		||||
 | 
			
		||||
size-limit@8.2.4:
 | 
			
		||||
  version "8.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.4.tgz#0ab0df7cbc89007d544a50b451f5fb4d110694ca"
 | 
			
		||||
  integrity sha512-Un16nSreD1v2CYwSorattiJcHuAWqXvg4TsGgzpjnoByqQwsSfCIEQHuaD14HNStzredR8cdsO9oGH91ibypTA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    bytes-iec "^3.1.1"
 | 
			
		||||
    chokidar "^3.5.3"
 | 
			
		||||
    globby "^11.1.0"
 | 
			
		||||
    lilconfig "^2.0.6"
 | 
			
		||||
    nanospinner "^1.1.0"
 | 
			
		||||
    picocolors "^1.0.0"
 | 
			
		||||
 | 
			
		||||
size-limit@8.2.6:
 | 
			
		||||
  version "8.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.6.tgz#e41dbc74a4d7fc13be72551b6ef31ea50007d18d"
 | 
			
		||||
  integrity sha512-zpznim/tX/NegjoQuRKgWTF4XiB0cn2qt90uJzxYNTFAqexk4b94DOAkBD3TwhC6c3kw2r0KcnA5upziVMZqDg==
 | 
			
		||||
size-limit@9.0.0:
 | 
			
		||||
  version "9.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-9.0.0.tgz#203c47303462a8351976eb26175acea5f4e80447"
 | 
			
		||||
  integrity sha512-DrA7o2DeRN3s+vwCA9nn7Ck9Y4pn9t0GNUwQRpKqBtBmNkl6LA2s/NlNCdtKHrEkRTeYA1ZQ65mnYveo9rUqgA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    bytes-iec "^3.1.1"
 | 
			
		||||
    chokidar "^3.5.3"
 | 
			
		||||
@@ -4556,7 +4523,7 @@ webpack@5.76.0:
 | 
			
		||||
    watchpack "^2.4.0"
 | 
			
		||||
    webpack-sources "^3.2.3"
 | 
			
		||||
 | 
			
		||||
webpack@^5.88.0:
 | 
			
		||||
webpack@^5.88.2:
 | 
			
		||||
  version "5.88.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e"
 | 
			
		||||
  integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import type { Drawable } from "roughjs/bin/core";
 | 
			
		||||
import type { RoughSVG } from "roughjs/bin/svg";
 | 
			
		||||
 | 
			
		||||
import { StaticCanvasRenderConfig } from "../scene/types";
 | 
			
		||||
import { distance, isRTL } from "../utils";
 | 
			
		||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
 | 
			
		||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
 | 
			
		||||
import rough from "roughjs/bin/rough";
 | 
			
		||||
import {
 | 
			
		||||
@@ -46,8 +46,6 @@ import {
 | 
			
		||||
  getLineHeightInPx,
 | 
			
		||||
  getBoundTextMaxHeight,
 | 
			
		||||
  getBoundTextMaxWidth,
 | 
			
		||||
  getFontFamilyString,
 | 
			
		||||
  getFontString,
 | 
			
		||||
} from "../element/textElement";
 | 
			
		||||
import { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
import {
 | 
			
		||||
 
 | 
			
		||||
@@ -934,10 +934,8 @@ const _renderStaticScene = ({
 | 
			
		||||
    strokeGrid(
 | 
			
		||||
      context,
 | 
			
		||||
      appState.gridSize,
 | 
			
		||||
      -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize +
 | 
			
		||||
        (appState.scrollX % appState.gridSize),
 | 
			
		||||
      -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize +
 | 
			
		||||
        (appState.scrollY % appState.gridSize),
 | 
			
		||||
      appState.scrollX,
 | 
			
		||||
      appState.scrollY,
 | 
			
		||||
      appState.zoom,
 | 
			
		||||
      normalizedWidth / appState.zoom.value,
 | 
			
		||||
      normalizedHeight / appState.zoom.value,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { isTextElement, refreshTextDimensions } from "../element";
 | 
			
		||||
import { newElementWith } from "../element/mutateElement";
 | 
			
		||||
import { getFontString } from "../element/textElement";
 | 
			
		||||
import { isBoundToContainer } from "../element/typeChecks";
 | 
			
		||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
 | 
			
		||||
import { getFontString } from "../utils";
 | 
			
		||||
import type Scene from "./Scene";
 | 
			
		||||
import { ShapeCache } from "./ShapeCache";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ describe("restoreElements", () => {
 | 
			
		||||
    const textElement = API.createElement({
 | 
			
		||||
      type: "text",
 | 
			
		||||
      fontSize: 14,
 | 
			
		||||
      fontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
 | 
			
		||||
      fontFamily: FONT_FAMILY.Virgil,
 | 
			
		||||
      text: "text",
 | 
			
		||||
      textAlign: "center",
 | 
			
		||||
      verticalAlign: "middle",
 | 
			
		||||
 
 | 
			
		||||
@@ -666,13 +666,9 @@ describe("regression tests", () => {
 | 
			
		||||
 | 
			
		||||
  it("updates fontSize & fontFamily appState", () => {
 | 
			
		||||
    UI.clickTool("text");
 | 
			
		||||
    expect(h.state.currentItemFontFamily).toEqual(
 | 
			
		||||
      FONT_FAMILY.HAND_DRAWN.fontFamilyId,
 | 
			
		||||
    );
 | 
			
		||||
    expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Virgil);
 | 
			
		||||
    fireEvent.click(screen.getByTitle(/code/i));
 | 
			
		||||
    expect(h.state.currentItemFontFamily).toEqual(
 | 
			
		||||
      FONT_FAMILY.CODE.fontFamilyId,
 | 
			
		||||
    );
 | 
			
		||||
    expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Cascadia);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import {
 | 
			
		||||
  ExcalidrawBindableElement,
 | 
			
		||||
  Arrowhead,
 | 
			
		||||
  ChartType,
 | 
			
		||||
  FontFamilyId,
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  FileId,
 | 
			
		||||
  ExcalidrawImageElement,
 | 
			
		||||
  Theme,
 | 
			
		||||
@@ -221,7 +221,7 @@ export type AppState = {
 | 
			
		||||
  currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
 | 
			
		||||
  currentItemRoughness: number;
 | 
			
		||||
  currentItemOpacity: number;
 | 
			
		||||
  currentItemFontFamily: FontFamilyId;
 | 
			
		||||
  currentItemFontFamily: FontFamilyValues;
 | 
			
		||||
  currentItemFontSize: number;
 | 
			
		||||
  currentItemTextAlign: TextAlign;
 | 
			
		||||
  currentItemStartArrowhead: Arrowhead | null;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								src/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								src/utils.ts
									
									
									
									
									
								
							@@ -4,11 +4,17 @@ import {
 | 
			
		||||
  CURSOR_TYPE,
 | 
			
		||||
  DEFAULT_VERSION,
 | 
			
		||||
  EVENT,
 | 
			
		||||
  FONT_FAMILY,
 | 
			
		||||
  isDarwin,
 | 
			
		||||
  MIME_TYPES,
 | 
			
		||||
  THEME,
 | 
			
		||||
  WINDOWS_EMOJI_FALLBACK_FONT,
 | 
			
		||||
} from "./constants";
 | 
			
		||||
import { NonDeletedExcalidrawElement } from "./element/types";
 | 
			
		||||
import {
 | 
			
		||||
  FontFamilyValues,
 | 
			
		||||
  FontString,
 | 
			
		||||
  NonDeletedExcalidrawElement,
 | 
			
		||||
} from "./element/types";
 | 
			
		||||
import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
 | 
			
		||||
import { unstable_batchedUpdates } from "react-dom";
 | 
			
		||||
import { SHAPES } from "./shapes";
 | 
			
		||||
@@ -79,6 +85,30 @@ export const isWritableElement = (
 | 
			
		||||
  (target instanceof HTMLInputElement &&
 | 
			
		||||
    (target.type === "text" || target.type === "number"));
 | 
			
		||||
 | 
			
		||||
export const getFontFamilyString = ({
 | 
			
		||||
  fontFamily,
 | 
			
		||||
}: {
 | 
			
		||||
  fontFamily: FontFamilyValues;
 | 
			
		||||
}) => {
 | 
			
		||||
  for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
 | 
			
		||||
    if (id === fontFamily) {
 | 
			
		||||
      return `${fontFamilyString}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return WINDOWS_EMOJI_FALLBACK_FONT;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** returns fontSize+fontFamily string for assignment to DOM elements */
 | 
			
		||||
export const getFontString = ({
 | 
			
		||||
  fontSize,
 | 
			
		||||
  fontFamily,
 | 
			
		||||
}: {
 | 
			
		||||
  fontSize: number;
 | 
			
		||||
  fontFamily: FontFamilyValues;
 | 
			
		||||
}) => {
 | 
			
		||||
  return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const debounce = <T extends any[]>(
 | 
			
		||||
  fn: (...args: T) => void,
 | 
			
		||||
  timeout: number,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user