Compare commits

...

48 Commits

Author SHA1 Message Date
Ryan Di c68c2be44c handle bound texts 2024-06-04 23:06:27 +08:00
Ryan Di be65ac7f22 resize linear & freedraw 2024-06-04 19:34:17 +08:00
Ryan Di 09e249ae57 capture history 2024-06-04 16:27:53 +08:00
Ryan Di f0c1e9707a change dimension for multiple elements 2024-06-04 15:28:06 +08:00
Ryan Di 7f4659339b custom font size 2024-05-31 17:21:53 +08:00
Ryan Di 0987c5b770 refactor to include dimension and step size 2024-05-31 17:21:41 +08:00
Ryan Di 0a529bd2ed change a rotated element's width and height 2024-05-28 19:57:34 +08:00
Ryan Di 794b2b21a7 merge with master 2024-05-24 16:21:09 +08:00
David Luzar a71bb63d1f fix: fix twitter og image (#8050) 2024-05-23 11:52:37 +02:00
Marcel Mraz 661d6a4a75 fix: flaky snapshot tests with floating point precision issues (#8049) 2024-05-23 11:51:01 +02:00
David Luzar defd34923a docs: fix updateScene storeAction default tsdoc & document types (#8048) 2024-05-22 13:40:23 +02:00
Ryan Di c540bd68aa feat: wrap long text when pasting (#8026)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-05-21 16:56:09 +02:00
Marcel Mraz eddbe55f50 fix: always re-generate index of defined moved elements (#8040) 2024-05-20 23:23:42 +02:00
Aakansha Doshi 2f9526da24 feat: upgrade to mermaid-to-excalidraw v1 🚀 (#8022)
* feat: upgrade to mermaid-to-excalidraw v1 🚀

* upgrade to v1
2024-05-20 11:19:38 +05:30
David Luzar 1b6e3fe05b feat: rerender canvas on focus (#8035) 2024-05-19 22:20:40 +02:00
VatsalSoni_13 afe52c89a7 fix: undo/redo when exiting view mode (#8024)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-05-19 15:54:52 +02:00
zsviczian be4e127f6c fix: Two finger panning is slow (#7849)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-05-19 14:23:43 +02:00
Karthik Nishanth ff0b4394b1 feat: add missing type="button" (#8030) 2024-05-18 08:36:08 +00:00
Hey 7d8b7fc14d fix: compatible safari layers button svg (#8020)
Co-authored-by: ysen <ysen.ge@hairobotics.com>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-05-15 15:22:05 +02:00
Ryan Di 971b4d4ae6 feat: text wrapping (#7999)
* resize single elements from the side

* fix lint

* do not resize texts from the sides (for we want to wrap/unwrap)

* omit side handles for frames too

* upgrade types

* enable resizing from the sides for multiple elements as well

* fix lint

* maintain aspect ratio when elements are not of the same angle

* lint

* always resize proportionally for multiple elements

* increase side resizing padding

* code cleanup

* adaptive handles

* do not resize for linear elements with only two points

* prioritize point dragging over edge resizing

* lint

* allow free resizing for multiple elements at degree 0

* always resize from the sides

* reduce hit threshold

* make small multiple elements movable

* lint

* show side handles on touch screen and mobile devices

* differentiate touchscreens

* keep proportional with text in multi-element resizing

* update snapshot

* update multi elements resizing logic

* lint

* reduce side resizing padding

* bound texts do not scale in normal cases

* lint

* test sides for texts

* wrap text

* do not update text size when changing its alignment

* keep text wrapped/unwrapped when editing

* change wrapped size to auto size from context menu

* fix test

* lint

* increase min width for wrapped texts

* wrap wrapped text in container

* unwrap when binding text to container

* rename `wrapped` to `autoResize`

* fix lint

* revert: use `center` align when wrapping text in container

* update snaps

* fix lint

* simplify logic on autoResize

* lint and test

* snapshots

* remove unnecessary code

* snapshots

* fix: defaults not set correctly

* tests for wrapping texts when resized

* tests for text wrapping when edited

* fix autoResize refactor

* include autoResize flag check

* refactor

* feat: rename action label & change contextmenu position

* fix: update version on `autoResize` action

* fix infinite loop when editing text in a container

* simplify

* always maintain `width` if `!autoResize`

* maintain `x` if `!autoResize`

* maintain `y` pos after fontSize change if `!autoResize`

* refactor

* when editing, do not wrap text in textWysiwyg

* simplify text editor

* make test more readable

* comment

* rename action to match file name

* revert function signature change

* only update  in app

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-05-15 21:04:53 +08:00
David Luzar cc4c51996c build: specify packageManager field (#8010) 2024-05-14 10:45:27 +02:00
Guillaume Grossetie 79257a1923 fix: correctly resolve the package version (#8016)
The property name is `VITE_PKG_VERSION` (not `PKG_VERSION`)

Resolves #7984
2024-05-14 13:31:02 +05:30
Marcel Mraz dc66261c19 fix: re-introduce wysiwyg width offset (#8014) 2024-05-13 17:38:21 +02:00
David Luzar 273ba803d9 fix: font not rendered correctly on init (#8002) 2024-05-10 16:37:46 +02:00
David Luzar 301e83805d feat: add install-PWA to command palette (#7935) 2024-05-08 22:02:28 +02:00
David Luzar ed5ce8d3de fix: command palette filter (#7981) 2024-05-08 17:56:05 +02:00
Aakansha Doshi 1ed53b153c build: enable consistent type imports eslint rule (#7992)
* build: enable consistent type imports eslint rule

* change to warn

* fix the warning in example and excalidraw-app

* fix packages

* enable type annotations and throw error for the rule
2024-05-08 14:21:50 +05:30
Aakansha Doshi c1926f33bb fix: remove unused param from drawImagePlaceholder (#7991) 2024-05-07 20:22:51 +05:30
Andreas Gebhardt 6539029d2a fix: docker build of Excalidraw app (#7430)
* fix: docker build of Excalidraw app

Fixes #7403.

* deps: update (container) Nginx to 1.24

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2024-05-07 18:00:58 +05:30
David Luzar d1f37cc64f feat: tweak a few icons & add line editor button to side panel (#7990) 2024-05-07 13:18:39 +02:00
Márk Tolmács f0d25e34c3 chore: Add lcov coverage output to vitest (#7987)
chore: Add lcov coverage output to vitest so VSCode coverage gutters extension works

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2024-05-06 17:35:23 +02:00
Márk Tolmács d9bbf1eda6 feat: Allow binding only via linear element ends (#7946)
Arrows now only bind to new shapes if their start or end point is dragged close to them. Arrows previously bound to shapes remain bound on move and drag if at the end of the drag/move the points remain in the original shapes' binding area.

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Sammy Lee <sammy.joe.lee@gmail.com>
2024-05-02 08:32:12 +02:00
Márk Tolmács f79fb9aae2 chore: Bump vitest to 1.5.3 to support VSCode vitest Extension (#7968)
Bump vitest to 1.5.3 to support VSCode vitest Extension

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2024-05-01 20:47:52 +02:00
Mritunjay Goutam 275f6fbe24 fix: typo in doc api (#7466) 2024-04-30 16:52:42 +00:00
Ryan Di 88812e0386 feat: resize elements from the sides (#7855)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-04-30 18:05:00 +02:00
Marcel Mraz 6e5aeb112d feat: record freedraw tool selection to history (#7949) 2024-04-25 17:24:05 +00:00
Marcel Mraz 4d83d1c91e fix: use Reflect API instead of Object.hasOwn (#7958) 2024-04-25 15:36:26 +02:00
Márk Tolmács a04676d423 fix: CTRL/CMD & arrow point drag unbinds both sides (#6459) (#7877) 2024-04-23 00:01:05 +02:00
Milos Vetesnik c851aaaf7b fix: z-index for laser pointer to be able to draw on embeds and such (#7918) 2024-04-22 23:53:55 +02:00
Marcel Mraz 1bd2b1fe55 feat: export reconciliation (#7917) 2024-04-22 17:27:57 +02:00
Marcel Mraz 015b46ab23 feat: expose StoreAction in relation to multiplayer history (#7898)
Improved Store API and improved handling of actions to eliminate potential concurrency issues
2024-04-22 09:22:25 +00:00
Ryan Di 6e577d1308 wip: drag input 2023-04-18 16:26:01 +08:00
Ryan Di 80b9fd18b9 throttled stats 2023-04-10 18:10:46 +08:00
Ryan Di dbc48cfee2 move stats from layerui to app component 2023-04-06 16:05:36 +08:00
Ryan Di 3fc89b716a editing single element 2023-03-27 17:51:31 +08:00
Ryan Di 30743ec726 split stats into general and element stats 2023-03-22 18:32:21 +08:00
Ryan Di 86d49a273b rename 'stats for nerds' to 'general stats' 2023-03-21 14:49:32 +08:00
Ryan Di 92fe9b95d5 remove element stats from 'stats for nerds' 2023-03-21 14:47:46 +08:00
285 changed files with 10110 additions and 6933 deletions
+7
View File
@@ -4,8 +4,15 @@
!.eslintrc.json
!.npmrc
!.prettierrc
!excalidraw-app/
!package.json
!public/
!packages/
!tsconfig.json
!yarn.lock
# keep (sub)sub directories at the end to exclude from explicit included
# e.g. ./packages/excalidraw/{dist,node_modules}
**/build
**/dist
**/node_modules
+2 -1
View File
@@ -2,6 +2,7 @@
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"import/no-anonymous-default-export": "off",
"no-restricted-globals": "off"
"no-restricted-globals": "off",
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
}
}
+7 -5
View File
@@ -2,16 +2,18 @@ FROM node:18 AS build
WORKDIR /opt/node_app
COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000
COPY . .
# do not ignore optional dependencies:
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
RUN yarn --network-timeout 600000
ARG NODE_ENV=production
COPY . .
RUN yarn build:app:docker
FROM nginx:1.21-alpine
FROM nginx:1.24-alpine
COPY --from=build /opt/node_app/build /usr/share/nginx/html
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
@@ -115,7 +115,7 @@ function App() {
<button className="custom-button" onClick={updateScene}>
Update Scene
</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
</div>
);
}
@@ -188,7 +188,7 @@ function App() {
Update Library
</button>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
excalidrawAPI={(api) => setExcalidrawAPI(api)}
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
initialData={{
libraryItems: initialData.libraryItems,
+1 -1
View File
@@ -12,9 +12,9 @@ import type * as TExcalidraw from "@excalidraw/excalidraw";
import { nanoid } from "nanoid";
import type { ResolvablePromise } from "../utils";
import {
resolvablePromise,
ResolvablePromise,
distance2d,
fileOpen,
withBatchedUpdates,
@@ -1,4 +1,4 @@
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw";
+1 -1
View File
@@ -1,6 +1,6 @@
import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access";
import type { MIME_TYPES } from "@excalidraw/excalidraw";
import { MIME_TYPES } from "@excalidraw/excalidraw";
import { AbortError } from "../../packages/excalidraw/errors";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+61 -16
View File
@@ -13,7 +13,7 @@ import {
VERSION_TIMEOUT,
} from "../packages/excalidraw/constants";
import { loadFromBlob } from "../packages/excalidraw/data/blob";
import {
import type {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
@@ -26,21 +26,23 @@ import {
LiveCollaborationTrigger,
TTDDialog,
TTDDialogTrigger,
} from "../packages/excalidraw/index";
import {
StoreAction,
reconcileElements,
} from "../packages/excalidraw";
import type {
AppState,
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "../packages/excalidraw/types";
import type { ResolvablePromise } from "../packages/excalidraw/utils";
import {
debounce,
getVersion,
getFrame,
isTestEnv,
preventUnload,
ResolvablePromise,
resolvablePromise,
isRunningInIframe,
} from "../packages/excalidraw/utils";
@@ -50,8 +52,8 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import type { CollabAPI } from "./collab/Collab";
import Collab, {
CollabAPI,
collabAPIAtom,
isCollaboratingAtom,
isOfflineAtom,
@@ -67,11 +69,8 @@ import {
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import {
restore,
restoreAppState,
RestoredDataState,
} from "../packages/excalidraw/data/restore";
import type { RestoredDataState } from "../packages/excalidraw/data/restore";
import { restore, restoreAppState } from "../packages/excalidraw/data/restore";
import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
@@ -99,17 +98,14 @@ import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
import { appJotaiStore } from "./app-jotai";
import "./index.scss";
import { ResolutionType } from "../packages/excalidraw/utility-types";
import type { ResolutionType } from "../packages/excalidraw/utility-types";
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
import Trans from "../packages/excalidraw/components/Trans";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import {
RemoteExcalidrawElement,
reconcileElements,
} from "../packages/excalidraw/data/reconcile";
import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile";
import {
CommandPalette,
DEFAULT_CATEGORIES,
@@ -130,6 +126,38 @@ polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true;
declare global {
interface BeforeInstallPromptEventChoiceResult {
outcome: "accepted" | "dismissed";
}
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
}
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
}
let pwaEvent: BeforeInstallPromptEvent | null = null;
// Adding a listener outside of the component as it may (?) need to be
// subscribed early to catch the event.
//
// Also note that it will fire only if certain heuristics are met (user has
// used the app for some time, etc.)
window.addEventListener(
"beforeinstallprompt",
(event: BeforeInstallPromptEvent) => {
// prevent Chrome <= 67 from automatically showing the prompt
event.preventDefault();
// cache for later use
pwaEvent = event;
},
);
let isSelfEmbedding = false;
if (window.self !== window.top) {
@@ -438,7 +466,7 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.updateScene({
...data.scene,
...restore(data.scene, null, null, { repairBindings: true }),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
}
});
@@ -469,6 +497,7 @@ const ExcalidrawWrapper = () => {
setLangCode(langCode);
excalidrawAPI.updateScene({
...localDataState,
storeAction: StoreAction.UPDATE,
});
LibraryIndexedDBAdapter.load().then((data) => {
if (data) {
@@ -604,6 +633,7 @@ const ExcalidrawWrapper = () => {
if (didChange) {
excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
}
}
@@ -1102,6 +1132,21 @@ const ExcalidrawWrapper = () => {
);
},
},
{
label: t("labels.installPWA"),
category: DEFAULT_CATEGORIES.app,
predicate: () => !!pwaEvent,
perform: () => {
if (pwaEvent) {
pwaEvent.prompt();
pwaEvent.userChoice.then(() => {
// event cannot be reused, but we'll hopefully
// grab new one as the event should be fired again
pwaEvent = null;
});
}
},
},
]}
/>
</Excalidraw>
+2 -2
View File
@@ -7,8 +7,8 @@ import {
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
import { t } from "../packages/excalidraw/i18n";
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import { UIAppState } from "../packages/excalidraw/types";
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import type { UIAppState } from "../packages/excalidraw/types";
type StorageSizes = { scene: number; total: number };
+16 -10
View File
@@ -1,23 +1,25 @@
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import {
import type {
ExcalidrawImperativeAPI,
SocketId,
} from "../../packages/excalidraw/types";
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import {
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type {
ExcalidrawElement,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import {
StoreAction,
getSceneVersion,
restoreElements,
zoomToFitBounds,
} from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
reconcileElements,
} from "../../packages/excalidraw";
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
import {
assertNever,
preventUnload,
@@ -34,12 +36,14 @@ import {
SYNC_FULL_SCENE_INTERVAL_MS,
WS_EVENTS,
} from "../app_constants";
import type {
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
generateCollaborationLinkData,
getCollaborationLink,
getSyncableElements,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
isSavedToFirebase,
@@ -75,14 +79,13 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom } from "jotai";
import { appJotaiStore } from "../app-jotai";
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
import { collabErrorIndicatorAtom } from "./CollabError";
import {
import type {
ReconciledExcalidrawElement,
RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile";
export const collabAPIAtom = atom<CollabAPI | null>(null);
@@ -356,6 +359,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
}
};
@@ -506,6 +510,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
// to database even if deleted before creating the room.
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
this.saveCollabRoomToFirebase(getSyncableElements(elements));
@@ -743,6 +748,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
) => {
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
this.loadImageFiles();
+7 -5
View File
@@ -1,15 +1,15 @@
import {
isSyncableElement,
import type {
SocketUpdateData,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import { isSyncableElement } from "../data";
import { TCollabClass } from "./Collab";
import type { TCollabClass } from "./Collab";
import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import {
import type {
OnUserFollowedPayload,
SocketId,
UserIdleState,
@@ -19,6 +19,7 @@ import throttle from "lodash.throttle";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { encryptData } from "../../packages/excalidraw/data/encryption";
import type { Socket } from "socket.io-client";
import { StoreAction } from "../../packages/excalidraw";
class Portal {
collab: TCollabClass;
@@ -127,6 +128,7 @@ class Portal {
}
return element;
}),
storeAction: StoreAction.UPDATE,
});
}, FILE_UPLOAD_TIMEOUT);
+3 -3
View File
@@ -1,9 +1,9 @@
import React from "react";
import {
arrowBarToLeftIcon,
loginIcon,
ExcalLogo,
} from "../../packages/excalidraw/components/icons";
import { Theme } from "../../packages/excalidraw/element/types";
import type { Theme } from "../../packages/excalidraw/element/types";
import { MainMenu } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { LanguageList } from "./LanguageList";
@@ -42,7 +42,7 @@ export const AppMainMenu: React.FC<{
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.ItemLink
icon={arrowBarToLeftIcon}
icon={loginIcon}
href={`${import.meta.env.VITE_APP_PLUS_APP}${
isExcalidrawPlusSignedUser ? "" : "/sign-up"
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
@@ -1,5 +1,5 @@
import React from "react";
import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
import { loginIcon } from "../../packages/excalidraw/components/icons";
import { useI18n } from "../../packages/excalidraw/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
@@ -61,7 +61,7 @@ export const AppWelcomeScreen: React.FC<{
import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null}
icon={arrowBarToLeftIcon}
icon={loginIcon}
>
Sign up
</WelcomeScreen.Center.MenuItemLink>
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import {
import type {
FileId,
NonDeletedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
+1 -1
View File
@@ -1,7 +1,7 @@
import oc from "open-color";
import React from "react";
import { THEME } from "../../packages/excalidraw/constants";
import { Theme } from "../../packages/excalidraw/element/types";
import type { Theme } from "../../packages/excalidraw/element/types";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
+4 -2
View File
@@ -1,14 +1,15 @@
import { StoreAction } from "../../packages/excalidraw";
import { compressData } from "../../packages/excalidraw/data/encode";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
InitializedExcalidrawImageElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
BinaryFileData,
BinaryFileMetadata,
ExcalidrawImperativeAPI,
@@ -238,5 +239,6 @@ export const updateStaleImageStatuses = (params: {
}
return element;
}),
storeAction: StoreAction.UPDATE,
});
};
+5 -5
View File
@@ -20,19 +20,19 @@ import {
get,
} from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import {
import type {
ExcalidrawElement,
FileId,
} from "../../packages/excalidraw/element/types";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
} from "../../packages/excalidraw/types";
import { MaybePromise } from "../../packages/excalidraw/utility-types";
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
import { debounce } from "../../packages/excalidraw/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager";
+8 -9
View File
@@ -1,12 +1,13 @@
import {
import { reconcileElements } from "../../packages/excalidraw";
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { getSceneVersion } from "../../packages/excalidraw/element";
import Portal from "../collab/Portal";
import type Portal from "../collab/Portal";
import { restoreElements } from "../../packages/excalidraw/data/restore";
import {
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
@@ -19,13 +20,11 @@ import {
decryptData,
} from "../../packages/excalidraw/data/encryption";
import { MIME_TYPES } from "../../packages/excalidraw/constants";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { SyncableExcalidrawElement } from ".";
import { getSyncableElements } from ".";
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { Socket } from "socket.io-client";
import {
RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
// private
// -----------------------------------------------------------------------------
+6 -7
View File
@@ -9,30 +9,30 @@ import {
} from "../../packages/excalidraw/data/encryption";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
SocketId,
UserIdleState,
} from "../../packages/excalidraw/types";
import { MakeBrand } from "../../packages/excalidraw/utility-types";
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
import { bytesToHexString } from "../../packages/excalidraw/utils";
import type { WS_SUBTYPES } from "../app_constants";
import {
DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants";
import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase";
@@ -269,7 +269,6 @@ export const loadScene = async (
// in the scene database/localStorage, and instead fetch them async
// from a different database
files: data.files,
commitToStore: false,
};
};
+2 -2
View File
@@ -1,5 +1,5 @@
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import { AppState } from "../../packages/excalidraw/types";
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { AppState } from "../../packages/excalidraw/types";
import {
clearAppStateForLocalStorage,
getDefaultAppState,
+3 -3
View File
@@ -20,7 +20,7 @@
name="description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<meta name="image" content="https://excalidraw.com/og-image-2.png" />
<meta name="image" content="https://excalidraw.com/og-image-3.png" />
<!-- Open Graph / Facebook -->
<meta property="og:site_name" content="Excalidraw" />
@@ -35,7 +35,7 @@
property="og:description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
<meta property="og:image" content="https://excalidraw.com/og-image-3.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
@@ -51,7 +51,7 @@
/>
<meta
property="twitter:image"
content="https://excalidraw.com/og-twitter-v2.png"
content="https://excalidraw.com/og-image-3.png"
/>
<!-- General tags -->
+4
View File
@@ -40,6 +40,10 @@
}
&.highlighted {
color: var(--color-promo);
font-weight: 700;
.dropdown-menu-item__icon g {
stroke-width: 2;
}
}
}
}
+2 -1
View File
@@ -18,7 +18,8 @@ import {
} from "../../packages/excalidraw/components/icons";
import { TextField } from "../../packages/excalidraw/components/TextField";
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
import type { CollabAPI } from "../collab/Collab";
import { activeRoomLinkAtom } from "../collab/Collab";
import { atom, useAtom, useAtomValue } from "jotai";
import "./ShareDialog.scss";
@@ -216,23 +216,22 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
stroke-width="2"
viewBox="0 0 24 24"
>
<g>
<g
stroke-width="1.5"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M10 12l10 0"
d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
/>
<path
d="M10 12l4 4"
d="M21 12h-13l3 -3"
/>
<path
d="M10 12l4 -4"
/>
<path
d="M4 4l0 16"
d="M11 15l-3 -3"
/>
</g>
</svg>
+6 -4
View File
@@ -12,7 +12,7 @@ import {
createRedoAction,
createUndoAction,
} from "../../packages/excalidraw/actions/actionHistory";
import { newElementWith } from "../../packages/excalidraw";
import { StoreAction, newElementWith } from "../../packages/excalidraw";
const { h } = window;
@@ -90,7 +90,7 @@ describe("collaboration", () => {
updateSceneData({
elements: syncInvalidIndices([rect1, rect2]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
updateSceneData({
@@ -98,7 +98,7 @@ describe("collaboration", () => {
rect1,
newElementWith(h.elements[1], { isDeleted: true }),
]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
await waitFor(() => {
@@ -145,6 +145,7 @@ describe("collaboration", () => {
// simulate force deleting the element remotely
updateSceneData({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
await waitFor(() => {
@@ -182,7 +183,7 @@ describe("collaboration", () => {
h.elements[0],
newElementWith(h.elements[1], { x: 100 }),
]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
await waitFor(() => {
@@ -217,6 +218,7 @@ describe("collaboration", () => {
// simulate force deleting the element remotely
updateSceneData({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
// snapshot was correctly updated and marked the element as deleted
+1 -1
View File
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
import { useEffect, useLayoutEffect, useState } from "react";
import { THEME } from "../packages/excalidraw";
import { EVENT } from "../packages/excalidraw/constants";
import { Theme } from "../packages/excalidraw/element/types";
import type { Theme } from "../packages/excalidraw/element/types";
import { CODES, KEYS } from "../packages/excalidraw/keys";
import { STORAGE_KEYS } from "./app_constants";
+7 -6
View File
@@ -1,6 +1,7 @@
{
"private": true,
"name": "excalidraw-monorepo",
"packageManager": "yarn@1.22.22",
"workspaces": [
"excalidraw-app",
"packages/excalidraw",
@@ -26,8 +27,8 @@
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/socket.io-client": "3.0.0",
"@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0",
@@ -50,7 +51,7 @@
"vite-plugin-ejs": "1.7.0",
"vite-plugin-pwa": "0.17.4",
"vite-plugin-svgr": "2.4.0",
"vitest": "1.0.1",
"vitest": "1.5.3",
"vitest-canvas-mock": "0.3.2"
},
"engines": {
@@ -60,9 +61,9 @@
"prettier": "@excalidraw/prettier-config",
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "node ./scripts/build-version.js",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
"build:app": "yarn --cwd ./excalidraw-app build:app",
"build:version": "yarn --cwd ./excalidraw-app build:version",
"build": "yarn --cwd ./excalidraw-app build",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
+6 -2
View File
@@ -35,9 +35,13 @@ Please add the latest change on the top under the correct section.
### Breaking Changes
- Renamed required `updatedScene` parameter from `commitToHistory` into `commitToStore` [#7348](https://github.com/excalidraw/excalidraw/pull/7348).
- `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898)
### Breaking Changes
| | Before `commitToHistory` | After `storeAction` | Notes |
| --- | --- | --- | --- |
| _Immediately undoable_ | `true` | `"capture"` | As before, use for all updates which should be recorded by the store & history. Should be used for the most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
| _Eventually undoable_ | `false` | `"none"` | Similar to before, use for all updates which should not be recorded immediately (likely exceptions which are part of some async multi-step process) or those not meant to be recorded at all (i.e. updates to `collaborators` object, parts of `AppState` which are not observed by the store & history - not `ObservedAppState`).<br/><br/>**IMPORTANT** It's likely you should switch to `"update"` in all the other cases. Otherwise, all such updates would end up being recorded with the next `"capture"` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
| _Never undoable_ | n/a | `"update"` | **NEW**: previously there was no equivalent for this value. Now, it's recommended to use `"update"` for all remote updates (from the other clients), scene initialization, or those updates, which should not be locally "undoable". These updates will _never_ make it to the local undo / redo stacks. |
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
+4 -3
View File
@@ -1,4 +1,5 @@
import { alignElements, Alignment } from "../align";
import type { Alignment } from "../align";
import { alignElements } from "../align";
import {
AlignBottomIcon,
AlignLeftIcon,
@@ -10,13 +11,13 @@ import {
import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import { AppClassProperties, AppState, UIAppState } from "../types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -1,8 +1,8 @@
import {
BOUND_TEXT_PADDING,
ROUNDNESS,
VERTICAL_ALIGN,
TEXT_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
@@ -23,14 +23,14 @@ import {
isTextBindableContainer,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import type { AppState } from "../types";
import type { Mutable } from "../utility-types";
import { arrayToMap, getFontString } from "../utils";
import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex";
@@ -142,6 +142,7 @@ export const actionBindText = register({
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
@@ -296,6 +297,7 @@ export const actionWrapTextInContainer = register({
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
},
false,
);
+3 -3
View File
@@ -18,13 +18,13 @@ import {
ZOOM_STEP,
} from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types";
import type { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
@@ -35,7 +35,7 @@ import {
isHandToolActive,
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { SceneBounds } from "../element/bounds";
import type { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor";
import { StoreAction } from "../store";
@@ -4,8 +4,8 @@ import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import type { ExcalidrawElement } from "../element/types";
import type { AppState } from "../types";
import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -3,16 +3,17 @@ import {
DistributeVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../distribute";
import type { Distribution } from "../distribute";
import { distributeElements } from "../distribute";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -1,6 +1,6 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
@@ -12,9 +12,9 @@ import {
getSelectedGroupForElement,
getElementsInGroup,
} from "../groups";
import { AppState } from "../types";
import type { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import type { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import {
bindTextToShapeAfterDuplication,
@@ -1,7 +1,7 @@
import { LockedIcon, UnlockedIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { StoreAction } from "../store";
+1 -1
View File
@@ -16,7 +16,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { Theme } from "../element/types";
import type { Theme } from "../element/types";
import "../components/ToolIcon.scss";
import { StoreAction } from "../store";
@@ -13,7 +13,7 @@ import {
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types";
import type { AppState } from "../types";
import { resetCursor } from "../cursor";
import { StoreAction } from "../store";
+11 -9
View File
@@ -1,24 +1,24 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import {
import type {
ExcalidrawElement,
NonDeleted,
NonDeletedSceneElementsMap,
} from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
bindOrUnbindSelectedElements,
bindOrUnbindLinearElements,
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons";
import { StoreAction } from "../store";
import { isLinearElement } from "../element/typeChecks";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
@@ -89,7 +89,6 @@ const flipSelectedElements = (
const updatedElements = flipElements(
selectedElements,
elements,
elementsMap,
appState,
flipDirection,
@@ -105,7 +104,6 @@ const flipSelectedElements = (
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
appState: AppState,
flipDirection: "horizontal" | "vertical",
@@ -119,13 +117,17 @@ const flipElements = (
elementsMap,
"nw",
true,
true,
flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY,
);
isBindingEnabled(appState)
? bindOrUnbindSelectedElements(selectedElements, app)
: unbindLinearElements(selectedElements, elementsMap);
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
app,
isBindingEnabled(appState),
[],
);
return selectedElements;
};
+2 -2
View File
@@ -1,9 +1,9 @@
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameChildren } from "../frame";
import { KEYS } from "../keys";
import { AppClassProperties, AppState, UIAppState } from "../types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { register } from "./register";
+2 -2
View File
@@ -17,12 +17,12 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import {
import type {
ExcalidrawElement,
ExcalidrawTextElement,
OrderedExcalidrawElement,
} from "../element/types";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
getElementsInResizingFrame,
+18 -8
View File
@@ -1,14 +1,16 @@
import { Action, ActionResult } from "./types";
import type { Action, ActionResult } from "./types";
import { UndoIcon, RedoIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { History, HistoryChangedEvent } from "../history";
import { AppState } from "../types";
import type { History } from "../history";
import { HistoryChangedEvent } from "../history";
import type { AppState } from "../types";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
import { SceneElementsMap } from "../element/types";
import { IStore, StoreAction } from "../store";
import type { SceneElementsMap } from "../element/types";
import type { Store } from "../store";
import { StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter";
const writeData = (
@@ -40,7 +42,7 @@ const writeData = (
return { storeAction: StoreAction.NONE };
};
type ActionCreator = (history: History, store: IStore) => Action;
type ActionCreator = (history: History, store: Store) => Action;
export const createUndoAction: ActionCreator = (history, store) => ({
name: "undo",
@@ -63,7 +65,10 @@ export const createUndoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(),
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
);
return (
@@ -74,6 +79,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isUndoStackEmpty}
data-testid="button-undo"
/>
);
},
@@ -101,7 +107,10 @@ export const createRedoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(),
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
);
return (
@@ -112,6 +121,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isRedoStackEmpty}
data-testid="button-redo"
/>
);
},
@@ -1,9 +1,12 @@
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types";
import type { ExcalidrawLinearElement } from "../element/types";
import { StoreAction } from "../store";
import { register } from "./register";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { lineEditorIcon } from "../components/icons";
export const actionToggleLinearEditor = register({
name: "toggleLinearEditor",
@@ -11,18 +14,23 @@ export const actionToggleLinearEditor = register({
label: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement?.id
? "labels.lineEditor.exit"
})[0] as ExcalidrawLinearElement | undefined;
return selectedElement?.type === "arrow"
? "labels.lineEditor.editArrow"
: "labels.lineEditor.edit";
},
keywords: ["line"],
trackEvent: {
category: "element",
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (
!appState.editingLinearElement &&
selectedElements.length === 1 &&
isLinearElement(selectedElements[0])
) {
return true;
}
return false;
@@ -45,4 +53,24 @@ export const actionToggleLinearEditor = register({
storeAction: StoreAction.CAPTURE,
};
},
PanelComponent: ({ appState, updateData, app }) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement;
const label = t(
selectedElement.type === "arrow"
? "labels.lineEditor.editArrow"
: "labels.lineEditor.edit",
);
return (
<ToolButton
type="button"
icon={lineEditorIcon}
title={label}
aria-label={label}
onClick={() => updateData(null)}
/>
);
},
});
@@ -1,6 +1,6 @@
import { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar";
import { GoToCollaboratorComponentProps } from "../components/UserList";
import type { GoToCollaboratorComponentProps } from "../components/UserList";
import {
eyeIcon,
microphoneIcon,
@@ -8,7 +8,7 @@ import {
} from "../components/icons";
import { t } from "../i18n";
import { StoreAction } from "../store";
import { Collaborator } from "../types";
import type { Collaborator } from "../types";
import { register } from "./register";
import clsx from "clsx";
@@ -1,4 +1,4 @@
import { AppClassProperties, AppState, Primitive } from "../types";
import type { AppClassProperties, AppState, Primitive } from "../types";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -74,7 +74,7 @@ import {
isLinearElement,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
import type {
Arrowhead,
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -167,7 +167,7 @@ const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement)) {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
return nextElement;
}
return mutateElement(
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
+1 -1
View File
@@ -24,7 +24,7 @@ import {
isArrowElement,
} from "../element/typeChecks";
import { getSelectedElements } from "../scene";
import { ExcalidrawTextElement } from "../element/types";
import type { ExcalidrawTextElement } from "../element/types";
import { paintIcon } from "../components/icons";
import { StoreAction } from "../store";
@@ -0,0 +1,48 @@
import { isTextElement } from "../element";
import { newElementWith } from "../element/mutateElement";
import { measureText } from "../element/textElement";
import { getSelectedElements } from "../scene";
import { StoreAction } from "../store";
import type { AppClassProperties } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionTextAutoResize = register({
name: "autoResize",
label: "labels.autoResize",
icon: null,
trackEvent: { category: "element" },
predicate: (elements, appState, _: unknown, app: AppClassProperties) => {
const selectedElements = getSelectedElements(elements, appState);
return (
selectedElements.length === 1 &&
isTextElement(selectedElements[0]) &&
!selectedElements[0].autoResize
);
},
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
return {
appState,
elements: elements.map((element) => {
if (element.id === selectedElements[0].id && isTextElement(element)) {
const metrics = measureText(
element.originalText,
getFontString(element),
element.lineHeight,
);
return newElementWith(element, {
autoResize: true,
width: metrics.width,
height: metrics.height,
text: element.originalText,
});
}
return element;
}),
storeAction: StoreAction.CAPTURE,
};
},
});
@@ -1,7 +1,7 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { GRID_SIZE } from "../constants";
import { AppState } from "../types";
import type { AppState } from "../types";
import { gridIcon } from "../components/icons";
import { StoreAction } from "../store";
@@ -20,6 +20,7 @@ import { StoreAction } from "../store";
export const actionSendBackward = register({
name: "sendBackward",
label: "labels.sendBackward",
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -49,6 +50,7 @@ export const actionSendBackward = register({
export const actionBringForward = register({
name: "bringForward",
label: "labels.bringForward",
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -78,6 +80,7 @@ export const actionBringForward = register({
export const actionSendToBack = register({
name: "sendToBack",
label: "labels.sendToBack",
keywords: ["move down", "zindex", "layer"],
icon: SendToBackIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -114,6 +117,7 @@ export const actionSendToBack = register({
export const actionBringToFront = register({
name: "bringToFront",
label: "labels.bringToFront",
keywords: ["move up", "zindex", "layer"],
icon: BringToFrontIcon,
trackEvent: { category: "element" },
+6 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import {
import type {
Action,
UpdaterFn,
ActionName,
@@ -7,8 +7,11 @@ import {
PanelComponentProps,
ActionSource,
} from "./types";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics";
import { isPromiseLike } from "../utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import { Action } from "./types";
import type { Action } from "./types";
export let actions: readonly Action[] = [];
+2 -2
View File
@@ -1,8 +1,8 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import { SubtypeOf } from "../utility-types";
import type { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
import type { ActionName } from "./types";
export type ShortcutName =
| SubtypeOf<
+12 -7
View File
@@ -1,14 +1,17 @@
import React from "react";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import {
import type React from "react";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type {
AppClassProperties,
AppState,
ExcalidrawProps,
BinaryFiles,
UIAppState,
} from "../types";
import { MarkOptional } from "../utility-types";
import { StoreAction } from "../store";
import type { MarkOptional } from "../utility-types";
import type { StoreActionType } from "../store";
export type ActionSource =
| "ui"
@@ -26,7 +29,7 @@ export type ActionResult =
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
files?: BinaryFiles | null;
storeAction: keyof typeof StoreAction;
storeAction: StoreActionType;
replaceFiles?: boolean;
}
| false;
@@ -131,7 +134,9 @@ export type ActionName =
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer"
| "commandPalette";
| "commandPalette"
| "autoResize"
| "elementStats";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
+3 -2
View File
@@ -1,6 +1,7 @@
import { ElementsMap, ExcalidrawElement } from "./element/types";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import type { BoundingBox } from "./element/bounds";
import { getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
+4 -3
View File
@@ -1,6 +1,7 @@
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
import { AnimationFrameHandler } from "./animation-frame-handler";
import { AppState } from "./types";
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
import { LaserPointer } from "@excalidraw/laser-pointer";
import type { AnimationFrameHandler } from "./animation-frame-handler";
import type { AppState } from "./types";
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
import type App from "./components/App";
import { SVG_NS } from "./constants";
+1 -1
View File
@@ -7,7 +7,7 @@ import {
EXPORT_SCALES,
THEME,
} from "./constants";
import { AppState, NormalizedZoomValue } from "./types";
import type { AppState, NormalizedZoomValue } from "./types";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio
+23 -18
View File
@@ -1,18 +1,14 @@
import { ENV } from "./constants";
import type { BindableProp, BindingProp } from "./element/binding";
import {
BoundElement,
BindableElement,
BindableProp,
BindingProp,
bindingProperties,
updateBoundElements,
} from "./element/binding";
import { LinearElementEditor } from "./element/linearElementEditor";
import {
ElementUpdate,
mutateElement,
newElementWith,
} from "./element/mutateElement";
import type { ElementUpdate } from "./element/mutateElement";
import { mutateElement, newElementWith } from "./element/mutateElement";
import {
getBoundTextElementId,
redrawTextBoundingBox,
@@ -23,7 +19,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
@@ -34,13 +30,13 @@ import {
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups";
import { getObservedAppState } from "./store";
import {
import type {
AppState,
ObservedAppState,
ObservedElementsAppState,
ObservedStandaloneAppState,
} from "./types";
import { SubtypeOf, ValueOf } from "./utility-types";
import type { SubtypeOf, ValueOf } from "./utility-types";
import {
arrayToMap,
arrayToObject,
@@ -1481,19 +1477,28 @@ export class ElementsChange implements Change<SceneElementsMap> {
return elements;
}
const previous = Array.from(elements.values());
const reordered = orderByFractionalIndex([...previous]);
const unordered = Array.from(elements.values());
const ordered = orderByFractionalIndex([...unordered]);
const moved = Delta.getRightDifferences(unordered, ordered, true).reduce(
(acc, arrayIndex) => {
const candidate = unordered[Number(arrayIndex)];
if (candidate && changed.has(candidate.id)) {
acc.set(candidate.id, candidate);
}
if (
!flags.containsVisibleDifference &&
Delta.isRightDifferent(previous, reordered, true)
) {
return acc;
},
new Map(),
);
if (!flags.containsVisibleDifference && moved.size) {
// we found a difference in order!
flags.containsVisibleDifference = true;
}
// let's synchronize all invalid indices of moved elements
return arrayToMap(syncMovedIndices(reordered, changed)) as typeof elements;
// synchronize all elements that were actually moved
// could fallback to synchronizing all invalid indices
return arrayToMap(syncMovedIndices(ordered, moved)) as typeof elements;
}
/**
+2 -6
View File
@@ -1,9 +1,5 @@
import {
Spreadsheet,
tryParseCells,
tryParseNumber,
VALID_SPREADSHEET,
} from "./charts";
import type { Spreadsheet } from "./charts";
import { tryParseCells, tryParseNumber, VALID_SPREADSHEET } from "./charts";
describe("charts", () => {
describe("tryParseNumber", () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types";
import type { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
+3 -3
View File
@@ -5,13 +5,13 @@ import {
THEME,
} from "./constants";
import { roundRect } from "./renderer/roundRect";
import { InteractiveCanvasRenderConfig } from "./scene/types";
import {
import type { InteractiveCanvasRenderConfig } from "./scene/types";
import type {
Collaborator,
InteractiveCanvasAppState,
SocketId,
UserIdleState,
} from "./types";
import { UserIdleState } from "./types";
function hashToInteger(id: string) {
let hash = 0;
+4 -3
View File
@@ -1,9 +1,10 @@
import {
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { BinaryFiles } from "./types";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import type { BinaryFiles } from "./types";
import type { Spreadsheet } from "./charts";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import {
ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES,
+1 -1
View File
@@ -1,5 +1,5 @@
import oc from "open-color";
import { Merge } from "./utility-types";
import type { Merge } from "./utility-types";
// FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
+18 -7
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { ActionManager } from "../actions/manager";
import {
import type { ActionManager } from "../actions/manager";
import type {
ExcalidrawElement,
ExcalidrawElementType,
NonDeletedElementsMap,
@@ -17,13 +17,17 @@ import {
hasStrokeWidth,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics";
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
import {
hasBoundTextElement,
isLinearElement,
isTextElement,
} from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import { Tooltip } from "./Tooltip";
@@ -114,6 +118,11 @@ export const SelectedShapeActions = ({
const showLinkIcon =
targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction =
!appState.editingLinearElement &&
targetElements.length === 1 &&
isLinearElement(targetElements[0]);
return (
<div className="panelColumn">
<div>
@@ -173,8 +182,8 @@ export const SelectedShapeActions = ({
<div className="buttonList">
{renderAction("sendToBack")}
{renderAction("sendBackward")}
{renderAction("bringToFront")}
{renderAction("bringForward")}
{renderAction("bringToFront")}
</div>
</fieldset>
@@ -229,6 +238,7 @@ export const SelectedShapeActions = ({
{renderAction("group")}
{renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")}
{showLineEditorAction && renderAction("toggleLinearEditor")}
</div>
</fieldset>
)}
@@ -333,8 +343,8 @@ export const ShapesSwitcher = ({
fontSize: 8,
fontFamily: "Cascadia, monospace",
position: "absolute",
background: "pink",
color: "black",
background: "var(--color-promo)",
color: "var(--color-surface-lowest)",
bottom: 3,
right: 4,
}}
@@ -458,6 +468,7 @@ export const ExitZenModeAction = ({
showExitZenModeBtn: boolean;
}) => (
<button
type="button"
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
+262 -189
View File
@@ -1,7 +1,7 @@
import React, { useContext } from "react";
import { flushSync } from "react-dom";
import { RoughCanvas } from "roughjs/bin/canvas";
import type { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
import { nanoid } from "nanoid";
@@ -39,18 +39,16 @@ import {
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { Action, ActionResult } from "../actions/types";
import type { Action, ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
import {
PastedMixedContent,
copyTextToSystemClipboard,
parseClipboard,
} from "../clipboard";
import type { PastedMixedContent } from "../clipboard";
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
import type { EXPORT_IMAGE_TYPES } from "../constants";
import {
APP_NAME,
CURSOR_TYPE,
@@ -62,7 +60,6 @@ import {
ENV,
EVENT,
FRAME_STYLE,
EXPORT_IMAGE_TYPES,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
@@ -90,8 +87,11 @@ import {
EDITOR_LS_KEYS,
isIOS,
supportsResizeObserver,
DEFAULT_COLLISION_THRESHOLD,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
import type { ExportedElements } from "../data";
import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore";
import {
@@ -115,23 +115,22 @@ import {
newTextElement,
newImageElement,
transformElements,
updateTextElement,
refreshTextDimensions,
redrawTextBoundingBox,
getElementAbsoluteCoords,
} from "../element";
import {
bindOrUnbindLinearElement,
bindOrUnbindSelectedElements,
bindOrUnbindLinearElements,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
getEligibleElementsForBinding,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
unbindLinearElements,
updateBoundElements,
getSuggestedBindingsForArrows,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -163,7 +162,7 @@ import {
isMagicFrameElement,
isTextBindableContainer,
} from "../element/typeChecks";
import {
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
@@ -183,7 +182,6 @@ import {
ExcalidrawIframeElement,
ExcalidrawEmbeddableElement,
Ordered,
OrderedExcalidrawElement,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@@ -221,11 +219,14 @@ import {
isSomeElementSelected,
} from "../scene";
import Scene from "../scene/Scene";
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
import type {
RenderInteractiveSceneCallback,
ScrollBars,
} from "../scene/types";
import { getStateForZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes";
import type { GeometricShape } from "../../utils/geometry/shape";
import {
GeometricShape,
getClosedCurveShape,
getCurveShape,
getEllipseShape,
@@ -234,7 +235,7 @@ import {
getSelectionBoxShape,
} from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision";
import {
import type {
AppClassProperties,
AppProps,
AppState,
@@ -292,11 +293,8 @@ import {
maybeParseEmbedSrc,
getEmbedLink,
} from "../element/embeddable";
import {
ContextMenu,
ContextMenuItems,
CONTEXT_MENU_SEPARATOR,
} from "./ContextMenu";
import type { ContextMenuItems } from "./ContextMenu";
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
import LayerUI from "./LayerUI";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
@@ -321,7 +319,8 @@ import {
updateImageCache as _updateImageCache,
} from "../element/image";
import throttle from "lodash.throttle";
import { fileOpen, FileSystemHandle } from "../data/filesystem";
import type { FileSystemHandle } from "../data/filesystem";
import { fileOpen } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
@@ -333,6 +332,8 @@ import {
getLineHeightInPx,
isMeasureTextSupported,
isValidTextContainer,
measureText,
wrapText,
} from "../element/textElement";
import {
showHyperlinkTooltip,
@@ -387,11 +388,9 @@ import {
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper";
import {
ExcalidrawElementSkeleton,
convertToExcalidrawElements,
} from "../data/transform";
import { ValueOf } from "../utility-types";
import type { ExcalidrawElementSkeleton } from "../data/transform";
import { convertToExcalidrawElements } from "../data/transform";
import type { ValueOf } from "../utility-types";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { StaticCanvas, InteractiveCanvas } from "./canvases";
import { Renderer } from "../scene/Renderer";
@@ -405,14 +404,15 @@ import {
} from "../cursor";
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
import { MagicCacheData, diagramToHTML } from "../data/magic";
import type { MagicCacheData } from "../data/magic";
import { diagramToHTML } from "../data/magic";
import { exportToBlob } from "../../utils/export";
import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
import { EditorLocalStorage } from "../data/EditorLocalStorage";
import FollowMode from "./FollowMode/FollowMode";
import { IStore, Store, StoreAction } from "../store";
import { Store, StoreAction } from "../store";
import { AnimationFrameHandler } from "../animation-frame-handler";
import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails";
@@ -432,6 +432,9 @@ import {
isPointHittingLinkIcon,
} from "./hyperlink/helpers";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
import { getVisibleSceneBounds } from "../element/bounds";
import { Stats } from "./Stats";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@@ -543,7 +546,7 @@ class App extends React.Component<AppProps, AppState> {
public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined;
public id: string;
private store: IStore;
store: Store;
private history: History;
private excalidrawContainerValue: {
container: HTMLDivElement | null;
@@ -717,10 +720,7 @@ class App extends React.Component<AppProps, AppState> {
id: this.id,
};
this.fonts = new Fonts({
scene: this.scene,
onSceneUpdated: this.onSceneUpdated,
});
this.fonts = new Fonts({ scene: this.scene });
this.history = new History();
this.actionManager.registerAll(actions);
@@ -943,7 +943,7 @@ class App extends React.Component<AppProps, AppState> {
});
if (updated) {
this.scene.informMutation();
this.scene.triggerUpdate();
}
// GC
@@ -1455,10 +1455,10 @@ class App extends React.Component<AppProps, AppState> {
const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props;
const versionNonce = this.scene.getVersionNonce();
const sceneNonce = this.scene.getSceneNonce();
const { elementsMap, visibleElements } =
this.renderer.getRenderableElements({
versionNonce,
sceneNonce,
zoom: this.state.zoom,
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
@@ -1670,13 +1670,26 @@ class App extends React.Component<AppProps, AppState> {
}}
/>
)}
{this.state.showStats && (
<Stats
appState={this.state}
setAppState={this.setState}
scene={this.scene}
onClose={() => {
this.actionManager.executeAction(
actionToggleStats,
);
}}
renderCustomStats={renderCustomStats}
/>
)}
<StaticCanvas
canvas={this.canvas}
rc={this.rc}
elementsMap={elementsMap}
allElementsMap={allElementsMap}
visibleElements={visibleElements}
versionNonce={versionNonce}
sceneNonce={sceneNonce}
selectionNonce={
this.state.selectionElement?.versionNonce
}
@@ -1698,12 +1711,13 @@ class App extends React.Component<AppProps, AppState> {
elementsMap={elementsMap}
visibleElements={visibleElements}
selectedElements={selectedElements}
versionNonce={versionNonce}
sceneNonce={sceneNonce}
selectionNonce={
this.state.selectionElement?.versionNonce
}
scale={window.devicePixelRatio}
appState={this.state}
device={this.device}
renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback
}
@@ -1821,7 +1835,7 @@ class App extends React.Component<AppProps, AppState> {
);
}
this.magicGenerations.set(frameElement.id, data);
this.onSceneUpdated();
this.triggerRender();
};
private getTextFromElements(elements: readonly ExcalidrawElement[]) {
@@ -2123,7 +2137,7 @@ class App extends React.Component<AppProps, AppState> {
}
return el;
}),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
}
},
@@ -2446,7 +2460,7 @@ class App extends React.Component<AppProps, AppState> {
this.history.record(increment.elementsChange, increment.appStateChange);
});
this.scene.addCallback(this.onSceneUpdated);
this.scene.onUpdate(this.triggerRender);
this.addEventListeners();
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
@@ -2491,6 +2505,7 @@ class App extends React.Component<AppProps, AppState> {
public componentWillUnmount() {
this.renderer.destroy();
this.scene = new Scene();
this.fonts = new Fonts({ scene: this.scene });
this.renderer = new Renderer(this.scene);
this.files = {};
this.imageCache.clear();
@@ -2568,7 +2583,7 @@ class App extends React.Component<AppProps, AppState> {
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
addEventListener(
document,
EVENT.MOUSE_MOVE,
EVENT.POINTER_MOVE,
this.updateCurrentCursorPosition,
),
// rerender text elements on font load to fix #637 && #1553
@@ -2597,6 +2612,9 @@ class App extends React.Component<AppProps, AppState> {
),
addEventListener(window, EVENT.FOCUS, () => {
this.maybeCleanupAfterMissingPointerUp(null);
// browsers (chrome?) tend to free up memory a lot, which results
// in canvas context being cleared. Thus re-render on focus.
this.triggerRender(true);
}),
);
@@ -2810,7 +2828,7 @@ class App extends React.Component<AppProps, AppState> {
);
}
this.store.capture(elementsMap, this.state);
this.store.commit(elementsMap, this.state);
// Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during
@@ -3341,32 +3359,53 @@ class App extends React.Component<AppProps, AppState> {
text,
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
textAlign: this.state.currentItemTextAlign,
textAlign: DEFAULT_TEXT_ALIGN,
verticalAlign: DEFAULT_VERTICAL_ALIGN,
locked: false,
};
const fontString = getFontString({
fontSize: textElementProps.fontSize,
fontFamily: textElementProps.fontFamily,
});
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
const [x1, , x2] = getVisibleSceneBounds(this.state);
// long texts should not go beyond 800 pixels in width nor should it go below 200 px
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
const LINE_GAP = 10;
let currentY = y;
const lines = isPlainPaste ? [text] : text.split("\n");
const textElements = lines.reduce(
(acc: ExcalidrawTextElement[], line, idx) => {
const text = line.trim();
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) {
const originalText = line.trim();
if (originalText.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x,
y: currentY,
});
let metrics = measureText(originalText, fontString, lineHeight);
const isTextWrapped = metrics.width > maxTextWidth;
const text = isTextWrapped
? wrapText(originalText, fontString, maxTextWidth)
: originalText;
metrics = isTextWrapped
? measureText(text, fontString, lineHeight)
: metrics;
const startX = x - metrics.width / 2;
const startY = currentY - metrics.height / 2;
const element = newTextElement({
...textElementProps,
x,
y: currentY,
x: startX,
y: startY,
text,
originalText,
lineHeight,
autoResize: !isTextWrapped,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
acc.push(element);
@@ -3672,7 +3711,7 @@ class App extends React.Component<AppProps, AppState> {
ShapeCache.delete(element);
}
});
this.scene.informMutation();
this.scene.triggerUpdate();
this.addNewImagesToImageCache();
},
@@ -3683,51 +3722,39 @@ class App extends React.Component<AppProps, AppState> {
elements?: SceneData["elements"];
appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"];
commitToStore?: SceneData["commitToStore"];
/** @default StoreAction.NONE */
storeAction?: SceneData["storeAction"];
}) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
if (sceneData.commitToStore) {
this.store.shouldCaptureIncrement();
}
if (sceneData.storeAction && sceneData.storeAction !== StoreAction.NONE) {
const prevCommittedAppState = this.store.snapshot.appState;
const prevCommittedElements = this.store.snapshot.elements;
if (sceneData.elements || sceneData.appState) {
let nextCommittedAppState = this.state;
let nextCommittedElements: Map<string, OrderedExcalidrawElement>;
const nextCommittedAppState = sceneData.appState
? Object.assign({}, prevCommittedAppState, sceneData.appState) // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState`
: prevCommittedAppState;
if (sceneData.appState) {
nextCommittedAppState = {
...this.state,
...sceneData.appState, // Here we expect just partial appState
};
}
const nextCommittedElements = sceneData.elements
? this.store.filterUncomittedElements(
this.scene.getElementsMapIncludingDeleted(), // Only used to detect uncomitted local elements
arrayToMap(nextElements), // We expect all (already reconciled) elements
)
: prevCommittedElements;
const prevElements = this.scene.getElementsIncludingDeleted();
if (sceneData.elements) {
/**
* We need to schedule a snapshot update, as in case `commitToStore` is false (i.e. remote update),
* as it's essential for computing local changes after the async action is completed (i.e. not to include remote changes in the diff).
*
* This is also a breaking change for all local `updateScene` calls without set `commitToStore` to true,
* as it makes such updates impossible to undo (previously they were undone coincidentally with the switch to the whole snapshot captured by the history).
*
* WARN: be careful here as moving it elsewhere could break the history for remote client without noticing
* - we need to find a way to test two concurrent client updates simultaneously, while having access to both stores & histories.
*/
this.store.shouldUpdateSnapshot();
// TODO#7348: deprecate once exchanging just store increments between clients
nextCommittedElements = this.store.ignoreUncomittedElements(
arrayToMap(prevElements),
arrayToMap(nextElements),
// WARN: store action always performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
// do NOT schedule store actions (execute after re-render), as it might cause unexpected concurrency issues if not handled well
if (sceneData.storeAction === StoreAction.CAPTURE) {
this.store.captureIncrement(
nextCommittedElements,
nextCommittedAppState,
);
} else if (sceneData.storeAction === StoreAction.UPDATE) {
this.store.updateSnapshot(
nextCommittedElements,
nextCommittedAppState,
);
} else {
nextCommittedElements = arrayToMap(prevElements);
}
// WARN: Performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
this.store.capture(nextCommittedElements, nextCommittedAppState);
}
if (sceneData.appState) {
@@ -3744,8 +3771,15 @@ class App extends React.Component<AppProps, AppState> {
},
);
private onSceneUpdated = () => {
this.setState({});
private triggerRender = (
/** force always re-renders canvas even if no change */
force?: boolean,
) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({});
}
};
/**
@@ -3949,7 +3983,12 @@ class App extends React.Component<AppProps, AppState> {
});
});
this.maybeSuggestBindingForAll(selectedElements);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
@@ -4116,11 +4155,12 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements, this)
: unbindLinearElements(selectedElements, elementsMap);
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
this.setState({ suggestedBindings: [] });
}
});
@@ -4179,6 +4219,11 @@ class App extends React.Component<AppProps, AppState> {
originSnapOffset: null,
activeEmbeddable: null,
} as const;
if (nextActiveTool.type === "freedraw") {
this.store.shouldCaptureIncrement();
}
if (nextActiveTool.type !== "selection") {
return {
...prevState,
@@ -4303,25 +4348,22 @@ class App extends React.Component<AppProps, AppState> {
) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = (
text: string,
originalText: string,
isDeleted: boolean,
) => {
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
{
text,
isDeleted,
originalText,
},
);
return newElementWith(_element, {
originalText: nextOriginalText,
isDeleted: isDeleted ?? _element.isDeleted,
// returns (wrapped) text and new dimensions
...refreshTextDimensions(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
nextOriginalText,
),
});
}
return _element;
}),
@@ -4344,15 +4386,15 @@ class App extends React.Component<AppProps, AppState> {
viewportY - this.state.offsetTop,
];
},
onChange: withBatchedUpdates((text) => {
updateElement(text, text, false);
onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element, elementsMap);
}
}),
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
const isDeleted = !text.trim();
updateElement(text, originalText, isDeleted);
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
const isDeleted = !nextOriginalText.trim();
updateElement(nextOriginalText, isDeleted);
// select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) {
@@ -4397,7 +4439,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.text, element.originalText, false);
updateElement(element.originalText, false);
}
private deselectElements() {
@@ -4536,7 +4578,7 @@ class App extends React.Component<AppProps, AppState> {
shape: this.getElementShape(elementWithHighestZIndex),
// when overlapping, we would like to be more precise
// this also avoids the need to update past tests
threshold: this.getHitThreshold() / 2,
threshold: this.getElementHitThreshold() / 2,
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
: null,
@@ -4599,8 +4641,8 @@ class App extends React.Component<AppProps, AppState> {
return elements;
}
private getHitThreshold() {
return 10 / this.state.zoom.value;
private getElementHitThreshold() {
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
}
private hitElement(
@@ -4618,7 +4660,7 @@ class App extends React.Component<AppProps, AppState> {
const selectionShape = getSelectionBoxShape(
element,
this.scene.getNonDeletedElementsMap(),
this.getHitThreshold(),
this.getElementHitThreshold(),
);
return isPointInShape([x, y], selectionShape);
@@ -4639,7 +4681,7 @@ class App extends React.Component<AppProps, AppState> {
y,
element,
shape: this.getElementShape(element),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(element)
? this.frameNameBoundsCache.get(element)
: null,
@@ -4671,7 +4713,7 @@ class App extends React.Component<AppProps, AppState> {
y,
element: elements[index],
shape: this.getElementShape(elements[index]),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
})
) {
hitElement = elements[index];
@@ -4924,7 +4966,7 @@ class App extends React.Component<AppProps, AppState> {
y: sceneY,
element: container,
shape: this.getElementShape(container),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
})
) {
const midPoint = getContainerCenter(
@@ -5104,8 +5146,11 @@ class App extends React.Component<AppProps, AppState> {
this.translateCanvas({
zoom: zoomState.zoom,
scrollX: zoomState.scrollX + deltaX / nextZoom,
scrollY: zoomState.scrollY + deltaY / nextZoom,
// 2x multiplier is just a magic number that makes this work correctly
// on touchscreen devices (note: if we get report that panning is slower/faster
// than actual movement, consider swapping with devicePixelRatio)
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
shouldCacheIgnoreZoom: true,
});
});
@@ -5339,24 +5384,41 @@ class App extends React.Component<AppProps, AppState> {
!isOverScrollBar &&
!this.state.editingLinearElement
) {
const elementWithTransformHandleType = getElementWithTransformHandleType(
elements,
this.state,
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
);
if (
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType),
// for linear elements, we'd like to prioritize point dragging over edge resizing
// therefore, we update and check hovered point index first
if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
);
return;
}
if (
!this.state.selectedLinearElement ||
this.state.selectedLinearElement.hoverPointIndex === -1
) {
const elementWithTransformHandleType =
getElementWithTransformHandleType(
elements,
this.state,
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device,
);
if (
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType),
);
return;
}
}
} else if (selectedElements.length > 1 && !isOverScrollBar) {
const transformHandleType = getTransformHandleTypeFromCoords(
@@ -5365,6 +5427,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY,
this.state.zoom,
event.pointerType,
this.device,
);
if (transformHandleType) {
setCursor(
@@ -5517,7 +5580,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointer.x,
scenePointer.y,
);
const threshold = this.getHitThreshold();
const threshold = this.getElementHitThreshold();
const point = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance) {
@@ -5562,7 +5625,7 @@ class App extends React.Component<AppProps, AppState> {
}
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.onSceneUpdated();
this.triggerRender();
}
};
@@ -5614,7 +5677,7 @@ class App extends React.Component<AppProps, AppState> {
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else {
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
}
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
@@ -5704,6 +5767,7 @@ class App extends React.Component<AppProps, AppState> {
this.state,
),
},
storeAction: StoreAction.UPDATE,
});
return;
}
@@ -6313,7 +6377,14 @@ class App extends React.Component<AppProps, AppState> {
const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
if (
selectedElements.length === 1 &&
!this.state.editingLinearElement &&
!(
this.state.selectedLinearElement &&
this.state.selectedLinearElement.hoverPointIndex !== -1
)
) {
const elementWithTransformHandleType =
getElementWithTransformHandleType(
elements,
@@ -6323,6 +6394,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device,
);
if (elementWithTransformHandleType != null) {
this.setState({
@@ -6338,6 +6410,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y,
this.state.zoom,
event.pointerType,
this.device,
);
}
if (pointerDownState.resize.handleType) {
@@ -6594,7 +6667,7 @@ class App extends React.Component<AppProps, AppState> {
}
// How many pixels off the shape boundary we still consider a hit
const threshold = this.getHitThreshold();
const threshold = this.getElementHitThreshold();
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
return (
point.x > x1 - threshold &&
@@ -7431,7 +7504,12 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
this.maybeSuggestBindingForAll(selectedElements);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
// We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
@@ -7993,6 +8071,7 @@ class App extends React.Component<AppProps, AppState> {
appState: {
draggingElement: null,
},
storeAction: StoreAction.UPDATE,
});
return;
@@ -8038,7 +8117,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
);
this.scene.informMutation();
this.scene.triggerUpdate();
}
}
}
@@ -8165,6 +8244,7 @@ class App extends React.Component<AppProps, AppState> {
elements: this.scene
.getElementsIncludingDeleted()
.filter((el) => el.id !== resizingElement.id),
storeAction: StoreAction.UPDATE,
});
}
@@ -8417,7 +8497,7 @@ class App extends React.Component<AppProps, AppState> {
y: pointerDownState.origin.y,
element: hitElement,
shape: this.getElementShape(hitElement),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(hitElement)
? this.frameNameBoundsCache.get(hitElement)
: null,
@@ -8476,15 +8556,18 @@ class App extends React.Component<AppProps, AppState> {
}
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(
this.scene.getSelectedElements(this.state),
this,
)
: unbindLinearElements(
this.scene.getNonDeletedElements(),
elementsMap,
);
// We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end").
const linearElements = this.scene
.getSelectedElements(this.state)
.filter(isLinearElement);
bindOrUnbindLinearElements(
linearElements,
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
}
if (activeTool.type === "laser") {
@@ -8529,7 +8612,7 @@ class App extends React.Component<AppProps, AppState> {
private restoreReadyToEraseElements = () => {
this.elementsPendingErasure = new Set();
this.onSceneUpdated();
this.triggerRender();
};
private eraseElements = () => {
@@ -8943,7 +9026,7 @@ class App extends React.Component<AppProps, AppState> {
files,
);
if (updatedFiles.size) {
this.scene.informMutation();
this.scene.triggerUpdate();
}
}
};
@@ -9016,19 +9099,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings });
};
private maybeSuggestBindingForAll(
selectedElements: NonDeleted<ExcalidrawElement>[],
): void {
if (selectedElements.length > 50) {
return;
}
const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this,
);
this.setState({ suggestedBindings });
}
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -9228,13 +9298,12 @@ class App extends React.Component<AppProps, AppState> {
}
if (ret.type === MIME_TYPES.excalidraw) {
// Restore the fractional indices by mutating elements and update the
// store snapshot, otherwise we would end up with duplicate indices
// restore the fractional indices by mutating elements
syncInvalidIndices(elements.concat(ret.data.elements));
this.store.snapshot = this.store.snapshot.clone(
arrayToMap(elements),
this.state,
);
// update the store snapshot for old elements, otherwise we would end up with duplicated fractional indices on undo
this.store.updateSnapshot(arrayToMap(elements), this.state);
this.setState({ isLoading: true });
this.syncActionResult({
...ret.data,
@@ -9416,8 +9485,6 @@ class App extends React.Component<AppProps, AppState> {
this.state.originSnapOffset,
);
this.maybeSuggestBindingForAll([draggingElement]);
// highlight elements that are to be added to frames on frames creation
if (
this.state.activeTool.type === TOOL_TYPE.frame ||
@@ -9531,7 +9598,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getElementsMapIncludingDeleted(),
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),
selectedElements.length === 1 && isImageElement(selectedElements[0])
selectedElements.some((element) => isImageElement(element))
? !shouldMaintainAspectRatio(event)
: shouldMaintainAspectRatio(event),
resizeX,
@@ -9540,7 +9607,10 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.resize.center.y,
)
) {
this.maybeSuggestBindingForAll(selectedElements);
const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements,
this,
);
const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => {
@@ -9554,6 +9624,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({
elementsToHighlight: [...elementsToHighlight],
suggestedBindings,
});
return true;
@@ -9610,6 +9681,7 @@ class App extends React.Component<AppProps, AppState> {
}
return [
CONTEXT_MENU_SEPARATOR,
actionCut,
actionCopy,
actionPaste,
@@ -9622,6 +9694,7 @@ class App extends React.Component<AppProps, AppState> {
actionPasteStyles,
CONTEXT_MENU_SEPARATOR,
actionGroup,
actionTextAutoResize,
actionUnbindText,
actionBindText,
actionWrapTextInContainer,
@@ -28,6 +28,7 @@ export const ButtonIconSelect = <T extends Object>(
{props.options.map((option) =>
props.type === "button" ? (
<button
type="button"
key={option.text}
onClick={(event) => props.onClick(option.value, event)}
className={clsx({
@@ -22,7 +22,12 @@ export const CheckboxItem: React.FC<{
).focus();
}}
>
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
<button
type="button"
className="Checkbox-box"
role="checkbox"
aria-checked={checked}
>
{checkIcon}
</button>
<div className="Checkbox-label">{children}</div>
@@ -1,10 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker";
import { useAtom } from "jotai";
import {
ColorPickerType,
activeColorPickerSectionAtom,
} from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys";
@@ -1,16 +1,15 @@
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import type { ExcalidrawElement } from "../../element/types";
import type { AppState } from "../../types";
import { TopPicks } from "./TopPicks";
import { Picker } from "./Picker";
import * as Popover from "@radix-ui/react-popover";
import { useAtom } from "jotai";
import {
activeColorPickerSectionAtom,
ColorPickerType,
} from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import { useDevice, useExcalidrawContainer } from "../App";
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
import { COLOR_PALETTE } from "../../colors";
import PickerHeading from "./PickerHeading";
import { t } from "../../i18n";
import clsx from "clsx";
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { t } from "../../i18n";
import { ExcalidrawElement } from "../../element/types";
import type { ExcalidrawElement } from "../../element/types";
import { ShadeList } from "./ShadeList";
import PickerColorList from "./PickerColorList";
@@ -9,15 +9,15 @@ import { useAtom } from "jotai";
import { CustomColorList } from "./CustomColorList";
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import PickerHeading from "./PickerHeading";
import type { ColorPickerType } from "./colorPickerUtils";
import {
ColorPickerType,
activeColorPickerSectionAtom,
getColorNameAndShadeFromColor,
getMostUsedCustomColors,
isCustomColor,
} from "./colorPickerUtils";
import type { ColorPaletteCustom } from "../../colors";
import {
ColorPaletteCustom,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../../colors";
@@ -7,8 +7,9 @@ import {
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { ColorPaletteCustom } from "../../colors";
import { TranslationKeys, t } from "../../i18n";
import type { ColorPaletteCustom } from "../../colors";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
interface PickerColorListProps {
palette: ColorPaletteCustom;
@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import type { ReactNode } from "react";
const PickerHeading = ({ children }: { children: ReactNode }) => (
<div className="color-picker__heading">{children}</div>
@@ -7,7 +7,7 @@ import {
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { t } from "../../i18n";
import { ColorPaletteCustom } from "../../colors";
import type { ColorPaletteCustom } from "../../colors";
interface ShadeListProps {
hex: string;
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { ColorPickerType } from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import {
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -1,10 +1,7 @@
import { ExcalidrawElement } from "../../element/types";
import type { ExcalidrawElement } from "../../element/types";
import { atom } from "jotai";
import {
ColorPickerColor,
ColorPaletteCustom,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
} from "../../colors";
import type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
export const getColorNameAndShadeFromColor = ({
palette,
@@ -1,14 +1,13 @@
import { KEYS } from "../../keys";
import {
import type {
ColorPickerColor,
ColorPalette,
ColorPaletteCustom,
COLORS_PER_ROW,
COLOR_PALETTE,
} from "../../colors";
import { ValueOf } from "../../utility-types";
import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
import type { ValueOf } from "../../utility-types";
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
import {
ActiveColorPickerSectionAtomType,
colorPickerHotkeyBindings,
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
@@ -10,12 +10,11 @@ import { Dialog } from "../Dialog";
import { TextField } from "../TextField";
import clsx from "clsx";
import { getSelectedElements } from "../../scene";
import { Action } from "../../actions/types";
import { TranslationKeys, t } from "../../i18n";
import {
ShortcutName,
getShortcutFromShortcutName,
} from "../../actions/shortcuts";
import type { Action } from "../../actions/types";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
import type { ShortcutName } from "../../actions/shortcuts";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
import {
LockedIcon,
@@ -31,7 +30,7 @@ import {
} from "../icons";
import fuzzy from "fuzzy";
import { useUIAppState } from "../../context/ui-appState";
import { AppProps, AppState, UIAppState } from "../../types";
import type { AppProps, AppState, UIAppState } from "../../types";
import {
capitalizeString,
getShortcutKey,
@@ -39,7 +38,7 @@ import {
} from "../../utils";
import { atom, useAtom } from "jotai";
import { deburr } from "../../deburr";
import { MarkRequired } from "../../utility-types";
import type { MarkRequired } from "../../utility-types";
import { InlineIcon } from "../InlineIcon";
import { SHAPES } from "../../shapes";
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
@@ -47,7 +46,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { actionClearCanvas, actionLink } from "../../actions";
import { jotaiStore } from "../../jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { CommandPaletteItem } from "./types";
import type { CommandPaletteItem } from "./types";
import * as defaultItems from "./defaultCommandPaletteItems";
import { trackEvent } from "../../analytics";
import { useStable } from "../../hooks/useStable";
@@ -258,10 +257,10 @@ function CommandPaletteInner({
actionManager.actions.deleteSelectedElements,
actionManager.actions.copyStyles,
actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,
actionManager.actions.bringForward,
actionManager.actions.sendBackward,
actionManager.actions.sendToBack,
actionManager.actions.bringForward,
actionManager.actions.bringToFront,
actionManager.actions.alignTop,
actionManager.actions.alignBottom,
actionManager.actions.alignLeft,
@@ -541,7 +540,7 @@ function CommandPaletteInner({
...command,
icon: command.icon || boltIcon,
order: command.order ?? getCategoryOrder(command.category),
haystack: `${deburr(command.label)} ${
haystack: `${deburr(command.label.toLocaleLowerCase())} ${
command.keywords?.join(" ") || ""
}`,
};
@@ -778,7 +777,9 @@ function CommandPaletteInner({
return;
}
const _query = deburr(commandSearch.replace(/[<>-_| ]/g, ""));
const _query = deburr(
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, ""),
);
matchingCommands = fuzzy
.filter(_query, matchingCommands, {
extract: (command) => command.haystack,
@@ -1,5 +1,5 @@
import { actionToggleTheme } from "../../actions";
import { CommandPaletteItem } from "./types";
import type { CommandPaletteItem } from "./types";
export const toggleTheme: CommandPaletteItem = {
...actionToggleTheme,
@@ -1,6 +1,6 @@
import { ActionManager } from "../../actions/manager";
import { Action } from "../../actions/types";
import { UIAppState } from "../../types";
import type { ActionManager } from "../../actions/manager";
import type { Action } from "../../actions/types";
import type { UIAppState } from "../../types";
export type CommandPaletteItem = {
label: string;
@@ -1,5 +1,6 @@
import { t } from "../i18n";
import { Dialog, DialogProps } from "./Dialog";
import type { DialogProps } from "./Dialog";
import { Dialog } from "./Dialog";
import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton";
@@ -1,14 +1,13 @@
import clsx from "clsx";
import { Popover } from "./Popover";
import { t, TranslationKeys } from "../i18n";
import type { TranslationKeys } from "../i18n";
import { t } from "../i18n";
import "./ContextMenu.scss";
import {
getShortcutFromShortcutName,
ShortcutName,
} from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import type { ShortcutName } from "../actions/shortcuts";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import type { Action } from "../actions/types";
import type { ActionManager } from "../actions/manager";
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
import React from "react";
@@ -106,6 +105,7 @@ export const ContextMenu = React.memo(
}}
>
<button
type="button"
className={clsx("context-menu-item", {
dangerous: actionName === "deleteSelectedElements",
checkmark: item.checked?.(appState),
@@ -3,7 +3,7 @@ import "./ToolIcon.scss";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import { THEME } from "../constants";
import { Theme } from "../element/types";
import type { Theme } from "../element/types";
// We chose to use only explicit toggle and not a third option for system value,
// but this could be added in the future.
@@ -3,12 +3,12 @@ import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n";
import { MarkOptional, Merge } from "../utility-types";
import type { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu";
import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback(
@@ -123,6 +123,7 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")}
type="button"
>
{CloseIcon}
</button>
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { ReactNode } from "react";
import type { ReactNode } from "react";
import "./DialogActionButton.scss";
import Spinner from "./Spinner";
@@ -12,8 +12,8 @@ import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import { useStable } from "../hooks/useStable";
import "./EyeDropper.scss";
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import { ExcalidrawElement } from "../element/types";
import type { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import type { ExcalidrawElement } from "../element/types";
export type EyeDropperProperties = {
keepOpenOnAlt: boolean;
@@ -1,4 +1,4 @@
import { UserToFollow } from "../../types";
import type { UserToFollow } from "../../types";
import { CloseIcon } from "../icons";
import "./FollowMode.scss";
@@ -27,7 +27,11 @@ const FollowMode = ({
{userToFollow.username}
</span>
</div>
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
<button
type="button"
onClick={onDisconnect}
className="follow-mode__disconnect-btn"
>
{CloseIcon}
</button>
</div>
@@ -1,5 +1,5 @@
import { t } from "../i18n";
import { AppClassProperties, Device, UIAppState } from "../types";
import type { AppClassProperties, Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@@ -108,6 +108,7 @@ function Picker<T>({
<div className="picker-content" ref={rGallery}>
{options.map((option, i) => (
<button
type="button"
className={clsx("picker-option", {
active: value === option.value,
})}
@@ -171,6 +172,7 @@ export function IconPicker<T>({
<div>
<button
name={group}
type="button"
className={isActive ? "active" : ""}
aria-label={label}
onClick={() => setActive(!isActive)}
@@ -20,7 +20,7 @@ import {
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../../utils/export";
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from "react";
import { LoadingMessage } from "./LoadingMessage";
import { defaultLang, Language, languages, setLanguage } from "../i18n";
import { Theme } from "../element/types";
import type { Language } from "../i18n";
import { defaultLang, languages, setLanguage } from "../i18n";
import type { Theme } from "../element/types";
interface Props {
langCode: Language["code"];
@@ -1,8 +1,8 @@
import React from "react";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import type { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton";
@@ -12,7 +12,7 @@ import { Card } from "./Card";
import "./ExportDialog.scss";
import { nativeFileSystemSupported } from "../data/filesystem";
import { trackEvent } from "../analytics";
import { ActionManager } from "../actions/manager";
import type { ActionManager } from "../actions/manager";
import { getFrame } from "../utils";
export type ExportCB = (
@@ -1,7 +1,7 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import { ToolButtonSize } from "./ToolButton";
import type { ToolButtonSize } from "./ToolButton";
import { laserPointerToolIcon } from "./icons";
type LaserPointerIconProps = {
+7 -18
View File
@@ -1,6 +1,6 @@
import clsx from "clsx";
import React from "react";
import { ActionManager } from "../actions/manager";
import type { ActionManager } from "../actions/manager";
import {
CLASSES,
DEFAULT_SIDEBAR,
@@ -8,10 +8,11 @@ import {
TOOL_TYPE,
} from "../constants";
import { showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import type { NonDeletedExcalidrawElement } from "../element/types";
import type { Language } from "../i18n";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import {
import type {
AppProps,
AppState,
ExcalidrawProps,
@@ -38,8 +39,6 @@ import { JSONExportDialog } from "./JSONExportDialog";
import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai";
@@ -443,7 +442,7 @@ const LayerUI = ({
);
ShapeCache.delete(element);
}
Scene.getScene(selectedElements[0])?.informMutation();
Scene.getScene(selectedElements[0])?.triggerUpdate();
} else if (colorPickerType === "elementBackground") {
setAppState({
currentItemBackgroundColor: color,
@@ -541,19 +540,9 @@ const LayerUI = ({
showExitZenModeBtn={showExitZenModeBtn}
renderWelcomeScreen={renderWelcomeScreen}
/>
{appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
{appState.scrolledOutside && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
@@ -1,11 +1,12 @@
import React, { useState, useCallback, useMemo, useRef } from "react";
import Library, {
import type Library from "../data/library";
import {
distributeLibraryItemsOnSquareGrid,
libraryItemsAtom,
} from "../data/library";
import { t } from "../i18n";
import { randomId } from "../random";
import {
import type {
LibraryItems,
LibraryItem,
ExcalidrawProps,
@@ -28,7 +29,7 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants";
export const isLibraryMenuOpenAtom = atom(false);
@@ -1,6 +1,6 @@
import { VERSIONS } from "../constants";
import { t } from "../i18n";
import { ExcalidrawProps, UIAppState } from "../types";
import type { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({
theme,
@@ -1,4 +1,4 @@
import { ExcalidrawProps, UIAppState } from "../types";
import type { ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx";
@@ -2,10 +2,11 @@ import { useCallback, useState } from "react";
import { t } from "../i18n";
import Trans from "./Trans";
import { jotaiScope } from "../jotai";
import { LibraryItem, LibraryItems, UIAppState } from "../types";
import type { LibraryItem, LibraryItems, UIAppState } from "../types";
import { useApp, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library";
import type Library from "../data/library";
import { libraryItemsAtom } from "../data/library";
import {
DotsIcon,
ExportIcon,
@@ -7,7 +7,7 @@ import React, {
} from "react";
import { serializeLibraryAsJSON } from "../data/json";
import { t } from "../i18n";
import {
import type {
ExcalidrawProps,
LibraryItem,
LibraryItems,
@@ -1,8 +1,9 @@
import React, { memo, ReactNode, useEffect, useState } from "react";
import type { ReactNode } from "react";
import React, { memo, useEffect, useState } from "react";
import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
import { LibraryItem } from "../types";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { SvgCache } from "../hooks/useLibraryItemSvg";
import type { LibraryItem } from "../types";
import type { ExcalidrawElement, NonDeleted } from "../element/types";
import type { SvgCache } from "../hooks/useLibraryItemSvg";
import { useTransition } from "../hooks/useTransition";
type LibraryOrPendingItem = (
@@ -1,11 +1,12 @@
import clsx from "clsx";
import { memo, useEffect, useRef, useState } from "react";
import { useDevice } from "./App";
import { LibraryItem } from "../types";
import type { LibraryItem } from "../types";
import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem";
import { PlusIcon } from "./icons";
import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
import type { SvgCache } from "../hooks/useLibraryItemSvg";
import { useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
export const LibraryUnit = memo(
({

Some files were not shown because too many files have changed in this diff Show More