Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 745cedf9a3 build(deps): bump ejs from 3.1.9 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 18:48:28 +00:00
281 changed files with 3160 additions and 5471 deletions
-7
View File
@@ -4,15 +4,8 @@
!.eslintrc.json
!.npmrc
!.prettierrc
!excalidraw-app/
!package.json
!public/
!packages/
!tsconfig.json
!yarn.lock
# keep (sub)sub directories at the end to exclude from explicit included
# e.g. ./packages/excalidraw/{dist,node_modules}
**/build
**/dist
**/node_modules
+1 -2
View File
@@ -2,7 +2,6 @@
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"import/no-anonymous-default-export": "off",
"no-restricted-globals": "off",
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
"no-restricted-globals": "off"
}
}
+5 -7
View File
@@ -2,18 +2,16 @@ FROM node:18 AS build
WORKDIR /opt/node_app
COPY . .
# do not ignore optional dependencies:
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
RUN yarn --network-timeout 600000
COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000
ARG NODE_ENV=production
COPY . .
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
+1 -1
View File
@@ -12,9 +12,9 @@ import type * as TExcalidraw from "@excalidraw/excalidraw";
import { nanoid } from "nanoid";
import type { ResolvablePromise } from "../utils";
import {
resolvablePromise,
ResolvablePromise,
distance2d,
fileOpen,
withBatchedUpdates,
@@ -1,4 +1,4 @@
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw";
+1 -1
View File
@@ -1,6 +1,6 @@
import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access";
import { MIME_TYPES } from "@excalidraw/excalidraw";
import type { MIME_TYPES } from "@excalidraw/excalidraw";
import { AbortError } from "../../packages/excalidraw/errors";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+10 -54
View File
@@ -13,7 +13,7 @@ import {
VERSION_TIMEOUT,
} from "../packages/excalidraw/constants";
import { loadFromBlob } from "../packages/excalidraw/data/blob";
import type {
import {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
@@ -29,20 +29,20 @@ import {
StoreAction,
reconcileElements,
} from "../packages/excalidraw";
import type {
import {
AppState,
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "../packages/excalidraw/types";
import type { ResolvablePromise } from "../packages/excalidraw/utils";
import {
debounce,
getVersion,
getFrame,
isTestEnv,
preventUnload,
ResolvablePromise,
resolvablePromise,
isRunningInIframe,
} from "../packages/excalidraw/utils";
@@ -52,8 +52,8 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import type { CollabAPI } from "./collab/Collab";
import Collab, {
CollabAPI,
collabAPIAtom,
isCollaboratingAtom,
isOfflineAtom,
@@ -69,8 +69,11 @@ import {
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import type { RestoredDataState } from "../packages/excalidraw/data/restore";
import { restore, restoreAppState } from "../packages/excalidraw/data/restore";
import {
restore,
restoreAppState,
RestoredDataState,
} from "../packages/excalidraw/data/restore";
import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
@@ -98,7 +101,7 @@ import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
import { appJotaiStore } from "./app-jotai";
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 { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
@@ -126,38 +129,6 @@ polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true;
declare global {
interface BeforeInstallPromptEventChoiceResult {
outcome: "accepted" | "dismissed";
}
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
}
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
}
let pwaEvent: BeforeInstallPromptEvent | null = null;
// Adding a listener outside of the component as it may (?) need to be
// subscribed early to catch the event.
//
// Also note that it will fire only if certain heuristics are met (user has
// used the app for some time, etc.)
window.addEventListener(
"beforeinstallprompt",
(event: BeforeInstallPromptEvent) => {
// prevent Chrome <= 67 from automatically showing the prompt
event.preventDefault();
// cache for later use
pwaEvent = event;
},
);
let isSelfEmbedding = false;
if (window.self !== window.top) {
@@ -1132,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>
+2 -2
View File
@@ -7,8 +7,8 @@ import {
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
import { t } from "../packages/excalidraw/i18n";
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import type { UIAppState } from "../packages/excalidraw/types";
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import { UIAppState } from "../packages/excalidraw/types";
type StorageSizes = { scene: number; total: number };
+7 -9
View File
@@ -1,13 +1,13 @@
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import type {
import {
ExcalidrawImperativeAPI,
SocketId,
} from "../../packages/excalidraw/types";
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type {
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import {
ExcalidrawElement,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
@@ -19,7 +19,7 @@ import {
zoomToFitBounds,
reconcileElements,
} from "../../packages/excalidraw";
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
import {
assertNever,
preventUnload,
@@ -36,14 +36,12 @@ import {
SYNC_FULL_SCENE_INTERVAL_MS,
WS_EVENTS,
} from "../app_constants";
import type {
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
generateCollaborationLinkData,
getCollaborationLink,
getSyncableElements,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
isSavedToFirebase,
@@ -79,7 +77,7 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom } from "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 { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
import { collabErrorIndicatorAtom } from "./CollabError";
+5 -5
View File
@@ -1,15 +1,15 @@
import type {
import {
isSyncableElement,
SocketUpdateData,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} 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 type {
import {
OnUserFollowedPayload,
SocketId,
UserIdleState,
+3 -3
View File
@@ -1,9 +1,9 @@
import React from "react";
import {
loginIcon,
arrowBarToLeftIcon,
ExcalLogo,
} 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 { isExcalidrawPlusSignedUser } from "../app_constants";
import { LanguageList } from "./LanguageList";
@@ -42,7 +42,7 @@ export const AppMainMenu: React.FC<{
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.ItemLink
icon={loginIcon}
icon={arrowBarToLeftIcon}
href={`${import.meta.env.VITE_APP_PLUS_APP}${
isExcalidrawPlusSignedUser ? "" : "/sign-up"
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
@@ -1,5 +1,5 @@
import React from "react";
import { loginIcon } from "../../packages/excalidraw/components/icons";
import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
import { useI18n } from "../../packages/excalidraw/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
@@ -61,7 +61,7 @@ export const AppWelcomeScreen: React.FC<{
import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null}
icon={loginIcon}
icon={arrowBarToLeftIcon}
>
Sign up
</WelcomeScreen.Center.MenuItemLink>
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import type {
import {
FileId,
NonDeletedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import type {
import {
AppState,
BinaryFileData,
BinaryFiles,
+1 -1
View File
@@ -1,7 +1,7 @@
import oc from "open-color";
import React from "react";
import { THEME } from "../../packages/excalidraw/constants";
import type { Theme } from "../../packages/excalidraw/element/types";
import { Theme } from "../../packages/excalidraw/element/types";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
+2 -2
View File
@@ -2,14 +2,14 @@ import { StoreAction } from "../../packages/excalidraw";
import { compressData } from "../../packages/excalidraw/data/encode";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import type {
import {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
InitializedExcalidrawImageElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import type {
import {
BinaryFileData,
BinaryFileMetadata,
ExcalidrawImperativeAPI,
+5 -5
View File
@@ -20,19 +20,19 @@ import {
get,
} from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import type {
import {
ExcalidrawElement,
FileId,
} from "../../packages/excalidraw/element/types";
import type {
import {
AppState,
BinaryFileData,
BinaryFiles,
} 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 { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager";
+5 -6
View File
@@ -1,13 +1,13 @@
import { reconcileElements } from "../../packages/excalidraw";
import type {
import {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
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 type {
import {
AppState,
BinaryFileData,
BinaryFileMetadata,
@@ -20,9 +20,8 @@ import {
decryptData,
} from "../../packages/excalidraw/data/encryption";
import { MIME_TYPES } from "../../packages/excalidraw/constants";
import type { SyncableExcalidrawElement } from ".";
import { getSyncableElements } from ".";
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { Socket } from "socket.io-client";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
+6 -6
View File
@@ -9,30 +9,30 @@ import {
} from "../../packages/excalidraw/data/encryption";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import type {
import {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import type {
import {
AppState,
BinaryFileData,
BinaryFiles,
SocketId,
UserIdleState,
} 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 type { WS_SUBTYPES } from "../app_constants";
import {
DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants";
import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase";
+2 -2
View File
@@ -1,5 +1,5 @@
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { AppState } from "../../packages/excalidraw/types";
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import { AppState } from "../../packages/excalidraw/types";
import {
clearAppStateForLocalStorage,
getDefaultAppState,
+3 -3
View File
@@ -20,7 +20,7 @@
name="description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<meta name="image" content="https://excalidraw.com/og-image-3.png" />
<meta name="image" content="https://excalidraw.com/og-image-2.png" />
<!-- Open Graph / Facebook -->
<meta property="og:site_name" content="Excalidraw" />
@@ -35,7 +35,7 @@
property="og:description"
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<meta property="og:image" content="https://excalidraw.com/og-image-3.png" />
<meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
@@ -51,7 +51,7 @@
/>
<meta
property="twitter:image"
content="https://excalidraw.com/og-image-3.png"
content="https://excalidraw.com/og-twitter-v2.png"
/>
<!-- General tags -->
-4
View File
@@ -40,10 +40,6 @@
}
&.highlighted {
color: var(--color-promo);
font-weight: 700;
.dropdown-menu-item__icon g {
stroke-width: 2;
}
}
}
}
+1 -2
View File
@@ -18,8 +18,7 @@ import {
} from "../../packages/excalidraw/components/icons";
import { TextField } from "../../packages/excalidraw/components/TextField";
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
import type { CollabAPI } from "../collab/Collab";
import { activeRoomLinkAtom } from "../collab/Collab";
import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
import { atom, useAtom, useAtomValue } from "jotai";
import "./ShareDialog.scss";
@@ -216,22 +216,23 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
stroke-width="2"
viewBox="0 0 24 24"
>
<g
stroke-width="1.5"
>
<g>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<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
d="M21 12h-13l3 -3"
d="M10 12l4 4"
/>
<path
d="M11 15l-3 -3"
d="M10 12l4 -4"
/>
<path
d="M4 4l0 16"
/>
</g>
</svg>
+1 -1
View File
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
import { useEffect, useLayoutEffect, useState } from "react";
import { THEME } from "../packages/excalidraw";
import { EVENT } from "../packages/excalidraw/constants";
import type { Theme } from "../packages/excalidraw/element/types";
import { Theme } from "../packages/excalidraw/element/types";
import { CODES, KEYS } from "../packages/excalidraw/keys";
import { STORAGE_KEYS } from "./app_constants";
+3 -4
View File
@@ -1,7 +1,6 @@
{
"private": true,
"name": "excalidraw-monorepo",
"packageManager": "yarn@1.22.22",
"workspaces": [
"excalidraw-app",
"packages/excalidraw",
@@ -61,9 +60,9 @@
"prettier": "@excalidraw/prettier-config",
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
"build:app": "yarn --cwd ./excalidraw-app build:app",
"build:version": "yarn --cwd ./excalidraw-app build:version",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "node ./scripts/build-version.js",
"build": "yarn --cwd ./excalidraw-app build",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
+3 -4
View File
@@ -1,5 +1,4 @@
import type { Alignment } from "../align";
import { alignElements } from "../align";
import { alignElements, Alignment } from "../align";
import {
AlignBottomIcon,
AlignLeftIcon,
@@ -11,13 +10,13 @@ import {
import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { AppClassProperties, AppState, UIAppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -1,8 +1,8 @@
import {
BOUND_TEXT_PADDING,
ROUNDNESS,
TEXT_ALIGN,
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
@@ -23,14 +23,14 @@ import {
isTextBindableContainer,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import type {
import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import type { AppState } from "../types";
import type { Mutable } from "../utility-types";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { arrayToMap, getFontString } from "../utils";
import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex";
@@ -142,7 +142,6 @@ export const actionBindText = register({
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
@@ -297,7 +296,6 @@ export const actionWrapTextInContainer = register({
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
},
false,
);
+3 -3
View File
@@ -18,13 +18,13 @@ import {
ZOOM_STEP,
} from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import type { AppState, NormalizedZoomValue } from "../types";
import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
@@ -35,7 +35,7 @@ import {
isHandToolActive,
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import type { SceneBounds } from "../element/bounds";
import { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor";
import { StoreAction } from "../store";
@@ -4,8 +4,8 @@ import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types";
import type { AppState } from "../types";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -3,17 +3,16 @@ import {
DistributeVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import type { Distribution } from "../distribute";
import { distributeElements } from "../distribute";
import { distributeElements, Distribution } from "../distribute";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import type { AppClassProperties, AppState } from "../types";
import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -1,6 +1,6 @@
import { KEYS } from "../keys";
import { register } from "./register";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
@@ -12,9 +12,9 @@ import {
getSelectedGroupForElement,
getElementsInGroup,
} from "../groups";
import type { AppState } from "../types";
import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import type { ActionResult } from "./types";
import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import {
bindTextToShapeAfterDuplication,
@@ -1,7 +1,7 @@
import { LockedIcon, UnlockedIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { isFrameLikeElement } from "../element/typeChecks";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { StoreAction } from "../store";
+1 -1
View File
@@ -16,7 +16,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import type { Theme } from "../element/types";
import { Theme } from "../element/types";
import "../components/ToolIcon.scss";
import { StoreAction } from "../store";
@@ -13,7 +13,7 @@ import {
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import type { AppState } from "../types";
import { AppState } from "../types";
import { resetCursor } from "../cursor";
import { StoreAction } from "../store";
+9 -10
View File
@@ -1,24 +1,24 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import type {
import {
ExcalidrawElement,
NonDeleted,
NonDeletedSceneElementsMap,
} from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements";
import type { AppClassProperties, AppState } from "../types";
import { AppClassProperties, AppState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
bindOrUnbindLinearElements,
bindOrUnbindSelectedElements,
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons";
import { StoreAction } from "../store";
import { isLinearElement } from "../element/typeChecks";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
@@ -89,6 +89,7 @@ const flipSelectedElements = (
const updatedElements = flipElements(
selectedElements,
elements,
elementsMap,
appState,
flipDirection,
@@ -104,6 +105,7 @@ const flipSelectedElements = (
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
appState: AppState,
flipDirection: "horizontal" | "vertical",
@@ -122,12 +124,9 @@ const flipElements = (
flipDirection === "horizontal" ? minY : maxY,
);
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
app,
isBindingEnabled(appState),
[],
);
isBindingEnabled(appState)
? bindOrUnbindSelectedElements(selectedElements, app)
: unbindLinearElements(selectedElements, elementsMap);
return selectedElements;
};
+2 -2
View File
@@ -1,9 +1,9 @@
import { getNonDeletedElements } from "../element";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameChildren } from "../frame";
import { KEYS } from "../keys";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { AppClassProperties, AppState, UIAppState } from "../types";
import { updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { register } from "./register";
+2 -2
View File
@@ -17,12 +17,12 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import type {
import {
ExcalidrawElement,
ExcalidrawTextElement,
OrderedExcalidrawElement,
} from "../element/types";
import type { AppClassProperties, AppState } from "../types";
import { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
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 { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import type { History } from "../history";
import { HistoryChangedEvent } from "../history";
import type { AppState } from "../types";
import { History, HistoryChangedEvent } from "../history";
import { AppState } from "../types";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
import type { SceneElementsMap } from "../element/types";
import type { Store } from "../store";
import { StoreAction } from "../store";
import { SceneElementsMap } from "../element/types";
import { Store, StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter";
const writeData = (
@@ -65,10 +63,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
new HistoryChangedEvent(),
);
return (
@@ -79,7 +74,6 @@ export const createUndoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isUndoStackEmpty}
data-testid="button-undo"
/>
);
},
@@ -107,10 +101,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
new HistoryChangedEvent(),
);
return (
@@ -121,7 +112,6 @@ export const createRedoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isRedoStackEmpty}
data-testid="button-redo"
/>
);
},
@@ -1,12 +1,9 @@
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks";
import type { ExcalidrawLinearElement } from "../element/types";
import { ExcalidrawLinearElement } from "../element/types";
import { StoreAction } from "../store";
import { register } from "./register";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { lineEditorIcon } from "../components/icons";
export const actionToggleLinearEditor = register({
name: "toggleLinearEditor",
@@ -14,23 +11,18 @@ export const actionToggleLinearEditor = register({
label: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement | undefined;
return selectedElement?.type === "arrow"
? "labels.lineEditor.editArrow"
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement?.id
? "labels.lineEditor.exit"
: "labels.lineEditor.edit";
},
keywords: ["line"],
trackEvent: {
category: "element",
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (
!appState.editingLinearElement &&
selectedElements.length === 1 &&
isLinearElement(selectedElements[0])
) {
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true;
}
return false;
@@ -53,24 +45,4 @@ export const actionToggleLinearEditor = register({
storeAction: StoreAction.CAPTURE,
};
},
PanelComponent: ({ appState, updateData, app }) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement;
const label = t(
selectedElement.type === "arrow"
? "labels.lineEditor.editArrow"
: "labels.lineEditor.edit",
);
return (
<ToolButton
type="button"
icon={lineEditorIcon}
title={label}
aria-label={label}
onClick={() => updateData(null)}
/>
);
},
});
@@ -1,6 +1,6 @@
import { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar";
import type { GoToCollaboratorComponentProps } from "../components/UserList";
import { GoToCollaboratorComponentProps } from "../components/UserList";
import {
eyeIcon,
microphoneIcon,
@@ -8,7 +8,7 @@ import {
} from "../components/icons";
import { t } from "../i18n";
import { StoreAction } from "../store";
import type { Collaborator } from "../types";
import { Collaborator } from "../types";
import { register } from "./register";
import clsx from "clsx";
@@ -1,4 +1,4 @@
import type { AppClassProperties, AppState, Primitive } from "../types";
import { AppClassProperties, AppState, Primitive } from "../types";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -74,7 +74,7 @@ import {
isLinearElement,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import type {
import {
Arrowhead,
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -167,7 +167,7 @@ const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
if (isBoundToContainer(nextElement)) {
return nextElement;
}
return mutateElement(
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import type { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
+1 -1
View File
@@ -24,7 +24,7 @@ import {
isArrowElement,
} from "../element/typeChecks";
import { getSelectedElements } from "../scene";
import type { ExcalidrawTextElement } from "../element/types";
import { ExcalidrawTextElement } from "../element/types";
import { paintIcon } from "../components/icons";
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 { register } from "./register";
import { GRID_SIZE } from "../constants";
import type { AppState } from "../types";
import { AppState } from "../types";
import { gridIcon } from "../components/icons";
import { StoreAction } from "../store";
@@ -20,7 +20,6 @@ import { StoreAction } from "../store";
export const actionSendBackward = register({
name: "sendBackward",
label: "labels.sendBackward",
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -50,7 +49,6 @@ export const actionSendBackward = register({
export const actionBringForward = register({
name: "bringForward",
label: "labels.bringForward",
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -80,7 +78,6 @@ export const actionBringForward = register({
export const actionSendToBack = register({
name: "sendToBack",
label: "labels.sendToBack",
keywords: ["move down", "zindex", "layer"],
icon: SendToBackIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -117,7 +114,6 @@ export const actionSendToBack = register({
export const actionBringToFront = register({
name: "bringToFront",
label: "labels.bringToFront",
keywords: ["move up", "zindex", "layer"],
icon: BringToFrontIcon,
trackEvent: { category: "element" },
+3 -6
View File
@@ -1,5 +1,5 @@
import React from "react";
import type {
import {
Action,
UpdaterFn,
ActionName,
@@ -7,11 +7,8 @@ import type {
PanelComponentProps,
ActionSource,
} from "./types";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type { AppClassProperties, AppState } from "../types";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics";
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[] = [];
+2 -2
View File
@@ -1,8 +1,8 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import type { SubtypeOf } from "../utility-types";
import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import type { ActionName } from "./types";
import { ActionName } from "./types";
export type ShortcutName =
| SubtypeOf<
+6 -11
View File
@@ -1,17 +1,14 @@
import type React from "react";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type {
import React from "react";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import {
AppClassProperties,
AppState,
ExcalidrawProps,
BinaryFiles,
UIAppState,
} from "../types";
import type { MarkOptional } from "../utility-types";
import type { StoreActionType } from "../store";
import { MarkOptional } from "../utility-types";
import { StoreActionType } from "../store";
export type ActionSource =
| "ui"
@@ -134,9 +131,7 @@ export type ActionName =
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer"
| "commandPalette"
| "autoResize"
| "elementStats";
| "commandPalette";
export type PanelComponentProps = {
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 type { BoundingBox } from "./element/bounds";
import { getCommonBoundingBox } from "./element/bounds";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
+3 -4
View File
@@ -1,7 +1,6 @@
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
import { LaserPointer } from "@excalidraw/laser-pointer";
import type { AnimationFrameHandler } from "./animation-frame-handler";
import type { AppState } from "./types";
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
import { AnimationFrameHandler } from "./animation-frame-handler";
import { AppState } from "./types";
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
import type App from "./components/App";
import { SVG_NS } from "./constants";
+1 -1
View File
@@ -7,7 +7,7 @@ import {
EXPORT_SCALES,
THEME,
} from "./constants";
import type { AppState, NormalizedZoomValue } from "./types";
import { AppState, NormalizedZoomValue } from "./types";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio
+18 -23
View File
@@ -1,14 +1,18 @@
import { ENV } from "./constants";
import type { BindableProp, BindingProp } from "./element/binding";
import {
BoundElement,
BindableElement,
BindableProp,
BindingProp,
bindingProperties,
updateBoundElements,
} from "./element/binding";
import { LinearElementEditor } from "./element/linearElementEditor";
import type { ElementUpdate } from "./element/mutateElement";
import { mutateElement, newElementWith } from "./element/mutateElement";
import {
ElementUpdate,
mutateElement,
newElementWith,
} from "./element/mutateElement";
import {
getBoundTextElementId,
redrawTextBoundingBox,
@@ -19,7 +23,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./element/typeChecks";
import type {
import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
@@ -30,13 +34,13 @@ import type {
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups";
import { getObservedAppState } from "./store";
import type {
import {
AppState,
ObservedAppState,
ObservedElementsAppState,
ObservedStandaloneAppState,
} from "./types";
import type { SubtypeOf, ValueOf } from "./utility-types";
import { SubtypeOf, ValueOf } from "./utility-types";
import {
arrayToMap,
arrayToObject,
@@ -1477,28 +1481,19 @@ export class ElementsChange implements Change<SceneElementsMap> {
return elements;
}
const unordered = Array.from(elements.values());
const ordered = orderByFractionalIndex([...unordered]);
const moved = Delta.getRightDifferences(unordered, ordered, true).reduce(
(acc, arrayIndex) => {
const candidate = unordered[Number(arrayIndex)];
if (candidate && changed.has(candidate.id)) {
acc.set(candidate.id, candidate);
}
const previous = Array.from(elements.values());
const reordered = orderByFractionalIndex([...previous]);
return acc;
},
new Map(),
);
if (!flags.containsVisibleDifference && moved.size) {
if (
!flags.containsVisibleDifference &&
Delta.isRightDifferent(previous, reordered, true)
) {
// we found a difference in order!
flags.containsVisibleDifference = true;
}
// synchronize all elements that were actually moved
// could fallback to synchronizing all invalid indices
return arrayToMap(syncMovedIndices(ordered, moved)) as typeof elements;
// let's synchronize all invalid indices of moved elements
return arrayToMap(syncMovedIndices(reordered, changed)) as typeof elements;
}
/**
+6 -2
View File
@@ -1,5 +1,9 @@
import type { Spreadsheet } from "./charts";
import { tryParseCells, tryParseNumber, VALID_SPREADSHEET } from "./charts";
import {
Spreadsheet,
tryParseCells,
tryParseNumber,
VALID_SPREADSHEET,
} from "./charts";
describe("charts", () => {
describe("tryParseNumber", () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
import type { NonDeletedExcalidrawElement } from "./element/types";
import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
+3 -3
View File
@@ -5,13 +5,13 @@ import {
THEME,
} from "./constants";
import { roundRect } from "./renderer/roundRect";
import type { InteractiveCanvasRenderConfig } from "./scene/types";
import type {
import { InteractiveCanvasRenderConfig } from "./scene/types";
import {
Collaborator,
InteractiveCanvasAppState,
SocketId,
UserIdleState,
} from "./types";
import { UserIdleState } from "./types";
function hashToInteger(id: string) {
let hash = 0;
+3 -4
View File
@@ -1,10 +1,9 @@
import type {
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import type { BinaryFiles } from "./types";
import type { Spreadsheet } from "./charts";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import { BinaryFiles } from "./types";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import {
ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES,
+1 -1
View File
@@ -1,5 +1,5 @@
import oc from "open-color";
import type { Merge } from "./utility-types";
import { Merge } from "./utility-types";
// FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
+7 -18
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import type { ActionManager } from "../actions/manager";
import type {
import { ActionManager } from "../actions/manager";
import {
ExcalidrawElement,
ExcalidrawElementType,
NonDeletedElementsMap,
@@ -17,17 +17,13 @@ import {
hasStrokeWidth,
} from "../scene";
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 Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics";
import {
hasBoundTextElement,
isLinearElement,
isTextElement,
} from "../element/typeChecks";
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import { Tooltip } from "./Tooltip";
@@ -118,11 +114,6 @@ export const SelectedShapeActions = ({
const showLinkIcon =
targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction =
!appState.editingLinearElement &&
targetElements.length === 1 &&
isLinearElement(targetElements[0]);
return (
<div className="panelColumn">
<div>
@@ -182,8 +173,8 @@ export const SelectedShapeActions = ({
<div className="buttonList">
{renderAction("sendToBack")}
{renderAction("sendBackward")}
{renderAction("bringForward")}
{renderAction("bringToFront")}
{renderAction("bringForward")}
</div>
</fieldset>
@@ -238,7 +229,6 @@ export const SelectedShapeActions = ({
{renderAction("group")}
{renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")}
{showLineEditorAction && renderAction("toggleLinearEditor")}
</div>
</fieldset>
)}
@@ -343,8 +333,8 @@ export const ShapesSwitcher = ({
fontSize: 8,
fontFamily: "Cascadia, monospace",
position: "absolute",
background: "var(--color-promo)",
color: "var(--color-surface-lowest)",
background: "pink",
color: "black",
bottom: 3,
right: 4,
}}
@@ -468,7 +458,6 @@ export const ExitZenModeAction = ({
showExitZenModeBtn: boolean;
}) => (
<button
type="button"
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
+113 -163
View File
@@ -1,7 +1,7 @@
import React, { useContext } from "react";
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 clsx from "clsx";
import { nanoid } from "nanoid";
@@ -39,16 +39,18 @@ import {
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import type { Action, ActionResult } from "../actions/types";
import { Action, ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
import type { PastedMixedContent } from "../clipboard";
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
import type { EXPORT_IMAGE_TYPES } from "../constants";
import {
PastedMixedContent,
copyTextToSystemClipboard,
parseClipboard,
} from "../clipboard";
import {
APP_NAME,
CURSOR_TYPE,
@@ -60,6 +62,7 @@ import {
ENV,
EVENT,
FRAME_STYLE,
EXPORT_IMAGE_TYPES,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
@@ -88,10 +91,8 @@ import {
isIOS,
supportsResizeObserver,
DEFAULT_COLLISION_THRESHOLD,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import type { ExportedElements } from "../data";
import { exportCanvas, loadFromBlob } from "../data";
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore";
import {
@@ -115,22 +116,23 @@ import {
newTextElement,
newImageElement,
transformElements,
refreshTextDimensions,
updateTextElement,
redrawTextBoundingBox,
getElementAbsoluteCoords,
} from "../element";
import {
bindOrUnbindLinearElement,
bindOrUnbindLinearElements,
bindOrUnbindSelectedElements,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
getEligibleElementsForBinding,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
unbindLinearElements,
updateBoundElements,
getSuggestedBindingsForArrows,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -162,7 +164,7 @@ import {
isMagicFrameElement,
isTextBindableContainer,
} from "../element/typeChecks";
import type {
import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
@@ -219,14 +221,11 @@ import {
isSomeElementSelected,
} from "../scene";
import Scene from "../scene/Scene";
import type {
RenderInteractiveSceneCallback,
ScrollBars,
} from "../scene/types";
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
import { getStateForZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes";
import type { GeometricShape } from "../../utils/geometry/shape";
import {
GeometricShape,
getClosedCurveShape,
getCurveShape,
getEllipseShape,
@@ -235,7 +234,7 @@ import {
getSelectionBoxShape,
} from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision";
import type {
import {
AppClassProperties,
AppProps,
AppState,
@@ -293,8 +292,11 @@ import {
maybeParseEmbedSrc,
getEmbedLink,
} from "../element/embeddable";
import type { ContextMenuItems } from "./ContextMenu";
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
import {
ContextMenu,
ContextMenuItems,
CONTEXT_MENU_SEPARATOR,
} from "./ContextMenu";
import LayerUI from "./LayerUI";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
@@ -319,8 +321,7 @@ import {
updateImageCache as _updateImageCache,
} from "../element/image";
import throttle from "lodash.throttle";
import type { FileSystemHandle } from "../data/filesystem";
import { fileOpen } from "../data/filesystem";
import { fileOpen, FileSystemHandle } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
@@ -332,8 +333,6 @@ import {
getLineHeightInPx,
isMeasureTextSupported,
isValidTextContainer,
measureText,
wrapText,
} from "../element/textElement";
import {
showHyperlinkTooltip,
@@ -388,9 +387,11 @@ import {
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper";
import type { ExcalidrawElementSkeleton } from "../data/transform";
import { convertToExcalidrawElements } from "../data/transform";
import type { ValueOf } from "../utility-types";
import {
ExcalidrawElementSkeleton,
convertToExcalidrawElements,
} from "../data/transform";
import { ValueOf } from "../utility-types";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { StaticCanvas, InteractiveCanvas } from "./canvases";
import { Renderer } from "../scene/Renderer";
@@ -404,8 +405,7 @@ import {
} from "../cursor";
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
import type { MagicCacheData } from "../data/magic";
import { diagramToHTML } from "../data/magic";
import { MagicCacheData, diagramToHTML } from "../data/magic";
import { exportToBlob } from "../../utils/export";
import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton";
@@ -432,9 +432,6 @@ import {
isPointHittingLinkIcon,
} from "./hyperlink/helpers";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
import { getVisibleSceneBounds } from "../element/bounds";
import { Stats } from "./Stats";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@@ -546,7 +543,7 @@ class App extends React.Component<AppProps, AppState> {
public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined;
public id: string;
store: Store;
private store: Store;
private history: History;
private excalidrawContainerValue: {
container: HTMLDivElement | null;
@@ -720,7 +717,10 @@ class App extends React.Component<AppProps, AppState> {
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.actionManager.registerAll(actions);
@@ -943,7 +943,7 @@ class App extends React.Component<AppProps, AppState> {
});
if (updated) {
this.scene.triggerUpdate();
this.scene.informMutation();
}
// GC
@@ -1455,10 +1455,10 @@ class App extends React.Component<AppProps, AppState> {
const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props;
const sceneNonce = this.scene.getSceneNonce();
const versionNonce = this.scene.getVersionNonce();
const { elementsMap, visibleElements } =
this.renderer.getRenderableElements({
sceneNonce,
versionNonce,
zoom: this.state.zoom,
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
@@ -1670,26 +1670,13 @@ class App extends React.Component<AppProps, AppState> {
}}
/>
)}
{this.state.showStats && (
<Stats
appState={this.state}
setAppState={this.setState}
scene={this.scene}
onClose={() => {
this.actionManager.executeAction(
actionToggleStats,
);
}}
renderCustomStats={renderCustomStats}
/>
)}
<StaticCanvas
canvas={this.canvas}
rc={this.rc}
elementsMap={elementsMap}
allElementsMap={allElementsMap}
visibleElements={visibleElements}
sceneNonce={sceneNonce}
versionNonce={versionNonce}
selectionNonce={
this.state.selectionElement?.versionNonce
}
@@ -1711,7 +1698,7 @@ class App extends React.Component<AppProps, AppState> {
elementsMap={elementsMap}
visibleElements={visibleElements}
selectedElements={selectedElements}
sceneNonce={sceneNonce}
versionNonce={versionNonce}
selectionNonce={
this.state.selectionElement?.versionNonce
}
@@ -1835,7 +1822,7 @@ class App extends React.Component<AppProps, AppState> {
);
}
this.magicGenerations.set(frameElement.id, data);
this.triggerRender();
this.onSceneUpdated();
};
private getTextFromElements(elements: readonly ExcalidrawElement[]) {
@@ -2460,7 +2447,7 @@ class App extends React.Component<AppProps, AppState> {
this.history.record(increment.elementsChange, increment.appStateChange);
});
this.scene.onUpdate(this.triggerRender);
this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners();
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
@@ -2505,7 +2492,6 @@ class App extends React.Component<AppProps, AppState> {
public componentWillUnmount() {
this.renderer.destroy();
this.scene = new Scene();
this.fonts = new Fonts({ scene: this.scene });
this.renderer = new Renderer(this.scene);
this.files = {};
this.imageCache.clear();
@@ -2583,7 +2569,7 @@ class App extends React.Component<AppProps, AppState> {
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
addEventListener(
document,
EVENT.POINTER_MOVE,
EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition,
),
// rerender text elements on font load to fix #637 && #1553
@@ -2612,9 +2598,6 @@ class App extends React.Component<AppProps, AppState> {
),
addEventListener(window, EVENT.FOCUS, () => {
this.maybeCleanupAfterMissingPointerUp(null);
// browsers (chrome?) tend to free up memory a lot, which results
// in canvas context being cleared. Thus re-render on focus.
this.triggerRender(true);
}),
);
@@ -3359,53 +3342,32 @@ class App extends React.Component<AppProps, AppState> {
text,
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
textAlign: DEFAULT_TEXT_ALIGN,
textAlign: this.state.currentItemTextAlign,
verticalAlign: DEFAULT_VERTICAL_ALIGN,
locked: false,
};
const fontString = getFontString({
fontSize: textElementProps.fontSize,
fontFamily: textElementProps.fontFamily,
});
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
const [x1, , x2] = getVisibleSceneBounds(this.state);
// long texts should not go beyond 800 pixels in width nor should it go below 200 px
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
const LINE_GAP = 10;
let currentY = y;
const lines = isPlainPaste ? [text] : text.split("\n");
const textElements = lines.reduce(
(acc: ExcalidrawTextElement[], line, idx) => {
const originalText = line.trim();
if (originalText.length) {
const text = line.trim();
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x,
y: currentY,
});
let metrics = measureText(originalText, fontString, lineHeight);
const isTextWrapped = metrics.width > maxTextWidth;
const text = isTextWrapped
? wrapText(originalText, fontString, maxTextWidth)
: originalText;
metrics = isTextWrapped
? measureText(text, fontString, lineHeight)
: metrics;
const startX = x - metrics.width / 2;
const startY = currentY - metrics.height / 2;
const element = newTextElement({
...textElementProps,
x: startX,
y: startY,
x,
y: currentY,
text,
originalText,
lineHeight,
autoResize: !isTextWrapped,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
acc.push(element);
@@ -3711,7 +3673,7 @@ class App extends React.Component<AppProps, AppState> {
ShapeCache.delete(element);
}
});
this.scene.triggerUpdate();
this.scene.informMutation();
this.addNewImagesToImageCache();
},
@@ -3722,7 +3684,7 @@ class App extends React.Component<AppProps, AppState> {
elements?: SceneData["elements"];
appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"];
/** @default StoreAction.NONE */
/** @default StoreAction.CAPTURE */
storeAction?: SceneData["storeAction"];
}) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
@@ -3771,15 +3733,8 @@ class App extends React.Component<AppProps, AppState> {
},
);
private triggerRender = (
/** force always re-renders canvas even if no change */
force?: boolean,
) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({});
}
private onSceneUpdated = () => {
this.setState({});
};
/**
@@ -3983,12 +3938,7 @@ class App extends React.Component<AppProps, AppState> {
});
});
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
this.maybeSuggestBindingForAll(selectedElements);
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
@@ -4155,12 +4105,11 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements, this)
: unbindLinearElements(selectedElements, elementsMap);
this.setState({ suggestedBindings: [] });
}
});
@@ -4348,22 +4297,25 @@ class App extends React.Component<AppProps, AppState> {
) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
const updateElement = (
text: string,
originalText: string,
isDeleted: boolean,
) => {
this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return newElementWith(_element, {
originalText: nextOriginalText,
isDeleted: isDeleted ?? _element.isDeleted,
// returns (wrapped) text and new dimensions
...refreshTextDimensions(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
nextOriginalText,
),
});
return updateTextElement(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
{
text,
isDeleted,
originalText,
},
);
}
return _element;
}),
@@ -4386,15 +4338,15 @@ class App extends React.Component<AppProps, AppState> {
viewportY - this.state.offsetTop,
];
},
onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false);
onChange: withBatchedUpdates((text) => {
updateElement(text, text, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element, elementsMap);
}
}),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
const isDeleted = !nextOriginalText.trim();
updateElement(nextOriginalText, isDeleted);
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
const isDeleted = !text.trim();
updateElement(text, originalText, isDeleted);
// select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) {
@@ -4439,7 +4391,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.originalText, false);
updateElement(element.text, element.originalText, false);
}
private deselectElements() {
@@ -5146,11 +5098,8 @@ class App extends React.Component<AppProps, AppState> {
this.translateCanvas({
zoom: zoomState.zoom,
// 2x multiplier is just a magic number that makes this work correctly
// on touchscreen devices (note: if we get report that panning is slower/faster
// than actual movement, consider swapping with devicePixelRatio)
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
scrollX: zoomState.scrollX + deltaX / nextZoom,
scrollY: zoomState.scrollY + deltaY / nextZoom,
shouldCacheIgnoreZoom: true,
});
});
@@ -5625,7 +5574,7 @@ class App extends React.Component<AppProps, AppState> {
}
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.triggerRender();
this.onSceneUpdated();
}
};
@@ -7504,12 +7453,7 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
this.maybeSuggestBindingForAll(selectedElements);
// We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
@@ -8117,7 +8061,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(),
);
this.scene.triggerUpdate();
this.scene.informMutation();
}
}
}
@@ -8556,18 +8500,15 @@ class App extends React.Component<AppProps, AppState> {
}
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
// We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end").
const linearElements = this.scene
.getSelectedElements(this.state)
.filter(isLinearElement);
bindOrUnbindLinearElements(
linearElements,
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(
this.scene.getSelectedElements(this.state),
this,
)
: unbindLinearElements(
this.scene.getSelectedElements(this.state),
elementsMap,
);
}
if (activeTool.type === "laser") {
@@ -8612,7 +8553,7 @@ class App extends React.Component<AppProps, AppState> {
private restoreReadyToEraseElements = () => {
this.elementsPendingErasure = new Set();
this.triggerRender();
this.onSceneUpdated();
};
private eraseElements = () => {
@@ -9026,7 +8967,7 @@ class App extends React.Component<AppProps, AppState> {
files,
);
if (updatedFiles.size) {
this.scene.triggerUpdate();
this.scene.informMutation();
}
}
};
@@ -9099,6 +9040,19 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings });
};
private maybeSuggestBindingForAll(
selectedElements: NonDeleted<ExcalidrawElement>[],
): void {
if (selectedElements.length > 50) {
return;
}
const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this,
);
this.setState({ suggestedBindings });
}
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -9485,6 +9439,8 @@ class App extends React.Component<AppProps, AppState> {
this.state.originSnapOffset,
);
this.maybeSuggestBindingForAll([draggingElement]);
// highlight elements that are to be added to frames on frames creation
if (
this.state.activeTool.type === TOOL_TYPE.frame ||
@@ -9607,10 +9563,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.resize.center.y,
)
) {
const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements,
this,
);
this.maybeSuggestBindingForAll(selectedElements);
const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => {
@@ -9624,7 +9577,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({
elementsToHighlight: [...elementsToHighlight],
suggestedBindings,
});
return true;
@@ -9681,7 +9633,6 @@ class App extends React.Component<AppProps, AppState> {
}
return [
CONTEXT_MENU_SEPARATOR,
actionCut,
actionCopy,
actionPaste,
@@ -9694,7 +9645,6 @@ class App extends React.Component<AppProps, AppState> {
actionPasteStyles,
CONTEXT_MENU_SEPARATOR,
actionGroup,
actionTextAutoResize,
actionUnbindText,
actionBindText,
actionWrapTextInContainer,
@@ -28,7 +28,6 @@ export const ButtonIconSelect = <T extends Object>(
{props.options.map((option) =>
props.type === "button" ? (
<button
type="button"
key={option.text}
onClick={(event) => props.onClick(option.value, event)}
className={clsx({
@@ -22,12 +22,7 @@ export const CheckboxItem: React.FC<{
).focus();
}}
>
<button
type="button"
className="Checkbox-box"
role="checkbox"
aria-checked={checked}
>
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
{checkIcon}
</button>
<div className="Checkbox-label">{children}</div>
@@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker";
import { useAtom } from "jotai";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import {
ColorPickerType,
activeColorPickerSectionAtom,
} from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys";
@@ -1,15 +1,16 @@
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
import type { ExcalidrawElement } from "../../element/types";
import type { AppState } from "../../types";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { TopPicks } from "./TopPicks";
import { Picker } from "./Picker";
import * as Popover from "@radix-ui/react-popover";
import { useAtom } from "jotai";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import {
activeColorPickerSectionAtom,
ColorPickerType,
} from "./colorPickerUtils";
import { useDevice, useExcalidrawContainer } from "../App";
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
import { COLOR_PALETTE } from "../../colors";
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
import PickerHeading from "./PickerHeading";
import { t } from "../../i18n";
import clsx from "clsx";
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { t } from "../../i18n";
import type { ExcalidrawElement } from "../../element/types";
import { ExcalidrawElement } from "../../element/types";
import { ShadeList } from "./ShadeList";
import PickerColorList from "./PickerColorList";
@@ -9,15 +9,15 @@ import { useAtom } from "jotai";
import { CustomColorList } from "./CustomColorList";
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import PickerHeading from "./PickerHeading";
import type { ColorPickerType } from "./colorPickerUtils";
import {
ColorPickerType,
activeColorPickerSectionAtom,
getColorNameAndShadeFromColor,
getMostUsedCustomColors,
isCustomColor,
} from "./colorPickerUtils";
import type { ColorPaletteCustom } from "../../colors";
import {
ColorPaletteCustom,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../../colors";
@@ -7,9 +7,8 @@ import {
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import type { ColorPaletteCustom } from "../../colors";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
import { ColorPaletteCustom } from "../../colors";
import { TranslationKeys, t } from "../../i18n";
interface PickerColorListProps {
palette: ColorPaletteCustom;
@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import { ReactNode } from "react";
const PickerHeading = ({ children }: { children: ReactNode }) => (
<div className="color-picker__heading">{children}</div>
@@ -7,7 +7,7 @@ import {
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { t } from "../../i18n";
import type { ColorPaletteCustom } from "../../colors";
import { ColorPaletteCustom } from "../../colors";
interface ShadeListProps {
hex: string;
@@ -1,5 +1,5 @@
import clsx from "clsx";
import type { ColorPickerType } from "./colorPickerUtils";
import { ColorPickerType } from "./colorPickerUtils";
import {
DEFAULT_CANVAS_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 type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
import {
ColorPickerColor,
ColorPaletteCustom,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
} from "../../colors";
export const getColorNameAndShadeFromColor = ({
palette,
@@ -1,13 +1,14 @@
import { KEYS } from "../../keys";
import type {
import {
ColorPickerColor,
ColorPalette,
ColorPaletteCustom,
COLORS_PER_ROW,
COLOR_PALETTE,
} from "../../colors";
import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
import type { ValueOf } from "../../utility-types";
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
import { ValueOf } from "../../utility-types";
import {
ActiveColorPickerSectionAtomType,
colorPickerHotkeyBindings,
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
@@ -10,11 +10,12 @@ import { Dialog } from "../Dialog";
import { TextField } from "../TextField";
import clsx from "clsx";
import { getSelectedElements } from "../../scene";
import type { Action } from "../../actions/types";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
import type { ShortcutName } from "../../actions/shortcuts";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { Action } from "../../actions/types";
import { TranslationKeys, t } from "../../i18n";
import {
ShortcutName,
getShortcutFromShortcutName,
} from "../../actions/shortcuts";
import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
import {
LockedIcon,
@@ -30,7 +31,7 @@ import {
} from "../icons";
import fuzzy from "fuzzy";
import { useUIAppState } from "../../context/ui-appState";
import type { AppProps, AppState, UIAppState } from "../../types";
import { AppProps, AppState, UIAppState } from "../../types";
import {
capitalizeString,
getShortcutKey,
@@ -38,7 +39,7 @@ import {
} from "../../utils";
import { atom, useAtom } from "jotai";
import { deburr } from "../../deburr";
import type { MarkRequired } from "../../utility-types";
import { MarkRequired } from "../../utility-types";
import { InlineIcon } from "../InlineIcon";
import { SHAPES } from "../../shapes";
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
@@ -46,7 +47,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { actionClearCanvas, actionLink } from "../../actions";
import { jotaiStore } from "../../jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import type { CommandPaletteItem } from "./types";
import { CommandPaletteItem } from "./types";
import * as defaultItems from "./defaultCommandPaletteItems";
import { trackEvent } from "../../analytics";
import { useStable } from "../../hooks/useStable";
@@ -257,10 +258,10 @@ function CommandPaletteInner({
actionManager.actions.deleteSelectedElements,
actionManager.actions.copyStyles,
actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,
actionManager.actions.bringForward,
actionManager.actions.sendBackward,
actionManager.actions.sendToBack,
actionManager.actions.bringForward,
actionManager.actions.bringToFront,
actionManager.actions.alignTop,
actionManager.actions.alignBottom,
actionManager.actions.alignLeft,
@@ -540,7 +541,7 @@ function CommandPaletteInner({
...command,
icon: command.icon || boltIcon,
order: command.order ?? getCategoryOrder(command.category),
haystack: `${deburr(command.label.toLocaleLowerCase())} ${
haystack: `${deburr(command.label)} ${
command.keywords?.join(" ") || ""
}`,
};
@@ -777,9 +778,7 @@ function CommandPaletteInner({
return;
}
const _query = deburr(
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, ""),
);
const _query = deburr(commandSearch.replace(/[<>-_| ]/g, ""));
matchingCommands = fuzzy
.filter(_query, matchingCommands, {
extract: (command) => command.haystack,
@@ -1,5 +1,5 @@
import { actionToggleTheme } from "../../actions";
import type { CommandPaletteItem } from "./types";
import { CommandPaletteItem } from "./types";
export const toggleTheme: CommandPaletteItem = {
...actionToggleTheme,
@@ -1,6 +1,6 @@
import type { ActionManager } from "../../actions/manager";
import type { Action } from "../../actions/types";
import type { UIAppState } from "../../types";
import { ActionManager } from "../../actions/manager";
import { Action } from "../../actions/types";
import { UIAppState } from "../../types";
export type CommandPaletteItem = {
label: string;
@@ -1,6 +1,5 @@
import { t } from "../i18n";
import type { DialogProps } from "./Dialog";
import { Dialog } from "./Dialog";
import { Dialog, DialogProps } from "./Dialog";
import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton";
@@ -1,13 +1,14 @@
import clsx from "clsx";
import { Popover } from "./Popover";
import type { TranslationKeys } from "../i18n";
import { t } from "../i18n";
import { t, TranslationKeys } from "../i18n";
import "./ContextMenu.scss";
import type { ShortcutName } from "../actions/shortcuts";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import type { Action } from "../actions/types";
import type { ActionManager } from "../actions/manager";
import {
getShortcutFromShortcutName,
ShortcutName,
} from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
import React from "react";
@@ -105,7 +106,6 @@ export const ContextMenu = React.memo(
}}
>
<button
type="button"
className={clsx("context-menu-item", {
dangerous: actionName === "deleteSelectedElements",
checkmark: item.checked?.(appState),
@@ -3,7 +3,7 @@ import "./ToolIcon.scss";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import { THEME } from "../constants";
import 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,
// but this could be added in the future.
@@ -3,12 +3,12 @@ import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n";
import type { MarkOptional, Merge } from "../utility-types";
import { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu";
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback(
@@ -123,7 +123,6 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")}
type="button"
>
{CloseIcon}
</button>
@@ -1,5 +1,5 @@
import clsx from "clsx";
import type { ReactNode } from "react";
import { ReactNode } from "react";
import "./DialogActionButton.scss";
import Spinner from "./Spinner";
@@ -12,8 +12,8 @@ import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import { useStable } from "../hooks/useStable";
import "./EyeDropper.scss";
import type { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import type { ExcalidrawElement } from "../element/types";
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import { ExcalidrawElement } from "../element/types";
export type EyeDropperProperties = {
keepOpenOnAlt: boolean;
@@ -1,4 +1,4 @@
import type { UserToFollow } from "../../types";
import { UserToFollow } from "../../types";
import { CloseIcon } from "../icons";
import "./FollowMode.scss";
@@ -27,11 +27,7 @@ const FollowMode = ({
{userToFollow.username}
</span>
</div>
<button
type="button"
onClick={onDisconnect}
className="follow-mode__disconnect-btn"
>
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
{CloseIcon}
</button>
</div>
@@ -1,5 +1,5 @@
import { t } from "../i18n";
import type { AppClassProperties, Device, UIAppState } from "../types";
import { AppClassProperties, Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@@ -108,7 +108,6 @@ function Picker<T>({
<div className="picker-content" ref={rGallery}>
{options.map((option, i) => (
<button
type="button"
className={clsx("picker-option", {
active: value === option.value,
})}
@@ -172,7 +171,6 @@ export function IconPicker<T>({
<div>
<button
name={group}
type="button"
className={isActive ? "active" : ""}
aria-label={label}
onClick={() => setActive(!isActive)}
@@ -20,7 +20,7 @@ import {
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../../utils/export";
@@ -1,9 +1,8 @@
import React, { useEffect, useState } from "react";
import { LoadingMessage } from "./LoadingMessage";
import type { Language } from "../i18n";
import { defaultLang, languages, setLanguage } from "../i18n";
import type { Theme } from "../element/types";
import { defaultLang, Language, languages, setLanguage } from "../i18n";
import { Theme } from "../element/types";
interface Props {
langCode: Language["code"];
@@ -1,8 +1,8 @@
import React from "react";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import type { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton";
@@ -12,7 +12,7 @@ import { Card } from "./Card";
import "./ExportDialog.scss";
import { nativeFileSystemSupported } from "../data/filesystem";
import { trackEvent } from "../analytics";
import type { ActionManager } from "../actions/manager";
import { ActionManager } from "../actions/manager";
import { getFrame } from "../utils";
export type ExportCB = (
@@ -1,7 +1,7 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import type { ToolButtonSize } from "./ToolButton";
import { ToolButtonSize } from "./ToolButton";
import { laserPointerToolIcon } from "./icons";
type LaserPointerIconProps = {
+18 -7
View File
@@ -1,6 +1,6 @@
import clsx from "clsx";
import React from "react";
import type { ActionManager } from "../actions/manager";
import { ActionManager } from "../actions/manager";
import {
CLASSES,
DEFAULT_SIDEBAR,
@@ -8,11 +8,10 @@ import {
TOOL_TYPE,
} from "../constants";
import { showSelectedShapeActions } from "../element";
import type { NonDeletedExcalidrawElement } from "../element/types";
import type { Language } from "../i18n";
import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import type {
import {
AppProps,
AppState,
ExcalidrawProps,
@@ -39,6 +38,8 @@ import { JSONExportDialog } from "./JSONExportDialog";
import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai";
@@ -442,7 +443,7 @@ const LayerUI = ({
);
ShapeCache.delete(element);
}
Scene.getScene(selectedElements[0])?.triggerUpdate();
Scene.getScene(selectedElements[0])?.informMutation();
} else if (colorPickerType === "elementBackground") {
setAppState({
currentItemBackgroundColor: color,
@@ -540,9 +541,19 @@ const LayerUI = ({
showExitZenModeBtn={showExitZenModeBtn}
renderWelcomeScreen={renderWelcomeScreen}
/>
{appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
{appState.scrolledOutside && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
@@ -1,12 +1,11 @@
import React, { useState, useCallback, useMemo, useRef } from "react";
import type Library from "../data/library";
import {
import Library, {
distributeLibraryItemsOnSquareGrid,
libraryItemsAtom,
} from "../data/library";
import { t } from "../i18n";
import { randomId } from "../random";
import type {
import {
LibraryItems,
LibraryItem,
ExcalidrawProps,
@@ -29,7 +28,7 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants";
export const isLibraryMenuOpenAtom = atom(false);
@@ -1,6 +1,6 @@
import { VERSIONS } from "../constants";
import { t } from "../i18n";
import type { ExcalidrawProps, UIAppState } from "../types";
import { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({
theme,
@@ -1,4 +1,4 @@
import type { ExcalidrawProps, UIAppState } from "../types";
import { ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx";
@@ -2,11 +2,10 @@ import { useCallback, useState } from "react";
import { t } from "../i18n";
import Trans from "./Trans";
import { jotaiScope } from "../jotai";
import type { LibraryItem, LibraryItems, UIAppState } from "../types";
import { LibraryItem, LibraryItems, UIAppState } from "../types";
import { useApp, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json";
import type Library from "../data/library";
import { libraryItemsAtom } from "../data/library";
import Library, { libraryItemsAtom } from "../data/library";
import {
DotsIcon,
ExportIcon,
@@ -7,7 +7,7 @@ import React, {
} from "react";
import { serializeLibraryAsJSON } from "../data/json";
import { t } from "../i18n";
import type {
import {
ExcalidrawProps,
LibraryItem,
LibraryItems,
@@ -1,9 +1,8 @@
import type { ReactNode } from "react";
import React, { memo, useEffect, useState } from "react";
import React, { memo, ReactNode, useEffect, useState } from "react";
import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
import type { LibraryItem } from "../types";
import type { ExcalidrawElement, NonDeleted } from "../element/types";
import type { SvgCache } from "../hooks/useLibraryItemSvg";
import { LibraryItem } from "../types";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { SvgCache } from "../hooks/useLibraryItemSvg";
import { useTransition } from "../hooks/useTransition";
type LibraryOrPendingItem = (
@@ -1,12 +1,11 @@
import clsx from "clsx";
import { memo, useEffect, useRef, useState } from "react";
import { useDevice } from "./App";
import type { LibraryItem } from "../types";
import { LibraryItem } from "../types";
import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem";
import { PlusIcon } from "./icons";
import type { SvgCache } from "../hooks/useLibraryItemSvg";
import { useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
export const LibraryUnit = memo(
({
@@ -3,7 +3,7 @@ import { useState, useEffect } from "react";
import Spinner from "./Spinner";
import clsx from "clsx";
import { THEME } from "../constants";
import type { Theme } from "../element/types";
import { Theme } from "../element/types";
export const LoadingMessage: React.FC<{ delay?: number; theme?: Theme }> = ({
delay,
@@ -1,7 +1,7 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import type { ToolButtonSize } from "./ToolButton";
import { ToolButtonSize } from "./ToolButton";
import { LockedIcon, UnlockedIcon } from "./icons";
type LockIconProps = {
@@ -1,7 +1,7 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import type { ToolButtonSize } from "./ToolButton";
import { ToolButtonSize } from "./ToolButton";
const DEFAULT_SIZE: ToolButtonSize = "small";

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