Compare commits

..

1 Commits

Author SHA1 Message Date
dwelle 027ef1d641 build: allow node v22 2024-05-07 12:58:27 +02:00
308 changed files with 3027 additions and 8218 deletions
-7
View File
@@ -4,15 +4,8 @@
!.eslintrc.json !.eslintrc.json
!.npmrc !.npmrc
!.prettierrc !.prettierrc
!excalidraw-app/
!package.json !package.json
!public/ !public/
!packages/ !packages/
!tsconfig.json !tsconfig.json
!yarn.lock !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
+1 -1
View File
@@ -22,7 +22,7 @@ VITE_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when # whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers. # debugging Service Workers.
VITE_APP_DEV_DISABLE_LIVE_RELOAD= VITE_APP_DEV_DISABLE_LIVE_RELOAD=
VITE_APP_ENABLE_TRACKING=true VITE_APP_DISABLE_TRACKING=true
FAST_REFRESH=false FAST_REFRESH=false
+1 -1
View File
@@ -14,4 +14,4 @@ VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
VITE_APP_ENABLE_TRACKING=false VITE_APP_DISABLE_TRACKING=
+1 -2
View File
@@ -2,7 +2,6 @@
"extends": ["@excalidraw/eslint-config", "react-app"], "extends": ["@excalidraw/eslint-config", "react-app"],
"rules": { "rules": {
"import/no-anonymous-default-export": "off", "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" }]
} }
} }
+3 -6
View File
@@ -1,17 +1,14 @@
name: Tests name: Tests
on: on: pull_request
pull_request:
push:
branches: master
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Setup Node.js 18.x - name: Setup Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v2
with: with:
node-version: 18.x node-version: 18.x
- name: Install and test - name: Install and test
+5 -7
View File
@@ -2,18 +2,16 @@ FROM node:18 AS build
WORKDIR /opt/node_app WORKDIR /opt/node_app
COPY . . COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000
# do not ignore optional dependencies:
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
RUN yarn --network-timeout 600000
ARG NODE_ENV=production ARG NODE_ENV=production
COPY . .
RUN yarn build:app:docker RUN yarn build:app:docker
FROM nginx:1.24-alpine FROM nginx:1.21-alpine
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html COPY --from=build /opt/node_app/build /usr/share/nginx/html
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1 HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
@@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
```jsx showLineNumbers ```jsx showLineNumbers
export default function App() { export default function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null); const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />; return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
} }
``` ```
@@ -90,7 +90,7 @@ function App() {
<img src={canvasUrl} alt="" /> <img src={canvasUrl} alt="" />
</div> </div>
<div style={{ height: "400px" }}> <div style={{ height: "400px" }}>
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} <Excalidraw ref={(api) => setExcalidrawAPI(api)}
/> />
</div> </div>
</> </>
+1 -1
View File
@@ -12,9 +12,9 @@ import type * as TExcalidraw from "@excalidraw/excalidraw";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { ResolvablePromise } from "../utils";
import { import {
resolvablePromise, resolvablePromise,
ResolvablePromise,
distance2d, distance2d,
fileOpen, fileOpen,
withBatchedUpdates, withBatchedUpdates,
@@ -1,4 +1,4 @@
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import CustomFooter from "./CustomFooter"; import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw"; import type * as TExcalidraw from "@excalidraw/excalidraw";
+1 -1
View File
@@ -1,6 +1,6 @@
import { unstable_batchedUpdates } from "react-dom"; import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access"; import { fileOpen as _fileOpen } from "browser-fs-access";
import { MIME_TYPES } from "@excalidraw/excalidraw"; import type { MIME_TYPES } from "@excalidraw/excalidraw";
import { AbortError } from "../../packages/excalidraw/errors"; import { AbortError } from "../../packages/excalidraw/errors";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+33 -60
View File
@@ -1,4 +1,5 @@
import polyfill from "../packages/excalidraw/polyfill"; import polyfill from "../packages/excalidraw/polyfill";
import LanguageDetector from "i18next-browser-languagedetector";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { trackEvent } from "../packages/excalidraw/analytics"; import { trackEvent } from "../packages/excalidraw/analytics";
import { getDefaultAppState } from "../packages/excalidraw/appState"; import { getDefaultAppState } from "../packages/excalidraw/appState";
@@ -12,7 +13,7 @@ import {
VERSION_TIMEOUT, VERSION_TIMEOUT,
} from "../packages/excalidraw/constants"; } from "../packages/excalidraw/constants";
import { loadFromBlob } from "../packages/excalidraw/data/blob"; import { loadFromBlob } from "../packages/excalidraw/data/blob";
import type { import {
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
@@ -21,26 +22,27 @@ import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRef
import { t } from "../packages/excalidraw/i18n"; import { t } from "../packages/excalidraw/i18n";
import { import {
Excalidraw, Excalidraw,
defaultLang,
LiveCollaborationTrigger, LiveCollaborationTrigger,
TTDDialog, TTDDialog,
TTDDialogTrigger, TTDDialogTrigger,
StoreAction, StoreAction,
reconcileElements, reconcileElements,
} from "../packages/excalidraw"; } from "../packages/excalidraw";
import type { import {
AppState, AppState,
ExcalidrawImperativeAPI, ExcalidrawImperativeAPI,
BinaryFiles, BinaryFiles,
ExcalidrawInitialDataState, ExcalidrawInitialDataState,
UIAppState, UIAppState,
} from "../packages/excalidraw/types"; } from "../packages/excalidraw/types";
import type { ResolvablePromise } from "../packages/excalidraw/utils";
import { import {
debounce, debounce,
getVersion, getVersion,
getFrame, getFrame,
isTestEnv, isTestEnv,
preventUnload, preventUnload,
ResolvablePromise,
resolvablePromise, resolvablePromise,
isRunningInIframe, isRunningInIframe,
} from "../packages/excalidraw/utils"; } from "../packages/excalidraw/utils";
@@ -50,8 +52,8 @@ import {
STORAGE_KEYS, STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT, SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants"; } from "./app_constants";
import type { CollabAPI } from "./collab/Collab";
import Collab, { import Collab, {
CollabAPI,
collabAPIAtom, collabAPIAtom,
isCollaboratingAtom, isCollaboratingAtom,
isOfflineAtom, isOfflineAtom,
@@ -67,8 +69,11 @@ import {
importUsernameFromLocalStorage, importUsernameFromLocalStorage,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import type { RestoredDataState } from "../packages/excalidraw/data/restore"; import {
import { restore, restoreAppState } from "../packages/excalidraw/data/restore"; restore,
restoreAppState,
RestoredDataState,
} from "../packages/excalidraw/data/restore";
import { import {
ExportToExcalidrawPlus, ExportToExcalidrawPlus,
exportToExcalidrawPlus, exportToExcalidrawPlus,
@@ -91,12 +96,12 @@ import {
import { AppMainMenu } from "./components/AppMainMenu"; import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter"; import { AppFooter } from "./components/AppFooter";
import { Provider, useAtom, useAtomValue } from "jotai"; import { atom, Provider, useAtom, useAtomValue } from "jotai";
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai"; import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
import { appJotaiStore } from "./app-jotai"; import { appJotaiStore } from "./app-jotai";
import "./index.scss"; import "./index.scss";
import type { ResolutionType } from "../packages/excalidraw/utility-types"; import { ResolutionType } from "../packages/excalidraw/utility-types";
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog"; import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState"; import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm"; import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
@@ -119,45 +124,11 @@ import {
youtubeIcon, youtubeIcon,
} from "../packages/excalidraw/components/icons"; } from "../packages/excalidraw/components/icons";
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme"; import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
import { getPreferredLanguage } from "./app-language/language-detector";
import { useAppLangCode } from "./app-language/language-state";
polyfill(); polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true; 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; let isSelfEmbedding = false;
if (window.self !== window.top) { if (window.self !== window.top) {
@@ -172,6 +143,11 @@ if (window.self !== window.top) {
} }
} }
const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {},
});
const shareableLinkConfirmDialog = { const shareableLinkConfirmDialog = {
title: t("overwriteConfirm.modal.shareableLink.title"), title: t("overwriteConfirm.modal.shareableLink.title"),
description: ( description: (
@@ -317,15 +293,19 @@ const initializeScene = async (opts: {
return { scene: null, isExternalScene: false }; return { scene: null, isExternalScene: false };
}; };
const detectedLangCode = languageDetector.detect() || defaultLang.code;
export const appLangCodeAtom = atom(
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
);
const ExcalidrawWrapper = () => { const ExcalidrawWrapper = () => {
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
const isCollabDisabled = isRunningInIframe(); const isCollabDisabled = isRunningInIframe();
const [appTheme, setAppTheme] = useAtom(appThemeAtom); const [appTheme, setAppTheme] = useAtom(appThemeAtom);
const { editorTheme } = useHandleAppTheme(); const { editorTheme } = useHandleAppTheme();
const [langCode, setLangCode] = useAppLangCode();
// initial state // initial state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -481,7 +461,11 @@ const ExcalidrawWrapper = () => {
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage(); const localDataState = importFromLocalStorage();
const username = importUsernameFromLocalStorage(); const username = importUsernameFromLocalStorage();
setLangCode(getPreferredLanguage()); let langCode = languageDetector.detect() || defaultLang.code;
if (Array.isArray(langCode)) {
langCode = langCode[0];
}
setLangCode(langCode);
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...localDataState, ...localDataState,
storeAction: StoreAction.UPDATE, storeAction: StoreAction.UPDATE,
@@ -582,6 +566,10 @@ const ExcalidrawWrapper = () => {
}; };
}, [excalidrawAPI]); }, [excalidrawAPI]);
useEffect(() => {
languageDetector.cacheUserLanguage(langCode);
}, [langCode]);
const onChange = ( const onChange = (
elements: readonly OrderedExcalidrawElement[], elements: readonly OrderedExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -1115,21 +1103,6 @@ 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> </Excalidraw>
+2 -2
View File
@@ -7,8 +7,8 @@ import {
import { DEFAULT_VERSION } from "../packages/excalidraw/constants"; import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
import { t } from "../packages/excalidraw/i18n"; import { t } from "../packages/excalidraw/i18n";
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard"; import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types"; import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import type { UIAppState } from "../packages/excalidraw/types"; import { UIAppState } from "../packages/excalidraw/types";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };
@@ -1,25 +0,0 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { defaultLang, languages } from "../../packages/excalidraw";
export const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {},
});
export const getPreferredLanguage = () => {
const detectedLanguages = languageDetector.detect();
const detectedLanguage = Array.isArray(detectedLanguages)
? detectedLanguages[0]
: detectedLanguages;
const initialLanguage =
(detectedLanguage
? // region code may not be defined if user uses generic preferred language
// (e.g. chinese vs instead of chienese-simplified)
languages.find((lang) => lang.code.startsWith(detectedLanguage))?.code
: null) || defaultLang.code;
return initialLanguage;
};
@@ -1,15 +0,0 @@
import { atom, useAtom } from "jotai";
import { useEffect } from "react";
import { getPreferredLanguage, languageDetector } from "./language-detector";
export const appLangCodeAtom = atom(getPreferredLanguage());
export const useAppLangCode = () => {
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
useEffect(() => {
languageDetector.cacheUserLanguage(langCode);
}, [langCode]);
return [langCode, setLangCode] as const;
};
+7 -9
View File
@@ -1,13 +1,13 @@
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { PureComponent } from "react"; import { PureComponent } from "react";
import type { import {
ExcalidrawImperativeAPI, ExcalidrawImperativeAPI,
SocketId, SocketId,
} from "../../packages/excalidraw/types"; } from "../../packages/excalidraw/types";
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog"; import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants"; import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
import type { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { import {
ExcalidrawElement, ExcalidrawElement,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
@@ -19,7 +19,7 @@ import {
zoomToFitBounds, zoomToFitBounds,
reconcileElements, reconcileElements,
} from "../../packages/excalidraw"; } from "../../packages/excalidraw";
import type { Collaborator, Gesture } from "../../packages/excalidraw/types"; import { Collaborator, Gesture } from "../../packages/excalidraw/types";
import { import {
assertNever, assertNever,
preventUnload, preventUnload,
@@ -36,14 +36,12 @@ import {
SYNC_FULL_SCENE_INTERVAL_MS, SYNC_FULL_SCENE_INTERVAL_MS,
WS_EVENTS, WS_EVENTS,
} from "../app_constants"; } from "../app_constants";
import type {
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import { import {
generateCollaborationLinkData, generateCollaborationLinkData,
getCollaborationLink, getCollaborationLink,
getSyncableElements, getSyncableElements,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data"; } from "../data";
import { import {
isSavedToFirebase, isSavedToFirebase,
@@ -79,7 +77,7 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData"; import { LocalData } from "../data/LocalData";
import { atom } from "jotai"; import { atom } from "jotai";
import { appJotaiStore } from "../app-jotai"; import { appJotaiStore } from "../app-jotai";
import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
import { collabErrorIndicatorAtom } from "./CollabError"; import { collabErrorIndicatorAtom } from "./CollabError";
+5 -5
View File
@@ -1,15 +1,15 @@
import type { import {
isSyncableElement,
SocketUpdateData, SocketUpdateData,
SocketUpdateDataSource, SocketUpdateDataSource,
SyncableExcalidrawElement, SyncableExcalidrawElement,
} from "../data"; } from "../data";
import { isSyncableElement } from "../data";
import type { TCollabClass } from "./Collab"; import { TCollabClass } from "./Collab";
import type { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types"; import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import type { import {
OnUserFollowedPayload, OnUserFollowedPayload,
SocketId, SocketId,
UserIdleState, UserIdleState,
+5 -5
View File
@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import { import {
loginIcon, arrowBarToLeftIcon,
ExcalLogo, ExcalLogo,
} from "../../packages/excalidraw/components/icons"; } from "../../packages/excalidraw/components/icons";
import type { Theme } from "../../packages/excalidraw/element/types"; import { Theme } from "../../packages/excalidraw/element/types";
import { MainMenu } from "../../packages/excalidraw/index"; import { MainMenu } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import { isExcalidrawPlusSignedUser } from "../app_constants";
import { LanguageList } from "../app-language/LanguageList"; import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{ export const AppMainMenu: React.FC<{
onCollabDialogOpen: () => any; onCollabDialogOpen: () => any;
@@ -34,7 +34,7 @@ export const AppMainMenu: React.FC<{
<MainMenu.ItemLink <MainMenu.ItemLink
icon={ExcalLogo} icon={ExcalLogo}
href={`${ href={`${
import.meta.env.VITE_APP_PLUS_LP import.meta.env.VITE_APP_PLUS_APP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`} }/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
className="" className=""
> >
@@ -42,7 +42,7 @@ export const AppMainMenu: React.FC<{
</MainMenu.ItemLink> </MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials /> <MainMenu.DefaultItems.Socials />
<MainMenu.ItemLink <MainMenu.ItemLink
icon={loginIcon} icon={arrowBarToLeftIcon}
href={`${import.meta.env.VITE_APP_PLUS_APP}${ href={`${import.meta.env.VITE_APP_PLUS_APP}${
isExcalidrawPlusSignedUser ? "" : "/sign-up" isExcalidrawPlusSignedUser ? "" : "/sign-up"
}?utm_source=signin&utm_medium=app&utm_content=hamburger`} }?utm_source=signin&utm_medium=app&utm_content=hamburger`}
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { loginIcon } from "../../packages/excalidraw/components/icons"; import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
import { useI18n } from "../../packages/excalidraw/i18n"; import { useI18n } from "../../packages/excalidraw/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index"; import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import { isExcalidrawPlusSignedUser } from "../app_constants";
@@ -61,7 +61,7 @@ export const AppWelcomeScreen: React.FC<{
import.meta.env.VITE_APP_PLUS_LP import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`} }/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null} shortcut={null}
icon={loginIcon} icon={arrowBarToLeftIcon}
> >
Sign up Sign up
</WelcomeScreen.Center.MenuItemLink> </WelcomeScreen.Center.MenuItemLink>
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
import { ToolButton } from "../../packages/excalidraw/components/ToolButton"; import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
import { serializeAsJSON } from "../../packages/excalidraw/data/json"; import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import type { import {
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import type { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
+1 -1
View File
@@ -1,7 +1,7 @@
import oc from "open-color"; import oc from "open-color";
import React from "react"; import React from "react";
import { THEME } from "../../packages/excalidraw/constants"; import { THEME } from "../../packages/excalidraw/constants";
import type { Theme } from "../../packages/excalidraw/element/types"; import { Theme } from "../../packages/excalidraw/element/types";
// https://github.com/tholman/github-corners // https://github.com/tholman/github-corners
export const GitHubCorner = React.memo( export const GitHubCorner = React.memo(
@@ -1,7 +1,8 @@
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import React from "react"; import React from "react";
import { useI18n, languages } from "../../packages/excalidraw/i18n"; import { appLangCodeAtom } from "../App";
import { appLangCodeAtom } from "./language-state"; import { useI18n } from "../../packages/excalidraw/i18n";
import { languages } from "../../packages/excalidraw/i18n";
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
const { t, langCode } = useI18n(); const { t, langCode } = useI18n();
+2 -2
View File
@@ -2,14 +2,14 @@ import { StoreAction } from "../../packages/excalidraw";
import { compressData } from "../../packages/excalidraw/data/encode"; import { compressData } from "../../packages/excalidraw/data/encode";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import type { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
FileId, FileId,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n"; import { t } from "../../packages/excalidraw/i18n";
import type { import {
BinaryFileData, BinaryFileData,
BinaryFileMetadata, BinaryFileMetadata,
ExcalidrawImperativeAPI, ExcalidrawImperativeAPI,
+5 -5
View File
@@ -20,19 +20,19 @@ import {
get, get,
} from "idb-keyval"; } from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library"; import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import type { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import type { import {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import type { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
} from "../../packages/excalidraw/types"; } from "../../packages/excalidraw/types";
import type { MaybePromise } from "../../packages/excalidraw/utility-types"; import { MaybePromise } from "../../packages/excalidraw/utility-types";
import { debounce } from "../../packages/excalidraw/utils"; import { debounce } from "../../packages/excalidraw/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager"; import { FileManager } from "./FileManager";
+5 -6
View File
@@ -1,13 +1,13 @@
import { reconcileElements } from "../../packages/excalidraw"; import { reconcileElements } from "../../packages/excalidraw";
import type { import {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import { getSceneVersion } from "../../packages/excalidraw/element"; import { getSceneVersion } from "../../packages/excalidraw/element";
import type Portal from "../collab/Portal"; import Portal from "../collab/Portal";
import { restoreElements } from "../../packages/excalidraw/data/restore"; import { restoreElements } from "../../packages/excalidraw/data/restore";
import type { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFileMetadata, BinaryFileMetadata,
@@ -20,9 +20,8 @@ import {
decryptData, decryptData,
} from "../../packages/excalidraw/data/encryption"; } from "../../packages/excalidraw/data/encryption";
import { MIME_TYPES } from "../../packages/excalidraw/constants"; import { MIME_TYPES } from "../../packages/excalidraw/constants";
import type { SyncableExcalidrawElement } from "."; import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { getSyncableElements } from "."; import { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile"; import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
+6 -6
View File
@@ -9,30 +9,30 @@ import {
} from "../../packages/excalidraw/data/encryption"; } from "../../packages/excalidraw/data/encryption";
import { serializeAsJSON } from "../../packages/excalidraw/data/json"; import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore"; import { restore } from "../../packages/excalidraw/data/restore";
import type { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { SceneBounds } from "../../packages/excalidraw/element/bounds"; import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers"; import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import type { import {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n"; import { t } from "../../packages/excalidraw/i18n";
import type { import {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
SocketId, SocketId,
UserIdleState, UserIdleState,
} from "../../packages/excalidraw/types"; } from "../../packages/excalidraw/types";
import type { MakeBrand } from "../../packages/excalidraw/utility-types"; import { MakeBrand } from "../../packages/excalidraw/utility-types";
import { bytesToHexString } from "../../packages/excalidraw/utils"; import { bytesToHexString } from "../../packages/excalidraw/utils";
import type { WS_SUBTYPES } from "../app_constants";
import { import {
DELETED_ELEMENT_TIMEOUT, DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES, ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants"; } from "../app_constants";
import { encodeFilesForUpload } from "./FileManager"; import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase"; import { saveFilesToFirebase } from "./firebase";
+2 -2
View File
@@ -1,5 +1,5 @@
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types"; import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { AppState } from "../../packages/excalidraw/types"; import { AppState } from "../../packages/excalidraw/types";
import { import {
clearAppStateForLocalStorage, clearAppStateForLocalStorage,
getDefaultAppState, getDefaultAppState,
+3 -3
View File
@@ -20,7 +20,7 @@
name="description" name="description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." 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-3.png" /> <meta name="image" content="https://excalidraw.com/og-image-2.png" />
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:site_name" content="Excalidraw" /> <meta property="og:site_name" content="Excalidraw" />
@@ -35,7 +35,7 @@
property="og:description" 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." 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-3.png" /> <meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
@@ -51,7 +51,7 @@
/> />
<meta <meta
property="twitter:image" property="twitter:image"
content="https://excalidraw.com/og-image-3.png" content="https://excalidraw.com/og-twitter-v2.png"
/> />
<!-- General tags --> <!-- General tags -->
-5
View File
@@ -25,7 +25,6 @@
margin-bottom: auto; margin-bottom: auto;
margin-inline-start: auto; margin-inline-start: auto;
margin-inline-end: 0.6em; margin-inline-end: 0.6em;
z-index: var(--zIndex-layerUI);
svg { svg {
width: 1.2rem; width: 1.2rem;
@@ -41,10 +40,6 @@
} }
&.highlighted { &.highlighted {
color: var(--color-promo); color: var(--color-promo);
font-weight: 700;
.dropdown-menu-item__icon g {
stroke-width: 2;
}
} }
} }
} }
+2 -2
View File
@@ -31,8 +31,8 @@
"prettier": "@excalidraw/prettier-config", "prettier": "@excalidraw/prettier-config",
"scripts": { "scripts": {
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build", "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 cross-env VITE_APP_ENABLE_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:version": "node ../scripts/build-version.js",
"build": "yarn build:app && yarn build:version", "build": "yarn build:app && yarn build:version",
"start": "yarn && vite", "start": "yarn && vite",
+1 -2
View File
@@ -18,8 +18,7 @@ import {
} from "../../packages/excalidraw/components/icons"; } from "../../packages/excalidraw/components/icons";
import { TextField } from "../../packages/excalidraw/components/TextField"; import { TextField } from "../../packages/excalidraw/components/TextField";
import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
import type { CollabAPI } from "../collab/Collab"; import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
import { activeRoomLinkAtom } from "../collab/Collab";
import { atom, useAtom, useAtomValue } from "jotai"; import { atom, useAtom, useAtomValue } from "jotai";
import "./ShareDialog.scss"; import "./ShareDialog.scss";
@@ -216,22 +216,23 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
stroke-width="2" stroke-width="2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g <g>
stroke-width="1.5"
>
<path <path
d="M0 0h24v24H0z" d="M0 0h24v24H0z"
fill="none" fill="none"
stroke="none" stroke="none"
/> />
<path <path
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" d="M10 12l10 0"
/> />
<path <path
d="M21 12h-13l3 -3" d="M10 12l4 4"
/> />
<path <path
d="M11 15l-3 -3" d="M10 12l4 -4"
/>
<path
d="M4 4l0 16"
/> />
</g> </g>
</svg> </svg>
+1 -1
View File
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
import { useEffect, useLayoutEffect, useState } from "react"; import { useEffect, useLayoutEffect, useState } from "react";
import { THEME } from "../packages/excalidraw"; import { THEME } from "../packages/excalidraw";
import { EVENT } from "../packages/excalidraw/constants"; import { EVENT } from "../packages/excalidraw/constants";
import type { Theme } from "../packages/excalidraw/element/types"; import { Theme } from "../packages/excalidraw/element/types";
import { CODES, KEYS } from "../packages/excalidraw/keys"; import { CODES, KEYS } from "../packages/excalidraw/keys";
import { STORAGE_KEYS } from "./app_constants"; import { STORAGE_KEYS } from "./app_constants";
+1 -6
View File
@@ -64,12 +64,7 @@ export default defineConfig({
workbox: { workbox: {
// Don't push fonts and locales to app precache // Don't push fonts and locales to app precache
globIgnores: [ globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
"fonts.css",
"**/locales/**",
"service-worker.js",
"lz-string",
],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"), urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
+4 -5
View File
@@ -1,7 +1,6 @@
{ {
"private": true, "private": true,
"name": "excalidraw-monorepo", "name": "excalidraw-monorepo",
"packageManager": "yarn@1.22.22",
"workspaces": [ "workspaces": [
"excalidraw-app", "excalidraw-app",
"packages/excalidraw", "packages/excalidraw",
@@ -55,15 +54,15 @@
"vitest-canvas-mock": "0.3.2" "vitest-canvas-mock": "0.3.2"
}, },
"engines": { "engines": {
"node": "18.0.0 - 20.x.x" "node": "18.0.0 - 22.x.x"
}, },
"homepage": ".", "homepage": ".",
"prettier": "@excalidraw/prettier-config", "prettier": "@excalidraw/prettier-config",
"scripts": { "scripts": {
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker", "build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "yarn --cwd ./excalidraw-app build:app", "build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "yarn --cwd ./excalidraw-app build:version", "build:version": "node ./scripts/build-version.js",
"build": "yarn --cwd ./excalidraw-app build", "build": "yarn --cwd ./excalidraw-app build",
"fix:code": "yarn test:code --fix", "fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
-2
View File
@@ -15,8 +15,6 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348) - Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853) - `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
+3 -4
View File
@@ -1,5 +1,4 @@
import type { Alignment } from "../align"; import { alignElements, Alignment } from "../align";
import { alignElements } from "../align";
import { import {
AlignBottomIcon, AlignBottomIcon,
AlignLeftIcon, AlignLeftIcon,
@@ -11,13 +10,13 @@ import {
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks"; import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import type { AppClassProperties, AppState, UIAppState } from "../types"; import { AppClassProperties, AppState, UIAppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
@@ -1,8 +1,8 @@
import { import {
BOUND_TEXT_PADDING, BOUND_TEXT_PADDING,
ROUNDNESS, ROUNDNESS,
TEXT_ALIGN,
VERTICAL_ALIGN, VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants"; } from "../constants";
import { isTextElement, newElement } from "../element"; import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
@@ -23,14 +23,14 @@ import {
isTextBindableContainer, isTextBindableContainer,
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
} from "../element/typeChecks"; } from "../element/typeChecks";
import type { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import type { AppState } from "../types"; import { AppState } from "../types";
import type { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { arrayToMap, getFontString } from "../utils"; import { arrayToMap, getFontString } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex"; import { syncMovedIndices } from "../fractionalIndex";
@@ -142,7 +142,6 @@ export const actionBindText = register({
containerId: container.id, containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER, textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
}); });
mutateElement(container, { mutateElement(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
@@ -297,7 +296,6 @@ export const actionWrapTextInContainer = register({
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null, boundElements: null,
textAlign: TEXT_ALIGN.CENTER, textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
}, },
false, false,
); );
+4 -4
View File
@@ -18,13 +18,13 @@ import {
ZOOM_STEP, ZOOM_STEP,
} from "../constants"; } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element"; import { getCommonBounds, getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene"; import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import type { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils"; import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
@@ -35,7 +35,7 @@ import {
isHandToolActive, isHandToolActive,
} from "../appState"; } from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import type { SceneBounds } from "../element/bounds"; import { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor"; import { setCursor } from "../cursor";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -104,7 +104,7 @@ export const actionClearCanvas = register({
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene, exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize, gridSize: appState.gridSize,
stats: appState.stats, showStats: appState.showStats,
pasteDialog: appState.pasteDialog, pasteDialog: appState.pasteDialog,
activeTool: activeTool:
appState.activeTool.type === "image" appState.activeTool.type === "image"
@@ -4,8 +4,8 @@ import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import { register } from "./register"; import { register } from "./register";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import type { AppState } from "../types"; import { AppState } from "../types";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups"; import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@@ -3,17 +3,16 @@ import {
DistributeVerticallyIcon, DistributeVerticallyIcon,
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import type { Distribution } from "../distribute"; import { distributeElements, Distribution } from "../distribute";
import { distributeElements } from "../distribute";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks"; import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import type { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
@@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element"; import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
@@ -12,9 +12,9 @@ import {
getSelectedGroupForElement, getSelectedGroupForElement,
getElementsInGroup, getElementsInGroup,
} from "../groups"; } from "../groups";
import type { AppState } from "../types"; import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding"; import { fixBindingsAfterDuplication } from "../element/binding";
import type { ActionResult } from "./types"; import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
@@ -1,7 +1,7 @@
import { LockedIcon, UnlockedIcon } from "../components/icons"; import { LockedIcon, UnlockedIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { isFrameLikeElement } from "../element/typeChecks"; import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
+1 -1
View File
@@ -16,7 +16,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import type { Theme } from "../element/types"; import { Theme } from "../element/types";
import "../components/ToolIcon.scss"; import "../components/ToolIcon.scss";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -13,7 +13,7 @@ import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
} from "../element/binding"; } from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks"; import { isBindingElement, isLinearElement } from "../element/typeChecks";
import type { AppState } from "../types"; import { AppState } from "../types";
import { resetCursor } from "../cursor"; import { resetCursor } from "../cursor";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -131,12 +131,7 @@ export const actionFinalize = register({
-1, -1,
arrayToMap(elements), arrayToMap(elements),
); );
maybeBindLinearElement( maybeBindLinearElement(multiPointElement, appState, { x, y }, app);
multiPointElement,
appState,
{ x, y },
elementsMap,
);
} }
} }
+3 -3
View File
@@ -1,13 +1,13 @@
import { register } from "./register"; import { register } from "./register";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import type { import {
ExcalidrawElement, ExcalidrawElement,
NonDeleted, NonDeleted,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
} from "../element/types"; } from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements"; import { resizeMultipleElements } from "../element/resizeElements";
import type { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds"; import { getCommonBoundingBox } from "../element/bounds";
@@ -124,7 +124,7 @@ const flipElements = (
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement), selectedElements.filter(isLinearElement),
elementsMap, app,
isBindingEnabled(appState), isBindingEnabled(appState),
[], [],
); );
+2 -2
View File
@@ -1,9 +1,9 @@
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame"; import { removeAllElementsFromFrame } from "../frame";
import { getFrameChildren } from "../frame"; import { getFrameChildren } from "../frame";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import type { AppClassProperties, AppState, UIAppState } from "../types"; import { AppClassProperties, AppState, UIAppState } from "../types";
import { updateActiveTool } from "../utils"; import { updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor"; import { setCursorForShape } from "../cursor";
import { register } from "./register"; import { register } from "./register";
+2 -2
View File
@@ -17,12 +17,12 @@ import {
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { randomId } from "../random"; import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import type { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement, ExcalidrawTextElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "../element/types"; } from "../element/types";
import type { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { import {
getElementsInResizingFrame, getElementsInResizingFrame,
+7 -17
View File
@@ -1,16 +1,14 @@
import type { Action, ActionResult } from "./types"; import { Action, ActionResult } from "./types";
import { UndoIcon, RedoIcon } from "../components/icons"; import { UndoIcon, RedoIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import type { History } from "../history"; import { History, HistoryChangedEvent } from "../history";
import { HistoryChangedEvent } from "../history"; import { AppState } from "../types";
import type { AppState } from "../types";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { isWindows } from "../constants"; import { isWindows } from "../constants";
import type { SceneElementsMap } from "../element/types"; import { SceneElementsMap } from "../element/types";
import type { Store } from "../store"; import { Store, StoreAction } from "../store";
import { StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter"; import { useEmitter } from "../hooks/useEmitter";
const writeData = ( const writeData = (
@@ -65,10 +63,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => { PanelComponent: ({ updateData, data }) => {
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>( const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
history.onHistoryChangedEmitter, history.onHistoryChangedEmitter,
new HistoryChangedEvent( new HistoryChangedEvent(),
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
); );
return ( return (
@@ -79,7 +74,6 @@ export const createUndoAction: ActionCreator = (history, store) => ({
onClick={updateData} onClick={updateData}
size={data?.size || "medium"} size={data?.size || "medium"}
disabled={isUndoStackEmpty} disabled={isUndoStackEmpty}
data-testid="button-undo"
/> />
); );
}, },
@@ -107,10 +101,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => { PanelComponent: ({ updateData, data }) => {
const { isRedoStackEmpty } = useEmitter( const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter, history.onHistoryChangedEmitter,
new HistoryChangedEvent( new HistoryChangedEvent(),
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
); );
return ( return (
@@ -121,7 +112,6 @@ export const createRedoAction: ActionCreator = (history, store) => ({
onClick={updateData} onClick={updateData}
size={data?.size || "medium"} size={data?.size || "medium"}
disabled={isRedoStackEmpty} disabled={isRedoStackEmpty}
data-testid="button-redo"
/> />
); );
}, },
@@ -1,12 +1,9 @@
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette"; import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import type { ExcalidrawLinearElement } from "../element/types"; import { ExcalidrawLinearElement } from "../element/types";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { register } from "./register"; import { register } from "./register";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { lineEditorIcon } from "../components/icons";
export const actionToggleLinearEditor = register({ export const actionToggleLinearEditor = register({
name: "toggleLinearEditor", name: "toggleLinearEditor",
@@ -14,23 +11,18 @@ export const actionToggleLinearEditor = register({
label: (elements, appState, app) => { label: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({ const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement | undefined; includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
return selectedElement?.type === "arrow" return appState.editingLinearElement?.elementId === selectedElement?.id
? "labels.lineEditor.editArrow" ? "labels.lineEditor.exit"
: "labels.lineEditor.edit"; : "labels.lineEditor.edit";
}, },
keywords: ["line"],
trackEvent: { trackEvent: {
category: "element", category: "element",
}, },
predicate: (elements, appState, _, app) => { predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
if ( if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
!appState.editingLinearElement &&
selectedElements.length === 1 &&
isLinearElement(selectedElements[0])
) {
return true; return true;
} }
return false; return false;
@@ -53,24 +45,4 @@ export const actionToggleLinearEditor = register({
storeAction: StoreAction.CAPTURE, 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 { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar"; import { Avatar } from "../components/Avatar";
import type { GoToCollaboratorComponentProps } from "../components/UserList"; import { GoToCollaboratorComponentProps } from "../components/UserList";
import { import {
eyeIcon, eyeIcon,
microphoneIcon, microphoneIcon,
@@ -8,7 +8,7 @@ import {
} from "../components/icons"; } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import type { Collaborator } from "../types"; import { Collaborator } from "../types";
import { register } from "./register"; import { register } from "./register";
import clsx from "clsx"; import clsx from "clsx";
@@ -1,4 +1,4 @@
import type { AppClassProperties, AppState, Primitive } from "../types"; import { AppClassProperties, AppState, Primitive } from "../types";
import { import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS, DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -74,7 +74,7 @@ import {
isLinearElement, isLinearElement,
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
} from "../element/typeChecks"; } from "../element/typeChecks";
import type { import {
Arrowhead, Arrowhead,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@@ -167,7 +167,7 @@ const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement, prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement, nextElement: ExcalidrawTextElement,
) => { ) => {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) { if (isBoundToContainer(nextElement)) {
return nextElement; return nextElement;
} }
return mutateElement( return mutateElement(
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups"; import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { excludeElementsInFramesFromSelection } from "../scene/selection"; import { excludeElementsInFramesFromSelection } from "../scene/selection";
+1 -1
View File
@@ -24,7 +24,7 @@ import {
isArrowElement, isArrowElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import type { ExcalidrawTextElement } from "../element/types"; import { ExcalidrawTextElement } from "../element/types";
import { paintIcon } from "../components/icons"; import { paintIcon } from "../components/icons";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -1,48 +0,0 @@
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 { CODES, KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import type { AppState } from "../types"; import { AppState } from "../types";
import { gridIcon } from "../components/icons"; import { gridIcon } from "../components/icons";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@@ -5,22 +5,21 @@ import { StoreAction } from "../store";
export const actionToggleStats = register({ export const actionToggleStats = register({
name: "stats", name: "stats",
label: "stats.fullTitle", label: "stats.title",
icon: abacusIcon, icon: abacusIcon,
paletteName: "Toggle stats", paletteName: "Toggle stats",
viewMode: true, viewMode: true,
trackEvent: { category: "menu" }, trackEvent: { category: "menu" },
keywords: ["edit", "attributes", "customize"],
perform(elements, appState) { perform(elements, appState) {
return { return {
appState: { appState: {
...appState, ...appState,
stats: { ...appState.stats, open: !this.checked!(appState) }, showStats: !this.checked!(appState),
}, },
storeAction: StoreAction.NONE, storeAction: StoreAction.NONE,
}; };
}, },
checked: (appState) => appState.stats.open, checked: (appState) => appState.showStats,
keyTest: (event) => keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH, !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
}); });
@@ -20,7 +20,6 @@ import { StoreAction } from "../store";
export const actionSendBackward = register({ export const actionSendBackward = register({
name: "sendBackward", name: "sendBackward",
label: "labels.sendBackward", label: "labels.sendBackward",
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon, icon: SendBackwardIcon,
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState) => {
@@ -50,7 +49,6 @@ export const actionSendBackward = register({
export const actionBringForward = register({ export const actionBringForward = register({
name: "bringForward", name: "bringForward",
label: "labels.bringForward", label: "labels.bringForward",
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon, icon: BringForwardIcon,
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState) => {
@@ -80,7 +78,6 @@ export const actionBringForward = register({
export const actionSendToBack = register({ export const actionSendToBack = register({
name: "sendToBack", name: "sendToBack",
label: "labels.sendToBack", label: "labels.sendToBack",
keywords: ["move down", "zindex", "layer"],
icon: SendToBackIcon, icon: SendToBackIcon,
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState) => {
@@ -117,7 +114,6 @@ export const actionSendToBack = register({
export const actionBringToFront = register({ export const actionBringToFront = register({
name: "bringToFront", name: "bringToFront",
label: "labels.bringToFront", label: "labels.bringToFront",
keywords: ["move up", "zindex", "layer"],
icon: BringToFrontIcon, icon: BringToFrontIcon,
trackEvent: { category: "element" }, trackEvent: { category: "element" },
+3 -6
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import type { import {
Action, Action,
UpdaterFn, UpdaterFn,
ActionName, ActionName,
@@ -7,11 +7,8 @@ import type {
PanelComponentProps, PanelComponentProps,
ActionSource, ActionSource,
} from "./types"; } from "./types";
import type { import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
ExcalidrawElement, import { AppClassProperties, AppState } from "../types";
OrderedExcalidrawElement,
} from "../element/types";
import type { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { isPromiseLike } from "../utils"; import { isPromiseLike } from "../utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Action } from "./types"; import { Action } from "./types";
export let actions: readonly Action[] = []; export let actions: readonly Action[] = [];
+2 -2
View File
@@ -1,8 +1,8 @@
import { isDarwin } from "../constants"; import { isDarwin } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import type { SubtypeOf } from "../utility-types"; import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import type { ActionName } from "./types"; import { ActionName } from "./types";
export type ShortcutName = export type ShortcutName =
| SubtypeOf< | SubtypeOf<
+6 -11
View File
@@ -1,17 +1,14 @@
import type React from "react"; import React from "react";
import type { import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
ExcalidrawElement, import {
OrderedExcalidrawElement,
} from "../element/types";
import type {
AppClassProperties, AppClassProperties,
AppState, AppState,
ExcalidrawProps, ExcalidrawProps,
BinaryFiles, BinaryFiles,
UIAppState, UIAppState,
} from "../types"; } from "../types";
import type { MarkOptional } from "../utility-types"; import { MarkOptional } from "../utility-types";
import type { StoreActionType } from "../store"; import { StoreActionType } from "../store";
export type ActionSource = export type ActionSource =
| "ui" | "ui"
@@ -134,9 +131,7 @@ export type ActionName =
| "setEmbeddableAsActiveTool" | "setEmbeddableAsActiveTool"
| "createContainerFromText" | "createContainerFromText"
| "wrapTextInContainer" | "wrapTextInContainer"
| "commandPalette" | "commandPalette";
| "autoResize"
| "elementStats";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
+2 -3
View File
@@ -1,7 +1,6 @@
import type { ElementsMap, ExcalidrawElement } from "./element/types"; import { ElementsMap, ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement"; import { newElementWith } from "./element/mutateElement";
import type { BoundingBox } from "./element/bounds"; import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import { getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups"; import { getMaximumGroups } from "./groups";
export interface Alignment { export interface Alignment {
+7 -10
View File
@@ -1,6 +1,6 @@
// place here categories that you want to track. We want to track just a // place here categories that you want to track. We want to track just a
// small subset of categories at a given time. // small subset of categories at a given time.
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette"]); const ALLOWED_CATEGORIES_TO_TRACK = ["ai", "command_palette"] as string[];
export const trackEvent = ( export const trackEvent = (
category: string, category: string,
@@ -9,20 +9,17 @@ export const trackEvent = (
value?: number, value?: number,
) => { ) => {
try { try {
// prettier-ignore
if ( if (
typeof window === "undefined" || typeof window === "undefined"
import.meta.env.VITE_WORKER_ID || || import.meta.env.VITE_WORKER_ID
import.meta.env.VITE_APP_ENABLE_TRACKING !== "true" // comment out to debug locally
|| import.meta.env.PROD
) { ) {
return; return;
} }
if (!ALLOWED_CATEGORIES_TO_TRACK.has(category)) { if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
return;
}
if (import.meta.env.DEV) {
// comment out to debug in dev
return; return;
} }
+3 -4
View File
@@ -1,7 +1,6 @@
import type { LaserPointerOptions } from "@excalidraw/laser-pointer"; import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
import { LaserPointer } from "@excalidraw/laser-pointer"; import { AnimationFrameHandler } from "./animation-frame-handler";
import type { AnimationFrameHandler } from "./animation-frame-handler"; import { AppState } from "./types";
import type { AppState } from "./types";
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils"; import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
import type App from "./components/App"; import type App from "./components/App";
import { SVG_NS } from "./constants"; import { SVG_NS } from "./constants";
+3 -7
View File
@@ -5,10 +5,9 @@ import {
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
EXPORT_SCALES, EXPORT_SCALES,
STATS_PANELS,
THEME, THEME,
} from "./constants"; } from "./constants";
import type { AppState, NormalizedZoomValue } from "./types"; import { AppState, NormalizedZoomValue } from "./types";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio) const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio ? devicePixelRatio
@@ -81,10 +80,7 @@ export const getDefaultAppState = (): Omit<
selectedElementsAreBeingDragged: false, selectedElementsAreBeingDragged: false,
selectionElement: null, selectionElement: null,
shouldCacheIgnoreZoom: false, shouldCacheIgnoreZoom: false,
stats: { showStats: false,
open: false,
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
},
startBoundElement: null, startBoundElement: null,
suggestedBindings: [], suggestedBindings: [],
frameRendering: { enabled: true, clip: true, name: true, outline: true }, frameRendering: { enabled: true, clip: true, name: true, outline: true },
@@ -200,7 +196,7 @@ const APP_STATE_STORAGE_CONF = (<
}, },
selectionElement: { browser: false, export: false, server: false }, selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false }, shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
stats: { browser: true, export: false, server: false }, showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false }, startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false }, suggestedBindings: { browser: false, export: false, server: false },
frameRendering: { browser: false, export: false, server: false }, frameRendering: { browser: false, export: false, server: false },
+18 -23
View File
@@ -1,14 +1,18 @@
import { ENV } from "./constants"; import { ENV } from "./constants";
import type { BindableProp, BindingProp } from "./element/binding";
import { import {
BoundElement, BoundElement,
BindableElement, BindableElement,
BindableProp,
BindingProp,
bindingProperties, bindingProperties,
updateBoundElements, updateBoundElements,
} from "./element/binding"; } from "./element/binding";
import { LinearElementEditor } from "./element/linearElementEditor"; import { LinearElementEditor } from "./element/linearElementEditor";
import type { ElementUpdate } from "./element/mutateElement"; import {
import { mutateElement, newElementWith } from "./element/mutateElement"; ElementUpdate,
mutateElement,
newElementWith,
} from "./element/mutateElement";
import { import {
getBoundTextElementId, getBoundTextElementId,
redrawTextBoundingBox, redrawTextBoundingBox,
@@ -19,7 +23,7 @@ import {
isBoundToContainer, isBoundToContainer,
isTextElement, isTextElement,
} from "./element/typeChecks"; } from "./element/typeChecks";
import type { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@@ -30,13 +34,13 @@ import type {
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex"; import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups"; import { getNonDeletedGroupIds } from "./groups";
import { getObservedAppState } from "./store"; import { getObservedAppState } from "./store";
import type { import {
AppState, AppState,
ObservedAppState, ObservedAppState,
ObservedElementsAppState, ObservedElementsAppState,
ObservedStandaloneAppState, ObservedStandaloneAppState,
} from "./types"; } from "./types";
import type { SubtypeOf, ValueOf } from "./utility-types"; import { SubtypeOf, ValueOf } from "./utility-types";
import { import {
arrayToMap, arrayToMap,
arrayToObject, arrayToObject,
@@ -1477,28 +1481,19 @@ export class ElementsChange implements Change<SceneElementsMap> {
return elements; return elements;
} }
const unordered = Array.from(elements.values()); const previous = Array.from(elements.values());
const ordered = orderByFractionalIndex([...unordered]); const reordered = orderByFractionalIndex([...previous]);
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);
}
return acc; if (
}, !flags.containsVisibleDifference &&
new Map(), Delta.isRightDifferent(previous, reordered, true)
); ) {
if (!flags.containsVisibleDifference && moved.size) {
// we found a difference in order! // we found a difference in order!
flags.containsVisibleDifference = true; flags.containsVisibleDifference = true;
} }
// synchronize all elements that were actually moved // let's synchronize all invalid indices of moved elements
// could fallback to synchronizing all invalid indices return arrayToMap(syncMovedIndices(reordered, changed)) as typeof elements;
return arrayToMap(syncMovedIndices(ordered, moved)) as typeof elements;
} }
/** /**
+6 -2
View File
@@ -1,5 +1,9 @@
import type { Spreadsheet } from "./charts"; import {
import { tryParseCells, tryParseNumber, VALID_SPREADSHEET } from "./charts"; Spreadsheet,
tryParseCells,
tryParseNumber,
VALID_SPREADSHEET,
} from "./charts";
describe("charts", () => { describe("charts", () => {
describe("tryParseNumber", () => { describe("tryParseNumber", () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "./constants"; } from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element"; import { newElement, newLinearElement, newTextElement } from "./element";
import type { NonDeletedExcalidrawElement } from "./element/types"; import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
export type ChartElements = readonly NonDeletedExcalidrawElement[]; export type ChartElements = readonly NonDeletedExcalidrawElement[];
+3 -3
View File
@@ -5,13 +5,13 @@ import {
THEME, THEME,
} from "./constants"; } from "./constants";
import { roundRect } from "./renderer/roundRect"; import { roundRect } from "./renderer/roundRect";
import type { InteractiveCanvasRenderConfig } from "./scene/types"; import { InteractiveCanvasRenderConfig } from "./scene/types";
import type { import {
Collaborator, Collaborator,
InteractiveCanvasAppState, InteractiveCanvasAppState,
SocketId, SocketId,
UserIdleState,
} from "./types"; } from "./types";
import { UserIdleState } from "./types";
function hashToInteger(id: string) { function hashToInteger(id: string) {
let hash = 0; let hash = 0;
+3 -4
View File
@@ -1,10 +1,9 @@
import type { import {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import type { BinaryFiles } from "./types"; import { BinaryFiles } from "./types";
import type { Spreadsheet } from "./charts"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import { import {
ALLOWED_PASTE_MIME_TYPES, ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
+1 -1
View File
@@ -1,5 +1,5 @@
import oc from "open-color"; import oc from "open-color";
import type { Merge } from "./utility-types"; import { Merge } from "./utility-types";
// FIXME can't put to utils.ts rn because of circular dependency // FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>( const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
+7 -18
View File
@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import type { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import type { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawElementType, ExcalidrawElementType,
NonDeletedElementsMap, NonDeletedElementsMap,
@@ -17,17 +17,13 @@ import {
hasStrokeWidth, hasStrokeWidth,
} from "../scene"; } from "../scene";
import { SHAPES } from "../shapes"; import { SHAPES } from "../shapes";
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types"; import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils"; import { capitalizeString, isTransparent } from "../utils";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
hasBoundTextElement,
isLinearElement,
isTextElement,
} from "../element/typeChecks";
import clsx from "clsx"; import clsx from "clsx";
import { actionToggleZenMode } from "../actions"; import { actionToggleZenMode } from "../actions";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
@@ -118,11 +114,6 @@ export const SelectedShapeActions = ({
const showLinkIcon = const showLinkIcon =
targetElements.length === 1 || isSingleElementBoundContainer; targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction =
!appState.editingLinearElement &&
targetElements.length === 1 &&
isLinearElement(targetElements[0]);
return ( return (
<div className="panelColumn"> <div className="panelColumn">
<div> <div>
@@ -182,8 +173,8 @@ export const SelectedShapeActions = ({
<div className="buttonList"> <div className="buttonList">
{renderAction("sendToBack")} {renderAction("sendToBack")}
{renderAction("sendBackward")} {renderAction("sendBackward")}
{renderAction("bringForward")}
{renderAction("bringToFront")} {renderAction("bringToFront")}
{renderAction("bringForward")}
</div> </div>
</fieldset> </fieldset>
@@ -238,7 +229,6 @@ export const SelectedShapeActions = ({
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}
{showLineEditorAction && renderAction("toggleLinearEditor")}
</div> </div>
</fieldset> </fieldset>
)} )}
@@ -343,8 +333,8 @@ export const ShapesSwitcher = ({
fontSize: 8, fontSize: 8,
fontFamily: "Cascadia, monospace", fontFamily: "Cascadia, monospace",
position: "absolute", position: "absolute",
background: "var(--color-promo)", background: "pink",
color: "var(--color-surface-lowest)", color: "black",
bottom: 3, bottom: 3,
right: 4, right: 4,
}} }}
@@ -468,7 +458,6 @@ export const ExitZenModeAction = ({
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
}) => ( }) => (
<button <button
type="button"
className={clsx("disable-zen-mode", { className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn, "disable-zen-mode--visible": showExitZenModeBtn,
})} })}
+178 -241
View File
@@ -1,7 +1,7 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import type { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import clsx from "clsx"; import clsx from "clsx";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -39,16 +39,18 @@ import {
import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register"; import { actions } from "../actions/register";
import type { Action, ActionResult } from "../actions/types"; import { Action, ActionResult } from "../actions/types";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { import {
getDefaultAppState, getDefaultAppState,
isEraserActive, isEraserActive,
isHandToolActive, isHandToolActive,
} from "../appState"; } from "../appState";
import type { PastedMixedContent } from "../clipboard"; import {
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard"; PastedMixedContent,
import type { EXPORT_IMAGE_TYPES } from "../constants"; copyTextToSystemClipboard,
parseClipboard,
} from "../clipboard";
import { import {
APP_NAME, APP_NAME,
CURSOR_TYPE, CURSOR_TYPE,
@@ -60,6 +62,7 @@ import {
ENV, ENV,
EVENT, EVENT,
FRAME_STYLE, FRAME_STYLE,
EXPORT_IMAGE_TYPES,
GRID_SIZE, GRID_SIZE,
IMAGE_MIME_TYPES, IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
@@ -88,10 +91,8 @@ import {
isIOS, isIOS,
supportsResizeObserver, supportsResizeObserver,
DEFAULT_COLLISION_THRESHOLD, DEFAULT_COLLISION_THRESHOLD,
DEFAULT_TEXT_ALIGN,
} from "../constants"; } from "../constants";
import type { ExportedElements } from "../data"; import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore"; import { restore, restoreElements } from "../data/restore";
import { import {
@@ -115,7 +116,7 @@ import {
newTextElement, newTextElement,
newImageElement, newImageElement,
transformElements, transformElements,
refreshTextDimensions, updateTextElement,
redrawTextBoundingBox, redrawTextBoundingBox,
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "../element"; } from "../element";
@@ -162,7 +163,7 @@ import {
isMagicFrameElement, isMagicFrameElement,
isTextBindableContainer, isTextBindableContainer,
} from "../element/typeChecks"; } from "../element/typeChecks";
import type { import {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
@@ -219,16 +220,20 @@ import {
isSomeElementSelected, isSomeElementSelected,
} from "../scene"; } from "../scene";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import type { import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
RenderInteractiveSceneCallback,
ScrollBars,
} from "../scene/types";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { findShapeByKey, getElementShape } from "../shapes"; import { findShapeByKey } from "../shapes";
import type { GeometricShape } from "../../utils/geometry/shape"; import {
import { getSelectionBoxShape } from "../../utils/geometry/shape"; GeometricShape,
getClosedCurveShape,
getCurveShape,
getEllipseShape,
getFreedrawShape,
getPolygonShape,
getSelectionBoxShape,
} from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision"; import { isPointInShape } from "../../utils/collision";
import type { import {
AppClassProperties, AppClassProperties,
AppProps, AppProps,
AppState, AppState,
@@ -286,8 +291,11 @@ import {
maybeParseEmbedSrc, maybeParseEmbedSrc,
getEmbedLink, getEmbedLink,
} from "../element/embeddable"; } from "../element/embeddable";
import type { ContextMenuItems } from "./ContextMenu"; import {
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu"; ContextMenu,
ContextMenuItems,
CONTEXT_MENU_SEPARATOR,
} from "./ContextMenu";
import LayerUI from "./LayerUI"; import LayerUI from "./LayerUI";
import { Toast } from "./Toast"; import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { actionToggleViewMode } from "../actions/actionToggleViewMode";
@@ -312,8 +320,7 @@ import {
updateImageCache as _updateImageCache, updateImageCache as _updateImageCache,
} from "../element/image"; } from "../element/image";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import type { FileSystemHandle } from "../data/filesystem"; import { fileOpen, FileSystemHandle } from "../data/filesystem";
import { fileOpen } from "../data/filesystem";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getApproxMinLineHeight, getApproxMinLineHeight,
@@ -323,11 +330,8 @@ import {
getContainerElement, getContainerElement,
getDefaultLineHeight, getDefaultLineHeight,
getLineHeightInPx, getLineHeightInPx,
getMinTextElementWidth,
isMeasureTextSupported, isMeasureTextSupported,
isValidTextContainer, isValidTextContainer,
measureText,
wrapText,
} from "../element/textElement"; } from "../element/textElement";
import { import {
showHyperlinkTooltip, showHyperlinkTooltip,
@@ -382,9 +386,11 @@ import {
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper"; import { activeEyeDropperAtom } from "./EyeDropper";
import type { ExcalidrawElementSkeleton } from "../data/transform"; import {
import { convertToExcalidrawElements } from "../data/transform"; ExcalidrawElementSkeleton,
import type { ValueOf } from "../utility-types"; convertToExcalidrawElements,
} from "../data/transform";
import { ValueOf } from "../utility-types";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { StaticCanvas, InteractiveCanvas } from "./canvases";
import { Renderer } from "../scene/Renderer"; import { Renderer } from "../scene/Renderer";
@@ -398,8 +404,7 @@ import {
} from "../cursor"; } from "../cursor";
import { Emitter } from "../emitter"; import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
import type { MagicCacheData } from "../data/magic"; import { MagicCacheData, diagramToHTML } from "../data/magic";
import { diagramToHTML } from "../data/magic";
import { exportToBlob } from "../../utils/export"; import { exportToBlob } from "../../utils/export";
import { COLOR_PALETTE } from "../colors"; import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton"; import { ElementCanvasButton } from "./MagicButton";
@@ -416,6 +421,7 @@ import {
hitElementBoundText, hitElementBoundText,
hitElementBoundingBoxOnly, hitElementBoundingBoxOnly,
hitElementItself, hitElementItself,
shouldTestInside,
} from "../element/collision"; } from "../element/collision";
import { textWysiwyg } from "../element/textWysiwyg"; import { textWysiwyg } from "../element/textWysiwyg";
import { isOverScrollBars } from "../scene/scrollbars"; import { isOverScrollBars } from "../scene/scrollbars";
@@ -425,9 +431,6 @@ import {
isPointHittingLinkIcon, isPointHittingLinkIcon,
} from "./hyperlink/helpers"; } from "./hyperlink/helpers";
import { getShortcutFromShortcutName } from "../actions/shortcuts"; import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
import { getVisibleSceneBounds } from "../element/bounds";
import { isMaybeMermaidDefinition } from "../mermaid";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -713,7 +716,10 @@ class App extends React.Component<AppProps, AppState> {
id: this.id, id: this.id,
}; };
this.fonts = new Fonts({ scene: this.scene }); this.fonts = new Fonts({
scene: this.scene,
onSceneUpdated: this.onSceneUpdated,
});
this.history = new History(); this.history = new History();
this.actionManager.registerAll(actions); this.actionManager.registerAll(actions);
@@ -936,7 +942,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
if (updated) { if (updated) {
this.scene.triggerUpdate(); this.scene.informMutation();
} }
// GC // GC
@@ -1448,10 +1454,10 @@ class App extends React.Component<AppProps, AppState> {
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props; const { renderTopRightUI, renderCustomStats } = this.props;
const sceneNonce = this.scene.getSceneNonce(); const versionNonce = this.scene.getVersionNonce();
const { elementsMap, visibleElements } = const { elementsMap, visibleElements } =
this.renderer.getRenderableElements({ this.renderer.getRenderableElements({
sceneNonce, versionNonce,
zoom: this.state.zoom, zoom: this.state.zoom,
offsetLeft: this.state.offsetLeft, offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop, offsetTop: this.state.offsetTop,
@@ -1669,7 +1675,7 @@ class App extends React.Component<AppProps, AppState> {
elementsMap={elementsMap} elementsMap={elementsMap}
allElementsMap={allElementsMap} allElementsMap={allElementsMap}
visibleElements={visibleElements} visibleElements={visibleElements}
sceneNonce={sceneNonce} versionNonce={versionNonce}
selectionNonce={ selectionNonce={
this.state.selectionElement?.versionNonce this.state.selectionElement?.versionNonce
} }
@@ -1690,9 +1696,8 @@ class App extends React.Component<AppProps, AppState> {
canvas={this.interactiveCanvas} canvas={this.interactiveCanvas}
elementsMap={elementsMap} elementsMap={elementsMap}
visibleElements={visibleElements} visibleElements={visibleElements}
allElementsMap={allElementsMap}
selectedElements={selectedElements} selectedElements={selectedElements}
sceneNonce={sceneNonce} versionNonce={versionNonce}
selectionNonce={ selectionNonce={
this.state.selectionElement?.versionNonce this.state.selectionElement?.versionNonce
} }
@@ -1816,7 +1821,7 @@ class App extends React.Component<AppProps, AppState> {
); );
} }
this.magicGenerations.set(frameElement.id, data); this.magicGenerations.set(frameElement.id, data);
this.triggerRender(); this.onSceneUpdated();
}; };
private getTextFromElements(elements: readonly ExcalidrawElement[]) { private getTextFromElements(elements: readonly ExcalidrawElement[]) {
@@ -2126,19 +2131,12 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
public syncActionResult = withBatchedUpdates((actionResult: ActionResult) => { private syncActionResult = withBatchedUpdates(
(actionResult: ActionResult) => {
if (this.unmounted || actionResult === false) { if (this.unmounted || actionResult === false) {
return; return;
} }
if (actionResult.storeAction === StoreAction.UPDATE) {
this.store.shouldUpdateSnapshot();
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
this.store.shouldCaptureIncrement();
}
let didUpdate = false;
let editingElement: AppState["editingElement"] | null = null; let editingElement: AppState["editingElement"] | null = null;
if (actionResult.elements) { if (actionResult.elements) {
actionResult.elements.forEach((element) => { actionResult.elements.forEach((element) => {
@@ -2151,8 +2149,13 @@ class App extends React.Component<AppProps, AppState> {
} }
}); });
if (actionResult.storeAction === StoreAction.UPDATE) {
this.store.shouldUpdateSnapshot();
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
this.store.shouldCaptureIncrement();
}
this.scene.replaceAllElements(actionResult.elements); this.scene.replaceAllElements(actionResult.elements);
didUpdate = true;
} }
if (actionResult.files) { if (actionResult.files) {
@@ -2163,6 +2166,12 @@ class App extends React.Component<AppProps, AppState> {
} }
if (actionResult.appState || editingElement || this.state.contextMenu) { if (actionResult.appState || editingElement || this.state.contextMenu) {
if (actionResult.storeAction === StoreAction.UPDATE) {
this.store.shouldUpdateSnapshot();
} else if (actionResult.storeAction === StoreAction.CAPTURE) {
this.store.shouldCaptureIncrement();
}
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
let gridSize = actionResult?.appState?.gridSize || null; let gridSize = actionResult?.appState?.gridSize || null;
@@ -2208,14 +2217,9 @@ class App extends React.Component<AppProps, AppState> {
errorMessage, errorMessage,
}); });
}); });
didUpdate = true;
} }
},
if (!didUpdate && actionResult.storeAction !== StoreAction.NONE) { );
this.scene.triggerUpdate();
}
});
// Lifecycle // Lifecycle
@@ -2282,11 +2286,7 @@ class App extends React.Component<AppProps, AppState> {
} }
let initialData = null; let initialData = null;
try { try {
if (typeof this.props.initialData === "function") {
initialData = (await this.props.initialData()) || null;
} else {
initialData = (await this.props.initialData) || null; initialData = (await this.props.initialData) || null;
}
if (initialData?.libraryItems) { if (initialData?.libraryItems) {
this.library this.library
.updateLibrary({ .updateLibrary({
@@ -2446,7 +2446,7 @@ class App extends React.Component<AppProps, AppState> {
this.history.record(increment.elementsChange, increment.appStateChange); this.history.record(increment.elementsChange, increment.appStateChange);
}); });
this.scene.onUpdate(this.triggerRender); this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
if (this.props.autoFocus && this.excalidrawContainerRef.current) { if (this.props.autoFocus && this.excalidrawContainerRef.current) {
@@ -2489,17 +2489,15 @@ class App extends React.Component<AppProps, AppState> {
} }
public componentWillUnmount() { public componentWillUnmount() {
(window as any).launchQueue?.setConsumer(() => {});
this.renderer.destroy(); this.renderer.destroy();
this.scene.destroy();
this.scene = new Scene(); this.scene = new Scene();
this.fonts = new Fonts({ scene: this.scene });
this.renderer = new Renderer(this.scene); this.renderer = new Renderer(this.scene);
this.files = {}; this.files = {};
this.imageCache.clear(); this.imageCache.clear();
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
this.unmounted = true; this.unmounted = true;
this.removeEventListeners(); this.removeEventListeners();
this.scene.destroy();
this.library.destroy(); this.library.destroy();
this.laserTrails.stop(); this.laserTrails.stop();
this.eraserTrail.stop(); this.eraserTrail.stop();
@@ -2570,7 +2568,7 @@ class App extends React.Component<AppProps, AppState> {
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }), addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
addEventListener( addEventListener(
document, document,
EVENT.POINTER_MOVE, EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition, this.updateCurrentCursorPosition,
), ),
// rerender text elements on font load to fix #637 && #1553 // rerender text elements on font load to fix #637 && #1553
@@ -2599,9 +2597,6 @@ class App extends React.Component<AppProps, AppState> {
), ),
addEventListener(window, EVENT.FOCUS, () => { addEventListener(window, EVENT.FOCUS, () => {
this.maybeCleanupAfterMissingPointerUp(null); 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);
}), }),
); );
@@ -2811,7 +2806,7 @@ class App extends React.Component<AppProps, AppState> {
nonDeletedElementsMap, nonDeletedElementsMap,
), ),
), ),
this.scene.getNonDeletedElementsMap(), this,
); );
} }
@@ -3049,31 +3044,6 @@ class App extends React.Component<AppProps, AppState> {
retainSeed: isPlainPaste, retainSeed: isPlainPaste,
}); });
} else if (data.text) { } else if (data.text) {
if (data.text && isMaybeMermaidDefinition(data.text)) {
const api = await import("@excalidraw/mermaid-to-excalidraw");
try {
const { elements: skeletonElements, files } =
await api.parseMermaidToExcalidraw(data.text);
const elements = convertToExcalidrawElements(skeletonElements, {
regenerateIds: true,
});
this.addElementsFromPasteOrLibrary({
elements,
files,
position: "cursor",
});
return;
} catch (err: any) {
console.warn(
`parsing pasted text as mermaid definition failed: ${err.message}`,
);
}
}
const nonEmptyLines = normalizeEOL(data.text) const nonEmptyLines = normalizeEOL(data.text)
.split(/\n+/) .split(/\n+/)
.map((s) => s.trim()) .map((s) => s.trim())
@@ -3371,53 +3341,32 @@ class App extends React.Component<AppProps, AppState> {
text, text,
fontSize: this.state.currentItemFontSize, fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily, fontFamily: this.state.currentItemFontFamily,
textAlign: DEFAULT_TEXT_ALIGN, textAlign: this.state.currentItemTextAlign,
verticalAlign: DEFAULT_VERTICAL_ALIGN, verticalAlign: DEFAULT_VERTICAL_ALIGN,
locked: false, 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; const LINE_GAP = 10;
let currentY = y; let currentY = y;
const lines = isPlainPaste ? [text] : text.split("\n"); const lines = isPlainPaste ? [text] : text.split("\n");
const textElements = lines.reduce( const textElements = lines.reduce(
(acc: ExcalidrawTextElement[], line, idx) => { (acc: ExcalidrawTextElement[], line, idx) => {
const originalText = line.trim(); const text = line.trim();
if (originalText.length) {
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x, x,
y: currentY, 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({ const element = newTextElement({
...textElementProps, ...textElementProps,
x: startX, x,
y: startY, y: currentY,
text, text,
originalText,
lineHeight, lineHeight,
autoResize: !isTextWrapped,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
}); });
acc.push(element); acc.push(element);
@@ -3723,7 +3672,7 @@ class App extends React.Component<AppProps, AppState> {
ShapeCache.delete(element); ShapeCache.delete(element);
} }
}); });
this.scene.triggerUpdate(); this.scene.informMutation();
this.addNewImagesToImageCache(); this.addNewImagesToImageCache();
}, },
@@ -3734,7 +3683,7 @@ class App extends React.Component<AppProps, AppState> {
elements?: SceneData["elements"]; elements?: SceneData["elements"];
appState?: Pick<AppState, K> | null; appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"]; collaborators?: SceneData["collaborators"];
/** @default StoreAction.NONE */ /** @default StoreAction.CAPTURE */
storeAction?: SceneData["storeAction"]; storeAction?: SceneData["storeAction"];
}) => { }) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []); const nextElements = syncInvalidIndices(sceneData.elements ?? []);
@@ -3783,15 +3732,8 @@ class App extends React.Component<AppProps, AppState> {
}, },
); );
private triggerRender = ( private onSceneUpdated = () => {
/** force always re-renders canvas even if no change */
force?: boolean,
) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({}); this.setState({});
}
}; };
/** /**
@@ -3998,7 +3940,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
suggestedBindings: getSuggestedBindingsForArrows( suggestedBindings: getSuggestedBindingsForArrows(
selectedElements, selectedElements,
this.scene.getNonDeletedElementsMap(), this,
), ),
}); });
@@ -4169,7 +4111,7 @@ class App extends React.Component<AppProps, AppState> {
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement), this.scene.getSelectedElements(this.state).filter(isLinearElement),
this.scene.getNonDeletedElementsMap(), this,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
); );
@@ -4360,22 +4302,25 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
const elementsMap = this.scene.getElementsMapIncludingDeleted(); const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = (nextOriginalText: string, isDeleted: boolean) => { const updateElement = (
text: string,
originalText: string,
isDeleted: boolean,
) => {
this.scene.replaceAllElements([ this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map // Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return newElementWith(_element, { return updateTextElement(
originalText: nextOriginalText,
isDeleted: isDeleted ?? _element.isDeleted,
// returns (wrapped) text and new dimensions
...refreshTextDimensions(
_element, _element,
getContainerElement(_element, elementsMap), getContainerElement(_element, elementsMap),
elementsMap, elementsMap,
nextOriginalText, {
), text,
}); isDeleted,
originalText,
},
);
} }
return _element; return _element;
}), }),
@@ -4398,15 +4343,15 @@ class App extends React.Component<AppProps, AppState> {
viewportY - this.state.offsetTop, viewportY - this.state.offsetTop,
]; ];
}, },
onChange: withBatchedUpdates((nextOriginalText) => { onChange: withBatchedUpdates((text) => {
updateElement(nextOriginalText, false); updateElement(text, text, false);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element, elementsMap); updateBoundElements(element, elementsMap);
} }
}), }),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
const isDeleted = !nextOriginalText.trim(); const isDeleted = !text.trim();
updateElement(nextOriginalText, isDeleted); updateElement(text, originalText, isDeleted);
// select the created text element only if submitting via keyboard // select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect) // (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) { if (!isDeleted && viaKeyboard) {
@@ -4445,18 +4390,13 @@ class App extends React.Component<AppProps, AppState> {
element, element,
excalidrawContainer: this.excalidrawContainerRef.current, excalidrawContainer: this.excalidrawContainerRef.current,
app: this, app: this,
// when text is selected, it's hard (at least on iOS) to re-position the
// caret (i.e. deselect). There's not much use for always selecting
// the text on edit anyway (and users can select-all from contextmenu
// if needed)
autoSelect: !this.device.isTouchScreen,
}); });
// deselect all other elements when inserting text // deselect all other elements when inserting text
this.deselectElements(); this.deselectElements();
// do an initial update to re-initialize element position since we were // 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) // modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.originalText, false); updateElement(element.text, element.originalText, false);
} }
private deselectElements() { private deselectElements() {
@@ -4481,6 +4421,59 @@ class App extends React.Component<AppProps, AppState> {
return null; return null;
} }
/**
* get the pure geometric shape of an excalidraw element
* which is then used for hit detection
*/
public getElementShape(element: ExcalidrawElement): GeometricShape {
switch (element.type) {
case "rectangle":
case "diamond":
case "frame":
case "magicframe":
case "embeddable":
case "image":
case "iframe":
case "text":
case "selection":
return getPolygonShape(element);
case "arrow":
case "line": {
const roughShape =
ShapeCache.get(element)?.[0] ??
ShapeCache.generateElementShape(element, null)[0];
const [, , , , cx, cy] = getElementAbsoluteCoords(
element,
this.scene.getNonDeletedElementsMap(),
);
return shouldTestInside(element)
? getClosedCurveShape(
element,
roughShape,
[element.x, element.y],
element.angle,
[cx, cy],
)
: getCurveShape(roughShape, [element.x, element.y], element.angle, [
cx,
cy,
]);
}
case "ellipse":
return getEllipseShape(element);
case "freedraw": {
const [, , , , cx, cy] = getElementAbsoluteCoords(
element,
this.scene.getNonDeletedElementsMap(),
);
return getFreedrawShape(element, [cx, cy], shouldTestInside(element));
}
}
}
private getBoundTextShape(element: ExcalidrawElement): GeometricShape | null { private getBoundTextShape(element: ExcalidrawElement): GeometricShape | null {
const boundTextElement = getBoundTextElement( const boundTextElement = getBoundTextElement(
element, element,
@@ -4489,8 +4482,7 @@ class App extends React.Component<AppProps, AppState> {
if (boundTextElement) { if (boundTextElement) {
if (element.type === "arrow") { if (element.type === "arrow") {
return getElementShape( return this.getElementShape({
{
...boundTextElement, ...boundTextElement,
// arrow's bound text accurate position is not stored in the element's property // arrow's bound text accurate position is not stored in the element's property
// but rather calculated and returned from the following static method // but rather calculated and returned from the following static method
@@ -4499,14 +4491,9 @@ class App extends React.Component<AppProps, AppState> {
boundTextElement, boundTextElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
}, });
this.scene.getNonDeletedElementsMap(),
);
} }
return getElementShape( return this.getElementShape(boundTextElement);
boundTextElement,
this.scene.getNonDeletedElementsMap(),
);
} }
return null; return null;
@@ -4545,10 +4532,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element: elementWithHighestZIndex, element: elementWithHighestZIndex,
shape: getElementShape( shape: this.getElementShape(elementWithHighestZIndex),
elementWithHighestZIndex,
this.scene.getNonDeletedElementsMap(),
),
// when overlapping, we would like to be more precise // when overlapping, we would like to be more precise
// this also avoids the need to update past tests // this also avoids the need to update past tests
threshold: this.getElementHitThreshold() / 2, threshold: this.getElementHitThreshold() / 2,
@@ -4653,7 +4637,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element, element,
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()), shape: this.getElementShape(element),
threshold: this.getElementHitThreshold(), threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(element) frameNameBound: isFrameLikeElement(element)
? this.frameNameBoundsCache.get(element) ? this.frameNameBoundsCache.get(element)
@@ -4685,10 +4669,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element: elements[index], element: elements[index],
shape: getElementShape( shape: this.getElementShape(elements[index]),
elements[index],
this.scene.getNonDeletedElementsMap(),
),
threshold: this.getElementHitThreshold(), threshold: this.getElementHitThreshold(),
}) })
) { ) {
@@ -4708,7 +4689,6 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
insertAtParentCenter = true, insertAtParentCenter = true,
container, container,
autoEdit = true,
}: { }: {
/** X position to insert text at */ /** X position to insert text at */
sceneX: number; sceneX: number;
@@ -4717,7 +4697,6 @@ class App extends React.Component<AppProps, AppState> {
/** whether to attempt to insert at element center if applicable */ /** whether to attempt to insert at element center if applicable */
insertAtParentCenter?: boolean; insertAtParentCenter?: boolean;
container?: ExcalidrawTextContainer | null; container?: ExcalidrawTextContainer | null;
autoEdit?: boolean;
}) => { }) => {
let shouldBindToContainer = false; let shouldBindToContainer = false;
@@ -4850,16 +4829,13 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (autoEdit || existingTextElement || container) { this.setState({
editingElement: element,
});
this.handleTextWysiwyg(element, { this.handleTextWysiwyg(element, {
isExistingElement: !!existingTextElement, isExistingElement: !!existingTextElement,
}); });
} else {
this.setState({
draggingElement: element,
multiElement: null,
});
}
}; };
private handleCanvasDoubleClick = ( private handleCanvasDoubleClick = (
@@ -4946,10 +4922,7 @@ class App extends React.Component<AppProps, AppState> {
x: sceneX, x: sceneX,
y: sceneY, y: sceneY,
element: container, element: container,
shape: getElementShape( shape: this.getElementShape(container),
container,
this.scene.getNonDeletedElementsMap(),
),
threshold: this.getElementHitThreshold(), threshold: this.getElementHitThreshold(),
}) })
) { ) {
@@ -5130,11 +5103,8 @@ class App extends React.Component<AppProps, AppState> {
this.translateCanvas({ this.translateCanvas({
zoom: zoomState.zoom, zoom: zoomState.zoom,
// 2x multiplier is just a magic number that makes this work correctly scrollX: zoomState.scrollX + deltaX / nextZoom,
// on touchscreen devices (note: if we get report that panning is slower/faster scrollY: zoomState.scrollY + deltaY / nextZoom,
// than actual movement, consider swapping with devicePixelRatio)
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
shouldCacheIgnoreZoom: true, shouldCacheIgnoreZoom: true,
}); });
}); });
@@ -5609,7 +5579,7 @@ class App extends React.Component<AppProps, AppState> {
} }
this.elementsPendingErasure = new Set(this.elementsPendingErasure); this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.triggerRender(); this.onSceneUpdated();
} }
}; };
@@ -5641,10 +5611,7 @@ class App extends React.Component<AppProps, AppState> {
x: scenePointerX, x: scenePointerX,
y: scenePointerY, y: scenePointerY,
element, element,
shape: getElementShape( shape: this.getElementShape(element),
element,
this.scene.getNonDeletedElementsMap(),
),
}) })
) { ) {
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor( hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
@@ -5900,6 +5867,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.activeTool.type === "text") { if (this.state.activeTool.type === "text") {
this.handleTextOnPointerDown(event, pointerDownState); this.handleTextOnPointerDown(event, pointerDownState);
return;
} else if ( } else if (
this.state.activeTool.type === "arrow" || this.state.activeTool.type === "arrow" ||
this.state.activeTool.type === "line" this.state.activeTool.type === "line"
@@ -6020,7 +5988,6 @@ class App extends React.Component<AppProps, AppState> {
); );
const clicklength = const clicklength =
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
if (this.device.editor.isMobile && clicklength < 300) { if (this.device.editor.isMobile && clicklength < 300) {
const hitElement = this.getElementAtPosition( const hitElement = this.getElementAtPosition(
scenePointer.x, scenePointer.x,
@@ -6694,7 +6661,6 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
insertAtParentCenter: !event.altKey, insertAtParentCenter: !event.altKey,
container, container,
autoEdit: false,
}); });
resetCursor(this.interactiveCanvas); resetCursor(this.interactiveCanvas);
@@ -6763,7 +6729,7 @@ class App extends React.Component<AppProps, AppState> {
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
this.scene.getNonDeletedElementsMap(), this,
); );
this.scene.insertElement(element); this.scene.insertElement(element);
this.setState({ this.setState({
@@ -7025,7 +6991,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
this.scene.getNonDeletedElementsMap(), this,
); );
this.scene.insertElement(element); this.scene.insertElement(element);
@@ -7495,7 +7461,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
suggestedBindings: getSuggestedBindingsForArrows( suggestedBindings: getSuggestedBindingsForArrows(
selectedElements, selectedElements,
this.scene.getNonDeletedElementsMap(), this,
), ),
}); });
@@ -8016,7 +7982,7 @@ class App extends React.Component<AppProps, AppState> {
draggingElement, draggingElement,
this.state, this.state,
pointerCoords, pointerCoords,
this.scene.getNonDeletedElementsMap(), this,
); );
} }
this.setState({ suggestedBindings: [], startBoundElement: null }); this.setState({ suggestedBindings: [], startBoundElement: null });
@@ -8045,28 +8011,6 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
if (isTextElement(draggingElement)) {
const minWidth = getMinTextElementWidth(
getFontString({
fontSize: draggingElement.fontSize,
fontFamily: draggingElement.fontFamily,
}),
draggingElement.lineHeight,
);
if (draggingElement.width < minWidth) {
mutateElement(draggingElement, {
autoResize: true,
});
}
this.resetCursor();
this.handleTextWysiwyg(draggingElement, {
isExistingElement: true,
});
}
if ( if (
activeTool.type !== "selection" && activeTool.type !== "selection" &&
draggingElement && draggingElement &&
@@ -8127,7 +8071,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
this.scene.triggerUpdate(); this.scene.informMutation();
} }
} }
} }
@@ -8506,10 +8450,7 @@ class App extends React.Component<AppProps, AppState> {
x: pointerDownState.origin.x, x: pointerDownState.origin.x,
y: pointerDownState.origin.y, y: pointerDownState.origin.y,
element: hitElement, element: hitElement,
shape: getElementShape( shape: this.getElementShape(hitElement),
hitElement,
this.scene.getNonDeletedElementsMap(),
),
threshold: this.getElementHitThreshold(), threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(hitElement) frameNameBound: isFrameLikeElement(hitElement)
? this.frameNameBoundsCache.get(hitElement) ? this.frameNameBoundsCache.get(hitElement)
@@ -8577,7 +8518,7 @@ class App extends React.Component<AppProps, AppState> {
bindOrUnbindLinearElements( bindOrUnbindLinearElements(
linearElements, linearElements,
this.scene.getNonDeletedElementsMap(), this,
isBindingEnabled(this.state), isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [], this.state.selectedLinearElement?.selectedPointsIndices ?? [],
); );
@@ -8625,7 +8566,7 @@ class App extends React.Component<AppProps, AppState> {
private restoreReadyToEraseElements = () => { private restoreReadyToEraseElements = () => {
this.elementsPendingErasure = new Set(); this.elementsPendingErasure = new Set();
this.triggerRender(); this.onSceneUpdated();
}; };
private eraseElements = () => { private eraseElements = () => {
@@ -9039,7 +8980,7 @@ class App extends React.Component<AppProps, AppState> {
files, files,
); );
if (updatedFiles.size) { if (updatedFiles.size) {
this.scene.triggerUpdate(); this.scene.informMutation();
} }
} }
}; };
@@ -9065,7 +9006,7 @@ class App extends React.Component<AppProps, AppState> {
}): void => { }): void => {
const hoveredBindableElement = getHoveredElementForBinding( const hoveredBindableElement = getHoveredElementForBinding(
pointerCoords, pointerCoords,
this.scene.getNonDeletedElementsMap(), this,
); );
this.setState({ this.setState({
suggestedBindings: suggestedBindings:
@@ -9092,7 +9033,7 @@ class App extends React.Component<AppProps, AppState> {
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => { (acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding( const hoveredBindableElement = getHoveredElementForBinding(
coords, coords,
this.scene.getNonDeletedElementsMap(), this,
); );
if ( if (
hoveredBindableElement != null && hoveredBindableElement != null &&
@@ -9437,7 +9378,6 @@ class App extends React.Component<AppProps, AppState> {
distance(pointerDownState.origin.y, pointerCoords.y), distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio(event), shouldMaintainAspectRatio(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),
this.state.zoom.value,
); );
} else { } else {
let [gridX, gridY] = getGridPoint( let [gridX, gridY] = getGridPoint(
@@ -9495,7 +9435,6 @@ class App extends React.Component<AppProps, AppState> {
? !shouldMaintainAspectRatio(event) ? !shouldMaintainAspectRatio(event)
: shouldMaintainAspectRatio(event), : shouldMaintainAspectRatio(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),
this.state.zoom.value,
aspectRatio, aspectRatio,
this.state.originSnapOffset, this.state.originSnapOffset,
); );
@@ -9624,7 +9563,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
const suggestedBindings = getSuggestedBindingsForArrows( const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements, selectedElements,
this.scene.getNonDeletedElementsMap(), this,
); );
const elementsToHighlight = new Set<ExcalidrawElement>(); const elementsToHighlight = new Set<ExcalidrawElement>();
@@ -9696,7 +9635,6 @@ class App extends React.Component<AppProps, AppState> {
} }
return [ return [
CONTEXT_MENU_SEPARATOR,
actionCut, actionCut,
actionCopy, actionCopy,
actionPaste, actionPaste,
@@ -9709,7 +9647,6 @@ class App extends React.Component<AppProps, AppState> {
actionPasteStyles, actionPasteStyles,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionGroup, actionGroup,
actionTextAutoResize,
actionUnbindText, actionUnbindText,
actionBindText, actionBindText,
actionWrapTextInContainer, actionWrapTextInContainer,
@@ -28,7 +28,6 @@ export const ButtonIconSelect = <T extends Object>(
{props.options.map((option) => {props.options.map((option) =>
props.type === "button" ? ( props.type === "button" ? (
<button <button
type="button"
key={option.text} key={option.text}
onClick={(event) => props.onClick(option.value, event)} onClick={(event) => props.onClick(option.value, event)}
className={clsx({ className={clsx({
@@ -22,12 +22,7 @@ export const CheckboxItem: React.FC<{
).focus(); ).focus();
}} }}
> >
<button <button className="Checkbox-box" role="checkbox" aria-checked={checked}>
type="button"
className="Checkbox-box"
role="checkbox"
aria-checked={checked}
>
{checkIcon} {checkIcon}
</button> </button>
<div className="Checkbox-label">{children}</div> <div className="Checkbox-label">{children}</div>
@@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker"; import { getColor } from "./ColorPicker";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import type { ColorPickerType } from "./colorPickerUtils"; import {
import { activeColorPickerSectionAtom } from "./colorPickerUtils"; ColorPickerType,
activeColorPickerSectionAtom,
} from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons"; import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys"; import { KEYS } from "../../keys";
@@ -1,15 +1,16 @@
import { isInteractive, isTransparent, isWritableElement } from "../../utils"; import { isInteractive, isTransparent, isWritableElement } from "../../utils";
import type { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import type { AppState } from "../../types"; import { AppState } from "../../types";
import { TopPicks } from "./TopPicks"; import { TopPicks } from "./TopPicks";
import { Picker } from "./Picker"; import { Picker } from "./Picker";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import type { ColorPickerType } from "./colorPickerUtils"; import {
import { activeColorPickerSectionAtom } from "./colorPickerUtils"; activeColorPickerSectionAtom,
ColorPickerType,
} from "./colorPickerUtils";
import { useDevice, useExcalidrawContainer } from "../App"; import { useDevice, useExcalidrawContainer } from "../App";
import type { ColorTuple, ColorPaletteCustom } from "../../colors"; import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
import { COLOR_PALETTE } from "../../colors";
import PickerHeading from "./PickerHeading"; import PickerHeading from "./PickerHeading";
import { t } from "../../i18n"; import { t } from "../../i18n";
import clsx from "clsx"; import clsx from "clsx";
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { t } from "../../i18n"; import { t } from "../../i18n";
import type { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { ShadeList } from "./ShadeList"; import { ShadeList } from "./ShadeList";
import PickerColorList from "./PickerColorList"; import PickerColorList from "./PickerColorList";
@@ -9,15 +9,15 @@ import { useAtom } from "jotai";
import { CustomColorList } from "./CustomColorList"; import { CustomColorList } from "./CustomColorList";
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers"; import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import PickerHeading from "./PickerHeading"; import PickerHeading from "./PickerHeading";
import type { ColorPickerType } from "./colorPickerUtils";
import { import {
ColorPickerType,
activeColorPickerSectionAtom, activeColorPickerSectionAtom,
getColorNameAndShadeFromColor, getColorNameAndShadeFromColor,
getMostUsedCustomColors, getMostUsedCustomColors,
isCustomColor, isCustomColor,
} from "./colorPickerUtils"; } from "./colorPickerUtils";
import type { ColorPaletteCustom } from "../../colors";
import { import {
ColorPaletteCustom,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX, DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../../colors"; } from "../../colors";
@@ -7,9 +7,8 @@ import {
getColorNameAndShadeFromColor, getColorNameAndShadeFromColor,
} from "./colorPickerUtils"; } from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel"; import HotkeyLabel from "./HotkeyLabel";
import type { ColorPaletteCustom } from "../../colors"; import { ColorPaletteCustom } from "../../colors";
import type { TranslationKeys } from "../../i18n"; import { TranslationKeys, t } from "../../i18n";
import { t } from "../../i18n";
interface PickerColorListProps { interface PickerColorListProps {
palette: ColorPaletteCustom; palette: ColorPaletteCustom;
@@ -1,4 +1,4 @@
import type { ReactNode } from "react"; import { ReactNode } from "react";
const PickerHeading = ({ children }: { children: ReactNode }) => ( const PickerHeading = ({ children }: { children: ReactNode }) => (
<div className="color-picker__heading">{children}</div> <div className="color-picker__heading">{children}</div>
@@ -7,7 +7,7 @@ import {
} from "./colorPickerUtils"; } from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel"; import HotkeyLabel from "./HotkeyLabel";
import { t } from "../../i18n"; import { t } from "../../i18n";
import type { ColorPaletteCustom } from "../../colors"; import { ColorPaletteCustom } from "../../colors";
interface ShadeListProps { interface ShadeListProps {
hex: string; hex: string;
@@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import type { ColorPickerType } from "./colorPickerUtils"; import { ColorPickerType } from "./colorPickerUtils";
import { import {
DEFAULT_CANVAS_BACKGROUND_PICKS, DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS, DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -1,7 +1,10 @@
import type { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { atom } from "jotai"; import { atom } from "jotai";
import type { ColorPickerColor, ColorPaletteCustom } from "../../colors"; import {
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors"; ColorPickerColor,
ColorPaletteCustom,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
} from "../../colors";
export const getColorNameAndShadeFromColor = ({ export const getColorNameAndShadeFromColor = ({
palette, palette,
@@ -1,13 +1,14 @@
import { KEYS } from "../../keys"; import { KEYS } from "../../keys";
import type { import {
ColorPickerColor, ColorPickerColor,
ColorPalette, ColorPalette,
ColorPaletteCustom, ColorPaletteCustom,
COLORS_PER_ROW,
COLOR_PALETTE,
} from "../../colors"; } from "../../colors";
import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors"; import { ValueOf } from "../../utility-types";
import type { ValueOf } from "../../utility-types";
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
import { import {
ActiveColorPickerSectionAtomType,
colorPickerHotkeyBindings, colorPickerHotkeyBindings,
getColorNameAndShadeFromColor, getColorNameAndShadeFromColor,
} from "./colorPickerUtils"; } from "./colorPickerUtils";
@@ -10,11 +10,12 @@ import { Dialog } from "../Dialog";
import { TextField } from "../TextField"; import { TextField } from "../TextField";
import clsx from "clsx"; import clsx from "clsx";
import { getSelectedElements } from "../../scene"; import { getSelectedElements } from "../../scene";
import type { Action } from "../../actions/types"; import { Action } from "../../actions/types";
import type { TranslationKeys } from "../../i18n"; import { TranslationKeys, t } from "../../i18n";
import { t } from "../../i18n"; import {
import type { ShortcutName } from "../../actions/shortcuts"; ShortcutName,
import { getShortcutFromShortcutName } from "../../actions/shortcuts"; getShortcutFromShortcutName,
} from "../../actions/shortcuts";
import { DEFAULT_SIDEBAR, EVENT } from "../../constants"; import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
import { import {
LockedIcon, LockedIcon,
@@ -30,7 +31,7 @@ import {
} from "../icons"; } from "../icons";
import fuzzy from "fuzzy"; import fuzzy from "fuzzy";
import { useUIAppState } from "../../context/ui-appState"; import { useUIAppState } from "../../context/ui-appState";
import type { AppProps, AppState, UIAppState } from "../../types"; import { AppProps, AppState, UIAppState } from "../../types";
import { import {
capitalizeString, capitalizeString,
getShortcutKey, getShortcutKey,
@@ -38,7 +39,7 @@ import {
} from "../../utils"; } from "../../utils";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { deburr } from "../../deburr"; import { deburr } from "../../deburr";
import type { MarkRequired } from "../../utility-types"; import { MarkRequired } from "../../utility-types";
import { InlineIcon } from "../InlineIcon"; import { InlineIcon } from "../InlineIcon";
import { SHAPES } from "../../shapes"; import { SHAPES } from "../../shapes";
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions"; import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
@@ -46,7 +47,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { actionClearCanvas, actionLink } from "../../actions"; import { actionClearCanvas, actionLink } from "../../actions";
import { jotaiStore } from "../../jotai"; import { jotaiStore } from "../../jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import type { CommandPaletteItem } from "./types"; import { CommandPaletteItem } from "./types";
import * as defaultItems from "./defaultCommandPaletteItems"; import * as defaultItems from "./defaultCommandPaletteItems";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../analytics";
import { useStable } from "../../hooks/useStable"; import { useStable } from "../../hooks/useStable";
@@ -257,10 +258,10 @@ function CommandPaletteInner({
actionManager.actions.deleteSelectedElements, actionManager.actions.deleteSelectedElements,
actionManager.actions.copyStyles, actionManager.actions.copyStyles,
actionManager.actions.pasteStyles, actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,
actionManager.actions.bringForward,
actionManager.actions.sendBackward, actionManager.actions.sendBackward,
actionManager.actions.sendToBack, actionManager.actions.sendToBack,
actionManager.actions.bringForward,
actionManager.actions.bringToFront,
actionManager.actions.alignTop, actionManager.actions.alignTop,
actionManager.actions.alignBottom, actionManager.actions.alignBottom,
actionManager.actions.alignLeft, actionManager.actions.alignLeft,
@@ -540,7 +541,7 @@ function CommandPaletteInner({
...command, ...command,
icon: command.icon || boltIcon, icon: command.icon || boltIcon,
order: command.order ?? getCategoryOrder(command.category), order: command.order ?? getCategoryOrder(command.category),
haystack: `${deburr(command.label.toLocaleLowerCase())} ${ haystack: `${deburr(command.label)} ${
command.keywords?.join(" ") || "" command.keywords?.join(" ") || ""
}`, }`,
}; };
@@ -777,9 +778,7 @@ function CommandPaletteInner({
return; return;
} }
const _query = deburr( const _query = deburr(commandSearch.replace(/[<>-_| ]/g, ""));
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, ""),
);
matchingCommands = fuzzy matchingCommands = fuzzy
.filter(_query, matchingCommands, { .filter(_query, matchingCommands, {
extract: (command) => command.haystack, extract: (command) => command.haystack,
@@ -1,5 +1,5 @@
import { actionToggleTheme } from "../../actions"; import { actionToggleTheme } from "../../actions";
import type { CommandPaletteItem } from "./types"; import { CommandPaletteItem } from "./types";
export const toggleTheme: CommandPaletteItem = { export const toggleTheme: CommandPaletteItem = {
...actionToggleTheme, ...actionToggleTheme,
@@ -1,6 +1,6 @@
import type { ActionManager } from "../../actions/manager"; import { ActionManager } from "../../actions/manager";
import type { Action } from "../../actions/types"; import { Action } from "../../actions/types";
import type { UIAppState } from "../../types"; import { UIAppState } from "../../types";
export type CommandPaletteItem = { export type CommandPaletteItem = {
label: string; label: string;
@@ -1,6 +1,5 @@
import { t } from "../i18n"; import { t } from "../i18n";
import type { DialogProps } from "./Dialog"; import { Dialog, DialogProps } from "./Dialog";
import { Dialog } from "./Dialog";
import "./ConfirmDialog.scss"; import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton"; import DialogActionButton from "./DialogActionButton";
@@ -1,13 +1,14 @@
import clsx from "clsx"; import clsx from "clsx";
import { Popover } from "./Popover"; import { Popover } from "./Popover";
import type { TranslationKeys } from "../i18n"; import { t, TranslationKeys } from "../i18n";
import { t } from "../i18n";
import "./ContextMenu.scss"; import "./ContextMenu.scss";
import type { ShortcutName } from "../actions/shortcuts"; import {
import { getShortcutFromShortcutName } from "../actions/shortcuts"; getShortcutFromShortcutName,
import type { Action } from "../actions/types"; ShortcutName,
import type { ActionManager } from "../actions/manager"; } from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import { useExcalidrawAppState, useExcalidrawElements } from "./App"; import { useExcalidrawAppState, useExcalidrawElements } from "./App";
import React from "react"; import React from "react";
@@ -105,7 +106,6 @@ export const ContextMenu = React.memo(
}} }}
> >
<button <button
type="button"
className={clsx("context-menu-item", { className={clsx("context-menu-item", {
dangerous: actionName === "deleteSelectedElements", dangerous: actionName === "deleteSelectedElements",
checkmark: item.checked?.(appState), checkmark: item.checked?.(appState),
@@ -3,7 +3,7 @@ import "./ToolIcon.scss";
import { t } from "../i18n"; import { t } from "../i18n";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { THEME } from "../constants"; import { THEME } from "../constants";
import type { Theme } from "../element/types"; import { Theme } from "../element/types";
// We chose to use only explicit toggle and not a third option for system value, // We chose to use only explicit toggle and not a third option for system value,
// but this could be added in the future. // 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 { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState"; import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n"; import { t } from "../i18n";
import type { MarkOptional, Merge } from "../utility-types"; import { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils"; import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App"; import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback"; import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu"; import { LibraryMenu } from "./LibraryMenu";
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common"; import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar"; import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback( const DefaultSidebarTrigger = withInternalFallback(
@@ -123,7 +123,6 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose} onClick={onClose}
title={t("buttons.close")} title={t("buttons.close")}
aria-label={t("buttons.close")} aria-label={t("buttons.close")}
type="button"
> >
{CloseIcon} {CloseIcon}
</button> </button>
@@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import type { ReactNode } from "react"; import { ReactNode } from "react";
import "./DialogActionButton.scss"; import "./DialogActionButton.scss";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
@@ -12,8 +12,8 @@ import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import { useStable } from "../hooks/useStable"; import { useStable } from "../hooks/useStable";
import "./EyeDropper.scss"; import "./EyeDropper.scss";
import type { ColorPickerType } from "./ColorPicker/colorPickerUtils"; import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import type { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
export type EyeDropperProperties = { export type EyeDropperProperties = {
keepOpenOnAlt: boolean; keepOpenOnAlt: boolean;
@@ -1,4 +1,4 @@
import type { UserToFollow } from "../../types"; import { UserToFollow } from "../../types";
import { CloseIcon } from "../icons"; import { CloseIcon } from "../icons";
import "./FollowMode.scss"; import "./FollowMode.scss";
@@ -27,11 +27,7 @@ const FollowMode = ({
{userToFollow.username} {userToFollow.username}
</span> </span>
</div> </div>
<button <button onClick={onDisconnect} className="follow-mode__disconnect-btn">
type="button"
onClick={onDisconnect}
className="follow-mode__disconnect-btn"
>
{CloseIcon} {CloseIcon}
</button> </button>
</div> </div>
@@ -285,7 +285,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={[getShortcutKey("Alt+Shift+D")]} shortcuts={[getShortcutKey("Alt+Shift+D")]}
/> />
<Shortcut <Shortcut
label={t("stats.fullTitle")} label={t("stats.title")}
shortcuts={[getShortcutKey("Alt+/")]} shortcuts={[getShortcutKey("Alt+/")]}
/> />
<Shortcut <Shortcut
@@ -1,5 +1,5 @@
import { t } from "../i18n"; import { t } from "../i18n";
import type { AppClassProperties, Device, UIAppState } from "../types"; import { AppClassProperties, Device, UIAppState } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
@@ -108,7 +108,6 @@ function Picker<T>({
<div className="picker-content" ref={rGallery}> <div className="picker-content" ref={rGallery}>
{options.map((option, i) => ( {options.map((option, i) => (
<button <button
type="button"
className={clsx("picker-option", { className={clsx("picker-option", {
active: value === option.value, active: value === option.value,
})} })}
@@ -172,7 +171,6 @@ export function IconPicker<T>({
<div> <div>
<button <button
name={group} name={group}
type="button"
className={isActive ? "active" : ""} className={isActive ? "active" : ""}
aria-label={label} aria-label={label}
onClick={() => setActive(!isActive)} onClick={() => setActive(!isActive)}
@@ -20,7 +20,7 @@ import {
import { canvasToBlob } from "../data/blob"; import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import type { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../../utils/export"; import { exportToCanvas } from "../../utils/export";

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