Files
excalidraw/src/tests/helpers/api.ts
T
Aakansha Doshi 760fd7b3a6 feat: Support labels for arrow 🔥 (#5723)
* feat: support arrow with text

* render arrow -> clear rect-> render text

* move bound text when linear elements move

* fix centering cursor when linear element rotated

* fix y coord when new line added and container has 3 points

* update text position when 2nd point moved

* support adding label on top of 2nd point when 3 points are present

* change linear element editor shortcut to cmd+enter and fix tests

* scale bound text points when resizing via bounding box

* ohh yeah rotation works :)

* fix coords when updating text properties

* calculate new position after rotation always from original position

* rotate the bound text by same angle as parent

* don't rotate text and make sure dimensions and coords are always calculated from original point

* hardcoding the text width for now

* Move the linear element when bound text hit

* Rotation working yaay

* consider text element angle when editing

* refactor

* update x2 coords if needed when text updated

* simplify

* consider bound text to be part of bounding box when hit

* show bounding box correctly when multiple element selected

* fix typo

* support rotating multiple elements

* support multiple element resizing

* shift bound text to mid point when odd points

* Always render linear element handles inside editor after element rendered so point is visible for bound text

* Delete bound text when point attached to it deleted

* move bound to mid segement mid point when points are even

* shift bound text when points nearby deleted and handle segment deletion

* Resize working :)

* more resize fixes

* don't update cache-its breaking delete points, look for better soln

* update mid point cache for bound elements when updated

* introduce wrapping when resizing

* wrap when resize for 2 pointer linear elements

* support adding text for linear elements with more than 3 points

* export to svg  working :)

* clip from nearest enclosing element with non transparent color if present when exporting and fill with correct color in canvas

* fix snap

* use visible elements

* Make export to svg work with Mask :)

* remove id

* mask canvas linear element area where label is added

* decide the position of bound text during render

* fix coords when editing

* fix multiple resize

* update cache when bound text version changes

* fix masking when rotated

* render text in correct position in preview

* remove unnecessary code

* fix masking when rotating linear element

* fix masking with zoom

* fix mask in preview for export

* fix offsets in export view

* fix coords on svg export

* fix mask when element rotated in svg

* enable double-click to enter text

* fix hint

* Position cursor correctly and text dimensiosn when height of element is negative

* don't allow 2 pointer linear element with bound text width to go beyond min width

* code cleanup

* fix freedraw

* Add padding

* don't show vertical align action for linear element containers

* Add specs for getBoundTextElementPosition

* more specs

* move some utils to linearElementEditor.ts

* remove only :p

* check absoulte coods in test

* Add test to hide vertical align for linear eleemnt with bound text

* improve export preview

* support labels only for arrows

* spec

* fix large texts

* fix tests

* fix zooming

* enter line editor with cmd+double click

* Allow points to move beyond min width/height for 2 pointer arrow with bound text

* fix hint for line editing

* attempt to fix arrow getting deselected

* fix hint and shortcut

* Add padding of 5px when creating bound text and add spec

* Wrap bound text when arrow binding containers moved

* Add spec

* remove

* set boundTextElementVersion to null if not present

* dont use cache when version mismatch

* Add a padding of 5px vertically when creating text

* Add box sizing content box

* Set bound elements when text element created to fix the padding

* fix zooming in editor

* fix zoom in export

* remove globalCompositeOperation and use clearRect instead of fillRect
2022-12-05 21:03:13 +05:30

260 lines
7.8 KiB
TypeScript

import {
ExcalidrawElement,
ExcalidrawGenericElement,
ExcalidrawTextElement,
ExcalidrawLinearElement,
ExcalidrawFreeDrawElement,
ExcalidrawImageElement,
FileId,
} from "../../element/types";
import { newElement, newTextElement, newLinearElement } from "../../element";
import { DEFAULT_VERTICAL_ALIGN } 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";
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<ExcalidrawElement["id"], true>),
});
};
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<ExcalidrawElement["type"], "selection"> = "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"];
strokeSharpness?: ExcalidrawGenericElement["strokeSharpness"];
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<ExcalidrawElement> = null!;
const appState = h?.state || getDefaultAppState();
const base = {
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,
strokeSharpness:
rest.strokeSharpness ?? appState.currentItemStrokeSharpness,
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 <T extends "utf8" | null>(
filepath: string,
encoding?: T,
): Promise<T extends "utf8" ? string : Buffer> => {
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<string>((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);
};
}