types.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import {
  2. PointerType,
  3. ExcalidrawLinearElement,
  4. NonDeletedExcalidrawElement,
  5. NonDeleted,
  6. TextAlign,
  7. ExcalidrawElement,
  8. GroupId,
  9. ExcalidrawBindableElement,
  10. Arrowhead,
  11. ChartType,
  12. FontFamilyValues,
  13. FileId,
  14. ExcalidrawImageElement,
  15. Theme,
  16. StrokeRoundness,
  17. } from "./element/types";
  18. import { SHAPES } from "./shapes";
  19. import { Point as RoughPoint } from "roughjs/bin/geometry";
  20. import { LinearElementEditor } from "./element/linearElementEditor";
  21. import { SuggestedBinding } from "./element/binding";
  22. import { ImportedDataState } from "./data/types";
  23. import type App from "./components/App";
  24. import type { ResolvablePromise, throttleRAF } from "./utils";
  25. import { Spreadsheet } from "./charts";
  26. import { Language } from "./i18n";
  27. import { ClipboardData } from "./clipboard";
  28. import { isOverScrollBars } from "./scene";
  29. import { MaybeTransformHandleType } from "./element/transformHandles";
  30. import Library from "./data/library";
  31. import type { FileSystemHandle } from "./data/filesystem";
  32. import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
  33. import { ContextMenuItems } from "./components/ContextMenu";
  34. export type Point = Readonly<RoughPoint>;
  35. export type Collaborator = {
  36. pointer?: {
  37. x: number;
  38. y: number;
  39. };
  40. button?: "up" | "down";
  41. selectedElementIds?: AppState["selectedElementIds"];
  42. username?: string | null;
  43. userState?: UserIdleState;
  44. color?: {
  45. background: string;
  46. stroke: string;
  47. };
  48. // The url of the collaborator's avatar, defaults to username intials
  49. // if not present
  50. avatarUrl?: string;
  51. // user id. If supplied, we'll filter out duplicates when rendering user avatars.
  52. id?: string;
  53. };
  54. export type DataURL = string & { _brand: "DataURL" };
  55. export type BinaryFileData = {
  56. mimeType:
  57. | typeof ALLOWED_IMAGE_MIME_TYPES[number]
  58. // future user or unknown file type
  59. | typeof MIME_TYPES.binary;
  60. id: FileId;
  61. dataURL: DataURL;
  62. /**
  63. * Epoch timestamp in milliseconds
  64. */
  65. created: number;
  66. /**
  67. * Indicates when the file was last retrieved from storage to be loaded
  68. * onto the scene. We use this flag to determine whether to delete unused
  69. * files from storage.
  70. *
  71. * Epoch timestamp in milliseconds.
  72. */
  73. lastRetrieved?: number;
  74. };
  75. export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
  76. export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
  77. export type LastActiveToolBeforeEraser =
  78. | {
  79. type: typeof SHAPES[number]["value"] | "eraser";
  80. customType: null;
  81. }
  82. | {
  83. type: "custom";
  84. customType: string;
  85. }
  86. | null;
  87. export type AppState = {
  88. contextMenu: {
  89. items: ContextMenuItems;
  90. top: number;
  91. left: number;
  92. } | null;
  93. showWelcomeScreen: boolean;
  94. isLoading: boolean;
  95. errorMessage: string | null;
  96. draggingElement: NonDeletedExcalidrawElement | null;
  97. resizingElement: NonDeletedExcalidrawElement | null;
  98. multiElement: NonDeleted<ExcalidrawLinearElement> | null;
  99. selectionElement: NonDeletedExcalidrawElement | null;
  100. isBindingEnabled: boolean;
  101. startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
  102. suggestedBindings: SuggestedBinding[];
  103. // element being edited, but not necessarily added to elements array yet
  104. // (e.g. text element when typing into the input)
  105. editingElement: NonDeletedExcalidrawElement | null;
  106. editingLinearElement: LinearElementEditor | null;
  107. activeTool:
  108. | {
  109. type: typeof SHAPES[number]["value"] | "eraser";
  110. lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
  111. locked: boolean;
  112. customType: null;
  113. }
  114. | {
  115. type: "custom";
  116. customType: string;
  117. lastActiveToolBeforeEraser: LastActiveToolBeforeEraser;
  118. locked: boolean;
  119. };
  120. penMode: boolean;
  121. penDetected: boolean;
  122. exportBackground: boolean;
  123. exportEmbedScene: boolean;
  124. exportWithDarkMode: boolean;
  125. exportScale: number;
  126. currentItemStrokeColor: string;
  127. currentItemBackgroundColor: string;
  128. currentItemFillStyle: ExcalidrawElement["fillStyle"];
  129. currentItemStrokeWidth: number;
  130. currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
  131. currentItemRoughness: number;
  132. currentItemOpacity: number;
  133. currentItemFontFamily: FontFamilyValues;
  134. currentItemFontSize: number;
  135. currentItemTextAlign: TextAlign;
  136. currentItemStartArrowhead: Arrowhead | null;
  137. currentItemEndArrowhead: Arrowhead | null;
  138. currentItemRoundness: StrokeRoundness;
  139. viewBackgroundColor: string;
  140. scrollX: number;
  141. scrollY: number;
  142. cursorButton: "up" | "down";
  143. scrolledOutside: boolean;
  144. name: string;
  145. isResizing: boolean;
  146. isRotating: boolean;
  147. zoom: Zoom;
  148. // mobile-only
  149. openMenu: "canvas" | "shape" | null;
  150. openPopup:
  151. | "canvasColorPicker"
  152. | "backgroundColorPicker"
  153. | "strokeColorPicker"
  154. | null;
  155. openSidebar: "library" | "customSidebar" | null;
  156. openDialog: "imageExport" | "help" | "jsonExport" | null;
  157. isSidebarDocked: boolean;
  158. lastPointerDownWith: PointerType;
  159. selectedElementIds: { [id: string]: boolean };
  160. previousSelectedElementIds: { [id: string]: boolean };
  161. shouldCacheIgnoreZoom: boolean;
  162. toast: { message: string; closable?: boolean; duration?: number } | null;
  163. zenModeEnabled: boolean;
  164. theme: Theme;
  165. gridSize: number | null;
  166. viewModeEnabled: boolean;
  167. /** top-most selected groups (i.e. does not include nested groups) */
  168. selectedGroupIds: { [groupId: string]: boolean };
  169. /** group being edited when you drill down to its constituent element
  170. (e.g. when you double-click on a group's element) */
  171. editingGroupId: GroupId | null;
  172. width: number;
  173. height: number;
  174. offsetTop: number;
  175. offsetLeft: number;
  176. fileHandle: FileSystemHandle | null;
  177. collaborators: Map<string, Collaborator>;
  178. showStats: boolean;
  179. currentChartType: ChartType;
  180. pasteDialog:
  181. | {
  182. shown: false;
  183. data: null;
  184. }
  185. | {
  186. shown: true;
  187. data: Spreadsheet;
  188. };
  189. /** imageElement waiting to be placed on canvas */
  190. pendingImageElementId: ExcalidrawImageElement["id"] | null;
  191. showHyperlinkPopup: false | "info" | "editor";
  192. selectedLinearElement: LinearElementEditor | null;
  193. };
  194. export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
  195. export type Zoom = Readonly<{
  196. value: NormalizedZoomValue;
  197. }>;
  198. export type PointerCoords = Readonly<{
  199. x: number;
  200. y: number;
  201. }>;
  202. export type Gesture = {
  203. pointers: Map<number, PointerCoords>;
  204. lastCenter: { x: number; y: number } | null;
  205. initialDistance: number | null;
  206. initialScale: number | null;
  207. };
  208. export declare class GestureEvent extends UIEvent {
  209. readonly rotation: number;
  210. readonly scale: number;
  211. }
  212. // libraries
  213. // -----------------------------------------------------------------------------
  214. /** @deprecated legacy: do not use outside of migration paths */
  215. export type LibraryItem_v1 = readonly NonDeleted<ExcalidrawElement>[];
  216. /** @deprecated legacy: do not use outside of migration paths */
  217. type LibraryItems_v1 = readonly LibraryItem_v1[];
  218. /** v2 library item */
  219. export type LibraryItem = {
  220. id: string;
  221. status: "published" | "unpublished";
  222. elements: readonly NonDeleted<ExcalidrawElement>[];
  223. /** timestamp in epoch (ms) */
  224. created: number;
  225. name?: string;
  226. error?: string;
  227. };
  228. export type LibraryItems = readonly LibraryItem[];
  229. export type LibraryItems_anyVersion = LibraryItems | LibraryItems_v1;
  230. export type LibraryItemsSource =
  231. | ((
  232. currentLibraryItems: LibraryItems,
  233. ) =>
  234. | Blob
  235. | LibraryItems_anyVersion
  236. | Promise<LibraryItems_anyVersion | Blob>)
  237. | Blob
  238. | LibraryItems_anyVersion
  239. | Promise<LibraryItems_anyVersion | Blob>;
  240. // -----------------------------------------------------------------------------
  241. // NOTE ready/readyPromise props are optional for host apps' sake (our own
  242. // implem guarantees existence)
  243. export type ExcalidrawAPIRefValue =
  244. | ExcalidrawImperativeAPI
  245. | {
  246. readyPromise?: ResolvablePromise<ExcalidrawImperativeAPI>;
  247. ready?: false;
  248. };
  249. export type ExcalidrawInitialDataState = Merge<
  250. ImportedDataState,
  251. {
  252. libraryItems?:
  253. | Required<ImportedDataState>["libraryItems"]
  254. | Promise<Required<ImportedDataState>["libraryItems"]>;
  255. }
  256. >;
  257. export interface ExcalidrawProps {
  258. onChange?: (
  259. elements: readonly ExcalidrawElement[],
  260. appState: AppState,
  261. files: BinaryFiles,
  262. ) => void;
  263. initialData?:
  264. | ExcalidrawInitialDataState
  265. | null
  266. | Promise<ExcalidrawInitialDataState | null>;
  267. excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
  268. onCollabButtonClick?: () => void;
  269. isCollaborating?: boolean;
  270. onPointerUpdate?: (payload: {
  271. pointer: { x: number; y: number };
  272. button: "down" | "up";
  273. pointersMap: Gesture["pointers"];
  274. }) => void;
  275. onPaste?: (
  276. data: ClipboardData,
  277. event: ClipboardEvent | null,
  278. ) => Promise<boolean> | boolean;
  279. renderTopRightUI?: (
  280. isMobile: boolean,
  281. appState: AppState,
  282. ) => JSX.Element | null;
  283. langCode?: Language["code"];
  284. viewModeEnabled?: boolean;
  285. zenModeEnabled?: boolean;
  286. gridModeEnabled?: boolean;
  287. libraryReturnUrl?: string;
  288. theme?: Theme;
  289. name?: string;
  290. renderCustomStats?: (
  291. elements: readonly NonDeletedExcalidrawElement[],
  292. appState: AppState,
  293. ) => JSX.Element;
  294. UIOptions?: Partial<UIOptions>;
  295. detectScroll?: boolean;
  296. handleKeyboardGlobally?: boolean;
  297. onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
  298. autoFocus?: boolean;
  299. generateIdForFile?: (file: File) => string | Promise<string>;
  300. onLinkOpen?: (
  301. element: NonDeletedExcalidrawElement,
  302. event: CustomEvent<{
  303. nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
  304. }>,
  305. ) => void;
  306. onPointerDown?: (
  307. activeTool: AppState["activeTool"],
  308. pointerDownState: PointerDownState,
  309. ) => void;
  310. onScrollChange?: (scrollX: number, scrollY: number) => void;
  311. /**
  312. * Render function that renders custom <Sidebar /> component.
  313. */
  314. renderSidebar?: () => JSX.Element | null;
  315. children?: React.ReactNode;
  316. }
  317. export type SceneData = {
  318. elements?: ImportedDataState["elements"];
  319. appState?: ImportedDataState["appState"];
  320. collaborators?: Map<string, Collaborator>;
  321. commitToHistory?: boolean;
  322. };
  323. export enum UserIdleState {
  324. ACTIVE = "active",
  325. AWAY = "away",
  326. IDLE = "idle",
  327. }
  328. export type ExportOpts = {
  329. saveFileToDisk?: boolean;
  330. onExportToBackend?: (
  331. exportedElements: readonly NonDeletedExcalidrawElement[],
  332. appState: AppState,
  333. files: BinaryFiles,
  334. canvas: HTMLCanvasElement | null,
  335. ) => void;
  336. renderCustomUI?: (
  337. exportedElements: readonly NonDeletedExcalidrawElement[],
  338. appState: AppState,
  339. files: BinaryFiles,
  340. canvas: HTMLCanvasElement | null,
  341. ) => JSX.Element;
  342. };
  343. // NOTE at the moment, if action name coressponds to canvasAction prop, its
  344. // truthiness value will determine whether the action is rendered or not
  345. // (see manager renderAction). We also override canvasAction values in
  346. // excalidraw package index.tsx.
  347. type CanvasActions = Partial<{
  348. changeViewBackgroundColor: boolean;
  349. clearCanvas: boolean;
  350. export: false | ExportOpts;
  351. loadScene: boolean;
  352. saveToActiveFile: boolean;
  353. toggleTheme: boolean | null;
  354. saveAsImage: boolean;
  355. }>;
  356. type UIOptions = Partial<{
  357. dockedSidebarBreakpoint: number;
  358. welcomeScreen: boolean;
  359. canvasActions: CanvasActions;
  360. }>;
  361. export type AppProps = Merge<
  362. ExcalidrawProps,
  363. {
  364. UIOptions: Merge<
  365. MarkRequired<UIOptions, "welcomeScreen">,
  366. {
  367. canvasActions: Required<CanvasActions> & { export: ExportOpts };
  368. }
  369. >;
  370. detectScroll: boolean;
  371. handleKeyboardGlobally: boolean;
  372. isCollaborating: boolean;
  373. children?: React.ReactNode;
  374. }
  375. >;
  376. /** A subset of App class properties that we need to use elsewhere
  377. * in the app, eg Manager. Factored out into a separate type to keep DRY. */
  378. export type AppClassProperties = {
  379. props: AppProps;
  380. canvas: HTMLCanvasElement | null;
  381. focusContainer(): void;
  382. library: Library;
  383. imageCache: Map<
  384. FileId,
  385. {
  386. image: HTMLImageElement | Promise<HTMLImageElement>;
  387. mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number];
  388. }
  389. >;
  390. files: BinaryFiles;
  391. device: App["device"];
  392. scene: App["scene"];
  393. pasteFromClipboard: App["pasteFromClipboard"];
  394. };
  395. export type PointerDownState = Readonly<{
  396. // The first position at which pointerDown happened
  397. origin: Readonly<{ x: number; y: number }>;
  398. // Same as "origin" but snapped to the grid, if grid is on
  399. originInGrid: Readonly<{ x: number; y: number }>;
  400. // Scrollbar checks
  401. scrollbars: ReturnType<typeof isOverScrollBars>;
  402. // The previous pointer position
  403. lastCoords: { x: number; y: number };
  404. // map of original elements data
  405. originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
  406. resize: {
  407. // Handle when resizing, might change during the pointer interaction
  408. handleType: MaybeTransformHandleType;
  409. // This is determined on the initial pointer down event
  410. isResizing: boolean;
  411. // This is determined on the initial pointer down event
  412. offset: { x: number; y: number };
  413. // This is determined on the initial pointer down event
  414. arrowDirection: "origin" | "end";
  415. // This is a center point of selected elements determined on the initial pointer down event (for rotation only)
  416. center: { x: number; y: number };
  417. };
  418. hit: {
  419. // The element the pointer is "hitting", is determined on the initial
  420. // pointer down event
  421. element: NonDeleted<ExcalidrawElement> | null;
  422. // The elements the pointer is "hitting", is determined on the initial
  423. // pointer down event
  424. allHitElements: NonDeleted<ExcalidrawElement>[];
  425. // This is determined on the initial pointer down event
  426. wasAddedToSelection: boolean;
  427. // Whether selected element(s) were duplicated, might change during the
  428. // pointer interaction
  429. hasBeenDuplicated: boolean;
  430. hasHitCommonBoundingBoxOfSelectedElements: boolean;
  431. };
  432. withCmdOrCtrl: boolean;
  433. drag: {
  434. // Might change during the pointer interaction
  435. hasOccurred: boolean;
  436. // Might change during the pointer interaction
  437. offset: { x: number; y: number } | null;
  438. };
  439. // We need to have these in the state so that we can unsubscribe them
  440. eventListeners: {
  441. // It's defined on the initial pointer down event
  442. onMove: null | ReturnType<typeof throttleRAF>;
  443. // It's defined on the initial pointer down event
  444. onUp: null | ((event: PointerEvent) => void);
  445. // It's defined on the initial pointer down event
  446. onKeyDown: null | ((event: KeyboardEvent) => void);
  447. // It's defined on the initial pointer down event
  448. onKeyUp: null | ((event: KeyboardEvent) => void);
  449. };
  450. boxSelection: {
  451. hasOccurred: boolean;
  452. };
  453. elementIdsToErase: {
  454. [key: ExcalidrawElement["id"]]: {
  455. opacity: ExcalidrawElement["opacity"];
  456. erase: boolean;
  457. };
  458. };
  459. }>;
  460. export type ExcalidrawImperativeAPI = {
  461. updateScene: InstanceType<typeof App>["updateScene"];
  462. updateLibrary: InstanceType<typeof Library>["updateLibrary"];
  463. resetScene: InstanceType<typeof App>["resetScene"];
  464. getSceneElementsIncludingDeleted: InstanceType<
  465. typeof App
  466. >["getSceneElementsIncludingDeleted"];
  467. history: {
  468. clear: InstanceType<typeof App>["resetHistory"];
  469. };
  470. scrollToContent: InstanceType<typeof App>["scrollToContent"];
  471. getSceneElements: InstanceType<typeof App>["getSceneElements"];
  472. getAppState: () => InstanceType<typeof App>["state"];
  473. getFiles: () => InstanceType<typeof App>["files"];
  474. refresh: InstanceType<typeof App>["refresh"];
  475. setToast: InstanceType<typeof App>["setToast"];
  476. addFiles: (data: BinaryFileData[]) => void;
  477. readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
  478. ready: true;
  479. id: string;
  480. setActiveTool: InstanceType<typeof App>["setActiveTool"];
  481. setCursor: InstanceType<typeof App>["setCursor"];
  482. resetCursor: InstanceType<typeof App>["resetCursor"];
  483. toggleMenu: InstanceType<typeof App>["toggleMenu"];
  484. };
  485. export type Device = Readonly<{
  486. isSmScreen: boolean;
  487. isMobile: boolean;
  488. isTouchScreen: boolean;
  489. canDeviceFitSidebar: boolean;
  490. }>;
  491. export type UIChildrenComponents = {
  492. [k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement<
  493. { children?: React.ReactNode },
  494. React.JSXElementConstructor<any>
  495. >;
  496. };
  497. export type UIWelcomeScreenComponents = {
  498. [k in
  499. | "Center"
  500. | "MenuHint"
  501. | "ToolbarHint"
  502. | "HelpHint"]?: React.ReactElement<
  503. { children?: React.ReactNode },
  504. React.JSXElementConstructor<any>
  505. >;
  506. };
  507. export type UIWelcomeScreenCenterComponents = {
  508. [k in
  509. | "Logo"
  510. | "Heading"
  511. | "Menu"
  512. | "MenuItemLoadScene"
  513. | "MenuItemHelp"]?: React.ReactElement<
  514. { children?: React.ReactNode },
  515. React.JSXElementConstructor<any>
  516. >;
  517. };