@zsviczian/excalidraw
Version:
Excalidraw as a React component
621 lines (620 loc) • 25 kB
TypeScript
import React from "react";
import { AppEventBus, type EXPORT_IMAGE_TYPES, Emitter, type EditorInterface, type StylesPanelMode } from "@excalidraw/common";
import { LinearElementEditor, FlowChartCreator, Scene, type ElementUpdate, StoreDelta, type ApplyToOptions } from "@excalidraw/element";
import type { ExcalidrawElement, ExcalidrawLinearElement, NonDeleted, NonDeletedExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawIframeElement, ExcalidrawEmbeddableElement, Ordered, SceneElementsMap } from "@excalidraw/element/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import { ActionManager } from "../actions/manager";
import { Fonts } from "../fonts";
import { type WritableAtom } from "../editor-jotai";
import { Renderer } from "../scene/Renderer";
import { LaserTrails } from "../laserTrails";
import { isOverScrollBars } from "../scene/scrollbars";
import { LassoTrail } from "../lasso";
import { EraserTrail } from "../eraser";
import { type OnStateChange } from "./AppStateObserver";
import type { ExportedElements } from "../data";
import type { AppClassProperties, AppProps, AppState, ExcalidrawImperativeAPI, BinaryFiles, LibraryItems, SceneData, FrameNameBoundsCache, SidebarName, SidebarTabName, ToolType, OnUserFollowedPayload, ExcalidrawImperativeAPIEventMap, GenerateDiagramToCode, NullableGridSize, Offsets } from "../types";
import type { RoughCanvas } from "roughjs/bin/canvas";
import type { ActionResult } from "../actions/types";
declare const editorLifecycleEventBehavior: {
readonly "editor:mount": {
readonly cardinality: "once";
readonly replay: "last";
};
readonly "editor:initialize": {
readonly cardinality: "once";
readonly replay: "last";
};
readonly "editor:unmount": {
readonly cardinality: "once";
readonly replay: "last";
};
};
export declare const ExcalidrawContainerContext: React.Context<{
container: HTMLDivElement | null;
id: string | null;
}>;
export declare const ExcalidrawAPIContext: React.Context<ExcalidrawImperativeAPI | null>;
export declare const ExcalidrawAPISetContext: React.Context<((api: ExcalidrawImperativeAPI | null) => void) | null>;
export declare const useApp: () => AppClassProperties;
export declare const useAppProps: () => AppProps;
export declare const useEditorInterface: () => Readonly<{
formFactor: "phone" | "tablet" | "desktop";
desktopUIMode: "compact" | "full" | "tray" | "mobile";
userAgent: Readonly<{
isMobileDevice: boolean;
platform: "ios" | "android" | "other" | "unknown";
}>;
isTouchScreen: boolean;
canFitSidebar: boolean;
isLandscape: boolean;
}>;
export declare const useStylesPanelMode: () => StylesPanelMode;
export declare const useExcalidrawContainer: () => {
container: HTMLDivElement | null;
id: string | null;
};
export declare const useExcalidrawElements: () => readonly NonDeletedExcalidrawElement[];
export declare const useExcalidrawAppState: () => AppState;
export declare const useExcalidrawSetAppState: () => <K extends keyof AppState>(state: AppState | ((prevState: Readonly<AppState>, props: Readonly<any>) => AppState | Pick<AppState, K> | null) | Pick<AppState, K> | null, callback?: (() => void) | undefined) => void;
export declare const useExcalidrawActionManager: () => ActionManager;
/**
* Requires wrapping your component in <ExcalidrawAPIContext.Provider>
*/
export declare const useExcalidrawAPI: () => ExcalidrawImperativeAPI | null;
declare class App extends React.Component<AppProps, AppState> {
canvas: AppClassProperties["canvas"];
interactiveCanvas: AppClassProperties["interactiveCanvas"];
sessionExportThemeOverride: AppState["theme"] | undefined;
rc: RoughCanvas;
unmounted: boolean;
actionManager: ActionManager;
editorInterface: EditorInterface;
private stylesPanelMode;
private excalidrawContainerRef;
scene: Scene;
fonts: Fonts;
renderer: Renderer;
visibleElements: readonly NonDeletedExcalidrawElement[];
private resizeObserver;
library: AppClassProperties["library"];
libraryItemsFromStorage: LibraryItems | undefined;
id: string;
private store;
private history;
private shouldRenderAllEmbeddables;
excalidrawContainerValue: {
container: HTMLDivElement | null;
id: string;
};
files: BinaryFiles;
imageCache: AppClassProperties["imageCache"];
private iFrameRefs;
/**
* Indicates whether the embeddable's url has been validated for rendering.
* If value not set, indicates that the validation is pending.
* Initially or on url change the flag is not reset so that we can guarantee
* the validation came from a trusted source (the editor).
**/
private embedsValidationStatus;
/** embeds that have been inserted to DOM (as a perf optim, we don't want to
* insert to DOM before user initially scrolls to them) */
private initializedEmbeds;
private elementsPendingErasure;
private _initialized;
private readonly editorLifecycleEvents;
onEvent: AppEventBus<ExcalidrawImperativeAPIEventMap, typeof editorLifecycleEventBehavior>["on"];
private appStateObserver;
onStateChange: OnStateChange;
flowChartCreator: FlowChartCreator;
private flowChartNavigator;
bindModeHandler: ReturnType<typeof setTimeout> | null;
hitLinkElement?: NonDeletedExcalidrawElement;
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null;
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null;
lastPointerUpIsDoubleClick: boolean;
lastPointerMoveEvent: PointerEvent | null;
/** current frame pointer cords */
lastPointerMoveCoords: {
x: number;
y: number;
} | null;
private lastCompletedCanvasClicks;
/** previous frame pointer coords */
previousPointerMoveCoords: {
x: number;
y: number;
} | null;
lastViewportPosition: {
x: number;
y: number;
};
allowMobileMode: boolean;
laserTrails: LaserTrails;
eraserTrail: EraserTrail;
lassoTrail: LassoTrail;
onChangeEmitter: Emitter<[elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles]>;
onPointerDownEmitter: Emitter<[activeTool: {
lastActiveTool: import("../types").ActiveTool | null;
locked: boolean;
fromSelection: boolean;
} & import("../types").ActiveTool, pointerDownState: Readonly<{
origin: Readonly<{
x: number;
y: number;
}>;
originInGrid: Readonly<{
x: number;
y: number;
}>;
scrollbars: ReturnType<typeof isOverScrollBars>;
lastCoords: {
x: number;
y: number;
};
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
resize: {
handleType: import("@excalidraw/element").MaybeTransformHandleType;
isResizing: boolean;
offset: {
x: number;
y: number;
};
arrowDirection: "origin" | "end";
center: {
x: number;
y: number;
};
};
hit: {
element: NonDeleted<ExcalidrawElement> | null;
allHitElements: NonDeleted<ExcalidrawElement>[];
wasAddedToSelection: boolean;
hasBeenDuplicated: boolean;
hasHitCommonBoundingBoxOfSelectedElements: boolean;
};
withCmdOrCtrl: boolean;
drag: {
hasOccurred: boolean;
offset: {
x: number;
y: number;
} | null;
origin: {
x: number;
y: number;
};
blockDragging: boolean;
};
eventListeners: {
onMove: null | ReturnType<typeof import("@excalidraw/common").throttleRAF>;
onUp: null | ((event: PointerEvent) => void);
onKeyDown: null | ((event: KeyboardEvent) => void);
onKeyUp: null | ((event: KeyboardEvent) => void);
};
boxSelection: {
hasOccurred: boolean;
};
}>, event: React.PointerEvent<HTMLElement>]>;
onPointerUpEmitter: Emitter<[activeTool: {
lastActiveTool: import("../types").ActiveTool | null;
locked: boolean;
fromSelection: boolean;
} & import("../types").ActiveTool, pointerDownState: Readonly<{
origin: Readonly<{
x: number;
y: number;
}>;
originInGrid: Readonly<{
x: number;
y: number;
}>;
scrollbars: ReturnType<typeof isOverScrollBars>;
lastCoords: {
x: number;
y: number;
};
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
resize: {
handleType: import("@excalidraw/element").MaybeTransformHandleType;
isResizing: boolean;
offset: {
x: number;
y: number;
};
arrowDirection: "origin" | "end";
center: {
x: number;
y: number;
};
};
hit: {
element: NonDeleted<ExcalidrawElement> | null;
allHitElements: NonDeleted<ExcalidrawElement>[];
wasAddedToSelection: boolean;
hasBeenDuplicated: boolean;
hasHitCommonBoundingBoxOfSelectedElements: boolean;
};
withCmdOrCtrl: boolean;
drag: {
hasOccurred: boolean;
offset: {
x: number;
y: number;
} | null;
origin: {
x: number;
y: number;
};
blockDragging: boolean;
};
eventListeners: {
onMove: null | ReturnType<typeof import("@excalidraw/common").throttleRAF>;
onUp: null | ((event: PointerEvent) => void);
onKeyDown: null | ((event: KeyboardEvent) => void);
onKeyUp: null | ((event: KeyboardEvent) => void);
};
boxSelection: {
hasOccurred: boolean;
};
}>, event: PointerEvent]>;
onUserFollowEmitter: Emitter<[payload: OnUserFollowedPayload]>;
onScrollChangeEmitter: Emitter<[scrollX: number, scrollY: number, zoom: Readonly<{
value: import("../types").NormalizedZoomValue;
}>]>;
missingPointerEventCleanupEmitter: Emitter<[event: PointerEvent | null]>;
onRemoveEventListenersEmitter: Emitter<[]>;
api: ExcalidrawImperativeAPI;
private createExcalidrawAPI;
constructor(props: AppProps);
updateEditorAtom: <Value, Args extends unknown[], Result>(atom: WritableAtom<Value, Args, Result>, ...args: Args) => Result;
private onWindowMessage;
private handleSkipBindMode;
private resetDelayedBindMode;
private previousHoveredBindableElement;
private handleDelayedBindModeChange;
private cacheEmbeddableRef;
/**
* Returns gridSize taking into account `gridModeEnabled`.
* If disabled, returns null.
*/
getEffectiveGridSize: () => NullableGridSize;
private getTextCreationGridPoint;
private getHTMLIFrameElement;
private handleIframeLikeElementHover;
/** @returns true if iframe-like element click handled */
private handleIframeLikeCenterClick;
private isDoubleClick;
private isIframeLikeElementCenter;
private updateEmbedValidationStatus;
private updateEmbeddables;
private renderEmbeddables;
private getFrameNameDOMId;
frameNameBoundsCache: FrameNameBoundsCache;
private resetEditingFrame;
private renderFrameNames;
private toggleOverscrollBehavior;
render(): import("react/jsx-runtime").JSX.Element;
focusContainer: AppClassProperties["focusContainer"];
getSceneElementsIncludingDeleted: () => readonly import("@excalidraw/element/types").OrderedExcalidrawElement[];
getSceneElementsMapIncludingDeleted: () => SceneElementsMap;
getSceneElements: () => readonly Ordered<NonDeletedExcalidrawElement>[];
onInsertElements: (elements: readonly ExcalidrawElement[]) => void;
onExportImage: (type: keyof typeof EXPORT_IMAGE_TYPES, elements: ExportedElements, opts: {
exportingFrame: ExcalidrawFrameLikeElement | null;
}) => Promise<void>;
private magicGenerations;
private updateMagicGeneration;
plugins: {
diagramToCode?: {
generate: GenerateDiagramToCode;
};
};
setPlugins(plugins: Partial<App["plugins"]>): void;
private onMagicFrameGenerate;
private onIframeSrcCopy;
onMagicframeToolSelect: () => void;
private openEyeDropper;
dismissLinearEditor: () => void;
syncActionResult: (actionResult: ActionResult) => void;
private onBlur;
private onUnload;
private disableEvent;
private resetHistory;
private undo;
private redo;
private resetStore;
/**
* Resets scene & history.
* ! Do not use to clear scene user action !
*/
private resetScene;
private initializeScene;
private getFormFactor;
refreshEditorInterface: () => void;
private reconcileStylesPanelMode;
/** TO BE USED LATER */
private setDesktopUIMode;
private isTouchScreen;
isTrayModeEnabled: () => boolean;
private clearImageShapeCache;
componentDidMount(): Promise<void>;
componentWillUnmount(): void;
private onResize;
/** generally invoked only if fullscreen was invoked programmatically */
private onFullscreenChange;
private removeEventListeners;
private addEventListeners;
componentDidUpdate(prevProps: AppProps, prevState: AppState): void;
private renderInteractiveSceneCallback;
private onScroll;
private onCut;
private onCopy;
private static resetTapTwice;
private onTouchStart;
private onTouchEnd;
private insertClipboardContent;
pasteFromClipboard: (event: ClipboardEvent) => Promise<void>;
addElementsFromPasteOrLibrary: (opts: {
elements: readonly ExcalidrawElement[];
files: BinaryFiles | null;
position: {
clientX: number;
clientY: number;
} | "cursor" | "center";
retainSeed?: boolean;
fitToContent?: boolean;
preserveFrameChildrenOrder?: boolean;
}) => void;
private addElementsFromMixedContentPaste;
private addTextFromPaste;
setAppState: React.Component<any, AppState>["setState"];
removePointer: (event: React.PointerEvent<HTMLElement> | PointerEvent) => void;
toggleLock: (source?: "keyboard" | "ui") => void;
updateFrameRendering: (opts: Partial<AppState["frameRendering"]> | ((prevState: AppState["frameRendering"]) => Partial<AppState["frameRendering"]>)) => void;
togglePenMode: (force: boolean | null) => void;
onHandToolToggle: () => void;
/**
* Zooms on canvas viewport center
*/
zoomCanvas: (
/**
* Decimal fraction, auto-clamped between MIN_ZOOM and MAX_ZOOM.
* 1 = 100% zoom, 2 = 200% zoom, 0.5 = 50% zoom
*/
value: number) => void;
private cancelInProgressAnimation;
scrollToContent: (
/**
* target to scroll to
*
* - string - id of element or group, or url containing elementLink
* - ExcalidrawElement | ExcalidrawElement[] - element(s) objects
*/
target?: string | ExcalidrawElement | readonly ExcalidrawElement[], opts?: ({
fitToContent?: boolean;
fitToViewport?: never;
viewportZoomFactor?: number;
animate?: boolean;
duration?: number;
} | {
fitToContent?: never;
fitToViewport?: boolean;
/** when fitToViewport=true, how much screen should the content cover,
* between 0.1 (10%) and 1 (100%)
*/
viewportZoomFactor?: number;
animate?: boolean;
duration?: number;
}) & {
minZoom?: number;
maxZoom?: number;
canvasOffsets?: Offsets;
}) => void;
private maybeUnfollowRemoteUser;
/** use when changing scrollX/scrollY/zoom based on user interaction */
private translateCanvas;
setForceRenderAllEmbeddables: (force: boolean) => void;
zoomToFit: (target?: readonly ExcalidrawElement[], maxZoom?: number, //null will zoom to max based on viewport
margin?: number) => void;
getColorAtScenePoint: ({ sceneX, sceneY, }: {
sceneX: number;
sceneY: number;
}) => string | null;
startLineEditor: (el: ExcalidrawLinearElement, selectedPointsIndices?: number[] | null) => void;
refreshAllArrows: () => void;
updateContainerSize: (containers: NonDeletedExcalidrawElement[]) => void;
setToast: (toast: AppState["toast"]) => void;
restoreFileFromShare: () => Promise<void>;
/**
* adds supplied files to existing files in the appState.
* NOTE if file already exists in editor state, the file data is not updated
* */
addFiles: ExcalidrawImperativeAPI["addFiles"];
setMobileModeAllowed: (allow: boolean) => void;
private debounceClearHighlightSearchResults;
selectElements: ExcalidrawImperativeAPI["selectElements"];
bringToFront: ExcalidrawImperativeAPI["bringToFront"];
bringForward: ExcalidrawImperativeAPI["bringForward"];
sendToBack: ExcalidrawImperativeAPI["sendToBack"];
sendBackward: ExcalidrawImperativeAPI["sendBackward"];
private addMissingFiles;
updateScene: <K extends keyof AppState>(sceneData: {
elements?: SceneData["elements"];
appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"];
/**
* Controls which updates should be captured by the `Store`. Captured updates are emmitted and listened to by other components, such as `History` for undo / redo purposes.
*
* - `CaptureUpdateAction.IMMEDIATELY`: Updates are immediately undoable. Use for most local updates.
* - `CaptureUpdateAction.NEVER`: Updates never make it to undo/redo stack. Use for remote updates or scene initialization.
* - `CaptureUpdateAction.EVENTUALLY`: Updates will be eventually be captured as part of a future increment.
*
* Check [API docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#captureUpdate) for more details.
*
* @default CaptureUpdateAction.EVENTUALLY
*/
captureUpdate?: SceneData["captureUpdate"];
forceFlushSync?: boolean;
}) => void;
applyDeltas: (deltas: StoreDelta[], options?: ApplyToOptions) => [SceneElementsMap, AppState, boolean];
mutateElement: <TElement extends Mutable<ExcalidrawElement>>(element: TElement, updates: ElementUpdate<TElement>, informMutation?: boolean) => TElement;
private triggerRender;
/**
* @returns whether the menu was toggled on or off
*/
toggleSidebar: ({ name, tab, force, }: {
name: SidebarName | null;
tab?: SidebarTabName;
force?: boolean;
}) => boolean;
private updateCurrentCursorPosition;
getEditorUIOffsets: () => Offsets;
private onKeyDown;
private onKeyUp;
private isToolSupported;
setActiveTool: (tool: ({
type: ToolType;
} | {
type: "custom";
customType: string;
}) & {
locked?: boolean;
fromSelection?: boolean;
}, keepSelection?: boolean) => void;
setOpenDialog: (dialogType: AppState["openDialog"]) => void;
private setCursor;
private resetCursor;
/**
* returns whether user is making a gesture with >= 2 fingers (points)
* on o touch screen (not on a trackpad). Currently only relates to Darwin
* (iOS/iPadOS,MacOS), but may work on other devices in the future if
* GestureEvent is standardized.
*/
private isTouchScreenMultiTouchGesture;
getName: () => string;
private onGestureStart;
private onGestureChange;
private onGestureEnd;
private handleTextWysiwyg;
private deselectElements;
private getSelectedTextElement;
private getSelectedTextEditingContainerAtPosition;
private getTextElementAtPosition;
private isHittingTextAutoResizeHandle;
private handleTextAutoResizeHandlePointerDown;
private getElementAtPosition;
private getElementsAtPosition;
getElementHitThreshold(element: ExcalidrawElement): number;
private hitElement;
private getTextBindableContainerAtPosition;
private startTextEditing;
private debounceDoubleClickTimestamp;
private startImageCropping;
private finishImageCropping;
private shouldHandleBrowserCanvasDoubleClick;
private handleCanvasDoubleClick;
private handleCanvasClick;
private getElementLinkAtPosition;
private handleElementLinkClick;
/**
* finds candidate frame under cursor (when dragging frame children/elements
* inside frames)
*/
private getTopLayerFrameAtSceneCoords;
private updateFrameToHighlight;
private maybeUpdateFrameToHighlightOnPointerMove;
private insertNewElements;
private insertNewElement;
private handleCanvasPointerMove;
private handleEraser;
private handleTouchMove;
handleHoverSelectedLinearElement(linearElementEditor: LinearElementEditor, scenePointerX: number, scenePointerY: number): void;
private handleCanvasPointerDown;
private handleCanvasPointerUp;
private maybeOpenContextMenuAfterPointerDownOnTouchDevices;
private resetContextMenuTimer;
/**
* pointerup may not fire in certian cases (user tabs away...), so in order
* to properly cleanup pointerdown state, we need to fire any hanging
* pointerup handlers manually
*/
private maybeCleanupAfterMissingPointerUp;
handleCanvasPanUsingWheelOrSpaceDrag: (event: React.PointerEvent<HTMLElement> | MouseEvent) => boolean;
private startRightClickPanning;
private updateGestureOnPointerDown;
private initialPointerDownState;
private handleDraggingScrollBar;
private clearSelectionIfNotUsingSelection;
/**
* @returns whether the pointer event has been completely handled
*/
private handleSelectionOnPointerDown;
private isASelectedElement;
private isHittingCommonBoundingBoxOfSelectedElements;
private handleTextOnPointerDown;
private handleFreeDrawElementOnPointerDown;
insertIframeElement: ({ sceneX, sceneY, width, height, }: {
sceneX: number;
sceneY: number;
width: number;
height: number;
}) => NonDeleted<ExcalidrawIframeElement>;
insertEmbeddableElement: ({ sceneX, sceneY, link, }: {
sceneX: number;
sceneY: number;
link: string;
}) => NonDeleted<ExcalidrawEmbeddableElement> | undefined;
private newImagePlaceholder;
private handleLinearElementOnPointerDown;
private getCurrentItemRoundness;
private createGenericElementOnPointerDown;
private createFrameElementOnPointerDown;
private maybeCacheReferenceSnapPoints;
private maybeCacheVisibleGaps;
private onKeyDownFromPointerDownHandler;
private onKeyUpFromPointerDownHandler;
private onPointerMoveFromPointerDownHandler;
private handlePointerMoveOverScrollbars;
private onPointerUpFromPointerDownHandler;
private restoreReadyToEraseElements;
private eraseElements;
private initializeImage;
/**
* use during async image initialization,
* when the placeholder image could have been modified in the meantime,
* and when you don't want to loose those modifications
*/
private getLatestInitializedImageElement;
private onImageToolbarButtonClick;
private getImageNaturalDimensions;
/** updates image cache, refreshing updated elements and/or setting status
to error for images that fail during <img> element creation */
private updateImageCache;
/** adds new images to imageCache and re-renders if needed */
private addNewImagesToImageCache;
/** generally you should use `addNewImagesToImageCache()` directly if you need
* to render new images. This is just a failsafe */
private scheduleImageRefresh;
setSelection(elements: readonly NonDeletedExcalidrawElement[]): void;
private clearSelection;
private handleInteractiveCanvasRef;
private insertImages;
private handleAppOnDrop;
loadFileToCanvas: (file: File, fileHandle: FileSystemFileHandle | null) => Promise<void>;
private handleCanvasContextMenu;
private maybeDragNewGenericElement;
private maybeHandleCrop;
private maybeHandleResize;
private getContextMenuItems;
private handleWheel;
private getTextWysiwygSnappedToCenterPosition;
private savePointer;
private resetShouldCacheIgnoreZoomDebounced;
private updateDOMRect;
refresh: () => void;
private getCanvasOffsets;
watchState: () => void;
private updateLanguage;
}
export default App;