types.ts 15 KB

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