merge with master

This commit is contained in:
Ryan Di
2025-04-14 16:18:11 +10:00
938 changed files with 127956 additions and 41748 deletions
+221 -66
View File
@@ -1,5 +1,17 @@
import React from "react";
import {
import type {
IMAGE_MIME_TYPES,
UserIdleState,
throttleRAF,
MIME_TYPES,
} from "@excalidraw/common";
import type { SuggestedBinding } from "@excalidraw/element/binding";
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import type { MaybeTransformHandleType } from "@excalidraw/element/transformHandles";
import type {
PointerType,
ExcalidrawLinearElement,
NonDeletedExcalidrawElement,
@@ -20,27 +32,32 @@ import {
ExcalidrawFrameLikeElement,
ExcalidrawElementType,
ExcalidrawIframeLikeElement,
} from "./element/types";
import { Action } from "./actions/types";
import { Point as RoughPoint } from "roughjs/bin/geometry";
import { LinearElementEditor } from "./element/linearElementEditor";
import { SuggestedBinding } from "./element/binding";
import { ImportedDataState } from "./data/types";
import type App from "./components/App";
import type { throttleRAF } from "./utils";
import { Spreadsheet } from "./charts";
import { Language } from "./i18n";
import { ClipboardData } from "./clipboard";
import { isOverScrollBars } from "./scene/scrollbars";
import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { SnapLine } from "./snapping";
import { Merge, ValueOf } from "./utility-types";
OrderedExcalidrawElement,
ExcalidrawNonSelectionElement,
} from "@excalidraw/element/types";
export type Point = Readonly<RoughPoint>;
import type {
Merge,
MaybePromise,
ValueOf,
MakeBrand,
} from "@excalidraw/common/utility-types";
import type { Action } from "./actions/types";
import type { Spreadsheet } from "./charts";
import type { ClipboardData } from "./clipboard";
import type App from "./components/App";
import type Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { ContextMenuItems } from "./components/ContextMenu";
import type { SnapLine } from "./snapping";
import type { CaptureUpdateActionType } from "./store";
import type { ImportedDataState } from "./data/types";
import type { Language } from "./i18n";
import type { isOverScrollBars } from "./scene/scrollbars";
import type React from "react";
import type { JSX } from "react";
export type SocketId = string & { _brand: "SocketId" };
@@ -61,12 +78,28 @@ export type Collaborator = Readonly<{
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" };
@@ -90,6 +123,11 @@ export type BinaryFileData = {
* 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<BinaryFileData, "dataURL">;
@@ -98,6 +136,7 @@ export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
export type ToolType =
| "selection"
| "lasso"
| "rectangle"
| "diamond"
| "ellipse"
@@ -140,6 +179,7 @@ type _CommonCanvasAppState = {
width: AppState["width"];
height: AppState["height"];
viewModeEnabled: AppState["viewModeEnabled"];
openDialog: AppState["openDialog"];
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
@@ -157,7 +197,12 @@ export type StaticCanvasAppState = Readonly<
exportScale: AppState["exportScale"];
selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"];
gridSize: AppState["gridSize"];
gridStep: AppState["gridStep"];
frameRendering: AppState["frameRendering"];
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
hoveredElementIds: AppState["hoveredElementIds"];
// Cropping
croppingElementId: AppState["croppingElementId"];
}
>;
@@ -179,9 +224,34 @@ export type InteractiveCanvasAppState = Readonly<
// SnapLines
snapLines: AppState["snapLines"];
zenModeEnabled: AppState["zenModeEnabled"];
editingTextElement: AppState["editingTextElement"];
// Cropping
isCropping: AppState["isCropping"];
croppingElementId: AppState["croppingElementId"];
// Search matches
searchMatches: AppState["searchMatches"];
}
>;
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"];
// Avoiding storing whole instance, as it could lead into state incosistencies, empty undos/redos and etc.
editingLinearElementId: LinearElementEditor["elementId"] | null;
// Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId
selectedLinearElementId: LinearElementEditor["elementId"] | null;
croppingElementId: AppState["croppingElementId"];
};
export interface AppState {
contextMenu: {
items: ContextMenuItems;
@@ -195,9 +265,25 @@ export interface AppState {
element: NonDeletedExcalidrawElement;
state: "hover" | "active";
} | null;
draggingElement: NonDeletedExcalidrawElement | null;
/**
* for a newly created element
* - set on pointer down, updated during pointer move, used on pointer up
*/
newElement: NonDeleted<ExcalidrawNonSelectionElement> | 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<ExcalidrawLinearElement> | null;
/**
* decoupled from newElement, dragging selection only creates selectionElement
* - set on pointer down, updated during pointer move
*/
selectionElement: NonDeletedExcalidrawElement | null;
isBindingEnabled: boolean;
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
@@ -211,9 +297,10 @@ export interface AppState {
};
editingFrame: string | null;
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
// element being edited, but not necessarily added to elements array yet
// (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null;
/**
* set when a new text is created or when an existing text is being edited
*/
editingTextElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
activeTool: {
/**
@@ -222,6 +309,8 @@ export interface AppState {
*/
lastActiveTool: ActiveTool | null;
locked: boolean;
// indicates if the current tool is temporarily switched on from the selection tool
fromSelection: boolean;
} & ActiveTool;
penMode: boolean;
penDetected: boolean;
@@ -241,31 +330,33 @@ export interface AppState {
currentItemTextAlign: TextAlign;
currentItemStartArrowhead: Arrowhead | null;
currentItemEndArrowhead: Arrowhead | null;
currentHoveredFontFamily: FontFamilyValues | null;
currentItemRoundness: StrokeRoundness;
currentItemArrowType: "sharp" | "round" | "elbow";
viewBackgroundColor: string;
scrollX: number;
scrollY: number;
cursorButton: "up" | "down";
scrolledOutside: boolean;
name: string;
name: string | null;
isResizing: boolean;
isRotating: boolean;
zoom: Zoom;
openMenu: "canvas" | "shape" | null;
openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
openPopup:
| "canvasBackground"
| "elementBackground"
| "elementStroke"
| "fontFamily"
| null;
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
openDialog:
| null
| { name: "imageExport" | "help" | "jsonExport" }
| {
name: "settings";
source:
| "tool" // when magicframe tool is selected
| "generation" // when magicframe generate button is clicked
| "settings"; // when AI settings dialog is explicitly invoked
tab: "text-to-diagram" | "diagram-to-code";
}
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" };
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
| { name: "commandPalette" }
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
/**
* Reflects user preference for whether the default sidebar should be docked.
*
@@ -277,13 +368,17 @@ export interface AppState {
lastPointerDownWith: PointerType;
selectedElementIds: Readonly<{ [id: string]: true }>;
hoveredElementIds: Readonly<{ [id: string]: true }>;
previousSelectedElementIds: { [id: string]: true };
selectedElementsAreBeingDragged: boolean;
shouldCacheIgnoreZoom: boolean;
toast: { message: string; closable?: boolean; duration?: number } | null;
zenModeEnabled: boolean;
theme: Theme;
gridSize: number | null;
/** grid cell px size */
gridSize: number;
gridStep: number;
gridModeEnabled: boolean;
viewModeEnabled: boolean;
/** top-most selected groups (i.e. does not include nested groups) */
@@ -298,7 +393,11 @@ export interface AppState {
fileHandle: FileSystemHandle | null;
collaborators: Map<SocketId, Collaborator>;
showStats: boolean;
stats: {
open: boolean;
/** bitmap. Use `STATS_PANELS` bit values */
panels: number;
};
currentChartType: ChartType;
pasteDialog:
| {
@@ -319,13 +418,30 @@ export interface AppState {
y: number;
} | null;
objectsSnapModeEnabled: boolean;
/** the user's clientId & username who is being followed on the canvas */
/** the user's socket id & username who is being followed on the canvas */
userToFollow: UserToFollow | null;
/** the clientIds of the users following the current user */
/** the socket ids of the users following the current user */
followedBy: Set<SocketId>;
scrollConstraints: ScrollConstraints | null;
/** image cropping */
isCropping: boolean;
croppingElementId: ExcalidrawElement["id"] | null;
searchMatches: readonly SearchMatch[];
}
type SearchMatch = {
id: string;
focus: boolean;
matchedLines: {
offsetX: number;
offsetY: number;
width: number;
height: number;
}[];
};
export type UIAppState = Omit<
AppState,
| "suggestedBindings"
@@ -381,21 +497,14 @@ export type LibraryItems_anyVersion = LibraryItems | LibraryItems_v1;
export type LibraryItemsSource =
| ((
currentLibraryItems: LibraryItems,
) =>
| Blob
| LibraryItems_anyVersion
| Promise<LibraryItems_anyVersion | Blob>)
| Blob
| LibraryItems_anyVersion
| Promise<LibraryItems_anyVersion | Blob>;
) => MaybePromise<LibraryItems_anyVersion | Blob>)
| MaybePromise<LibraryItems_anyVersion | Blob>;
// -----------------------------------------------------------------------------
export type ExcalidrawInitialDataState = Merge<
ImportedDataState,
{
libraryItems?:
| Required<ImportedDataState>["libraryItems"]
| Promise<Required<ImportedDataState>["libraryItems"]>;
libraryItems?: MaybePromise<Required<ImportedDataState>["libraryItems"]>;
}
>;
@@ -406,14 +515,13 @@ export type OnUserFollowedPayload = {
export interface ExcalidrawProps {
onChange?: (
elements: readonly ExcalidrawElement[],
elements: readonly OrderedExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => void;
initialData?:
| ExcalidrawInitialDataState
| null
| Promise<ExcalidrawInitialDataState | null>;
| (() => MaybePromise<ExcalidrawInitialDataState | null>)
| MaybePromise<ExcalidrawInitialDataState | null>;
excalidrawAPI?: (api: ExcalidrawImperativeAPI) => void;
isCollaborating?: boolean;
onPointerUpdate?: (payload: {
@@ -425,6 +533,22 @@ export interface ExcalidrawProps {
data: ClipboardData,
event: ClipboardEvent | null,
) => Promise<boolean> | boolean;
/**
* Called when element(s) are duplicated so you can listen or modify as
* needed.
*
* Called when duplicating via mouse-drag, keyboard, paste, library insert
* 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;
renderTopRightUI?: (
isMobile: boolean,
appState: UIAppState,
@@ -436,6 +560,7 @@ export interface ExcalidrawProps {
objectsSnapModeEnabled?: boolean;
libraryReturnUrl?: string;
theme?: Theme;
// @TODO come with better API before v0.18.0
name?: string;
renderCustomStats?: (
elements: readonly NonDeletedExcalidrawElement[],
@@ -447,6 +572,7 @@ export interface ExcalidrawProps {
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
autoFocus?: boolean;
generateIdForFile?: (file: File) => string | Promise<string>;
generateLinkForSelection?: (id: string, type: "element" | "group") => string;
onLinkOpen?: (
element: NonDeletedExcalidrawElement,
event: CustomEvent<{
@@ -476,21 +602,16 @@ export interface ExcalidrawProps {
appState: AppState,
) => JSX.Element | null;
aiEnabled?: boolean;
showDeprecatedFonts?: boolean;
}
export type SceneData = {
elements?: ImportedDataState["elements"];
appState?: ImportedDataState["appState"];
collaborators?: Map<SocketId, Collaborator>;
commitToHistory?: boolean;
captureUpdate?: CaptureUpdateActionType;
};
export enum UserIdleState {
ACTIVE = "active",
AWAY = "away",
IDLE = "idle",
}
export type ExportOpts = {
saveFileToDisk?: boolean;
onExportToBackend?: (
@@ -551,6 +672,7 @@ export type AppProps = Merge<
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
export type AppClassProperties = {
props: AppProps;
state: AppState;
interactiveCanvas: HTMLCanvasElement | null;
/** static canvas */
canvas: HTMLCanvasElement;
@@ -566,6 +688,8 @@ export type AppClassProperties = {
files: BinaryFiles;
device: App["device"];
scene: App["scene"];
syncActionResult: App["syncActionResult"];
fonts: App["fonts"];
pasteFromClipboard: App["pasteFromClipboard"];
id: App["id"];
onInsertElements: App["onInsertElements"];
@@ -575,10 +699,22 @@ export type AppClassProperties = {
addFiles: App["addFiles"];
addElementsFromPasteOrLibrary: App["addElementsFromPasteOrLibrary"];
togglePenMode: App["togglePenMode"];
toggleLock: App["toggleLock"];
setActiveTool: App["setActiveTool"];
setOpenDialog: App["setOpenDialog"];
insertEmbeddableElement: App["insertEmbeddableElement"];
onMagicframeToolSelect: App["onMagicframeToolSelect"];
getName: App["getName"];
dismissLinearEditor: App["dismissLinearEditor"];
flowChartCreator: App["flowChartCreator"];
getEffectiveGridSize: App["getEffectiveGridSize"];
setPlugins: App["setPlugins"];
plugins: App["plugins"];
getEditorUIOffsets: App["getEditorUIOffsets"];
visibleElements: App["visibleElements"];
excalidrawContainerValue: App["excalidrawContainerValue"];
onPointerUpEmitter: App["onPointerUpEmitter"];
};
export type PointerDownState = Readonly<{
@@ -643,7 +779,7 @@ export type PointerDownState = Readonly<{
export type UnsubscribeCallback = () => void;
export type ExcalidrawImperativeAPI = {
export interface ExcalidrawImperativeAPI {
updateScene: InstanceType<typeof App>["updateScene"];
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
resetScene: InstanceType<typeof App>["resetScene"];
@@ -653,10 +789,11 @@ export type ExcalidrawImperativeAPI = {
history: {
clear: InstanceType<typeof App>["resetHistory"];
};
scrollToContent: InstanceType<typeof App>["scrollToContent"];
getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"];
getFiles: () => InstanceType<typeof App>["files"];
getName: InstanceType<typeof App>["getName"];
scrollToContent: InstanceType<typeof App>["scrollToContent"];
registerAction: (action: Action) => void;
refresh: InstanceType<typeof App>["refresh"];
setToast: InstanceType<typeof App>["setToast"];
@@ -700,7 +837,7 @@ export type ExcalidrawImperativeAPI = {
callback: (payload: OnUserFollowedPayload) => void,
) => UnsubscribeCallback;
setScrollConstraints: InstanceType<typeof App>["setScrollConstraints"];
};
}
export type Device = Readonly<{
viewport: {
@@ -714,7 +851,7 @@ export type Device = Readonly<{
isTouchScreen: boolean;
}>;
type FrameNameBounds = {
export type FrameNameBounds = {
x: number;
y: number;
width: number;
@@ -775,3 +912,21 @@ export type ScrollConstraints = {
viewportZoomFactor?: number;
lockZoom?: boolean;
};
export type PendingExcalidrawElements = ExcalidrawElement[];
/** Runtime gridSize value. Null indicates disabled grid. */
export type NullableGridSize =
| (AppState["gridSize"] & MakeBrand<"NullableGridSize">)
| null;
export type GenerateDiagramToCode = (props: {
frame: ExcalidrawMagicFrameElement;
children: readonly ExcalidrawElement[];
}) => MaybePromise<{ html: string }>;
export type Offsets = Partial<{
top: number;
right: number;
bottom: number;
left: number;
}>;