Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax-maint-stage

This commit is contained in:
Daniel J. Geiger 2023-09-22 15:19:21 -05:00
commit 62f5475c4a
91 changed files with 1575 additions and 604 deletions

View File

@ -0,0 +1,429 @@
# Creating Elements programmatically
We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details.
For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers).
The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements).
## convertToExcalidrawElements
**_Signature_**
<pre>
convertToExcalidrawElements(elements:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133">
ExcalidrawElementSkeleton
</a>
)
</pre>
**_How to use_**
```js
import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
```
This function converts the Excalidraw Element Skeleton to excalidraw elements which could be then rendered on the canvas. Hence calling this function is necessary before passing it to APIs like [`initialData`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/initialdata), [`updateScene`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#updatescene) if you are using the Skeleton API
## Supported Features
### Rectangle, Ellipse, and Diamond
To create these shapes you need to pass its `type` and `x` and `y` coordinates for position. The rest of the attributes are optional_.
For the Skeleton API to work, `convertToExcalidrawElements` needs to be called before passing it to Excalidraw Component via initialData, updateScene or any such API.
```jsx live
function App() {
const elements = convertToExcalidrawElements([
{
type: "rectangle",
x: 100,
y: 250,
},
{
type: "ellipse",
x: 250,
y: 250,
},
{
type: "diamond",
x: 380,
y: 250,
},
]);
return (
<div style={{ height: "500px" }}>
<Excalidraw
initialData={{
elements,
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
scrollToContent: true,
}}
/>
</div>
);
}
```
You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes.
:::info
You can copy the below test examples and replace the elements in the live editor above to test it out.
:::
```js
convertToExcalidrawElements([
{
type: "rectangle",
x: 50,
y: 250,
width: 200,
height: 100,
backgroundColor: "#c0eb75",
strokeWidth: 2,
},
{
type: "ellipse",
x: 300,
y: 250,
width: 200,
height: 100,
backgroundColor: "#ffc9c9",
strokeStyle: "dotted",
fillStyle: "solid",
strokeWidth: 2,
},
{
type: "diamond",
x: 550,
y: 250,
width: 200,
height: 100,
backgroundColor: "#a5d8ff",
strokeColor: "#1971c2",
strokeStyle: "dashed",
fillStyle: "cross-hatch",
strokeWidth: 2,
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/70ca7063-88fb-434c-838a-cd466e1bc3c2)
### Text Element
The `type`, `x`, `y` and `text` properties are required to create a text element, rest of the attributes are optional
```js
convertToExcalidrawElements([
{
type: "text",
x: 100,
y: 100,
text: "HELLO WORLD!",
},
{
type: "text",
x: 100,
y: 150,
text: "STYLED HELLO WORLD!",
fontSize: 20,
strokeColor: "#5f3dc4",
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/085c7ac3-7952-4f22-b9c3-6beb51438526)
### Lines and Arrows
The `type`, `x`, and `y` properties are required, rest of the attributes are optional
```js
convertToExcalidrawElements([
{
type: "arrow",
x: 100,
y: 20,
},
{
type: "line",
x: 100,
y: 60,
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/0c22a06b-b568-4ab5-9848-a5f0160f66a6)
#### With Addtional properties
```js
convertToExcalidrawElements([
{
type: "arrow",
x: 450,
y: 20,
startArrowhead: "dot",
endArrowhead: "triangle",
strokeColor: "#1971c2",
strokeWidth: 2,
},
{
type: "line",
x: 450,
y: 60,
strokeColor: "#2f9e44",
strokeWidth: 2,
strokeStyle: "dotted",
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/14f1bf3f-ad81-4096-8c1c-f35235084ec5)
### Text Containers
In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional.
If you don't provide the dimensions of container, we calculate it based of the label dimensions.
```js
convertToExcalidrawElements([
{
type: "rectangle",
x: 300,
y: 290,
label: {
text: "RECTANGLE TEXT CONTAINER",
},
},
{
type: "ellipse",
x: 500,
y: 100,
label: {
text: "ELLIPSE\n TEXT CONTAINER",
},
},
{
type: "diamond",
x: 100,
y: 100,
label: {
text: "DIAMOND\nTEXT CONTAINER",
},
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/1e2c7b5d-fcb4-4f86-946d-0bfb0e97d532)
#### With Additional properties
```js
convertToExcalidrawElements([
{
type: "diamond",
x: -120,
y: 100,
width: 270,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "STYLED DIAMOND TEXT CONTAINER",
strokeColor: "#099268",
fontSize: 20,
},
},
{
type: "rectangle",
x: 180,
y: 150,
width: 200,
strokeColor: "#c2255c",
label: {
text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
textAlign: "left",
verticalAlign: "top",
fontSize: 20,
},
},
{
type: "ellipse",
x: 400,
y: 130,
strokeColor: "#f08c00",
backgroundColor: "#ffec99",
width: 200,
label: {
text: "STYLED ELLIPSE TEXT CONTAINER",
strokeColor: "#c2255c",
},
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/f8123cd1-c9aa-452d-b96b-05c846c5030d)
### Labelled Arrows
Similar to Text Containers, you can create labelled arrows as well.
```js
convertToExcalidrawElements([
{
type: "arrow",
x: 100,
y: 100,
label: {
text: "LABELED ARROW",
},
},
{
type: "arrow",
x: 100,
y: 200,
label: {
text: "STYLED LABELED ARROW",
strokeColor: "#099268",
fontSize: 20,
},
},
{
type: "arrow",
x: 100,
y: 300,
strokeColor: "#1098ad",
strokeWidth: 2,
label: {
text: "ANOTHER STYLED LABELLED ARROW",
},
},
{
type: "arrow",
x: 100,
y: 400,
strokeColor: "#1098ad",
strokeWidth: 2,
label: {
text: "ANOTHER STYLED LABELLED ARROW",
strokeColor: "#099268",
},
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/70635e9b-f1c8-4839-89e1-73b813abeb93)
### Arrow bindings
To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional
```js
convertToExcalidrawElements([
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
type: "rectangle",
},
end: {
type: "ellipse",
},
},
]);
```
When position for `start` and `end ` properties are not specified, we compute it according to arrow position.
![image](https://github.com/excalidraw/excalidraw/assets/11256141/5aff09fd-b7e8-4c63-98be-da40b0698704)
```js
convertToExcalidrawElements([
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
type: "text",
text: "HEYYYYY",
},
end: {
type: "text",
text: "WHATS UP ?",
},
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/2a9f03ac-e45c-4fbd-9be0-5d9f8c8e0343)
#### When passing `id`
Useful when you want to bind multiple arrows to one diagram / use some existing diagram
```js
convertToExcalidrawElements([
{
type: "ellipse",
id: "ellipse-1",
strokeColor: "#66a80f",
x: 390,
y: 356,
width: 150,
height: 150,
backgroundColor: "#d8f5a2",
},
{
type: "diamond",
id: "diamond-1",
strokeColor: "#9c36b5",
width: 100,
x: -30,
y: 380,
},
{
type: "arrow",
x: 100,
y: 440,
width: 295,
height: 35,
strokeColor: "#1864ab",
start: {
type: "rectangle",
width: 150,
height: 150,
},
end: {
id: "ellipse-1",
},
},
{
type: "arrow",
x: 60,
y: 420,
width: 330,
strokeColor: "#e67700",
start: {
id: "diamond-1",
},
end: {
id: "ellipse-1",
},
},
]);
```
![image](https://github.com/excalidraw/excalidraw/assets/11256141/a8b047c8-2eed-4aea-82a2-e1e6bbddb8d4)

View File

@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0", "@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0", "@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.15.2-6546-3398d86", "@excalidraw/excalidraw": "0.15.2-eb020d0",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3", "docusaurus-plugin-sass": "0.2.3",

View File

@ -81,12 +81,8 @@ const sidebars = {
"@excalidraw/excalidraw/api/utils/restore", "@excalidraw/excalidraw/api/utils/restore",
], ],
}, },
{ "@excalidraw/excalidraw/api/constants",
type: "category", "@excalidraw/excalidraw/api/excalidraw-element-skeleton",
label: "Constants",
link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" },
items: [],
},
], ],
}, },
"@excalidraw/excalidraw/faq", "@excalidraw/excalidraw/faq",

View File

@ -1631,10 +1631,10 @@
url-loader "^4.1.1" url-loader "^4.1.1"
webpack "^5.73.0" webpack "^5.73.0"
"@excalidraw/excalidraw@0.15.2-6546-3398d86": "@excalidraw/excalidraw@0.15.2-eb020d0":
version "0.15.2-6546-3398d86" version "0.15.2-eb020d0"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-6546-3398d86.tgz#e74d5ad944b8b414924d27ee91469a32b4f08dbf" resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-eb020d0.tgz#25bd61e6f23da7c084fb16a3e0fe0dd9ad8e6533"
integrity sha512-Tzq6qighJUytXRA8iMzQ8onoGclo9CuvPSw7DMvPxME8nxAxn5CeK/gsxIs3zwooj9CC6XF42BSrx0+n+fPxaQ== integrity sha512-TKGLzpOVqFQcwK1GFKTDXgg1s2U6tc5KE3qXuv87osbzOtftQn3x4+VH61vwdj11l00nEN80SMdXUC43T9uJqQ==
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.3.0" version "9.3.0"

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { debounce, getVersion, nFormatter } from "../utils"; import { debounce, getVersion, nFormatter } from "../src/utils";
import { import {
getElementsStorageSize, getElementsStorageSize,
getTotalStorageSize, getTotalStorageSize,
} from "./data/localStorage"; } from "./data/localStorage";
import { DEFAULT_VERSION } from "../constants"; import { DEFAULT_VERSION } from "../src/constants";
import { t } from "../i18n"; import { t } from "../src/i18n";
import { copyTextToSystemClipboard } from "../clipboard"; import { copyTextToSystemClipboard } from "../src/clipboard";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../src/element/types";
import { UIAppState } from "../types"; import { UIAppState } from "../src/types";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };

View File

@ -1,23 +1,23 @@
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { PureComponent } from "react"; import { PureComponent } from "react";
import { ExcalidrawImperativeAPI } from "../../types"; import { ExcalidrawImperativeAPI } from "../../src/types";
import { ErrorDialog } from "../../components/ErrorDialog"; import { ErrorDialog } from "../../src/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../constants"; import { APP_NAME, ENV, EVENT } from "../../src/constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../src/data/types";
import { import {
ExcalidrawElement, ExcalidrawElement,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
} from "../../element/types"; } from "../../src/element/types";
import { import {
getSceneVersion, getSceneVersion,
restoreElements, restoreElements,
} from "../../packages/excalidraw/index"; } from "../../src/packages/excalidraw/index";
import { Collaborator, Gesture } from "../../types"; import { Collaborator, Gesture } from "../../src/types";
import { import {
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
withBatchedUpdates, withBatchedUpdates,
} from "../../utils"; } from "../../src/utils";
import { import {
CURSOR_SYNC_TIMEOUT, CURSOR_SYNC_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
@ -48,25 +48,25 @@ import {
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal"; import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
import { t } from "../../i18n"; import { t } from "../../src/i18n";
import { UserIdleState } from "../../types"; import { UserIdleState } from "../../src/types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants"; import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
import { import {
encodeFilesForUpload, encodeFilesForUpload,
FileManager, FileManager,
updateStaleImageStatuses, updateStaleImageStatuses,
} from "../data/FileManager"; } from "../data/FileManager";
import { AbortError } from "../../errors"; import { AbortError } from "../../src/errors";
import { import {
isImageElement, isImageElement,
isInitializedImageElement, isInitializedImageElement,
} from "../../element/typeChecks"; } from "../../src/element/typeChecks";
import { newElementWith } from "../../element/mutateElement"; import { newElementWith } from "../../src/element/mutateElement";
import { import {
ReconciledElements, ReconciledElements,
reconcileElements as _reconcileElements, reconcileElements as _reconcileElements,
} from "./reconciliation"; } from "./reconciliation";
import { decryptData } from "../../data/encryption"; import { decryptData } from "../../src/data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync"; import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData"; import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";

View File

@ -6,19 +6,19 @@ import {
import { TCollabClass } from "./Collab"; import { TCollabClass } from "./Collab";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../src/element/types";
import { import {
WS_EVENTS, WS_EVENTS,
FILE_UPLOAD_TIMEOUT, FILE_UPLOAD_TIMEOUT,
WS_SCENE_EVENT_TYPES, WS_SCENE_EVENT_TYPES,
} from "../app_constants"; } from "../app_constants";
import { UserIdleState } from "../../types"; import { UserIdleState } from "../../src/types";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../src/analytics";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { newElementWith } from "../../element/mutateElement"; import { newElementWith } from "../../src/element/mutateElement";
import { BroadcastedExcalidrawElement } from "./reconciliation"; import { BroadcastedExcalidrawElement } from "./reconciliation";
import { encryptData } from "../../data/encryption"; import { encryptData } from "../../src/data/encryption";
import { PRECEDING_ELEMENT_KEY } from "../../constants"; import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
class Portal { class Portal {
collab: TCollabClass; collab: TCollabClass;

View File

@ -1,4 +1,4 @@
@import "../../css/variables.module"; @import "../../src/css/variables.module";
.excalidraw { .excalidraw {
.RoomDialog { .RoomDialog {

View File

@ -1,13 +1,13 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { copyTextToSystemClipboard } from "../../clipboard"; import { copyTextToSystemClipboard } from "../../src/clipboard";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../src/analytics";
import { getFrame } from "../../utils"; import { getFrame } from "../../src/utils";
import { useI18n } from "../../i18n"; import { useI18n } from "../../src/i18n";
import { KEYS } from "../../keys"; import { KEYS } from "../../src/keys";
import { Dialog } from "../../components/Dialog"; import { Dialog } from "../../src/components/Dialog";
import { import {
copyIcon, copyIcon,
playerPlayIcon, playerPlayIcon,
@ -16,11 +16,11 @@ import {
shareIOS, shareIOS,
shareWindows, shareWindows,
tablerCheckIcon, tablerCheckIcon,
} from "../../components/icons"; } from "../../src/components/icons";
import { TextField } from "../../components/TextField"; import { TextField } from "../../src/components/TextField";
import { FilledButton } from "../../components/FilledButton"; import { FilledButton } from "../../src/components/FilledButton";
import { ReactComponent as CollabImage } from "../../assets/lock.svg"; import { ReactComponent as CollabImage } from "../../src/assets/lock.svg";
import "./RoomDialog.scss"; import "./RoomDialog.scss";
const getShareIcon = () => { const getShareIcon = () => {

View File

@ -1,7 +1,7 @@
import { PRECEDING_ELEMENT_KEY } from "../../constants"; import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../src/element/types";
import { AppState } from "../../types"; import { AppState } from "../../src/types";
import { arrayToMapWithIndex } from "../../utils"; import { arrayToMapWithIndex } from "../../src/utils";
export type ReconciledElements = readonly ExcalidrawElement[] & { export type ReconciledElements = readonly ExcalidrawElement[] & {
_brand: "reconciledElements"; _brand: "reconciledElements";

View File

@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import { Footer } from "../../packages/excalidraw/index"; import { Footer } from "../../src/packages/excalidraw/index";
import { EncryptedIcon } from "./EncryptedIcon"; import { EncryptedIcon } from "./EncryptedIcon";
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
import { isExcalidrawPlusSignedUser } from "../app_constants";
export const AppFooter = React.memo(() => { export const AppFooter = React.memo(() => {
return ( return (
@ -13,8 +14,11 @@ export const AppFooter = React.memo(() => {
alignItems: "center", alignItems: "center",
}} }}
> >
<ExcalidrawPlusAppLink /> {isExcalidrawPlusSignedUser ? (
<EncryptedIcon /> <ExcalidrawPlusAppLink />
) : (
<EncryptedIcon />
)}
</div> </div>
</Footer> </Footer>
); );

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { PlusPromoIcon } from "../../components/icons"; import { PlusPromoIcon } from "../../src/components/icons";
import { MainMenu } from "../../packages/excalidraw/index"; import { MainMenu } from "../../src/packages/excalidraw/index";
import { LanguageList } from "./LanguageList"; import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{ export const AppMainMenu: React.FC<{

View File

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { PlusPromoIcon } from "../../components/icons"; import { PlusPromoIcon } from "../../src/components/icons";
import { useI18n } from "../../i18n"; import { useI18n } from "../../src/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index"; import { WelcomeScreen } from "../../src/packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import { isExcalidrawPlusSignedUser } from "../app_constants";
import { POINTER_EVENTS } from "../../src/constants";
export const AppWelcomeScreen: React.FC<{ export const AppWelcomeScreen: React.FC<{
setCollabDialogShown: (toggle: boolean) => any; setCollabDialogShown: (toggle: boolean) => any;
@ -18,7 +19,7 @@ export const AppWelcomeScreen: React.FC<{
if (bit === "Excalidraw+") { if (bit === "Excalidraw+") {
return ( return (
<a <a
style={{ pointerEvents: "all" }} style={{ pointerEvents: POINTER_EVENTS.inheritFromUI }}
href={`${ href={`${
import.meta.env.VITE_APP_PLUS_APP import.meta.env.VITE_APP_PLUS_APP
}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`} }?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}

View File

@ -1,6 +1,6 @@
import { shield } from "../../components/icons"; import { shield } from "../../src/components/icons";
import { Tooltip } from "../../components/Tooltip"; import { Tooltip } from "../../src/components/Tooltip";
import { useI18n } from "../../i18n"; import { useI18n } from "../../src/i18n";
export const EncryptedIcon = () => { export const EncryptedIcon = () => {
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,20 +1,20 @@
import React from "react"; import React from "react";
import { Card } from "../../components/Card"; import { Card } from "../../src/components/Card";
import { ToolButton } from "../../components/ToolButton"; import { ToolButton } from "../../src/components/ToolButton";
import { serializeAsJSON } from "../../data/json"; import { serializeAsJSON } from "../../src/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import { FileId, NonDeletedExcalidrawElement } from "../../element/types"; import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types";
import { AppState, BinaryFileData, BinaryFiles } from "../../types"; import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { useI18n } from "../../i18n"; import { useI18n } from "../../src/i18n";
import { encryptData, generateEncryptionKey } from "../../data/encryption"; import { encryptData, generateEncryptionKey } from "../../src/data/encryption";
import { isInitializedImageElement } from "../../element/typeChecks"; import { isInitializedImageElement } from "../../src/element/typeChecks";
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
import { encodeFilesForUpload } from "../data/FileManager"; import { encodeFilesForUpload } from "../data/FileManager";
import { MIME_TYPES } from "../../constants"; import { MIME_TYPES } from "../../src/constants";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../src/analytics";
import { getFrame } from "../../utils"; import { getFrame } from "../../src/utils";
import { ExcalidrawLogo } from "../../components/ExcalidrawLogo"; import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo";
export const exportToExcalidrawPlus = async ( export const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],

View File

@ -1,7 +1,7 @@
import oc from "open-color"; import oc from "open-color";
import React from "react"; import React from "react";
import { THEME } from "../../constants"; import { THEME } from "../../src/constants";
import { Theme } from "../../element/types"; import { Theme } from "../../src/element/types";
// https://github.com/tholman/github-corners // https://github.com/tholman/github-corners
export const GitHubCorner = React.memo( export const GitHubCorner = React.memo(

View File

@ -1,8 +1,8 @@
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import React from "react"; import React from "react";
import { appLangCodeAtom } from ".."; import { appLangCodeAtom } from "..";
import { useI18n } from "../../i18n"; import { useI18n } from "../../src/i18n";
import { languages } from "../../i18n"; import { languages } from "../../src/i18n";
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
const { t, langCode } = useI18n(); const { t, langCode } = useI18n();

View File

@ -1,19 +1,19 @@
import { compressData } from "../../data/encode"; import { compressData } from "../../src/data/encode";
import { newElementWith } from "../../element/mutateElement"; import { newElementWith } from "../../src/element/mutateElement";
import { isInitializedImageElement } from "../../element/typeChecks"; import { isInitializedImageElement } from "../../src/element/typeChecks";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
FileId, FileId,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
} from "../../element/types"; } from "../../src/element/types";
import { t } from "../../i18n"; import { t } from "../../src/i18n";
import { import {
BinaryFileData, BinaryFileData,
BinaryFileMetadata, BinaryFileMetadata,
ExcalidrawImperativeAPI, ExcalidrawImperativeAPI,
BinaryFiles, BinaryFiles,
} from "../../types"; } from "../../src/types";
export class FileManager { export class FileManager {
/** files being fetched */ /** files being fetched */

View File

@ -11,11 +11,11 @@
*/ */
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval"; import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../appState"; import { clearAppStateForLocalStorage } from "../../src/appState";
import { clearElementsForLocalStorage } from "../../element"; import { clearElementsForLocalStorage } from "../../src/element";
import { ExcalidrawElement, FileId } from "../../element/types"; import { ExcalidrawElement, FileId } from "../../src/element/types";
import { AppState, BinaryFileData, BinaryFiles } from "../../types"; import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
import { debounce } from "../../utils"; import { debounce } from "../../src/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager"; import { FileManager } from "./FileManager";
import { Locker } from "./Locker"; import { Locker } from "./Locker";

View File

@ -1,20 +1,20 @@
import { ExcalidrawElement, FileId } from "../../element/types"; import { ExcalidrawElement, FileId } from "../../src/element/types";
import { getSceneVersion } from "../../element"; import { getSceneVersion } from "../../src/element";
import Portal from "../collab/Portal"; import Portal from "../collab/Portal";
import { restoreElements } from "../../data/restore"; import { restoreElements } from "../../src/data/restore";
import { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFileMetadata, BinaryFileMetadata,
DataURL, DataURL,
} from "../../types"; } from "../../src/types";
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
import { decompressData } from "../../data/encode"; import { decompressData } from "../../src/data/encode";
import { encryptData, decryptData } from "../../data/encryption"; import { encryptData, decryptData } from "../../src/data/encryption";
import { MIME_TYPES } from "../../constants"; import { MIME_TYPES } from "../../src/constants";
import { reconcileElements } from "../collab/reconciliation"; import { reconcileElements } from "../collab/reconciliation";
import { getSyncableElements, SyncableExcalidrawElement } from "."; import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../utility-types"; import { ResolutionType } from "../../src/utility-types";
// private // private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@ -1,23 +1,23 @@
import { compressData, decompressData } from "../../data/encode"; import { compressData, decompressData } from "../../src/data/encode";
import { import {
decryptData, decryptData,
generateEncryptionKey, generateEncryptionKey,
IV_LENGTH_BYTES, IV_LENGTH_BYTES,
} from "../../data/encryption"; } from "../../src/data/encryption";
import { serializeAsJSON } from "../../data/json"; import { serializeAsJSON } from "../../src/data/json";
import { restore } from "../../data/restore"; import { restore } from "../../src/data/restore";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../src/data/types";
import { isInvisiblySmallElement } from "../../element/sizeHelpers"; import { isInvisiblySmallElement } from "../../src/element/sizeHelpers";
import { isInitializedImageElement } from "../../element/typeChecks"; import { isInitializedImageElement } from "../../src/element/typeChecks";
import { ExcalidrawElement, FileId } from "../../element/types"; import { ExcalidrawElement, FileId } from "../../src/element/types";
import { t } from "../../i18n"; import { t } from "../../src/i18n";
import { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
UserIdleState, UserIdleState,
} from "../../types"; } from "../../src/types";
import { bytesToHexString } from "../../utils"; import { bytesToHexString } from "../../src/utils";
import { import {
DELETED_ELEMENT_TIMEOUT, DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,

View File

@ -1,12 +1,12 @@
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../src/element/types";
import { AppState } from "../../types"; import { AppState } from "../../src/types";
import { import {
clearAppStateForLocalStorage, clearAppStateForLocalStorage,
getDefaultAppState, getDefaultAppState,
} from "../../appState"; } from "../../src/appState";
import { clearElementsForLocalStorage } from "../../element"; import { clearElementsForLocalStorage } from "../../src/element";
import { STORAGE_KEYS } from "../app_constants"; import { STORAGE_KEYS } from "../app_constants";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../src/data/types";
export const saveUsernameToLocalStorage = (username: string) => { export const saveUsernameToLocalStorage = (username: string) => {
try { try {

View File

@ -77,13 +77,14 @@
align-items: center; align-items: center;
border: 1px solid var(--color-primary); border: 1px solid var(--color-primary);
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: var(--space-factor); border-radius: var(--border-radius-lg);
background-color: var(--island-bg-color);
color: var(--color-primary) !important; color: var(--color-primary) !important;
text-decoration: none !important; text-decoration: none !important;
font-size: 0.75rem; font-size: 0.75rem;
box-sizing: border-box; box-sizing: border-box;
height: var(--default-button-size); height: var(--lg-button-size);
&:hover { &:hover {
background-color: var(--color-primary); background-color: var(--color-primary);

View File

@ -1,32 +1,32 @@
import polyfill from "../polyfill"; import polyfill from "../src/polyfill";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics"; import { trackEvent } from "../src/analytics";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../src/appState";
import { ErrorDialog } from "../components/ErrorDialog"; import { ErrorDialog } from "../src/components/ErrorDialog";
import { TopErrorBoundary } from "../components/TopErrorBoundary"; import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
import { useSubtypes } from "../element/subtypes/use"; import { useSubtypes } from "../src/element/subtypes/use";
import { import {
APP_NAME, APP_NAME,
EVENT, EVENT,
THEME, THEME,
TITLE_TIMEOUT, TITLE_TIMEOUT,
VERSION_TIMEOUT, VERSION_TIMEOUT,
} from "../constants"; } from "../src/constants";
import { loadFromBlob } from "../data/blob"; import { loadFromBlob } from "../src/data/blob";
import { import {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
Theme, Theme,
} from "../element/types"; } from "../src/element/types";
import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { useCallbackRefState } from "../src/hooks/useCallbackRefState";
import { t } from "../i18n"; import { t } from "../src/i18n";
import { import {
Excalidraw, Excalidraw,
defaultLang, defaultLang,
LiveCollaborationTrigger, LiveCollaborationTrigger,
} from "../packages/excalidraw/index"; } from "../src/packages/excalidraw/index";
import { import {
AppState, AppState,
LibraryItems, LibraryItems,
@ -34,7 +34,7 @@ import {
BinaryFiles, BinaryFiles,
ExcalidrawInitialDataState, ExcalidrawInitialDataState,
UIAppState, UIAppState,
} from "../types"; } from "../src/types";
import { import {
debounce, debounce,
getVersion, getVersion,
@ -44,7 +44,7 @@ import {
ResolvablePromise, ResolvablePromise,
resolvablePromise, resolvablePromise,
isRunningInIframe, isRunningInIframe,
} from "../utils"; } from "../src/utils";
import { import {
FIREBASE_STORAGE_PREFIXES, FIREBASE_STORAGE_PREFIXES,
STORAGE_KEYS, STORAGE_KEYS,
@ -69,33 +69,40 @@ import {
importUsernameFromLocalStorage, importUsernameFromLocalStorage,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore"; import {
restore,
restoreAppState,
RestoredDataState,
} from "../src/data/restore";
import { import {
ExportToExcalidrawPlus, ExportToExcalidrawPlus,
exportToExcalidrawPlus, exportToExcalidrawPlus,
} from "./components/ExportToExcalidrawPlus"; } from "./components/ExportToExcalidrawPlus";
import { updateStaleImageStatuses } from "./data/FileManager"; import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../src/element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks"; import { isInitializedImageElement } from "../src/element/typeChecks";
import { loadFilesFromFirebase } from "./data/firebase"; import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData"; import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync"; import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx"; import clsx from "clsx";
import { reconcileElements } from "./collab/reconciliation"; import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; import {
parseLibraryTokensFromUrl,
useHandleLibrary,
} from "../src/data/library";
import { AppMainMenu } from "./components/AppMainMenu"; import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter"; import { AppFooter } from "./components/AppFooter";
import { atom, Provider, useAtom, useAtomValue } from "jotai"; import { atom, Provider, useAtom, useAtomValue } from "jotai";
import { useAtomWithInitialValue } from "../jotai"; import { useAtomWithInitialValue } from "../src/jotai";
import { appJotaiStore } from "./app-jotai"; import { appJotaiStore } from "./app-jotai";
import "./index.scss"; import "./index.scss";
import { ResolutionType } from "../utility-types"; import { ResolutionType } from "../src/utility-types";
import { ShareableLinkDialog } from "../components/ShareableLinkDialog"; import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog";
import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState"; import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm"; import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
import Trans from "../components/Trans"; import Trans from "../src/components/Trans";
polyfill(); polyfill();

View File

@ -0,0 +1,29 @@
import { defaultLang } from "../../src/i18n";
import { UI } from "../../src/tests/helpers/ui";
import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils";
import ExcalidrawApp from "../../excalidraw-app";
describe("Test LanguageList", () => {
it("rerenders UI on language change", async () => {
await render(<ExcalidrawApp />);
// select rectangle tool to show properties menu
UI.clickTool("rectangle");
// english lang should display `thin` label
expect(screen.queryByTitle(/thin/i)).not.toBeNull();
fireEvent.click(document.querySelector(".dropdown-menu-button")!);
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: "de-DE" },
});
// switching to german, `thin` label should no longer exist
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
// reset language
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: defaultLang.code },
});
// switching back to English
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
});
});

View File

@ -1,11 +1,11 @@
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../../excalidraw-app";
import { import {
mockBoundingClientRect, mockBoundingClientRect,
render, render,
restoreOriginalGetBoundingClientRect, restoreOriginalGetBoundingClientRect,
} from "./test-utils"; } from "../../src/tests/test-utils";
import { UI } from "./helpers/ui"; import { UI } from "../../src/tests/helpers/ui";
describe("Test MobileMenu", () => { describe("Test MobileMenu", () => {
const { h } = window; const { h } = window;

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
import { vi } from "vitest"; import { vi } from "vitest";
import { render, updateSceneData, waitFor } from "./test-utils"; import { render, updateSceneData, waitFor } from "../../src/tests/test-utils";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../../excalidraw-app";
import { API } from "./helpers/api"; import { API } from "../../src/tests/helpers/api";
import { createUndoAction } from "../actions/actionHistory"; import { createUndoAction } from "../../src/actions/actionHistory";
const { h } = window; const { h } = window;
Object.defineProperty(window, "crypto", { Object.defineProperty(window, "crypto", {
@ -16,7 +16,7 @@ Object.defineProperty(window, "crypto", {
}, },
}); });
vi.mock("../excalidraw-app/data/index.ts", async (importActual) => { vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
const module = (await importActual()) as any; const module = (await importActual()) as any;
return { return {
__esmodule: true, __esmodule: true,
@ -27,7 +27,7 @@ vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
}; };
}); });
vi.mock("../excalidraw-app/data/firebase.ts", () => { vi.mock("../../excalidraw-app/data/firebase.ts", () => {
const loadFromFirebase = async () => null; const loadFromFirebase = async () => null;
const saveToFirebase = () => {}; const saveToFirebase = () => {};
const isSavedToFirebase = () => true; const isSavedToFirebase = () => true;

View File

@ -1,13 +1,13 @@
import { expect } from "chai"; import { expect } from "chai";
import { PRECEDING_ELEMENT_KEY } from "../constants"; import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../../src/element/types";
import { import {
BroadcastedExcalidrawElement, BroadcastedExcalidrawElement,
ReconciledElements, ReconciledElements,
reconcileElements, reconcileElements,
} from "../excalidraw-app/collab/reconciliation"; } from "../../excalidraw-app/collab/reconciliation";
import { randomInteger } from "../random"; import { randomInteger } from "../../src/random";
import { AppState } from "../types"; import { AppState } from "../../src/types";
type Id = string; type Id = string;
type ElementLike = { type ElementLike = {

View File

@ -85,6 +85,7 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
YOUTUBE_STATES, YOUTUBE_STATES,
ZOOM_STEP, ZOOM_STEP,
POINTER_EVENTS,
} from "../constants"; } from "../constants";
import { exportCanvas, loadFromBlob } from "../data"; import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
@ -886,7 +887,9 @@ class App extends React.Component<AppProps, AppState> {
width: isVisible ? `${el.width}px` : 0, width: isVisible ? `${el.width}px` : 0,
height: isVisible ? `${el.height}px` : 0, height: isVisible ? `${el.height}px` : 0,
transform: isVisible ? `rotate(${el.angle}rad)` : "none", transform: isVisible ? `rotate(${el.angle}rad)` : "none",
pointerEvents: isActive ? "auto" : "none", pointerEvents: isActive
? POINTER_EVENTS.enabled
: POINTER_EVENTS.disabled,
}} }}
> >
{isHovered && ( {isHovered && (
@ -1110,9 +1113,9 @@ class App extends React.Component<AppProps, AppState> {
whiteSpace: "nowrap", whiteSpace: "nowrap",
textOverflow: "ellipsis", textOverflow: "ellipsis",
cursor: CURSOR_TYPE.MOVE, cursor: CURSOR_TYPE.MOVE,
// disable all interaction (e.g. cursor change) when in view pointerEvents: this.state.viewModeEnabled
// mode ? POINTER_EVENTS.disabled
pointerEvents: this.state.viewModeEnabled ? "none" : "all", : POINTER_EVENTS.inheritFromUI,
}} }}
onPointerDown={(event) => this.handleCanvasPointerDown(event)} onPointerDown={(event) => this.handleCanvasPointerDown(event)}
onWheel={(event) => this.handleWheel(event)} onWheel={(event) => this.handleWheel(event)}
@ -1154,6 +1157,16 @@ class App extends React.Component<AppProps, AppState> {
"excalidraw--view-mode": this.state.viewModeEnabled, "excalidraw--view-mode": this.state.viewModeEnabled,
"excalidraw--mobile": this.device.isMobile, "excalidraw--mobile": this.device.isMobile,
})} })}
style={{
["--ui-pointerEvents" as any]:
this.state.selectionElement ||
this.state.draggingElement ||
this.state.resizingElement ||
(this.state.editingElement &&
!isTextElement(this.state.editingElement))
? POINTER_EVENTS.disabled
: POINTER_EVENTS.enabled,
}}
ref={this.excalidrawContainerRef} ref={this.excalidrawContainerRef}
onDrop={this.handleAppOnDrop} onDrop={this.handleAppOnDrop}
tabIndex={0} tabIndex={0}
@ -1346,7 +1359,8 @@ class App extends React.Component<AppProps, AppState> {
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => { private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
jotaiStore.set(activeEyeDropperAtom, { jotaiStore.set(activeEyeDropperAtom, {
swapPreviewOnAlt: true, swapPreviewOnAlt: true,
previewType: type === "stroke" ? "strokeColor" : "backgroundColor", colorPickerType:
type === "stroke" ? "elementStroke" : "elementBackground",
onSelect: (color, event) => { onSelect: (color, event) => {
const shouldUpdateStrokeColor = const shouldUpdateStrokeColor =
(type === "background" && event.altKey) || (type === "background" && event.altKey) ||
@ -1357,12 +1371,14 @@ class App extends React.Component<AppProps, AppState> {
this.state.activeTool.type !== "selection" this.state.activeTool.type !== "selection"
) { ) {
if (shouldUpdateStrokeColor) { if (shouldUpdateStrokeColor) {
this.setState({ this.syncActionResult({
currentItemStrokeColor: color, appState: { ...this.state, currentItemStrokeColor: color },
commitToHistory: true,
}); });
} else { } else {
this.setState({ this.syncActionResult({
currentItemBackgroundColor: color, appState: { ...this.state, currentItemBackgroundColor: color },
commitToHistory: true,
}); });
} }
} else { } else {
@ -3975,7 +3991,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const [lastCommittedX, lastCommittedY] = const [lastCommittedX, lastCommittedY] =
@ -4792,7 +4808,11 @@ class App extends React.Component<AppProps, AppState> {
origin, origin,
withCmdOrCtrl: event[KEYS.CTRL_OR_CMD], withCmdOrCtrl: event[KEYS.CTRL_OR_CMD],
originInGrid: tupleToCoors( originInGrid: tupleToCoors(
getGridPoint(origin.x, origin.y, this.state.gridSize), getGridPoint(
origin.x,
origin.y,
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
),
), ),
scrollbars: isOverScrollBars( scrollbars: isOverScrollBars(
currentScrollBars, currentScrollBars,
@ -5316,7 +5336,11 @@ class App extends React.Component<AppProps, AppState> {
sceneY: number; sceneY: number;
link: string; link: string;
}) => { }) => {
const [gridX, gridY] = getGridPoint(sceneX, sceneY, this.state.gridSize); const [gridX, gridY] = getGridPoint(
sceneX,
sceneY,
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
const embedLink = getEmbedLink(link); const embedLink = getEmbedLink(link);
@ -5362,7 +5386,11 @@ class App extends React.Component<AppProps, AppState> {
sceneX: number; sceneX: number;
sceneY: number; sceneY: number;
}) => { }) => {
const [gridX, gridY] = getGridPoint(sceneX, sceneY, this.state.gridSize); const [gridX, gridY] = getGridPoint(
sceneX,
sceneY,
this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: gridX, x: gridX,
@ -5446,7 +5474,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@ -5540,7 +5568,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.gridSize, this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@ -5599,7 +5627,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.gridSize, this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const frame = newFrameElement({ const frame = newFrameElement({
@ -5682,7 +5710,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
// for arrows/lines, don't start dragging until a given threshold // for arrows/lines, don't start dragging until a given threshold
@ -5728,6 +5756,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.selectedLinearElement, this.state.selectedLinearElement,
pointerCoords, pointerCoords,
this.state, this.state,
!event[KEYS.CTRL_OR_CMD],
); );
if (!ret) { if (!ret) {
return; return;
@ -5853,7 +5882,7 @@ class App extends React.Component<AppProps, AppState> {
const [dragX, dragY] = getGridPoint( const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y, pointerCoords.y - pointerDownState.drag.offset.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const [dragDistanceX, dragDistanceY] = [ const [dragDistanceX, dragDistanceY] = [
@ -5920,7 +5949,7 @@ class App extends React.Component<AppProps, AppState> {
const [originDragX, originDragY] = getGridPoint( const [originDragX, originDragY] = getGridPoint(
pointerDownState.origin.x - pointerDownState.drag.offset.x, pointerDownState.origin.x - pointerDownState.drag.offset.x,
pointerDownState.origin.y - pointerDownState.drag.offset.y, pointerDownState.origin.y - pointerDownState.drag.offset.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
mutateElement(duplicatedElement, { mutateElement(duplicatedElement, {
x: duplicatedElement.x + (originDragX - dragX), x: duplicatedElement.x + (originDragX - dragX),
@ -7713,7 +7742,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const image = const image =
@ -7782,7 +7811,7 @@ class App extends React.Component<AppProps, AppState> {
const [resizeX, resizeY] = getGridPoint( const [resizeX, resizeY] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y, pointerCoords.y - pointerDownState.resize.offset.y,
this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
const frameElementsOffsetsMap = new Map< const frameElementsOffsetsMap = new Map<

View File

@ -1,7 +1,10 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker"; import { getColor } from "./ColorPicker";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { activeColorPickerSectionAtom } from "./colorPickerUtils"; import {
ColorPickerType,
activeColorPickerSectionAtom,
} from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons"; import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys"; import { KEYS } from "../../keys";
@ -15,14 +18,14 @@ interface ColorInputProps {
color: string; color: string;
onChange: (color: string) => void; onChange: (color: string) => void;
label: string; label: string;
eyeDropperType: "strokeColor" | "backgroundColor"; colorPickerType: ColorPickerType;
} }
export const ColorInput = ({ export const ColorInput = ({
color, color,
onChange, onChange,
label, label,
eyeDropperType, colorPickerType,
}: ColorInputProps) => { }: ColorInputProps) => {
const device = useDevice(); const device = useDevice();
const [innerValue, setInnerValue] = useState(color); const [innerValue, setInnerValue] = useState(color);
@ -116,7 +119,7 @@ export const ColorInput = ({
: { : {
keepOpenOnAlt: false, keepOpenOnAlt: false,
onSelect: (color) => onChange(color), onSelect: (color) => onChange(color),
previewType: eyeDropperType, colorPickerType,
}, },
) )
} }

View File

@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
const { container } = useExcalidrawContainer(); const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice(); const { isMobile, isLandscape } = useDevice();
const eyeDropperType = const colorInputJSX = (
type === "canvasBackground"
? undefined
: type === "elementBackground"
? "backgroundColor"
: "strokeColor";
const colorInputJSX = eyeDropperType && (
<div> <div>
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading> <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
<ColorInput <ColorInput
@ -98,7 +91,7 @@ const ColorPickerPopupContent = ({
onChange={(color) => { onChange={(color) => {
onChange(color); onChange(color);
}} }}
eyeDropperType={eyeDropperType} colorPickerType={type}
/> />
</div> </div>
); );
@ -160,7 +153,7 @@ const ColorPickerPopupContent = ({
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)", "0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
}} }}
> >
{palette && eyeDropperType ? ( {palette ? (
<Picker <Picker
palette={palette} palette={palette}
color={color} color={color}
@ -173,7 +166,7 @@ const ColorPickerPopupContent = ({
state = state || { state = state || {
keepOpenOnAlt: true, keepOpenOnAlt: true,
onSelect: onChange, onSelect: onChange,
previewType: eyeDropperType, colorPickerType: type,
}; };
state.keepOpenOnAlt = true; state.keepOpenOnAlt = true;
return state; return state;
@ -184,7 +177,7 @@ const ColorPickerPopupContent = ({
: { : {
keepOpenOnAlt: false, keepOpenOnAlt: false,
onSelect: onChange, onSelect: onChange,
previewType: eyeDropperType, colorPickerType: type,
}; };
}); });
}} }}

View File

@ -1,35 +1,47 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { rgbToHex } from "../colors"; import { rgbToHex } from "../colors";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
import { useUIAppState } from "../context/ui-appState"; import { useUIAppState } from "../context/ui-appState";
import { mutateElement } from "../element/mutateElement";
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer"; import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
import { useOutsideClick } from "../hooks/useOutsideClick"; import { useOutsideClick } from "../hooks/useOutsideClick";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import Scene from "../scene/Scene";
import { ShapeCache } from "../scene/ShapeCache";
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App"; import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import { useStable } from "../hooks/useStable";
import "./EyeDropper.scss"; import "./EyeDropper.scss";
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import { ExcalidrawElement } from "../element/types";
type EyeDropperProperties = { export type EyeDropperProperties = {
keepOpenOnAlt: boolean; keepOpenOnAlt: boolean;
swapPreviewOnAlt?: boolean; swapPreviewOnAlt?: boolean;
/** called when user picks color (on pointerup) */
onSelect: (color: string, event: PointerEvent) => void; onSelect: (color: string, event: PointerEvent) => void;
previewType: "strokeColor" | "backgroundColor"; /**
* property of selected elements to update live when alt-dragging.
* Supply `null` if not applicable (e.g. updating the canvas bg instead of
* elements)
**/
colorPickerType: ColorPickerType;
}; };
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null); export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
export const EyeDropper: React.FC<{ export const EyeDropper: React.FC<{
onCancel: () => void; onCancel: () => void;
onSelect: Required<EyeDropperProperties>["onSelect"]; onSelect: EyeDropperProperties["onSelect"];
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"]; /** called when color changes, on pointerdown for preview */
previewType: EyeDropperProperties["previewType"]; onChange: (
}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => { type: ColorPickerType,
color: string,
selectedElements: ExcalidrawElement[],
event: { altKey: boolean },
) => void;
colorPickerType: EyeDropperProperties["colorPickerType"];
}> = ({ onCancel, onChange, onSelect, colorPickerType }) => {
const eyeDropperContainer = useCreatePortalContainer({ const eyeDropperContainer = useCreatePortalContainer({
className: "excalidraw-eye-dropper-backdrop", className: "excalidraw-eye-dropper-backdrop",
parentSelector: ".excalidraw-eye-dropper-container", parentSelector: ".excalidraw-eye-dropper-container",
@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
const metaStuffRef = useRef({ selectedElements, app }); const stableProps = useStable({
metaStuffRef.current.selectedElements = selectedElements; app,
metaStuffRef.current.app = app; onCancel,
onChange,
onSelect,
selectedElements,
});
const { container: excalidrawContainer } = useExcalidrawContainer(); const { container: excalidrawContainer } = useExcalidrawContainer();
@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{
const currentColor = getCurrentColor({ clientX, clientY }); const currentColor = getCurrentColor({ clientX, clientY });
if (isHoldingPointerDown) { if (isHoldingPointerDown) {
for (const element of metaStuffRef.current.selectedElements) { stableProps.onChange(
mutateElement( colorPickerType,
element, currentColor,
{ stableProps.selectedElements,
[altKey && swapPreviewOnAlt { altKey },
? previewType === "strokeColor" );
? "backgroundColor"
: "strokeColor"
: previewType]: currentColor,
},
false,
);
ShapeCache.delete(element);
}
Scene.getScene(
metaStuffRef.current.selectedElements[0],
)?.informMutation();
} }
colorPreviewDiv.style.background = currentColor; colorPreviewDiv.style.background = currentColor;
}; };
const onCancel = () => {
stableProps.onCancel();
};
const onSelect: Required<EyeDropperProperties>["onSelect"] = (
color,
event,
) => {
stableProps.onSelect(color, event);
};
const pointerDownListener = (event: PointerEvent) => { const pointerDownListener = (event: PointerEvent) => {
isHoldingPointerDown = true; isHoldingPointerDown = true;
// NOTE we can't event.preventDefault() as that would stop // NOTE we can't event.preventDefault() as that would stop
@ -148,8 +164,8 @@ export const EyeDropper: React.FC<{
// init color preview else it would show only after the first mouse move // init color preview else it would show only after the first mouse move
mouseMoveListener({ mouseMoveListener({
clientX: metaStuffRef.current.app.lastViewportPosition.x, clientX: stableProps.app.lastViewportPosition.x,
clientY: metaStuffRef.current.app.lastViewportPosition.y, clientY: stableProps.app.lastViewportPosition.y,
altKey: false, altKey: false,
}); });
@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{
window.removeEventListener(EVENT.BLUR, onCancel); window.removeEventListener(EVENT.BLUR, onCancel);
}; };
}, [ }, [
stableProps,
app.canvas, app.canvas,
eyeDropperContainer, eyeDropperContainer,
onCancel, colorPickerType,
onSelect,
swapPreviewOnAlt,
previewType,
excalidrawContainer, excalidrawContainer,
appState.offsetLeft, appState.offsetLeft,
appState.offsetTop, appState.offsetTop,

View File

@ -7,7 +7,7 @@
} }
.FixedSideContainer > * { .FixedSideContainer > * {
pointer-events: all; pointer-events: var(--ui-pointerEvents);
} }
.FixedSideContainer_side_top { .FixedSideContainer_side_top {

View File

@ -83,27 +83,36 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
if (activeTool.type === "selection") { if (activeTool.type === "selection") {
if ( if (
appState.draggingElement?.type === "selection" && appState.draggingElement?.type === "selection" &&
!selectedElements.length &&
!appState.editingElement && !appState.editingElement &&
!appState.editingLinearElement !appState.editingLinearElement
) { ) {
return t("hints.deepBoxSelect"); return t("hints.deepBoxSelect");
} }
if (appState.gridSize && appState.draggingElement) {
return t("hints.disableSnapping");
}
if (!selectedElements.length && !isMobile) { if (!selectedElements.length && !isMobile) {
return t("hints.canvasPanning"); return t("hints.canvasPanning");
} }
}
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) { if (isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
return appState.editingLinearElement.selectedPointsIndices return appState.editingLinearElement.selectedPointsIndices
? t("hints.lineEditor_pointSelected") ? t("hints.lineEditor_pointSelected")
: t("hints.lineEditor_nothingSelected"); : t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (
!appState.draggingElement &&
isTextBindableContainer(selectedElements[0])
) {
return t("hints.bindTextToElement");
} }
return t("hints.lineEditor_info");
}
if (isTextBindableContainer(selectedElements[0])) {
return t("hints.bindTextToElement");
} }
} }

View File

@ -56,34 +56,52 @@
} }
.disable-zen-mode { .disable-zen-mode {
height: 30px; padding: 10px;
position: absolute; position: absolute;
bottom: 10px; bottom: 0;
[dir="ltr"] & { [dir="ltr"] & {
right: 15px; right: 1rem;
} }
[dir="rtl"] & { [dir="rtl"] & {
left: 15px; left: 1rem;
} }
font-size: 10px;
padding: 10px;
font-weight: 500;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: visibility 0s linear 0s, opacity 0.5s; transition: visibility 0s linear 0s, opacity 0.5s;
font-family: var(--ui-font);
font-size: 0.75rem;
font-weight: 500;
line-height: 1;
border-radius: var(--border-radius-lg);
border: 1px solid var(--default-border-color);
background-color: var(--island-bg-color);
color: var(--text-primary-color);
&:hover {
background-color: var(--button-hover-bg);
}
&:active {
border-color: var(--color-primary);
}
&--visible { &--visible {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transition: visibility 0s linear 300ms, opacity 0.5s; transition: visibility 0s linear 300ms, opacity 0.5s;
transition-delay: 0.8s; transition-delay: 0.8s;
pointer-events: var(--ui-pointerEvents);
} }
} }
.layer-ui__wrapper__footer-left, .layer-ui__wrapper__footer-left,
.layer-ui__wrapper__footer-right, .footer-center,
.disable-zen-mode--visible { .layer-ui__wrapper__footer-right {
pointer-events: all; & > * {
pointer-events: var(--ui-pointerEvents);
}
} }
.layer-ui__wrapper__footer-right { .layer-ui__wrapper__footer-right {

View File

@ -2,7 +2,7 @@ import clsx from "clsx";
import React from "react"; import React from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { isTextElement, showSelectedShapeActions } from "../element"; import { showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
@ -52,6 +52,9 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
import "./LayerUI.scss"; import "./LayerUI.scss";
import "./Toolbar.scss"; import "./Toolbar.scss";
import { mutateElement } from "../element/mutateElement";
import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -368,11 +371,44 @@ const LayerUI = ({
)} )}
{eyeDropperState && !device.isMobile && ( {eyeDropperState && !device.isMobile && (
<EyeDropper <EyeDropper
swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt} colorPickerType={eyeDropperState.colorPickerType}
previewType={eyeDropperState.previewType}
onCancel={() => { onCancel={() => {
setEyeDropperState(null); setEyeDropperState(null);
}} }}
onChange={(colorPickerType, color, selectedElements, { altKey }) => {
if (
colorPickerType !== "elementBackground" &&
colorPickerType !== "elementStroke"
) {
return;
}
if (selectedElements.length) {
for (const element of selectedElements) {
mutateElement(
element,
{
[altKey && eyeDropperState.swapPreviewOnAlt
? colorPickerType === "elementBackground"
? "strokeColor"
: "backgroundColor"
: colorPickerType === "elementBackground"
? "backgroundColor"
: "strokeColor"]: color,
},
false,
);
ShapeCache.delete(element);
}
Scene.getScene(selectedElements[0])?.informMutation();
} else if (colorPickerType === "elementBackground") {
setAppState({
currentItemBackgroundColor: color,
});
} else {
setAppState({ currentItemStrokeColor: color });
}
}}
onSelect={(color, event) => { onSelect={(color, event) => {
setEyeDropperState((state) => { setEyeDropperState((state) => {
return state?.keepOpenOnAlt && event.altKey ? state : null; return state?.keepOpenOnAlt && event.altKey ? state : null;
@ -427,13 +463,7 @@ const LayerUI = ({
{!device.isMobile && ( {!device.isMobile && (
<> <>
<div <div
className={clsx("layer-ui__wrapper", { className="layer-ui__wrapper"
"disable-pointerEvents":
appState.draggingElement ||
appState.resizingElement ||
(appState.editingElement &&
!isTextElement(appState.editingElement)),
})}
style={ style={
appState.openSidebar && appState.openSidebar &&
isSidebarDocked && isSidebarDocked &&

View File

@ -17,6 +17,8 @@
background-color: var(--sidebar-bg-color); background-color: var(--sidebar-bg-color);
box-shadow: var(--sidebar-shadow); box-shadow: var(--sidebar-shadow);
pointer-events: var(--ui-pointerEvents);
:root[dir="rtl"] & { :root[dir="rtl"] & {
left: 0; left: 0;
right: auto; right: auto;

View File

@ -7,7 +7,7 @@
right: 12px; right: 12px;
font-size: 12px; font-size: 12px;
z-index: 10; z-index: 10;
pointer-events: all; pointer-events: var(--ui-pointerEvents);
h3 { h3 {
margin: 0 24px 8px 0; margin: 0 24px 8px 0;

View File

@ -26,7 +26,7 @@
} }
.UserList > * { .UserList > * {
pointer-events: all; pointer-events: var(--ui-pointerEvents);
} }
.UserList_mobile { .UserList_mobile {

View File

@ -73,7 +73,7 @@ const Footer = ({
<FooterCenterTunnel.Out /> <FooterCenterTunnel.Out />
<div <div
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", { className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
"transition-right disable-pointerEvents": appState.zenModeEnabled, "transition-right": appState.zenModeEnabled,
})} })}
> >
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>

View File

@ -1,10 +1,11 @@
.footer-center { .footer-center {
pointer-events: none; pointer-events: none;
& > * { & > * {
pointer-events: all; pointer-events: var(--ui-pointerEvents);
} }
display: flex; display: flex;
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
margin-inline-end: 0.6rem;
} }

View File

@ -161,7 +161,7 @@
.welcome-screen-menu-item { .welcome-screen-menu-item {
box-sizing: border-box; box-sizing: border-box;
pointer-events: all; pointer-events: var(--ui-pointerEvents);
color: var(--color-gray-50); color: var(--color-gray-50);
font-size: 0.875rem; font-size: 0.875rem;
@ -202,7 +202,7 @@
} }
} }
&:not(:active) .welcome-screen-menu-item:hover { .welcome-screen-menu-item:hover {
text-decoration: none; text-decoration: none;
background: var(--color-gray-10); background: var(--color-gray-10);
@ -246,7 +246,7 @@
} }
} }
&:not(:active) .welcome-screen-menu-item:hover { .welcome-screen-menu-item:hover {
background: var(--color-gray-85); background: var(--color-gray-85);
.welcome-screen-menu-item__shortcut { .welcome-screen-menu-item__shortcut {

View File

@ -41,6 +41,14 @@ export const POINTER_BUTTON = {
TOUCH: -1, TOUCH: -1,
} as const; } as const;
export const POINTER_EVENTS = {
enabled: "all",
disabled: "none",
// asserted as any so it can be freely assigned to React Element
// "pointerEnvets" CSS prop
inheritFromUI: "var(--ui-pointerEvents)" as any,
} as const;
export enum EVENT { export enum EVENT {
COPY = "copy", COPY = "copy",
PASTE = "paste", PASTE = "paste",

View File

@ -253,7 +253,7 @@
max-height: 100%; max-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: initial; pointer-events: var(--ui-pointerEvents);
.panelColumn { .panelColumn {
padding: 8px 8px 0 8px; padding: 8px 8px 0 8px;
@ -301,7 +301,7 @@
pointer-events: none !important; pointer-events: none !important;
& > * { & > * {
pointer-events: all; pointer-events: var(--ui-pointerEvents);
} }
} }
@ -312,16 +312,16 @@
cursor: default; cursor: default;
pointer-events: none !important; pointer-events: none !important;
& > * {
pointer-events: var(--ui-pointerEvents);
}
@media (min-width: 1536px) { @media (min-width: 1536px) {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
grid-gap: 3rem; grid-gap: 3rem;
} }
} }
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
pointer-events: all;
}
.App-menu_top > *:first-child { .App-menu_top > *:first-child {
justify-self: flex-start; justify-self: flex-start;
} }
@ -425,17 +425,6 @@
} }
} }
.disable-zen-mode {
border-radius: var(--border-radius-lg);
background-color: var(--color-gray-20);
border: 1px solid var(--color-gray-30);
padding: 10px 20px;
&:hover {
background-color: var(--color-gray-30);
}
}
.scroll-back-to-content { .scroll-back-to-content {
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
background-color: var(--island-bg-color); background-color: var(--island-bg-color);
@ -447,7 +436,7 @@
left: 50%; left: 50%;
bottom: 30px; bottom: 30px;
transform: translateX(-50%); transform: translateX(-50%);
pointer-events: all; pointer-events: var(--ui-pointerEvents);
font-family: inherit; font-family: inherit;
&:hover { &:hover {

View File

@ -42,7 +42,7 @@ import {
} from "./binding"; } from "./binding";
import { tupleToCoors } from "../utils"; import { tupleToCoors } from "../utils";
import { isBindingElement } from "./typeChecks"; import { isBindingElement } from "./typeChecks";
import { shouldRotateWithDiscreteAngle } from "../keys"; import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { DRAGGING_THRESHOLD } from "../constants"; import { DRAGGING_THRESHOLD } from "../constants";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
@ -221,7 +221,7 @@ export class LinearElementEditor {
element, element,
referencePoint, referencePoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
); );
LinearElementEditor.movePoints(element, [ LinearElementEditor.movePoints(element, [
@ -238,7 +238,7 @@ export class LinearElementEditor {
element, element,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
); );
const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
@ -254,7 +254,7 @@ export class LinearElementEditor {
element, element,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
) )
: ([ : ([
element.points[pointIndex][0] + deltaX, element.points[pointIndex][0] + deltaX,
@ -647,7 +647,7 @@ export class LinearElementEditor {
element, element,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
), ),
], ],
}); });
@ -798,7 +798,7 @@ export class LinearElementEditor {
element, element,
lastCommittedPoint, lastCommittedPoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
); );
newPoint = [ newPoint = [
@ -810,7 +810,7 @@ export class LinearElementEditor {
element, element,
scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y, scenePointerY - appState.editingLinearElement.pointerOffset.y,
appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
); );
} }
@ -1176,6 +1176,7 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
appState: AppState, appState: AppState,
snapToGrid: boolean,
) { ) {
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
@ -1196,7 +1197,7 @@ export class LinearElementEditor {
element, element,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
appState.gridSize, snapToGrid ? appState.gridSize : null,
); );
const points = [ const points = [
...element.points.slice(0, segmentMidpoint.index!), ...element.points.slice(0, segmentMidpoint.index!),

View File

@ -203,7 +203,6 @@ describe("duplicating multiple elements", () => {
); );
clonedArrows.forEach((arrow) => { clonedArrows.forEach((arrow) => {
// console.log(arrow);
expect( expect(
clonedRectangle.boundElements!.find((e) => e.id === arrow.id), clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
).toEqual( ).toEqual(

View File

@ -1,13 +1,13 @@
import { render } from "../../../../tests/test-utils"; import { render } from "../../../../tests/test-utils";
import { API } from "../../../../tests/helpers/api"; import { API } from "../../../../tests/helpers/api";
import ExcalidrawApp from "../../../../excalidraw-app"; import { Excalidraw } from "../../../../packages/excalidraw/index";
import { measureTextElement } from "../../../textElement"; import { measureTextElement } from "../../../textElement";
import { ensureSubtypesLoaded } from "../../"; import { ensureSubtypesLoaded } from "../../";
describe("mathjax", () => { describe("mathjax", () => {
it("text-only measurements match", async () => { it("text-only measurements match", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
await ensureSubtypesLoaded(["math"]); await ensureSubtypesLoaded(["math"]);
const text = "A quick brown fox jumps over the lazy dog."; const text = "A quick brown fox jumps over the lazy dog.";
const elements = [ const elements = [
@ -19,7 +19,7 @@ describe("mathjax", () => {
expect(metrics1).toStrictEqual(metrics2); expect(metrics1).toStrictEqual(metrics2);
}); });
it("minimum height remains", async () => { it("minimum height remains", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
await ensureSubtypesLoaded(["math"]); await ensureSubtypesLoaded(["math"]);
const elements = [ const elements = [
API.createElement({ type: "text", id: "A", text: "a" }), API.createElement({ type: "text", id: "A", text: "a" }),

View File

@ -1,5 +1,5 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { GlobalTestState, render, screen } from "../tests/test-utils"; import { GlobalTestState, render, screen } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
@ -41,7 +41,7 @@ describe("textWysiwyg", () => {
describe("start text editing", () => { describe("start text editing", () => {
const { h } = window; const { h } = window;
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = []; h.elements = [];
}); });
@ -243,7 +243,7 @@ describe("textWysiwyg", () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
//@ts-ignore //@ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
@ -477,7 +477,7 @@ describe("textWysiwyg", () => {
const { h } = window; const { h } = window;
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = []; h.elements = [];
rectangle = UI.createElement("rectangle", { rectangle = UI.createElement("rectangle", {
@ -1511,7 +1511,7 @@ describe("textWysiwyg", () => {
}); });
it("should bump the version of labelled arrow when label updated", async () => { it("should bump the version of labelled arrow when label updated", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
width: 300, width: 300,
height: 0, height: 0,

7
src/hooks/useStable.ts Normal file
View File

@ -0,0 +1,7 @@
import { useRef } from "react";
export const useStable = <T extends Record<string, any>>(value: T) => {
const ref = useRef<T>(value);
Object.assign(ref.current, value);
return ref.current;
};

View File

@ -1,9 +1,9 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import ExcalidrawApp from "./excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { registerSW } from "virtual:pwa-register"; import { registerSW } from "virtual:pwa-register";
import "./excalidraw-app/sentry"; import "../excalidraw-app/sentry";
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
const rootElement = document.getElementById("root")!; const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement); const root = createRoot(rootElement);

View File

@ -264,7 +264,8 @@
"bindTextToElement": "Press enter to add text", "bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion", "eraserRevert": "Hold Alt to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page." "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
"disableSnapping": "Hold CtrlOrCmd to disable snapping"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",

View File

@ -11,29 +11,11 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased ## 0.16.0 (2023-09-19)
### renderEmbeddable
```tsx
(element: NonDeletedExcalidrawElement, radius: number, appState: UIAppState) => JSX.Element | null;`
```
The renderEmbeddable function allows you to customize the rendering of a JSX component instead of using the default `<iframe>`. By setting props.renderEmbeddable, you can provide a custom implementation for rendering the element.
#### Parameters:
- element (NonDeletedExcalidrawElement): The element to be rendered.
- radius (number): The calculated border radius in pixels.
- appState (UIAppState): The current state of the UI.
#### Return value:
JSX.Element | null: The JSX component representing the custom rendering, or null if the default `<iframe>` should be rendered.
### Features
- Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037). - Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037).
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
- Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691) - Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581). - Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728). - Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
@ -49,6 +31,235 @@ JSX.Element | null: The JSX component representing the custom rendering, or null
- `props.onClose` replaced with `props.onStateChange`. - `props.onClose` replaced with `props.onStateChange`.
- `restore()`/`restoreAppState()` now retains `appState.openSidebar` regardless of docked state. - `restore()`/`restoreAppState()` now retains `appState.openSidebar` regardless of docked state.
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Features
- allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types [#6500](https://github.com/excalidraw/excalidraw/pull/6500)
- Zen-mode/go-to-plus button style tweaks [#7006](https://github.com/excalidraw/excalidraw/pull/7006)
- Holding down CMD/CTRL will disable snap to grid when grid is active [#6983](https://github.com/excalidraw/excalidraw/pull/6983)
- Update logo [#6979](https://github.com/excalidraw/excalidraw/pull/6979)
- Export `changeProperty()` and `getFormValue()`. [#6957](https://github.com/excalidraw/excalidraw/pull/6957)
- Partition main canvas vertically [#6759](https://github.com/excalidraw/excalidraw/pull/6759)
- Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546)
- Add support for simplePDF in Web-Embeds [#6810](https://github.com/excalidraw/excalidraw/pull/6810)
- Add support for val.town embeds [#6821](https://github.com/excalidraw/excalidraw/pull/6821)
- Render bold lines in grid [#6779](https://github.com/excalidraw/excalidraw/pull/6779)
- Adds support for stackblitz.com embeds [#6813](https://github.com/excalidraw/excalidraw/pull/6813)
- Cache most of element selection [#6747](https://github.com/excalidraw/excalidraw/pull/6747)
- Support customizing what parts of frames are rendered [#6752](https://github.com/excalidraw/excalidraw/pull/6752)
- Make `appState.selectedElementIds` more stable [#6745](https://github.com/excalidraw/excalidraw/pull/6745)
- Overwrite confirmation dialogs [#6658](https://github.com/excalidraw/excalidraw/pull/6658)
- Simple analitycs [#6683](https://github.com/excalidraw/excalidraw/pull/6683)
- Introduce frames [#6123](https://github.com/excalidraw/excalidraw/pull/6123)
- Add canvas-roundrect-polyfill package [#6675](https://github.com/excalidraw/excalidraw/pull/6675)
- Polyfill `CanvasRenderingContext2D.roundRect` [#6673](https://github.com/excalidraw/excalidraw/pull/6673)
- Disable collab feature when running in iframe [#6646](https://github.com/excalidraw/excalidraw/pull/6646)
- Assign random user name when not set [#6663](https://github.com/excalidraw/excalidraw/pull/6663)
- Redesigned collab cursors [#6659](https://github.com/excalidraw/excalidraw/pull/6659)
- Eye dropper [#6615](https://github.com/excalidraw/excalidraw/pull/6615)
- Redesign of Live Collaboration dialog [#6635](https://github.com/excalidraw/excalidraw/pull/6635)
- Recover scrolled position after Library re-opening [#6624](https://github.com/excalidraw/excalidraw/pull/6624)
- Clearing library cache [#6621](https://github.com/excalidraw/excalidraw/pull/6621)
- Update design of ImageExportDialog [#6614](https://github.com/excalidraw/excalidraw/pull/6614)
- Add flipping for multiple elements [#5578](https://github.com/excalidraw/excalidraw/pull/5578)
- Color picker redesign [#6216](https://github.com/excalidraw/excalidraw/pull/6216)
- Add "unlock all elements" to canvas contextMenu [#5894](https://github.com/excalidraw/excalidraw/pull/5894)
- Library sidebar design tweaks [#6582](https://github.com/excalidraw/excalidraw/pull/6582)
- Add Trans component for interpolating JSX in translations [#6534](https://github.com/excalidraw/excalidraw/pull/6534)
- Testing simple analytics and fathom analytics for better privacy of the users [#6529](https://github.com/excalidraw/excalidraw/pull/6529)
- Retain `seed` on shift-paste [#6509](https://github.com/excalidraw/excalidraw/pull/6509)
- Allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types (#6500
### Fixes
- Improperly disabling UI pointer-events on canvas interaction [#7005](https://github.com/excalidraw/excalidraw/pull/7005)
- Several eyeDropper fixes [#7002](https://github.com/excalidraw/excalidraw/pull/7002)
- IsBindableElement to affirm frames [#6900](https://github.com/excalidraw/excalidraw/pull/6900)
- Use `device.isMobile` for sidebar trigger label breakpoint [#6994](https://github.com/excalidraw/excalidraw/pull/6994)
- Export to plus url [#6980](https://github.com/excalidraw/excalidraw/pull/6980)
- Z-index inconsistencies during addition / deletion in frames [#6914](https://github.com/excalidraw/excalidraw/pull/6914)
- Update size-limit so react is not installed as dependency [#6964](https://github.com/excalidraw/excalidraw/pull/6964)
- Stale labeled arrow bounds cache after editing the label [#6893](https://github.com/excalidraw/excalidraw/pull/6893)
- Canvas flickering due to resetting canvas on skipped frames [#6960](https://github.com/excalidraw/excalidraw/pull/6960)
- Grid jittery after partition PR [#6935](https://github.com/excalidraw/excalidraw/pull/6935)
- Regression in indexing when adding elements to frame [#6904](https://github.com/excalidraw/excalidraw/pull/6904)
- Stabilize `selectedElementIds` when box selecting [#6912](https://github.com/excalidraw/excalidraw/pull/6912)
- Resetting deleted elements on duplication [#6906](https://github.com/excalidraw/excalidraw/pull/6906)
- Make canvas compos memoize appState on props they declare [#6897](https://github.com/excalidraw/excalidraw/pull/6897)
- Scope `--color-selection` retrieval to given instance [#6886](https://github.com/excalidraw/excalidraw/pull/6886)
- Webpack config exclude statement to system agnostic [#6857](https://github.com/excalidraw/excalidraw/pull/6857)
- Remove `embeddable` from generic elements [#6853](https://github.com/excalidraw/excalidraw/pull/6853)
- Resizing arrow labels [#6789](https://github.com/excalidraw/excalidraw/pull/6789)
- Eye-dropper not working with app offset correctly on non-1 dPR [#6835](https://github.com/excalidraw/excalidraw/pull/6835)
- Add self destroying service-worker.js to migrate everyone from CRA to Vite [#6833](https://github.com/excalidraw/excalidraw/pull/6833)
- Forgotten REACT_APP env variables [#6834](https://github.com/excalidraw/excalidraw/pull/6834)
- Refresh sw when browser refreshed [#6824](https://github.com/excalidraw/excalidraw/pull/6824)
- Adding to selection via shift box-select [#6815](https://github.com/excalidraw/excalidraw/pull/6815)
- Prevent binding focus NaN value [#6803](https://github.com/excalidraw/excalidraw/pull/6803)
- Use pull request in semantic workflow for better security [#6799](https://github.com/excalidraw/excalidraw/pull/6799)
- Don't show `canvasBackground` label when `UIOptions.canvasActions.changeViewBackgroundColor` is false [#6781](https://github.com/excalidraw/excalidraw/pull/6781)
- Use subdirectory for @excalidraw/excalidraw size limit [#6787](https://github.com/excalidraw/excalidraw/pull/6787)
- Use actual dock state to not close docked library on insert [#6766](https://github.com/excalidraw/excalidraw/pull/6766)
- UI disappears when pressing the eyedropper shortcut on mobile [#6725](https://github.com/excalidraw/excalidraw/pull/6725)
- Elements in non-existing frame getting removed [#6708](https://github.com/excalidraw/excalidraw/pull/6708)
- Scrollbars renders but disable [#6706](https://github.com/excalidraw/excalidraw/pull/6706)
- Typo in chart.ts [#6696](https://github.com/excalidraw/excalidraw/pull/6696)
- Do not bind text to container using text tool when it has text already [#6694](https://github.com/excalidraw/excalidraw/pull/6694)
- Don't allow binding text to images [#6693](https://github.com/excalidraw/excalidraw/pull/6693)
- Updated link for documentation page under help section [#6654](https://github.com/excalidraw/excalidraw/pull/6654)
- Collab username style fixes [#6668](https://github.com/excalidraw/excalidraw/pull/6668)
- Bound arrows not updated when rotating multiple elements [#6662](https://github.com/excalidraw/excalidraw/pull/6662)
- Delete setCursor when resize [#6660](https://github.com/excalidraw/excalidraw/pull/6660)
- Creating text while color picker open [#6651](https://github.com/excalidraw/excalidraw/pull/6651)
- Cleanup textWysiwyg and getAdjustedDimensions [#6520](https://github.com/excalidraw/excalidraw/pull/6520)
- Eye dropper not accounting for offsets [#6640](https://github.com/excalidraw/excalidraw/pull/6640)
- Color picker input closing problem [#6599](https://github.com/excalidraw/excalidraw/pull/6599)
- Export dialog shortcut toggles console on firefox [#6620](https://github.com/excalidraw/excalidraw/pull/6620)
- Add react v17 `useTransition` polyfill [#6618](https://github.com/excalidraw/excalidraw/pull/6618)
- Library dropdown visibility issue for mobile [#6613](https://github.com/excalidraw/excalidraw/pull/6613)
- `withInternalFallback` leaking state in multi-instance scenarios [#6602](https://github.com/excalidraw/excalidraw/pull/6602)
- Language list containing duplicate `en` lang [#6583](https://github.com/excalidraw/excalidraw/pull/6583)
- Garbled text displayed on avatars [#6575](https://github.com/excalidraw/excalidraw/pull/6575)
- Assign the original text to text editor only during init [#6580](https://github.com/excalidraw/excalidraw/pull/6580)
- I18n: Apply Trans component to publish library dialogue [#6564](https://github.com/excalidraw/excalidraw/pull/6564)
- Fix brave error i18n string and remove unused [#6561](https://github.com/excalidraw/excalidraw/pull/6561)
- Revert add version tags to Docker build [#6540](https://github.com/excalidraw/excalidraw/pull/6540)
- Don't refresh dimensions for text containers on font load [#6523](https://github.com/excalidraw/excalidraw/pull/6523)
- Cleanup getMaxContainerHeight and getMaxContainerWidth [#6519](https://github.com/excalidraw/excalidraw/pull/6519)
- Cleanup redrawTextBoundingBox [#6518](https://github.com/excalidraw/excalidraw/pull/6518)
- Text jumps when editing on Android Chrome [#6503](https://github.com/excalidraw/excalidraw/pull/6503)
### Styles
- Removes extra spaces [#6558](https://github.com/excalidraw/excalidraw/pull/6558)
- Fix font family inconsistencies [#6501](https://github.com/excalidraw/excalidraw/pull/6501)
### Refactor
- Factor out shape generation from `renderElement.ts` pt 2 [#6878](https://github.com/excalidraw/excalidraw/pull/6878)
- Add typeScript support to enforce valid translation keys [#6776](https://github.com/excalidraw/excalidraw/pull/6776)
- Simplify `ImageExportDialog` [#6578](https://github.com/excalidraw/excalidraw/pull/6578)
### Performance
- Limiting the suggested binding to fix performance issue [#6877](https://github.com/excalidraw/excalidraw/pull/6877)
- Memoize rendering of library [#6622](https://github.com/excalidraw/excalidraw/pull/6622)
- Improve rendering performance for Library [#6587](https://github.com/excalidraw/excalidraw/pull/6587)
- Use `UIAppState` where possible to reduce UI rerenders [#6560](https://github.com/excalidraw/excalidraw/pull/6560)
### Build
- Increase limit for bundle by 1kb [#6880](https://github.com/excalidraw/excalidraw/pull/6880)
- Update to node 18 in docker [#6822](https://github.com/excalidraw/excalidraw/pull/6822)
- Migrate to Vite 🚀 [#6818](https://github.com/excalidraw/excalidraw/pull/6818)
- Migrate to Vite 🚀 [#6713](https://github.com/excalidraw/excalidraw/pull/6713)
- Increase limit to 290 kB for prod bundle [#6809](https://github.com/excalidraw/excalidraw/pull/6809)
- Add version tags to Docker build [#6508](https://github.com/excalidraw/excalidraw/pull/6508)
---
## 0.15.2 (2023-04-20) ## 0.15.2 (2023-04-20)
### Docs ### Docs

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.15.2", "version": "0.16.0",
"main": "main.js", "main": "main.js",
"types": "types/packages/excalidraw/index.d.ts", "types": "types/packages/excalidraw/index.d.ts",
"files": [ "files": [

View File

@ -3,7 +3,7 @@ import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random"; import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils"; import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { vi } from "vitest"; import { vi } from "vitest";
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
@ -35,7 +35,7 @@ describe("Test <App/>", () => {
}; };
}; };
await render(<ExcalidrawApp />); await render(<Excalidraw />);
expect( expect(
queryByTestId( queryByTestId(
document.querySelector(".excalidraw-modal-container")!, document.querySelector(".excalidraw-modal-container")!,

View File

@ -0,0 +1,50 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
<div
data-testid="brave-measure-text-error"
>
<p>
Looks like you are using Brave browser with the
<span
style="font-weight: 600;"
>
Aggressively Block Fingerprinting
</span>
setting enabled.
</p>
<p>
This could result in breaking the
<span
style="font-weight: 600;"
>
Text Elements
</span>
in your drawings.
</p>
<p>
We strongly recommend disabling this setting. You can follow
<a
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
>
these steps
</a>
on how to do so.
</p>
<p>
If disabling this setting doesn't fix the display of text elements, please open an
<a
href="https://github.com/excalidraw/excalidraw/issues/new"
>
issue
</a>
on our GitHub, or write us on
<a
href="https://discord.gg/UexuTaE"
>
Discord
.
</a>
</p>
</div>
`;

View File

@ -13089,126 +13089,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] number of elemen
exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `7`; exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `7`;
exports[`regression tests > rerenders UI on language change > [end of test] appState 1`] = `
{
"activeEmbeddable": null,
"activeTool": {
"customType": null,
"lastActiveTool": null,
"locked": false,
"type": "rectangle",
},
"collaborators": Map {},
"contextMenu": null,
"currentChartType": "bar",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "hachure",
"currentItemFontFamily": 1,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemRoundness": "round",
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 1,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"elementsToHighlight": null,
"errorMessage": null,
"exportBackground": true,
"exportEmbedScene": false,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"offsetLeft": 0,
"offsetTop": 0,
"openDialog": null,
"openMenu": "canvas",
"openPopup": null,
"openSidebar": null,
"pasteDialog": {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElementId": null,
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
"startBoundElement": null,
"suggestedBindings": [],
"theme": "light",
"toast": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024,
"zenModeEnabled": false,
"zoom": {
"value": 1,
},
}
`;
exports[`regression tests > rerenders UI on language change > [end of test] history 1`] = `
{
"recording": false,
"redoStack": [],
"stateHistory": [
{
"appState": {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": {},
"selectedGroupIds": {},
"viewBackgroundColor": "#ffffff",
},
"elements": [],
},
],
}
`;
exports[`regression tests > rerenders UI on language change > [end of test] number of elements 1`] = `0`;
exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `5`;
exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = ` exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = `
{ {
"activeEmbeddable": null, "activeEmbeddable": null,

View File

@ -1,4 +1,4 @@
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { CODES } from "../keys"; import { CODES } from "../keys";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
@ -9,7 +9,7 @@ import {
screen, screen,
togglePopover, togglePopover,
} from "../tests/test-utils"; } from "../tests/test-utils";
import { copiedStyles } from "./actionStyles"; import { copiedStyles } from "../actions/actionStyles";
const { h } = window; const { h } = window;
@ -17,7 +17,7 @@ const mouse = new Pointer("mouse");
describe("actionStyles", () => { describe("actionStyles", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -1,6 +1,6 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render } from "./test-utils"; import { render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../../src/packages/excalidraw/index";
import { defaultLang, setLanguage } from "../i18n"; import { defaultLang, setLanguage } from "../i18n";
import { UI, Pointer, Keyboard } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
@ -60,7 +60,7 @@ describe("aligning", () => {
mouse.reset(); mouse.reset();
await setLanguage(defaultLang); await setLanguage(defaultLang);
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
}); });
it("aligns two objects correctly to the top", () => { it("aligns two objects correctly to the top", () => {

View File

@ -1,6 +1,6 @@
import { queryByTestId, render, waitFor } from "./test-utils"; import { queryByTestId, render, waitFor } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
@ -14,14 +14,17 @@ describe("appState", () => {
const defaultAppState = getDefaultAppState(); const defaultAppState = getDefaultAppState();
const exportBackground = !defaultAppState.exportBackground; const exportBackground = !defaultAppState.exportBackground;
await render(<ExcalidrawApp />, { await render(
localStorageData: { <Excalidraw
appState: { initialData={{
exportBackground, appState: {
viewBackgroundColor: "#F00", exportBackground,
}, viewBackgroundColor: "#F00",
}, },
}); }}
/>,
{},
);
await waitFor(() => { await waitFor(() => {
expect(h.state.exportBackground).toBe(exportBackground); expect(h.state.exportBackground).toBe(exportBackground);
@ -53,13 +56,15 @@ describe("appState", () => {
}); });
it("changing fontSize with text tool selected (no element created yet)", async () => { it("changing fontSize with text tool selected (no element created yet)", async () => {
const { container } = await render(<ExcalidrawApp />, { const { container } = await render(
localStorageData: { <Excalidraw
appState: { initialData={{
currentItemFontSize: 30, appState: {
}, currentItemFontSize: 30,
}, },
}); }}
/>,
);
UI.clickTool("text"); UI.clickTool("text");

View File

@ -1,5 +1,5 @@
import { fireEvent, render } from "./test-utils"; import { fireEvent, render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../../src/packages/excalidraw/index";
import { UI, Pointer, Keyboard } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
import { getTransformHandles } from "../element/transformHandles"; import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
@ -12,7 +12,7 @@ const mouse = new Pointer("mouse");
describe("element binding", () => { describe("element binding", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
}); });
it("should create valid binding if duplicate start/end points", async () => { it("should create valid binding if duplicate start/end points", async () => {

View File

@ -7,7 +7,7 @@ import {
createPasteEvent, createPasteEvent,
} from "./test-utils"; } from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui"; import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { import {
getDefaultLineHeight, getDefaultLineHeight,
@ -79,8 +79,13 @@ beforeEach(async () => {
mouse.reset(); mouse.reset();
await render(<ExcalidrawApp />); await render(
h.app.setAppState({ zoom: { value: 1 as NormalizedZoomValue } }); <Excalidraw
autoFocus={true}
handleKeyboardGlobally={true}
initialData={{ appState: { zoom: { value: 1 as NormalizedZoomValue } } }}
/>,
);
setClipboardText(""); setClipboardText("");
Object.assign(document, { Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas, elementFromPoint: () => GlobalTestState.canvas,
@ -91,7 +96,6 @@ describe("general paste behavior", () => {
it("should randomize seed on paste", async () => { it("should randomize seed on paste", async () => {
const rectangle = API.createElement({ type: "rectangle" }); const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!; const clipboardJSON = (await copyToClipboard([rectangle], null))!;
pasteWithCtrlCmdV(clipboardJSON); pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => { await waitFor(() => {

View File

@ -11,7 +11,7 @@ import {
waitFor, waitFor,
togglePopover, togglePopover,
} from "./test-utils"; } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random"; import { reseed } from "../random";
import { UI, Pointer, Keyboard } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
@ -20,7 +20,6 @@ import { ShortcutName } from "../actions/shortcuts";
import { copiedStyles } from "../actions/actionStyles"; import { copiedStyles } from "../actions/actionStyles";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { setDateTimeForTests } from "../utils"; import { setDateTimeForTests } from "../utils";
import { LibraryItem } from "../types";
import { vi } from "vitest"; import { vi } from "vitest";
const checkpoint = (name: string) => { const checkpoint = (name: string) => {
@ -56,7 +55,7 @@ describe("contextMenu element", () => {
reseed(7); reseed(7);
setDateTimeForTests("201933152653"); setDateTimeForTests("201933152653");
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
}); });
beforeAll(() => { beforeAll(() => {
@ -394,11 +393,9 @@ describe("contextMenu element", () => {
const contextMenu = UI.queryContextMenu(); const contextMenu = UI.queryContextMenu();
fireEvent.click(queryByText(contextMenu!, "Add to library")!); fireEvent.click(queryByText(contextMenu!, "Add to library")!);
await waitFor(() => { await waitFor(async () => {
const library = localStorage.getItem("excalidraw-library"); const libraryItems = await h.app.library.getLatestLibrary();
expect(library).not.toBeNull(); expect(libraryItems[0].elements[0]).toEqual(h.elements[0]);
const addedElement = JSON.parse(library!)[0] as LibraryItem;
expect(addedElement.elements[0]).toEqual(h.elements[0]);
}); });
}); });

View File

@ -2,7 +2,7 @@ import { ExcalidrawElement } from "../element/types";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { render } from "./test-utils"; import { render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { import {
CustomShortcutName, CustomShortcutName,
getShortcutFromShortcutName, getShortcutFromShortcutName,
@ -27,7 +27,7 @@ describe("regression tests", () => {
}); });
it("should apply universal action predicates", async () => { it("should apply universal action predicates", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
// Create the test elements // Create the test elements
const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 }); const el1 = API.createElement({ type: "rectangle", id: "A", y: 0 });
const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 }); const el2 = API.createElement({ type: "rectangle", id: "B", y: 30 });

View File

@ -1,5 +1,5 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { import {
@ -30,7 +30,7 @@ const { h } = window;
describe("Test dragCreate", () => { describe("Test dragCreate", () => {
describe("add element to the scene when pointer dragging long enough", () => { describe("add element to the scene when pointer dragging long enough", () => {
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("rectangle"); const tool = getByToolName("rectangle");
fireEvent.click(tool); fireEvent.click(tool);
@ -62,7 +62,7 @@ describe("Test dragCreate", () => {
}); });
it("ellipse", async () => { it("ellipse", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("ellipse"); const tool = getByToolName("ellipse");
fireEvent.click(tool); fireEvent.click(tool);
@ -95,7 +95,7 @@ describe("Test dragCreate", () => {
}); });
it("diamond", async () => { it("diamond", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("diamond"); const tool = getByToolName("diamond");
fireEvent.click(tool); fireEvent.click(tool);
@ -127,7 +127,7 @@ describe("Test dragCreate", () => {
}); });
it("arrow", async () => { it("arrow", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("arrow"); const tool = getByToolName("arrow");
fireEvent.click(tool); fireEvent.click(tool);
@ -163,7 +163,7 @@ describe("Test dragCreate", () => {
}); });
it("line", async () => { it("line", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("line"); const tool = getByToolName("line");
fireEvent.click(tool); fireEvent.click(tool);
@ -207,7 +207,7 @@ describe("Test dragCreate", () => {
}); });
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("rectangle"); const tool = getByToolName("rectangle");
fireEvent.click(tool); fireEvent.click(tool);
@ -227,7 +227,7 @@ describe("Test dragCreate", () => {
}); });
it("ellipse", async () => { it("ellipse", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("ellipse"); const tool = getByToolName("ellipse");
fireEvent.click(tool); fireEvent.click(tool);
@ -247,7 +247,7 @@ describe("Test dragCreate", () => {
}); });
it("diamond", async () => { it("diamond", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("diamond"); const tool = getByToolName("diamond");
fireEvent.click(tool); fireEvent.click(tool);
@ -267,7 +267,9 @@ describe("Test dragCreate", () => {
}); });
it("arrow", async () => { it("arrow", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
// select tool // select tool
const tool = getByToolName("arrow"); const tool = getByToolName("arrow");
fireEvent.click(tool); fireEvent.click(tool);
@ -292,7 +294,9 @@ describe("Test dragCreate", () => {
}); });
it("line", async () => { it("line", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
// select tool // select tool
const tool = getByToolName("line"); const tool = getByToolName("line");
fireEvent.click(tool); fireEvent.click(tool);

View File

@ -1,5 +1,5 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { render } from "../tests/test-utils"; import { render } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@ -15,7 +15,7 @@ const h = window.h;
describe("element locking", () => { describe("element locking", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = []; h.elements = [];
}); });

View File

@ -1,5 +1,5 @@
import { render, waitFor } from "./test-utils"; import { render, waitFor } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { import {
encodePngMetadata, encodePngMetadata,
@ -42,7 +42,7 @@ Object.defineProperty(window, "TextDecoder", {
describe("export", () => { describe("export", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("export embedded png and reimport", async () => { it("export embedded png and reimport", async () => {

View File

@ -1,14 +1,14 @@
import { render } from "./test-utils"; import { render } from "./test-utils";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { vi } from "vitest"; import { vi } from "vitest";
const { h } = window; const { h } = window;
describe("fitToContent", () => { describe("fitToContent", () => {
it("should zoom to fit the selected element", async () => { it("should zoom to fit the selected element", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
h.state.width = 10; h.state.width = 10;
h.state.height = 10; h.state.height = 10;
@ -30,7 +30,7 @@ describe("fitToContent", () => {
}); });
it("should zoom to fit multiple elements", async () => { it("should zoom to fit multiple elements", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
const topLeft = API.createElement({ const topLeft = API.createElement({
width: 20, width: 20,
@ -61,7 +61,7 @@ describe("fitToContent", () => {
}); });
it("should scroll the viewport to the selected element", async () => { it("should scroll the viewport to the selected element", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
h.state.width = 10; h.state.width = 10;
h.state.height = 10; h.state.height = 10;
@ -106,7 +106,7 @@ describe("fitToContent animated", () => {
}); });
it("should ease scroll the viewport to the selected element", async () => { it("should ease scroll the viewport to the selected element", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
h.state.width = 10; h.state.width = 10;
h.state.height = 10; h.state.height = 10;
@ -142,7 +142,7 @@ describe("fitToContent animated", () => {
}); });
it("should animate the scroll but not the zoom", async () => { it("should animate the scroll but not the zoom", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
h.state.width = 50; h.state.width = 50;
h.state.height = 50; h.state.height = 50;

View File

@ -19,7 +19,7 @@ import {
FileId, FileId,
} from "../element/types"; } from "../element/types";
import { newLinearElement } from "../element"; import { newLinearElement } from "../element";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { NormalizedZoomValue } from "../types"; import { NormalizedZoomValue } from "../types";
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
@ -52,7 +52,7 @@ beforeEach(async () => {
Object.assign(document, { Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas, elementFromPoint: () => GlobalTestState.canvas,
}); });
await render(<ExcalidrawApp />); await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
h.setState({ h.setState({
zoom: { zoom: {
value: 1 as NormalizedZoomValue, value: 1 as NormalizedZoomValue,

View File

@ -1,5 +1,5 @@
import { assertSelectedElements, render } from "./test-utils"; import { assertSelectedElements, render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { Keyboard, Pointer, UI } from "./helpers/ui"; import { Keyboard, Pointer, UI } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
@ -13,14 +13,16 @@ const mouse = new Pointer("mouse");
describe("history", () => { describe("history", () => {
it("initializing scene should end up with single history entry", async () => { it("initializing scene should end up with single history entry", async () => {
await render(<ExcalidrawApp />, { await render(
localStorageData: { <Excalidraw
elements: [API.createElement({ type: "rectangle", id: "A" })], initialData={{
appState: { elements: [API.createElement({ type: "rectangle", id: "A" })],
zenModeEnabled: true, appState: {
}, zenModeEnabled: true,
}, },
}); }}
/>,
);
await waitFor(() => expect(h.state.zenModeEnabled).toBe(true)); await waitFor(() => expect(h.state.zenModeEnabled).toBe(true));
await waitFor(() => await waitFor(() =>
@ -60,14 +62,16 @@ describe("history", () => {
}); });
it("scene import via drag&drop should create new history entry", async () => { it("scene import via drag&drop should create new history entry", async () => {
await render(<ExcalidrawApp />, { await render(
localStorageData: { <Excalidraw
elements: [API.createElement({ type: "rectangle", id: "A" })], initialData={{
appState: { elements: [API.createElement({ type: "rectangle", id: "A" })],
viewBackgroundColor: "#FFF", appState: {
}, viewBackgroundColor: "#FFF",
}, },
}); }}
/>,
);
await waitFor(() => expect(h.state.viewBackgroundColor).toBe("#FFF")); await waitFor(() => expect(h.state.viewBackgroundColor).toBe("#FFF"));
await waitFor(() => await waitFor(() =>
@ -113,7 +117,7 @@ describe("history", () => {
}); });
it("undo/redo works properly with groups", async () => { it("undo/redo works properly with groups", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] }); const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] }); const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });

View File

@ -2,7 +2,7 @@ import { vi } from "vitest";
import { fireEvent, render, waitFor } from "./test-utils"; import { fireEvent, render, waitFor } from "./test-utils";
import { queryByTestId } from "@testing-library/react"; import { queryByTestId } from "@testing-library/react";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import { LibraryItem, LibraryItems } from "../types"; import { LibraryItem, LibraryItems } from "../types";
@ -42,7 +42,7 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => {
describe("library", () => { describe("library", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
h.app.library.resetLibrary(); h.app.library.resetLibrary();
}); });
@ -189,7 +189,7 @@ describe("library", () => {
describe("library menu", () => { describe("library menu", () => {
it("should load library from file picker", async () => { it("should load library from file picker", async () => {
const { container } = await render(<ExcalidrawApp />); const { container } = await render(<Excalidraw />);
const latestLibrary = await h.app.library.getLatestLibrary(); const latestLibrary = await h.app.library.getLatestLibrary();
expect(latestLibrary.length).toBe(0); expect(latestLibrary.length).toBe(0);

View File

@ -5,7 +5,7 @@ import {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
FontString, FontString,
} from "../element/types"; } from "../element/types";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { centerPoint } from "../math"; import { centerPoint } from "../math";
import { reseed } from "../random"; import { reseed } from "../random";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
@ -43,7 +43,7 @@ describe("Test Linear Elements", () => {
renderInteractiveScene.mockClear(); renderInteractiveScene.mockClear();
renderStaticScene.mockClear(); renderStaticScene.mockClear();
reseed(7); reseed(7);
const comp = await render(<ExcalidrawApp />); const comp = await render(<Excalidraw handleKeyboardGlobally={true} />);
h.state.width = 1000; h.state.width = 1000;
h.state.height = 1000; h.state.height = 1000;
container = comp.container; container = comp.container;

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render, fireEvent } from "./test-utils"; import { render, fireEvent } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random"; import { reseed } from "../random";
import { bindOrUnbindLinearElement } from "../element/binding"; import { bindOrUnbindLinearElement } from "../element/binding";
@ -31,7 +31,7 @@ const { h } = window;
describe("move element", () => { describe("move element", () => {
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
@ -67,7 +67,7 @@ describe("move element", () => {
}); });
it("rectangles with binding arrow", async () => { it("rectangles with binding arrow", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
// create elements // create elements
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
@ -119,7 +119,7 @@ describe("move element", () => {
describe("duplicate element on move when ALT is clicked", () => { describe("duplicate element on move when ALT is clicked", () => {
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {

View File

@ -5,7 +5,7 @@ import {
mockBoundingClientRect, mockBoundingClientRect,
restoreOriginalGetBoundingClientRect, restoreOriginalGetBoundingClientRect,
} from "./test-utils"; } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { ExcalidrawLinearElement } from "../element/types"; import { ExcalidrawLinearElement } from "../element/types";
@ -29,7 +29,7 @@ const { h } = window;
describe("remove shape in non linear elements", () => { describe("remove shape in non linear elements", () => {
beforeAll(() => { beforeAll(() => {
mockBoundingClientRect(); mockBoundingClientRect({ width: 1000, height: 1000 });
}); });
afterAll(() => { afterAll(() => {
@ -37,12 +37,13 @@ describe("remove shape in non linear elements", () => {
}); });
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("rectangle"); const tool = getByToolName("rectangle");
fireEvent.click(tool); fireEvent.click(tool);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
@ -52,7 +53,7 @@ describe("remove shape in non linear elements", () => {
}); });
it("ellipse", async () => { it("ellipse", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("ellipse"); const tool = getByToolName("ellipse");
fireEvent.click(tool); fireEvent.click(tool);
@ -67,7 +68,7 @@ describe("remove shape in non linear elements", () => {
}); });
it("diamond", async () => { it("diamond", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("diamond"); const tool = getByToolName("diamond");
fireEvent.click(tool); fireEvent.click(tool);
@ -84,7 +85,7 @@ describe("remove shape in non linear elements", () => {
describe("multi point mode in linear elements", () => { describe("multi point mode in linear elements", () => {
it("arrow", async () => { it("arrow", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("arrow"); const tool = getByToolName("arrow");
fireEvent.click(tool); fireEvent.click(tool);
@ -109,8 +110,8 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderInteractiveScene).toHaveBeenCalledTimes(10); expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(renderStaticScene).toHaveBeenCalledTimes(10);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;
@ -128,7 +129,7 @@ describe("multi point mode in linear elements", () => {
}); });
it("line", async () => { it("line", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("line"); const tool = getByToolName("line");
fireEvent.click(tool); fireEvent.click(tool);
@ -153,8 +154,8 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderInteractiveScene).toHaveBeenCalledTimes(10); expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(renderStaticScene).toHaveBeenCalledTimes(10);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;

View File

@ -1,7 +1,7 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { reseed } from "../random"; import { reseed } from "../random";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { setDateTimeForTests } from "../utils"; import { setDateTimeForTests } from "../utils";
@ -13,9 +13,7 @@ import {
render, render,
screen, screen,
togglePopover, togglePopover,
waitFor,
} from "./test-utils"; } from "./test-utils";
import { defaultLang } from "../i18n";
import { FONT_FAMILY } from "../constants"; import { FONT_FAMILY } from "../constants";
import { vi } from "vitest"; import { vi } from "vitest";
@ -56,7 +54,7 @@ beforeEach(async () => {
finger1.reset(); finger1.reset();
finger2.reset(); finger2.reset();
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
h.setState({ height: 768, width: 1024 }); h.setState({ height: 768, width: 1024 });
}); });
@ -443,26 +441,6 @@ describe("regression tests", () => {
expect(h.state.zoom.value).toBe(1); expect(h.state.zoom.value).toBe(1);
}); });
it("rerenders UI on language change", async () => {
// select rectangle tool to show properties menu
UI.clickTool("rectangle");
// english lang should display `thin` label
expect(screen.queryByTitle(/thin/i)).not.toBeNull();
fireEvent.click(document.querySelector(".dropdown-menu-button")!);
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: "de-DE" },
});
// switching to german, `thin` label should no longer exist
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
// reset language
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: defaultLang.code },
});
// switching back to English
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
});
it("make a group and duplicate it", () => { it("make a group and duplicate it", () => {
UI.clickTool("rectangle"); UI.clickTool("rectangle");
mouse.down(10, 10); mouse.down(10, 10);

View File

@ -6,7 +6,7 @@ import { reseed } from "../random";
import { UI, Keyboard } from "./helpers/ui"; import { UI, Keyboard } from "./helpers/ui";
import { resize } from "./utils"; import { resize } from "./utils";
import { ExcalidrawTextElement } from "../element/types"; import { ExcalidrawTextElement } from "../element/types";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { vi } from "vitest"; import { vi } from "vitest";
@ -126,7 +126,7 @@ describe("resize rectangle ellipses and diamond elements", () => {
describe("Test text element", () => { describe("Test text element", () => {
it("should update font size via keyboard", async () => { it("should update font size via keyboard", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw handleKeyboardGlobally={true} />);
const textElement = API.createElement({ const textElement = API.createElement({
type: "text", type: "text",

View File

@ -8,7 +8,6 @@ import { Excalidraw } from "../packages/excalidraw/index";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui"; import { Keyboard } from "./helpers/ui";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import ExcalidrawApp from "../excalidraw-app";
const { h } = window; const { h } = window;
@ -56,7 +55,7 @@ describe("appState", () => {
it("moving by page up/down/left/right", async () => { it("moving by page up/down/left/right", async () => {
mockBoundingClientRect(); mockBoundingClientRect();
await render(<ExcalidrawApp />, {}); await render(<Excalidraw handleKeyboardGlobally={true} />, {});
const scrollTest = () => { const scrollTest = () => {
const initialScrollY = h.state.scrollY; const initialScrollY = h.state.scrollY;

View File

@ -6,7 +6,7 @@ import {
restoreOriginalGetBoundingClientRect, restoreOriginalGetBoundingClientRect,
assertSelectedElements, assertSelectedElements,
} from "./test-utils"; } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { reseed } from "../random"; import { reseed } from "../random";
@ -34,7 +34,7 @@ const mouse = new Pointer("mouse");
describe("box-selection", () => { describe("box-selection", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("should allow adding to selection via box-select when holding shift", async () => { it("should allow adding to selection via box-select when holding shift", async () => {
@ -102,7 +102,7 @@ describe("box-selection", () => {
describe("inner box-selection", () => { describe("inner box-selection", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("selecting elements visually nested inside another", async () => { it("selecting elements visually nested inside another", async () => {
const rect1 = API.createElement({ const rect1 = API.createElement({
@ -218,7 +218,7 @@ describe("inner box-selection", () => {
describe("selection element", () => { describe("selection element", () => {
it("create selection element on pointer down", async () => { it("create selection element on pointer down", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("selection"); const tool = getByToolName("selection");
fireEvent.click(tool); fireEvent.click(tool);
@ -239,7 +239,7 @@ describe("selection element", () => {
}); });
it("resize selection element on pointer move", async () => { it("resize selection element on pointer move", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("selection"); const tool = getByToolName("selection");
fireEvent.click(tool); fireEvent.click(tool);
@ -261,7 +261,7 @@ describe("selection element", () => {
}); });
it("remove selection element on pointer up", async () => { it("remove selection element on pointer up", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<Excalidraw />);
// select tool // select tool
const tool = getByToolName("selection"); const tool = getByToolName("selection");
fireEvent.click(tool); fireEvent.click(tool);
@ -287,7 +287,9 @@ describe("select single element on the scene", () => {
}); });
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
// create element // create element
@ -317,7 +319,9 @@ describe("select single element on the scene", () => {
}); });
it("diamond", async () => { it("diamond", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
// create element // create element
@ -347,7 +351,9 @@ describe("select single element on the scene", () => {
}); });
it("ellipse", async () => { it("ellipse", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
// create element // create element
@ -377,7 +383,9 @@ describe("select single element on the scene", () => {
}); });
it("arrow", async () => { it("arrow", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
// create element // create element
@ -419,7 +427,9 @@ describe("select single element on the scene", () => {
}); });
it("arrow escape", async () => { it("arrow escape", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(
<Excalidraw handleKeyboardGlobally={true} />,
);
const canvas = container.querySelector("canvas.interactive")!; const canvas = container.querySelector("canvas.interactive")!;
{ {
// create element // create element
@ -464,7 +474,7 @@ describe("select single element on the scene", () => {
describe("tool locking & selection", () => { describe("tool locking & selection", () => {
it("should not select newly created element while tool is locked", async () => { it("should not select newly created element while tool is locked", async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
UI.clickTool("lock"); UI.clickTool("lock");
expect(h.state.activeTool.locked).toBe(true); expect(h.state.activeTool.locked).toBe(true);
@ -480,7 +490,7 @@ describe("tool locking & selection", () => {
describe("selectedElementIds stability", () => { describe("selectedElementIds stability", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("box-selection should be stable when not changing selection", () => { it("box-selection should be stable when not changing selection", () => {

View File

@ -16,7 +16,7 @@ import {
import { render } from "./test-utils"; import { render } from "./test-utils";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { import {
ExcalidrawElement, ExcalidrawElement,
@ -252,7 +252,7 @@ const { h } = window;
describe("subtype registration", () => { describe("subtype registration", () => {
it("should check for invalid subtype or parents", async () => { it("should check for invalid subtype or parents", async () => {
await render(<ExcalidrawApp />, {}); await render(<Excalidraw />, {});
// Define invalid subtype records // Define invalid subtype records
const null1 = {} as SubtypeRecord; const null1 = {} as SubtypeRecord;
const null2 = { subtype: "" } as SubtypeRecord; const null2 = { subtype: "" } as SubtypeRecord;
@ -368,7 +368,7 @@ describe("subtypes", () => {
API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }), API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }),
API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }), API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }),
]; ];
await render(<ExcalidrawApp />, { localStorageData: { elements } }); await render(<Excalidraw />, { localStorageData: { elements } });
elements.forEach((el) => expect(el.subtype).toBe(test1.subtype)); elements.forEach((el) => expect(el.subtype).toBe(test1.subtype));
}); });
it("should enforce prop value restrictions", async () => { it("should enforce prop value restrictions", async () => {
@ -381,7 +381,7 @@ describe("subtypes", () => {
}), }),
API.createElement({ type: "line", id: "B", roughness: 1 }), API.createElement({ type: "line", id: "B", roughness: 1 }),
]; ];
await render(<ExcalidrawApp />, { localStorageData: { elements } }); await render(<Excalidraw />, { localStorageData: { elements } });
elements.forEach((el) => { elements.forEach((el) => {
if (el.subtype === test1.subtype) { if (el.subtype === test1.subtype) {
expect(el.roughness).toBe(0); expect(el.roughness).toBe(0);
@ -440,7 +440,7 @@ describe("subtypes", () => {
fontSize: FONTSIZE, fontSize: FONTSIZE,
}), }),
]; ];
await render(<ExcalidrawApp />, { localStorageData: { elements } }); await render(<Excalidraw />, { localStorageData: { elements } });
const mockMeasureText = (text: string, font: FontString) => { const mockMeasureText = (text: string, font: FontString) => {
if (text === testString) { if (text === testString) {
let multiplier = 1; let multiplier = 1;
@ -608,7 +608,7 @@ describe("subtype actions", () => {
API.createElement({ type: "line", id: "C", subtype: test3.subtype }), API.createElement({ type: "line", id: "C", subtype: test3.subtype }),
API.createElement({ type: "text", id: "D", subtype: test3.subtype }), API.createElement({ type: "text", id: "D", subtype: test3.subtype }),
]; ];
await render(<ExcalidrawApp />, { localStorageData: { elements } }); await render(<Excalidraw />, { localStorageData: { elements } });
}); });
it("should apply to elements with their subtype", async () => { it("should apply to elements with their subtype", async () => {
h.setState({ selectedElementIds: { A: true } }); h.setState({ selectedElementIds: { A: true } });
@ -672,7 +672,8 @@ describe("subtype loading", () => {
text: testString, text: testString,
}), }),
]; ];
await render(<ExcalidrawApp />, { localStorageData: { elements } }); await render(<Excalidraw />, { localStorageData: { elements } });
h.elements = elements;
}); });
it("should redraw text bounding boxes", async () => { it("should redraw text bounding boxes", async () => {
h.setState({ selectedElementIds: { A: true } }); h.setState({ selectedElementIds: { A: true } });

View File

@ -11,7 +11,7 @@ import {
import * as toolQueries from "./queries/toolQueries"; import * as toolQueries from "./queries/toolQueries";
import { ImportedDataState } from "../data/types"; import { ImportedDataState } from "../data/types";
import { STORAGE_KEYS } from "../excalidraw-app/app_constants"; import { STORAGE_KEYS } from "../../excalidraw-app/app_constants";
import { SceneData } from "../types"; import { SceneData } from "../types";
import { getSelectedElements } from "../scene/selection"; import { getSelectedElements } from "../scene/selection";

View File

@ -1,5 +1,5 @@
import { render, GlobalTestState } from "./test-utils"; import { render, GlobalTestState } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { Keyboard, Pointer, UI } from "./helpers/ui"; import { Keyboard, Pointer, UI } from "./helpers/ui";
import { CURSOR_TYPE } from "../constants"; import { CURSOR_TYPE } from "../constants";
@ -12,7 +12,7 @@ const pointerTypes = [mouse, touch, pen];
describe("view mode", () => { describe("view mode", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("after switching to view mode cursor type should be pointer", async () => { it("after switching to view mode cursor type should be pointer", async () => {

View File

@ -1,6 +1,6 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render } from "./test-utils"; import { render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import { Excalidraw } from "../packages/excalidraw/index";
import { reseed } from "../random"; import { reseed } from "../random";
import { import {
actionSendBackward, actionSendBackward,
@ -121,7 +121,6 @@ const assertZindex = ({
operations: [Actions, string[]][]; operations: [Actions, string[]][];
}) => { }) => {
const selectedElementIds = populateElements(elements, appState); const selectedElementIds = populateElements(elements, appState);
operations.forEach(([action, expected]) => { operations.forEach(([action, expected]) => {
h.app.actionManager.executeAction(action); h.app.actionManager.executeAction(action);
expect(h.elements.map((element) => element.id)).toEqual(expected); expect(h.elements.map((element) => element.id)).toEqual(expected);
@ -131,7 +130,7 @@ const assertZindex = ({
describe("z-index manipulation", () => { describe("z-index manipulation", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<Excalidraw />);
}); });
it("send back", () => { it("send back", () => {

View File

@ -96,9 +96,9 @@ const getTargetIndexAccountingForBinding = (
if (direction === "left") { if (direction === "left") {
return elements.indexOf(nextElement); return elements.indexOf(nextElement);
} }
const boundTextElement = const boundTextElement =
Scene.getScene(nextElement)!.getElement(boundElementId); Scene.getScene(nextElement)!.getElement(boundElementId);
if (boundTextElement) { if (boundTextElement) {
return elements.indexOf(boundTextElement); return elements.indexOf(boundTextElement);
} }

View File

@ -16,6 +16,6 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src"], "include": ["src", "excalidraw-app"],
"exclude": ["src/packages/excalidraw/types"] "exclude": ["src/packages/excalidraw/types"]
} }