make convertToExcalidrawElements more typesafe

This commit is contained in:
dwelle 2023-08-04 18:43:29 +02:00
parent 0404a3ecf1
commit 07c21446e0
2 changed files with 170 additions and 130 deletions

View file

@ -24,6 +24,7 @@ import {
import { import {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawEmbeddableElement,
ExcalidrawFrameElement, ExcalidrawFrameElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
ExcalidrawGenericElement, ExcalidrawGenericElement,
@ -37,7 +38,7 @@ import {
VerticalAlign, VerticalAlign,
} from "../element/types"; } from "../element/types";
import { MarkOptional } from "../utility-types"; import { MarkOptional } from "../utility-types";
import { getFontString } from "../utils"; import { assertNever, getFontString } from "../utils";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
@ -56,7 +57,7 @@ export type ValidLinearElement = {
| { | {
type: Exclude< type: Exclude<
ExcalidrawBindableElement["type"], ExcalidrawBindableElement["type"],
"image" | "selection" | "text" | "frame" "image" | "text" | "frame" | "embeddable"
>; >;
id?: ExcalidrawGenericElement["id"]; id?: ExcalidrawGenericElement["id"];
} }
@ -64,7 +65,7 @@ export type ValidLinearElement = {
id: ExcalidrawGenericElement["id"]; id: ExcalidrawGenericElement["id"];
type?: Exclude< type?: Exclude<
ExcalidrawBindableElement["type"], ExcalidrawBindableElement["type"],
"image" | "selection" | "text" | "frame" "image" | "text" | "frame" | "embeddable"
>; >;
} }
) )
@ -88,7 +89,7 @@ export type ValidLinearElement = {
| { | {
type: Exclude< type: Exclude<
ExcalidrawBindableElement["type"], ExcalidrawBindableElement["type"],
"image" | "selection" | "text" | "frame" "image" | "text" | "frame" | "embeddable"
>; >;
id?: ExcalidrawGenericElement["id"]; id?: ExcalidrawGenericElement["id"];
} }
@ -96,7 +97,7 @@ export type ValidLinearElement = {
id: ExcalidrawGenericElement["id"]; id: ExcalidrawGenericElement["id"];
type?: Exclude< type?: Exclude<
ExcalidrawBindableElement["type"], ExcalidrawBindableElement["type"],
"image" | "selection" | "text" | "frame" "image" | "text" | "frame" | "embeddable"
>; >;
} }
) )
@ -131,8 +132,8 @@ export type ValidContainer =
export type ExcalidrawProgrammaticElement = export type ExcalidrawProgrammaticElement =
| Extract< | Extract<
ExcalidrawElement, Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
| ExcalidrawSelectionElement | ExcalidrawEmbeddableElement
| ExcalidrawFreeDrawElement | ExcalidrawFreeDrawElement
| ExcalidrawFrameElement | ExcalidrawFrameElement
> >
@ -160,15 +161,6 @@ export type ExcalidrawProgrammaticElement =
export interface ExcalidrawProgrammaticAPI { export interface ExcalidrawProgrammaticAPI {
elements?: readonly ExcalidrawProgrammaticElement[] | null; elements?: readonly ExcalidrawProgrammaticElement[] | null;
} }
export const ELEMENTS_SUPPORTING_PROGRAMMATIC_API = [
"rectangle",
"ellipse",
"diamond",
"text",
"arrow",
"line",
"image",
];
const DEFAULT_LINEAR_ELEMENT_PROPS = { const DEFAULT_LINEAR_ELEMENT_PROPS = {
width: 300, width: 300,
@ -269,7 +261,9 @@ const bindLinearElementToElement = (
.getElements() .getElements()
.find((ele) => ele?.id === start.id) as Exclude< .find((ele) => ele?.id === start.id) as Exclude<
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawImageElement | ExcalidrawFrameElement | ExcalidrawImageElement
| ExcalidrawFrameElement
| ExcalidrawEmbeddableElement
>; >;
if (!existingElement) { if (!existingElement) {
console.error(`No element for start binding with id ${start.id} found`); console.error(`No element for start binding with id ${start.id} found`);
@ -339,7 +333,9 @@ const bindLinearElementToElement = (
ele, ele,
): ele is Exclude< ): ele is Exclude<
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawImageElement | ExcalidrawFrameElement | ExcalidrawImageElement
| ExcalidrawFrameElement
| ExcalidrawEmbeddableElement
> => ele?.id === end.id, > => ele?.id === end.id,
); );
if (!existingElement) { if (!existingElement) {
@ -423,10 +419,6 @@ class ElementStore {
getElements = () => { getElements = () => {
return this.res; return this.res;
}; };
hasElementWithId = (id: string) => {
const index = this.elementMap.get(id);
return index !== undefined && index >= 0;
};
} }
export const convertToExcalidrawElements = ( export const convertToExcalidrawElements = (
@ -435,50 +427,51 @@ export const convertToExcalidrawElements = (
if (!elements) { if (!elements) {
return []; return [];
} }
const elementStore = new ElementStore(); const elementStore = new ElementStore();
// Push all elements to array as there could be cases where element id
// is referenced before element is created // ensure unique ids
elements.forEach((element) => { // ---------------------------------------------------------------------------
let elementId = element.id || regenerateId(null); const ids = new Set<ExcalidrawElement["id"]>();
const elementsWithIds = elements.map((element) => {
let id = element.id || regenerateId(null);
// To make sure every element has a unique id since regenerateId appends // To make sure every element has a unique id since regenerateId appends
// _copy to the original id and if it exists we need to generate again // _copy to the original id and if it exists we need to generate again
// hence a loop // hence a loop
while (elementStore.hasElementWithId(elementId)) { while (ids.has(id)) {
elementId = regenerateId(elementId); id = regenerateId(id);
} }
const elementWithId = { ...element, id: elementId };
elementStore.add(elementWithId as ExcalidrawElement); ids.add(id);
return { ...element, id };
}); });
// ---------------------------------------------------------------------------
const pushedElements = for (const element of elementsWithIds) {
elementStore.getElements() as readonly ExcalidrawProgrammaticElement[]; switch (element.type) {
pushedElements.forEach((element) => { case "rectangle":
const elementWithId = { ...element }; case "ellipse":
case "diamond":
if ( case "arrow": {
(elementWithId.type === "rectangle" || if (element.label?.text) {
elementWithId.type === "ellipse" ||
elementWithId.type === "diamond" ||
elementWithId.type === "arrow") &&
elementWithId?.label?.text
) {
let [container, text] = bindTextToContainer( let [container, text] = bindTextToContainer(
elementWithId as element as
| ValidContainer | ValidContainer
| ({ | ({
type: "arrow"; type: "arrow";
} & ValidLinearElement), } & ValidLinearElement),
elementWithId?.label, element?.label,
); );
elementStore.add(container); elementStore.add(container);
elementStore.add(text); elementStore.add(text);
if (container.type === "arrow") { if (container.type === "arrow") {
const originalStart = const originalStart =
elementWithId.type === "arrow" ? elementWithId?.start : undefined; element.type === "arrow" ? element?.start : undefined;
const originalEnd = const originalEnd =
elementWithId.type === "arrow" ? elementWithId?.end : undefined; element.type === "arrow" ? element?.end : undefined;
const { linearElement, startBoundElement, endBoundElement } = const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement( bindLinearElementToElement(
{ {
@ -493,12 +486,59 @@ export const convertToExcalidrawElements = (
elementStore.add(startBoundElement); elementStore.add(startBoundElement);
elementStore.add(endBoundElement); elementStore.add(endBoundElement);
} }
} else if (elementWithId.type === "text") { } else {
const fontFamily = elementWithId?.fontFamily || DEFAULT_FONT_FAMILY; switch (element.type) {
const fontSize = elementWithId?.fontSize || DEFAULT_FONT_SIZE; case "arrow": {
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(element, elementStore);
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
break;
}
case "rectangle":
case "ellipse":
case "diamond": {
elementStore.add(
newElement({
...element,
width: element?.width || DEFAULT_DIMENSION,
height: element?.height || DEFAULT_DIMENSION,
}),
);
break;
}
default: {
assertNever(
element.type,
`Unhandled element type "${element.type}"`,
);
}
}
}
break;
}
case "line": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
const lineElement = newLinearElement({
width,
height,
points: [
[0, 0],
[width, height],
],
...element,
});
elementStore.add(lineElement);
break;
}
case "text": {
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = const lineHeight =
elementWithId?.lineHeight || getDefaultLineHeight(fontFamily); element?.lineHeight || getDefaultLineHeight(fontFamily);
const text = elementWithId.text ?? ""; const text = element.text ?? "";
const normalizedText = normalizeText(text); const normalizedText = normalizeText(text);
const metrics = measureText( const metrics = measureText(
normalizedText, normalizedText,
@ -511,48 +551,35 @@ export const convertToExcalidrawElements = (
height: metrics.height, height: metrics.height,
fontFamily, fontFamily,
fontSize, fontSize,
...elementWithId, ...element,
}); });
elementStore.add(textElement); elementStore.add(textElement);
} else if (elementWithId.type === "arrow") { break;
const { linearElement, startBoundElement, endBoundElement } = }
bindLinearElementToElement(elementWithId, elementStore); case "image": {
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
} else if (elementWithId.type === "line") {
const width = elementWithId.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height =
elementWithId.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
const lineElement = newLinearElement({
width,
height,
points: [
[0, 0],
[width, height],
],
...elementWithId,
});
elementStore.add(lineElement);
} else if (elementWithId.type === "image") {
const imageElement = newImageElement({ const imageElement = newImageElement({
width: elementWithId?.width || DEFAULT_DIMENSION, width: element?.width || DEFAULT_DIMENSION,
height: elementWithId?.height || DEFAULT_DIMENSION, height: element?.height || DEFAULT_DIMENSION,
...elementWithId, ...element,
}); });
elementStore.add(imageElement); elementStore.add(imageElement);
} else if ( break;
elementWithId.type === "rectangle" || }
elementWithId.type === "ellipse" || case "freedraw":
elementWithId.type === "diamond" case "frame":
) { case "embeddable": {
const element = newElement({ elementStore.add(element);
...elementWithId, break;
width: elementWithId?.width || DEFAULT_DIMENSION, }
height: elementWithId?.height || DEFAULT_DIMENSION, default: {
}); elementStore.add(element);
elementStore.add(element); assertNever(
element,
`Unhandled element type "${(element as any).type}"`,
true,
);
}
}
} }
});
return elementStore.getElements(); return elementStore.getElements();
}; };

View file

@ -914,3 +914,16 @@ export const isOnlyExportingSingleFrame = (
) )
); );
}; };
export const assertNever = (
value: never,
message: string,
softAssert?: boolean,
): never => {
if (softAssert) {
console.error(message);
return value;
}
throw new Error(message);
};