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