import { ExcalidrawElement, ExcalidrawGenericElement, ExcalidrawTextElement, ExcalidrawLinearElement, ExcalidrawFreeDrawElement, ExcalidrawImageElement, FileId, } from "../../element/types"; import { newElement, newTextElement, newLinearElement } from "../../element"; import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants"; import { getDefaultAppState } from "../../appState"; import { GlobalTestState, createEvent, fireEvent } from "../test-utils"; import fs from "fs"; import util from "util"; import path from "path"; import { getMimeType } from "../../data/blob"; import { newFreeDrawElement, newImageElement } from "../../element/newElement"; import { Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; import { isLinearElementType } from "../../element/typeChecks"; const readFile = util.promisify(fs.readFile); const { h } = window; export class API { static setSelectedElements = (elements: ExcalidrawElement[]) => { h.setState({ selectedElementIds: elements.reduce((acc, element) => { acc[element.id] = true; return acc; }, {} as Record), }); }; static getSelectedElements = ( includeBoundTextElement: boolean = false, ): ExcalidrawElement[] => { return getSelectedElements(h.elements, h.state, includeBoundTextElement); }; static getSelectedElement = (): ExcalidrawElement => { const selectedElements = API.getSelectedElements(); if (selectedElements.length !== 1) { throw new Error( `expected 1 selected element; got ${selectedElements.length}`, ); } return selectedElements[0]; }; static getStateHistory = () => { // @ts-ignore return h.history.stateHistory; }; static clearSelection = () => { // @ts-ignore h.app.clearSelection(null); expect(API.getSelectedElements().length).toBe(0); }; static createElement = < T extends Exclude = "rectangle", >({ // @ts-ignore type = "rectangle", id, x = 0, y = x, width = 100, height = width, isDeleted = false, groupIds = [], ...rest }: { type?: T; x?: number; y?: number; height?: number; width?: number; angle?: number; id?: string; isDeleted?: boolean; groupIds?: string[]; // generic element props strokeColor?: ExcalidrawGenericElement["strokeColor"]; backgroundColor?: ExcalidrawGenericElement["backgroundColor"]; fillStyle?: ExcalidrawGenericElement["fillStyle"]; strokeWidth?: ExcalidrawGenericElement["strokeWidth"]; strokeStyle?: ExcalidrawGenericElement["strokeStyle"]; roundness?: ExcalidrawGenericElement["roundness"]; roughness?: ExcalidrawGenericElement["roughness"]; opacity?: ExcalidrawGenericElement["opacity"]; // text props text?: T extends "text" ? ExcalidrawTextElement["text"] : never; fontSize?: T extends "text" ? ExcalidrawTextElement["fontSize"] : never; fontFamily?: T extends "text" ? ExcalidrawTextElement["fontFamily"] : never; textAlign?: T extends "text" ? ExcalidrawTextElement["textAlign"] : never; verticalAlign?: T extends "text" ? ExcalidrawTextElement["verticalAlign"] : never; boundElements?: ExcalidrawGenericElement["boundElements"]; containerId?: T extends "text" ? ExcalidrawTextElement["containerId"] : never; points?: T extends "arrow" | "line" ? readonly Point[] : never; locked?: boolean; fileId?: T extends "image" ? string : never; scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never; status?: T extends "image" ? ExcalidrawImageElement["status"] : never; endBinding?: T extends "arrow" ? ExcalidrawLinearElement["endBinding"] : never; }): T extends "arrow" | "line" ? ExcalidrawLinearElement : T extends "freedraw" ? ExcalidrawFreeDrawElement : T extends "text" ? ExcalidrawTextElement : T extends "image" ? ExcalidrawImageElement : ExcalidrawGenericElement => { let element: Mutable = null!; const appState = h?.state || getDefaultAppState(); const base: Omit< ExcalidrawGenericElement, | "id" | "width" | "height" | "type" | "seed" | "version" | "versionNonce" | "isDeleted" | "groupIds" | "link" | "updated" > = { x, y, angle: rest.angle ?? 0, strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor, backgroundColor: rest.backgroundColor ?? appState.currentItemBackgroundColor, fillStyle: rest.fillStyle ?? appState.currentItemFillStyle, strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth, strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle, roundness: ( rest.roundness === undefined ? appState.currentItemRoundness === "round" : rest.roundness ) ? { type: isLinearElementType(type) ? ROUNDNESS.PROPORTIONAL_RADIUS : ROUNDNESS.ADAPTIVE_RADIUS, } : null, roughness: rest.roughness ?? appState.currentItemRoughness, opacity: rest.opacity ?? appState.currentItemOpacity, boundElements: rest.boundElements ?? null, locked: rest.locked ?? false, }; switch (type) { case "rectangle": case "diamond": case "ellipse": element = newElement({ type: type as "rectangle" | "diamond" | "ellipse", width, height, ...base, }); break; case "text": element = newTextElement({ ...base, text: rest.text || "test", fontSize: rest.fontSize ?? appState.currentItemFontSize, fontFamily: rest.fontFamily ?? appState.currentItemFontFamily, textAlign: rest.textAlign ?? appState.currentItemTextAlign, verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN, containerId: rest.containerId ?? undefined, }); element.width = width; element.height = height; break; case "freedraw": element = newFreeDrawElement({ type: type as "freedraw", simulatePressure: true, ...base, }); break; case "arrow": case "line": element = newLinearElement({ ...base, width, height, type, startArrowhead: null, endArrowhead: null, points: rest.points ?? [], }); break; case "image": element = newImageElement({ ...base, width, height, type, fileId: (rest.fileId as string as FileId) ?? null, status: rest.status || "saved", scale: rest.scale || [1, 1], }); break; } if (id) { element.id = id; } if (isDeleted) { element.isDeleted = isDeleted; } if (groupIds) { element.groupIds = groupIds; } return element as any; }; static readFile = async ( filepath: string, encoding?: T, ): Promise => { filepath = path.isAbsolute(filepath) ? filepath : path.resolve(path.join(__dirname, "../", filepath)); return readFile(filepath, { encoding }) as any; }; static loadFile = async (filepath: string) => { const { base, ext } = path.parse(filepath); return new File([await API.readFile(filepath, null)], base, { type: getMimeType(ext), }); }; static drop = async (blob: Blob) => { const fileDropEvent = createEvent.drop(GlobalTestState.canvas); const text = await new Promise((resolve, reject) => { try { const reader = new FileReader(); reader.onload = () => { resolve(reader.result as string); }; reader.readAsText(blob); } catch (error: any) { reject(error); } }); const files = [blob] as File[] & { item: (index: number) => File }; files.item = (index: number) => files[index]; Object.defineProperty(fileDropEvent, "dataTransfer", { value: { files, getData: (type: string) => { if (type === blob.type) { return text; } return ""; }, }, }); fireEvent(GlobalTestState.canvas, fileDropEvent); }; }