760fd7b3a6
* 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
260 lines
7.8 KiB
TypeScript
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);
|
|
};
|
|
}
|