feat: support frames via programmatic API (#7205)
* update frame id post generation * support frames via programmatic API * fix types * add test for frames * throw error when element doesn't exist * naming tweaks * update the api to use children * consider max of frame dimensions and calculated bounds of elements * consider bound elements in frame api
This commit is contained in:
parent
9b8de8a12e
commit
f5c91c3a0f
File diff suppressed because it is too large
Load Diff
@ -309,6 +309,90 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Test Frames", () => {
|
||||||
|
it("should transform frames and update frame ids when regenerated", () => {
|
||||||
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elementsSkeleton,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchObject({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should consider max of calculated and frame dimensions when provided", () => {
|
||||||
|
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 120,
|
||||||
|
y: 20,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "HELLO EXCALIDRAW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 30,
|
||||||
|
},
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
width: 800,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elementsSkeleton,
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
const frame = excaldrawElements.find((ele) => ele.type === "frame")!;
|
||||||
|
expect(frame.width).toBe(800);
|
||||||
|
expect(frame.height).toBe(126);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Test arrow bindings", () => {
|
describe("Test arrow bindings", () => {
|
||||||
it("should bind arrows to shapes when start / end provided without ids", () => {
|
it("should bind arrows to shapes when start / end provided without ids", () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
|
getCommonBounds,
|
||||||
newElement,
|
newElement,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
import { bindLinearElement } from "../element/binding";
|
import { bindLinearElement } from "../element/binding";
|
||||||
import {
|
import {
|
||||||
ElementConstructorOpts,
|
ElementConstructorOpts,
|
||||||
|
newFrameElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "../element/newElement";
|
} from "../element/newElement";
|
||||||
@ -135,9 +137,7 @@ export type ValidContainer =
|
|||||||
export type ExcalidrawElementSkeleton =
|
export type ExcalidrawElementSkeleton =
|
||||||
| Extract<
|
| Extract<
|
||||||
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
| ExcalidrawEmbeddableElement
|
ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawFreeDrawElement
|
|
||||||
| ExcalidrawFrameElement
|
|
||||||
>
|
>
|
||||||
| ({
|
| ({
|
||||||
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
||||||
@ -158,7 +158,12 @@ export type ExcalidrawElementSkeleton =
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
fileId: FileId;
|
fileId: FileId;
|
||||||
} & Partial<ExcalidrawImageElement>);
|
} & Partial<ExcalidrawImageElement>)
|
||||||
|
| ({
|
||||||
|
type: "frame";
|
||||||
|
children: readonly ExcalidrawElement["id"][];
|
||||||
|
name?: string;
|
||||||
|
} & Partial<ExcalidrawFrameElement>);
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
width: 100,
|
width: 100,
|
||||||
@ -437,7 +442,6 @@ export const convertToExcalidrawElements = (
|
|||||||
const elements: ExcalidrawElementSkeleton[] = JSON.parse(
|
const elements: ExcalidrawElementSkeleton[] = JSON.parse(
|
||||||
JSON.stringify(elementsSkeleton),
|
JSON.stringify(elementsSkeleton),
|
||||||
);
|
);
|
||||||
|
|
||||||
const elementStore = new ElementStore();
|
const elementStore = new ElementStore();
|
||||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||||
const oldToNewElementIdMap = new Map<string, string>();
|
const oldToNewElementIdMap = new Map<string, string>();
|
||||||
@ -536,8 +540,15 @@ export const convertToExcalidrawElements = (
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "frame": {
|
||||||
|
excalidrawElement = newFrameElement({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "freedraw":
|
case "freedraw":
|
||||||
case "frame":
|
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
excalidrawElement = element;
|
excalidrawElement = element;
|
||||||
break;
|
break;
|
||||||
@ -641,5 +652,60 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Once all the excalidraw elements are created, we can add frames since we
|
||||||
|
// need to calculate coordinates and dimensions of frame which is possibe after all
|
||||||
|
// frame children are processed.
|
||||||
|
for (const [id, element] of elementsWithIds) {
|
||||||
|
if (element.type !== "frame") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const frame = elementStore.getElement(id);
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
|
||||||
|
}
|
||||||
|
const childrenElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
element.children.forEach((id) => {
|
||||||
|
const newElementId = oldToNewElementIdMap.get(id);
|
||||||
|
if (!newElementId) {
|
||||||
|
throw new Error(`Element with ${id} wasn't mapped correctly`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementInFrame = elementStore.getElement(newElementId);
|
||||||
|
if (!elementInFrame) {
|
||||||
|
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
|
||||||
|
}
|
||||||
|
Object.assign(elementInFrame, { frameId: frame.id });
|
||||||
|
|
||||||
|
elementInFrame?.boundElements?.forEach((boundElement) => {
|
||||||
|
const ele = elementStore.getElement(boundElement.id);
|
||||||
|
if (!ele) {
|
||||||
|
throw new Error(
|
||||||
|
`Bound element with id ${boundElement.id} doesn't exist`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.assign(ele, { frameId: frame.id });
|
||||||
|
childrenElements.push(ele);
|
||||||
|
});
|
||||||
|
|
||||||
|
childrenElements.push(elementInFrame);
|
||||||
|
});
|
||||||
|
|
||||||
|
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
|
||||||
|
|
||||||
|
const PADDING = 10;
|
||||||
|
minX = minX - PADDING;
|
||||||
|
minY = minY - PADDING;
|
||||||
|
maxX = maxX + PADDING;
|
||||||
|
maxY = maxY + PADDING;
|
||||||
|
|
||||||
|
// Take the max of calculated and provided frame dimensions, whichever is higher
|
||||||
|
const width = Math.max(frame?.width, maxX - minX);
|
||||||
|
const height = Math.max(frame?.height, maxY - minY);
|
||||||
|
Object.assign(frame, { x: minX, y: minY, width, height });
|
||||||
|
}
|
||||||
|
|
||||||
return elementStore.getElements();
|
return elementStore.getElements();
|
||||||
};
|
};
|
||||||
|
@ -144,13 +144,15 @@ export const newEmbeddableElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const newFrameElement = (
|
export const newFrameElement = (
|
||||||
opts: ElementConstructorOpts,
|
opts: {
|
||||||
|
name?: string;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFrameElement> => {
|
): NonDeleted<ExcalidrawFrameElement> => {
|
||||||
const frameElement = newElementWith(
|
const frameElement = newElementWith(
|
||||||
{
|
{
|
||||||
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
||||||
type: "frame",
|
type: "frame",
|
||||||
name: null,
|
name: opts?.name || null,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
x: 10,
|
x: 10,
|
||||||
y: 10,
|
y: 10,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
id: "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "diamond",
|
type: "diamond",
|
||||||
@ -19,6 +20,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
strokeColor: "#099268",
|
strokeColor: "#099268",
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
},
|
},
|
||||||
|
id: "2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
@ -36,6 +38,11 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||||||
height: 230,
|
height: 230,
|
||||||
fileId: "rocket" as FileId,
|
fileId: "rocket" as FileId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "frame",
|
||||||
|
children: ["1", "2"],
|
||||||
|
name: "My frame",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
export default {
|
export default {
|
||||||
elements,
|
elements,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user