types.ts 16 KB

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