Compare commits

..

8 Commits

Author SHA1 Message Date
Daniel J. Geiger
48c1f93e3e fix: Include src/data/restore.ts. 2023-09-08 13:37:08 -05:00
Daniel J. Geiger
3dd446dc15 feat: Add a subtype?: string property to ExcalidrawElement. 2023-09-08 13:17:03 -05:00
Rajnikant dash
56c21529db docs: Adding the json Schema to the documentation (#6817)
Co-authored-by: Rajni2002 <rajnikant.dash@everlytics.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-09-07 12:43:37 +02:00
Marcel Mraz
a13aed92f2 fix: z-index inconsistencies during addition / deletion in frames (#6914)
Co-authored-by: Marcel Mraz <marcel.mraz@adacta-fintech.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-09-06 22:41:44 +00:00
Aakansha Doshi
134df7bfbb fix: update size-limit so react is not installed as dependency (#6964) 2023-09-06 10:39:04 +05:30
Alex Kim
5191cdbe26 fix: stale labeled arrow bounds cache after editing the label (#6893)
* fix stale labeled arrow bounds cache after editing the label

* add arrow bounds test

* fix test to check the arrow version

* fix

* fix test - remove unused import

* Update src/element/textWysiwyg.test.tsx

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-09-05 21:20:27 +05:30
David Luzar
27fd150a20 fix: canvas flickering due to resetting canvas on skipped frames (#6960) 2023-09-05 12:06:48 +02:00
zsviczian
188921c247 fix: grid jittery after partition PR (#6935) 2023-08-27 19:30:47 +02:00
31 changed files with 727 additions and 304 deletions

View File

@@ -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

View 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 |

View File

@@ -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"],
},
],
};

View File

@@ -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({

View File

@@ -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(

View File

@@ -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,
);

View File

@@ -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(
{

View File

@@ -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}";

View File

@@ -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 ?? "";

View File

@@ -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">;

View File

@@ -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,

View File

@@ -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",
});

View File

@@ -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;

View File

@@ -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";

View File

@@ -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);
});
});

View File

@@ -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;
};

View File

@@ -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);
});
});

View File

@@ -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, {

View File

@@ -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;

View File

@@ -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]);
});
});
});

View File

@@ -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,

View File

@@ -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: [
[

View File

@@ -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",

View File

@@ -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==

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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";

View File

@@ -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",

View File

@@ -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", () => {

View File

@@ -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;

View File

@@ -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,