diff --git a/src/components/App.tsx b/src/components/App.tsx index 2c0583b26..bf1c989b3 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -248,6 +248,7 @@ export type ExcalidrawImperativeAPI = { }; setScrollToCenter: InstanceType["setScrollToCenter"]; getSceneElements: InstanceType["getSceneElements"]; + getAppState: () => InstanceType["state"]; readyPromise: ResolvablePromise; ready: true; }; @@ -298,6 +299,7 @@ class App extends React.Component { }, setScrollToCenter: this.setScrollToCenter, getSceneElements: this.getSceneElements, + getAppState: () => this.state, } as const; if (typeof excalidrawRef === "function") { excalidrawRef(api); diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 217b625c4..507dc1772 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -1,5 +1,6 @@ import clsx from "clsx"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect } from "react"; +import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { t } from "../i18n"; import useIsMobile from "../is-mobile"; import { KEYS } from "../keys"; @@ -8,14 +9,6 @@ import { back, close } from "./icons"; import { Island } from "./Island"; import { Modal } from "./Modal"; -const useRefState = () => { - const [refValue, setRefValue] = useState(null); - const refCallback = useCallback((value: T) => { - setRefValue(value); - }, []); - return [refValue, refCallback] as const; -}; - export const Dialog = (props: { children: React.ReactNode; className?: string; @@ -24,7 +17,7 @@ export const Dialog = (props: { title: React.ReactNode; autofocus?: boolean; }) => { - const [islandNode, setIslandNode] = useRefState(); + const [islandNode, setIslandNode] = useCallbackRefState(); useEffect(() => { if (!islandNode) { diff --git a/src/createInverseContext.tsx b/src/createInverseContext.tsx new file mode 100644 index 000000000..ac6cc223e --- /dev/null +++ b/src/createInverseContext.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +export const createInverseContext = ( + initialValue: T, +) => { + const Context = React.createContext(initialValue) as React.Context & { + _updateProviderValue?: (value: T) => void; + }; + + class InverseConsumer extends React.Component { + state = { value: initialValue }; + constructor(props: any) { + super(props); + Context._updateProviderValue = (value: T) => this.setState({ value }); + } + render() { + return ( + + {this.props.children} + + ); + } + } + + class InverseProvider extends React.Component<{ value: T }> { + componentDidMount() { + Context._updateProviderValue?.(this.props.value); + } + componentDidUpdate() { + Context._updateProviderValue?.(this.props.value); + } + render() { + return {() => this.props.children}; + } + } + + return { + Context, + Consumer: InverseConsumer, + Provider: InverseProvider, + }; +}; diff --git a/src/excalidraw-app/collab/CollabWrapper.tsx b/src/excalidraw-app/collab/CollabWrapper.tsx index d27adb35d..c308a70d6 100644 --- a/src/excalidraw-app/collab/CollabWrapper.tsx +++ b/src/excalidraw-app/collab/CollabWrapper.tsx @@ -6,10 +6,11 @@ import { APP_NAME, ENV, EVENT } from "../../constants"; import { ImportedDataState } from "../../data/types"; import { ExcalidrawElement } from "../../element/types"; import { + getElementMap, getSceneVersion, getSyncableElements, } from "../../packages/excalidraw/index"; -import { AppState, Collaborator, Gesture } from "../../types"; +import { Collaborator, Gesture } from "../../types"; import { resolvablePromise, withBatchedUpdates } from "../../utils"; import { INITIAL_SCENE_UPDATE_TIMEOUT, @@ -31,6 +32,7 @@ import { } from "../data/localStorage"; import Portal from "./Portal"; import RoomDialog from "./RoomDialog"; +import { createInverseContext } from "../../createInverseContext"; interface CollabState { isCollaborating: boolean; @@ -56,17 +58,21 @@ type ReconciledElements = readonly ExcalidrawElement[] & { }; interface Props { - children: (collab: CollabAPI) => React.ReactNode; - // NOTE not type-safe because the refObject may in fact not be initialized - // with ExcalidrawImperativeAPI yet - excalidrawRef: React.MutableRefObject; + excalidrawAPI: ExcalidrawImperativeAPI; } +const { + Context: CollabContext, + Consumer: CollabContextConsumer, + Provider: CollabContextProvider, +} = createInverseContext<{ api: CollabAPI | null }>({ api: null }); + +export { CollabContext, CollabContextConsumer }; + class CollabWrapper extends PureComponent { portal: Portal; + excalidrawAPI: Props["excalidrawAPI"]; private socketInitializationTimer?: NodeJS.Timeout; - private excalidrawRef: Props["excalidrawRef"]; - excalidrawAppState?: AppState; private lastBroadcastedOrReceivedSceneVersion: number = -1; private collaborators = new Map(); @@ -80,7 +86,7 @@ class CollabWrapper extends PureComponent { activeRoomLink: "", }; this.portal = new Portal(this); - this.excalidrawRef = props.excalidrawRef; + this.excalidrawAPI = props.excalidrawAPI; } componentDidMount() { @@ -142,7 +148,7 @@ class CollabWrapper extends PureComponent { saveCollabRoomToFirebase = async ( syncableElements: ExcalidrawElement[] = getSyncableElements( - this.excalidrawRef.current!.getSceneElementsIncludingDeleted(), + this.excalidrawAPI.getSceneElementsIncludingDeleted(), ), ) => { try { @@ -154,13 +160,13 @@ class CollabWrapper extends PureComponent { openPortal = async () => { window.history.pushState({}, APP_NAME, await generateCollaborationLink()); - const elements = this.excalidrawRef.current!.getSceneElements(); + const elements = this.excalidrawAPI.getSceneElements(); // remove deleted elements from elements array & history to ensure we don't // expose potentially sensitive user data in case user manually deletes // existing elements (or clears scene), which would otherwise be persisted // to database even if deleted before creating the room. - this.excalidrawRef.current!.history.clear(); - this.excalidrawRef.current!.updateScene({ + this.excalidrawAPI.history.clear(); + this.excalidrawAPI.updateScene({ elements, commitToHistory: true, }); @@ -175,7 +181,7 @@ class CollabWrapper extends PureComponent { private destroySocketClient = () => { this.collaborators = new Map(); - this.excalidrawRef.current!.updateScene({ + this.excalidrawAPI.updateScene({ collaborators: this.collaborators, }); this.setState({ @@ -265,7 +271,7 @@ class CollabWrapper extends PureComponent { user.selectedElementIds = selectedElementIds; user.username = username; collaborators.set(socketId, user); - this.excalidrawRef.current!.updateScene({ + this.excalidrawAPI.updateScene({ collaborators, }); break; @@ -300,7 +306,55 @@ class CollabWrapper extends PureComponent { private reconcileElements = ( elements: readonly ExcalidrawElement[], ): ReconciledElements => { - const newElements = this.portal.reconcileElements(elements); + const currentElements = this.getSceneElementsIncludingDeleted(); + // create a map of ids so we don't have to iterate + // over the array more than once. + const localElementMap = getElementMap(currentElements); + + const appState = this.excalidrawAPI.getAppState(); + + // Reconcile + const newElements: readonly ExcalidrawElement[] = elements + .reduce((elements, element) => { + // if the remote element references one that's currently + // edited on local, skip it (it'll be added in the next step) + if ( + element.id === appState.editingElement?.id || + element.id === appState.resizingElement?.id || + element.id === appState.draggingElement?.id + ) { + return elements; + } + + if ( + localElementMap.hasOwnProperty(element.id) && + localElementMap[element.id].version > element.version + ) { + elements.push(localElementMap[element.id]); + delete localElementMap[element.id]; + } else if ( + localElementMap.hasOwnProperty(element.id) && + localElementMap[element.id].version === element.version && + localElementMap[element.id].versionNonce !== element.versionNonce + ) { + // resolve conflicting edits deterministically by taking the one with the lowest versionNonce + if (localElementMap[element.id].versionNonce < element.versionNonce) { + elements.push(localElementMap[element.id]); + } else { + // it should be highly unlikely that the two versionNonces are the same. if we are + // really worried about this, we can replace the versionNonce with the socket id. + elements.push(element); + } + delete localElementMap[element.id]; + } else { + elements.push(element); + delete localElementMap[element.id]; + } + + return elements; + }, [] as Mutable) + // add local elements that weren't deleted or on remote + .concat(...Object.values(localElementMap)); // Avoid broadcasting to the rest of the collaborators the scene // we just received! @@ -319,10 +373,10 @@ class CollabWrapper extends PureComponent { }: { init?: boolean; initFromSnapshot?: boolean } = {}, ) => { if (init || initFromSnapshot) { - this.excalidrawRef.current!.setScrollToCenter(elements); + this.excalidrawAPI.setScrollToCenter(elements); } - this.excalidrawRef.current!.updateScene({ + this.excalidrawAPI.updateScene({ elements, commitToHistory: !!init, }); @@ -331,7 +385,7 @@ class CollabWrapper extends PureComponent { // when we receive any messages from another peer. This UX can be pretty rough -- if you // undo, a user makes a change, and then try to redo, your element(s) will be lost. However, // right now we think this is the right tradeoff. - this.excalidrawRef.current!.history.clear(); + this.excalidrawAPI.history.clear(); }; setCollaborators(sockets: string[]) { @@ -347,7 +401,7 @@ class CollabWrapper extends PureComponent { } } this.collaborators = collaborators; - this.excalidrawRef.current!.updateScene({ collaborators }); + this.excalidrawAPI.updateScene({ collaborators }); }); } @@ -360,7 +414,7 @@ class CollabWrapper extends PureComponent { }; public getSceneElementsIncludingDeleted = () => { - return this.excalidrawRef.current!.getSceneElementsIncludingDeleted(); + return this.excalidrawAPI.getSceneElementsIncludingDeleted(); }; onPointerUpdate = (payload: { @@ -373,11 +427,7 @@ class CollabWrapper extends PureComponent { this.portal.broadcastMouseLocation(payload); }; - broadcastElements = ( - elements: readonly ExcalidrawElement[], - state: AppState, - ) => { - this.excalidrawAppState = state; + broadcastElements = (elements: readonly ExcalidrawElement[]) => { if ( getSceneVersion(elements) > this.getLastBroadcastedOrReceivedSceneVersion() @@ -396,7 +446,7 @@ class CollabWrapper extends PureComponent { this.portal.broadcastScene( SCENE.UPDATE, getSyncableElements( - this.excalidrawRef.current!.getSceneElementsIncludingDeleted(), + this.excalidrawAPI.getSceneElementsIncludingDeleted(), ), true, ); @@ -425,8 +475,23 @@ class CollabWrapper extends PureComponent { }); }; + /** PRIVATE. Use `this.getContextValue()` instead. */ + private contextValue: CollabAPI | null = null; + + /** Getter of context value. Returned object is stable. */ + getContextValue = (): CollabAPI => { + this.contextValue = this.contextValue || ({} as CollabAPI); + + this.contextValue.isCollaborating = this.state.isCollaborating; + this.contextValue.username = this.state.username; + this.contextValue.onPointerUpdate = this.onPointerUpdate; + this.contextValue.initializeSocketClient = this.initializeSocketClient; + this.contextValue.onCollabButtonClick = this.onCollabButtonClick; + this.contextValue.broadcastElements = this.broadcastElements; + return this.contextValue; + }; + render() { - const { children } = this.props; const { modalIsShown, username, errorMessage, activeRoomLink } = this.state; return ( @@ -450,14 +515,11 @@ class CollabWrapper extends PureComponent { onClose={() => this.setState({ errorMessage: "" })} /> )} - {children({ - isCollaborating: this.state.isCollaborating, - username: this.state.username, - onPointerUpdate: this.onPointerUpdate, - initializeSocketClient: this.initializeSocketClient, - onCollabButtonClick: this.onCollabButtonClick, - broadcastElements: this.broadcastElements, - })} + ); } diff --git a/src/excalidraw-app/collab/Portal.tsx b/src/excalidraw-app/collab/Portal.tsx index 2b66b0bd0..92086a27b 100644 --- a/src/excalidraw-app/collab/Portal.tsx +++ b/src/excalidraw-app/collab/Portal.tsx @@ -6,23 +6,20 @@ import { import CollabWrapper from "./CollabWrapper"; -import { - getElementMap, - getSyncableElements, -} from "../../packages/excalidraw/index"; +import { getSyncableElements } from "../../packages/excalidraw/index"; import { ExcalidrawElement } from "../../element/types"; import { BROADCAST, SCENE } from "../app_constants"; class Portal { - app: CollabWrapper; + collab: CollabWrapper; socket: SocketIOClient.Socket | null = null; socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized roomId: string | null = null; roomKey: string | null = null; broadcastedElementVersions: Map = new Map(); - constructor(app: CollabWrapper) { - this.app = app; + constructor(collab: CollabWrapper) { + this.collab = collab; } open(socket: SocketIOClient.Socket, id: string, key: string) { @@ -30,7 +27,7 @@ class Portal { this.roomId = id; this.roomKey = key; - // Initialize socket listeners (moving from App) + // Initialize socket listeners this.socket.on("init-room", () => { if (this.socket) { this.socket.emit("join-room", this.roomId); @@ -39,12 +36,12 @@ class Portal { this.socket.on("new-user", async (_socketId: string) => { this.broadcastScene( SCENE.INIT, - getSyncableElements(this.app.getSceneElementsIncludingDeleted()), + getSyncableElements(this.collab.getSceneElementsIncludingDeleted()), /* syncAll */ true, ); }); this.socket.on("room-user-change", (clients: string[]) => { - this.app.setCollaborators(clients); + this.collab.setCollaborators(clients); }); } @@ -125,10 +122,10 @@ class Portal { data as SocketUpdateData, ); - if (syncAll && this.app.state.isCollaborating) { + if (syncAll && this.collab.state.isCollaborating) { await Promise.all([ broadcastPromise, - this.app.saveCollabRoomToFirebase(syncableElements), + this.collab.saveCollabRoomToFirebase(syncableElements), ]); } else { await broadcastPromise; @@ -146,9 +143,9 @@ class Portal { socketId: this.socket.id, pointer: payload.pointer, button: payload.button || "up", - selectedElementIds: - this.app.excalidrawAppState?.selectedElementIds || {}, - username: this.app.state.username, + selectedElementIds: this.collab.excalidrawAPI.getAppState() + .selectedElementIds, + username: this.collab.state.username, }, }; return this._broadcastSocketData( @@ -157,62 +154,6 @@ class Portal { ); } }; - - reconcileElements = ( - sceneElements: readonly ExcalidrawElement[], - ): readonly ExcalidrawElement[] => { - const currentElements = this.app.getSceneElementsIncludingDeleted(); - // create a map of ids so we don't have to iterate - // over the array more than once. - const localElementMap = getElementMap(currentElements); - - // Reconcile - return ( - sceneElements - .reduce((elements, element) => { - // if the remote element references one that's currently - // edited on local, skip it (it'll be added in the next step) - if ( - element.id === this.app.excalidrawAppState?.editingElement?.id || - element.id === this.app.excalidrawAppState?.resizingElement?.id || - element.id === this.app.excalidrawAppState?.draggingElement?.id - ) { - return elements; - } - - if ( - localElementMap.hasOwnProperty(element.id) && - localElementMap[element.id].version > element.version - ) { - elements.push(localElementMap[element.id]); - delete localElementMap[element.id]; - } else if ( - localElementMap.hasOwnProperty(element.id) && - localElementMap[element.id].version === element.version && - localElementMap[element.id].versionNonce !== element.versionNonce - ) { - // resolve conflicting edits deterministically by taking the one with the lowest versionNonce - if ( - localElementMap[element.id].versionNonce < element.versionNonce - ) { - elements.push(localElementMap[element.id]); - } else { - // it should be highly unlikely that the two versionNonces are the same. if we are - // really worried about this, we can replace the versionNonce with the socket id. - elements.push(element); - } - delete localElementMap[element.id]; - } else { - elements.push(element); - delete localElementMap[element.id]; - } - - return elements; - }, [] as Mutable) - // add local elements that weren't deleted or on remote - .concat(...Object.values(localElementMap)) - ); - }; } export default Portal; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index e90664e8e..fcc7f45f7 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -1,6 +1,7 @@ import LanguageDetector from "i18next-browser-languagedetector"; import React, { useCallback, + useContext, useEffect, useLayoutEffect, useRef, @@ -17,12 +18,13 @@ import { ExcalidrawElement, NonDeletedExcalidrawElement, } from "../element/types"; +import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { Language, t } from "../i18n"; import Excalidraw, { defaultLang, languages, } from "../packages/excalidraw/index"; -import { AppState, ExcalidrawAPIRefValue } from "../types"; +import { AppState } from "../types"; import { debounce, getVersion, @@ -30,7 +32,11 @@ import { resolvablePromise, } from "../utils"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants"; -import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper"; +import CollabWrapper, { + CollabAPI, + CollabContext, + CollabContextConsumer, +} from "./collab/CollabWrapper"; import { LanguageList } from "./components/LanguageList"; import { exportToBackend, getCollaborationLinkData, loadScene } from "./data"; import { loadFromFirebase } from "./data/firebase"; @@ -49,15 +55,6 @@ languageDetector.init({ checkWhitelist: false, }); -const excalidrawRef: React.MutableRefObject< - MarkRequired -> = { - current: { - readyPromise: resolvablePromise(), - ready: false, - }, -}; - const saveDebounced = debounce( (elements: readonly ExcalidrawElement[], state: AppState) => { saveToLocalStorage(elements, state); @@ -191,7 +188,7 @@ const initializeScene = async (opts: { return null; }; -function ExcalidrawWrapper(props: { collab: CollabAPI }) { +function ExcalidrawWrapper() { // dimensions // --------------------------------------------------------------------------- @@ -226,35 +223,40 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) { initialStatePromiseRef.current.promise = resolvablePromise(); } - const { collab } = props; - useEffect(() => { // Delayed so that the app has a time to load the latest SW setTimeout(() => { trackEvent("load", "version", getVersion()); }, VERSION_TIMEOUT); + }, []); - excalidrawRef.current!.readyPromise.then((excalidrawApi) => { - initializeScene({ - resetScene: excalidrawApi.resetScene, - initializeSocketClient: collab.initializeSocketClient, - }).then((scene) => { - initialStatePromiseRef.current.promise.resolve(scene); - }); + const [ + excalidrawAPI, + excalidrawRefCallback, + ] = useCallbackRefState(); + + const collabAPI = useContext(CollabContext)?.api; + + useEffect(() => { + if (!collabAPI || !excalidrawAPI) { + return; + } + + initializeScene({ + resetScene: excalidrawAPI.resetScene, + initializeSocketClient: collabAPI.initializeSocketClient, + }).then((scene) => { + initialStatePromiseRef.current.promise.resolve(scene); }); const onHashChange = (_: HashChangeEvent) => { - const api = excalidrawRef.current!; - if (!api.ready) { - return; - } if (window.location.hash.length > 1) { initializeScene({ - resetScene: api.resetScene, - initializeSocketClient: collab.initializeSocketClient, + resetScene: excalidrawAPI.resetScene, + initializeSocketClient: collabAPI.initializeSocketClient, }).then((scene) => { if (scene) { - api.updateScene(scene); + excalidrawAPI.updateScene(scene); } }); } @@ -273,7 +275,7 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) { window.removeEventListener(EVENT.BLUR, onBlur, false); clearTimeout(titleTimeout); }; - }, [collab.initializeSocketClient]); + }, [collabAPI, excalidrawAPI]); useEffect(() => { languageDetector.cacheUserLanguage(langCode); @@ -284,8 +286,8 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) { appState: AppState, ) => { saveDebounced(elements, appState); - if (collab.isCollaborating) { - collab.broadcastElements(elements, appState); + if (collabAPI?.isCollaborating) { + collabAPI.broadcastElements(elements); } }; @@ -343,19 +345,20 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) { return ( <> + {excalidrawAPI && } {errorMessage && ( - - } - > - {(collab) => } - + + + ); } diff --git a/src/hooks/useCallbackRefState.ts b/src/hooks/useCallbackRefState.ts new file mode 100644 index 000000000..4a8552b58 --- /dev/null +++ b/src/hooks/useCallbackRefState.ts @@ -0,0 +1,7 @@ +import { useCallback, useState } from "react"; + +export const useCallbackRefState = () => { + const [refValue, setRefValue] = useState(null); + const refCallback = useCallback((value: T | null) => setRefValue(value), []); + return [refValue, refCallback] as const; +}; diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 7ab2dd7cd..ba90fd513 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -14,6 +14,10 @@ Please add the latest change on the top under the correct section. ## [Unreleased] +## Excalidraw API + +- Expose `getAppState` on `excalidrawRef` [#2834](https://github.com/excalidraw/excalidraw/pull/2834). + ## Excalidraw Library ### Features diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index 9cad15d76..409c3fba0 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -147,7 +147,7 @@ export default function App() {
 import { getSceneVersion } from "@excalidraw/excalidraw";
-getSceneVersion(elements:  ExcalidrawElement [])
+getSceneVersion(elements:  ExcalidrawElement[])
 
This function returns the current scene version. @@ -157,7 +157,7 @@ This function returns the current scene version. **_Signature_**
-getSyncableElements(elements:  ExcalidrawElement []):ExcalidrawElement []
+getSyncableElements(elements:  ExcalidrawElement[]):ExcalidrawElement[]
 
**How to use** @@ -173,7 +173,7 @@ This function returns all the deleted elements of the scene. **_Signature_**
-getElementsMap(elements:  ExcalidrawElement []): {[id: string]: ExcalidrawElement}
+getElementsMap(elements:  ExcalidrawElement[]): {[id: string]: ExcalidrawElement}
 
**How to use** @@ -220,7 +220,7 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro | name | type | | --- | --- | -| elements | [ExcalidrawElement []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | +| elements | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) | ```json @@ -268,8 +268,9 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the | readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) | | updateScene |
(sceneData)) => void 
| updates the scene with the sceneData | | resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | -| getSceneElementsIncludingDeleted |
 () => ExcalidrawElement []
| Returns all the elements including the deleted in the scene | -| getSceneElements |
 () => ExcalidrawElement []
| Returns all the elements excluding the deleted in the scene | +| getSceneElementsIncludingDeleted |
 () => ExcalidrawElement[]
| Returns all the elements including the deleted in the scene | +| getSceneElements |
 () => ExcalidrawElement[]
| Returns all the elements excluding the deleted in the scene | +| getAppState |
 () => AppState
| Returns current appState | | history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history | | setScrollToCenter |
 (ExcalidrawElement[]) => void 
| sets the elements to center | @@ -324,7 +325,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw"; | name | type | | --- | --- | | defaultLang | string | -| languages | [Language []](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) | +| languages | [Language[]](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) | #### `renderFooter` diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index ce23cf2de..8c8bf78a5 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -457,7 +457,7 @@ Object { exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of elements 1`] = `3`; -exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of renders 1`] = `25`; +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of renders 1`] = `26`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] appState 1`] = ` Object { @@ -922,7 +922,7 @@ Object { exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of elements 1`] = `3`; -exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `21`; +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `22`; exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = ` Object { @@ -1696,7 +1696,7 @@ Object { exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of elements 1`] = `3`; -exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `40`; +exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `41`; exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = ` Object { @@ -1898,7 +1898,7 @@ Object { exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of elements 1`] = `1`; -exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of renders 1`] = `9`; +exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of renders 1`] = `10`; exports[`regression tests adjusts z order when grouping: [end of test] appState 1`] = ` Object { @@ -2354,7 +2354,7 @@ Object { exports[`regression tests adjusts z order when grouping: [end of test] number of elements 1`] = `3`; -exports[`regression tests adjusts z order when grouping: [end of test] number of renders 1`] = `19`; +exports[`regression tests adjusts z order when grouping: [end of test] number of renders 1`] = `20`; exports[`regression tests alt-drag duplicates an element: [end of test] appState 1`] = ` Object { @@ -2605,7 +2605,7 @@ Object { exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `2`; -exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `9`; +exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `10`; exports[`regression tests arrow keys: [end of test] appState 1`] = ` Object { @@ -2767,7 +2767,7 @@ Object { exports[`regression tests arrow keys: [end of test] number of elements 1`] = `1`; -exports[`regression tests arrow keys: [end of test] number of renders 1`] = `18`; +exports[`regression tests arrow keys: [end of test] number of renders 1`] = `19`; exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] appState 1`] = ` Object { @@ -3242,7 +3242,7 @@ Object { exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of elements 1`] = `3`; -exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of renders 1`] = `17`; +exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of renders 1`] = `18`; exports[`regression tests change the properties of a shape: [end of test] appState 1`] = ` Object { @@ -3548,7 +3548,7 @@ Object { exports[`regression tests change the properties of a shape: [end of test] number of elements 1`] = `1`; -exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `10`; +exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `11`; exports[`regression tests click on an element and drag it: [dragged] appState 1`] = ` Object { @@ -3750,7 +3750,7 @@ Object { exports[`regression tests click on an element and drag it: [dragged] number of elements 1`] = `1`; -exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `9`; +exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `10`; exports[`regression tests click on an element and drag it: [end of test] appState 1`] = ` Object { @@ -3992,7 +3992,7 @@ Object { exports[`regression tests click on an element and drag it: [end of test] number of elements 1`] = `1`; -exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `12`; +exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `13`; exports[`regression tests click to select a shape: [end of test] appState 1`] = ` Object { @@ -4242,7 +4242,7 @@ Object { exports[`regression tests click to select a shape: [end of test] number of elements 1`] = `2`; -exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `12`; +exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `13`; exports[`regression tests click-drag to select a group: [end of test] appState 1`] = ` Object { @@ -4601,7 +4601,7 @@ Object { exports[`regression tests click-drag to select a group: [end of test] number of elements 1`] = `3`; -exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `18`; +exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `19`; exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` Object { @@ -4894,7 +4894,7 @@ Object { exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `2`; -exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `13`; +exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `14`; exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] appState 1`] = ` Object { @@ -5199,7 +5199,7 @@ Object { exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of elements 1`] = `2`; -exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of renders 1`] = `14`; +exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of renders 1`] = `15`; exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` Object { @@ -5405,7 +5405,7 @@ Object { exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `1`; -exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `7`; +exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `8`; exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] appState 1`] = ` Object { @@ -5589,7 +5589,7 @@ Object { exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of elements 1`] = `1`; -exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of renders 1`] = `8`; +exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of renders 1`] = `9`; exports[`regression tests double click to edit a group: [end of test] appState 1`] = ` Object { @@ -6040,7 +6040,7 @@ Object { exports[`regression tests double click to edit a group: [end of test] number of elements 1`] = `3`; -exports[`regression tests double click to edit a group: [end of test] number of renders 1`] = `17`; +exports[`regression tests double click to edit a group: [end of test] number of renders 1`] = `18`; exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] appState 1`] = ` Object { @@ -6356,7 +6356,7 @@ Object { exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of elements 1`] = `2`; -exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of renders 1`] = `15`; +exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of renders 1`] = `16`; exports[`regression tests draw every type of shape: [end of test] appState 1`] = ` Object { @@ -8388,7 +8388,7 @@ Object { exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `8`; -exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `50`; +exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `51`; exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] appState 1`] = ` Object { @@ -8748,7 +8748,7 @@ Object { exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of elements 1`] = `3`; -exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of renders 1`] = `18`; +exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of renders 1`] = `19`; exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] appState 1`] = ` Object { @@ -9001,7 +9001,7 @@ Object { exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of elements 1`] = `2`; -exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of renders 1`] = `16`; +exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of renders 1`] = `17`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] appState 1`] = ` Object { @@ -9252,7 +9252,7 @@ Object { exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of elements 1`] = `2`; -exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of renders 1`] = `16`; +exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of renders 1`] = `17`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] appState 1`] = ` Object { @@ -9565,7 +9565,7 @@ Object { exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of elements 1`] = `2`; -exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of renders 1`] = `17`; +exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of renders 1`] = `18`; exports[`regression tests key 2 selects rectangle tool: [end of test] appState 1`] = ` Object { @@ -9727,7 +9727,7 @@ Object { exports[`regression tests key 2 selects rectangle tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 2 selects rectangle tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key 2 selects rectangle tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 3 selects diamond tool: [end of test] appState 1`] = ` Object { @@ -9889,7 +9889,7 @@ Object { exports[`regression tests key 3 selects diamond tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 3 selects diamond tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key 3 selects diamond tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 4 selects ellipse tool: [end of test] appState 1`] = ` Object { @@ -10051,7 +10051,7 @@ Object { exports[`regression tests key 4 selects ellipse tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 4 selects ellipse tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key 4 selects ellipse tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 5 selects arrow tool: [end of test] appState 1`] = ` Object { @@ -10243,7 +10243,7 @@ Object { exports[`regression tests key 5 selects arrow tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 5 selects arrow tool: [end of test] number of renders 1`] = `7`; +exports[`regression tests key 5 selects arrow tool: [end of test] number of renders 1`] = `8`; exports[`regression tests key 6 selects line tool: [end of test] appState 1`] = ` Object { @@ -10435,7 +10435,7 @@ Object { exports[`regression tests key 6 selects line tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 7 selects draw tool: [end of test] appState 1`] = ` Object { @@ -10627,7 +10627,7 @@ Object { exports[`regression tests key 7 selects draw tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = ` Object { @@ -10819,7 +10819,7 @@ Object { exports[`regression tests key a selects arrow tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key a selects arrow tool: [end of test] number of renders 1`] = `7`; +exports[`regression tests key a selects arrow tool: [end of test] number of renders 1`] = `8`; exports[`regression tests key d selects diamond tool: [end of test] appState 1`] = ` Object { @@ -10981,7 +10981,7 @@ Object { exports[`regression tests key d selects diamond tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key d selects diamond tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key d selects diamond tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key e selects ellipse tool: [end of test] appState 1`] = ` Object { @@ -11143,7 +11143,7 @@ Object { exports[`regression tests key e selects ellipse tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key e selects ellipse tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key e selects ellipse tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key l selects line tool: [end of test] appState 1`] = ` Object { @@ -11335,7 +11335,7 @@ Object { exports[`regression tests key l selects line tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key l selects line tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key l selects line tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key r selects rectangle tool: [end of test] appState 1`] = ` Object { @@ -11497,7 +11497,7 @@ Object { exports[`regression tests key r selects rectangle tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key x selects draw tool: [end of test] appState 1`] = ` Object { @@ -11689,7 +11689,7 @@ Object { exports[`regression tests key x selects draw tool: [end of test] number of elements 1`] = `1`; -exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `6`; +exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `7`; exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = ` Object { @@ -12403,7 +12403,7 @@ Object { exports[`regression tests make a group and duplicate it: [end of test] number of elements 1`] = `6`; -exports[`regression tests make a group and duplicate it: [end of test] number of renders 1`] = `21`; +exports[`regression tests make a group and duplicate it: [end of test] number of renders 1`] = `22`; exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] appState 1`] = ` Object { @@ -12654,7 +12654,7 @@ Object { exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of elements 1`] = `2`; -exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of renders 1`] = `18`; +exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of renders 1`] = `19`; exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = ` Object { @@ -12754,7 +12754,7 @@ Object { exports[`regression tests pinch-to-zoom works: [end of test] number of elements 1`] = `0`; -exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `8`; +exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `9`; exports[`regression tests rerenders UI on language change: [end of test] appState 1`] = ` Object { @@ -12852,7 +12852,7 @@ Object { exports[`regression tests rerenders UI on language change: [end of test] number of elements 1`] = `0`; -exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `7`; +exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] appState 1`] = ` Object { @@ -13014,7 +13014,7 @@ Object { exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of elements 1`] = `1`; -exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `6`; +exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `7`; exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] appState 1`] = ` Object { @@ -13320,7 +13320,7 @@ Object { exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `12`; +exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `13`; exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] appState 1`] = ` Object { @@ -13626,7 +13626,7 @@ Object { exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `12`; +exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `13`; exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] appState 1`] = ` Object { @@ -13788,7 +13788,7 @@ Object { exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of elements 1`] = `1`; -exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `7`; +exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] appState 1`] = ` Object { @@ -13982,7 +13982,7 @@ Object { exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of elements 1`] = `1`; -exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `7`; +exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] appState 1`] = ` Object { @@ -14229,7 +14229,7 @@ Object { exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `7`; +exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] appState 1`] = ` Object { @@ -14551,7 +14551,7 @@ Object { exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `13`; +exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `14`; exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] appState 1`] = ` Object { @@ -15388,7 +15388,7 @@ Object { exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `22`; +exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `23`; exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] appState 1`] = ` Object { @@ -15694,7 +15694,7 @@ Object { exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `11`; +exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `12`; exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] appState 1`] = ` Object { @@ -16000,7 +16000,7 @@ Object { exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `11`; +exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `12`; exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] appState 1`] = ` Object { @@ -16377,7 +16377,7 @@ Object { exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of elements 1`] = `2`; -exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `14`; +exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `15`; exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = ` Object { @@ -16542,7 +16542,7 @@ Object { exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of elements 1`] = `1`; -exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of renders 1`] = `8`; +exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of renders 1`] = `9`; exports[`regression tests shift-click to multiselect, then drag: [end of test] appState 1`] = ` Object { @@ -16861,7 +16861,7 @@ Object { exports[`regression tests shift-click to multiselect, then drag: [end of test] number of elements 1`] = `2`; -exports[`regression tests shift-click to multiselect, then drag: [end of test] number of renders 1`] = `17`; +exports[`regression tests shift-click to multiselect, then drag: [end of test] number of renders 1`] = `18`; exports[`regression tests should show fill icons when element has non transparent background: [end of test] appState 1`] = ` Object { @@ -17098,7 +17098,7 @@ Object { exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of elements 1`] = `1`; -exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of renders 1`] = `10`; +exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of renders 1`] = `11`; exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = ` Object { @@ -17351,7 +17351,7 @@ Object { exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of elements 1`] = `2`; -exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `14`; +exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `15`; exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = ` Object { @@ -17676,7 +17676,7 @@ Object { exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of elements 1`] = `2`; -exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `15`; +exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `16`; exports[`regression tests shows context menu for canvas: [end of test] appState 1`] = ` Object { @@ -17774,7 +17774,7 @@ Object { exports[`regression tests shows context menu for canvas: [end of test] number of elements 1`] = `0`; -exports[`regression tests shows context menu for canvas: [end of test] number of renders 1`] = `2`; +exports[`regression tests shows context menu for canvas: [end of test] number of renders 1`] = `3`; exports[`regression tests shows context menu for element: [end of test] appState 1`] = ` Object { @@ -17936,7 +17936,7 @@ Object { exports[`regression tests shows context menu for element: [end of test] number of elements 1`] = `1`; -exports[`regression tests shows context menu for element: [end of test] number of renders 1`] = `6`; +exports[`regression tests shows context menu for element: [end of test] number of renders 1`] = `7`; exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] appState 1`] = ` Object { @@ -18755,7 +18755,7 @@ Object { exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of elements 1`] = `4`; -exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of renders 1`] = `36`; +exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of renders 1`] = `37`; exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appState 1`] = ` Object { @@ -18853,7 +18853,7 @@ Object { exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of elements 1`] = `0`; -exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `5`; +exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `6`; exports[`regression tests supports nested groups: [end of test] appState 1`] = ` Object { @@ -19583,7 +19583,7 @@ Object { exports[`regression tests supports nested groups: [end of test] number of elements 1`] = `3`; -exports[`regression tests supports nested groups: [end of test] number of renders 1`] = `29`; +exports[`regression tests supports nested groups: [end of test] number of renders 1`] = `30`; exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] appState 1`] = ` Object { @@ -19986,7 +19986,7 @@ Object { exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of elements 1`] = `3`; -exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of renders 1`] = `17`; +exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of renders 1`] = `18`; exports[`regression tests switches selected element on pointer down: [end of test] appState 1`] = ` Object { @@ -20279,7 +20279,7 @@ Object { exports[`regression tests switches selected element on pointer down: [end of test] number of elements 1`] = `2`; -exports[`regression tests switches selected element on pointer down: [end of test] number of renders 1`] = `11`; +exports[`regression tests switches selected element on pointer down: [end of test] number of renders 1`] = `12`; exports[`regression tests two-finger scroll works: [end of test] appState 1`] = ` Object { @@ -20379,7 +20379,7 @@ Object { exports[`regression tests two-finger scroll works: [end of test] number of elements 1`] = `0`; -exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `10`; +exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `11`; exports[`regression tests undo/redo drawing an element: [end of test] appState 1`] = ` Object { @@ -20875,7 +20875,7 @@ Object { exports[`regression tests undo/redo drawing an element: [end of test] number of elements 1`] = `3`; -exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `27`; +exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `28`; exports[`regression tests updates fontSize & fontFamily appState: [end of test] appState 1`] = ` Object { @@ -20973,7 +20973,7 @@ Object { exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of elements 1`] = `0`; -exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of renders 1`] = `4`; +exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of renders 1`] = `5`; exports[`regression tests zoom hotkeys: [end of test] appState 1`] = ` Object { @@ -21071,4 +21071,4 @@ Object { exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`; -exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `4`; +exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `5`; diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index ae08977c3..7a55ed89a 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -37,7 +37,7 @@ describe("add element to the scene when pointer dragging long enough", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -68,7 +68,7 @@ describe("add element to the scene when pointer dragging long enough", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -99,7 +99,7 @@ describe("add element to the scene when pointer dragging long enough", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -130,7 +130,7 @@ describe("add element to the scene when pointer dragging long enough", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -165,7 +165,7 @@ describe("add element to the scene when pointer dragging long enough", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -198,7 +198,7 @@ describe("do not add element to the scene if size is too small", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -217,7 +217,7 @@ describe("do not add element to the scene if size is too small", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -236,7 +236,7 @@ describe("do not add element to the scene if size is too small", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -258,7 +258,7 @@ describe("do not add element to the scene if size is too small", () => { // we need to finalize it because arrows and lines enter multi-mode fireEvent.keyDown(document, { key: KEYS.ENTER }); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -280,7 +280,7 @@ describe("do not add element to the scene if size is too small", () => { // we need to finalize it because arrows and lines enter multi-mode fireEvent.keyDown(document, { key: KEYS.ENTER }); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx index e97ace31f..e356f89a3 100644 --- a/src/tests/move.test.tsx +++ b/src/tests/move.test.tsx @@ -38,7 +38,7 @@ describe("move element", () => { fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -77,7 +77,7 @@ describe("move element", () => { // select the second rectangles new Pointer("mouse").clickOn(rectB); - expect(renderScene).toHaveBeenCalledTimes(20); + expect(renderScene).toHaveBeenCalledTimes(21); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); @@ -120,7 +120,7 @@ describe("duplicate element on move when ALT is clicked", () => { fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index 406c5888b..93bbc6639 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -30,7 +30,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); @@ -44,7 +44,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); @@ -58,7 +58,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); }); @@ -88,7 +88,7 @@ describe("multi point mode in linear elements", () => { fireEvent.pointerUp(canvas); fireEvent.keyDown(document, { key: KEYS.ENTER }); - expect(renderScene).toHaveBeenCalledTimes(12); + expect(renderScene).toHaveBeenCalledTimes(13); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; @@ -129,7 +129,7 @@ describe("multi point mode in linear elements", () => { fireEvent.pointerUp(canvas); fireEvent.keyDown(document, { key: KEYS.ENTER }); - expect(renderScene).toHaveBeenCalledTimes(12); + expect(renderScene).toHaveBeenCalledTimes(13); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index 0d2350d1f..004c4bcd9 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -28,7 +28,7 @@ describe("selection element", () => { const canvas = container.querySelector("canvas")!; fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); - expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderScene).toHaveBeenCalledTimes(4); const selectionElement = h.state.selectionElement!; expect(selectionElement).not.toBeNull(); expect(selectionElement.type).toEqual("selection"); @@ -49,7 +49,7 @@ describe("selection element", () => { fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(4); + expect(renderScene).toHaveBeenCalledTimes(5); const selectionElement = h.state.selectionElement!; expect(selectionElement).not.toBeNull(); expect(selectionElement.type).toEqual("selection"); @@ -71,7 +71,7 @@ describe("selection element", () => { fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); }); }); @@ -96,7 +96,7 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderScene).toHaveBeenCalledTimes(10); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -123,7 +123,7 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderScene).toHaveBeenCalledTimes(10); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -150,7 +150,7 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderScene).toHaveBeenCalledTimes(10); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -190,7 +190,7 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderScene).toHaveBeenCalledTimes(10); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -229,7 +229,7 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderScene).toHaveBeenCalledTimes(10); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts index 1822ecc57..f78295be6 100644 --- a/src/tests/test-utils.ts +++ b/src/tests/test-utils.ts @@ -100,5 +100,5 @@ const initLocalStorage = (data: ImportedDataState) => { }; export const updateSceneData = (data: SceneData) => { - (window.h.collab as any).excalidrawRef.current.updateScene(data); + (window.h.collab as any).excalidrawAPI.updateScene(data); }; diff --git a/src/types.ts b/src/types.ts index b94872def..fc18c8c8a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,10 +146,7 @@ export type LibraryItems = readonly LibraryItem[]; // NOTE ready/readyPromise props are optional for host apps' sake (our own // implem guarantees existence) export type ExcalidrawAPIRefValue = - | (ExcalidrawImperativeAPI & { - readyPromise?: ResolvablePromise; - ready?: true; - }) + | ExcalidrawImperativeAPI | { readyPromise?: ResolvablePromise; ready?: false;