Compare commits

..

6 Commits

Author SHA1 Message Date
Marcel Mraz 77c4eb6db4 Multiple base url fallbacks 2024-07-31 18:17:31 +02:00
Marcel Mraz 1ac2626e47 Docs for font picker 2024-07-24 19:50:59 +02:00
Marcel Mraz 43d6a7e286 Docs for font picker 2024-07-17 17:51:04 +02:00
Marcel Mraz 15b7d141c1 Fractional indices normalization docs 2024-05-21 16:16:01 +01:00
Marcel Mraz 550e23a2ab Expose StoreAction instead of StoreActionType 2024-04-22 11:01:04 +01:00
Marcel Mraz d4c6462ab1 Adding initial storeAction documentation 2024-04-18 23:16:51 +01:00
289 changed files with 6937 additions and 10077 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 -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" }]
} }
} }
+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
@@ -8,15 +8,15 @@
import { FONT_FAMILY } from "@excalidraw/excalidraw"; import { FONT_FAMILY } from "@excalidraw/excalidraw";
``` ```
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below `FONT_FAMILY` contains all the font families used in `Excalidraw`. The default families are the following:
| Font Family | Description | | Font Family | Description |
| ----------- | ---------------------- | | ----------- | ---------------------- |
| `Virgil` | The `handwritten` font | | `Excalifont` | The `Hand-drawn` font |
| `Helvetica` | The `Normal` Font | | `Nunito` | The `Normal` Font |
| `Cascadia` | The `Code` Font | | `Comic Shanns` | The `Code` Font |
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`. Pre-selected family is `FONT_FAMILY.Excalifont`, unless it's overriden with `initialData.appState.currentItemFontFamily`.
### THEME ### THEME
@@ -65,7 +65,7 @@ You can use this function to update the scene with the sceneData. It accepts the
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene | | `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene |
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. | | `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. | | `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
| `commitToStore` | `boolean` | Implies if the change should be captured and commited to the `store`. Commited changes are emmitted and listened to by other components, such as `History` for undo / redo purposes. Defaults to `false`. | | `storeAction` | [`StoreAction`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/store.ts#L40) | Parameter to control which updates should be captured by the `Store`. Captured updates are emmitted as increments and listened to by other components, such as `History` for undo / redo purposes. |
```jsx live ```jsx live
function App() { function App() {
@@ -105,6 +105,7 @@ function App() {
appState: { appState: {
viewBackgroundColor: "#edf2ff", viewBackgroundColor: "#edf2ff",
}, },
storeAction: StoreAction.CAPTURE,
}; };
excalidrawAPI.updateScene(sceneData); excalidrawAPI.updateScene(sceneData);
}; };
@@ -115,12 +116,25 @@ function App() {
<button className="custom-button" onClick={updateScene}> <button className="custom-button" onClick={updateScene}>
Update Scene Update Scene
</button> </button>
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} /> <Excalidraw ref={(api) => setExcalidrawAPI(api)} />
</div> </div>
); );
} }
``` ```
#### storeAction
You can use the `storeAction` to influence undo / redo behaviour.
> **NOTE**: Some updates are not observed by the store / history - i.e. updates to `collaborators` object or parts of `AppState` which are not observed (not `ObservedAppState`). Such updates will never make it to the undo / redo stacks, regardless of the passed `storeAction` value.
| | `storeAction` value | Notes |
| --- | --- | --- |
| _Immediately undoable_ | `StoreAction.CAPTURE` | Use for updates which should be captured. Should be used for most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
| _Eventually undoable_ | `StoreAction.NONE` | Use for updates which should not be captured immediately - likely exceptions which are part of some async multi-step process. Otherwise, all such updates would end up being captured with the next `StoreAction.CAPTURE` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
| _Never undoable_ | `StoreAction.UPDATE` | Use for updates which should never be recorded, such as remote updates or scene initialization. These updates will _never_ make it to the local undo / redo stacks. |
### updateLibrary ### updateLibrary
<pre> <pre>
@@ -188,7 +202,7 @@ function App() {
Update Library Update Library
</button> </button>
<Excalidraw <Excalidraw
excalidrawAPI={(api) => setExcalidrawAPI(api)} ref={(api) => setExcalidrawAPI(api)}
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
initialData={{ initialData={{
libraryItems: initialData.libraryItems, libraryItems: initialData.libraryItems,
@@ -31,7 +31,7 @@ You can pass `null` / `undefined` if not applicable.
restoreElements( restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp; elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp; localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/> opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean, normalizeIndices?: boolean }<br/>
) )
</pre> </pre>
@@ -51,8 +51,9 @@ The extra optional parameter to configure restored elements. It has the followin
| Prop | Type | Description| | Prop | Type | Description|
| --- | --- | ------| | --- | --- | ------|
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. | | `refreshDimensions` | `boolean` | Indicates whether we should also _recalculate_ text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. | | `repairBindings` |`boolean` | Indicates whether the _bindings_ for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
| `normalizeIndices` |`boolean` | Indicates whether _fractional indices_ for the elements should be normalized. This is to prevent possible issues caused by using stale (too old, too long) indices. |
**_How to use_** **_How to use_**
@@ -73,7 +74,7 @@ restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp; data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/> localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/> opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean, normalizeIndices?: boolean }<br/>
) )
</pre> </pre>
@@ -24,7 +24,7 @@ yarn add react react-dom @excalidraw/excalidraw
Excalidraw depends on assets such as localization files (if you opt to use them), fonts, and others. Excalidraw depends on assets such as localization files (if you opt to use them), fonts, and others.
By default these assets are loaded from a public CDN [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist), so you don't need to do anything on your end. By default these assets are loaded from a public CDN [`https://unpkg.com/@excalidraw/excalidraw/dist/prod/`](https://unpkg.com/@excalidraw/excalidraw/dist/prod/), so you don't need to do anything on your end.
However, if you want to host these files yourself, you can find them in your `node_modules/@excalidraw/excalidraw/dist` directory, in folders `excalidraw-assets` (for production) and `excalidraw-assets-dev` (for development). However, if you want to host these files yourself, you can find them in your `node_modules/@excalidraw/excalidraw/dist` directory, in folders `excalidraw-assets` (for production) and `excalidraw-assets-dev` (for development).
@@ -34,6 +34,26 @@ Copy these folders to your static assets directory, and add a `window.EXCALIDRAW
window.EXCALIDRAW_ASSET_PATH = "/"; window.EXCALIDRAW_ASSET_PATH = "/";
``` ```
or, if you serve your assets from the root of your CDN, you would do:
```js
// Vanilla
<head>
<script>
window.EXCALIDRAW_ASSET_PATH = "https://my.cdn.com/assets/";
</script>
</head>
```
or, if you prefer the path to be dynamicly set based on the `location.origin`, you could do the following:
```jsx
// Next.js
<Script id="load-env-variables" strategy="beforeInteractive" >
{ `window["EXCALIDRAW_ASSET_PATH"] = location.origin;` } // or use just "/"!
</Script>
```
### Dimensions of Excalidraw ### Dimensions of Excalidraw
Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions. Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions.
+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">;
+16 -61
View File
@@ -13,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,
@@ -26,23 +26,21 @@ import {
LiveCollaborationTrigger, LiveCollaborationTrigger,
TTDDialog, TTDDialog,
TTDDialogTrigger, TTDDialogTrigger,
StoreAction, } from "../packages/excalidraw/index";
reconcileElements, import {
} from "../packages/excalidraw";
import type {
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";
@@ -52,8 +50,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,
@@ -69,8 +67,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,
@@ -98,14 +99,17 @@ 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";
import Trans from "../packages/excalidraw/components/Trans"; import Trans from "../packages/excalidraw/components/Trans";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError"; import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile"; import {
RemoteExcalidrawElement,
reconcileElements,
} from "../packages/excalidraw/data/reconcile";
import { import {
CommandPalette, CommandPalette,
DEFAULT_CATEGORIES, DEFAULT_CATEGORIES,
@@ -126,38 +130,6 @@ 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) {
@@ -466,7 +438,7 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...data.scene, ...data.scene,
...restore(data.scene, null, null, { repairBindings: true }), ...restore(data.scene, null, null, { repairBindings: true }),
storeAction: StoreAction.CAPTURE, commitToStore: true,
}); });
} }
}); });
@@ -497,7 +469,6 @@ const ExcalidrawWrapper = () => {
setLangCode(langCode); setLangCode(langCode);
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...localDataState, ...localDataState,
storeAction: StoreAction.UPDATE,
}); });
LibraryIndexedDBAdapter.load().then((data) => { LibraryIndexedDBAdapter.load().then((data) => {
if (data) { if (data) {
@@ -633,7 +604,6 @@ const ExcalidrawWrapper = () => {
if (didChange) { if (didChange) {
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
elements, elements,
storeAction: StoreAction.UPDATE,
}); });
} }
} }
@@ -1132,21 +1102,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 };
+10 -16
View File
@@ -1,25 +1,23 @@
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,
} from "../../packages/excalidraw/element/types"; } from "../../packages/excalidraw/element/types";
import { import {
StoreAction,
getSceneVersion, getSceneVersion,
restoreElements, restoreElements,
zoomToFitBounds, zoomToFitBounds,
reconcileElements, } from "../../packages/excalidraw/index";
} from "../../packages/excalidraw"; import { Collaborator, Gesture } from "../../packages/excalidraw/types";
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
import { import {
assertNever, assertNever,
preventUnload, preventUnload,
@@ -36,14 +34,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,13 +75,14 @@ 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";
import type { import {
ReconciledExcalidrawElement, ReconciledExcalidrawElement,
RemoteExcalidrawElement, RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile"; } from "../../packages/excalidraw/data/reconcile";
export const collabAPIAtom = atom<CollabAPI | null>(null); export const collabAPIAtom = atom<CollabAPI | null>(null);
@@ -359,7 +356,6 @@ class Collab extends PureComponent<CollabProps, CollabState> {
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
elements, elements,
storeAction: StoreAction.UPDATE,
}); });
} }
}; };
@@ -510,7 +506,6 @@ class Collab extends PureComponent<CollabProps, CollabState> {
// to database even if deleted before creating the room. // to database even if deleted before creating the room.
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
elements, elements,
storeAction: StoreAction.UPDATE,
}); });
this.saveCollabRoomToFirebase(getSyncableElements(elements)); this.saveCollabRoomToFirebase(getSyncableElements(elements));
@@ -748,7 +743,6 @@ class Collab extends PureComponent<CollabProps, CollabState> {
) => { ) => {
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
elements, elements,
storeAction: StoreAction.UPDATE,
}); });
this.loadImageFiles(); this.loadImageFiles();
+5 -7
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,
@@ -19,7 +19,6 @@ import throttle from "lodash.throttle";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { encryptData } from "../../packages/excalidraw/data/encryption"; import { encryptData } from "../../packages/excalidraw/data/encryption";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import { StoreAction } from "../../packages/excalidraw";
class Portal { class Portal {
collab: TCollabClass; collab: TCollabClass;
@@ -128,7 +127,6 @@ class Portal {
} }
return element; return element;
}), }),
storeAction: StoreAction.UPDATE,
}); });
}, FILE_UPLOAD_TIMEOUT); }, FILE_UPLOAD_TIMEOUT);
+3 -3
View File
@@ -1,9 +1,9 @@
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 "./LanguageList"; import { LanguageList } from "./LanguageList";
@@ -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(
+2 -4
View File
@@ -1,15 +1,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,
@@ -239,6 +238,5 @@ export const updateStaleImageStatuses = (params: {
} }
return element; return element;
}), }),
storeAction: StoreAction.UPDATE,
}); });
}; };
+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";
+9 -8
View File
@@ -1,13 +1,12 @@
import { reconcileElements } from "../../packages/excalidraw"; import {
import type {
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,11 +19,13 @@ 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 {
RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile";
// private // private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+7 -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";
@@ -269,6 +269,7 @@ export const loadScene = async (
// in the scene database/localStorage, and instead fetch them async // in the scene database/localStorage, and instead fetch them async
// from a different database // from a different database
files: data.files, files: data.files,
commitToStore: false,
}; };
}; };
+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 -->
-4
View File
@@ -40,10 +40,6 @@
} }
&.highlighted { &.highlighted {
color: var(--color-promo); 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"; } 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>
+4 -6
View File
@@ -12,7 +12,7 @@ import {
createRedoAction, createRedoAction,
createUndoAction, createUndoAction,
} from "../../packages/excalidraw/actions/actionHistory"; } from "../../packages/excalidraw/actions/actionHistory";
import { StoreAction, newElementWith } from "../../packages/excalidraw"; import { newElementWith } from "../../packages/excalidraw";
const { h } = window; const { h } = window;
@@ -90,7 +90,7 @@ describe("collaboration", () => {
updateSceneData({ updateSceneData({
elements: syncInvalidIndices([rect1, rect2]), elements: syncInvalidIndices([rect1, rect2]),
storeAction: StoreAction.CAPTURE, commitToStore: true,
}); });
updateSceneData({ updateSceneData({
@@ -98,7 +98,7 @@ describe("collaboration", () => {
rect1, rect1,
newElementWith(h.elements[1], { isDeleted: true }), newElementWith(h.elements[1], { isDeleted: true }),
]), ]),
storeAction: StoreAction.CAPTURE, commitToStore: true,
}); });
await waitFor(() => { await waitFor(() => {
@@ -145,7 +145,6 @@ describe("collaboration", () => {
// simulate force deleting the element remotely // simulate force deleting the element remotely
updateSceneData({ updateSceneData({
elements: syncInvalidIndices([rect1]), elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
}); });
await waitFor(() => { await waitFor(() => {
@@ -183,7 +182,7 @@ describe("collaboration", () => {
h.elements[0], h.elements[0],
newElementWith(h.elements[1], { x: 100 }), newElementWith(h.elements[1], { x: 100 }),
]), ]),
storeAction: StoreAction.CAPTURE, commitToStore: true,
}); });
await waitFor(() => { await waitFor(() => {
@@ -218,7 +217,6 @@ describe("collaboration", () => {
// simulate force deleting the element remotely // simulate force deleting the element remotely
updateSceneData({ updateSceneData({
elements: syncInvalidIndices([rect1]), elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
}); });
// snapshot was correctly updated and marked the element as deleted // snapshot was correctly updated and marked the element as deleted
+1 -1
View File
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
import { useEffect, useLayoutEffect, useState } from "react"; import { 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";
+6 -7
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",
@@ -27,8 +26,8 @@
"@types/chai": "4.3.0", "@types/chai": "4.3.0",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7", "@types/lodash.throttle": "4.1.7",
"@types/react": "18.2.0", "@types/react": "18.0.15",
"@types/react-dom": "18.2.0", "@types/react-dom": "18.0.6",
"@types/socket.io-client": "3.0.0", "@types/socket.io-client": "3.0.0",
"@vitejs/plugin-react": "3.1.0", "@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0", "@vitest/coverage-v8": "0.33.0",
@@ -51,7 +50,7 @@
"vite-plugin-ejs": "1.7.0", "vite-plugin-ejs": "1.7.0",
"vite-plugin-pwa": "0.17.4", "vite-plugin-pwa": "0.17.4",
"vite-plugin-svgr": "2.4.0", "vite-plugin-svgr": "2.4.0",
"vitest": "1.5.3", "vitest": "1.0.1",
"vitest-canvas-mock": "0.3.2" "vitest-canvas-mock": "0.3.2"
}, },
"engines": { "engines": {
@@ -61,9 +60,9 @@
"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",
+4 -6
View File
@@ -29,19 +29,17 @@ Please add the latest change on the top under the correct section.
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450) - Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
- Extended `window.EXCALIDRAW_ASSET_PATH` to accept array of paths `string[]` as a value, allowing to specify multiple base `URL` fallbacks. [#8286](https://github.com/excalidraw/excalidraw/pull/8286)
### Fixes ### Fixes
- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656) - Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656)
### Breaking Changes ### Breaking Changes
- `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898) - Renamed required `updatedScene` parameter from `commitToHistory` into `commitToStore` [#7348](https://github.com/excalidraw/excalidraw/pull/7348).
| | Before `commitToHistory` | After `storeAction` | Notes | ### Breaking Changes
| --- | --- | --- | --- |
| _Immediately undoable_ | `true` | `"capture"` | As before, use for all updates which should be recorded by the store & history. Should be used for the most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
| _Eventually undoable_ | `false` | `"none"` | Similar to before, use for all updates which should not be recorded immediately (likely exceptions which are part of some async multi-step process) or those not meant to be recorded at all (i.e. updates to `collaborators` object, parts of `AppState` which are not observed by the store & history - not `ObservedAppState`).<br/><br/>**IMPORTANT** It's likely you should switch to `"update"` in all the other cases. Otherwise, all such updates would end up being recorded with the next `"capture"` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
| _Never undoable_ | n/a | `"update"` | **NEW**: previously there was no equivalent for this value. Now, it's recommended to use `"update"` for all remote updates (from the other clients), scene initialization, or those updates, which should not be locally "undoable". These updates will _never_ make it to the local undo / redo stacks. |
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539) - `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
+1 -1
View File
@@ -20,7 +20,7 @@ After installation you will see a folder `excalidraw-assets` and `excalidraw-ass
Move the folder `excalidraw-assets` and `excalidraw-assets-dev` to the path where your assets are served. Move the folder `excalidraw-assets` and `excalidraw-assets-dev` to the path where your assets are served.
By default it will try to load the files from [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist) By default it will try to load the files from [`https://unpkg.com/@excalidraw/excalidraw/dist/prod/`](https://unpkg.com/@excalidraw/excalidraw/dist/prod/)
If you want to load assets from a different path you can set a variable `window.EXCALIDRAW_ASSET_PATH` depending on environment (for example if you have different URL's for dev and prod) to the url from where you want to load the assets. If you want to load assets from a different path you can set a variable `window.EXCALIDRAW_ASSET_PATH` depending on environment (for example if you have different URL's for dev and prod) to the url from where you want to load the assets.
+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,
); );
+3 -3
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";
@@ -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";
+9 -11
View File
@@ -1,24 +1,24 @@
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";
import { import {
bindOrUnbindLinearElements, bindOrUnbindSelectedElements,
isBindingEnabled, isBindingEnabled,
unbindLinearElements,
} from "../element/binding"; } from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons"; import { flipHorizontal, flipVertical } from "../components/icons";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { isLinearElement } from "../element/typeChecks";
export const actionFlipHorizontal = register({ export const actionFlipHorizontal = register({
name: "flipHorizontal", name: "flipHorizontal",
@@ -89,6 +89,7 @@ const flipSelectedElements = (
const updatedElements = flipElements( const updatedElements = flipElements(
selectedElements, selectedElements,
elements,
elementsMap, elementsMap,
appState, appState,
flipDirection, flipDirection,
@@ -104,6 +105,7 @@ const flipSelectedElements = (
const flipElements = ( const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
appState: AppState, appState: AppState,
flipDirection: "horizontal" | "vertical", flipDirection: "horizontal" | "vertical",
@@ -117,17 +119,13 @@ const flipElements = (
elementsMap, elementsMap,
"nw", "nw",
true, true,
true,
flipDirection === "horizontal" ? maxX : minX, flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY, flipDirection === "horizontal" ? minY : maxY,
); );
bindOrUnbindLinearElements( isBindingEnabled(appState)
selectedElements.filter(isLinearElement), ? bindOrUnbindSelectedElements(selectedElements, app)
app, : unbindLinearElements(selectedElements, elementsMap);
isBindingEnabled(appState),
[],
);
return selectedElements; return selectedElements;
}; };
+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,
+8 -18
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 { IStore, StoreAction } from "../store";
import { StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter"; import { useEmitter } from "../hooks/useEmitter";
const writeData = ( const writeData = (
@@ -42,7 +40,7 @@ const writeData = (
return { storeAction: StoreAction.NONE }; return { storeAction: StoreAction.NONE };
}; };
type ActionCreator = (history: History, store: Store) => Action; type ActionCreator = (history: History, store: IStore) => Action;
export const createUndoAction: ActionCreator = (history, store) => ({ export const createUndoAction: ActionCreator = (history, store) => ({
name: "undo", name: "undo",
@@ -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";
@@ -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<
+7 -12
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 { StoreAction } from "../store";
export type ActionSource = export type ActionSource =
| "ui" | "ui"
@@ -29,7 +26,7 @@ export type ActionResult =
"offsetTop" | "offsetLeft" | "width" | "height" "offsetTop" | "offsetLeft" | "width" | "height"
> | null; > | null;
files?: BinaryFiles | null; files?: BinaryFiles | null;
storeAction: StoreActionType; storeAction: keyof typeof StoreAction;
replaceFiles?: boolean; replaceFiles?: boolean;
} }
| false; | false;
@@ -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 {
+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";
+1 -1
View File
@@ -7,7 +7,7 @@ import {
EXPORT_SCALES, EXPORT_SCALES,
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
+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,
})} })}
+190 -263
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,
@@ -87,11 +90,8 @@ import {
EDITOR_LS_KEYS, EDITOR_LS_KEYS,
isIOS, isIOS,
supportsResizeObserver, supportsResizeObserver,
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,22 +115,23 @@ import {
newTextElement, newTextElement,
newImageElement, newImageElement,
transformElements, transformElements,
refreshTextDimensions, updateTextElement,
redrawTextBoundingBox, redrawTextBoundingBox,
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "../element"; } from "../element";
import { import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
bindOrUnbindLinearElements, bindOrUnbindSelectedElements,
fixBindingsAfterDeletion, fixBindingsAfterDeletion,
fixBindingsAfterDuplication, fixBindingsAfterDuplication,
getEligibleElementsForBinding,
getHoveredElementForBinding, getHoveredElementForBinding,
isBindingEnabled, isBindingEnabled,
isLinearElementSimpleAndAlreadyBound, isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement, maybeBindLinearElement,
shouldEnableBindingForPointerEvent, shouldEnableBindingForPointerEvent,
unbindLinearElements,
updateBoundElements, updateBoundElements,
getSuggestedBindingsForArrows,
} from "../element/binding"; } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -162,7 +163,7 @@ import {
isMagicFrameElement, isMagicFrameElement,
isTextBindableContainer, isTextBindableContainer,
} from "../element/typeChecks"; } from "../element/typeChecks";
import type { import {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
@@ -182,6 +183,7 @@ import type {
ExcalidrawIframeElement, ExcalidrawIframeElement,
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
Ordered, Ordered,
OrderedExcalidrawElement,
} from "../element/types"; } from "../element/types";
import { getCenter, getDistance } from "../gesture"; import { getCenter, getDistance } from "../gesture";
import { import {
@@ -219,14 +221,11 @@ 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 } from "../shapes"; import { findShapeByKey } from "../shapes";
import type { GeometricShape } from "../../utils/geometry/shape";
import { import {
GeometricShape,
getClosedCurveShape, getClosedCurveShape,
getCurveShape, getCurveShape,
getEllipseShape, getEllipseShape,
@@ -235,7 +234,7 @@ import {
getSelectionBoxShape, getSelectionBoxShape,
} from "../../utils/geometry/shape"; } from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision"; import { isPointInShape } from "../../utils/collision";
import type { import {
AppClassProperties, AppClassProperties,
AppProps, AppProps,
AppState, AppState,
@@ -293,8 +292,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";
@@ -319,8 +321,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,
@@ -332,8 +333,6 @@ import {
getLineHeightInPx, getLineHeightInPx,
isMeasureTextSupported, isMeasureTextSupported,
isValidTextContainer, isValidTextContainer,
measureText,
wrapText,
} from "../element/textElement"; } from "../element/textElement";
import { import {
showHyperlinkTooltip, showHyperlinkTooltip,
@@ -388,9 +387,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";
@@ -404,15 +405,14 @@ 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";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
import { EditorLocalStorage } from "../data/EditorLocalStorage"; import { EditorLocalStorage } from "../data/EditorLocalStorage";
import FollowMode from "./FollowMode/FollowMode"; import FollowMode from "./FollowMode/FollowMode";
import { Store, StoreAction } from "../store"; import { IStore, Store, StoreAction } from "../store";
import { AnimationFrameHandler } from "../animation-frame-handler"; import { AnimationFrameHandler } from "../animation-frame-handler";
import { AnimatedTrail } from "../animated-trail"; import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails"; import { LaserTrails } from "../laser-trails";
@@ -432,9 +432,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 { Stats } from "./Stats";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -546,7 +543,7 @@ class App extends React.Component<AppProps, AppState> {
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined; public libraryItemsFromStorage: LibraryItems | undefined;
public id: string; public id: string;
store: Store; private store: IStore;
private history: History; private history: History;
private excalidrawContainerValue: { private excalidrawContainerValue: {
container: HTMLDivElement | null; container: HTMLDivElement | null;
@@ -720,7 +717,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);
@@ -943,7 +943,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
if (updated) { if (updated) {
this.scene.triggerUpdate(); this.scene.informMutation();
} }
// GC // GC
@@ -1455,10 +1455,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,
@@ -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 <StaticCanvas
canvas={this.canvas} canvas={this.canvas}
rc={this.rc} rc={this.rc}
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
} }
@@ -1711,13 +1698,12 @@ class App extends React.Component<AppProps, AppState> {
elementsMap={elementsMap} elementsMap={elementsMap}
visibleElements={visibleElements} visibleElements={visibleElements}
selectedElements={selectedElements} selectedElements={selectedElements}
sceneNonce={sceneNonce} versionNonce={versionNonce}
selectionNonce={ selectionNonce={
this.state.selectionElement?.versionNonce this.state.selectionElement?.versionNonce
} }
scale={window.devicePixelRatio} scale={window.devicePixelRatio}
appState={this.state} appState={this.state}
device={this.device}
renderInteractiveSceneCallback={ renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback this.renderInteractiveSceneCallback
} }
@@ -1835,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[]) {
@@ -2137,7 +2123,7 @@ class App extends React.Component<AppProps, AppState> {
} }
return el; return el;
}), }),
storeAction: StoreAction.CAPTURE, commitToStore: true,
}); });
} }
}, },
@@ -2460,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) {
@@ -2505,7 +2491,6 @@ class App extends React.Component<AppProps, AppState> {
public componentWillUnmount() { public componentWillUnmount() {
this.renderer.destroy(); this.renderer.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();
@@ -2583,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
@@ -2612,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);
}), }),
); );
@@ -2828,7 +2810,7 @@ class App extends React.Component<AppProps, AppState> {
); );
} }
this.store.commit(elementsMap, this.state); this.store.capture(elementsMap, this.state);
// Do not notify consumers if we're still loading the scene. Among other // Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during // potential issues, this fixes a case where the tab isn't focused during
@@ -3359,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);
@@ -3711,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();
}, },
@@ -3722,39 +3683,51 @@ 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 */ commitToStore?: SceneData["commitToStore"];
storeAction?: SceneData["storeAction"];
}) => { }) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []); const nextElements = syncInvalidIndices(sceneData.elements ?? []);
if (sceneData.storeAction && sceneData.storeAction !== StoreAction.NONE) { if (sceneData.commitToStore) {
const prevCommittedAppState = this.store.snapshot.appState; this.store.shouldCaptureIncrement();
const prevCommittedElements = this.store.snapshot.elements; }
const nextCommittedAppState = sceneData.appState if (sceneData.elements || sceneData.appState) {
? Object.assign({}, prevCommittedAppState, sceneData.appState) // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState` let nextCommittedAppState = this.state;
: prevCommittedAppState; let nextCommittedElements: Map<string, OrderedExcalidrawElement>;
const nextCommittedElements = sceneData.elements if (sceneData.appState) {
? this.store.filterUncomittedElements( nextCommittedAppState = {
this.scene.getElementsMapIncludingDeleted(), // Only used to detect uncomitted local elements ...this.state,
arrayToMap(nextElements), // We expect all (already reconciled) elements ...sceneData.appState, // Here we expect just partial appState
) };
: prevCommittedElements;
// WARN: store action always performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
// do NOT schedule store actions (execute after re-render), as it might cause unexpected concurrency issues if not handled well
if (sceneData.storeAction === StoreAction.CAPTURE) {
this.store.captureIncrement(
nextCommittedElements,
nextCommittedAppState,
);
} else if (sceneData.storeAction === StoreAction.UPDATE) {
this.store.updateSnapshot(
nextCommittedElements,
nextCommittedAppState,
);
} }
const prevElements = this.scene.getElementsIncludingDeleted();
if (sceneData.elements) {
/**
* We need to schedule a snapshot update, as in case `commitToStore` is false (i.e. remote update),
* as it's essential for computing local changes after the async action is completed (i.e. not to include remote changes in the diff).
*
* This is also a breaking change for all local `updateScene` calls without set `commitToStore` to true,
* as it makes such updates impossible to undo (previously they were undone coincidentally with the switch to the whole snapshot captured by the history).
*
* WARN: be careful here as moving it elsewhere could break the history for remote client without noticing
* - we need to find a way to test two concurrent client updates simultaneously, while having access to both stores & histories.
*/
this.store.shouldUpdateSnapshot();
// TODO#7348: deprecate once exchanging just store increments between clients
nextCommittedElements = this.store.ignoreUncomittedElements(
arrayToMap(prevElements),
arrayToMap(nextElements),
);
} else {
nextCommittedElements = arrayToMap(prevElements);
}
// WARN: Performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
this.store.capture(nextCommittedElements, nextCommittedAppState);
} }
if (sceneData.appState) { if (sceneData.appState) {
@@ -3771,15 +3744,8 @@ class App extends React.Component<AppProps, AppState> {
}, },
); );
private triggerRender = ( private onSceneUpdated = () => {
/** force always re-renders canvas even if no change */ this.setState({});
force?: boolean,
) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({});
}
}; };
/** /**
@@ -3983,12 +3949,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
}); });
this.setState({ this.maybeSuggestBindingForAll(selectedElements);
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ENTER) { } else if (event.key === KEYS.ENTER) {
@@ -4155,12 +4116,11 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true }); this.setState({ isBindingEnabled: true });
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
bindOrUnbindLinearElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getSelectedElements(this.state).filter(isLinearElement), const elementsMap = this.scene.getNonDeletedElementsMap();
this, isBindingEnabled(this.state)
isBindingEnabled(this.state), ? bindOrUnbindSelectedElements(selectedElements, this)
this.state.selectedLinearElement?.selectedPointsIndices ?? [], : unbindLinearElements(selectedElements, elementsMap);
);
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
} }
}); });
@@ -4219,11 +4179,6 @@ class App extends React.Component<AppProps, AppState> {
originSnapOffset: null, originSnapOffset: null,
activeEmbeddable: null, activeEmbeddable: null,
} as const; } as const;
if (nextActiveTool.type === "freedraw") {
this.store.shouldCaptureIncrement();
}
if (nextActiveTool.type !== "selection") { if (nextActiveTool.type !== "selection") {
return { return {
...prevState, ...prevState,
@@ -4348,22 +4303,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, _element,
isDeleted: isDeleted ?? _element.isDeleted, getContainerElement(_element, elementsMap),
// returns (wrapped) text and new dimensions elementsMap,
...refreshTextDimensions( {
_element, text,
getContainerElement(_element, elementsMap), isDeleted,
elementsMap, originalText,
nextOriginalText, },
), );
});
} }
return _element; return _element;
}), }),
@@ -4386,15 +4344,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) {
@@ -4439,7 +4397,7 @@ class App extends React.Component<AppProps, AppState> {
// 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() {
@@ -4578,7 +4536,7 @@ class App extends React.Component<AppProps, AppState> {
shape: this.getElementShape(elementWithHighestZIndex), shape: this.getElementShape(elementWithHighestZIndex),
// 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.getHitThreshold() / 2,
frameNameBound: isFrameLikeElement(elementWithHighestZIndex) frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
? this.frameNameBoundsCache.get(elementWithHighestZIndex) ? this.frameNameBoundsCache.get(elementWithHighestZIndex)
: null, : null,
@@ -4641,8 +4599,8 @@ class App extends React.Component<AppProps, AppState> {
return elements; return elements;
} }
private getElementHitThreshold() { private getHitThreshold() {
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value; return 10 / this.state.zoom.value;
} }
private hitElement( private hitElement(
@@ -4660,7 +4618,7 @@ class App extends React.Component<AppProps, AppState> {
const selectionShape = getSelectionBoxShape( const selectionShape = getSelectionBoxShape(
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.getElementHitThreshold(), this.getHitThreshold(),
); );
return isPointInShape([x, y], selectionShape); return isPointInShape([x, y], selectionShape);
@@ -4681,7 +4639,7 @@ class App extends React.Component<AppProps, AppState> {
y, y,
element, element,
shape: this.getElementShape(element), shape: this.getElementShape(element),
threshold: this.getElementHitThreshold(), threshold: this.getHitThreshold(),
frameNameBound: isFrameLikeElement(element) frameNameBound: isFrameLikeElement(element)
? this.frameNameBoundsCache.get(element) ? this.frameNameBoundsCache.get(element)
: null, : null,
@@ -4713,7 +4671,7 @@ class App extends React.Component<AppProps, AppState> {
y, y,
element: elements[index], element: elements[index],
shape: this.getElementShape(elements[index]), shape: this.getElementShape(elements[index]),
threshold: this.getElementHitThreshold(), threshold: this.getHitThreshold(),
}) })
) { ) {
hitElement = elements[index]; hitElement = elements[index];
@@ -4966,7 +4924,7 @@ class App extends React.Component<AppProps, AppState> {
y: sceneY, y: sceneY,
element: container, element: container,
shape: this.getElementShape(container), shape: this.getElementShape(container),
threshold: this.getElementHitThreshold(), threshold: this.getHitThreshold(),
}) })
) { ) {
const midPoint = getContainerCenter( const midPoint = getContainerCenter(
@@ -5146,11 +5104,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,
}); });
}); });
@@ -5384,41 +5339,24 @@ class App extends React.Component<AppProps, AppState> {
!isOverScrollBar && !isOverScrollBar &&
!this.state.editingLinearElement !this.state.editingLinearElement
) { ) {
// for linear elements, we'd like to prioritize point dragging over edge resizing const elementWithTransformHandleType = getElementWithTransformHandleType(
// therefore, we update and check hovered point index first elements,
if (this.state.selectedLinearElement) { this.state,
this.handleHoverSelectedLinearElement( scenePointerX,
this.state.selectedLinearElement, scenePointerY,
scenePointerX, this.state.zoom,
scenePointerY, event.pointerType,
); this.scene.getNonDeletedElementsMap(),
} );
if ( if (
!this.state.selectedLinearElement || elementWithTransformHandleType &&
this.state.selectedLinearElement.hoverPointIndex === -1 elementWithTransformHandleType.transformHandleType
) { ) {
const elementWithTransformHandleType = setCursor(
getElementWithTransformHandleType( this.interactiveCanvas,
elements, getCursorForResizingElement(elementWithTransformHandleType),
this.state, );
scenePointerX, return;
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device,
);
if (
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType),
);
return;
}
} }
} else if (selectedElements.length > 1 && !isOverScrollBar) { } else if (selectedElements.length > 1 && !isOverScrollBar) {
const transformHandleType = getTransformHandleTypeFromCoords( const transformHandleType = getTransformHandleTypeFromCoords(
@@ -5427,7 +5365,6 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY, scenePointerY,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.device,
); );
if (transformHandleType) { if (transformHandleType) {
setCursor( setCursor(
@@ -5580,7 +5517,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
); );
const threshold = this.getElementHitThreshold(); const threshold = this.getHitThreshold();
const point = { ...pointerDownState.lastCoords }; const point = { ...pointerDownState.lastCoords };
let samplingInterval = 0; let samplingInterval = 0;
while (samplingInterval <= distance) { while (samplingInterval <= distance) {
@@ -5625,7 +5562,7 @@ class App extends React.Component<AppProps, AppState> {
} }
this.elementsPendingErasure = new Set(this.elementsPendingErasure); this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.triggerRender(); this.onSceneUpdated();
} }
}; };
@@ -5677,7 +5614,7 @@ class App extends React.Component<AppProps, AppState> {
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else if (this.hitElement(scenePointerX, scenePointerY, element)) { } else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
} }
} else if (this.hitElement(scenePointerX, scenePointerY, element)) { } else if (this.hitElement(scenePointerX, scenePointerY, element)) {
@@ -5767,7 +5704,6 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
), ),
}, },
storeAction: StoreAction.UPDATE,
}); });
return; return;
} }
@@ -6377,14 +6313,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsMap = this.scene.getNonDeletedElementsMap(); const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if ( if (selectedElements.length === 1 && !this.state.editingLinearElement) {
selectedElements.length === 1 &&
!this.state.editingLinearElement &&
!(
this.state.selectedLinearElement &&
this.state.selectedLinearElement.hoverPointIndex !== -1
)
) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
getElementWithTransformHandleType( getElementWithTransformHandleType(
elements, elements,
@@ -6394,7 +6323,6 @@ class App extends React.Component<AppProps, AppState> {
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.device,
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
this.setState({ this.setState({
@@ -6410,7 +6338,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.device,
); );
} }
if (pointerDownState.resize.handleType) { if (pointerDownState.resize.handleType) {
@@ -6667,7 +6594,7 @@ class App extends React.Component<AppProps, AppState> {
} }
// How many pixels off the shape boundary we still consider a hit // How many pixels off the shape boundary we still consider a hit
const threshold = this.getElementHitThreshold(); const threshold = this.getHitThreshold();
const [x1, y1, x2, y2] = getCommonBounds(selectedElements); const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
return ( return (
point.x > x1 - threshold && point.x > x1 - threshold &&
@@ -7504,12 +7431,7 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
); );
this.setState({ this.maybeSuggestBindingForAll(selectedElements);
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
// We duplicate the selected element if alt is pressed on pointer move // We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) { if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
@@ -8071,7 +7993,6 @@ class App extends React.Component<AppProps, AppState> {
appState: { appState: {
draggingElement: null, draggingElement: null,
}, },
storeAction: StoreAction.UPDATE,
}); });
return; return;
@@ -8117,7 +8038,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
this.scene.triggerUpdate(); this.scene.informMutation();
} }
} }
} }
@@ -8244,7 +8165,6 @@ class App extends React.Component<AppProps, AppState> {
elements: this.scene elements: this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.filter((el) => el.id !== resizingElement.id), .filter((el) => el.id !== resizingElement.id),
storeAction: StoreAction.UPDATE,
}); });
} }
@@ -8497,7 +8417,7 @@ class App extends React.Component<AppProps, AppState> {
y: pointerDownState.origin.y, y: pointerDownState.origin.y,
element: hitElement, element: hitElement,
shape: this.getElementShape(hitElement), shape: this.getElementShape(hitElement),
threshold: this.getElementHitThreshold(), threshold: this.getHitThreshold(),
frameNameBound: isFrameLikeElement(hitElement) frameNameBound: isFrameLikeElement(hitElement)
? this.frameNameBoundsCache.get(hitElement) ? this.frameNameBoundsCache.get(hitElement)
: null, : null,
@@ -8556,18 +8476,15 @@ class App extends React.Component<AppProps, AppState> {
} }
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
// We only allow binding via linear elements, specifically via dragging isBindingEnabled(this.state)
// the endpoints ("start" or "end"). ? bindOrUnbindSelectedElements(
const linearElements = this.scene this.scene.getSelectedElements(this.state),
.getSelectedElements(this.state) this,
.filter(isLinearElement); )
: unbindLinearElements(
bindOrUnbindLinearElements( this.scene.getNonDeletedElements(),
linearElements, elementsMap,
this, );
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
} }
if (activeTool.type === "laser") { if (activeTool.type === "laser") {
@@ -8612,7 +8529,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 = () => {
@@ -9026,7 +8943,7 @@ class App extends React.Component<AppProps, AppState> {
files, files,
); );
if (updatedFiles.size) { if (updatedFiles.size) {
this.scene.triggerUpdate(); this.scene.informMutation();
} }
} }
}; };
@@ -9099,6 +9016,19 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings }); 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 { private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState), selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -9298,12 +9228,13 @@ class App extends React.Component<AppProps, AppState> {
} }
if (ret.type === MIME_TYPES.excalidraw) { if (ret.type === MIME_TYPES.excalidraw) {
// restore the fractional indices by mutating elements // Restore the fractional indices by mutating elements and update the
// store snapshot, otherwise we would end up with duplicate indices
syncInvalidIndices(elements.concat(ret.data.elements)); syncInvalidIndices(elements.concat(ret.data.elements));
this.store.snapshot = this.store.snapshot.clone(
// update the store snapshot for old elements, otherwise we would end up with duplicated fractional indices on undo arrayToMap(elements),
this.store.updateSnapshot(arrayToMap(elements), this.state); this.state,
);
this.setState({ isLoading: true }); this.setState({ isLoading: true });
this.syncActionResult({ this.syncActionResult({
...ret.data, ...ret.data,
@@ -9485,6 +9416,8 @@ class App extends React.Component<AppProps, AppState> {
this.state.originSnapOffset, this.state.originSnapOffset,
); );
this.maybeSuggestBindingForAll([draggingElement]);
// highlight elements that are to be added to frames on frames creation // highlight elements that are to be added to frames on frames creation
if ( if (
this.state.activeTool.type === TOOL_TYPE.frame || this.state.activeTool.type === TOOL_TYPE.frame ||
@@ -9598,7 +9531,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getElementsMapIncludingDeleted(), this.scene.getElementsMapIncludingDeleted(),
shouldRotateWithDiscreteAngle(event), shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event), shouldResizeFromCenter(event),
selectedElements.some((element) => isImageElement(element)) selectedElements.length === 1 && isImageElement(selectedElements[0])
? !shouldMaintainAspectRatio(event) ? !shouldMaintainAspectRatio(event)
: shouldMaintainAspectRatio(event), : shouldMaintainAspectRatio(event),
resizeX, resizeX,
@@ -9607,10 +9540,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.resize.center.y, pointerDownState.resize.center.y,
) )
) { ) {
const suggestedBindings = getSuggestedBindingsForArrows( this.maybeSuggestBindingForAll(selectedElements);
selectedElements,
this,
);
const elementsToHighlight = new Set<ExcalidrawElement>(); const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => { selectedFrames.forEach((frame) => {
@@ -9624,7 +9554,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
elementsToHighlight: [...elementsToHighlight], elementsToHighlight: [...elementsToHighlight],
suggestedBindings,
}); });
return true; return true;
@@ -9681,7 +9610,6 @@ class App extends React.Component<AppProps, AppState> {
} }
return [ return [
CONTEXT_MENU_SEPARATOR,
actionCut, actionCut,
actionCopy, actionCopy,
actionPaste, actionPaste,
@@ -9694,7 +9622,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>
@@ -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";
@@ -1,9 +1,8 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { LoadingMessage } from "./LoadingMessage"; import { LoadingMessage } from "./LoadingMessage";
import type { Language } from "../i18n"; import { defaultLang, Language, languages, setLanguage } from "../i18n";
import { defaultLang, languages, setLanguage } from "../i18n"; import { Theme } from "../element/types";
import type { Theme } from "../element/types";
interface Props { interface Props {
langCode: Language["code"]; langCode: Language["code"];
@@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import type { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import type { ExportOpts, BinaryFiles, UIAppState } from "../types"; import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons"; import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@@ -12,7 +12,7 @@ import { Card } from "./Card";
import "./ExportDialog.scss"; import "./ExportDialog.scss";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import type { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { getFrame } from "../utils"; import { getFrame } from "../utils";
export type ExportCB = ( export type ExportCB = (
@@ -1,7 +1,7 @@
import "./ToolIcon.scss"; import "./ToolIcon.scss";
import clsx from "clsx"; import clsx from "clsx";
import type { ToolButtonSize } from "./ToolButton"; import { ToolButtonSize } from "./ToolButton";
import { laserPointerToolIcon } from "./icons"; import { laserPointerToolIcon } from "./icons";
type LaserPointerIconProps = { type LaserPointerIconProps = {
+18 -7
View File
@@ -1,6 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react"; import React from "react";
import type { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { import {
CLASSES, CLASSES,
DEFAULT_SIDEBAR, DEFAULT_SIDEBAR,
@@ -8,11 +8,10 @@ import {
TOOL_TYPE, TOOL_TYPE,
} from "../constants"; } from "../constants";
import { showSelectedShapeActions } from "../element"; import { showSelectedShapeActions } from "../element";
import type { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import type { Language } from "../i18n"; import { Language, t } from "../i18n";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import type { import {
AppProps, AppProps,
AppState, AppState,
ExcalidrawProps, ExcalidrawProps,
@@ -39,6 +38,8 @@ import { JSONExportDialog } from "./JSONExportDialog";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useDevice } from "./App"; import { useDevice } from "./App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer"; import Footer from "./footer/Footer";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
@@ -442,7 +443,7 @@ const LayerUI = ({
); );
ShapeCache.delete(element); ShapeCache.delete(element);
} }
Scene.getScene(selectedElements[0])?.triggerUpdate(); Scene.getScene(selectedElements[0])?.informMutation();
} else if (colorPickerType === "elementBackground") { } else if (colorPickerType === "elementBackground") {
setAppState({ setAppState({
currentItemBackgroundColor: color, currentItemBackgroundColor: color,
@@ -540,9 +541,19 @@ const LayerUI = ({
showExitZenModeBtn={showExitZenModeBtn} showExitZenModeBtn={showExitZenModeBtn}
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
/> />
{appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
{appState.scrolledOutside && ( {appState.scrolledOutside && (
<button <button
type="button"
className="scroll-back-to-content" className="scroll-back-to-content"
onClick={() => { onClick={() => {
setAppState((appState) => ({ setAppState((appState) => ({
@@ -1,12 +1,11 @@
import React, { useState, useCallback, useMemo, useRef } from "react"; import React, { useState, useCallback, useMemo, useRef } from "react";
import type Library from "../data/library"; import Library, {
import {
distributeLibraryItemsOnSquareGrid, distributeLibraryItemsOnSquareGrid,
libraryItemsAtom, libraryItemsAtom,
} from "../data/library"; } from "../data/library";
import { t } from "../i18n"; import { t } from "../i18n";
import { randomId } from "../random"; import { randomId } from "../random";
import type { import {
LibraryItems, LibraryItems,
LibraryItem, LibraryItem,
ExcalidrawProps, ExcalidrawProps,
@@ -29,7 +28,7 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss"; import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils"; import { isShallowEqual } from "../utils";
import type { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants"; import { LIBRARY_DISABLED_TYPES } from "../constants";
export const isLibraryMenuOpenAtom = atom(false); export const isLibraryMenuOpenAtom = atom(false);
@@ -1,6 +1,6 @@
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import type { ExcalidrawProps, UIAppState } from "../types"; import { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({ const LibraryMenuBrowseButton = ({
theme, theme,
@@ -1,4 +1,4 @@
import type { ExcalidrawProps, UIAppState } from "../types"; import { ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx"; import clsx from "clsx";

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