api.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import {
  2. ExcalidrawElement,
  3. ExcalidrawGenericElement,
  4. ExcalidrawTextElement,
  5. ExcalidrawLinearElement,
  6. ExcalidrawFreeDrawElement,
  7. } from "../../element/types";
  8. import { newElement, newTextElement, newLinearElement } from "../../element";
  9. import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
  10. import { getDefaultAppState } from "../../appState";
  11. import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
  12. import fs from "fs";
  13. import util from "util";
  14. import path from "path";
  15. import { getMimeType } from "../../data/blob";
  16. import { newFreeDrawElement } from "../../element/newElement";
  17. const readFile = util.promisify(fs.readFile);
  18. const { h } = window;
  19. export class API {
  20. static setSelectedElements = (elements: ExcalidrawElement[]) => {
  21. h.setState({
  22. selectedElementIds: elements.reduce((acc, element) => {
  23. acc[element.id] = true;
  24. return acc;
  25. }, {} as Record<ExcalidrawElement["id"], true>),
  26. });
  27. };
  28. static getSelectedElements = (): ExcalidrawElement[] => {
  29. return h.elements.filter(
  30. (element) => h.state.selectedElementIds[element.id],
  31. );
  32. };
  33. static getSelectedElement = (): ExcalidrawElement => {
  34. const selectedElements = API.getSelectedElements();
  35. if (selectedElements.length !== 1) {
  36. throw new Error(
  37. `expected 1 selected element; got ${selectedElements.length}`,
  38. );
  39. }
  40. return selectedElements[0];
  41. };
  42. static getStateHistory = () => {
  43. // @ts-ignore
  44. return h.history.stateHistory;
  45. };
  46. static clearSelection = () => {
  47. // @ts-ignore
  48. h.app.clearSelection(null);
  49. expect(API.getSelectedElements().length).toBe(0);
  50. };
  51. static createElement = <
  52. T extends Exclude<ExcalidrawElement["type"], "selection">,
  53. >({
  54. type,
  55. id,
  56. x = 0,
  57. y = x,
  58. width = 100,
  59. height = width,
  60. isDeleted = false,
  61. groupIds = [],
  62. ...rest
  63. }: {
  64. type: T;
  65. x?: number;
  66. y?: number;
  67. height?: number;
  68. width?: number;
  69. id?: string;
  70. isDeleted?: boolean;
  71. groupIds?: string[];
  72. // generic element props
  73. strokeColor?: ExcalidrawGenericElement["strokeColor"];
  74. backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
  75. fillStyle?: ExcalidrawGenericElement["fillStyle"];
  76. strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
  77. strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
  78. strokeSharpness?: ExcalidrawGenericElement["strokeSharpness"];
  79. roughness?: ExcalidrawGenericElement["roughness"];
  80. opacity?: ExcalidrawGenericElement["opacity"];
  81. // text props
  82. text?: T extends "text" ? ExcalidrawTextElement["text"] : never;
  83. fontSize?: T extends "text" ? ExcalidrawTextElement["fontSize"] : never;
  84. fontFamily?: T extends "text" ? ExcalidrawTextElement["fontFamily"] : never;
  85. textAlign?: T extends "text" ? ExcalidrawTextElement["textAlign"] : never;
  86. verticalAlign?: T extends "text"
  87. ? ExcalidrawTextElement["verticalAlign"]
  88. : never;
  89. }): T extends "arrow" | "line"
  90. ? ExcalidrawLinearElement
  91. : T extends "freedraw"
  92. ? ExcalidrawFreeDrawElement
  93. : T extends "text"
  94. ? ExcalidrawTextElement
  95. : ExcalidrawGenericElement => {
  96. let element: Mutable<ExcalidrawElement> = null!;
  97. const appState = h?.state || getDefaultAppState();
  98. const base = {
  99. x,
  100. y,
  101. strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
  102. backgroundColor:
  103. rest.backgroundColor ?? appState.currentItemBackgroundColor,
  104. fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
  105. strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
  106. strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
  107. strokeSharpness:
  108. rest.strokeSharpness ?? appState.currentItemStrokeSharpness,
  109. roughness: rest.roughness ?? appState.currentItemRoughness,
  110. opacity: rest.opacity ?? appState.currentItemOpacity,
  111. };
  112. switch (type) {
  113. case "rectangle":
  114. case "diamond":
  115. case "ellipse":
  116. element = newElement({
  117. type: type as "rectangle" | "diamond" | "ellipse",
  118. width,
  119. height,
  120. ...base,
  121. });
  122. break;
  123. case "text":
  124. element = newTextElement({
  125. ...base,
  126. text: rest.text || "test",
  127. fontSize: rest.fontSize ?? appState.currentItemFontSize,
  128. fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
  129. textAlign: rest.textAlign ?? appState.currentItemTextAlign,
  130. verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
  131. });
  132. element.width = width;
  133. element.height = height;
  134. break;
  135. case "freedraw":
  136. element = newFreeDrawElement({
  137. type: type as "freedraw",
  138. simulatePressure: true,
  139. ...base,
  140. });
  141. break;
  142. case "arrow":
  143. case "line":
  144. element = newLinearElement({
  145. type: type as "arrow" | "line",
  146. startArrowhead: null,
  147. endArrowhead: null,
  148. ...base,
  149. });
  150. break;
  151. }
  152. if (id) {
  153. element.id = id;
  154. }
  155. if (isDeleted) {
  156. element.isDeleted = isDeleted;
  157. }
  158. if (groupIds) {
  159. element.groupIds = groupIds;
  160. }
  161. return element as any;
  162. };
  163. static readFile = async <T extends "utf8" | null>(
  164. filepath: string,
  165. encoding?: T,
  166. ): Promise<T extends "utf8" ? string : Buffer> => {
  167. filepath = path.isAbsolute(filepath)
  168. ? filepath
  169. : path.resolve(path.join(__dirname, "../", filepath));
  170. return readFile(filepath, { encoding }) as any;
  171. };
  172. static loadFile = async (filepath: string) => {
  173. const { base, ext } = path.parse(filepath);
  174. return new File([await API.readFile(filepath, null)], base, {
  175. type: getMimeType(ext),
  176. });
  177. };
  178. static drop = async (blob: Blob) => {
  179. const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
  180. const text = await new Promise<string>((resolve, reject) => {
  181. try {
  182. const reader = new FileReader();
  183. reader.onload = () => {
  184. resolve(reader.result as string);
  185. };
  186. reader.readAsText(blob);
  187. } catch (error) {
  188. reject(error);
  189. }
  190. });
  191. Object.defineProperty(fileDropEvent, "dataTransfer", {
  192. value: {
  193. files: [blob],
  194. getData: (type: string) => {
  195. if (type === blob.type) {
  196. return text;
  197. }
  198. return "";
  199. },
  200. },
  201. });
  202. fireEvent(GlobalTestState.canvas, fileDropEvent);
  203. };
  204. }