|
@@ -33,7 +33,7 @@ import {
|
|
actionBindText,
|
|
actionBindText,
|
|
actionUngroup,
|
|
actionUngroup,
|
|
actionLink,
|
|
actionLink,
|
|
- actionToggleLock,
|
|
|
|
|
|
+ actionToggleElementLock,
|
|
actionToggleLinearEditor,
|
|
actionToggleLinearEditor,
|
|
} from "../actions";
|
|
} from "../actions";
|
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
|
@@ -59,7 +59,9 @@ import {
|
|
ELEMENT_TRANSLATE_AMOUNT,
|
|
ELEMENT_TRANSLATE_AMOUNT,
|
|
ENV,
|
|
ENV,
|
|
EVENT,
|
|
EVENT,
|
|
|
|
+ EXPORT_IMAGE_TYPES,
|
|
GRID_SIZE,
|
|
GRID_SIZE,
|
|
|
|
+ IMAGE_MIME_TYPES,
|
|
IMAGE_RENDER_TIMEOUT,
|
|
IMAGE_RENDER_TIMEOUT,
|
|
isAndroid,
|
|
isAndroid,
|
|
isBrave,
|
|
isBrave,
|
|
@@ -81,7 +83,7 @@ import {
|
|
VERTICAL_ALIGN,
|
|
VERTICAL_ALIGN,
|
|
ZOOM_STEP,
|
|
ZOOM_STEP,
|
|
} from "../constants";
|
|
} from "../constants";
|
|
-import { loadFromBlob } from "../data";
|
|
|
|
|
|
+import { exportCanvas, loadFromBlob } from "../data";
|
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
|
import { restore, restoreElements } from "../data/restore";
|
|
import { restore, restoreElements } from "../data/restore";
|
|
import {
|
|
import {
|
|
@@ -209,6 +211,8 @@ import {
|
|
PointerDownState,
|
|
PointerDownState,
|
|
SceneData,
|
|
SceneData,
|
|
Device,
|
|
Device,
|
|
|
|
+ SidebarName,
|
|
|
|
+ SidebarTabName,
|
|
} from "../types";
|
|
} from "../types";
|
|
import {
|
|
import {
|
|
debounce,
|
|
debounce,
|
|
@@ -234,6 +238,7 @@ import {
|
|
getShortcutKey,
|
|
getShortcutKey,
|
|
isTransparent,
|
|
isTransparent,
|
|
easeToValuesRAF,
|
|
easeToValuesRAF,
|
|
|
|
+ muteFSAbortError,
|
|
} from "../utils";
|
|
} from "../utils";
|
|
import {
|
|
import {
|
|
ContextMenu,
|
|
ContextMenu,
|
|
@@ -248,6 +253,7 @@ import {
|
|
generateIdFromFile,
|
|
generateIdFromFile,
|
|
getDataURL,
|
|
getDataURL,
|
|
getFileFromEvent,
|
|
getFileFromEvent,
|
|
|
|
+ isImageFileHandle,
|
|
isSupportedImageFile,
|
|
isSupportedImageFile,
|
|
loadSceneOrLibraryFromBlob,
|
|
loadSceneOrLibraryFromBlob,
|
|
normalizeFile,
|
|
normalizeFile,
|
|
@@ -287,6 +293,7 @@ import {
|
|
isLocalLink,
|
|
isLocalLink,
|
|
} from "../element/Hyperlink";
|
|
} from "../element/Hyperlink";
|
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
|
|
|
+import { actionUnlockAllElements } from "../actions/actionElementLock";
|
|
import { Fonts } from "../scene/Fonts";
|
|
import { Fonts } from "../scene/Fonts";
|
|
import { actionPaste } from "../actions/actionClipboard";
|
|
import { actionPaste } from "../actions/actionClipboard";
|
|
import {
|
|
import {
|
|
@@ -297,12 +304,17 @@ import { jotaiStore } from "../jotai";
|
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
|
|
|
+import { activeEyeDropperAtom } from "./EyeDropper";
|
|
|
|
+
|
|
|
|
+const AppContext = React.createContext<AppClassProperties>(null!);
|
|
|
|
+const AppPropsContext = React.createContext<AppProps>(null!);
|
|
|
|
|
|
const deviceContextInitialValue = {
|
|
const deviceContextInitialValue = {
|
|
isSmScreen: false,
|
|
isSmScreen: false,
|
|
isMobile: false,
|
|
isMobile: false,
|
|
isTouchScreen: false,
|
|
isTouchScreen: false,
|
|
canDeviceFitSidebar: false,
|
|
canDeviceFitSidebar: false,
|
|
|
|
+ isLandscape: false,
|
|
};
|
|
};
|
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
|
DeviceContext.displayName = "DeviceContext";
|
|
DeviceContext.displayName = "DeviceContext";
|
|
@@ -339,6 +351,8 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
|
);
|
|
);
|
|
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
|
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
|
|
|
|
|
|
|
+export const useApp = () => useContext(AppContext);
|
|
|
|
+export const useAppProps = () => useContext(AppPropsContext);
|
|
export const useDevice = () => useContext<Device>(DeviceContext);
|
|
export const useDevice = () => useContext<Device>(DeviceContext);
|
|
export const useExcalidrawContainer = () =>
|
|
export const useExcalidrawContainer = () =>
|
|
useContext(ExcalidrawContainerContext);
|
|
useContext(ExcalidrawContainerContext);
|
|
@@ -353,8 +367,6 @@ export const useExcalidrawActionManager = () =>
|
|
|
|
|
|
let didTapTwice: boolean = false;
|
|
let didTapTwice: boolean = false;
|
|
let tappedTwiceTimer = 0;
|
|
let tappedTwiceTimer = 0;
|
|
-let cursorX = 0;
|
|
|
|
-let cursorY = 0;
|
|
|
|
let isHoldingSpace: boolean = false;
|
|
let isHoldingSpace: boolean = false;
|
|
let isPanning: boolean = false;
|
|
let isPanning: boolean = false;
|
|
let isDraggingScrollBar: boolean = false;
|
|
let isDraggingScrollBar: boolean = false;
|
|
@@ -399,7 +411,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
|
public library: AppClassProperties["library"];
|
|
public library: AppClassProperties["library"];
|
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
|
- private id: string;
|
|
|
|
|
|
+ public id: string;
|
|
private history: History;
|
|
private history: History;
|
|
private excalidrawContainerValue: {
|
|
private excalidrawContainerValue: {
|
|
container: HTMLDivElement | null;
|
|
container: HTMLDivElement | null;
|
|
@@ -412,7 +424,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
|
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
|
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
|
- lastScenePointer: { x: number; y: number } | null = null;
|
|
|
|
|
|
+ lastViewportPosition = { x: 0, y: 0 };
|
|
|
|
|
|
constructor(props: AppProps) {
|
|
constructor(props: AppProps) {
|
|
super(props);
|
|
super(props);
|
|
@@ -437,7 +449,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
width: window.innerWidth,
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
height: window.innerHeight,
|
|
showHyperlinkPopup: false,
|
|
showHyperlinkPopup: false,
|
|
- isSidebarDocked: false,
|
|
|
|
|
|
+ defaultSidebarDockedPreference: false,
|
|
};
|
|
};
|
|
|
|
|
|
this.id = nanoid();
|
|
this.id = nanoid();
|
|
@@ -468,7 +480,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
setActiveTool: this.setActiveTool,
|
|
setActiveTool: this.setActiveTool,
|
|
setCursor: this.setCursor,
|
|
setCursor: this.setCursor,
|
|
resetCursor: this.resetCursor,
|
|
resetCursor: this.resetCursor,
|
|
- toggleMenu: this.toggleMenu,
|
|
|
|
|
|
+ toggleSidebar: this.toggleSidebar,
|
|
} as const;
|
|
} as const;
|
|
if (typeof excalidrawRef === "function") {
|
|
if (typeof excalidrawRef === "function") {
|
|
excalidrawRef(api);
|
|
excalidrawRef(api);
|
|
@@ -576,101 +588,93 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
|
|
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
|
|
}
|
|
}
|
|
>
|
|
>
|
|
- <ExcalidrawContainerContext.Provider
|
|
|
|
- value={this.excalidrawContainerValue}
|
|
|
|
- >
|
|
|
|
- <DeviceContext.Provider value={this.device}>
|
|
|
|
- <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
|
|
|
|
- <ExcalidrawAppStateContext.Provider value={this.state}>
|
|
|
|
- <ExcalidrawElementsContext.Provider
|
|
|
|
- value={this.scene.getNonDeletedElements()}
|
|
|
|
- >
|
|
|
|
- <ExcalidrawActionManagerContext.Provider
|
|
|
|
- value={this.actionManager}
|
|
|
|
- >
|
|
|
|
- <LayerUI
|
|
|
|
- canvas={this.canvas}
|
|
|
|
- appState={this.state}
|
|
|
|
- files={this.files}
|
|
|
|
- setAppState={this.setAppState}
|
|
|
|
- actionManager={this.actionManager}
|
|
|
|
- elements={this.scene.getNonDeletedElements()}
|
|
|
|
- onLockToggle={this.toggleLock}
|
|
|
|
- onPenModeToggle={this.togglePenMode}
|
|
|
|
- onHandToolToggle={this.onHandToolToggle}
|
|
|
|
- onInsertElements={(elements) =>
|
|
|
|
- this.addElementsFromPasteOrLibrary({
|
|
|
|
- elements,
|
|
|
|
- position: "center",
|
|
|
|
- files: null,
|
|
|
|
- })
|
|
|
|
- }
|
|
|
|
- langCode={getLanguage().code}
|
|
|
|
- renderTopRightUI={renderTopRightUI}
|
|
|
|
- renderCustomStats={renderCustomStats}
|
|
|
|
- renderCustomSidebar={this.props.renderSidebar}
|
|
|
|
- showExitZenModeBtn={
|
|
|
|
- typeof this.props?.zenModeEnabled === "undefined" &&
|
|
|
|
- this.state.zenModeEnabled
|
|
|
|
- }
|
|
|
|
- libraryReturnUrl={this.props.libraryReturnUrl}
|
|
|
|
- UIOptions={this.props.UIOptions}
|
|
|
|
- focusContainer={this.focusContainer}
|
|
|
|
- library={this.library}
|
|
|
|
- id={this.id}
|
|
|
|
- onImageAction={this.onImageAction}
|
|
|
|
- renderWelcomeScreen={
|
|
|
|
- !this.state.isLoading &&
|
|
|
|
- this.state.showWelcomeScreen &&
|
|
|
|
- this.state.activeTool.type === "selection" &&
|
|
|
|
- !this.scene.getElementsIncludingDeleted().length
|
|
|
|
- }
|
|
|
|
|
|
+ <AppContext.Provider value={this}>
|
|
|
|
+ <AppPropsContext.Provider value={this.props}>
|
|
|
|
+ <ExcalidrawContainerContext.Provider
|
|
|
|
+ value={this.excalidrawContainerValue}
|
|
|
|
+ >
|
|
|
|
+ <DeviceContext.Provider value={this.device}>
|
|
|
|
+ <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
|
|
|
|
+ <ExcalidrawAppStateContext.Provider value={this.state}>
|
|
|
|
+ <ExcalidrawElementsContext.Provider
|
|
|
|
+ value={this.scene.getNonDeletedElements()}
|
|
>
|
|
>
|
|
- {this.props.children}
|
|
|
|
- </LayerUI>
|
|
|
|
- <div className="excalidraw-textEditorContainer" />
|
|
|
|
- <div className="excalidraw-contextMenuContainer" />
|
|
|
|
- {selectedElement.length === 1 &&
|
|
|
|
- !this.state.contextMenu &&
|
|
|
|
- this.state.showHyperlinkPopup && (
|
|
|
|
- <Hyperlink
|
|
|
|
- key={selectedElement[0].id}
|
|
|
|
- element={selectedElement[0]}
|
|
|
|
|
|
+ <ExcalidrawActionManagerContext.Provider
|
|
|
|
+ value={this.actionManager}
|
|
|
|
+ >
|
|
|
|
+ <LayerUI
|
|
|
|
+ canvas={this.canvas}
|
|
|
|
+ appState={this.state}
|
|
|
|
+ files={this.files}
|
|
setAppState={this.setAppState}
|
|
setAppState={this.setAppState}
|
|
- onLinkOpen={this.props.onLinkOpen}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- {this.state.toast !== null && (
|
|
|
|
- <Toast
|
|
|
|
- message={this.state.toast.message}
|
|
|
|
- onClose={() => this.setToast(null)}
|
|
|
|
- duration={this.state.toast.duration}
|
|
|
|
- closable={this.state.toast.closable}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- {this.state.contextMenu && (
|
|
|
|
- <ContextMenu
|
|
|
|
- items={this.state.contextMenu.items}
|
|
|
|
- top={this.state.contextMenu.top}
|
|
|
|
- left={this.state.contextMenu.left}
|
|
|
|
- actionManager={this.actionManager}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- <main>{this.renderCanvas()}</main>
|
|
|
|
- </ExcalidrawActionManagerContext.Provider>
|
|
|
|
- </ExcalidrawElementsContext.Provider>{" "}
|
|
|
|
- </ExcalidrawAppStateContext.Provider>
|
|
|
|
- </ExcalidrawSetAppStateContext.Provider>
|
|
|
|
- </DeviceContext.Provider>
|
|
|
|
- </ExcalidrawContainerContext.Provider>
|
|
|
|
|
|
+ actionManager={this.actionManager}
|
|
|
|
+ elements={this.scene.getNonDeletedElements()}
|
|
|
|
+ onLockToggle={this.toggleLock}
|
|
|
|
+ onPenModeToggle={this.togglePenMode}
|
|
|
|
+ onHandToolToggle={this.onHandToolToggle}
|
|
|
|
+ langCode={getLanguage().code}
|
|
|
|
+ renderTopRightUI={renderTopRightUI}
|
|
|
|
+ renderCustomStats={renderCustomStats}
|
|
|
|
+ showExitZenModeBtn={
|
|
|
|
+ typeof this.props?.zenModeEnabled === "undefined" &&
|
|
|
|
+ this.state.zenModeEnabled
|
|
|
|
+ }
|
|
|
|
+ UIOptions={this.props.UIOptions}
|
|
|
|
+ onImageAction={this.onImageAction}
|
|
|
|
+ onExportImage={this.onExportImage}
|
|
|
|
+ renderWelcomeScreen={
|
|
|
|
+ !this.state.isLoading &&
|
|
|
|
+ this.state.showWelcomeScreen &&
|
|
|
|
+ this.state.activeTool.type === "selection" &&
|
|
|
|
+ !this.scene.getElementsIncludingDeleted().length
|
|
|
|
+ }
|
|
|
|
+ >
|
|
|
|
+ {this.props.children}
|
|
|
|
+ </LayerUI>
|
|
|
|
+ <div className="excalidraw-textEditorContainer" />
|
|
|
|
+ <div className="excalidraw-contextMenuContainer" />
|
|
|
|
+ <div className="excalidraw-eye-dropper-container" />
|
|
|
|
+ {selectedElement.length === 1 &&
|
|
|
|
+ !this.state.contextMenu &&
|
|
|
|
+ this.state.showHyperlinkPopup && (
|
|
|
|
+ <Hyperlink
|
|
|
|
+ key={selectedElement[0].id}
|
|
|
|
+ element={selectedElement[0]}
|
|
|
|
+ setAppState={this.setAppState}
|
|
|
|
+ onLinkOpen={this.props.onLinkOpen}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ {this.state.toast !== null && (
|
|
|
|
+ <Toast
|
|
|
|
+ message={this.state.toast.message}
|
|
|
|
+ onClose={() => this.setToast(null)}
|
|
|
|
+ duration={this.state.toast.duration}
|
|
|
|
+ closable={this.state.toast.closable}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ {this.state.contextMenu && (
|
|
|
|
+ <ContextMenu
|
|
|
|
+ items={this.state.contextMenu.items}
|
|
|
|
+ top={this.state.contextMenu.top}
|
|
|
|
+ left={this.state.contextMenu.left}
|
|
|
|
+ actionManager={this.actionManager}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ <main>{this.renderCanvas()}</main>
|
|
|
|
+ </ExcalidrawActionManagerContext.Provider>
|
|
|
|
+ </ExcalidrawElementsContext.Provider>
|
|
|
|
+ </ExcalidrawAppStateContext.Provider>
|
|
|
|
+ </ExcalidrawSetAppStateContext.Provider>
|
|
|
|
+ </DeviceContext.Provider>
|
|
|
|
+ </ExcalidrawContainerContext.Provider>
|
|
|
|
+ </AppPropsContext.Provider>
|
|
|
|
+ </AppContext.Provider>
|
|
</div>
|
|
</div>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
public focusContainer: AppClassProperties["focusContainer"] = () => {
|
|
public focusContainer: AppClassProperties["focusContainer"] = () => {
|
|
- if (this.props.autoFocus) {
|
|
|
|
- this.excalidrawContainerRef.current?.focus();
|
|
|
|
- }
|
|
|
|
|
|
+ this.excalidrawContainerRef.current?.focus();
|
|
};
|
|
};
|
|
|
|
|
|
public getSceneElementsIncludingDeleted = () => {
|
|
public getSceneElementsIncludingDeleted = () => {
|
|
@@ -681,6 +685,88 @@ class App extends React.Component<AppProps, AppState> {
|
|
return this.scene.getNonDeletedElements();
|
|
return this.scene.getNonDeletedElements();
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
|
|
|
|
+ this.addElementsFromPasteOrLibrary({
|
|
|
|
+ elements,
|
|
|
|
+ position: "center",
|
|
|
|
+ files: null,
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ public onExportImage = async (
|
|
|
|
+ type: keyof typeof EXPORT_IMAGE_TYPES,
|
|
|
|
+ elements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
+ ) => {
|
|
|
|
+ trackEvent("export", type, "ui");
|
|
|
|
+ const fileHandle = await exportCanvas(
|
|
|
|
+ type,
|
|
|
|
+ elements,
|
|
|
|
+ this.state,
|
|
|
|
+ this.files,
|
|
|
|
+ {
|
|
|
|
+ exportBackground: this.state.exportBackground,
|
|
|
|
+ name: this.state.name,
|
|
|
|
+ viewBackgroundColor: this.state.viewBackgroundColor,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ .catch(muteFSAbortError)
|
|
|
|
+ .catch((error) => {
|
|
|
|
+ console.error(error);
|
|
|
|
+ this.setState({ errorMessage: error.message });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (
|
|
|
|
+ this.state.exportEmbedScene &&
|
|
|
|
+ fileHandle &&
|
|
|
|
+ isImageFileHandle(fileHandle)
|
|
|
|
+ ) {
|
|
|
|
+ this.setState({ fileHandle });
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
|
|
|
+ jotaiStore.set(activeEyeDropperAtom, {
|
|
|
|
+ swapPreviewOnAlt: true,
|
|
|
|
+ previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
|
|
|
|
+ onSelect: (color, event) => {
|
|
|
|
+ const shouldUpdateStrokeColor =
|
|
|
|
+ (type === "background" && event.altKey) ||
|
|
|
|
+ (type === "stroke" && !event.altKey);
|
|
|
|
+ const selectedElements = getSelectedElements(
|
|
|
|
+ this.scene.getElementsIncludingDeleted(),
|
|
|
|
+ this.state,
|
|
|
|
+ );
|
|
|
|
+ if (
|
|
|
|
+ !selectedElements.length ||
|
|
|
|
+ this.state.activeTool.type !== "selection"
|
|
|
|
+ ) {
|
|
|
|
+ if (shouldUpdateStrokeColor) {
|
|
|
|
+ this.setState({
|
|
|
|
+ currentItemStrokeColor: color,
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ this.setState({
|
|
|
|
+ currentItemBackgroundColor: color,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ this.updateScene({
|
|
|
|
+ elements: this.scene.getElementsIncludingDeleted().map((el) => {
|
|
|
|
+ if (this.state.selectedElementIds[el.id]) {
|
|
|
|
+ return newElementWith(el, {
|
|
|
|
+ [shouldUpdateStrokeColor ? "strokeColor" : "backgroundColor"]:
|
|
|
|
+ color,
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ return el;
|
|
|
|
+ }),
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ keepOpenOnAlt: false,
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
private syncActionResult = withBatchedUpdates(
|
|
private syncActionResult = withBatchedUpdates(
|
|
(actionResult: ActionResult) => {
|
|
(actionResult: ActionResult) => {
|
|
if (this.unmounted || actionResult === false) {
|
|
if (this.unmounted || actionResult === false) {
|
|
@@ -905,6 +991,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
|
this.device = updateObject(this.device, {
|
|
this.device = updateObject(this.device, {
|
|
|
|
+ isLandscape: width > height,
|
|
isSmScreen: width < MQ_SM_MAX_WIDTH,
|
|
isSmScreen: width < MQ_SM_MAX_WIDTH,
|
|
isMobile:
|
|
isMobile:
|
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
@@ -950,7 +1037,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.scene.addCallback(this.onSceneUpdated);
|
|
this.scene.addCallback(this.onSceneUpdated);
|
|
this.addEventListeners();
|
|
this.addEventListeners();
|
|
|
|
|
|
- if (this.excalidrawContainerRef.current) {
|
|
|
|
|
|
+ if (this.props.autoFocus && this.excalidrawContainerRef.current) {
|
|
this.focusContainer();
|
|
this.focusContainer();
|
|
}
|
|
}
|
|
|
|
|
|
@@ -1028,6 +1115,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.unmounted = true;
|
|
this.unmounted = true;
|
|
this.removeEventListeners();
|
|
this.removeEventListeners();
|
|
this.scene.destroy();
|
|
this.scene.destroy();
|
|
|
|
+ this.library.destroy();
|
|
clearTimeout(touchTimeout);
|
|
clearTimeout(touchTimeout);
|
|
touchTimeout = 0;
|
|
touchTimeout = 0;
|
|
}
|
|
}
|
|
@@ -1524,7 +1612,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
|
|
|
|
|
+ const elementUnderCursor = document.elementFromPoint(
|
|
|
|
+ this.lastViewportPosition.x,
|
|
|
|
+ this.lastViewportPosition.y,
|
|
|
|
+ );
|
|
if (
|
|
if (
|
|
event &&
|
|
event &&
|
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
|
@@ -1552,7 +1643,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
- { clientX: cursorX, clientY: cursorY },
|
|
|
|
|
|
+ {
|
|
|
|
+ clientX: this.lastViewportPosition.x,
|
|
|
|
+ clientY: this.lastViewportPosition.y,
|
|
|
|
+ },
|
|
this.state,
|
|
this.state,
|
|
);
|
|
);
|
|
|
|
|
|
@@ -1589,6 +1683,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
elements: data.elements,
|
|
elements: data.elements,
|
|
files: data.files || null,
|
|
files: data.files || null,
|
|
position: "cursor",
|
|
position: "cursor",
|
|
|
|
+ retainSeed: isPlainPaste,
|
|
});
|
|
});
|
|
} else if (data.text) {
|
|
} else if (data.text) {
|
|
this.addTextFromPaste(data.text, isPlainPaste);
|
|
this.addTextFromPaste(data.text, isPlainPaste);
|
|
@@ -1602,6 +1697,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
elements: readonly ExcalidrawElement[];
|
|
elements: readonly ExcalidrawElement[];
|
|
files: BinaryFiles | null;
|
|
files: BinaryFiles | null;
|
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
|
|
|
+ retainSeed?: boolean;
|
|
}) => {
|
|
}) => {
|
|
const elements = restoreElements(opts.elements, null);
|
|
const elements = restoreElements(opts.elements, null);
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
|
@@ -1613,13 +1709,13 @@ class App extends React.Component<AppProps, AppState> {
|
|
typeof opts.position === "object"
|
|
typeof opts.position === "object"
|
|
? opts.position.clientX
|
|
? opts.position.clientX
|
|
: opts.position === "cursor"
|
|
: opts.position === "cursor"
|
|
- ? cursorX
|
|
|
|
|
|
+ ? this.lastViewportPosition.x
|
|
: this.state.width / 2 + this.state.offsetLeft;
|
|
: this.state.width / 2 + this.state.offsetLeft;
|
|
const clientY =
|
|
const clientY =
|
|
typeof opts.position === "object"
|
|
typeof opts.position === "object"
|
|
? opts.position.clientY
|
|
? opts.position.clientY
|
|
: opts.position === "cursor"
|
|
: opts.position === "cursor"
|
|
- ? cursorY
|
|
|
|
|
|
+ ? this.lastViewportPosition.y
|
|
: this.state.height / 2 + this.state.offsetTop;
|
|
: this.state.height / 2 + this.state.offsetTop;
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
@@ -1639,6 +1735,9 @@ class App extends React.Component<AppProps, AppState> {
|
|
y: element.y + gridY - minY,
|
|
y: element.y + gridY - minY,
|
|
});
|
|
});
|
|
}),
|
|
}),
|
|
|
|
+ {
|
|
|
|
+ randomizeSeed: !opts.retainSeed,
|
|
|
|
+ },
|
|
);
|
|
);
|
|
|
|
|
|
const nextElements = [
|
|
const nextElements = [
|
|
@@ -1673,7 +1772,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
openSidebar:
|
|
openSidebar:
|
|
this.state.openSidebar &&
|
|
this.state.openSidebar &&
|
|
this.device.canDeviceFitSidebar &&
|
|
this.device.canDeviceFitSidebar &&
|
|
- this.state.isSidebarDocked
|
|
|
|
|
|
+ this.state.defaultSidebarDockedPreference
|
|
? this.state.openSidebar
|
|
? this.state.openSidebar
|
|
: null,
|
|
: null,
|
|
selectedElementIds: newElements.reduce(
|
|
selectedElementIds: newElements.reduce(
|
|
@@ -1700,7 +1799,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
private addTextFromPaste(text: string, isPlainPaste = false) {
|
|
private addTextFromPaste(text: string, isPlainPaste = false) {
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
- { clientX: cursorX, clientY: cursorY },
|
|
|
|
|
|
+ {
|
|
|
|
+ clientX: this.lastViewportPosition.x,
|
|
|
|
+ clientY: this.lastViewportPosition.y,
|
|
|
|
+ },
|
|
this.state,
|
|
this.state,
|
|
);
|
|
);
|
|
|
|
|
|
@@ -2011,36 +2113,30 @@ class App extends React.Component<AppProps, AppState> {
|
|
/**
|
|
/**
|
|
* @returns whether the menu was toggled on or off
|
|
* @returns whether the menu was toggled on or off
|
|
*/
|
|
*/
|
|
- public toggleMenu = (
|
|
|
|
- type: "library" | "customSidebar",
|
|
|
|
- force?: boolean,
|
|
|
|
- ): boolean => {
|
|
|
|
- if (type === "customSidebar" && !this.props.renderSidebar) {
|
|
|
|
- console.warn(
|
|
|
|
- `attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`,
|
|
|
|
- );
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (type === "library" || type === "customSidebar") {
|
|
|
|
- let nextValue;
|
|
|
|
- if (force === undefined) {
|
|
|
|
- nextValue = this.state.openSidebar === type ? null : type;
|
|
|
|
- } else {
|
|
|
|
- nextValue = force ? type : null;
|
|
|
|
- }
|
|
|
|
- this.setState({ openSidebar: nextValue });
|
|
|
|
-
|
|
|
|
- return !!nextValue;
|
|
|
|
|
|
+ public toggleSidebar = ({
|
|
|
|
+ name,
|
|
|
|
+ tab,
|
|
|
|
+ force,
|
|
|
|
+ }: {
|
|
|
|
+ name: SidebarName;
|
|
|
|
+ tab?: SidebarTabName;
|
|
|
|
+ force?: boolean;
|
|
|
|
+ }): boolean => {
|
|
|
|
+ let nextName;
|
|
|
|
+ if (force === undefined) {
|
|
|
|
+ nextName = this.state.openSidebar?.name === name ? null : name;
|
|
|
|
+ } else {
|
|
|
|
+ nextName = force ? name : null;
|
|
}
|
|
}
|
|
|
|
+ this.setState({ openSidebar: nextName ? { name: nextName, tab } : null });
|
|
|
|
|
|
- return false;
|
|
|
|
|
|
+ return !!nextName;
|
|
};
|
|
};
|
|
|
|
|
|
private updateCurrentCursorPosition = withBatchedUpdates(
|
|
private updateCurrentCursorPosition = withBatchedUpdates(
|
|
(event: MouseEvent) => {
|
|
(event: MouseEvent) => {
|
|
- cursorX = event.clientX;
|
|
|
|
- cursorY = event.clientY;
|
|
|
|
|
|
+ this.lastViewportPosition.x = event.clientX;
|
|
|
|
+ this.lastViewportPosition.y = event.clientY;
|
|
},
|
|
},
|
|
);
|
|
);
|
|
|
|
|
|
@@ -2113,6 +2209,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
event.shiftKey &&
|
|
event.shiftKey &&
|
|
event[KEYS.CTRL_OR_CMD]
|
|
event[KEYS.CTRL_OR_CMD]
|
|
) {
|
|
) {
|
|
|
|
+ event.preventDefault();
|
|
this.setState({ openDialog: "imageExport" });
|
|
this.setState({ openDialog: "imageExport" });
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
@@ -2282,11 +2379,11 @@ class App extends React.Component<AppProps, AppState> {
|
|
(hasBackground(this.state.activeTool.type) ||
|
|
(hasBackground(this.state.activeTool.type) ||
|
|
selectedElements.some((element) => hasBackground(element.type)))
|
|
selectedElements.some((element) => hasBackground(element.type)))
|
|
) {
|
|
) {
|
|
- this.setState({ openPopup: "backgroundColorPicker" });
|
|
|
|
|
|
+ this.setState({ openPopup: "elementBackground" });
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
if (event.key === KEYS.S) {
|
|
if (event.key === KEYS.S) {
|
|
- this.setState({ openPopup: "strokeColorPicker" });
|
|
|
|
|
|
+ this.setState({ openPopup: "elementStroke" });
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -2297,6 +2394,20 @@ class App extends React.Component<AppProps, AppState> {
|
|
) {
|
|
) {
|
|
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
|
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // eye dropper
|
|
|
|
+ // -----------------------------------------------------------------------
|
|
|
|
+ const lowerCased = event.key.toLocaleLowerCase();
|
|
|
|
+ const isPickingStroke = lowerCased === KEYS.S && event.shiftKey;
|
|
|
|
+ const isPickingBackground =
|
|
|
|
+ event.key === KEYS.I || (lowerCased === KEYS.G && event.shiftKey);
|
|
|
|
+
|
|
|
|
+ if (isPickingStroke || isPickingBackground) {
|
|
|
|
+ this.openEyeDropper({
|
|
|
|
+ type: isPickingStroke ? "stroke" : "background",
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ // -----------------------------------------------------------------------
|
|
},
|
|
},
|
|
);
|
|
);
|
|
|
|
|
|
@@ -2426,8 +2537,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.setState((state) => ({
|
|
this.setState((state) => ({
|
|
...getStateForZoom(
|
|
...getStateForZoom(
|
|
{
|
|
{
|
|
- viewportX: cursorX,
|
|
|
|
- viewportY: cursorY,
|
|
|
|
|
|
+ viewportX: this.lastViewportPosition.x,
|
|
|
|
+ viewportY: this.lastViewportPosition.y,
|
|
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
|
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
|
},
|
|
},
|
|
state,
|
|
state,
|
|
@@ -2744,6 +2855,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
|
groupIds: container?.groupIds ?? [],
|
|
groupIds: container?.groupIds ?? [],
|
|
lineHeight,
|
|
lineHeight,
|
|
|
|
+ angle: container?.angle ?? 0,
|
|
});
|
|
});
|
|
|
|
|
|
if (!existingTextElement && shouldBindToContainer && container) {
|
|
if (!existingTextElement && shouldBindToContainer && container) {
|
|
@@ -4719,7 +4831,12 @@ class App extends React.Component<AppProps, AppState> {
|
|
pointerDownState.drag.hasOccurred = true;
|
|
pointerDownState.drag.hasOccurred = true;
|
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
|
// it would have weird results (stuff jumping all over the screen)
|
|
// it would have weird results (stuff jumping all over the screen)
|
|
- if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
|
|
|
|
|
|
+ // Checking for editingElement to avoid jump while editing on mobile #6503
|
|
|
|
+ if (
|
|
|
|
+ selectedElements.length > 0 &&
|
|
|
|
+ !pointerDownState.withCmdOrCtrl &&
|
|
|
|
+ !this.state.editingElement
|
|
|
|
+ ) {
|
|
const [dragX, dragY] = getGridPoint(
|
|
const [dragX, dragY] = getGridPoint(
|
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
|
@@ -5742,7 +5859,9 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
const imageFile = await fileOpen({
|
|
const imageFile = await fileOpen({
|
|
description: "Image",
|
|
description: "Image",
|
|
- extensions: ["jpg", "png", "svg", "gif"],
|
|
|
|
|
|
+ extensions: Object.keys(
|
|
|
|
+ IMAGE_MIME_TYPES,
|
|
|
|
+ ) as (keyof typeof IMAGE_MIME_TYPES)[],
|
|
});
|
|
});
|
|
|
|
|
|
const imageElement = this.createImageElement({
|
|
const imageElement = this.createImageElement({
|
|
@@ -6334,6 +6453,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
copyText,
|
|
copyText,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
actionSelectAll,
|
|
actionSelectAll,
|
|
|
|
+ actionUnlockAllElements,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
actionToggleGridMode,
|
|
actionToggleGridMode,
|
|
actionToggleZenMode,
|
|
actionToggleZenMode,
|
|
@@ -6380,7 +6500,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
actionToggleLinearEditor,
|
|
actionToggleLinearEditor,
|
|
actionLink,
|
|
actionLink,
|
|
actionDuplicateSelection,
|
|
actionDuplicateSelection,
|
|
- actionToggleLock,
|
|
|
|
|
|
+ actionToggleElementLock,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
CONTEXT_MENU_SEPARATOR,
|
|
actionDeleteSelected,
|
|
actionDeleteSelected,
|
|
];
|
|
];
|
|
@@ -6414,8 +6534,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.translateCanvas((state) => ({
|
|
this.translateCanvas((state) => ({
|
|
...getStateForZoom(
|
|
...getStateForZoom(
|
|
{
|
|
{
|
|
- viewportX: cursorX,
|
|
|
|
- viewportY: cursorY,
|
|
|
|
|
|
+ viewportX: this.lastViewportPosition.x,
|
|
|
|
+ viewportY: this.lastViewportPosition.y,
|
|
nextZoom: getNormalizedZoom(newZoom),
|
|
nextZoom: getNormalizedZoom(newZoom),
|
|
},
|
|
},
|
|
state,
|
|
state,
|