api.ts 7.8 KB

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