api.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import {
  2. ExcalidrawElement,
  3. ExcalidrawGenericElement,
  4. ExcalidrawTextElement,
  5. ExcalidrawLinearElement,
  6. ExcalidrawFreeDrawElement,
  7. ExcalidrawImageElement,
  8. FileId,
  9. } from "../../element/types";
  10. import { newElement, newTextElement, newLinearElement } from "../../element";
  11. import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
  12. import { getDefaultAppState } from "../../appState";
  13. import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
  14. import fs from "fs";
  15. import util from "util";
  16. import path from "path";
  17. import { getMimeType } from "../../data/blob";
  18. import { newFreeDrawElement, newImageElement } from "../../element/newElement";
  19. import { Point } from "../../types";
  20. import { getSelectedElements } from "../../scene/selection";
  21. import { isLinearElementType } from "../../element/typeChecks";
  22. const readFile = util.promisify(fs.readFile);
  23. const { h } = window;
  24. export class API {
  25. static setSelectedElements = (elements: ExcalidrawElement[]) => {
  26. h.setState({
  27. selectedElementIds: elements.reduce((acc, element) => {
  28. acc[element.id] = true;
  29. return acc;
  30. }, {} as Record<ExcalidrawElement["id"], true>),
  31. });
  32. };
  33. static getSelectedElements = (
  34. includeBoundTextElement: boolean = false,
  35. ): ExcalidrawElement[] => {
  36. return getSelectedElements(h.elements, h.state, includeBoundTextElement);
  37. };
  38. static getSelectedElement = (): ExcalidrawElement => {
  39. const selectedElements = API.getSelectedElements();
  40. if (selectedElements.length !== 1) {
  41. throw new Error(
  42. `expected 1 selected element; got ${selectedElements.length}`,
  43. );
  44. }
  45. return selectedElements[0];
  46. };
  47. static getStateHistory = () => {
  48. // @ts-ignore
  49. return h.history.stateHistory;
  50. };
  51. static clearSelection = () => {
  52. // @ts-ignore
  53. h.app.clearSelection(null);
  54. expect(API.getSelectedElements().length).toBe(0);
  55. };
  56. static createElement = <
  57. T extends Exclude<ExcalidrawElement["type"], "selection"> = "rectangle",
  58. >({
  59. // @ts-ignore
  60. type = "rectangle",
  61. id,
  62. x = 0,
  63. y = x,
  64. width = 100,
  65. height = width,
  66. isDeleted = false,
  67. groupIds = [],
  68. ...rest
  69. }: {
  70. type?: T;
  71. x?: number;
  72. y?: number;
  73. height?: number;
  74. width?: number;
  75. angle?: number;
  76. id?: string;
  77. isDeleted?: boolean;
  78. groupIds?: string[];
  79. // generic element props
  80. strokeColor?: ExcalidrawGenericElement["strokeColor"];
  81. backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
  82. fillStyle?: ExcalidrawGenericElement["fillStyle"];
  83. strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
  84. strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
  85. roundness?: ExcalidrawGenericElement["roundness"];
  86. roughness?: ExcalidrawGenericElement["roughness"];
  87. opacity?: ExcalidrawGenericElement["opacity"];
  88. // text props
  89. text?: T extends "text" ? ExcalidrawTextElement["text"] : never;
  90. fontSize?: T extends "text" ? ExcalidrawTextElement["fontSize"] : never;
  91. fontFamily?: T extends "text" ? ExcalidrawTextElement["fontFamily"] : never;
  92. textAlign?: T extends "text" ? ExcalidrawTextElement["textAlign"] : never;
  93. verticalAlign?: T extends "text"
  94. ? ExcalidrawTextElement["verticalAlign"]
  95. : never;
  96. boundElements?: ExcalidrawGenericElement["boundElements"];
  97. containerId?: T extends "text"
  98. ? ExcalidrawTextElement["containerId"]
  99. : never;
  100. points?: T extends "arrow" | "line" ? readonly Point[] : never;
  101. locked?: boolean;
  102. fileId?: T extends "image" ? string : never;
  103. scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
  104. status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
  105. endBinding?: T extends "arrow"
  106. ? ExcalidrawLinearElement["endBinding"]
  107. : never;
  108. }): T extends "arrow" | "line"
  109. ? ExcalidrawLinearElement
  110. : T extends "freedraw"
  111. ? ExcalidrawFreeDrawElement
  112. : T extends "text"
  113. ? ExcalidrawTextElement
  114. : T extends "image"
  115. ? ExcalidrawImageElement
  116. : ExcalidrawGenericElement => {
  117. let element: Mutable<ExcalidrawElement> = null!;
  118. const appState = h?.state || getDefaultAppState();
  119. const base: Omit<
  120. ExcalidrawGenericElement,
  121. | "id"
  122. | "width"
  123. | "height"
  124. | "type"
  125. | "seed"
  126. | "version"
  127. | "versionNonce"
  128. | "isDeleted"
  129. | "groupIds"
  130. | "link"
  131. | "updated"
  132. > = {
  133. x,
  134. y,
  135. angle: rest.angle ?? 0,
  136. strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
  137. backgroundColor:
  138. rest.backgroundColor ?? appState.currentItemBackgroundColor,
  139. fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
  140. strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
  141. strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
  142. roundness: (
  143. rest.roundness === undefined
  144. ? appState.currentItemRoundness === "round"
  145. : rest.roundness
  146. )
  147. ? {
  148. type: isLinearElementType(type)
  149. ? ROUNDNESS.PROPORTIONAL_RADIUS
  150. : ROUNDNESS.ADAPTIVE_RADIUS,
  151. }
  152. : null,
  153. roughness: rest.roughness ?? appState.currentItemRoughness,
  154. opacity: rest.opacity ?? appState.currentItemOpacity,
  155. boundElements: rest.boundElements ?? null,
  156. locked: rest.locked ?? false,
  157. };
  158. switch (type) {
  159. case "rectangle":
  160. case "diamond":
  161. case "ellipse":
  162. element = newElement({
  163. type: type as "rectangle" | "diamond" | "ellipse",
  164. width,
  165. height,
  166. ...base,
  167. });
  168. break;
  169. case "text":
  170. element = newTextElement({
  171. ...base,
  172. text: rest.text || "test",
  173. fontSize: rest.fontSize ?? appState.currentItemFontSize,
  174. fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
  175. textAlign: rest.textAlign ?? appState.currentItemTextAlign,
  176. verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
  177. containerId: rest.containerId ?? undefined,
  178. });
  179. element.width = width;
  180. element.height = height;
  181. break;
  182. case "freedraw":
  183. element = newFreeDrawElement({
  184. type: type as "freedraw",
  185. simulatePressure: true,
  186. ...base,
  187. });
  188. break;
  189. case "arrow":
  190. case "line":
  191. element = newLinearElement({
  192. ...base,
  193. width,
  194. height,
  195. type,
  196. startArrowhead: null,
  197. endArrowhead: null,
  198. points: rest.points ?? [],
  199. });
  200. break;
  201. case "image":
  202. element = newImageElement({
  203. ...base,
  204. width,
  205. height,
  206. type,
  207. fileId: (rest.fileId as string as FileId) ?? null,
  208. status: rest.status || "saved",
  209. scale: rest.scale || [1, 1],
  210. });
  211. break;
  212. }
  213. if (id) {
  214. element.id = id;
  215. }
  216. if (isDeleted) {
  217. element.isDeleted = isDeleted;
  218. }
  219. if (groupIds) {
  220. element.groupIds = groupIds;
  221. }
  222. return element as any;
  223. };
  224. static readFile = async <T extends "utf8" | null>(
  225. filepath: string,
  226. encoding?: T,
  227. ): Promise<T extends "utf8" ? string : Buffer> => {
  228. filepath = path.isAbsolute(filepath)
  229. ? filepath
  230. : path.resolve(path.join(__dirname, "../", filepath));
  231. return readFile(filepath, { encoding }) as any;
  232. };
  233. static loadFile = async (filepath: string) => {
  234. const { base, ext } = path.parse(filepath);
  235. return new File([await API.readFile(filepath, null)], base, {
  236. type: getMimeType(ext),
  237. });
  238. };
  239. static drop = async (blob: Blob) => {
  240. const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
  241. const text = await new Promise<string>((resolve, reject) => {
  242. try {
  243. const reader = new FileReader();
  244. reader.onload = () => {
  245. resolve(reader.result as string);
  246. };
  247. reader.readAsText(blob);
  248. } catch (error: any) {
  249. reject(error);
  250. }
  251. });
  252. const files = [blob] as File[] & { item: (index: number) => File };
  253. files.item = (index: number) => files[index];
  254. Object.defineProperty(fileDropEvent, "dataTransfer", {
  255. value: {
  256. files,
  257. getData: (type: string) => {
  258. if (type === blob.type) {
  259. return text;
  260. }
  261. return "";
  262. },
  263. },
  264. });
  265. fireEvent(GlobalTestState.canvas, fileDropEvent);
  266. };
  267. }