Compare commits

..

38 Commits

Author SHA1 Message Date
Aakansha Doshi d5b406b055 path fixes 2024-05-09 13:38:37 +05:30
Aakansha Doshi 7522285869 watch utils folder and build it when updated 2024-05-09 13:31:07 +05:30
Aakansha Doshi 58567dd2b6 move the files to src and tests folders under utils 2024-05-09 13:30:44 +05:30
Aakansha Doshi a76999e9a2 lint 2024-05-08 19:19:14 +05:30
Aakansha Doshi 5af532d229 Merge remote-tracking branch 'origin/master' into aakansha/esm 2024-05-08 19:15:37 +05:30
Aakansha Doshi 1ed53b153c build: enable consistent type imports eslint rule (#7992)
* build: enable consistent type imports eslint rule

* change to warn

* fix the warning in example and excalidraw-app

* fix packages

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

Fixes #7403.

* deps: update (container) Nginx to 1.24

---------

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

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2024-05-06 17:35:23 +02:00
Aakansha Doshi 96fc3ac5bb lint 2024-05-06 19:09:09 +05:30
Aakansha Doshi 906652bac2 lint 2024-05-06 19:05:36 +05:30
Aakansha Doshi 53a0428705 update remaining paths to use utils workspace 2024-05-06 18:56:54 +05:30
Aakansha Doshi 927e36c7b4 fix tests 2024-05-06 18:42:07 +05:30
Aakansha Doshi e187faee77 update test script 2024-05-06 17:35:33 +05:30
Aakansha Doshi d12d97bfcb add cleanup workspaces script and test-utils 2024-05-06 17:28:58 +05:30
Aakansha Doshi 5acb5c9d91 update test script 2024-05-06 17:11:02 +05:30
Aakansha Doshi 23ee054025 fix lint 2024-05-06 17:05:19 +05:30
Aakansha Doshi d91b234db1 fix typo 2024-05-06 16:24:35 +05:30
Aakansha Doshi b2b03af1ec fix script 2024-05-06 16:22:18 +05:30
Aakansha Doshi f0876e3c03 tweaks 2024-05-06 16:18:05 +05:30
Aakansha Doshi 6d0ee330b7 update build scripts 2024-05-06 15:35:34 +05:30
Aakansha Doshi e48eed6b21 add utils to external 2024-05-06 15:25:44 +05:30
Aakansha Doshi 549786a504 add utils to external 2024-05-06 15:15:48 +05:30
Aakansha Doshi 73c53a3c7c extend ts config 2024-05-06 13:55:02 +05:30
Aakansha Doshi 72a98da527 ignore types in tests 2024-05-03 17:18:42 +05:30
Aakansha Doshi 45ff9d1053 add @excalidraw/utils to external and fixes 2024-05-03 17:08:07 +05:30
Aakansha Doshi 6a1477a55c feat: use @excalidraw/utils as a workspace in the codebase 2024-05-03 15:54:06 +05:30
Márk Tolmács d9bbf1eda6 feat: Allow binding only via linear element ends (#7946)
Arrows now only bind to new shapes if their start or end point is dragged close to them. Arrows previously bound to shapes remain bound on move and drag if at the end of the drag/move the points remain in the original shapes' binding area.

---------

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

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2024-05-01 20:47:52 +02:00
Mritunjay Goutam 275f6fbe24 fix: typo in doc api (#7466) 2024-04-30 16:52:42 +00:00
Ryan Di 88812e0386 feat: resize elements from the sides (#7855)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-04-30 18:05:00 +02:00
Marcel Mraz 6e5aeb112d feat: record freedraw tool selection to history (#7949) 2024-04-25 17:24:05 +00:00
Marcel Mraz 4d83d1c91e fix: use Reflect API instead of Object.hasOwn (#7958) 2024-04-25 15:36:26 +02:00
Márk Tolmács a04676d423 fix: CTRL/CMD & arrow point drag unbinds both sides (#6459) (#7877) 2024-04-23 00:01:05 +02:00
Milos Vetesnik c851aaaf7b fix: z-index for laser pointer to be able to draw on embeds and such (#7918) 2024-04-22 23:53:55 +02:00
Marcel Mraz 1bd2b1fe55 feat: export reconciliation (#7917) 2024-04-22 17:27:57 +02:00
Marcel Mraz 015b46ab23 feat: expose StoreAction in relation to multiplayer history (#7898)
Improved Store API and improved handling of actions to eliminate potential concurrency issues
2024-04-22 09:22:25 +00:00
284 changed files with 7398 additions and 6324 deletions
+7
View File
@@ -4,8 +4,15 @@
!.eslintrc.json
!.npmrc
!.prettierrc
!excalidraw-app/
!package.json
!public/
!packages/
!tsconfig.json
!yarn.lock
# keep (sub)sub directories at the end to exclude from explicit included
# e.g. ./packages/excalidraw/{dist,node_modules}
**/build
**/dist
**/node_modules
+2 -1
View File
@@ -2,6 +2,7 @@
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"import/no-anonymous-default-export": "off",
"no-restricted-globals": "off"
"no-restricted-globals": "off",
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
}
}
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
- name: Install and test
run: |
yarn install
yarn test:app
yarn test
+7 -5
View File
@@ -2,16 +2,18 @@ FROM node:18 AS build
WORKDIR /opt/node_app
COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000
COPY . .
# do not ignore optional dependencies:
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
RUN yarn --network-timeout 600000
ARG NODE_ENV=production
COPY . .
RUN yarn build:app:docker
FROM nginx:1.21-alpine
FROM nginx:1.24-alpine
COPY --from=build /opt/node_app/build /usr/share/nginx/html
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
@@ -8,15 +8,15 @@
import { FONT_FAMILY } from "@excalidraw/excalidraw";
```
`FONT_FAMILY` contains all the font families used in `Excalidraw`. The default families are the following:
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
| Font Family | Description |
| ----------- | ---------------------- |
| `Excalifont` | The `Hand-drawn` font |
| `Nunito` | The `Normal` Font |
| `Comic Shanns` | The `Code` Font |
| `Virgil` | The `handwritten` font |
| `Helvetica` | The `Normal` Font |
| `Cascadia` | The `Code` Font |
Pre-selected family is `FONT_FAMILY.Excalifont`, unless it's overriden with `initialData.appState.currentItemFontFamily`.
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
### 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 |
| `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. |
| `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. |
| `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`. |
```jsx live
function App() {
@@ -105,7 +105,6 @@ function App() {
appState: {
viewBackgroundColor: "#edf2ff",
},
storeAction: StoreAction.CAPTURE,
};
excalidrawAPI.updateScene(sceneData);
};
@@ -116,25 +115,12 @@ function App() {
<button className="custom-button" onClick={updateScene}>
Update Scene
</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
</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
<pre>
@@ -202,7 +188,7 @@ function App() {
Update Library
</button>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
excalidrawAPI={(api) => setExcalidrawAPI(api)}
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
initialData={{
libraryItems: initialData.libraryItems,
@@ -31,7 +31,7 @@ You can pass `null` / `undefined` if not applicable.
restoreElements(
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;
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean, normalizeIndices?: boolean }<br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</pre>
@@ -51,9 +51,8 @@ The extra optional parameter to configure restored elements. It has the followin
| 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. |
| `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. |
| `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. |
**_How to use_**
@@ -74,7 +73,7 @@ restore(
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;
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, normalizeIndices?: boolean }<br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</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.
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.
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.
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,26 +34,6 @@ Copy these folders to your static assets directory, and add a `window.EXCALIDRAW
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
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 type { ResolvablePromise } from "../utils";
import {
resolvablePromise,
ResolvablePromise,
distance2d,
fileOpen,
withBatchedUpdates,
@@ -1,4 +1,4 @@
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw";
+1 -1
View File
@@ -1,6 +1,6 @@
import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access";
import type { MIME_TYPES } from "@excalidraw/excalidraw";
import { MIME_TYPES } from "@excalidraw/excalidraw";
import { AbortError } from "../../packages/excalidraw/errors";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
+3 -3
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
"dev": "yarn build:workspace && next dev -p 3005",
"build": "yarn build:workspace && next build",
"build:workspaces": "yarn workspace @excalidraw/utils run build:esm && yarn workspace @excalidraw/excalidraw run build:esm",
"dev": "yarn build:workspaces && next dev -p 3005",
"build": "yarn build:workspaces && next build",
"start": "next start -p 3006",
"lint": "next lint"
},
@@ -12,8 +12,9 @@
"typescript": "^5"
},
"scripts": {
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
"build:workspaces": "yarn workspace @excalidraw/utils run build:esm && yarn workspace @excalidraw/excalidraw run build:esm",
"start": "yarn build:workspaces && vite",
"build": "yarn build:workspaces && vite build",
"build:preview": "yarn build && vite preview --port 5002"
}
}
+14 -16
View File
@@ -13,7 +13,7 @@ import {
VERSION_TIMEOUT,
} from "../packages/excalidraw/constants";
import { loadFromBlob } from "../packages/excalidraw/data/blob";
import {
import type {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
@@ -26,21 +26,23 @@ import {
LiveCollaborationTrigger,
TTDDialog,
TTDDialogTrigger,
} from "../packages/excalidraw/index";
import {
StoreAction,
reconcileElements,
} from "../packages/excalidraw";
import type {
AppState,
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "../packages/excalidraw/types";
import type { ResolvablePromise } from "../packages/excalidraw/utils";
import {
debounce,
getVersion,
getFrame,
isTestEnv,
preventUnload,
ResolvablePromise,
resolvablePromise,
isRunningInIframe,
} from "../packages/excalidraw/utils";
@@ -50,8 +52,8 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import type { CollabAPI } from "./collab/Collab";
import Collab, {
CollabAPI,
collabAPIAtom,
isCollaboratingAtom,
isOfflineAtom,
@@ -67,11 +69,8 @@ import {
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import {
restore,
restoreAppState,
RestoredDataState,
} from "../packages/excalidraw/data/restore";
import type { RestoredDataState } from "../packages/excalidraw/data/restore";
import { restore, restoreAppState } from "../packages/excalidraw/data/restore";
import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
@@ -99,17 +98,14 @@ import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
import { appJotaiStore } from "./app-jotai";
import "./index.scss";
import { ResolutionType } from "../packages/excalidraw/utility-types";
import type { ResolutionType } from "../packages/excalidraw/utility-types";
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
import Trans from "../packages/excalidraw/components/Trans";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import {
RemoteExcalidrawElement,
reconcileElements,
} from "../packages/excalidraw/data/reconcile";
import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile";
import {
CommandPalette,
DEFAULT_CATEGORIES,
@@ -438,7 +434,7 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.updateScene({
...data.scene,
...restore(data.scene, null, null, { repairBindings: true }),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
}
});
@@ -469,6 +465,7 @@ const ExcalidrawWrapper = () => {
setLangCode(langCode);
excalidrawAPI.updateScene({
...localDataState,
storeAction: StoreAction.UPDATE,
});
LibraryIndexedDBAdapter.load().then((data) => {
if (data) {
@@ -604,6 +601,7 @@ const ExcalidrawWrapper = () => {
if (didChange) {
excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
}
}
+2 -2
View File
@@ -7,8 +7,8 @@ import {
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
import { t } from "../packages/excalidraw/i18n";
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import { UIAppState } from "../packages/excalidraw/types";
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
import type { UIAppState } from "../packages/excalidraw/types";
type StorageSizes = { scene: number; total: number };
+16 -10
View File
@@ -1,23 +1,25 @@
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import {
import type {
ExcalidrawImperativeAPI,
SocketId,
} from "../../packages/excalidraw/types";
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import {
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type {
ExcalidrawElement,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import {
StoreAction,
getSceneVersion,
restoreElements,
zoomToFitBounds,
} from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
reconcileElements,
} from "../../packages/excalidraw";
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
import {
assertNever,
preventUnload,
@@ -34,12 +36,14 @@ import {
SYNC_FULL_SCENE_INTERVAL_MS,
WS_EVENTS,
} from "../app_constants";
import type {
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
generateCollaborationLinkData,
getCollaborationLink,
getSyncableElements,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import {
isSavedToFirebase,
@@ -75,14 +79,13 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom } from "jotai";
import { appJotaiStore } from "../app-jotai";
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
import { collabErrorIndicatorAtom } from "./CollabError";
import {
import type {
ReconciledExcalidrawElement,
RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile";
export const collabAPIAtom = atom<CollabAPI | null>(null);
@@ -356,6 +359,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
}
};
@@ -506,6 +510,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
// to database even if deleted before creating the room.
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
this.saveCollabRoomToFirebase(getSyncableElements(elements));
@@ -743,6 +748,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
) => {
this.excalidrawAPI.updateScene({
elements,
storeAction: StoreAction.UPDATE,
});
this.loadImageFiles();
+7 -5
View File
@@ -1,15 +1,15 @@
import {
isSyncableElement,
import type {
SocketUpdateData,
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import { isSyncableElement } from "../data";
import { TCollabClass } from "./Collab";
import type { TCollabClass } from "./Collab";
import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import {
import type {
OnUserFollowedPayload,
SocketId,
UserIdleState,
@@ -19,6 +19,7 @@ import throttle from "lodash.throttle";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { encryptData } from "../../packages/excalidraw/data/encryption";
import type { Socket } from "socket.io-client";
import { StoreAction } from "../../packages/excalidraw";
class Portal {
collab: TCollabClass;
@@ -127,6 +128,7 @@ class Portal {
}
return element;
}),
storeAction: StoreAction.UPDATE,
});
}, FILE_UPLOAD_TIMEOUT);
+3 -3
View File
@@ -1,9 +1,9 @@
import React from "react";
import {
arrowBarToLeftIcon,
loginIcon,
ExcalLogo,
} from "../../packages/excalidraw/components/icons";
import { Theme } from "../../packages/excalidraw/element/types";
import type { Theme } from "../../packages/excalidraw/element/types";
import { MainMenu } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { LanguageList } from "./LanguageList";
@@ -42,7 +42,7 @@ export const AppMainMenu: React.FC<{
</MainMenu.ItemLink>
<MainMenu.DefaultItems.Socials />
<MainMenu.ItemLink
icon={arrowBarToLeftIcon}
icon={loginIcon}
href={`${import.meta.env.VITE_APP_PLUS_APP}${
isExcalidrawPlusSignedUser ? "" : "/sign-up"
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
@@ -1,5 +1,5 @@
import React from "react";
import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
import { loginIcon } from "../../packages/excalidraw/components/icons";
import { useI18n } from "../../packages/excalidraw/i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
@@ -61,7 +61,7 @@ export const AppWelcomeScreen: React.FC<{
import.meta.env.VITE_APP_PLUS_LP
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
shortcut={null}
icon={arrowBarToLeftIcon}
icon={loginIcon}
>
Sign up
</WelcomeScreen.Center.MenuItemLink>
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import {
import type {
FileId,
NonDeletedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
+1 -1
View File
@@ -1,7 +1,7 @@
import oc from "open-color";
import React from "react";
import { THEME } from "../../packages/excalidraw/constants";
import { Theme } from "../../packages/excalidraw/element/types";
import type { Theme } from "../../packages/excalidraw/element/types";
// https://github.com/tholman/github-corners
export const GitHubCorner = React.memo(
+4 -2
View File
@@ -1,14 +1,15 @@
import { StoreAction } from "../../packages/excalidraw";
import { compressData } from "../../packages/excalidraw/data/encode";
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawImageElement,
FileId,
InitializedExcalidrawImageElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
BinaryFileData,
BinaryFileMetadata,
ExcalidrawImperativeAPI,
@@ -238,5 +239,6 @@ export const updateStaleImageStatuses = (params: {
}
return element;
}),
storeAction: StoreAction.UPDATE,
});
};
+5 -5
View File
@@ -20,19 +20,19 @@ import {
get,
} from "idb-keyval";
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
import {
import type {
ExcalidrawElement,
FileId,
} from "../../packages/excalidraw/element/types";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
} from "../../packages/excalidraw/types";
import { MaybePromise } from "../../packages/excalidraw/utility-types";
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
import { debounce } from "../../packages/excalidraw/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager";
+8 -9
View File
@@ -1,12 +1,13 @@
import {
import { reconcileElements } from "../../packages/excalidraw";
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { getSceneVersion } from "../../packages/excalidraw/element";
import Portal from "../collab/Portal";
import type Portal from "../collab/Portal";
import { restoreElements } from "../../packages/excalidraw/data/restore";
import {
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
@@ -19,13 +20,11 @@ import {
decryptData,
} from "../../packages/excalidraw/data/encryption";
import { MIME_TYPES } from "../../packages/excalidraw/constants";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { SyncableExcalidrawElement } from ".";
import { getSyncableElements } from ".";
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
import type { Socket } from "socket.io-client";
import {
RemoteExcalidrawElement,
reconcileElements,
} from "../../packages/excalidraw/data/reconcile";
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
// private
// -----------------------------------------------------------------------------
+6 -7
View File
@@ -9,30 +9,30 @@ import {
} from "../../packages/excalidraw/data/encryption";
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore";
import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import {
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "../../packages/excalidraw/element/types";
import { t } from "../../packages/excalidraw/i18n";
import {
import type {
AppState,
BinaryFileData,
BinaryFiles,
SocketId,
UserIdleState,
} from "../../packages/excalidraw/types";
import { MakeBrand } from "../../packages/excalidraw/utility-types";
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
import { bytesToHexString } from "../../packages/excalidraw/utils";
import type { WS_SUBTYPES } from "../app_constants";
import {
DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants";
import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase";
@@ -269,7 +269,6 @@ export const loadScene = async (
// in the scene database/localStorage, and instead fetch them async
// from a different database
files: data.files,
commitToStore: false,
};
};
+2 -2
View File
@@ -1,5 +1,5 @@
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import { AppState } from "../../packages/excalidraw/types";
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
import type { AppState } from "../../packages/excalidraw/types";
import {
clearAppStateForLocalStorage,
getDefaultAppState,
+4
View File
@@ -40,6 +40,10 @@
}
&.highlighted {
color: var(--color-promo);
font-weight: 700;
.dropdown-menu-item__icon g {
stroke-width: 2;
}
}
}
}
+2 -1
View File
@@ -18,7 +18,8 @@ import {
} from "../../packages/excalidraw/components/icons";
import { TextField } from "../../packages/excalidraw/components/TextField";
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
import type { CollabAPI } from "../collab/Collab";
import { activeRoomLinkAtom } from "../collab/Collab";
import { atom, useAtom, useAtomValue } from "jotai";
import "./ShareDialog.scss";
@@ -216,23 +216,22 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
stroke-width="2"
viewBox="0 0 24 24"
>
<g>
<g
stroke-width="1.5"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M10 12l10 0"
d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
/>
<path
d="M10 12l4 4"
d="M21 12h-13l3 -3"
/>
<path
d="M10 12l4 -4"
/>
<path
d="M4 4l0 16"
d="M11 15l-3 -3"
/>
</g>
</svg>
+6 -4
View File
@@ -12,7 +12,7 @@ import {
createRedoAction,
createUndoAction,
} from "../../packages/excalidraw/actions/actionHistory";
import { newElementWith } from "../../packages/excalidraw";
import { StoreAction, newElementWith } from "../../packages/excalidraw";
const { h } = window;
@@ -90,7 +90,7 @@ describe("collaboration", () => {
updateSceneData({
elements: syncInvalidIndices([rect1, rect2]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
updateSceneData({
@@ -98,7 +98,7 @@ describe("collaboration", () => {
rect1,
newElementWith(h.elements[1], { isDeleted: true }),
]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
await waitFor(() => {
@@ -145,6 +145,7 @@ describe("collaboration", () => {
// simulate force deleting the element remotely
updateSceneData({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
await waitFor(() => {
@@ -182,7 +183,7 @@ describe("collaboration", () => {
h.elements[0],
newElementWith(h.elements[1], { x: 100 }),
]),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
await waitFor(() => {
@@ -217,6 +218,7 @@ describe("collaboration", () => {
// simulate force deleting the element remotely
updateSceneData({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
// snapshot was correctly updated and marked the element as deleted
+17
View File
@@ -0,0 +1,17 @@
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
import { STORAGE_KEYS } from "../app_constants";
export const initLocalStorage = (data: ImportedDataState) => {
if (data.elements) {
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
JSON.stringify(data.elements),
);
}
if (data.appState) {
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
JSON.stringify(data.appState),
);
}
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
import { useEffect, useLayoutEffect, useState } from "react";
import { THEME } from "../packages/excalidraw";
import { EVENT } from "../packages/excalidraw/constants";
import { Theme } from "../packages/excalidraw/element/types";
import type { Theme } from "../packages/excalidraw/element/types";
import { CODES, KEYS } from "../packages/excalidraw/keys";
import { STORAGE_KEYS } from "./app_constants";
+11 -10
View File
@@ -26,8 +26,8 @@
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/socket.io-client": "3.0.0",
"@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0",
@@ -50,7 +50,7 @@
"vite-plugin-ejs": "1.7.0",
"vite-plugin-pwa": "0.17.4",
"vite-plugin-svgr": "2.4.0",
"vitest": "1.0.1",
"vitest": "1.5.3",
"vitest-canvas-mock": "0.3.2"
},
"engines": {
@@ -60,10 +60,11 @@
"prettier": "@excalidraw/prettier-config",
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "node ./scripts/build-version.js",
"build": "yarn --cwd ./excalidraw-app build",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
"build:app": "yarn --cwd ./excalidraw-app build:app",
"build:version": "yarn --cwd ./excalidraw-app build:version",
"build": "yarn workspace @excalidraw/utils build:esm && yarn --cwd ./excalidraw-app build",
"clear:workspaces": "yarn workspace @excalidraw/utils run clear && yarn workspace @excalidraw/excalidraw run clear",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
"fix": "yarn fix:other && yarn fix:code",
@@ -71,7 +72,7 @@
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "yarn --cwd ./excalidraw-app start",
"start": "yarn clear:workspaces && yarn workspace @excalidraw/utils run build:esm && yarn --cwd ./excalidraw-app start & node scripts/watchUtils.js",
"start:app:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
"test:app": "vitest",
@@ -79,8 +80,8 @@
"test:other": "yarn prettier --list-different",
"test:typecheck": "tsc",
"test:update": "yarn test:app --update --watch=false",
"test": "yarn test:app",
"test:coverage": "vitest --coverage",
"test": "yarn clear:workspaces && yarn workspace @excalidraw/utils build:esm && yarn test:app",
"test:coverage": "yarn clear:workspaces && yarn workspace @excalidraw/utils build:esm && yarn test:app --coverage",
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui --coverage.enabled=true",
"autorelease": "node scripts/autorelease.js",
+6 -4
View File
@@ -29,17 +29,19 @@ 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)
- 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
- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656)
### Breaking Changes
- Renamed required `updatedScene` parameter from `commitToHistory` into `commitToStore` [#7348](https://github.com/excalidraw/excalidraw/pull/7348).
- `updateScene` API has changed due to the added `Store` component as part of the multiplayer undo / redo initiative. Specifically, `sceneData` property `commitToHistory: boolean` was replaced with `storeAction: StoreActionType`. Make sure to update all instances of `updateScene` according to the _before / after_ table below. [#7898](https://github.com/excalidraw/excalidraw/pull/7898)
### Breaking Changes
| | Before `commitToHistory` | After `storeAction` | Notes |
| --- | --- | --- | --- |
| _Immediately undoable_ | `true` | `"capture"` | As before, use for all updates which should be recorded by the store & history. Should be used for the most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
| _Eventually undoable_ | `false` | `"none"` | Similar to before, use for all updates which should not be recorded immediately (likely exceptions which are part of some async multi-step process) or those not meant to be recorded at all (i.e. updates to `collaborators` object, parts of `AppState` which are not observed by the store & history - not `ObservedAppState`).<br/><br/>**IMPORTANT** It's likely you should switch to `"update"` in all the other cases. Otherwise, all such updates would end up being recorded with the next `"capture"` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
| _Never undoable_ | n/a | `"update"` | **NEW**: previously there was no equivalent for this value. Now, it's recommended to use `"update"` for all remote updates (from the other clients), scene initialization, or those updates, which should not be locally "undoable". These updates will _never_ make it to the local undo / redo stacks. |
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
+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.
By default it will try to load the files from [`https://unpkg.com/@excalidraw/excalidraw/dist/prod/`](https://unpkg.com/@excalidraw/excalidraw/dist/prod/)
By default it will try to load the files from [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist)
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.
+4 -3
View File
@@ -1,4 +1,5 @@
import { alignElements, Alignment } from "../align";
import type { Alignment } from "../align";
import { alignElements } from "../align";
import {
AlignBottomIcon,
AlignLeftIcon,
@@ -10,13 +11,13 @@ import {
import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import { AppClassProperties, AppState, UIAppState } from "../types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -23,14 +23,14 @@ import {
isTextBindableContainer,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import type { AppState } from "../types";
import type { Mutable } from "../utility-types";
import { arrayToMap, getFontString } from "../utils";
import { register } from "./register";
import { syncMovedIndices } from "../fractionalIndex";
+3 -3
View File
@@ -18,13 +18,13 @@ import {
ZOOM_STEP,
} from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types";
import type { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
@@ -35,7 +35,7 @@ import {
isHandToolActive,
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { SceneBounds } from "../element/bounds";
import type { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor";
import { StoreAction } from "../store";
@@ -4,8 +4,8 @@ import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import type { ExcalidrawElement } from "../element/types";
import type { AppState } from "../types";
import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -3,16 +3,17 @@ import {
DistributeVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../distribute";
import type { Distribution } from "../distribute";
import { distributeElements } from "../distribute";
import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { StoreAction } from "../store";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
@@ -1,6 +1,6 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
@@ -12,9 +12,9 @@ import {
getSelectedGroupForElement,
getElementsInGroup,
} from "../groups";
import { AppState } from "../types";
import type { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import type { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import {
bindTextToShapeAfterDuplication,
@@ -1,7 +1,7 @@
import { LockedIcon, UnlockedIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { isFrameLikeElement } from "../element/typeChecks";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { StoreAction } from "../store";
+1 -1
View File
@@ -16,7 +16,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { Theme } from "../element/types";
import type { Theme } from "../element/types";
import "../components/ToolIcon.scss";
import { StoreAction } from "../store";
@@ -13,7 +13,7 @@ import {
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types";
import type { AppState } from "../types";
import { resetCursor } from "../cursor";
import { StoreAction } from "../store";
+11 -9
View File
@@ -1,24 +1,24 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import {
import type {
ExcalidrawElement,
NonDeleted,
NonDeletedSceneElementsMap,
} from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
bindOrUnbindSelectedElements,
bindOrUnbindLinearElements,
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons";
import { StoreAction } from "../store";
import { isLinearElement } from "../element/typeChecks";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
@@ -89,7 +89,6 @@ const flipSelectedElements = (
const updatedElements = flipElements(
selectedElements,
elements,
elementsMap,
appState,
flipDirection,
@@ -105,7 +104,6 @@ const flipSelectedElements = (
const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
appState: AppState,
flipDirection: "horizontal" | "vertical",
@@ -119,13 +117,17 @@ const flipElements = (
elementsMap,
"nw",
true,
true,
flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY,
);
isBindingEnabled(appState)
? bindOrUnbindSelectedElements(selectedElements, app)
: unbindLinearElements(selectedElements, elementsMap);
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
app,
isBindingEnabled(appState),
[],
);
return selectedElements;
};
+2 -2
View File
@@ -1,9 +1,9 @@
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameChildren } from "../frame";
import { KEYS } from "../keys";
import { AppClassProperties, AppState, UIAppState } from "../types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
import { updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { register } from "./register";
+2 -2
View File
@@ -17,12 +17,12 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import {
import type {
ExcalidrawElement,
ExcalidrawTextElement,
OrderedExcalidrawElement,
} from "../element/types";
import { AppClassProperties, AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
getElementsInResizingFrame,
@@ -1,14 +1,16 @@
import { Action, ActionResult } from "./types";
import type { Action, ActionResult } from "./types";
import { UndoIcon, RedoIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { History, HistoryChangedEvent } from "../history";
import { AppState } from "../types";
import type { History } from "../history";
import { HistoryChangedEvent } from "../history";
import type { AppState } from "../types";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
import { SceneElementsMap } from "../element/types";
import { IStore, StoreAction } from "../store";
import type { SceneElementsMap } from "../element/types";
import type { Store } from "../store";
import { StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter";
const writeData = (
@@ -40,7 +42,7 @@ const writeData = (
return { storeAction: StoreAction.NONE };
};
type ActionCreator = (history: History, store: IStore) => Action;
type ActionCreator = (history: History, store: Store) => Action;
export const createUndoAction: ActionCreator = (history, store) => ({
name: "undo",
@@ -1,9 +1,12 @@
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types";
import type { ExcalidrawLinearElement } from "../element/types";
import { StoreAction } from "../store";
import { register } from "./register";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { lineEditorIcon } from "../components/icons";
export const actionToggleLinearEditor = register({
name: "toggleLinearEditor",
@@ -11,18 +14,23 @@ export const actionToggleLinearEditor = register({
label: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement?.id
? "labels.lineEditor.exit"
})[0] as ExcalidrawLinearElement | undefined;
return selectedElement?.type === "arrow"
? "labels.lineEditor.editArrow"
: "labels.lineEditor.edit";
},
keywords: ["line"],
trackEvent: {
category: "element",
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (
!appState.editingLinearElement &&
selectedElements.length === 1 &&
isLinearElement(selectedElements[0])
) {
return true;
}
return false;
@@ -45,4 +53,24 @@ export const actionToggleLinearEditor = register({
storeAction: StoreAction.CAPTURE,
};
},
PanelComponent: ({ appState, updateData, app }) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
})[0] as ExcalidrawLinearElement;
const label = t(
selectedElement.type === "arrow"
? "labels.lineEditor.editArrow"
: "labels.lineEditor.edit",
);
return (
<ToolButton
type="button"
icon={lineEditorIcon}
title={label}
aria-label={label}
onClick={() => updateData(null)}
/>
);
},
});
@@ -1,6 +1,6 @@
import { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar";
import { GoToCollaboratorComponentProps } from "../components/UserList";
import type { GoToCollaboratorComponentProps } from "../components/UserList";
import {
eyeIcon,
microphoneIcon,
@@ -8,7 +8,7 @@ import {
} from "../components/icons";
import { t } from "../i18n";
import { StoreAction } from "../store";
import { Collaborator } from "../types";
import type { Collaborator } from "../types";
import { register } from "./register";
import clsx from "clsx";
@@ -1,4 +1,4 @@
import { AppClassProperties, AppState, Primitive } from "../types";
import type { AppClassProperties, AppState, Primitive } from "../types";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -74,7 +74,7 @@ import {
isLinearElement,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
import type {
Arrowhead,
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import type { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
+1 -1
View File
@@ -24,7 +24,7 @@ import {
isArrowElement,
} from "../element/typeChecks";
import { getSelectedElements } from "../scene";
import { ExcalidrawTextElement } from "../element/types";
import type { ExcalidrawTextElement } from "../element/types";
import { paintIcon } from "../components/icons";
import { StoreAction } from "../store";
@@ -1,7 +1,7 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { GRID_SIZE } from "../constants";
import { AppState } from "../types";
import type { AppState } from "../types";
import { gridIcon } from "../components/icons";
import { StoreAction } from "../store";
@@ -20,6 +20,7 @@ import { StoreAction } from "../store";
export const actionSendBackward = register({
name: "sendBackward",
label: "labels.sendBackward",
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -49,6 +50,7 @@ export const actionSendBackward = register({
export const actionBringForward = register({
name: "bringForward",
label: "labels.bringForward",
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -78,6 +80,7 @@ export const actionBringForward = register({
export const actionSendToBack = register({
name: "sendToBack",
label: "labels.sendToBack",
keywords: ["move down", "zindex", "layer"],
icon: SendToBackIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
@@ -114,6 +117,7 @@ export const actionSendToBack = register({
export const actionBringToFront = register({
name: "bringToFront",
label: "labels.bringToFront",
keywords: ["move up", "zindex", "layer"],
icon: BringToFrontIcon,
trackEvent: { category: "element" },
+6 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import {
import type {
Action,
UpdaterFn,
ActionName,
@@ -7,8 +7,11 @@ import {
PanelComponentProps,
ActionSource,
} from "./types";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics";
import { isPromiseLike } from "../utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import { Action } from "./types";
import type { Action } from "./types";
export let actions: readonly Action[] = [];
+2 -2
View File
@@ -1,8 +1,8 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import { SubtypeOf } from "../utility-types";
import type { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
import type { ActionName } from "./types";
export type ShortcutName =
| SubtypeOf<
+9 -6
View File
@@ -1,14 +1,17 @@
import React from "react";
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
import {
import type React from "react";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../element/types";
import type {
AppClassProperties,
AppState,
ExcalidrawProps,
BinaryFiles,
UIAppState,
} from "../types";
import { MarkOptional } from "../utility-types";
import { StoreAction } from "../store";
import type { MarkOptional } from "../utility-types";
import type { StoreActionType } from "../store";
export type ActionSource =
| "ui"
@@ -26,7 +29,7 @@ export type ActionResult =
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
files?: BinaryFiles | null;
storeAction: keyof typeof StoreAction;
storeAction: StoreActionType;
replaceFiles?: boolean;
}
| false;
+3 -2
View File
@@ -1,6 +1,7 @@
import { ElementsMap, ExcalidrawElement } from "./element/types";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import type { BoundingBox } from "./element/bounds";
import { getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
+4 -3
View File
@@ -1,6 +1,7 @@
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
import { AnimationFrameHandler } from "./animation-frame-handler";
import { AppState } from "./types";
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
import { LaserPointer } from "@excalidraw/laser-pointer";
import type { AnimationFrameHandler } from "./animation-frame-handler";
import type { AppState } from "./types";
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
import type App from "./components/App";
import { SVG_NS } from "./constants";
+1 -1
View File
@@ -7,7 +7,7 @@ import {
EXPORT_SCALES,
THEME,
} from "./constants";
import { AppState, NormalizedZoomValue } from "./types";
import type { AppState, NormalizedZoomValue } from "./types";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio
+6 -10
View File
@@ -1,18 +1,14 @@
import { ENV } from "./constants";
import type { BindableProp, BindingProp } from "./element/binding";
import {
BoundElement,
BindableElement,
BindableProp,
BindingProp,
bindingProperties,
updateBoundElements,
} from "./element/binding";
import { LinearElementEditor } from "./element/linearElementEditor";
import {
ElementUpdate,
mutateElement,
newElementWith,
} from "./element/mutateElement";
import type { ElementUpdate } from "./element/mutateElement";
import { mutateElement, newElementWith } from "./element/mutateElement";
import {
getBoundTextElementId,
redrawTextBoundingBox,
@@ -23,7 +19,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./element/typeChecks";
import {
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
@@ -34,13 +30,13 @@ import {
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups";
import { getObservedAppState } from "./store";
import {
import type {
AppState,
ObservedAppState,
ObservedElementsAppState,
ObservedStandaloneAppState,
} from "./types";
import { SubtypeOf, ValueOf } from "./utility-types";
import type { SubtypeOf, ValueOf } from "./utility-types";
import {
arrayToMap,
arrayToObject,
+2 -6
View File
@@ -1,9 +1,5 @@
import {
Spreadsheet,
tryParseCells,
tryParseNumber,
VALID_SPREADSHEET,
} from "./charts";
import type { Spreadsheet } from "./charts";
import { tryParseCells, tryParseNumber, VALID_SPREADSHEET } from "./charts";
describe("charts", () => {
describe("tryParseNumber", () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types";
import type { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
+3 -3
View File
@@ -5,13 +5,13 @@ import {
THEME,
} from "./constants";
import { roundRect } from "./renderer/roundRect";
import { InteractiveCanvasRenderConfig } from "./scene/types";
import {
import type { InteractiveCanvasRenderConfig } from "./scene/types";
import type {
Collaborator,
InteractiveCanvasAppState,
SocketId,
UserIdleState,
} from "./types";
import { UserIdleState } from "./types";
function hashToInteger(id: string) {
let hash = 0;
+4 -3
View File
@@ -1,9 +1,10 @@
import {
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { BinaryFiles } from "./types";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import type { BinaryFiles } from "./types";
import type { Spreadsheet } from "./charts";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import {
ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES,
+1 -1
View File
@@ -1,5 +1,5 @@
import oc from "open-color";
import { Merge } from "./utility-types";
import type { Merge } from "./utility-types";
// FIXME can't put to utils.ts rn because of circular dependency
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
+17 -7
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { ActionManager } from "../actions/manager";
import {
import type { ActionManager } from "../actions/manager";
import type {
ExcalidrawElement,
ExcalidrawElementType,
NonDeletedElementsMap,
@@ -17,13 +17,17 @@ import {
hasStrokeWidth,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics";
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
import {
hasBoundTextElement,
isLinearElement,
isTextElement,
} from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import { Tooltip } from "./Tooltip";
@@ -114,6 +118,11 @@ export const SelectedShapeActions = ({
const showLinkIcon =
targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction =
!appState.editingLinearElement &&
targetElements.length === 1 &&
isLinearElement(targetElements[0]);
return (
<div className="panelColumn">
<div>
@@ -173,8 +182,8 @@ export const SelectedShapeActions = ({
<div className="buttonList">
{renderAction("sendToBack")}
{renderAction("sendBackward")}
{renderAction("bringToFront")}
{renderAction("bringForward")}
{renderAction("bringToFront")}
</div>
</fieldset>
@@ -229,6 +238,7 @@ export const SelectedShapeActions = ({
{renderAction("group")}
{renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")}
{showLineEditorAction && renderAction("toggleLinearEditor")}
</div>
</fieldset>
)}
@@ -333,8 +343,8 @@ export const ShapesSwitcher = ({
fontSize: 8,
fontFamily: "Cascadia, monospace",
position: "absolute",
background: "pink",
color: "black",
background: "var(--color-promo)",
color: "var(--color-surface-lowest)",
bottom: 3,
right: 4,
}}
+164 -141
View File
@@ -1,7 +1,7 @@
import React, { useContext } from "react";
import { flushSync } from "react-dom";
import { RoughCanvas } from "roughjs/bin/canvas";
import type { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
import { nanoid } from "nanoid";
@@ -39,18 +39,16 @@ import {
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { Action, ActionResult } from "../actions/types";
import type { Action, ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
import {
PastedMixedContent,
copyTextToSystemClipboard,
parseClipboard,
} from "../clipboard";
import type { PastedMixedContent } from "../clipboard";
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
import type { EXPORT_IMAGE_TYPES } from "../constants";
import {
APP_NAME,
CURSOR_TYPE,
@@ -62,7 +60,6 @@ import {
ENV,
EVENT,
FRAME_STYLE,
EXPORT_IMAGE_TYPES,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
@@ -90,8 +87,10 @@ import {
EDITOR_LS_KEYS,
isIOS,
supportsResizeObserver,
DEFAULT_COLLISION_THRESHOLD,
} from "../constants";
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
import type { ExportedElements } from "../data";
import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore";
import {
@@ -121,17 +120,16 @@ import {
} from "../element";
import {
bindOrUnbindLinearElement,
bindOrUnbindSelectedElements,
bindOrUnbindLinearElements,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
getEligibleElementsForBinding,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
unbindLinearElements,
updateBoundElements,
getSuggestedBindingsForArrows,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -163,7 +161,7 @@ import {
isMagicFrameElement,
isTextBindableContainer,
} from "../element/typeChecks";
import {
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
@@ -183,7 +181,6 @@ import {
ExcalidrawIframeElement,
ExcalidrawEmbeddableElement,
Ordered,
OrderedExcalidrawElement,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@@ -221,20 +218,23 @@ import {
isSomeElementSelected,
} from "../scene";
import Scene from "../scene/Scene";
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
import type {
RenderInteractiveSceneCallback,
ScrollBars,
} from "../scene/types";
import { getStateForZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes";
import type { GeometricShape } from "@excalidraw/utils";
import {
GeometricShape,
getClosedCurveShape,
getCurveShape,
getEllipseShape,
getFreedrawShape,
getPolygonShape,
getSelectionBoxShape,
} from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision";
import {
isPointInShape,
} from "@excalidraw/utils";
import type {
AppClassProperties,
AppProps,
AppState,
@@ -292,11 +292,8 @@ import {
maybeParseEmbedSrc,
getEmbedLink,
} from "../element/embeddable";
import {
ContextMenu,
ContextMenuItems,
CONTEXT_MENU_SEPARATOR,
} from "./ContextMenu";
import type { ContextMenuItems } from "./ContextMenu";
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
import LayerUI from "./LayerUI";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
@@ -321,7 +318,8 @@ import {
updateImageCache as _updateImageCache,
} from "../element/image";
import throttle from "lodash.throttle";
import { fileOpen, FileSystemHandle } from "../data/filesystem";
import type { FileSystemHandle } from "../data/filesystem";
import { fileOpen } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
@@ -387,11 +385,9 @@ import {
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper";
import {
ExcalidrawElementSkeleton,
convertToExcalidrawElements,
} from "../data/transform";
import { ValueOf } from "../utility-types";
import type { ExcalidrawElementSkeleton } from "../data/transform";
import { convertToExcalidrawElements } from "../data/transform";
import type { ValueOf } from "../utility-types";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { StaticCanvas, InteractiveCanvas } from "./canvases";
import { Renderer } from "../scene/Renderer";
@@ -405,14 +401,15 @@ import {
} from "../cursor";
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
import { MagicCacheData, diagramToHTML } from "../data/magic";
import { exportToBlob } from "../../utils/export";
import type { MagicCacheData } from "../data/magic";
import { diagramToHTML } from "../data/magic";
import { exportToBlob } from "@excalidraw/utils";
import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
import { EditorLocalStorage } from "../data/EditorLocalStorage";
import FollowMode from "./FollowMode/FollowMode";
import { IStore, Store, StoreAction } from "../store";
import { Store, StoreAction } from "../store";
import { AnimationFrameHandler } from "../animation-frame-handler";
import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails";
@@ -543,7 +540,7 @@ class App extends React.Component<AppProps, AppState> {
public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined;
public id: string;
private store: IStore;
private store: Store;
private history: History;
private excalidrawContainerValue: {
container: HTMLDivElement | null;
@@ -1704,6 +1701,7 @@ class App extends React.Component<AppProps, AppState> {
}
scale={window.devicePixelRatio}
appState={this.state}
device={this.device}
renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback
}
@@ -2123,7 +2121,7 @@ class App extends React.Component<AppProps, AppState> {
}
return el;
}),
commitToStore: true,
storeAction: StoreAction.CAPTURE,
});
}
},
@@ -2810,7 +2808,7 @@ class App extends React.Component<AppProps, AppState> {
);
}
this.store.capture(elementsMap, this.state);
this.store.commit(elementsMap, this.state);
// Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during
@@ -3683,51 +3681,39 @@ class App extends React.Component<AppProps, AppState> {
elements?: SceneData["elements"];
appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"];
commitToStore?: SceneData["commitToStore"];
/** @default StoreAction.CAPTURE */
storeAction?: SceneData["storeAction"];
}) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
if (sceneData.commitToStore) {
this.store.shouldCaptureIncrement();
}
if (sceneData.storeAction && sceneData.storeAction !== StoreAction.NONE) {
const prevCommittedAppState = this.store.snapshot.appState;
const prevCommittedElements = this.store.snapshot.elements;
if (sceneData.elements || sceneData.appState) {
let nextCommittedAppState = this.state;
let nextCommittedElements: Map<string, OrderedExcalidrawElement>;
const nextCommittedAppState = sceneData.appState
? Object.assign({}, prevCommittedAppState, sceneData.appState) // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState`
: prevCommittedAppState;
if (sceneData.appState) {
nextCommittedAppState = {
...this.state,
...sceneData.appState, // Here we expect just partial appState
};
}
const nextCommittedElements = sceneData.elements
? this.store.filterUncomittedElements(
this.scene.getElementsMapIncludingDeleted(), // Only used to detect uncomitted local elements
arrayToMap(nextElements), // We expect all (already reconciled) elements
)
: prevCommittedElements;
const prevElements = this.scene.getElementsIncludingDeleted();
if (sceneData.elements) {
/**
* We need to schedule a snapshot update, as in case `commitToStore` is false (i.e. remote update),
* as it's essential for computing local changes after the async action is completed (i.e. not to include remote changes in the diff).
*
* This is also a breaking change for all local `updateScene` calls without set `commitToStore` to true,
* as it makes such updates impossible to undo (previously they were undone coincidentally with the switch to the whole snapshot captured by the history).
*
* WARN: be careful here as moving it elsewhere could break the history for remote client without noticing
* - we need to find a way to test two concurrent client updates simultaneously, while having access to both stores & histories.
*/
this.store.shouldUpdateSnapshot();
// TODO#7348: deprecate once exchanging just store increments between clients
nextCommittedElements = this.store.ignoreUncomittedElements(
arrayToMap(prevElements),
arrayToMap(nextElements),
// WARN: store action always performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
// do NOT schedule store actions (execute after re-render), as it might cause unexpected concurrency issues if not handled well
if (sceneData.storeAction === StoreAction.CAPTURE) {
this.store.captureIncrement(
nextCommittedElements,
nextCommittedAppState,
);
} else if (sceneData.storeAction === StoreAction.UPDATE) {
this.store.updateSnapshot(
nextCommittedElements,
nextCommittedAppState,
);
} else {
nextCommittedElements = arrayToMap(prevElements);
}
// WARN: Performs deep clone of changed elements, for ephemeral remote updates (i.e. remote dragging, resizing, drawing) we might consider doing something smarter
this.store.capture(nextCommittedElements, nextCommittedAppState);
}
if (sceneData.appState) {
@@ -3949,7 +3935,12 @@ class App extends React.Component<AppProps, AppState> {
});
});
this.maybeSuggestBindingForAll(selectedElements);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
@@ -4116,11 +4107,12 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements, this)
: unbindLinearElements(selectedElements, elementsMap);
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
this.setState({ suggestedBindings: [] });
}
});
@@ -4179,6 +4171,11 @@ class App extends React.Component<AppProps, AppState> {
originSnapOffset: null,
activeEmbeddable: null,
} as const;
if (nextActiveTool.type === "freedraw") {
this.store.shouldCaptureIncrement();
}
if (nextActiveTool.type !== "selection") {
return {
...prevState,
@@ -4536,7 +4533,7 @@ class App extends React.Component<AppProps, AppState> {
shape: this.getElementShape(elementWithHighestZIndex),
// when overlapping, we would like to be more precise
// this also avoids the need to update past tests
threshold: this.getHitThreshold() / 2,
threshold: this.getElementHitThreshold() / 2,
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
: null,
@@ -4599,8 +4596,8 @@ class App extends React.Component<AppProps, AppState> {
return elements;
}
private getHitThreshold() {
return 10 / this.state.zoom.value;
private getElementHitThreshold() {
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
}
private hitElement(
@@ -4618,7 +4615,7 @@ class App extends React.Component<AppProps, AppState> {
const selectionShape = getSelectionBoxShape(
element,
this.scene.getNonDeletedElementsMap(),
this.getHitThreshold(),
this.getElementHitThreshold(),
);
return isPointInShape([x, y], selectionShape);
@@ -4639,7 +4636,7 @@ class App extends React.Component<AppProps, AppState> {
y,
element,
shape: this.getElementShape(element),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(element)
? this.frameNameBoundsCache.get(element)
: null,
@@ -4671,7 +4668,7 @@ class App extends React.Component<AppProps, AppState> {
y,
element: elements[index],
shape: this.getElementShape(elements[index]),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
})
) {
hitElement = elements[index];
@@ -4924,7 +4921,7 @@ class App extends React.Component<AppProps, AppState> {
y: sceneY,
element: container,
shape: this.getElementShape(container),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
})
) {
const midPoint = getContainerCenter(
@@ -5339,24 +5336,41 @@ class App extends React.Component<AppProps, AppState> {
!isOverScrollBar &&
!this.state.editingLinearElement
) {
const elementWithTransformHandleType = getElementWithTransformHandleType(
elements,
this.state,
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
);
if (
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType),
// for linear elements, we'd like to prioritize point dragging over edge resizing
// therefore, we update and check hovered point index first
if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
);
return;
}
if (
!this.state.selectedLinearElement ||
this.state.selectedLinearElement.hoverPointIndex === -1
) {
const elementWithTransformHandleType =
getElementWithTransformHandleType(
elements,
this.state,
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device,
);
if (
elementWithTransformHandleType &&
elementWithTransformHandleType.transformHandleType
) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType),
);
return;
}
}
} else if (selectedElements.length > 1 && !isOverScrollBar) {
const transformHandleType = getTransformHandleTypeFromCoords(
@@ -5365,6 +5379,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY,
this.state.zoom,
event.pointerType,
this.device,
);
if (transformHandleType) {
setCursor(
@@ -5517,7 +5532,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointer.x,
scenePointer.y,
);
const threshold = this.getHitThreshold();
const threshold = this.getElementHitThreshold();
const point = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance) {
@@ -5614,7 +5629,7 @@ class App extends React.Component<AppProps, AppState> {
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else {
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
}
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
@@ -5704,6 +5719,7 @@ class App extends React.Component<AppProps, AppState> {
this.state,
),
},
storeAction: StoreAction.UPDATE,
});
return;
}
@@ -6313,7 +6329,14 @@ class App extends React.Component<AppProps, AppState> {
const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
if (
selectedElements.length === 1 &&
!this.state.editingLinearElement &&
!(
this.state.selectedLinearElement &&
this.state.selectedLinearElement.hoverPointIndex !== -1
)
) {
const elementWithTransformHandleType =
getElementWithTransformHandleType(
elements,
@@ -6323,6 +6346,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device,
);
if (elementWithTransformHandleType != null) {
this.setState({
@@ -6338,6 +6362,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y,
this.state.zoom,
event.pointerType,
this.device,
);
}
if (pointerDownState.resize.handleType) {
@@ -6594,7 +6619,7 @@ class App extends React.Component<AppProps, AppState> {
}
// How many pixels off the shape boundary we still consider a hit
const threshold = this.getHitThreshold();
const threshold = this.getElementHitThreshold();
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
return (
point.x > x1 - threshold &&
@@ -7431,7 +7456,12 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
);
this.maybeSuggestBindingForAll(selectedElements);
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this,
),
});
// We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
@@ -7993,6 +8023,7 @@ class App extends React.Component<AppProps, AppState> {
appState: {
draggingElement: null,
},
storeAction: StoreAction.UPDATE,
});
return;
@@ -8165,6 +8196,7 @@ class App extends React.Component<AppProps, AppState> {
elements: this.scene
.getElementsIncludingDeleted()
.filter((el) => el.id !== resizingElement.id),
storeAction: StoreAction.UPDATE,
});
}
@@ -8417,7 +8449,7 @@ class App extends React.Component<AppProps, AppState> {
y: pointerDownState.origin.y,
element: hitElement,
shape: this.getElementShape(hitElement),
threshold: this.getHitThreshold(),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(hitElement)
? this.frameNameBoundsCache.get(hitElement)
: null,
@@ -8476,15 +8508,18 @@ class App extends React.Component<AppProps, AppState> {
}
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(
this.scene.getSelectedElements(this.state),
this,
)
: unbindLinearElements(
this.scene.getNonDeletedElements(),
elementsMap,
);
// We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end").
const linearElements = this.scene
.getSelectedElements(this.state)
.filter(isLinearElement);
bindOrUnbindLinearElements(
linearElements,
this,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
);
}
if (activeTool.type === "laser") {
@@ -9016,19 +9051,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ suggestedBindings });
};
private maybeSuggestBindingForAll(
selectedElements: NonDeleted<ExcalidrawElement>[],
): void {
if (selectedElements.length > 50) {
return;
}
const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this,
);
this.setState({ suggestedBindings });
}
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -9228,13 +9250,12 @@ class App extends React.Component<AppProps, AppState> {
}
if (ret.type === MIME_TYPES.excalidraw) {
// Restore the fractional indices by mutating elements and update the
// store snapshot, otherwise we would end up with duplicate indices
// restore the fractional indices by mutating elements
syncInvalidIndices(elements.concat(ret.data.elements));
this.store.snapshot = this.store.snapshot.clone(
arrayToMap(elements),
this.state,
);
// update the store snapshot for old elements, otherwise we would end up with duplicated fractional indices on undo
this.store.updateSnapshot(arrayToMap(elements), this.state);
this.setState({ isLoading: true });
this.syncActionResult({
...ret.data,
@@ -9416,8 +9437,6 @@ class App extends React.Component<AppProps, AppState> {
this.state.originSnapOffset,
);
this.maybeSuggestBindingForAll([draggingElement]);
// highlight elements that are to be added to frames on frames creation
if (
this.state.activeTool.type === TOOL_TYPE.frame ||
@@ -9531,7 +9550,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getElementsMapIncludingDeleted(),
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),
selectedElements.length === 1 && isImageElement(selectedElements[0])
selectedElements.some((element) => isImageElement(element))
? !shouldMaintainAspectRatio(event)
: shouldMaintainAspectRatio(event),
resizeX,
@@ -9540,7 +9559,10 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.resize.center.y,
)
) {
this.maybeSuggestBindingForAll(selectedElements);
const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements,
this,
);
const elementsToHighlight = new Set<ExcalidrawElement>();
selectedFrames.forEach((frame) => {
@@ -9554,6 +9576,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({
elementsToHighlight: [...elementsToHighlight],
suggestedBindings,
});
return true;
@@ -1,10 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getColor } from "./ColorPicker";
import { useAtom } from "jotai";
import {
ColorPickerType,
activeColorPickerSectionAtom,
} from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import { eyeDropperIcon } from "../icons";
import { jotaiScope } from "../../jotai";
import { KEYS } from "../../keys";
@@ -1,16 +1,15 @@
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import type { ExcalidrawElement } from "../../element/types";
import type { AppState } from "../../types";
import { TopPicks } from "./TopPicks";
import { Picker } from "./Picker";
import * as Popover from "@radix-ui/react-popover";
import { useAtom } from "jotai";
import {
activeColorPickerSectionAtom,
ColorPickerType,
} from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
import { useDevice, useExcalidrawContainer } from "../App";
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } from "../../colors";
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
import { COLOR_PALETTE } from "../../colors";
import PickerHeading from "./PickerHeading";
import { t } from "../../i18n";
import clsx from "clsx";
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { t } from "../../i18n";
import { ExcalidrawElement } from "../../element/types";
import type { ExcalidrawElement } from "../../element/types";
import { ShadeList } from "./ShadeList";
import PickerColorList from "./PickerColorList";
@@ -9,15 +9,15 @@ import { useAtom } from "jotai";
import { CustomColorList } from "./CustomColorList";
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import PickerHeading from "./PickerHeading";
import type { ColorPickerType } from "./colorPickerUtils";
import {
ColorPickerType,
activeColorPickerSectionAtom,
getColorNameAndShadeFromColor,
getMostUsedCustomColors,
isCustomColor,
} from "./colorPickerUtils";
import type { ColorPaletteCustom } from "../../colors";
import {
ColorPaletteCustom,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../../colors";
@@ -7,8 +7,9 @@ import {
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { ColorPaletteCustom } from "../../colors";
import { TranslationKeys, t } from "../../i18n";
import type { ColorPaletteCustom } from "../../colors";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
interface PickerColorListProps {
palette: ColorPaletteCustom;
@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import type { ReactNode } from "react";
const PickerHeading = ({ children }: { children: ReactNode }) => (
<div className="color-picker__heading">{children}</div>
@@ -7,7 +7,7 @@ import {
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { t } from "../../i18n";
import { ColorPaletteCustom } from "../../colors";
import type { ColorPaletteCustom } from "../../colors";
interface ShadeListProps {
hex: string;
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { ColorPickerType } from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
import {
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -1,10 +1,7 @@
import { ExcalidrawElement } from "../../element/types";
import type { ExcalidrawElement } from "../../element/types";
import { atom } from "jotai";
import {
ColorPickerColor,
ColorPaletteCustom,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
} from "../../colors";
import type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
export const getColorNameAndShadeFromColor = ({
palette,
@@ -1,14 +1,13 @@
import { KEYS } from "../../keys";
import {
import type {
ColorPickerColor,
ColorPalette,
ColorPaletteCustom,
COLORS_PER_ROW,
COLOR_PALETTE,
} from "../../colors";
import { ValueOf } from "../../utility-types";
import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
import type { ValueOf } from "../../utility-types";
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
import {
ActiveColorPickerSectionAtomType,
colorPickerHotkeyBindings,
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
@@ -10,12 +10,11 @@ import { Dialog } from "../Dialog";
import { TextField } from "../TextField";
import clsx from "clsx";
import { getSelectedElements } from "../../scene";
import { Action } from "../../actions/types";
import { TranslationKeys, t } from "../../i18n";
import {
ShortcutName,
getShortcutFromShortcutName,
} from "../../actions/shortcuts";
import type { Action } from "../../actions/types";
import type { TranslationKeys } from "../../i18n";
import { t } from "../../i18n";
import type { ShortcutName } from "../../actions/shortcuts";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
import {
LockedIcon,
@@ -31,7 +30,7 @@ import {
} from "../icons";
import fuzzy from "fuzzy";
import { useUIAppState } from "../../context/ui-appState";
import { AppProps, AppState, UIAppState } from "../../types";
import type { AppProps, AppState, UIAppState } from "../../types";
import {
capitalizeString,
getShortcutKey,
@@ -39,7 +38,7 @@ import {
} from "../../utils";
import { atom, useAtom } from "jotai";
import { deburr } from "../../deburr";
import { MarkRequired } from "../../utility-types";
import type { MarkRequired } from "../../utility-types";
import { InlineIcon } from "../InlineIcon";
import { SHAPES } from "../../shapes";
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
@@ -47,7 +46,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { actionClearCanvas, actionLink } from "../../actions";
import { jotaiStore } from "../../jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { CommandPaletteItem } from "./types";
import type { CommandPaletteItem } from "./types";
import * as defaultItems from "./defaultCommandPaletteItems";
import { trackEvent } from "../../analytics";
import { useStable } from "../../hooks/useStable";
@@ -258,10 +257,10 @@ function CommandPaletteInner({
actionManager.actions.deleteSelectedElements,
actionManager.actions.copyStyles,
actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,
actionManager.actions.bringForward,
actionManager.actions.sendBackward,
actionManager.actions.sendToBack,
actionManager.actions.bringForward,
actionManager.actions.bringToFront,
actionManager.actions.alignTop,
actionManager.actions.alignBottom,
actionManager.actions.alignLeft,
@@ -1,5 +1,5 @@
import { actionToggleTheme } from "../../actions";
import { CommandPaletteItem } from "./types";
import type { CommandPaletteItem } from "./types";
export const toggleTheme: CommandPaletteItem = {
...actionToggleTheme,
@@ -1,6 +1,6 @@
import { ActionManager } from "../../actions/manager";
import { Action } from "../../actions/types";
import { UIAppState } from "../../types";
import type { ActionManager } from "../../actions/manager";
import type { Action } from "../../actions/types";
import type { UIAppState } from "../../types";
export type CommandPaletteItem = {
label: string;
@@ -1,5 +1,6 @@
import { t } from "../i18n";
import { Dialog, DialogProps } from "./Dialog";
import type { DialogProps } from "./Dialog";
import { Dialog } from "./Dialog";
import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton";
@@ -1,14 +1,13 @@
import clsx from "clsx";
import { Popover } from "./Popover";
import { t, TranslationKeys } from "../i18n";
import type { TranslationKeys } from "../i18n";
import { t } from "../i18n";
import "./ContextMenu.scss";
import {
getShortcutFromShortcutName,
ShortcutName,
} from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import type { ShortcutName } from "../actions/shortcuts";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import type { Action } from "../actions/types";
import type { ActionManager } from "../actions/manager";
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
import React from "react";
@@ -3,7 +3,7 @@ import "./ToolIcon.scss";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import { THEME } from "../constants";
import { Theme } from "../element/types";
import type { Theme } from "../element/types";
// We chose to use only explicit toggle and not a third option for system value,
// but this could be added in the future.
@@ -3,12 +3,12 @@ import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n";
import { MarkOptional, Merge } from "../utility-types";
import type { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu";
import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback(
@@ -1,5 +1,5 @@
import clsx from "clsx";
import { ReactNode } from "react";
import type { ReactNode } from "react";
import "./DialogActionButton.scss";
import Spinner from "./Spinner";
@@ -12,8 +12,8 @@ import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import { useStable } from "../hooks/useStable";
import "./EyeDropper.scss";
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import { ExcalidrawElement } from "../element/types";
import type { ColorPickerType } from "./ColorPicker/colorPickerUtils";
import type { ExcalidrawElement } from "../element/types";
export type EyeDropperProperties = {
keepOpenOnAlt: boolean;
@@ -1,4 +1,4 @@
import { UserToFollow } from "../../types";
import type { UserToFollow } from "../../types";
import { CloseIcon } from "../icons";
import "./FollowMode.scss";
@@ -1,5 +1,5 @@
import { t } from "../i18n";
import { AppClassProperties, Device, UIAppState } from "../types";
import type { AppClassProperties, Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@@ -20,10 +20,10 @@ import {
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../../utils/export";
import { exportToCanvas } from "@excalidraw/utils";
import { copyIcon, downloadIcon, helpIcon } from "./icons";
import { Dialog } from "./Dialog";
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from "react";
import { LoadingMessage } from "./LoadingMessage";
import { defaultLang, Language, languages, setLanguage } from "../i18n";
import { Theme } from "../element/types";
import type { Language } from "../i18n";
import { defaultLang, languages, setLanguage } from "../i18n";
import type { Theme } from "../element/types";
interface Props {
langCode: Language["code"];
@@ -1,8 +1,8 @@
import React from "react";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import type { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton";
@@ -12,7 +12,7 @@ import { Card } from "./Card";
import "./ExportDialog.scss";
import { nativeFileSystemSupported } from "../data/filesystem";
import { trackEvent } from "../analytics";
import { ActionManager } from "../actions/manager";
import type { ActionManager } from "../actions/manager";
import { getFrame } from "../utils";
export type ExportCB = (
@@ -1,7 +1,7 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import { ToolButtonSize } from "./ToolButton";
import type { ToolButtonSize } from "./ToolButton";
import { laserPointerToolIcon } from "./icons";
type LaserPointerIconProps = {
+5 -4
View File
@@ -1,6 +1,6 @@
import clsx from "clsx";
import React from "react";
import { ActionManager } from "../actions/manager";
import type { ActionManager } from "../actions/manager";
import {
CLASSES,
DEFAULT_SIDEBAR,
@@ -8,10 +8,11 @@ import {
TOOL_TYPE,
} from "../constants";
import { showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import type { NonDeletedExcalidrawElement } from "../element/types";
import type { Language } from "../i18n";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import {
import type {
AppProps,
AppState,
ExcalidrawProps,
@@ -1,11 +1,12 @@
import React, { useState, useCallback, useMemo, useRef } from "react";
import Library, {
import type Library from "../data/library";
import {
distributeLibraryItemsOnSquareGrid,
libraryItemsAtom,
} from "../data/library";
import { t } from "../i18n";
import { randomId } from "../random";
import {
import type {
LibraryItems,
LibraryItem,
ExcalidrawProps,
@@ -28,7 +29,7 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import { NonDeletedExcalidrawElement } from "../element/types";
import type { NonDeletedExcalidrawElement } from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants";
export const isLibraryMenuOpenAtom = atom(false);
@@ -1,6 +1,6 @@
import { VERSIONS } from "../constants";
import { t } from "../i18n";
import { ExcalidrawProps, UIAppState } from "../types";
import type { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({
theme,
@@ -1,4 +1,4 @@
import { ExcalidrawProps, UIAppState } from "../types";
import type { ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx";
@@ -2,10 +2,11 @@ import { useCallback, useState } from "react";
import { t } from "../i18n";
import Trans from "./Trans";
import { jotaiScope } from "../jotai";
import { LibraryItem, LibraryItems, UIAppState } from "../types";
import type { LibraryItem, LibraryItems, UIAppState } from "../types";
import { useApp, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library";
import type Library from "../data/library";
import { libraryItemsAtom } from "../data/library";
import {
DotsIcon,
ExportIcon,
@@ -7,7 +7,7 @@ import React, {
} from "react";
import { serializeLibraryAsJSON } from "../data/json";
import { t } from "../i18n";
import {
import type {
ExcalidrawProps,
LibraryItem,
LibraryItems,

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