types.ts 14 KB

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