mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
make convertToExcalidrawElements
more typesafe
This commit is contained in:
parent
0404a3ecf1
commit
07c21446e0
2 changed files with 170 additions and 130 deletions
|
@ -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,124 +427,159 @@ 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" ||
|
let [container, text] = bindTextToContainer(
|
||||||
elementWithId.type === "diamond" ||
|
element as
|
||||||
elementWithId.type === "arrow") &&
|
| ValidContainer
|
||||||
elementWithId?.label?.text
|
| ({
|
||||||
) {
|
type: "arrow";
|
||||||
let [container, text] = bindTextToContainer(
|
} & ValidLinearElement),
|
||||||
elementWithId as
|
element?.label,
|
||||||
| ValidContainer
|
|
||||||
| ({
|
|
||||||
type: "arrow";
|
|
||||||
} & ValidLinearElement),
|
|
||||||
elementWithId?.label,
|
|
||||||
);
|
|
||||||
elementStore.add(container);
|
|
||||||
elementStore.add(text);
|
|
||||||
|
|
||||||
if (container.type === "arrow") {
|
|
||||||
const originalStart =
|
|
||||||
elementWithId.type === "arrow" ? elementWithId?.start : undefined;
|
|
||||||
const originalEnd =
|
|
||||||
elementWithId.type === "arrow" ? elementWithId?.end : undefined;
|
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
|
||||||
bindLinearElementToElement(
|
|
||||||
{
|
|
||||||
...container,
|
|
||||||
start: originalStart,
|
|
||||||
end: originalEnd,
|
|
||||||
},
|
|
||||||
elementStore,
|
|
||||||
);
|
);
|
||||||
container = linearElement;
|
elementStore.add(container);
|
||||||
elementStore.add(linearElement);
|
elementStore.add(text);
|
||||||
elementStore.add(startBoundElement);
|
|
||||||
elementStore.add(endBoundElement);
|
|
||||||
}
|
|
||||||
} else if (elementWithId.type === "text") {
|
|
||||||
const fontFamily = elementWithId?.fontFamily || DEFAULT_FONT_FAMILY;
|
|
||||||
const fontSize = elementWithId?.fontSize || DEFAULT_FONT_SIZE;
|
|
||||||
const lineHeight =
|
|
||||||
elementWithId?.lineHeight || getDefaultLineHeight(fontFamily);
|
|
||||||
const text = elementWithId.text ?? "";
|
|
||||||
const normalizedText = normalizeText(text);
|
|
||||||
const metrics = measureText(
|
|
||||||
normalizedText,
|
|
||||||
getFontString({ fontFamily, fontSize }),
|
|
||||||
lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
const textElement = newTextElement({
|
if (container.type === "arrow") {
|
||||||
width: metrics.width,
|
const originalStart =
|
||||||
height: metrics.height,
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
fontFamily,
|
const originalEnd =
|
||||||
fontSize,
|
element.type === "arrow" ? element?.end : undefined;
|
||||||
...elementWithId,
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
});
|
bindLinearElementToElement(
|
||||||
elementStore.add(textElement);
|
{
|
||||||
} else if (elementWithId.type === "arrow") {
|
...container,
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
start: originalStart,
|
||||||
bindLinearElementToElement(elementWithId, elementStore);
|
end: originalEnd,
|
||||||
elementStore.add(linearElement);
|
},
|
||||||
elementStore.add(startBoundElement);
|
elementStore,
|
||||||
elementStore.add(endBoundElement);
|
);
|
||||||
} else if (elementWithId.type === "line") {
|
container = linearElement;
|
||||||
const width = elementWithId.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
elementStore.add(linearElement);
|
||||||
const height =
|
elementStore.add(startBoundElement);
|
||||||
elementWithId.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
elementStore.add(endBoundElement);
|
||||||
const lineElement = newLinearElement({
|
}
|
||||||
width,
|
} else {
|
||||||
height,
|
switch (element.type) {
|
||||||
points: [
|
case "arrow": {
|
||||||
[0, 0],
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
[width, height],
|
bindLinearElementToElement(element, elementStore);
|
||||||
],
|
elementStore.add(linearElement);
|
||||||
...elementWithId,
|
elementStore.add(startBoundElement);
|
||||||
});
|
elementStore.add(endBoundElement);
|
||||||
elementStore.add(lineElement);
|
break;
|
||||||
} else if (elementWithId.type === "image") {
|
}
|
||||||
const imageElement = newImageElement({
|
case "rectangle":
|
||||||
width: elementWithId?.width || DEFAULT_DIMENSION,
|
case "ellipse":
|
||||||
height: elementWithId?.height || DEFAULT_DIMENSION,
|
case "diamond": {
|
||||||
...elementWithId,
|
elementStore.add(
|
||||||
});
|
newElement({
|
||||||
elementStore.add(imageElement);
|
...element,
|
||||||
} else if (
|
width: element?.width || DEFAULT_DIMENSION,
|
||||||
elementWithId.type === "rectangle" ||
|
height: element?.height || DEFAULT_DIMENSION,
|
||||||
elementWithId.type === "ellipse" ||
|
}),
|
||||||
elementWithId.type === "diamond"
|
);
|
||||||
) {
|
break;
|
||||||
const element = newElement({
|
}
|
||||||
...elementWithId,
|
default: {
|
||||||
width: elementWithId?.width || DEFAULT_DIMENSION,
|
assertNever(
|
||||||
height: elementWithId?.height || DEFAULT_DIMENSION,
|
element.type,
|
||||||
});
|
`Unhandled element type "${element.type}"`,
|
||||||
elementStore.add(element);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 =
|
||||||
|
element?.lineHeight || getDefaultLineHeight(fontFamily);
|
||||||
|
const text = element.text ?? "";
|
||||||
|
const normalizedText = normalizeText(text);
|
||||||
|
const metrics = measureText(
|
||||||
|
normalizedText,
|
||||||
|
getFontString({ fontFamily, fontSize }),
|
||||||
|
lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const textElement = newTextElement({
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
elementStore.add(textElement);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "image": {
|
||||||
|
const imageElement = newImageElement({
|
||||||
|
width: element?.width || DEFAULT_DIMENSION,
|
||||||
|
height: element?.height || DEFAULT_DIMENSION,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
elementStore.add(imageElement);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "freedraw":
|
||||||
|
case "frame":
|
||||||
|
case "embeddable": {
|
||||||
|
elementStore.add(element);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
elementStore.add(element);
|
||||||
|
assertNever(
|
||||||
|
element,
|
||||||
|
`Unhandled element type "${(element as any).type}"`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return elementStore.getElements();
|
return elementStore.getElements();
|
||||||
};
|
};
|
||||||
|
|
13
src/utils.ts
13
src/utils.ts
|
@ -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);
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue