diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx new file mode 100644 index 000000000..3c1baeeaf --- /dev/null +++ b/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx @@ -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_** + +
+  convertToExcalidrawElements(elements:{" "}
+  
+    ExcalidrawElementSkeleton
+  
+  )
+
+ +**_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 ( +
+ +
+ ); +} +``` + +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) diff --git a/dev-docs/package.json b/dev-docs/package.json index bef57d76e..4601891c9 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "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", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/sidebars.js b/dev-docs/sidebars.js index 86356aeff..257d16b54 100644 --- a/dev-docs/sidebars.js +++ b/dev-docs/sidebars.js @@ -81,12 +81,8 @@ const sidebars = { "@excalidraw/excalidraw/api/utils/restore", ], }, - { - type: "category", - label: "Constants", - link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" }, - items: [], - }, + "@excalidraw/excalidraw/api/constants", + "@excalidraw/excalidraw/api/excalidraw-element-skeleton", ], }, "@excalidraw/excalidraw/faq", diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 3ef8a9190..f06b9fb24 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.15.2-6546-3398d86": - version "0.15.2-6546-3398d86" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-6546-3398d86.tgz#e74d5ad944b8b414924d27ee91469a32b4f08dbf" - integrity sha512-Tzq6qighJUytXRA8iMzQ8onoGclo9CuvPSw7DMvPxME8nxAxn5CeK/gsxIs3zwooj9CC6XF42BSrx0+n+fPxaQ== +"@excalidraw/excalidraw@0.15.2-eb020d0": + version "0.15.2-eb020d0" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-eb020d0.tgz#25bd61e6f23da7c084fb16a3e0fe0dd9ad8e6533" + integrity sha512-TKGLzpOVqFQcwK1GFKTDXgg1s2U6tc5KE3qXuv87osbzOtftQn3x4+VH61vwdj11l00nEN80SMdXUC43T9uJqQ== "@hapi/hoek@^9.0.0": version "9.3.0" diff --git a/src/excalidraw-app/CustomStats.tsx b/excalidraw-app/CustomStats.tsx similarity index 86% rename from src/excalidraw-app/CustomStats.tsx rename to excalidraw-app/CustomStats.tsx index addea4cd9..b34c1f7d7 100644 --- a/src/excalidraw-app/CustomStats.tsx +++ b/excalidraw-app/CustomStats.tsx @@ -1,14 +1,14 @@ import { useEffect, useState } from "react"; -import { debounce, getVersion, nFormatter } from "../utils"; +import { debounce, getVersion, nFormatter } from "../src/utils"; import { getElementsStorageSize, getTotalStorageSize, } from "./data/localStorage"; -import { DEFAULT_VERSION } from "../constants"; -import { t } from "../i18n"; -import { copyTextToSystemClipboard } from "../clipboard"; -import { NonDeletedExcalidrawElement } from "../element/types"; -import { UIAppState } from "../types"; +import { DEFAULT_VERSION } from "../src/constants"; +import { t } from "../src/i18n"; +import { copyTextToSystemClipboard } from "../src/clipboard"; +import { NonDeletedExcalidrawElement } from "../src/element/types"; +import { UIAppState } from "../src/types"; type StorageSizes = { scene: number; total: number }; diff --git a/src/excalidraw-app/app-jotai.ts b/excalidraw-app/app-jotai.ts similarity index 100% rename from src/excalidraw-app/app-jotai.ts rename to excalidraw-app/app-jotai.ts diff --git a/src/excalidraw-app/app_constants.ts b/excalidraw-app/app_constants.ts similarity index 100% rename from src/excalidraw-app/app_constants.ts rename to excalidraw-app/app_constants.ts diff --git a/src/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx similarity index 97% rename from src/excalidraw-app/collab/Collab.tsx rename to excalidraw-app/collab/Collab.tsx index 673d89e8b..0d57a8906 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -1,23 +1,23 @@ import throttle from "lodash.throttle"; import { PureComponent } from "react"; -import { ExcalidrawImperativeAPI } from "../../types"; -import { ErrorDialog } from "../../components/ErrorDialog"; -import { APP_NAME, ENV, EVENT } from "../../constants"; -import { ImportedDataState } from "../../data/types"; +import { ExcalidrawImperativeAPI } from "../../src/types"; +import { ErrorDialog } from "../../src/components/ErrorDialog"; +import { APP_NAME, ENV, EVENT } from "../../src/constants"; +import { ImportedDataState } from "../../src/data/types"; import { ExcalidrawElement, InitializedExcalidrawImageElement, -} from "../../element/types"; +} from "../../src/element/types"; import { getSceneVersion, restoreElements, -} from "../../packages/excalidraw/index"; -import { Collaborator, Gesture } from "../../types"; +} from "../../src/packages/excalidraw/index"; +import { Collaborator, Gesture } from "../../src/types"; import { preventUnload, resolvablePromise, withBatchedUpdates, -} from "../../utils"; +} from "../../src/utils"; import { CURSOR_SYNC_TIMEOUT, FILE_UPLOAD_MAX_BYTES, @@ -48,25 +48,25 @@ import { } from "../data/localStorage"; import Portal from "./Portal"; import RoomDialog from "./RoomDialog"; -import { t } from "../../i18n"; -import { UserIdleState } from "../../types"; -import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants"; +import { t } from "../../src/i18n"; +import { UserIdleState } from "../../src/types"; +import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants"; import { encodeFilesForUpload, FileManager, updateStaleImageStatuses, } from "../data/FileManager"; -import { AbortError } from "../../errors"; +import { AbortError } from "../../src/errors"; import { isImageElement, isInitializedImageElement, -} from "../../element/typeChecks"; -import { newElementWith } from "../../element/mutateElement"; +} from "../../src/element/typeChecks"; +import { newElementWith } from "../../src/element/mutateElement"; import { ReconciledElements, reconcileElements as _reconcileElements, } from "./reconciliation"; -import { decryptData } from "../../data/encryption"; +import { decryptData } from "../../src/data/encryption"; import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; import { atom, useAtom } from "jotai"; diff --git a/src/excalidraw-app/collab/Portal.tsx b/excalidraw-app/collab/Portal.tsx similarity index 94% rename from src/excalidraw-app/collab/Portal.tsx rename to excalidraw-app/collab/Portal.tsx index 1d4db3c0c..401b83ec5 100644 --- a/src/excalidraw-app/collab/Portal.tsx +++ b/excalidraw-app/collab/Portal.tsx @@ -6,19 +6,19 @@ import { import { TCollabClass } from "./Collab"; -import { ExcalidrawElement } from "../../element/types"; +import { ExcalidrawElement } from "../../src/element/types"; import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SCENE_EVENT_TYPES, } from "../app_constants"; -import { UserIdleState } from "../../types"; -import { trackEvent } from "../../analytics"; +import { UserIdleState } from "../../src/types"; +import { trackEvent } from "../../src/analytics"; import throttle from "lodash.throttle"; -import { newElementWith } from "../../element/mutateElement"; +import { newElementWith } from "../../src/element/mutateElement"; import { BroadcastedExcalidrawElement } from "./reconciliation"; -import { encryptData } from "../../data/encryption"; -import { PRECEDING_ELEMENT_KEY } from "../../constants"; +import { encryptData } from "../../src/data/encryption"; +import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; class Portal { collab: TCollabClass; diff --git a/src/excalidraw-app/collab/RoomDialog.scss b/excalidraw-app/collab/RoomDialog.scss similarity index 98% rename from src/excalidraw-app/collab/RoomDialog.scss rename to excalidraw-app/collab/RoomDialog.scss index 0d1bcad6c..b5f62330e 100644 --- a/src/excalidraw-app/collab/RoomDialog.scss +++ b/excalidraw-app/collab/RoomDialog.scss @@ -1,4 +1,4 @@ -@import "../../css/variables.module"; +@import "../../src/css/variables.module"; .excalidraw { .RoomDialog { diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/excalidraw-app/collab/RoomDialog.tsx similarity index 90% rename from src/excalidraw-app/collab/RoomDialog.tsx rename to excalidraw-app/collab/RoomDialog.tsx index 6b0706270..3f2a9e7bc 100644 --- a/src/excalidraw-app/collab/RoomDialog.tsx +++ b/excalidraw-app/collab/RoomDialog.tsx @@ -1,13 +1,13 @@ import { useRef, useState } from "react"; import * as Popover from "@radix-ui/react-popover"; -import { copyTextToSystemClipboard } from "../../clipboard"; -import { trackEvent } from "../../analytics"; -import { getFrame } from "../../utils"; -import { useI18n } from "../../i18n"; -import { KEYS } from "../../keys"; +import { copyTextToSystemClipboard } from "../../src/clipboard"; +import { trackEvent } from "../../src/analytics"; +import { getFrame } from "../../src/utils"; +import { useI18n } from "../../src/i18n"; +import { KEYS } from "../../src/keys"; -import { Dialog } from "../../components/Dialog"; +import { Dialog } from "../../src/components/Dialog"; import { copyIcon, playerPlayIcon, @@ -16,11 +16,11 @@ import { shareIOS, shareWindows, tablerCheckIcon, -} from "../../components/icons"; -import { TextField } from "../../components/TextField"; -import { FilledButton } from "../../components/FilledButton"; +} from "../../src/components/icons"; +import { TextField } from "../../src/components/TextField"; +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"; const getShareIcon = () => { diff --git a/src/excalidraw-app/collab/reconciliation.ts b/excalidraw-app/collab/reconciliation.ts similarity index 95% rename from src/excalidraw-app/collab/reconciliation.ts rename to excalidraw-app/collab/reconciliation.ts index 3f50bc358..1efc5db46 100644 --- a/src/excalidraw-app/collab/reconciliation.ts +++ b/excalidraw-app/collab/reconciliation.ts @@ -1,7 +1,7 @@ -import { PRECEDING_ELEMENT_KEY } from "../../constants"; -import { ExcalidrawElement } from "../../element/types"; -import { AppState } from "../../types"; -import { arrayToMapWithIndex } from "../../utils"; +import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; +import { ExcalidrawElement } from "../../src/element/types"; +import { AppState } from "../../src/types"; +import { arrayToMapWithIndex } from "../../src/utils"; export type ReconciledElements = readonly ExcalidrawElement[] & { _brand: "reconciledElements"; diff --git a/src/excalidraw-app/components/AppFooter.tsx b/excalidraw-app/components/AppFooter.tsx similarity index 59% rename from src/excalidraw-app/components/AppFooter.tsx rename to excalidraw-app/components/AppFooter.tsx index 7011a1c4d..81846f0d3 100644 --- a/src/excalidraw-app/components/AppFooter.tsx +++ b/excalidraw-app/components/AppFooter.tsx @@ -1,7 +1,8 @@ import React from "react"; -import { Footer } from "../../packages/excalidraw/index"; +import { Footer } from "../../src/packages/excalidraw/index"; import { EncryptedIcon } from "./EncryptedIcon"; import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; +import { isExcalidrawPlusSignedUser } from "../app_constants"; export const AppFooter = React.memo(() => { return ( @@ -13,8 +14,11 @@ export const AppFooter = React.memo(() => { alignItems: "center", }} > - - + {isExcalidrawPlusSignedUser ? ( + + ) : ( + + )} ); diff --git a/src/excalidraw-app/components/AppMainMenu.tsx b/excalidraw-app/components/AppMainMenu.tsx similarity index 91% rename from src/excalidraw-app/components/AppMainMenu.tsx rename to excalidraw-app/components/AppMainMenu.tsx index 6e12d7811..7a984a8f5 100644 --- a/src/excalidraw-app/components/AppMainMenu.tsx +++ b/excalidraw-app/components/AppMainMenu.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { PlusPromoIcon } from "../../components/icons"; -import { MainMenu } from "../../packages/excalidraw/index"; +import { PlusPromoIcon } from "../../src/components/icons"; +import { MainMenu } from "../../src/packages/excalidraw/index"; import { LanguageList } from "./LanguageList"; export const AppMainMenu: React.FC<{ diff --git a/src/excalidraw-app/components/AppWelcomeScreen.tsx b/excalidraw-app/components/AppWelcomeScreen.tsx similarity index 87% rename from src/excalidraw-app/components/AppWelcomeScreen.tsx rename to excalidraw-app/components/AppWelcomeScreen.tsx index 699c3ba88..cdefd1fe4 100644 --- a/src/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/excalidraw-app/components/AppWelcomeScreen.tsx @@ -1,8 +1,9 @@ import React from "react"; -import { PlusPromoIcon } from "../../components/icons"; -import { useI18n } from "../../i18n"; -import { WelcomeScreen } from "../../packages/excalidraw/index"; +import { PlusPromoIcon } from "../../src/components/icons"; +import { useI18n } from "../../src/i18n"; +import { WelcomeScreen } from "../../src/packages/excalidraw/index"; import { isExcalidrawPlusSignedUser } from "../app_constants"; +import { POINTER_EVENTS } from "../../src/constants"; export const AppWelcomeScreen: React.FC<{ setCollabDialogShown: (toggle: boolean) => any; @@ -18,7 +19,7 @@ export const AppWelcomeScreen: React.FC<{ if (bit === "Excalidraw+") { return ( { const { t } = useI18n(); diff --git a/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx b/excalidraw-app/components/ExcalidrawPlusAppLink.tsx similarity index 100% rename from src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx rename to excalidraw-app/components/ExcalidrawPlusAppLink.tsx diff --git a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx b/excalidraw-app/components/ExportToExcalidrawPlus.tsx similarity index 80% rename from src/excalidraw-app/components/ExportToExcalidrawPlus.tsx rename to excalidraw-app/components/ExportToExcalidrawPlus.tsx index 42b7a7d4f..0b577ad7d 100644 --- a/src/excalidraw-app/components/ExportToExcalidrawPlus.tsx +++ b/excalidraw-app/components/ExportToExcalidrawPlus.tsx @@ -1,20 +1,20 @@ import React from "react"; -import { Card } from "../../components/Card"; -import { ToolButton } from "../../components/ToolButton"; -import { serializeAsJSON } from "../../data/json"; +import { Card } from "../../src/components/Card"; +import { ToolButton } from "../../src/components/ToolButton"; +import { serializeAsJSON } from "../../src/data/json"; import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; -import { FileId, NonDeletedExcalidrawElement } from "../../element/types"; -import { AppState, BinaryFileData, BinaryFiles } from "../../types"; +import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types"; +import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; import { nanoid } from "nanoid"; -import { useI18n } from "../../i18n"; -import { encryptData, generateEncryptionKey } from "../../data/encryption"; -import { isInitializedImageElement } from "../../element/typeChecks"; +import { useI18n } from "../../src/i18n"; +import { encryptData, generateEncryptionKey } from "../../src/data/encryption"; +import { isInitializedImageElement } from "../../src/element/typeChecks"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; import { encodeFilesForUpload } from "../data/FileManager"; -import { MIME_TYPES } from "../../constants"; -import { trackEvent } from "../../analytics"; -import { getFrame } from "../../utils"; -import { ExcalidrawLogo } from "../../components/ExcalidrawLogo"; +import { MIME_TYPES } from "../../src/constants"; +import { trackEvent } from "../../src/analytics"; +import { getFrame } from "../../src/utils"; +import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo"; export const exportToExcalidrawPlus = async ( elements: readonly NonDeletedExcalidrawElement[], diff --git a/src/excalidraw-app/components/GitHubCorner.tsx b/excalidraw-app/components/GitHubCorner.tsx similarity index 94% rename from src/excalidraw-app/components/GitHubCorner.tsx rename to excalidraw-app/components/GitHubCorner.tsx index 34e8d29ab..e0575433d 100644 --- a/src/excalidraw-app/components/GitHubCorner.tsx +++ b/excalidraw-app/components/GitHubCorner.tsx @@ -1,7 +1,7 @@ import oc from "open-color"; import React from "react"; -import { THEME } from "../../constants"; -import { Theme } from "../../element/types"; +import { THEME } from "../../src/constants"; +import { Theme } from "../../src/element/types"; // https://github.com/tholman/github-corners export const GitHubCorner = React.memo( diff --git a/src/excalidraw-app/components/LanguageList.tsx b/excalidraw-app/components/LanguageList.tsx similarity index 88% rename from src/excalidraw-app/components/LanguageList.tsx rename to excalidraw-app/components/LanguageList.tsx index c80acc9b6..11d4b6d00 100644 --- a/src/excalidraw-app/components/LanguageList.tsx +++ b/excalidraw-app/components/LanguageList.tsx @@ -1,8 +1,8 @@ import { useSetAtom } from "jotai"; import React from "react"; import { appLangCodeAtom } from ".."; -import { useI18n } from "../../i18n"; -import { languages } from "../../i18n"; +import { useI18n } from "../../src/i18n"; +import { languages } from "../../src/i18n"; export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { const { t, langCode } = useI18n(); diff --git a/src/excalidraw-app/data/FileManager.ts b/excalidraw-app/data/FileManager.ts similarity index 95% rename from src/excalidraw-app/data/FileManager.ts rename to excalidraw-app/data/FileManager.ts index 419407a6b..426afc4d3 100644 --- a/src/excalidraw-app/data/FileManager.ts +++ b/excalidraw-app/data/FileManager.ts @@ -1,19 +1,19 @@ -import { compressData } from "../../data/encode"; -import { newElementWith } from "../../element/mutateElement"; -import { isInitializedImageElement } from "../../element/typeChecks"; +import { compressData } from "../../src/data/encode"; +import { newElementWith } from "../../src/element/mutateElement"; +import { isInitializedImageElement } from "../../src/element/typeChecks"; import { ExcalidrawElement, ExcalidrawImageElement, FileId, InitializedExcalidrawImageElement, -} from "../../element/types"; -import { t } from "../../i18n"; +} from "../../src/element/types"; +import { t } from "../../src/i18n"; import { BinaryFileData, BinaryFileMetadata, ExcalidrawImperativeAPI, BinaryFiles, -} from "../../types"; +} from "../../src/types"; export class FileManager { /** files being fetched */ diff --git a/src/excalidraw-app/data/LocalData.ts b/excalidraw-app/data/LocalData.ts similarity index 94% rename from src/excalidraw-app/data/LocalData.ts rename to excalidraw-app/data/LocalData.ts index 08f91d8d6..5d9ba0e14 100644 --- a/src/excalidraw-app/data/LocalData.ts +++ b/excalidraw-app/data/LocalData.ts @@ -11,11 +11,11 @@ */ import { createStore, entries, del, getMany, set, setMany } from "idb-keyval"; -import { clearAppStateForLocalStorage } from "../../appState"; -import { clearElementsForLocalStorage } from "../../element"; -import { ExcalidrawElement, FileId } from "../../element/types"; -import { AppState, BinaryFileData, BinaryFiles } from "../../types"; -import { debounce } from "../../utils"; +import { clearAppStateForLocalStorage } from "../../src/appState"; +import { clearElementsForLocalStorage } from "../../src/element"; +import { ExcalidrawElement, FileId } from "../../src/element/types"; +import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; +import { debounce } from "../../src/utils"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; import { FileManager } from "./FileManager"; import { Locker } from "./Locker"; diff --git a/src/excalidraw-app/data/Locker.ts b/excalidraw-app/data/Locker.ts similarity index 100% rename from src/excalidraw-app/data/Locker.ts rename to excalidraw-app/data/Locker.ts diff --git a/src/excalidraw-app/data/firebase.ts b/excalidraw-app/data/firebase.ts similarity index 95% rename from src/excalidraw-app/data/firebase.ts rename to excalidraw-app/data/firebase.ts index bb914de12..831213d60 100644 --- a/src/excalidraw-app/data/firebase.ts +++ b/excalidraw-app/data/firebase.ts @@ -1,20 +1,20 @@ -import { ExcalidrawElement, FileId } from "../../element/types"; -import { getSceneVersion } from "../../element"; +import { ExcalidrawElement, FileId } from "../../src/element/types"; +import { getSceneVersion } from "../../src/element"; import Portal from "../collab/Portal"; -import { restoreElements } from "../../data/restore"; +import { restoreElements } from "../../src/data/restore"; import { AppState, BinaryFileData, BinaryFileMetadata, DataURL, -} from "../../types"; +} from "../../src/types"; import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; -import { decompressData } from "../../data/encode"; -import { encryptData, decryptData } from "../../data/encryption"; -import { MIME_TYPES } from "../../constants"; +import { decompressData } from "../../src/data/encode"; +import { encryptData, decryptData } from "../../src/data/encryption"; +import { MIME_TYPES } from "../../src/constants"; import { reconcileElements } from "../collab/reconciliation"; import { getSyncableElements, SyncableExcalidrawElement } from "."; -import { ResolutionType } from "../../utility-types"; +import { ResolutionType } from "../../src/utility-types"; // private // ----------------------------------------------------------------------------- diff --git a/src/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts similarity index 93% rename from src/excalidraw-app/data/index.ts rename to excalidraw-app/data/index.ts index d3108c761..3870ca37c 100644 --- a/src/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -1,23 +1,23 @@ -import { compressData, decompressData } from "../../data/encode"; +import { compressData, decompressData } from "../../src/data/encode"; import { decryptData, generateEncryptionKey, IV_LENGTH_BYTES, -} from "../../data/encryption"; -import { serializeAsJSON } from "../../data/json"; -import { restore } from "../../data/restore"; -import { ImportedDataState } from "../../data/types"; -import { isInvisiblySmallElement } from "../../element/sizeHelpers"; -import { isInitializedImageElement } from "../../element/typeChecks"; -import { ExcalidrawElement, FileId } from "../../element/types"; -import { t } from "../../i18n"; +} from "../../src/data/encryption"; +import { serializeAsJSON } from "../../src/data/json"; +import { restore } from "../../src/data/restore"; +import { ImportedDataState } from "../../src/data/types"; +import { isInvisiblySmallElement } from "../../src/element/sizeHelpers"; +import { isInitializedImageElement } from "../../src/element/typeChecks"; +import { ExcalidrawElement, FileId } from "../../src/element/types"; +import { t } from "../../src/i18n"; import { AppState, BinaryFileData, BinaryFiles, UserIdleState, -} from "../../types"; -import { bytesToHexString } from "../../utils"; +} from "../../src/types"; +import { bytesToHexString } from "../../src/utils"; import { DELETED_ELEMENT_TIMEOUT, FILE_UPLOAD_MAX_BYTES, diff --git a/src/excalidraw-app/data/localStorage.ts b/excalidraw-app/data/localStorage.ts similarity index 91% rename from src/excalidraw-app/data/localStorage.ts rename to excalidraw-app/data/localStorage.ts index 6902052bd..2a1b93ebf 100644 --- a/src/excalidraw-app/data/localStorage.ts +++ b/excalidraw-app/data/localStorage.ts @@ -1,12 +1,12 @@ -import { ExcalidrawElement } from "../../element/types"; -import { AppState } from "../../types"; +import { ExcalidrawElement } from "../../src/element/types"; +import { AppState } from "../../src/types"; import { clearAppStateForLocalStorage, getDefaultAppState, -} from "../../appState"; -import { clearElementsForLocalStorage } from "../../element"; +} from "../../src/appState"; +import { clearElementsForLocalStorage } from "../../src/element"; import { STORAGE_KEYS } from "../app_constants"; -import { ImportedDataState } from "../../data/types"; +import { ImportedDataState } from "../../src/data/types"; export const saveUsernameToLocalStorage = (username: string) => { try { diff --git a/src/excalidraw-app/data/tabSync.ts b/excalidraw-app/data/tabSync.ts similarity index 100% rename from src/excalidraw-app/data/tabSync.ts rename to excalidraw-app/data/tabSync.ts diff --git a/src/excalidraw-app/debug.ts b/excalidraw-app/debug.ts similarity index 100% rename from src/excalidraw-app/debug.ts rename to excalidraw-app/debug.ts diff --git a/src/excalidraw-app/index.scss b/excalidraw-app/index.scss similarity index 94% rename from src/excalidraw-app/index.scss rename to excalidraw-app/index.scss index 38c4c1a38..d7ab79836 100644 --- a/src/excalidraw-app/index.scss +++ b/excalidraw-app/index.scss @@ -77,13 +77,14 @@ align-items: center; border: 1px solid var(--color-primary); 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; text-decoration: none !important; font-size: 0.75rem; box-sizing: border-box; - height: var(--default-button-size); + height: var(--lg-button-size); &:hover { background-color: var(--color-primary); diff --git a/src/excalidraw-app/index.tsx b/excalidraw-app/index.tsx similarity index 94% rename from src/excalidraw-app/index.tsx rename to excalidraw-app/index.tsx index 82449fd6d..93a73bd50 100644 --- a/src/excalidraw-app/index.tsx +++ b/excalidraw-app/index.tsx @@ -1,32 +1,32 @@ -import polyfill from "../polyfill"; +import polyfill from "../src/polyfill"; import LanguageDetector from "i18next-browser-languagedetector"; import { useEffect, useRef, useState } from "react"; -import { trackEvent } from "../analytics"; -import { getDefaultAppState } from "../appState"; -import { ErrorDialog } from "../components/ErrorDialog"; -import { TopErrorBoundary } from "../components/TopErrorBoundary"; -import { useSubtypes } from "../element/subtypes/use"; +import { trackEvent } from "../src/analytics"; +import { getDefaultAppState } from "../src/appState"; +import { ErrorDialog } from "../src/components/ErrorDialog"; +import { TopErrorBoundary } from "../src/components/TopErrorBoundary"; +import { useSubtypes } from "../src/element/subtypes/use"; import { APP_NAME, EVENT, THEME, TITLE_TIMEOUT, VERSION_TIMEOUT, -} from "../constants"; -import { loadFromBlob } from "../data/blob"; +} from "../src/constants"; +import { loadFromBlob } from "../src/data/blob"; import { ExcalidrawElement, FileId, NonDeletedExcalidrawElement, Theme, -} from "../element/types"; -import { useCallbackRefState } from "../hooks/useCallbackRefState"; -import { t } from "../i18n"; +} from "../src/element/types"; +import { useCallbackRefState } from "../src/hooks/useCallbackRefState"; +import { t } from "../src/i18n"; import { Excalidraw, defaultLang, LiveCollaborationTrigger, -} from "../packages/excalidraw/index"; +} from "../src/packages/excalidraw/index"; import { AppState, LibraryItems, @@ -34,7 +34,7 @@ import { BinaryFiles, ExcalidrawInitialDataState, UIAppState, -} from "../types"; +} from "../src/types"; import { debounce, getVersion, @@ -44,7 +44,7 @@ import { ResolvablePromise, resolvablePromise, isRunningInIframe, -} from "../utils"; +} from "../src/utils"; import { FIREBASE_STORAGE_PREFIXES, STORAGE_KEYS, @@ -69,33 +69,40 @@ import { importUsernameFromLocalStorage, } from "./data/localStorage"; import CustomStats from "./CustomStats"; -import { restore, restoreAppState, RestoredDataState } from "../data/restore"; +import { + restore, + restoreAppState, + RestoredDataState, +} from "../src/data/restore"; import { ExportToExcalidrawPlus, exportToExcalidrawPlus, } from "./components/ExportToExcalidrawPlus"; import { updateStaleImageStatuses } from "./data/FileManager"; -import { newElementWith } from "../element/mutateElement"; -import { isInitializedImageElement } from "../element/typeChecks"; +import { newElementWith } from "../src/element/mutateElement"; +import { isInitializedImageElement } from "../src/element/typeChecks"; import { loadFilesFromFirebase } from "./data/firebase"; import { LocalData } from "./data/LocalData"; import { isBrowserStorageStateNewer } from "./data/tabSync"; import clsx from "clsx"; import { reconcileElements } from "./collab/reconciliation"; -import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; +import { + parseLibraryTokensFromUrl, + useHandleLibrary, +} from "../src/data/library"; import { AppMainMenu } from "./components/AppMainMenu"; import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; import { AppFooter } from "./components/AppFooter"; import { atom, Provider, useAtom, useAtomValue } from "jotai"; -import { useAtomWithInitialValue } from "../jotai"; +import { useAtomWithInitialValue } from "../src/jotai"; import { appJotaiStore } from "./app-jotai"; import "./index.scss"; -import { ResolutionType } from "../utility-types"; -import { ShareableLinkDialog } from "../components/ShareableLinkDialog"; -import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState"; -import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm"; -import Trans from "../components/Trans"; +import { ResolutionType } from "../src/utility-types"; +import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog"; +import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState"; +import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm"; +import Trans from "../src/components/Trans"; polyfill(); diff --git a/src/excalidraw-app/sentry.ts b/excalidraw-app/sentry.ts similarity index 100% rename from src/excalidraw-app/sentry.ts rename to excalidraw-app/sentry.ts diff --git a/excalidraw-app/tests/LanguageList.test.tsx b/excalidraw-app/tests/LanguageList.test.tsx new file mode 100644 index 000000000..49a70a14c --- /dev/null +++ b/excalidraw-app/tests/LanguageList.test.tsx @@ -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(); + + // 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()); + }); +}); diff --git a/src/tests/MobileMenu.test.tsx b/excalidraw-app/tests/MobileMenu.test.tsx similarity index 88% rename from src/tests/MobileMenu.test.tsx rename to excalidraw-app/tests/MobileMenu.test.tsx index a5014062d..2aec60b17 100644 --- a/src/tests/MobileMenu.test.tsx +++ b/excalidraw-app/tests/MobileMenu.test.tsx @@ -1,11 +1,11 @@ -import ExcalidrawApp from "../excalidraw-app"; +import ExcalidrawApp from "../../excalidraw-app"; import { mockBoundingClientRect, render, restoreOriginalGetBoundingClientRect, -} from "./test-utils"; +} from "../../src/tests/test-utils"; -import { UI } from "./helpers/ui"; +import { UI } from "../../src/tests/helpers/ui"; describe("Test MobileMenu", () => { const { h } = window; diff --git a/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap b/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap new file mode 100644 index 000000000..ad0c9f0f1 --- /dev/null +++ b/excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap @@ -0,0 +1,257 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test MobileMenu > should initialize with welcome screen and hide once user interacts 1`] = ` + +`; diff --git a/src/tests/collab.test.tsx b/excalidraw-app/tests/collab.test.tsx similarity index 86% rename from src/tests/collab.test.tsx rename to excalidraw-app/tests/collab.test.tsx index c5d4f6c44..343a67ac4 100644 --- a/src/tests/collab.test.tsx +++ b/excalidraw-app/tests/collab.test.tsx @@ -1,8 +1,8 @@ import { vi } from "vitest"; -import { render, updateSceneData, waitFor } from "./test-utils"; -import ExcalidrawApp from "../excalidraw-app"; -import { API } from "./helpers/api"; -import { createUndoAction } from "../actions/actionHistory"; +import { render, updateSceneData, waitFor } from "../../src/tests/test-utils"; +import ExcalidrawApp from "../../excalidraw-app"; +import { API } from "../../src/tests/helpers/api"; +import { createUndoAction } from "../../src/actions/actionHistory"; const { h } = window; 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; return { __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 saveToFirebase = () => {}; const isSavedToFirebase = () => true; diff --git a/src/tests/reconciliation.test.ts b/excalidraw-app/tests/reconciliation.test.ts similarity index 97% rename from src/tests/reconciliation.test.ts rename to excalidraw-app/tests/reconciliation.test.ts index f050ed659..c3e247406 100644 --- a/src/tests/reconciliation.test.ts +++ b/excalidraw-app/tests/reconciliation.test.ts @@ -1,13 +1,13 @@ import { expect } from "chai"; -import { PRECEDING_ELEMENT_KEY } from "../constants"; -import { ExcalidrawElement } from "../element/types"; +import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; +import { ExcalidrawElement } from "../../src/element/types"; import { BroadcastedExcalidrawElement, ReconciledElements, reconcileElements, -} from "../excalidraw-app/collab/reconciliation"; -import { randomInteger } from "../random"; -import { AppState } from "../types"; +} from "../../excalidraw-app/collab/reconciliation"; +import { randomInteger } from "../../src/random"; +import { AppState } from "../../src/types"; type Id = string; type ElementLike = { diff --git a/src/components/App.tsx b/src/components/App.tsx index 60346dd92..f0400122c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -85,6 +85,7 @@ import { VERTICAL_ALIGN, YOUTUBE_STATES, ZOOM_STEP, + POINTER_EVENTS, } from "../constants"; import { exportCanvas, loadFromBlob } from "../data"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; @@ -886,7 +887,9 @@ class App extends React.Component { width: isVisible ? `${el.width}px` : 0, height: isVisible ? `${el.height}px` : 0, transform: isVisible ? `rotate(${el.angle}rad)` : "none", - pointerEvents: isActive ? "auto" : "none", + pointerEvents: isActive + ? POINTER_EVENTS.enabled + : POINTER_EVENTS.disabled, }} > {isHovered && ( @@ -1110,9 +1113,9 @@ class App extends React.Component { whiteSpace: "nowrap", textOverflow: "ellipsis", cursor: CURSOR_TYPE.MOVE, - // disable all interaction (e.g. cursor change) when in view - // mode - pointerEvents: this.state.viewModeEnabled ? "none" : "all", + pointerEvents: this.state.viewModeEnabled + ? POINTER_EVENTS.disabled + : POINTER_EVENTS.inheritFromUI, }} onPointerDown={(event) => this.handleCanvasPointerDown(event)} onWheel={(event) => this.handleWheel(event)} @@ -1154,6 +1157,16 @@ class App extends React.Component { "excalidraw--view-mode": this.state.viewModeEnabled, "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} onDrop={this.handleAppOnDrop} tabIndex={0} @@ -1346,7 +1359,8 @@ class App extends React.Component { private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => { jotaiStore.set(activeEyeDropperAtom, { swapPreviewOnAlt: true, - previewType: type === "stroke" ? "strokeColor" : "backgroundColor", + colorPickerType: + type === "stroke" ? "elementStroke" : "elementBackground", onSelect: (color, event) => { const shouldUpdateStrokeColor = (type === "background" && event.altKey) || @@ -1357,12 +1371,14 @@ class App extends React.Component { this.state.activeTool.type !== "selection" ) { if (shouldUpdateStrokeColor) { - this.setState({ - currentItemStrokeColor: color, + this.syncActionResult({ + appState: { ...this.state, currentItemStrokeColor: color }, + commitToHistory: true, }); } else { - this.setState({ - currentItemBackgroundColor: color, + this.syncActionResult({ + appState: { ...this.state, currentItemBackgroundColor: color }, + commitToHistory: true, }); } } else { @@ -3975,7 +3991,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( scenePointerX, scenePointerY, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const [lastCommittedX, lastCommittedY] = @@ -4792,7 +4808,11 @@ class App extends React.Component { origin, withCmdOrCtrl: event[KEYS.CTRL_OR_CMD], 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( currentScrollBars, @@ -5316,7 +5336,11 @@ class App extends React.Component { sceneY: number; 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); @@ -5362,7 +5386,11 @@ class App extends React.Component { sceneX: 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({ x: gridX, @@ -5446,7 +5474,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -5540,7 +5568,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.state.gridSize, + this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -5599,7 +5627,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - this.state.gridSize, + this.lastPointerDown?.[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const frame = newFrameElement({ @@ -5682,7 +5710,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerCoords.x, 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 @@ -5728,6 +5756,7 @@ class App extends React.Component { this.state.selectedLinearElement, pointerCoords, this.state, + !event[KEYS.CTRL_OR_CMD], ); if (!ret) { return; @@ -5853,7 +5882,7 @@ class App extends React.Component { const [dragX, dragY] = getGridPoint( pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.y - pointerDownState.drag.offset.y, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const [dragDistanceX, dragDistanceY] = [ @@ -5920,7 +5949,7 @@ class App extends React.Component { const [originDragX, originDragY] = getGridPoint( pointerDownState.origin.x - pointerDownState.drag.offset.x, pointerDownState.origin.y - pointerDownState.drag.offset.y, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); mutateElement(duplicatedElement, { x: duplicatedElement.x + (originDragX - dragX), @@ -7713,7 +7742,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerCoords.x, pointerCoords.y, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const image = @@ -7782,7 +7811,7 @@ class App extends React.Component { const [resizeX, resizeY] = getGridPoint( pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.y - pointerDownState.resize.offset.y, - this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, ); const frameElementsOffsetsMap = new Map< diff --git a/src/components/ColorPicker/ColorInput.tsx b/src/components/ColorPicker/ColorInput.tsx index 968729cc7..f10a174d1 100644 --- a/src/components/ColorPicker/ColorInput.tsx +++ b/src/components/ColorPicker/ColorInput.tsx @@ -1,7 +1,10 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { getColor } from "./ColorPicker"; import { useAtom } from "jotai"; -import { activeColorPickerSectionAtom } from "./colorPickerUtils"; +import { + ColorPickerType, + activeColorPickerSectionAtom, +} from "./colorPickerUtils"; import { eyeDropperIcon } from "../icons"; import { jotaiScope } from "../../jotai"; import { KEYS } from "../../keys"; @@ -15,14 +18,14 @@ interface ColorInputProps { color: string; onChange: (color: string) => void; label: string; - eyeDropperType: "strokeColor" | "backgroundColor"; + colorPickerType: ColorPickerType; } export const ColorInput = ({ color, onChange, label, - eyeDropperType, + colorPickerType, }: ColorInputProps) => { const device = useDevice(); const [innerValue, setInnerValue] = useState(color); @@ -116,7 +119,7 @@ export const ColorInput = ({ : { keepOpenOnAlt: false, onSelect: (color) => onChange(color), - previewType: eyeDropperType, + colorPickerType, }, ) } diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx index 4a378950c..ff0b6f416 100644 --- a/src/components/ColorPicker/ColorPicker.tsx +++ b/src/components/ColorPicker/ColorPicker.tsx @@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({ const { container } = useExcalidrawContainer(); const { isMobile, isLandscape } = useDevice(); - const eyeDropperType = - type === "canvasBackground" - ? undefined - : type === "elementBackground" - ? "backgroundColor" - : "strokeColor"; - - const colorInputJSX = eyeDropperType && ( + const colorInputJSX = (
{t("colorPicker.hexCode")} { onChange(color); }} - eyeDropperType={eyeDropperType} + colorPickerType={type} />
); @@ -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)", }} > - {palette && eyeDropperType ? ( + {palette ? ( 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); export const EyeDropper: React.FC<{ onCancel: () => void; - onSelect: Required["onSelect"]; - swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"]; - previewType: EyeDropperProperties["previewType"]; -}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => { + onSelect: EyeDropperProperties["onSelect"]; + /** called when color changes, on pointerdown for preview */ + onChange: ( + type: ColorPickerType, + color: string, + selectedElements: ExcalidrawElement[], + event: { altKey: boolean }, + ) => void; + colorPickerType: EyeDropperProperties["colorPickerType"]; +}> = ({ onCancel, onChange, onSelect, colorPickerType }) => { const eyeDropperContainer = useCreatePortalContainer({ className: "excalidraw-eye-dropper-backdrop", parentSelector: ".excalidraw-eye-dropper-container", @@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{ const selectedElements = getSelectedElements(elements, appState); - const metaStuffRef = useRef({ selectedElements, app }); - metaStuffRef.current.selectedElements = selectedElements; - metaStuffRef.current.app = app; + const stableProps = useStable({ + app, + onCancel, + onChange, + onSelect, + selectedElements, + }); const { container: excalidrawContainer } = useExcalidrawContainer(); @@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{ const currentColor = getCurrentColor({ clientX, clientY }); if (isHoldingPointerDown) { - for (const element of metaStuffRef.current.selectedElements) { - mutateElement( - element, - { - [altKey && swapPreviewOnAlt - ? previewType === "strokeColor" - ? "backgroundColor" - : "strokeColor" - : previewType]: currentColor, - }, - false, - ); - ShapeCache.delete(element); - } - Scene.getScene( - metaStuffRef.current.selectedElements[0], - )?.informMutation(); + stableProps.onChange( + colorPickerType, + currentColor, + stableProps.selectedElements, + { altKey }, + ); } colorPreviewDiv.style.background = currentColor; }; + const onCancel = () => { + stableProps.onCancel(); + }; + + const onSelect: Required["onSelect"] = ( + color, + event, + ) => { + stableProps.onSelect(color, event); + }; + const pointerDownListener = (event: PointerEvent) => { isHoldingPointerDown = true; // 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 mouseMoveListener({ - clientX: metaStuffRef.current.app.lastViewportPosition.x, - clientY: metaStuffRef.current.app.lastViewportPosition.y, + clientX: stableProps.app.lastViewportPosition.x, + clientY: stableProps.app.lastViewportPosition.y, altKey: false, }); @@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{ window.removeEventListener(EVENT.BLUR, onCancel); }; }, [ + stableProps, app.canvas, eyeDropperContainer, - onCancel, - onSelect, - swapPreviewOnAlt, - previewType, + colorPickerType, excalidrawContainer, appState.offsetLeft, appState.offsetTop, diff --git a/src/components/FixedSideContainer.scss b/src/components/FixedSideContainer.scss index a485d6d25..62d77d505 100644 --- a/src/components/FixedSideContainer.scss +++ b/src/components/FixedSideContainer.scss @@ -7,7 +7,7 @@ } .FixedSideContainer > * { - pointer-events: all; + pointer-events: var(--ui-pointerEvents); } .FixedSideContainer_side_top { diff --git a/src/components/HintViewer.tsx b/src/components/HintViewer.tsx index 611f24925..18dba0e11 100644 --- a/src/components/HintViewer.tsx +++ b/src/components/HintViewer.tsx @@ -83,27 +83,36 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => { if (activeTool.type === "selection") { if ( appState.draggingElement?.type === "selection" && + !selectedElements.length && !appState.editingElement && !appState.editingLinearElement ) { return t("hints.deepBoxSelect"); } + + if (appState.gridSize && appState.draggingElement) { + return t("hints.disableSnapping"); + } + if (!selectedElements.length && !isMobile) { return t("hints.canvasPanning"); } - } - if (selectedElements.length === 1) { - if (isLinearElement(selectedElements[0])) { - if (appState.editingLinearElement) { - return appState.editingLinearElement.selectedPointsIndices - ? t("hints.lineEditor_pointSelected") - : t("hints.lineEditor_nothingSelected"); + if (selectedElements.length === 1) { + if (isLinearElement(selectedElements[0])) { + if (appState.editingLinearElement) { + return appState.editingLinearElement.selectedPointsIndices + ? t("hints.lineEditor_pointSelected") + : 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"); } } diff --git a/src/components/LayerUI.scss b/src/components/LayerUI.scss index b5c88cdff..8898b0f83 100644 --- a/src/components/LayerUI.scss +++ b/src/components/LayerUI.scss @@ -56,34 +56,52 @@ } .disable-zen-mode { - height: 30px; + padding: 10px; position: absolute; - bottom: 10px; + bottom: 0; [dir="ltr"] & { - right: 15px; + right: 1rem; } [dir="rtl"] & { - left: 15px; + left: 1rem; } - font-size: 10px; - padding: 10px; - font-weight: 500; opacity: 0; visibility: hidden; 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 { opacity: 1; visibility: visible; transition: visibility 0s linear 300ms, opacity 0.5s; transition-delay: 0.8s; + + pointer-events: var(--ui-pointerEvents); } } .layer-ui__wrapper__footer-left, - .layer-ui__wrapper__footer-right, - .disable-zen-mode--visible { - pointer-events: all; + .footer-center, + .layer-ui__wrapper__footer-right { + & > * { + pointer-events: var(--ui-pointerEvents); + } } .layer-ui__wrapper__footer-right { diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 26be77aef..3bc8436cc 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -2,7 +2,7 @@ import clsx from "clsx"; import React from "react"; import { ActionManager } from "../actions/manager"; import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants"; -import { isTextElement, showSelectedShapeActions } from "../element"; +import { showSelectedShapeActions } from "../element"; import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; import { calculateScrollCenter } from "../scene"; @@ -52,6 +52,9 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper"; import "./LayerUI.scss"; import "./Toolbar.scss"; +import { mutateElement } from "../element/mutateElement"; +import { ShapeCache } from "../scene/ShapeCache"; +import Scene from "../scene/Scene"; interface LayerUIProps { actionManager: ActionManager; @@ -368,11 +371,44 @@ const LayerUI = ({ )} {eyeDropperState && !device.isMobile && ( { 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) => { setEyeDropperState((state) => { return state?.keepOpenOnAlt && event.altKey ? state : null; @@ -427,13 +463,7 @@ const LayerUI = ({ {!device.isMobile && ( <>
* { - pointer-events: all; + pointer-events: var(--ui-pointerEvents); } .UserList_mobile { diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 173ea451a..85844aad6 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -73,7 +73,7 @@ const Footer = ({
diff --git a/src/components/footer/FooterCenter.scss b/src/components/footer/FooterCenter.scss index 1e17db6c5..ce6565922 100644 --- a/src/components/footer/FooterCenter.scss +++ b/src/components/footer/FooterCenter.scss @@ -1,10 +1,11 @@ .footer-center { pointer-events: none; & > * { - pointer-events: all; + pointer-events: var(--ui-pointerEvents); } display: flex; width: 100%; justify-content: flex-start; + margin-inline-end: 0.6rem; } diff --git a/src/components/welcome-screen/WelcomeScreen.scss b/src/components/welcome-screen/WelcomeScreen.scss index a1b88d8bd..8f1a09bf3 100644 --- a/src/components/welcome-screen/WelcomeScreen.scss +++ b/src/components/welcome-screen/WelcomeScreen.scss @@ -161,7 +161,7 @@ .welcome-screen-menu-item { box-sizing: border-box; - pointer-events: all; + pointer-events: var(--ui-pointerEvents); color: var(--color-gray-50); font-size: 0.875rem; @@ -202,7 +202,7 @@ } } - &:not(:active) .welcome-screen-menu-item:hover { + .welcome-screen-menu-item:hover { text-decoration: none; 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); .welcome-screen-menu-item__shortcut { diff --git a/src/constants.ts b/src/constants.ts index 46ae25b40..90fc66758 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -41,6 +41,14 @@ export const POINTER_BUTTON = { TOUCH: -1, } 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 { COPY = "copy", PASTE = "paste", diff --git a/src/css/styles.scss b/src/css/styles.scss index d46ab1bc2..4ca58af4f 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -253,7 +253,7 @@ max-height: 100%; display: flex; flex-direction: column; - pointer-events: initial; + pointer-events: var(--ui-pointerEvents); .panelColumn { padding: 8px 8px 0 8px; @@ -301,7 +301,7 @@ pointer-events: none !important; & > * { - pointer-events: all; + pointer-events: var(--ui-pointerEvents); } } @@ -312,16 +312,16 @@ cursor: default; pointer-events: none !important; + & > * { + pointer-events: var(--ui-pointerEvents); + } + @media (min-width: 1536px) { grid-template-columns: 1fr 1fr 1fr; grid-gap: 3rem; } } - .layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * { - pointer-events: all; - } - .App-menu_top > *:first-child { 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 { border-radius: var(--border-radius-lg); background-color: var(--island-bg-color); @@ -447,7 +436,7 @@ left: 50%; bottom: 30px; transform: translateX(-50%); - pointer-events: all; + pointer-events: var(--ui-pointerEvents); font-family: inherit; &:hover { diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index f0dee4faa..adc5aafc4 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -42,7 +42,7 @@ import { } from "./binding"; import { tupleToCoors } from "../utils"; import { isBindingElement } from "./typeChecks"; -import { shouldRotateWithDiscreteAngle } from "../keys"; +import { KEYS, shouldRotateWithDiscreteAngle } from "../keys"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { DRAGGING_THRESHOLD } from "../constants"; import { Mutable } from "../utility-types"; @@ -221,7 +221,7 @@ export class LinearElementEditor { element, referencePoint, [scenePointerX, scenePointerY], - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ); LinearElementEditor.movePoints(element, [ @@ -238,7 +238,7 @@ export class LinearElementEditor { element, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ); const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; @@ -254,7 +254,7 @@ export class LinearElementEditor { element, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ) : ([ element.points[pointIndex][0] + deltaX, @@ -647,7 +647,7 @@ export class LinearElementEditor { element, scenePointer.x, scenePointer.y, - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ), ], }); @@ -798,7 +798,7 @@ export class LinearElementEditor { element, lastCommittedPoint, [scenePointerX, scenePointerY], - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ); newPoint = [ @@ -810,7 +810,7 @@ export class LinearElementEditor { element, scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerY - appState.editingLinearElement.pointerOffset.y, - appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, ); } @@ -1176,6 +1176,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, pointerCoords: PointerCoords, appState: AppState, + snapToGrid: boolean, ) { const element = LinearElementEditor.getElement( linearElementEditor.elementId, @@ -1196,7 +1197,7 @@ export class LinearElementEditor { element, pointerCoords.x, pointerCoords.y, - appState.gridSize, + snapToGrid ? appState.gridSize : null, ); const points = [ ...element.points.slice(0, segmentMidpoint.index!), diff --git a/src/element/newElement.test.ts b/src/element/newElement.test.ts index ba7c63ee2..19761231b 100644 --- a/src/element/newElement.test.ts +++ b/src/element/newElement.test.ts @@ -203,7 +203,6 @@ describe("duplicating multiple elements", () => { ); clonedArrows.forEach((arrow) => { - // console.log(arrow); expect( clonedRectangle.boundElements!.find((e) => e.id === arrow.id), ).toEqual( diff --git a/src/element/subtypes/mathjax/tests/implementation.test.tsx b/src/element/subtypes/mathjax/tests/implementation.test.tsx index 95a827bc2..c895c74ae 100644 --- a/src/element/subtypes/mathjax/tests/implementation.test.tsx +++ b/src/element/subtypes/mathjax/tests/implementation.test.tsx @@ -1,13 +1,13 @@ import { render } from "../../../../tests/test-utils"; import { API } from "../../../../tests/helpers/api"; -import ExcalidrawApp from "../../../../excalidraw-app"; +import { Excalidraw } from "../../../../packages/excalidraw/index"; import { measureTextElement } from "../../../textElement"; import { ensureSubtypesLoaded } from "../../"; describe("mathjax", () => { it("text-only measurements match", async () => { - await render(); + await render(); await ensureSubtypesLoaded(["math"]); const text = "A quick brown fox jumps over the lazy dog."; const elements = [ @@ -19,7 +19,7 @@ describe("mathjax", () => { expect(metrics1).toStrictEqual(metrics2); }); it("minimum height remains", async () => { - await render(); + await render(); await ensureSubtypesLoaded(["math"]); const elements = [ API.createElement({ type: "text", id: "A", text: "a" }), diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index c855de357..fc1e8cf2e 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1,5 +1,5 @@ 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 { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { CODES, KEYS } from "../keys"; @@ -41,7 +41,7 @@ describe("textWysiwyg", () => { describe("start text editing", () => { const { h } = window; beforeEach(async () => { - await render(); + await render(); h.elements = []; }); @@ -243,7 +243,7 @@ describe("textWysiwyg", () => { }); beforeEach(async () => { - await render(); + await render(); //@ts-ignore h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); @@ -477,7 +477,7 @@ describe("textWysiwyg", () => { const { h } = window; beforeEach(async () => { - await render(); + await render(); h.elements = []; rectangle = UI.createElement("rectangle", { @@ -1511,7 +1511,7 @@ describe("textWysiwyg", () => { }); it("should bump the version of labelled arrow when label updated", async () => { - await render(); + await render(); const arrow = UI.createElement("arrow", { width: 300, height: 0, diff --git a/src/hooks/useStable.ts b/src/hooks/useStable.ts new file mode 100644 index 000000000..56608489d --- /dev/null +++ b/src/hooks/useStable.ts @@ -0,0 +1,7 @@ +import { useRef } from "react"; + +export const useStable = >(value: T) => { + const ref = useRef(value); + Object.assign(ref.current, value); + return ref.current; +}; diff --git a/src/index.tsx b/src/index.tsx index ea8c39cb1..2165ded77 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,9 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import ExcalidrawApp from "./excalidraw-app"; +import ExcalidrawApp from "../excalidraw-app"; import { registerSW } from "virtual:pwa-register"; -import "./excalidraw-app/sentry"; +import "../excalidraw-app/sentry"; window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; const rootElement = document.getElementById("root")!; const root = createRoot(rootElement); diff --git a/src/locales/en.json b/src/locales/en.json index 938535c5e..6799d0649 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -264,7 +264,8 @@ "bindTextToElement": "Press enter to add text", "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "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": { "cannotShowPreview": "Cannot show preview", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 7c86eda84..1c151c772 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -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. --> -## Unreleased - -### 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 `