import type { IMAGE_MIME_TYPES, UserIdleState, throttleRAF, MIME_TYPES, ColorTuple, EditorInterface, StrokeWidthKey, ViewpointFlip } from "@excalidraw/common"; import type { LinearElementEditor } from "@excalidraw/element"; import type { MaybeTransformHandleType } from "@excalidraw/element"; import type { PointerType, ExcalidrawLinearElement, NonDeletedExcalidrawElement, NonDeleted, TextAlign, ExcalidrawElement, GroupId, ExcalidrawBindableElement, ExcalidrawArrowElement, Arrowhead, FontFamilyValues, FileId, Theme, StrokeRoundness, ExcalidrawEmbeddableElement, ExcalidrawMagicFrameElement, ExcalidrawFrameLikeElement, ExcalidrawElementType, ExcalidrawIframeLikeElement, OrderedExcalidrawElement, ExcalidrawNonSelectionElement, ArrangeAlgorithms, BindMode, ExcalidrawTextElement, StrokeVariability, ExcalidrawGifCache } from "@excalidraw/element/types"; import type { Merge, MaybePromise, ValueOf, MakeBrand } from "@excalidraw/common/utility-types"; import type { CaptureUpdateActionType, DurableIncrement, EphemeralIncrement } from "@excalidraw/element"; import type { GlobalPoint } from "@excalidraw/math"; import type { Action } from "./actions/types"; import type { Spreadsheet } from "./charts"; import type { ClipboardData } from "./clipboard"; import type App from "./components/App"; import type { ContextMenuCustomItem, ContextMenuItems } from "./components/ContextMenu"; import type { SnapLine } from "./snapping"; import type { ImportedDataState } from "./data/types"; import type { SetViewportOptions } from "./viewport"; import type { Language } from "./i18n"; import type { isOverScrollBars } from "./scene/scrollbars"; import type React from "react"; import type { JSX } from "react"; export type { App }; export type { ViewpointFlip } from "@excalidraw/common"; export type SocketId = string & { _brand: "SocketId"; }; export type Collaborator = Readonly<{ pointer?: CollaboratorPointer; button?: "up" | "down"; selectedElementIds?: AppState["selectedElementIds"]; username?: string | null; userState?: UserIdleState; color?: { background: string; stroke: string; }; avatarUrl?: string; id?: string; socketId?: SocketId; isCurrentUser?: boolean; isInCall?: boolean; isSpeaking?: boolean; isMuted?: boolean; }>; export type CollaboratorPointer = { x: number; y: number; tool: "pointer" | "laser"; /** * Whether to render cursor + username. Useful when you only want to render * laser trail. * * @default true */ renderCursor?: boolean; /** * Explicit laser color. * * @default string collaborator's cursor color */ laserColor?: string; }; export type DataURL = string & { _brand: "DataURL"; }; export type BinaryFileData = { mimeType: ValueOf | typeof MIME_TYPES.binary; id: FileId; fileName: string; dataURL: DataURL; /** compact image placeholder, also stored on the corresponding image element */ thumbHash?: string; /** * Epoch timestamp in milliseconds */ created: number; /** * Indicates when the file was last retrieved from storage to be loaded * onto the scene. We use this flag to determine whether to delete unused * files from storage. * * Epoch timestamp in milliseconds. */ lastRetrieved?: number; /** * indicates the version of the file. This can be used to determine whether * the file dataURL has changed e.g. as part of restore due to schema update. */ version?: number; }; export type BinaryFileMetadata = Omit; export type BinaryFiles = Record; export type ImageContextMenuItem = ContextMenuCustomItem; export type ImageStatusStyle = { backgroundColor?: string; color?: string; trackColor?: string; }; export type ImageDownloadErrorStatus = ImageStatusStyle & { /** Optional error message rendered below the error icon. */ text?: string; /** Makes the rendered download error icon actionable when supplied. */ onClick?: (fileId: FileId) => void; }; export type ImageUploadProgressStatus = ImageStatusStyle & { /** * `pending` renders the cloud indicator before upload starts. * `uploading` renders the progress ring around the cloud. * `error` renders the cloud warning indicator. */ state?: "pending" | "uploading" | "error"; progress?: number; /** Optional upload error message for host-owned UI/toasts. */ text?: string; /** Makes the rendered upload progress indicator actionable when supplied. */ onClick?: (fileId: FileId) => void; }; export type ImageStatus = { downloadError?: ImageDownloadErrorStatus | null; uploadProgress?: ImageUploadProgressStatus | null; }; export type RenderCustomizations = { /** * Host-controlled color for transient canvas traces whose visibility depends * on the embedding app background, such as eraser and draw-to-shape trails. */ traceColor?: string; }; export type ToolType = "selection" | "lasso" | "rectangle" | "diamond" | "ellipse" | "arrow" | "line" | "freedraw" | "text" | "image" | "eraser" | "hand" | "frame" | "magicframe" | "embeddable" | "laser" | "autoshape" | "bucketfill"; export type ElementOrToolType = ExcalidrawElementType | ToolType | "custom"; export type ActiveTool = { type: ToolType; customType: null; } | { type: "custom"; customType: string; }; export type SidebarName = string; export type SidebarTabName = string; export type UserToFollow = { socketId: SocketId; username: string; }; type _CommonCanvasAppState = { zoom: AppState["zoom"]; scrollX: AppState["scrollX"]; scrollY: AppState["scrollY"]; width: AppState["width"]; height: AppState["height"]; viewpointFlip: AppState["viewpointFlip"]; viewModeEnabled: AppState["viewModeEnabled"]; openDialog: AppState["openDialog"]; editingGroupId: AppState["editingGroupId"]; selectedElementIds: AppState["selectedElementIds"]; frameToHighlight: AppState["frameToHighlight"]; offsetLeft: AppState["offsetLeft"]; offsetTop: AppState["offsetTop"]; theme: AppState["theme"]; }; export type StaticCanvasAppState = Readonly<_CommonCanvasAppState & { shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; /** null indicates transparent bg */ viewBackgroundColor: AppState["viewBackgroundColor"] | null; exportScale: AppState["exportScale"]; selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; gridSize: AppState["gridSize"]; gridStep: AppState["gridStep"]; frameRendering: AppState["frameRendering"]; currentHoveredFontFamily: AppState["currentHoveredFontFamily"]; hoveredElementIds: AppState["hoveredElementIds"]; suggestedBinding: AppState["suggestedBinding"]; croppingElementId: AppState["croppingElementId"]; }>; export type InteractiveCanvasAppState = Readonly<_CommonCanvasAppState & { activeTool: AppState["activeTool"]; activeEmbeddable: AppState["activeEmbeddable"]; selectionElement: AppState["selectionElement"]; selectedGroupIds: AppState["selectedGroupIds"]; selectedLinearElement: AppState["selectedLinearElement"]; multiElement: AppState["multiElement"]; newElement: AppState["newElement"]; isBindingEnabled: AppState["isBindingEnabled"]; isMidpointSnappingEnabled: AppState["isMidpointSnappingEnabled"]; gridModeEnabled: AppState["gridModeEnabled"]; suggestedBinding: AppState["suggestedBinding"]; hoveredArrowTextAnchor: AppState["hoveredArrowTextAnchor"]; isRotating: AppState["isRotating"]; elementsToHighlight: AppState["elementsToHighlight"]; collaborators: AppState["collaborators"]; snapLines: AppState["snapLines"]; zenModeEnabled: AppState["zenModeEnabled"]; editingTextElement: AppState["editingTextElement"]; isCropping: AppState["isCropping"]; croppingElementId: AppState["croppingElementId"]; searchMatches: AppState["searchMatches"]; activeLockedId: AppState["activeLockedId"]; hoveredElementIds: AppState["hoveredElementIds"]; frameRendering: AppState["frameRendering"]; shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; exportScale: AppState["exportScale"]; currentItemArrowType: AppState["currentItemArrowType"]; }>; export type ObservedAppState = ObservedStandaloneAppState & ObservedElementsAppState; export type ObservedStandaloneAppState = { name: AppState["name"]; viewBackgroundColor: AppState["viewBackgroundColor"]; }; export type ObservedElementsAppState = { editingGroupId: AppState["editingGroupId"]; selectedElementIds: AppState["selectedElementIds"]; selectedGroupIds: AppState["selectedGroupIds"]; selectedLinearElement: { elementId: LinearElementEditor["elementId"]; isEditing: boolean; } | null; croppingElementId: AppState["croppingElementId"]; lockedMultiSelections: AppState["lockedMultiSelections"]; activeLockedId: AppState["activeLockedId"]; }; export type NormaliseMode = "first" | "average"; export type NormaliseMetric = "scale" | "height" | "width" | "size"; export type SmartZoomPreferences = { fitToViewport?: boolean; animate?: boolean; duration?: number; respectUIElements?: boolean; viewportZoomFactor?: number; }; export type ArrangePreferences = { algorithm?: ArrangeAlgorithms; gap?: number; }; export type AlignPreferences = { stacking?: boolean; }; export type NormalisePreferences = { mode?: NormaliseMode; metric?: NormaliseMetric; }; export type EditorPreferences = { smartZoom?: SmartZoomPreferences; align?: AlignPreferences; arrange?: ArrangePreferences; normalise?: NormalisePreferences; }; export type BoxSelectionMode = "contain" | "overlap"; /** * A box, in scene coordinates, that pan & zoom are constrained to. * * This is a private type. For public API, only use specific properties, * needed. */ export type ScrollConstraints = { x: number; y: number; width: number; height: number; /** when set, panning is constrained so the viewport stays within the box */ lockScroll: boolean; /** when set, the viewport cannot zoom out below `zoom` */ lockZoom: boolean; /** * The zoom resolved after the `setViewport` navigation settled. */ zoom: number; /** * Pixel amount the viewport may overscroll past its resting clamp before * snapping back (rubberband). Screen pixels, zoom-independent. Resolved * from `lock.overscroll` at the time the lock was installed (`true` → * default give, `false` → 0). */ overscroll: number; /** * Extra scrollable margin around the box (CSS-style), letting the viewport * scroll past each box edge to reveal that much empty space. Values are * viewport pixels and zoom-independent (a fixed on-screen distance). * Resolved from the `offsets` passed to `setViewport` (see * {@link ViewportOffsets}) at the time the lock was installed. */ offsets?: Offsets; }; export interface AppState { contextMenu: { items: ContextMenuItems; top: number; left: number; } | null; showWelcomeScreen: boolean; isLoading: boolean; myocSimplifiedMode: boolean; dontResizeLimitMBs: number; hideMainMenus: boolean; wheelZoomsOnDefault?: boolean; viewpointFlip: ViewpointFlip; alignConfiguration: { stacking: boolean; }; arrangeConfiguration: { algorithm: ArrangeAlgorithms; gap: number; }; normaliseConfiguration: { mode: NormaliseMode; metric: NormaliseMetric; }; errorMessage: React.ReactNode; activeEmbeddable: { element: NonDeletedExcalidrawElement; state: "hover" | "active"; } | null; /** * for a newly created element * - set on pointer down, updated during pointer move, used on pointer up */ newElement: NonDeleted | null; /** * for a single element that's being resized * - set on pointer down when it's selected and the active tool is selection */ resizingElement: NonDeletedExcalidrawElement | null; /** * multiElement is for multi-point linear element that's created by clicking as opposed to dragging * - when set and present, the editor will handle linear element creation logic accordingly */ multiElement: NonDeleted | null; /** * decoupled from newElement, dragging selection only creates selectionElement * - set on pointer down, updated during pointer move */ selectionElement: NonDeletedExcalidrawElement | null; /** * tracking current arrow binding editor state (takes into account * `bindingPreference` and keyboard modifiers (ctrl/alt) */ isBindingEnabled: boolean; /** user box selection preference; defaults to "contain" when unset */ boxSelectionMode: BoxSelectionMode; /** user arrow binding preference */ bindingPreference: "enabled" | "disabled"; /** user preference whether arrow snap to midpoints while binding */ isMidpointSnappingEnabled: boolean; /** * The bindable element the UI highlights for the user when an arrow is * dragged or otherwise its endpoint being close to said element. */ suggestedBinding: { element: NonDeleted; midPoint?: GlobalPoint; } | null; /** * Where on a hovered arrow the text tool would attach text if clicked — * a free endpoint (binds the arrow to a new text element positioned against * that endpoint) or the arrow's midpoint (adds a label bound to the arrow). */ hoveredArrowTextAnchor: { elementId: ExcalidrawArrowElement["id"]; anchor: "start" | "end" | "label"; } | null; frameToHighlight: NonDeleted | null; frameRendering: { enabled: boolean; name: boolean; outline: boolean; clip: boolean; }; /** * frame-like element whose name is currently being edited */ editingFrame: ExcalidrawFrameLikeElement["id"] | null; elementsToHighlight: readonly NonDeletedExcalidrawElement[] | null; /** * set when a new text is created or when an existing text is being edited */ editingTextElement: ExcalidrawTextElement | null; activeTool: { /** * indicates a previous tool we should revert back to if we deselect the * currently active tool. At the moment applies to `eraser` and `hand` tool. */ lastActiveTool: ActiveTool | null; locked: boolean; fromSelection: boolean; } & ActiveTool; preferredSelectionTool: { type: "selection" | "lasso"; initialized: boolean; }; penMode: boolean; penDetected: boolean; exportBackground: boolean; exportEmbedScene: boolean; exportWithDarkMode: boolean; exportScale: number; currentItemStrokeColor: string; currentItemBackgroundColor: string; currentItemFillStyle: ExcalidrawElement["fillStyle"]; currentItemStrokeWidthKey: StrokeWidthKey; currentItemStrokeStyle: ExcalidrawElement["strokeStyle"]; currentItemRoughness: number; currentItemStrokeVariability: StrokeVariability; currentItemOpacity: number; currentItemFontFamily: FontFamilyValues; currentItemFontSize: number; currentItemTextAlign: TextAlign; currentItemStartArrowhead: Arrowhead | null; currentItemEndArrowhead: Arrowhead | null; currentHoveredFontFamily: FontFamilyValues | null; currentItemRoundness: StrokeRoundness; currentItemArrowType: "sharp" | "round" | "elbow"; viewBackgroundColor: string; scrollX: number; scrollY: number; scrollConstraints: ScrollConstraints | null; cursorButton: "up" | "down"; scrolledOutside: boolean; name: string | null; isResizing: boolean; isRotating: boolean; zoom: Zoom; openMenu: "canvas" | null; openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | "fontFamily" | "compactTextProperties" | "compactStrokeStyles" | "compactOtherProperties" | "compactArrowProperties" | "gifFrameGallery" | "gifSpeedPicker" | null; openSidebar: { name: SidebarName; tab?: SidebarTabName; } | null; openDialog: null | { name: "imageExport" | "help" | "jsonExport"; } | { name: "settings"; } | { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"]; } | { name: "charts"; data: Spreadsheet; rawText: string; }; /** * Reflects user preference for whether the default sidebar should be docked. * * NOTE this is only a user preference and does not reflect the actual docked * state of the sidebar, because the host apps can override this through * a DefaultSidebar prop, which is not reflected back to the appState. */ defaultSidebarDockedPreference: boolean; lastPointerDownWith: PointerType; selectedElementIds: Readonly<{ [id: string]: true; }>; hoveredElementIds: Readonly<{ [id: string]: true; }>; previousSelectedElementIds: { [id: string]: true; }; selectedElementsAreBeingDragged: boolean; shouldCacheIgnoreZoom: boolean; toast: { message: React.ReactNode; closable?: boolean; duration?: number; } | null; zenModeEnabled: boolean; theme: Theme; /** grid cell px size */ gridSize: number; gridStep: number; gridModeEnabled: boolean; viewModeEnabled: boolean; viewModeOnly: boolean; /** top-most selected groups (i.e. does not include nested groups) */ selectedGroupIds: { [groupId: string]: boolean; }; /** group being edited when you drill down to its constituent element (e.g. when you double-click on a group's element) */ editingGroupId: GroupId | null; width: number; height: number; offsetTop: number; offsetLeft: number; fileHandle: FileSystemFileHandle | null; collaborators: Map; stats: { open: boolean; /** bitmap. Use `STATS_PANELS` bit values */ panels: number; }; showHyperlinkPopup: false | "info" | "editor"; selectedLinearElement: LinearElementEditor | null; snapLines: readonly SnapLine[]; originSnapOffset: { x: number; y: number; } | null; objectsSnapModeEnabled: boolean; /** image cropping */ isCropping: boolean; croppingElementId: ExcalidrawElement["id"] | null; /** null if no search matches found / search closed */ searchMatches: Readonly<{ focusedId: ExcalidrawElement["id"] | null; matches: readonly SearchMatch[]; }> | null; /** the locked element/group that's active and shows unlock popup */ activeLockedId: string | null; lockedMultiSelections: { [groupId: string]: true; }; bindMode: BindMode; /** user-customized color-picker top picks (pinned via drag & drop from the * color picker popup). `null` means no customization (defaults, or * host-supplied `topPicks`, are used). Kept per picker. */ colorTopPicks: { elementStroke: readonly string[] | null; elementBackground: readonly string[] | null; /** the bucket-fill tool keeps a list separate from `elementBackground` * even though both drive `currentItemBackgroundColor` (its defaults and * use case differ — no transparent) */ bucketFill: readonly string[] | null; }; } export type SearchMatch = { id: string; focus: boolean; matchedLines: { offsetX: number; offsetY: number; width: number; height: number; showOnCanvas: boolean; }[]; }; export type UIAppState = Omit; export type NormalizedZoomValue = number & { _brand: "normalizedZoom"; }; export type Zoom = Readonly<{ value: NormalizedZoomValue; }>; export type PointerCoords = Readonly<{ x: number; y: number; }>; export type Gesture = { pointers: Map; lastCenter: { x: number; y: number; } | null; initialDistance: number | null; initialScale: number | null; }; export declare class GestureEvent extends UIEvent { readonly rotation: number; readonly scale: number; } export type ExcalidrawInitialDataState = ImportedDataState; export type ExcalidrawInitialState = { viewport?: Omit; }; export type OnUserFollowedPayload = { userToFollow: UserToFollow; action: "FOLLOW" | "UNFOLLOW"; }; export type ViewportStatusFrame = { /** the badge (bottom-center pill) */ label?: { label: React.ReactNode; icon?: React.ReactNode; /** badge background; defaults to var(--color-primary-hover) */ background?: string; /** badge text color; defaults to var(--color-primary-light) */ color?: string; /** makes the badge label interactive */ onClick?: () => void; /** renders a close button when set */ onClose?: () => void; }; /** viewport-edge border: CSS color, or `false` for none */ border: false | string; }; export type OnExportProgress = { type: "progress"; message?: React.ReactNode; /** 0-1 range */ progress?: number; }; export type CompressImageFileOpts = { /** undefined indicates auto */ outputType?: typeof MIME_TYPES["jpg"]; maxWidthOrHeight: number; }; export type CompressImageFile = (file: File, opts: CompressImageFileOpts) => Promise; export type InteractionConfig = { /** * Interactions that stay enabled while the editor is otherwise * non-interactive. Opt-in: anything omitted or `false` is disabled. */ enabled?: { /** * Element links render their link icon and stay clickable — clicking * anywhere on a linked element opens the link, same as in view mode. * When disabled, link icons are not rendered at all. * * @default false */ links?: boolean; /** * Embeddable & iframe elements stay interactive — hovering & clicking * activates them so their content can be used, same as in view mode. * * @default false */ embeds?: boolean; /** * Umbrella for all interactive content on canvas — shorthand for * enabling `links` & `embeds` (and future interactive content kinds) * together. Additive: `interactiveContent: true` enables them * regardless of their individual values. * * @default false */ interactiveContent?: boolean; /** * Canvas navigation — panning (pointer drag, wheel, PageUp/PageDown) * and zooming (ctrl/cmd + wheel, pinch, and the canvas zoom & * zoom-to-fit shortcuts: ctrl/cmd +/-/0, shift+1/2/3), same as in view * mode. Respects `appState.scrollConstraints` if set, so it composes * with viewport locking. The rest of the keyboard stays disabled. Note * the editor consumes wheel & touch input again when enabled, so the * page no longer scrolls over the editor. * * @default false */ navigation?: boolean; /** * Whether the browser's own zoom remains available over the editor — * ctrl/cmd + wheel, pinch, and (while the editor has focus) * ctrl/cmd +/-/0 shortcuts. Prevented by default, mirroring the * interactive editor. Regular page scrolling stays available either way. * With `navigation` enabled, the zoom input (wheel, pinch, keyboard * shortcuts) zooms the canvas instead either way, making this moot. * * @default false */ browserZoom?: boolean; /** * Tools that stay user-driven while the editor is otherwise * non-interactive: pointer input keeps driving the listed tool when it's * the active tool. Does not enable user-driven tool *switching* — the * keyboard stays disabled and tool selection remains host-driven * (`ExcalidrawAPI.setActiveTool`). * * Composes with `navigation`: the enabled tool wins the primary-pointer * drag, while wheel input (and wheel-button drag) still pans/zooms. */ tools?: { /** * The laser pointer stays usable — pointer strokes draw laser trails * and pointer positions keep broadcasting via `onPointerUpdate`, so * e.g. collaborators see a presenter's laser & cursor. * * @default false */ laser?: boolean; /** * Custom tools (`activeTool.type === "custom"`) stay usable — the * editor keeps dispatching `onPointerDown` / `onPointerUp` for them. * Tool behavior is host-implemented; activate custom tools with * `locked: true` or they revert to the selection tool (and go inert) * after the first pointer interaction. * * @default false */ custom?: boolean; }; }; }; export type UIConfig = { /** * Default UI controls that stay enabled while the rest of Excalidraw's * default UI is hidden. Opt-in: anything omitted or `false` is disabled. */ enabled: { /** * The zoom-out, reset-zoom, and zoom-in controls. * * @default false */ zoom?: boolean; /** * The button shown when the viewport is scrolled away from all content. * * @default false */ scrollBackToContent?: boolean; }; }; export interface ExcalidrawProps { className?: string; onChange?: (elements: readonly OrderedExcalidrawElement[], appState: AppState, files: BinaryFiles) => void; onThemeChange?: (theme: Theme | "system") => void; /** * note: only subscribes if the props.onIncrement is defined on initial render */ onIncrement?: (event: DurableIncrement | EphemeralIncrement) => void; initialData?: (() => MaybePromise) | MaybePromise; initialState?: ExcalidrawInitialState; /** * Invoked as soon as the Excalidraw API is available * NOTE editor is not yet mounted, and state is not yet initialized */ onExcalidrawAPI?: (api: ExcalidrawImperativeAPI | null) => void; /** * Invoked once the editor root is mounted. */ onMount?: (payload: ExcalidrawMountPayload) => void; /** * Invoked when the editor root is unmounted. */ onUnmount?: () => void; /** * Invoked once the initial scene is loaded. */ onInitialize?: (api: ExcalidrawImperativeAPI) => void; isCollaborating?: boolean; onPointerUpdate?: (payload: { pointer: { x: number; y: number; tool: "pointer" | "laser"; }; button: "down" | "up"; pointersMap: Gesture["pointers"]; }) => void; onPaste?: (data: ClipboardData, event: ClipboardEvent | null) => Promise | boolean; /** * Controls image paste/drop while the editor is in view mode and not * `viewModeOnly`. * * - `exit-view-mode`: leave view mode and insert the image. * - `reject`: keep view mode and call `onViewModeImageInsertRejected`. * * `viewModeOnly` always rejects because view mode is forced. * * @default "exit-view-mode" */ viewModeImageInsertBehavior?: "exit-view-mode" | "reject"; onViewModeImageInsertRejected?: (payload: { source: "paste" | "drop"; files: readonly File[]; data?: ClipboardData; event: ClipboardEvent | React.DragEvent | null; }) => void; /** * Called when element(s) are duplicated so you can listen or modify as * needed. * * Called when duplicating via mouse-drag, keyboard, paste, etc. * * Returned elements will be used in place of the next elements * (you should return all elements, including deleted, and not mutate * the element if changes are made) */ onDuplicate?: (nextElements: readonly ExcalidrawElement[], /** excludes the duplicated elements */ prevElements: readonly ExcalidrawElement[]) => ExcalidrawElement[] | void; renderTopLeftUI?: (isMobile: boolean, appState: UIAppState) => JSX.Element | null; renderTopRightUI?: (isMobile: boolean, appState: UIAppState) => JSX.Element | null; langCode?: Language["code"]; viewModeEnabled?: boolean; viewModeOnly?: boolean; /** * Whether the editor accepts user input (pointer, keyboard, wheel, touch, * clipboard, drag&drop). When `false`, the scene still renders and reacts * to programmatic updates (imperative API), but the user cannot affect it * in any way. Implies view mode. * * Pass a config object to keep specific interactions enabled while the * editor is otherwise non-interactive (see `InteractionConfig`): * * ```tsx * * ``` * * @default true */ interaction?: boolean | InteractionConfig; /** * Whether Excalidraw's default UI is rendered — toolbar, default menus, * footer controls, sidebars, and canvas popups. Host UI passed through * children (including exported components such as `MainMenu` and `Footer`) * or render props continues to render, together with any supporting dialogs * it opens. * * Canvas content (elements, text editing surface, frame names, embeds) still * renders, and the editor remains interactive unless `interaction` is set to * `false`. * * Pass a config object to keep specific default controls rendered while the * rest of the default UI is hidden (see `UIConfig`): * * ```tsx * * ``` * * NOTE: this is WIP and what default UI is/is not rendered when ui=false * may yet change. * * @default true */ ui?: boolean | UIConfig; /** * Forces the active editor tool (controlled). While set, user- and * API-driven tool switching is ignored — `setActiveTool` refuses with a * console warning, non-forced toolbar buttons render disabled — and the * editor snaps back if internal flows reset the tool. The forced tool * behaves as if locked (see the tool lock / padlock): it doesn't revert to * the selection tool after use, and elements drawn with it aren't * auto-selected — without mutating `appState.activeTool.locked`, so the * user's persisted padlock preference stays untouched. Unset to return * tool control to the editor (the current tool stays active). * * The forced tool must be activatable to take effect: not disabled via * `UIOptions.tools`, and — while the editor is non-interactive — allowed * via `interaction.enabled.tools`. Otherwise the editor stays on (or, when * non-interactive, resets to) the `selection` tool, and the forced tool is * applied once it becomes activatable. `image` cannot be forced (its * activation opens the file picker). */ activeTool?: { type: Exclude; } | { type: "custom"; customType: string; }; gridModeEnabled?: boolean; objectsSnapModeEnabled?: boolean; theme?: Theme; renderCustomizations?: RenderCustomizations; name?: string; renderCustomStats?: (elements: readonly NonDeletedExcalidrawElement[], appState: UIAppState) => JSX.Element; editorPreferences?: EditorPreferences; onEditorPreferencesChange?: (next: EditorPreferences) => void; UIOptions?: Partial; /** * dimensions and size constraints for inserted images */ imageOptions?: ImageOptions; detectScroll?: boolean; handleKeyboardGlobally?: boolean; autoFocus?: boolean; compressImageFile?: CompressImageFile; generateIdForFile?: (file: File) => string | Promise; generateLinkForSelection?: (id: string, type: "element" | "group") => string; onLinkOpen?: (element: NonDeletedExcalidrawElement, event: CustomEvent<{ nativeEvent: MouseEvent | React.PointerEvent; }>) => void; imageContextMenuItems?: (imageIds: readonly ExcalidrawElement["id"][]) => readonly ImageContextMenuItem[]; /** @internal Myoc app-only diagnostics for mobile drag/drop payloads. */ showDropEventDebugAlert?: boolean; onPointerDown?: (activeTool: AppState["activeTool"], pointerDownState: PointerDownState) => void; onPointerUp?: (activeTool: AppState["activeTool"], pointerDownState: PointerDownState) => void; onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void; onUserFollow?: (payload: OnUserFollowedPayload) => void; children?: React.ReactNode; validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) => boolean | undefined); renderEmbeddable?: (element: NonDeleted, appState: AppState) => JSX.Element | null; showDeprecatedFonts?: boolean; wheelZoomsOnDefault?: boolean; strokeColorTopPicks?: ColorTuple; backgroundColorTopPicks?: ColorTuple; renderScrollbars?: boolean; viewportStatusFrame?: ViewportStatusFrame | null; /** * Rendered inside the UserList "who's here" dropdown (desktop) and inline * in the mobile menu's collaborators section, below a divider. Accepts a * render function — called with `isMobile` so hosts can render different * UI for each surface — in addition to a plain node. */ currentUserControls?: React.ReactNode | ((isMobile: boolean) => React.ReactNode); /** * The user being followed on the canvas, if any. Controlled by the host — * the editor never sets it; it emits follow/unfollow intents via * `onUserFollow` (prop or imperative API) and renders the followed * user's avatar highlight from this value. */ userToFollow?: UserToFollow | null; /** * Called before exporting to a file. * * Allows the host app to intercept and delay saving until async operations * (e.g., images are loaded) complete. * * If Promise/AsyncGenerator is returned, a progress toast will be shown * until the operation completes. Generator can yield progress updates. */ onExport?: ( /** type of export. Currently we only call for JSON exports or * JSON-embedded PNG (which is also identified as `json` type here)*/ type: "json", data: { elements: readonly ExcalidrawElement[]; appState: AppState; files: BinaryFiles; }, options: { /** signal that gets aborted if user cancels the export (e.g. closes * the native file picker dialog). In that case, you can either * return immediately, or throw AbortError. */ signal: AbortSignal; }) => MaybePromise | AsyncGenerator; } export type SceneData = { elements?: ImportedDataState["elements"]; appState?: ImportedDataState["appState"]; collaborators?: Map; captureUpdate?: CaptureUpdateActionType; }; export type ExportOpts = { saveFileToDisk?: boolean; onExportToBackend?: (exportedElements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, files: BinaryFiles) => void; renderCustomUI?: (exportedElements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, files: BinaryFiles, canvas: HTMLCanvasElement) => JSX.Element; }; export type ImageOptions = Partial<{ maxWidthOrHeight: number; maxFileSizeBytes: number; /** GIF files larger than this are inserted as static images until manually loaded */ gifAutoDecodeMaxFileSizeBytes: number; /** duration of the placeholder-to-full-image crossfade in milliseconds */ placeholderTransitionDuration: number; }>; export type CanvasActions = Partial<{ changeViewBackgroundColor: boolean; export: false | ExportOpts; loadScene: boolean; saveToActiveFile: boolean; /** * defaults to true if `props.theme` is omitted or `props.onThemeChange` * is supplied (at which point the theme is considered as host-app controlled), * else default to false * */ toggleTheme: boolean | null; saveAsImage: boolean; }>; export type UIOptions = Partial<{ dockedSidebarBreakpoint: number; canvasActions: CanvasActions; tools: { image: boolean; }; /** * Optionally control the editor form factor and desktop UI mode from the host app. * If not provided, we will take care of it internally. */ getFormFactor?: (editorWidth: number, editorHeight: number) => EditorInterface["formFactor"]; /** @deprecated does nothing. Will be removed in 0.15 */ welcomeScreen?: boolean; }>; export type AppProps = Merge & { export: ExportOpts; }; }>; imageOptions: Required; detectScroll: boolean; handleKeyboardGlobally: boolean; isCollaborating: boolean; children?: React.ReactNode; wheelZoomsOnDefault?: boolean; }>; /** A subset of App class properties that we need to use elsewhere * in the app, eg Manager. Factored out into a separate type to keep DRY. */ export type AppClassProperties = { props: AppProps; state: AppState; api: App["api"]; sessionExportThemeOverride: App["sessionExportThemeOverride"]; interactiveCanvas: HTMLCanvasElement | null; /** static canvas */ canvas: HTMLCanvasElement; focusContainer(): void; imageCache: Map; mimeType: ValueOf; isPlaceholder?: boolean; placeholderImage?: HTMLImageElement; transitionStart?: number; gifDecodeStatus?: "pending" | "success" | "error" | "deferred"; gif?: ExcalidrawGifCache; }>; imageLoadingProgress: App["imageLoadingProgress"]; imageLoadingProgressEmitter: App["imageLoadingProgressEmitter"]; imageStatus: App["imageStatus"]; imageStatusEmitter: App["imageStatusEmitter"]; imagePlaceholderUpdateEmitter: App["imagePlaceholderUpdateEmitter"]; files: BinaryFiles; editorInterface: App["editorInterface"]; scene: App["scene"]; syncActionResult: App["syncActionResult"]; fonts: App["fonts"]; pasteFromClipboard: App["pasteFromClipboard"]; id: App["id"]; onInsertElements: App["onInsertElements"]; onExportImage: App["onExportImage"]; viewport: App["viewport"]; lastViewportPosition: App["lastViewportPosition"]; scrollToViewport: App["scrollToViewport"]; scrollToContent: App["scrollToContent"]; addFiles: App["addFiles"]; scheduleCapture: App["scheduleCapture"]; scheduleUpdate: App["scheduleUpdate"]; ensureGifPlaybackLoop: App["ensureGifPlaybackLoop"]; loadDeferredGif: App["loadDeferredGif"]; getGifPlaybackFrameIndex: App["getGifPlaybackFrameIndex"]; setGifPlaybackFrameIndex: App["setGifPlaybackFrameIndex"]; addElementsFromPaste: App["addElementsFromPaste"]; togglePenMode: App["togglePenMode"]; toggleLock: App["toggleLock"]; openEyeDropper: App["openEyeDropper"]; setActiveTool: App["setActiveTool"]; setOpenDialog: App["setOpenDialog"]; insertEmbeddableElement: App["insertEmbeddableElement"]; getName: App["getName"]; dismissLinearEditor: App["dismissLinearEditor"]; flowchart: App["flowchart"]; drawShape: App["drawShape"]; cursor: App["cursor"]; bucketFill: App["bucketFill"]; isToolLocked: App["isToolLocked"]; getEffectiveGridSize: App["getEffectiveGridSize"]; visibleElements: App["visibleElements"]; excalidrawContainerValue: App["excalidrawContainerValue"]; onPointerUpEmitter: App["onPointerUpEmitter"]; updateEditorAtom: App["updateEditorAtom"]; onPointerDownEmitter: App["onPointerDownEmitter"]; onEvent: App["onEvent"]; onStateChange: App["onStateChange"]; lastPointerMoveCoords: App["lastPointerMoveCoords"]; lastPointerMoveEvent: App["lastPointerMoveEvent"]; bindModeHandler: App["bindModeHandler"]; emitUserFollowIntent: App["emitUserFollowIntent"]; requestUnfollow: App["requestUnfollow"]; setAppState: App["setAppState"]; isInteractionEnabled: App["isInteractionEnabled"]; isNavigationEnabled: App["isNavigationEnabled"]; }; export type PointerDownState = Readonly<{ origin: Readonly<{ x: number; y: number; }>; originInGrid: Readonly<{ x: number; y: number; }>; scrollbars: ReturnType; lastCoords: { x: number; y: number; }; originalElements: Map>; resize: { handleType: MaybeTransformHandleType; isResizing: boolean; offset: { x: number; y: number; }; arrowDirection: "origin" | "end"; center: { x: number; y: number; }; }; hit: { element: NonDeleted | null; allHitElements: NonDeleted[]; 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; onUp: null | ((event: PointerEvent) => void); onKeyDown: null | ((event: KeyboardEvent) => void); onKeyUp: null | ((event: KeyboardEvent) => void); }; boxSelection: { hasOccurred: boolean; }; }>; export type UnsubscribeCallback = () => void; export type ExcalidrawMountPayload = { excalidrawAPI: ExcalidrawImperativeAPI; container: HTMLDivElement | null; }; export type ExcalidrawImperativeAPIEventMap = { "editor:mount": [payload: ExcalidrawMountPayload]; "editor:initialize": [api: ExcalidrawImperativeAPI]; "editor:unmount": []; }; export interface ExcalidrawImperativeAPI { /** Whether the editor has been unmounted and the API is no longer usable. */ isDestroyed: boolean; updateScene: InstanceType["updateScene"]; applyDeltas: InstanceType["applyDeltas"]; mutateElement: InstanceType["mutateElement"]; resetScene: InstanceType["resetScene"]; getSceneElementsIncludingDeleted: InstanceType["getSceneElementsIncludingDeleted"]; getSceneElementsMapIncludingDeleted: InstanceType["getSceneElementsMapIncludingDeleted"]; history: { clear: InstanceType["resetHistory"]; }; getSceneElements: InstanceType["getSceneElements"]; getAppState: () => InstanceType["state"]; getFiles: () => InstanceType["files"]; getName: InstanceType["getName"]; scrollToViewport: InstanceType["scrollToViewport"]; scrollToContent: InstanceType["scrollToContent"]; setViewport: InstanceType["viewport"]["setViewport"]; getViewportOffsets: InstanceType["viewport"]["getOffsets"]; registerAction: (action: Action) => void; refresh: InstanceType["refresh"]; setToast: InstanceType["setToast"]; addFiles: (data: BinaryFileData[]) => void; addImagePlaceholder: (fileId: FileId, file: File) => Promise; setDownloadProgress: (fileId: FileId, progress: number | null) => void; setDownloadError: (fileId: FileId, error: boolean | ImageDownloadErrorStatus | null) => void; setUploadProgress: (fileId: FileId, progress: number | "pending" | "error" | null, status?: Omit | null) => void; addImageElementsToScene: (imageFiles: { file: File; customData: Record; }[], sceneX: number, sceneY: number) => Promise; id: string; setActiveTool: InstanceType["setActiveTool"]; setCursor: InstanceType["cursor"]["set"]; resetCursor: InstanceType["cursor"]["reset"]; toggleSidebar: InstanceType["toggleSidebar"]; getEditorInterface: () => EditorInterface; /** * Disables rendering of frames (including element clipping), but currently * the frames are still interactive in edit mode. As such, this API should be * used in conjunction with view mode (props.viewModeEnabled). */ updateFrameRendering: InstanceType["updateFrameRendering"]; onChange: (callback: (elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles) => void) => UnsubscribeCallback; onIncrement: (callback: (event: DurableIncrement | EphemeralIncrement) => void) => UnsubscribeCallback; onPointerDown: (callback: (activeTool: AppState["activeTool"], pointerDownState: PointerDownState, event: React.PointerEvent) => void) => UnsubscribeCallback; onPointerUp: (callback: (activeTool: AppState["activeTool"], pointerDownState: PointerDownState, event: PointerEvent) => void) => UnsubscribeCallback; onScrollChange: (callback: (scrollX: number, scrollY: number, zoom: Zoom) => void) => UnsubscribeCallback; onUserFollow: (callback: (payload: OnUserFollowedPayload) => void) => UnsubscribeCallback; onStateChange: InstanceType["onStateChange"]; onEvent: InstanceType["onEvent"]; } export type FrameNameBounds = { x: number; y: number; width: number; height: number; }; export type FrameNameBoundsCache = { get: (frameElement: ExcalidrawFrameLikeElement | ExcalidrawMagicFrameElement) => FrameNameBounds | null; _cache: Map; }; export type KeyboardModifiersObject = { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean; }; export type Primitive = number | string | boolean | bigint | symbol | null | undefined; export type JSONValue = string | number | boolean | null | object; export type EmbedsValidationStatus = Map; export type ElementsPendingErasure = Set; export type PendingExcalidrawElements = NonDeletedExcalidrawElement[]; /** Runtime gridSize value. Null indicates disabled grid. */ export type NullableGridSize = (AppState["gridSize"] & MakeBrand<"NullableGridSize">) | null; export type Offsets = Partial<{ top: number; right: number; bottom: number; left: number; }>; /** * Value of the `data-viewport-ui` attribute, marking a DOM node as a UI * surface that occludes the canvas. Such nodes are measured by * `getViewportOffsets` to compute the default per-side viewport offsets: * * - `top` / `bottom` — offsets that side by the node's bottom/top edge * - `side` — a panel hugging the left or right edge. Which side is not * declared but resolved geometrically: if the node's horizontal center * lies in the left half of the viewport it counts against the left * offset (by its right edge), otherwise against the right offset (by * `viewportWidth - left edge`). Measuring the rendered position instead * of declaring a side means RTL layouts and host-configurable docking * (e.g. sidebar side) are handled for free — but it assumes the surface * actually hugs one edge; don't mark a centered/near-full-width node as * `side` (its midpoint would classify it to one side and the offset * would swallow most of the viewport). * * The attribute should only be present while the surface is actually * rendered — omit it (don't just hide the node) when the surface shouldn't * push the viewport around. */ export type ViewportUIDock = "top" | "bottom" | "side"; /** * Options for `getViewportOffsets` (and the `ui` key of * {@link ViewportOffsets}), controlling how offsets are derived from the * currently rendered editor UI. * * NOTE unlike the physical sides of {@link Offsets}, the horizontal values * here are logical, i.e. flipped in RTL layouts (`left` refers to the * reading-direction start side). */ export type ViewportOffsetsOptions = { /** padding added to each measured side (default 24) */ padding?: number; paddingTop?: number; paddingRight?: number; paddingBottom?: number; paddingLeft?: number; /** final value for the given side, replacing the measured UI size * (padding is not added on top) */ top?: number; bottom?: number; left?: number; right?: number; /** * Reserve space for the given conditionally-rendered surfaces even while * they're hidden, so the resulting offsets don't shift when they * (dis)appear. Uses the surface's last-measured footprint, falling back * to an approximate default if it hasn't been rendered yet. Ignored on * phones (where these surfaces never occlude the canvas). */ reserve?: { /** styles panel (rendered when a tool or selection is active) */ stylesPanel?: boolean; /** sidebar (e.g. library) */ sidebar?: boolean; }; }; /** * Viewport offsets accepted by the `setViewport`-family APIs (`setViewport`, * `props.initialState.viewport`), insetting the usable viewport area per * side so the target isn't fitted/centered underneath overlaid UI. * * Two (combinable) ways to specify: * * - **Static sides** (`top`/`right`/`bottom`/`left`) — absolute pixel * values, used as-is: physical (not flipped in RTL), zoom-independent, * no padding added. Sides not specified default to `0` (unless `ui` is * set, see below). * * - **`ui`** — derive the offsets from the editor UI (toolbar, styles * panel, sidebar...) as rendered at the time the viewport is set, * equivalent to calling `getViewportOffsets()`. Pass `true` for the * defaults, or options ({@link ViewportOffsetsOptions}) to customize * padding or reserve space for currently-hidden surfaces. * * When both are given, a static side always wins for that side — it * replaces whatever `ui` would yield (including `ui`'s own side overrides, * which — unlike the physical static sides — are RTL-relative). The * remaining sides fall back to the `ui`-derived values. * * @example * { top: 40 } // top 40px, other sides 0 * { ui: true } // measured UI + default padding * { ui: { reserve: { stylesPanel: true } } } // + keep space for hidden panel * { top: 40, ui: true } // top exactly 40px, rest from UI */ export type ViewportOffsets = Offsets & { ui?: true | ViewportOffsetsOptions; }; /** * Value of the `data-viewport-ui-name` attribute, identifying a * conditionally-rendered surface (marked with `data-viewport-ui`) so that * `getViewportOffsets` can reserve space for it while it's hidden (see the * `reserve` option). Whenever a named surface is rendered, its measured * footprint is remembered; reserving uses that remembered footprint, or an * approximate default if the surface hasn't been rendered yet. */ export type ViewportUIName = "sidebar" | "stylesPanel";