api.ts 9.1 KB

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