types.ts 16 KB

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