types.ts 14 KB

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