Compare commits
4 Commits
arnost/exp
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
3ea07076ad | ||
|
ded0222e8d | ||
|
e7983bc493 | ||
|
083bcf802c |
@ -69,6 +69,10 @@ It's also a good idea to consider if your change should include additional tests
|
|||||||
|
|
||||||
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
|
||||||
|
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
|
||||||
|
:::
|
||||||
|
|
||||||
## Translating
|
## Translating
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ export interface ClipboardData {
|
|||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
programmaticAPI?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
let CLIPBOARD = "";
|
||||||
@ -48,6 +49,7 @@ const clipboardContainsElements = (
|
|||||||
[
|
[
|
||||||
EXPORT_DATA_TYPES.excalidraw,
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
EXPORT_DATA_TYPES.excalidrawClipboard,
|
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
|
EXPORT_DATA_TYPES.excalidrawClipboardWithAPI,
|
||||||
].includes(contents?.type) &&
|
].includes(contents?.type) &&
|
||||||
Array.isArray(contents.elements)
|
Array.isArray(contents.elements)
|
||||||
) {
|
) {
|
||||||
@ -191,6 +193,8 @@ export const parseClipboard = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
|
const programmaticAPI =
|
||||||
|
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
return {
|
return {
|
||||||
elements: systemClipboardData.elements,
|
elements: systemClipboardData.elements,
|
||||||
@ -198,6 +202,7 @@ export const parseClipboard = async (
|
|||||||
text: isPlainPaste
|
text: isPlainPaste
|
||||||
? JSON.stringify(systemClipboardData.elements, null, 2)
|
? JSON.stringify(systemClipboardData.elements, null, 2)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
programmaticAPI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
@ -346,6 +346,10 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
|
import {
|
||||||
|
ExcalidrawElementSkeleton,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
} from "../data/transform";
|
||||||
import { ValueOf } from "../utility-types";
|
import { ValueOf } from "../utility-types";
|
||||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
|
|
||||||
@ -2231,7 +2235,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let file = event?.clipboardData?.files[0];
|
let file = event?.clipboardData?.files[0];
|
||||||
|
|
||||||
const data = await parseClipboard(event, isPlainPaste);
|
const data = await parseClipboard(event, isPlainPaste);
|
||||||
|
|
||||||
if (!file && data.text && !isPlainPaste) {
|
if (!file && data.text && !isPlainPaste) {
|
||||||
const string = data.text.trim();
|
const string = data.text.trim();
|
||||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||||
@ -2286,9 +2289,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
|
const elements = (
|
||||||
|
data.programmaticAPI
|
||||||
|
? convertToExcalidrawElements(
|
||||||
|
data.elements as ExcalidrawElementSkeleton[],
|
||||||
|
)
|
||||||
|
: data.elements
|
||||||
|
) as readonly ExcalidrawElement[];
|
||||||
// TODO remove formatting from elements if isPlainPaste
|
// TODO remove formatting from elements if isPlainPaste
|
||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements: data.elements,
|
elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
@ -5395,7 +5405,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
width: embedLink.aspectRatio.w,
|
width: embedLink.aspectRatio.w,
|
||||||
height: embedLink.aspectRatio.h,
|
height: embedLink.aspectRatio.h,
|
||||||
link,
|
link,
|
||||||
validated: undefined,
|
validated: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
@ -5583,7 +5593,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createGenericElementOnPointerDown = (
|
private createGenericElementOnPointerDown = (
|
||||||
elementType: ExcalidrawGenericElement["type"],
|
elementType: ExcalidrawGenericElement["type"] | "embeddable",
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): void => {
|
): void => {
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
@ -5597,8 +5607,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y: gridY,
|
y: gridY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const element = newElement({
|
const baseElementAttributes = {
|
||||||
type: elementType,
|
|
||||||
x: gridX,
|
x: gridX,
|
||||||
y: gridY,
|
y: gridY,
|
||||||
strokeColor: this.state.currentItemStrokeColor,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
@ -5611,8 +5620,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roundness: this.getCurrentItemRoundness(elementType),
|
roundness: this.getCurrentItemRoundness(elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
...(elementType === "embeddable" ? { validated: false } : {}),
|
} as const;
|
||||||
});
|
|
||||||
|
let element;
|
||||||
|
if (elementType === "embeddable") {
|
||||||
|
element = newEmbeddableElement({
|
||||||
|
type: "embeddable",
|
||||||
|
validated: null,
|
||||||
|
...baseElementAttributes,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element = newElement({
|
||||||
|
type: elementType,
|
||||||
|
...baseElementAttributes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (element.type === "selection") {
|
if (element.type === "selection") {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -164,6 +164,7 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidraw: "excalidraw",
|
excalidraw: "excalidraw",
|
||||||
excalidrawClipboard: "excalidraw/clipboard",
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
excalidrawLibrary: "excalidrawlib",
|
excalidrawLibrary: "excalidrawlib",
|
||||||
|
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_SOURCE =
|
export const EXPORT_SOURCE =
|
||||||
|
2032
src/data/__snapshots__/transform.test.ts.snap
Normal file
2032
src/data/__snapshots__/transform.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,7 @@ import {
|
|||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
|
DEFAULT_ELEMENT_PROPS,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
@ -41,7 +42,6 @@ import {
|
|||||||
getDefaultLineHeight,
|
getDefaultLineHeight,
|
||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
@ -122,16 +122,18 @@ const restoreElementWithProperties = <
|
|||||||
versionNonce: element.versionNonce ?? 0,
|
versionNonce: element.versionNonce ?? 0,
|
||||||
isDeleted: element.isDeleted ?? false,
|
isDeleted: element.isDeleted ?? false,
|
||||||
id: element.id || randomId(),
|
id: element.id || randomId(),
|
||||||
fillStyle: element.fillStyle || "hachure",
|
fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle,
|
||||||
strokeWidth: element.strokeWidth || 1,
|
strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
strokeStyle: element.strokeStyle ?? "solid",
|
strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
roughness: element.roughness ?? 1,
|
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
opacity: element.opacity == null ? 100 : element.opacity,
|
opacity:
|
||||||
|
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
|
||||||
angle: element.angle || 0,
|
angle: element.angle || 0,
|
||||||
x: extra.x ?? element.x ?? 0,
|
x: extra.x ?? element.x ?? 0,
|
||||||
y: extra.y ?? element.y ?? 0,
|
y: extra.y ?? element.y ?? 0,
|
||||||
strokeColor: element.strokeColor || COLOR_PALETTE.black,
|
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent,
|
backgroundColor:
|
||||||
|
element.backgroundColor || DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||||
width: element.width || 0,
|
width: element.width || 0,
|
||||||
height: element.height || 0,
|
height: element.height || 0,
|
||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
@ -246,7 +248,6 @@ const restoreElement = (
|
|||||||
startArrowhead = null,
|
startArrowhead = null,
|
||||||
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
||||||
} = element;
|
} = element;
|
||||||
|
|
||||||
let x = element.x;
|
let x = element.x;
|
||||||
let y = element.y;
|
let y = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points = // migrate old arrow model to new one
|
||||||
@ -286,7 +287,7 @@ const restoreElement = (
|
|||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
validated: undefined,
|
validated: null,
|
||||||
});
|
});
|
||||||
case "frame":
|
case "frame":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
@ -410,7 +411,6 @@ export const restoreElements = (
|
|||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
|
|
||||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||||
const restoredElements = (elements || []).reduce((elements, element) => {
|
const restoredElements = (elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
@ -429,6 +429,7 @@ export const restoreElements = (
|
|||||||
migratedElement = { ...migratedElement, id: randomId() };
|
migratedElement = { ...migratedElement, id: randomId() };
|
||||||
}
|
}
|
||||||
existingIds.add(migratedElement.id);
|
existingIds.add(migratedElement.id);
|
||||||
|
|
||||||
elements.push(migratedElement);
|
elements.push(migratedElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
706
src/data/transform.test.ts
Normal file
706
src/data/transform.test.ts
Normal file
@ -0,0 +1,706 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
import {
|
||||||
|
ExcalidrawElementSkeleton,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
} from "./transform";
|
||||||
|
import { ExcalidrawArrowElement } from "../element/types";
|
||||||
|
|
||||||
|
describe("Test Transform", () => {
|
||||||
|
it("should transform regular shapes", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
x: 100,
|
||||||
|
y: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 100,
|
||||||
|
y: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 300,
|
||||||
|
y: 100,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: "#c0eb75",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
x: 300,
|
||||||
|
y: 250,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: "#ffc9c9",
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 300,
|
||||||
|
y: 400,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: "#a5d8ff",
|
||||||
|
strokeColor: "#1971c2",
|
||||||
|
strokeStyle: "dashed",
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
).forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform text element", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
text: "HELLO WORLD!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
x: 100,
|
||||||
|
y: 150,
|
||||||
|
text: "STYLED HELLO WORLD!",
|
||||||
|
fontSize: 20,
|
||||||
|
strokeColor: "#5f3dc4",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
).forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform linear elements", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 450,
|
||||||
|
y: 20,
|
||||||
|
startArrowhead: "dot",
|
||||||
|
endArrowhead: "triangle",
|
||||||
|
strokeColor: "#1971c2",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "line",
|
||||||
|
x: 100,
|
||||||
|
y: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "line",
|
||||||
|
x: 450,
|
||||||
|
y: 60,
|
||||||
|
strokeColor: "#2f9e44",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "dotted",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform to text containers when label provided", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
label: {
|
||||||
|
text: "RECTANGLE TEXT CONTAINER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
x: 500,
|
||||||
|
y: 100,
|
||||||
|
width: 200,
|
||||||
|
label: {
|
||||||
|
text: "ELLIPSE TEXT CONTAINER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 100,
|
||||||
|
y: 150,
|
||||||
|
width: 280,
|
||||||
|
label: {
|
||||||
|
text: "DIAMOND\nTEXT CONTAINER",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
x: 100,
|
||||||
|
y: 400,
|
||||||
|
width: 300,
|
||||||
|
backgroundColor: "#fff3bf",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "STYLED DIAMOND TEXT CONTAINER",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 500,
|
||||||
|
y: 300,
|
||||||
|
width: 200,
|
||||||
|
strokeColor: "#c2255c",
|
||||||
|
label: {
|
||||||
|
text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
|
||||||
|
textAlign: "left",
|
||||||
|
verticalAlign: "top",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
strokeColor: "#f08c00",
|
||||||
|
backgroundColor: "#ffec99",
|
||||||
|
width: 200,
|
||||||
|
label: {
|
||||||
|
text: "STYLED ELLIPSE TEXT CONTAINER",
|
||||||
|
strokeColor: "#c2255c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(12);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform to labelled arrows when label provided for arrows", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
label: {
|
||||||
|
text: "LABELED ARROW",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
label: {
|
||||||
|
text: "STYLED LABELED ARROW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 300,
|
||||||
|
strokeColor: "#1098ad",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "ANOTHER STYLED LABELLED ARROW",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 100,
|
||||||
|
y: 400,
|
||||||
|
strokeColor: "#1098ad",
|
||||||
|
strokeWidth: 2,
|
||||||
|
label: {
|
||||||
|
text: "ANOTHER STYLED LABELLED ARROW",
|
||||||
|
strokeColor: "#099268",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(8);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test arrow bindings", () => {
|
||||||
|
it("should bind arrows to shapes when start / end provided without ids", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
label: {
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
type: "rectangle",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: "ellipse",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
const [arrow, text, rectangle, ellipse] = excaldrawElements;
|
||||||
|
expect(arrow).toMatchObject({
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
|
startBinding: {
|
||||||
|
elementId: rectangle.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 1,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: ellipse.id,
|
||||||
|
focus: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toMatchObject({
|
||||||
|
x: 340,
|
||||||
|
y: 226.5,
|
||||||
|
type: "text",
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
containerId: arrow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rectangle).toMatchObject({
|
||||||
|
x: 155,
|
||||||
|
y: 189,
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ellipse).toMatchObject({
|
||||||
|
x: 555,
|
||||||
|
y: 189,
|
||||||
|
type: "ellipse",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind arrows to text when start / end provided without ids", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
label: {
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
type: "text",
|
||||||
|
text: "HEYYYYY",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: "text",
|
||||||
|
text: "WHATS UP ?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
|
const [arrow, text1, text2, text3] = excaldrawElements;
|
||||||
|
|
||||||
|
expect(arrow).toMatchObject({
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
boundElements: [{ id: text1.id, type: "text" }],
|
||||||
|
startBinding: {
|
||||||
|
elementId: text2.id,
|
||||||
|
focus: 0,
|
||||||
|
gap: 1,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: text3.id,
|
||||||
|
focus: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text1).toMatchObject({
|
||||||
|
x: 340,
|
||||||
|
y: 226.5,
|
||||||
|
type: "text",
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
containerId: arrow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text2).toMatchObject({
|
||||||
|
x: 185,
|
||||||
|
y: 226.5,
|
||||||
|
type: "text",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text3).toMatchObject({
|
||||||
|
x: 555,
|
||||||
|
y: 226.5,
|
||||||
|
type: "text",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind arrows to existing shapes when start / end provided with ids", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "ellipse",
|
||||||
|
id: "ellipse-1",
|
||||||
|
strokeColor: "#66a80f",
|
||||||
|
x: 630,
|
||||||
|
y: 316,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
backgroundColor: "#d8f5a2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "diamond",
|
||||||
|
id: "diamond-1",
|
||||||
|
strokeColor: "#9c36b5",
|
||||||
|
width: 140,
|
||||||
|
x: 96,
|
||||||
|
y: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 247,
|
||||||
|
y: 420,
|
||||||
|
width: 395,
|
||||||
|
height: 35,
|
||||||
|
strokeColor: "#1864ab",
|
||||||
|
start: {
|
||||||
|
type: "rectangle",
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "ellipse-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 227,
|
||||||
|
y: 450,
|
||||||
|
width: 400,
|
||||||
|
strokeColor: "#e67700",
|
||||||
|
start: {
|
||||||
|
id: "diamond-1",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "ellipse-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(5);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind arrows to existing text elements when start / end provided with ids", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
x: 100,
|
||||||
|
y: 239,
|
||||||
|
type: "text",
|
||||||
|
text: "HEYYYYY",
|
||||||
|
id: "text-1",
|
||||||
|
strokeColor: "#c2255c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
id: "text-2",
|
||||||
|
x: 560,
|
||||||
|
y: 239,
|
||||||
|
text: "Whats up ?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
label: {
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
id: "text-1",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "text-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
|
excaldrawElements.forEach((ele) => {
|
||||||
|
expect(ele).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
id: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind arrows to existing elements if ids are correct", () => {
|
||||||
|
const consoleErrorSpy = vi
|
||||||
|
.spyOn(console, "error")
|
||||||
|
.mockImplementationOnce(() => void 0);
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
x: 100,
|
||||||
|
y: 239,
|
||||||
|
type: "text",
|
||||||
|
text: "HEYYYYY",
|
||||||
|
id: "text-1",
|
||||||
|
strokeColor: "#c2255c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 560,
|
||||||
|
y: 139,
|
||||||
|
id: "rect-1",
|
||||||
|
width: 100,
|
||||||
|
height: 200,
|
||||||
|
backgroundColor: "#bac8ff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
label: {
|
||||||
|
text: "HELLO WORLD!!",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
id: "text-13",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: "rect-11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
const [, , arrow] = excaldrawElements;
|
||||||
|
expect(arrow).toMatchObject({
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: "id46",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
startBinding: null,
|
||||||
|
endBinding: null,
|
||||||
|
});
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"No element for start binding with id text-13 found",
|
||||||
|
);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"No element for end binding with id rect-11 found",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bind when ids referenced before the element data", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 255,
|
||||||
|
y: 239,
|
||||||
|
end: {
|
||||||
|
id: "rect-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 560,
|
||||||
|
y: 139,
|
||||||
|
id: "rect-1",
|
||||||
|
width: 100,
|
||||||
|
height: 200,
|
||||||
|
backgroundColor: "#bac8ff",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
expect(excaldrawElements.length).toBe(2);
|
||||||
|
const [arrow, rect] = excaldrawElements;
|
||||||
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
|
elementId: "rect-1",
|
||||||
|
focus: 0,
|
||||||
|
gap: 5,
|
||||||
|
});
|
||||||
|
expect(rect.boundElements).toStrictEqual([
|
||||||
|
{
|
||||||
|
id: "id47",
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow duplicate ids", () => {
|
||||||
|
const consoleErrorSpy = vi
|
||||||
|
.spyOn(console, "error")
|
||||||
|
.mockImplementationOnce(() => void 0);
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 300,
|
||||||
|
y: 100,
|
||||||
|
id: "rect-1",
|
||||||
|
width: 100,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
id: "rect-1",
|
||||||
|
width: 100,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excaldrawElements.length).toBe(1);
|
||||||
|
expect(excaldrawElements[0]).toMatchSnapshot({
|
||||||
|
seed: expect.any(Number),
|
||||||
|
versionNonce: expect.any(Number),
|
||||||
|
});
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
"Duplicate id found for rect-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
561
src/data/transform.ts
Normal file
561
src/data/transform.ts
Normal file
@ -0,0 +1,561 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
TEXT_ALIGN,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
} from "../constants";
|
||||||
|
import {
|
||||||
|
newElement,
|
||||||
|
newLinearElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
} from "../element";
|
||||||
|
import { bindLinearElement } from "../element/binding";
|
||||||
|
import {
|
||||||
|
ElementConstructorOpts,
|
||||||
|
newImageElement,
|
||||||
|
newTextElement,
|
||||||
|
} from "../element/newElement";
|
||||||
|
import {
|
||||||
|
getDefaultLineHeight,
|
||||||
|
measureText,
|
||||||
|
normalizeText,
|
||||||
|
} from "../element/textElement";
|
||||||
|
import {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawSelectionElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
FileId,
|
||||||
|
FontFamilyValues,
|
||||||
|
TextAlign,
|
||||||
|
VerticalAlign,
|
||||||
|
} from "../element/types";
|
||||||
|
import { MarkOptional } from "../utility-types";
|
||||||
|
import { assertNever, getFontString } from "../utils";
|
||||||
|
|
||||||
|
export type ValidLinearElement = {
|
||||||
|
type: "arrow" | "line";
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
label?: {
|
||||||
|
text: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontFamily?: FontFamilyValues;
|
||||||
|
textAlign?: TextAlign;
|
||||||
|
verticalAlign?: VerticalAlign;
|
||||||
|
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||||
|
end?:
|
||||||
|
| (
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
type: Exclude<
|
||||||
|
ExcalidrawBindableElement["type"],
|
||||||
|
"image" | "text" | "frame" | "embeddable"
|
||||||
|
>;
|
||||||
|
id?: ExcalidrawGenericElement["id"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: ExcalidrawGenericElement["id"];
|
||||||
|
type?: Exclude<
|
||||||
|
ExcalidrawBindableElement["type"],
|
||||||
|
"image" | "text" | "frame" | "embeddable"
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
| ((
|
||||||
|
| {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type?: "text";
|
||||||
|
id: ExcalidrawTextElement["id"];
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
Partial<ExcalidrawTextElement>)
|
||||||
|
) &
|
||||||
|
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||||
|
start?:
|
||||||
|
| (
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
type: Exclude<
|
||||||
|
ExcalidrawBindableElement["type"],
|
||||||
|
"image" | "text" | "frame" | "embeddable"
|
||||||
|
>;
|
||||||
|
id?: ExcalidrawGenericElement["id"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: ExcalidrawGenericElement["id"];
|
||||||
|
type?: Exclude<
|
||||||
|
ExcalidrawBindableElement["type"],
|
||||||
|
"image" | "text" | "frame" | "embeddable"
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
| ((
|
||||||
|
| {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type?: "text";
|
||||||
|
id: ExcalidrawTextElement["id"];
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
Partial<ExcalidrawTextElement>)
|
||||||
|
) &
|
||||||
|
MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||||
|
} & Partial<ExcalidrawLinearElement>;
|
||||||
|
|
||||||
|
export type ValidContainer =
|
||||||
|
| {
|
||||||
|
type: Exclude<ExcalidrawGenericElement["type"], "selection">;
|
||||||
|
id?: ExcalidrawGenericElement["id"];
|
||||||
|
label?: {
|
||||||
|
text: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontFamily?: FontFamilyValues;
|
||||||
|
textAlign?: TextAlign;
|
||||||
|
verticalAlign?: VerticalAlign;
|
||||||
|
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
|
||||||
|
} & ElementConstructorOpts;
|
||||||
|
|
||||||
|
export type ExcalidrawElementSkeleton =
|
||||||
|
| Extract<
|
||||||
|
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawFreeDrawElement
|
||||||
|
| ExcalidrawFrameElement
|
||||||
|
>
|
||||||
|
| ({
|
||||||
|
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} & Partial<ExcalidrawLinearElement>)
|
||||||
|
| ValidContainer
|
||||||
|
| ValidLinearElement
|
||||||
|
| ({
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
id?: ExcalidrawTextElement["id"];
|
||||||
|
} & Partial<ExcalidrawTextElement>)
|
||||||
|
| ({
|
||||||
|
type: Extract<ExcalidrawImageElement["type"], "image">;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
fileId: FileId;
|
||||||
|
} & Partial<ExcalidrawImageElement>);
|
||||||
|
|
||||||
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
|
width: 300,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DIMENSION = 100;
|
||||||
|
|
||||||
|
const bindTextToContainer = (
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||||
|
) => {
|
||||||
|
const textElement: ExcalidrawTextElement = newTextElement({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
...textProps,
|
||||||
|
containerId: container.id,
|
||||||
|
strokeColor: textProps.strokeColor || container.strokeColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(container, {
|
||||||
|
boundElements: (container.boundElements || []).concat({
|
||||||
|
type: "text",
|
||||||
|
id: textElement.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
redrawTextBoundingBox(textElement, container);
|
||||||
|
return [container, textElement] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindLinearElementToElement = (
|
||||||
|
linearElement: ExcalidrawArrowElement,
|
||||||
|
start: ValidLinearElement["start"],
|
||||||
|
end: ValidLinearElement["end"],
|
||||||
|
elementStore: ElementStore,
|
||||||
|
): {
|
||||||
|
linearElement: ExcalidrawLinearElement;
|
||||||
|
startBoundElement?: ExcalidrawElement;
|
||||||
|
endBoundElement?: ExcalidrawElement;
|
||||||
|
} => {
|
||||||
|
let startBoundElement;
|
||||||
|
let endBoundElement;
|
||||||
|
|
||||||
|
Object.assign(linearElement, {
|
||||||
|
startBinding: linearElement?.startBinding || null,
|
||||||
|
endBinding: linearElement.endBinding || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
const width = start?.width ?? DEFAULT_DIMENSION;
|
||||||
|
const height = start?.height ?? DEFAULT_DIMENSION;
|
||||||
|
|
||||||
|
let existingElement;
|
||||||
|
if (start.id) {
|
||||||
|
existingElement = elementStore.getElement(start.id);
|
||||||
|
if (!existingElement) {
|
||||||
|
console.error(`No element for start binding with id ${start.id} found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startX = start.x || linearElement.x - width;
|
||||||
|
const startY = start.y || linearElement.y - height / 2;
|
||||||
|
const startType = existingElement ? existingElement.type : start.type;
|
||||||
|
|
||||||
|
if (startType) {
|
||||||
|
if (startType === "text") {
|
||||||
|
let text = "";
|
||||||
|
if (existingElement && existingElement.type === "text") {
|
||||||
|
text = existingElement.text;
|
||||||
|
} else if (start.type === "text") {
|
||||||
|
text = start.text;
|
||||||
|
}
|
||||||
|
if (!text) {
|
||||||
|
console.error(
|
||||||
|
`No text found for start binding text element for ${linearElement.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
startBoundElement = newTextElement({
|
||||||
|
x: startX,
|
||||||
|
y: startY,
|
||||||
|
type: "text",
|
||||||
|
...existingElement,
|
||||||
|
...start,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
// to position the text correctly when coordinates not provided
|
||||||
|
Object.assign(startBoundElement, {
|
||||||
|
x: start.x || linearElement.x - startBoundElement.width,
|
||||||
|
y: start.y || linearElement.y - startBoundElement.height / 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
switch (startType) {
|
||||||
|
case "rectangle":
|
||||||
|
case "ellipse":
|
||||||
|
case "diamond": {
|
||||||
|
startBoundElement = newElement({
|
||||||
|
x: startX,
|
||||||
|
y: startY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...existingElement,
|
||||||
|
...start,
|
||||||
|
type: startType,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(
|
||||||
|
linearElement as never,
|
||||||
|
`Unhandled element start type "${start.type}"`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindLinearElement(
|
||||||
|
linearElement,
|
||||||
|
startBoundElement as ExcalidrawBindableElement,
|
||||||
|
"start",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (end) {
|
||||||
|
const height = end?.height ?? DEFAULT_DIMENSION;
|
||||||
|
const width = end?.width ?? DEFAULT_DIMENSION;
|
||||||
|
|
||||||
|
let existingElement;
|
||||||
|
if (end.id) {
|
||||||
|
existingElement = elementStore.getElement(end.id);
|
||||||
|
if (!existingElement) {
|
||||||
|
console.error(`No element for end binding with id ${end.id} found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const endX = end.x || linearElement.x + linearElement.width;
|
||||||
|
const endY = end.y || linearElement.y - height / 2;
|
||||||
|
const endType = existingElement ? existingElement.type : end.type;
|
||||||
|
|
||||||
|
if (endType) {
|
||||||
|
if (endType === "text") {
|
||||||
|
let text = "";
|
||||||
|
if (existingElement && existingElement.type === "text") {
|
||||||
|
text = existingElement.text;
|
||||||
|
} else if (end.type === "text") {
|
||||||
|
text = end.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
console.error(
|
||||||
|
`No text found for end binding text element for ${linearElement.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
endBoundElement = newTextElement({
|
||||||
|
x: endX,
|
||||||
|
y: endY,
|
||||||
|
type: "text",
|
||||||
|
...existingElement,
|
||||||
|
...end,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
// to position the text correctly when coordinates not provided
|
||||||
|
Object.assign(endBoundElement, {
|
||||||
|
y: end.y || linearElement.y - endBoundElement.height / 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
switch (endType) {
|
||||||
|
case "rectangle":
|
||||||
|
case "ellipse":
|
||||||
|
case "diamond": {
|
||||||
|
endBoundElement = newElement({
|
||||||
|
x: endX,
|
||||||
|
y: endY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...existingElement,
|
||||||
|
...end,
|
||||||
|
type: endType,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(
|
||||||
|
linearElement as never,
|
||||||
|
`Unhandled element end type "${endType}"`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindLinearElement(
|
||||||
|
linearElement,
|
||||||
|
endBoundElement as ExcalidrawBindableElement,
|
||||||
|
"end",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
linearElement,
|
||||||
|
startBoundElement,
|
||||||
|
endBoundElement,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ElementStore {
|
||||||
|
excalidrawElements = new Map<string, ExcalidrawElement>();
|
||||||
|
|
||||||
|
add = (ele?: ExcalidrawElement) => {
|
||||||
|
if (!ele) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.excalidrawElements.set(ele.id, ele);
|
||||||
|
};
|
||||||
|
getElements = () => {
|
||||||
|
return Array.from(this.excalidrawElements.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
getElement = (id: string) => {
|
||||||
|
return this.excalidrawElements.get(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToExcalidrawElements = (
|
||||||
|
elements: ExcalidrawElementSkeleton[] | null,
|
||||||
|
) => {
|
||||||
|
if (!elements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementStore = new ElementStore();
|
||||||
|
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||||
|
|
||||||
|
// Create individual elements
|
||||||
|
for (const element of elements) {
|
||||||
|
let excalidrawElement: ExcalidrawElement;
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "ellipse":
|
||||||
|
case "diamond": {
|
||||||
|
const width =
|
||||||
|
element?.label?.text && element.width === undefined
|
||||||
|
? 0
|
||||||
|
: element?.width || DEFAULT_DIMENSION;
|
||||||
|
const height =
|
||||||
|
element?.label?.text && element.height === undefined
|
||||||
|
? 0
|
||||||
|
: element?.height || DEFAULT_DIMENSION;
|
||||||
|
excalidrawElement = newElement({
|
||||||
|
...element,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "line": {
|
||||||
|
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
||||||
|
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
||||||
|
excalidrawElement = newLinearElement({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[width, height],
|
||||||
|
],
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "arrow": {
|
||||||
|
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
||||||
|
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
||||||
|
excalidrawElement = newLinearElement({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
endArrowhead: "arrow",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[width, height],
|
||||||
|
],
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
excalidrawElement = newTextElement({
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "image": {
|
||||||
|
excalidrawElement = newImageElement({
|
||||||
|
width: element?.width || DEFAULT_DIMENSION,
|
||||||
|
height: element?.height || DEFAULT_DIMENSION,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "freedraw":
|
||||||
|
case "frame":
|
||||||
|
case "embeddable": {
|
||||||
|
excalidrawElement = element;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
excalidrawElement = element;
|
||||||
|
assertNever(
|
||||||
|
element,
|
||||||
|
`Unhandled element type "${(element as any).type}"`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existingElement = elementStore.getElement(excalidrawElement.id);
|
||||||
|
if (existingElement) {
|
||||||
|
console.error(`Duplicate id found for ${excalidrawElement.id}`);
|
||||||
|
} else {
|
||||||
|
elementStore.add(excalidrawElement);
|
||||||
|
elementsWithIds.set(excalidrawElement.id, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add labels and arrow bindings
|
||||||
|
for (const [id, element] of elementsWithIds) {
|
||||||
|
const excalidrawElement = elementStore.getElement(id)!;
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "ellipse":
|
||||||
|
case "diamond":
|
||||||
|
case "arrow": {
|
||||||
|
if (element.label?.text) {
|
||||||
|
let [container, text] = bindTextToContainer(
|
||||||
|
excalidrawElement,
|
||||||
|
element?.label,
|
||||||
|
);
|
||||||
|
elementStore.add(container);
|
||||||
|
elementStore.add(text);
|
||||||
|
|
||||||
|
if (container.type === "arrow") {
|
||||||
|
const originalStart =
|
||||||
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
|
const originalEnd =
|
||||||
|
element.type === "arrow" ? element?.end : undefined;
|
||||||
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
|
bindLinearElementToElement(
|
||||||
|
container as ExcalidrawArrowElement,
|
||||||
|
originalStart,
|
||||||
|
originalEnd,
|
||||||
|
elementStore,
|
||||||
|
);
|
||||||
|
container = linearElement;
|
||||||
|
elementStore.add(linearElement);
|
||||||
|
elementStore.add(startBoundElement);
|
||||||
|
elementStore.add(endBoundElement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (element.type) {
|
||||||
|
case "arrow": {
|
||||||
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
|
bindLinearElementToElement(
|
||||||
|
excalidrawElement as ExcalidrawArrowElement,
|
||||||
|
element.start,
|
||||||
|
element.end,
|
||||||
|
elementStore,
|
||||||
|
);
|
||||||
|
elementStore.add(linearElement);
|
||||||
|
elementStore.add(startBoundElement);
|
||||||
|
elementStore.add(endBoundElement);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elementStore.getElements();
|
||||||
|
};
|
@ -190,7 +190,7 @@ export const maybeBindLinearElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindLinearElement = (
|
export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
@ -46,7 +46,7 @@ import {
|
|||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
| "width"
|
| "width"
|
||||||
| "height"
|
| "height"
|
||||||
@ -134,7 +134,7 @@ export const newElement = (
|
|||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: "embeddable";
|
type: "embeddable";
|
||||||
validated: boolean | undefined;
|
validated: ExcalidrawEmbeddableElement["validated"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
||||||
return {
|
return {
|
||||||
@ -187,7 +187,7 @@ export const newTextElement = (
|
|||||||
fontFamily?: FontFamilyValues;
|
fontFamily?: FontFamilyValues;
|
||||||
textAlign?: TextAlign;
|
textAlign?: TextAlign;
|
||||||
verticalAlign?: VerticalAlign;
|
verticalAlign?: VerticalAlign;
|
||||||
containerId?: ExcalidrawTextContainer["id"];
|
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
@ -361,8 +361,8 @@ export const newFreeDrawElement = (
|
|||||||
export const newLinearElement = (
|
export const newLinearElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
startArrowhead: Arrowhead | null;
|
startArrowhead?: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
@ -372,8 +372,8 @@ export const newLinearElement = (
|
|||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
endArrowhead: opts.endArrowhead,
|
endArrowhead: opts.endArrowhead || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -477,7 +477,7 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
|
|||||||
* utility wrapper to generate new id. In test env it reuses the old + postfix
|
* utility wrapper to generate new id. In test env it reuses the old + postfix
|
||||||
* for test assertions.
|
* for test assertions.
|
||||||
*/
|
*/
|
||||||
const regenerateId = (
|
export const regenerateId = (
|
||||||
/** supply null if no previous id exists */
|
/** supply null if no previous id exists */
|
||||||
previousId: string | null,
|
previousId: string | null,
|
||||||
) => {
|
) => {
|
||||||
|
@ -89,16 +89,23 @@ export const redrawTextBoundingBox = (
|
|||||||
container,
|
container,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
);
|
);
|
||||||
|
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||||
|
|
||||||
let nextHeight = container.height;
|
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (metrics.height > maxContainerHeight) {
|
||||||
nextHeight = computeContainerDimensionForBoundText(
|
const nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
|
if (metrics.width > maxContainerWidth) {
|
||||||
|
const nextWidth = computeContainerDimensionForBoundText(
|
||||||
|
metrics.width,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
mutateElement(container, { width: nextWidth });
|
||||||
|
}
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
@ -859,8 +866,9 @@ const VALID_CONTAINER_TYPES = new Set([
|
|||||||
"arrow",
|
"arrow",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
export const isValidTextContainer = (element: {
|
||||||
VALID_CONTAINER_TYPES.has(element.type);
|
type: ExcalidrawElement["type"];
|
||||||
|
}) => VALID_CONTAINER_TYPES.has(element.type);
|
||||||
|
|
||||||
export const computeContainerDimensionForBoundText = (
|
export const computeContainerDimensionForBoundText = (
|
||||||
dimension: number,
|
dimension: number,
|
||||||
|
@ -86,15 +86,15 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
|||||||
|
|
||||||
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
|
type: "embeddable";
|
||||||
/**
|
/**
|
||||||
* indicates whether the embeddable src (url) has been validated for rendering.
|
* indicates whether the embeddable src (url) has been validated for rendering.
|
||||||
* nullish value indicates that the validation is pending. We reset the
|
* null value indicates that the validation is pending. We reset the
|
||||||
* value on each restore (or url change) so that we can guarantee
|
* value on each restore (or url change) so that we can guarantee
|
||||||
* the validation came from a trusted source (the editor). Also because we
|
* the validation came from a trusted source (the editor). Also because we
|
||||||
* may not have access to host-app supplied url validator during restore.
|
* may not have access to host-app supplied url validator during restore.
|
||||||
*/
|
*/
|
||||||
validated?: boolean;
|
validated: boolean | null;
|
||||||
type: "embeddable";
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||||
@ -123,7 +123,6 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
|||||||
export type ExcalidrawGenericElement =
|
export type ExcalidrawGenericElement =
|
||||||
| ExcalidrawSelectionElement
|
| ExcalidrawSelectionElement
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawEmbeddableElement
|
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement;
|
| ExcalidrawEllipseElement;
|
||||||
|
|
||||||
@ -138,7 +137,8 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawLinearElement
|
| ExcalidrawLinearElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawFrameElement;
|
| ExcalidrawFrameElement
|
||||||
|
| ExcalidrawEmbeddableElement;
|
||||||
|
|
||||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
@ -75,6 +75,7 @@ const {
|
|||||||
WelcomeScreen,
|
WelcomeScreen,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
|
convertToExcalidrawElements,
|
||||||
} = window.ExcalidrawLib;
|
} = window.ExcalidrawLib;
|
||||||
|
|
||||||
const COMMENT_ICON_DIMENSION = 32;
|
const COMMENT_ICON_DIMENSION = 32;
|
||||||
@ -140,7 +141,10 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
initialStatePromiseRef.current.promise.resolve(initialData);
|
initialStatePromiseRef.current.promise.resolve({
|
||||||
|
...initialData,
|
||||||
|
elements: convertToExcalidrawElements(initialData.elements),
|
||||||
|
});
|
||||||
excalidrawAPI.addFiles(imagesArray);
|
excalidrawAPI.addFiles(imagesArray);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -184,38 +188,40 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
const updateScene = () => {
|
const updateScene = () => {
|
||||||
const sceneData = {
|
const sceneData = {
|
||||||
elements: restoreElements(
|
elements: restoreElements(
|
||||||
[
|
convertToExcalidrawElements([
|
||||||
{
|
{
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
version: 141,
|
id: "rect-1",
|
||||||
versionNonce: 361174001,
|
|
||||||
isDeleted: false,
|
|
||||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
|
||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
|
||||||
angle: 0,
|
angle: 0,
|
||||||
x: 100.50390625,
|
x: 100.50390625,
|
||||||
y: 93.67578125,
|
y: 93.67578125,
|
||||||
strokeColor: "#c92a2a",
|
strokeColor: "#c92a2a",
|
||||||
backgroundColor: "transparent",
|
|
||||||
width: 186.47265625,
|
width: 186.47265625,
|
||||||
height: 141.9765625,
|
height: 141.9765625,
|
||||||
seed: 1968410350,
|
seed: 1968410350,
|
||||||
groupIds: [],
|
|
||||||
frameId: null,
|
|
||||||
boundElements: null,
|
|
||||||
locked: false,
|
|
||||||
link: null,
|
|
||||||
updated: 1,
|
|
||||||
roundness: {
|
roundness: {
|
||||||
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
||||||
value: 32,
|
value: 32,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
type: "arrow",
|
||||||
|
x: 300,
|
||||||
|
y: 150,
|
||||||
|
start: { id: "rect-1" },
|
||||||
|
end: { type: "ellipse" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
x: 300,
|
||||||
|
y: 100,
|
||||||
|
text: "HELLO WORLD!",
|
||||||
|
},
|
||||||
|
]),
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
appState: {
|
appState: {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -253,3 +253,4 @@ export { LiveCollaborationTrigger };
|
|||||||
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
||||||
|
|
||||||
export { normalizeLink } from "../../data/url";
|
export { normalizeLink } from "../../data/url";
|
||||||
|
export { convertToExcalidrawElements } from "../../data/transform";
|
||||||
|
@ -2,8 +2,8 @@ const path = require("path");
|
|||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const autoprefixer = require("autoprefixer");
|
const autoprefixer = require("autoprefixer");
|
||||||
const { parseEnvVariables } = require("./env");
|
const { parseEnvVariables } = require("./env");
|
||||||
|
|
||||||
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
|
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "development",
|
mode: "development",
|
||||||
devtool: false,
|
devtool: false,
|
||||||
@ -17,7 +17,6 @@ module.exports = {
|
|||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
|
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
|
||||||
assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
|
assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
|
||||||
|
|
||||||
publicPath: "",
|
publicPath: "",
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -45,7 +44,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: "import-meta-loader",
|
loader: "import-meta-loader",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const autoprefixer = require("autoprefixer");
|
||||||
|
const { parseEnvVariables } = require("./env");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const BundleAnalyzerPlugin =
|
const BundleAnalyzerPlugin =
|
||||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
const autoprefixer = require("autoprefixer");
|
|
||||||
const webpack = require("webpack");
|
|
||||||
const { parseEnvVariables } = require("./env");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: "production",
|
mode: "production",
|
||||||
@ -47,8 +47,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude:
|
exclude:
|
||||||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
|
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: "import-meta-loader",
|
loader: "import-meta-loader",
|
||||||
|
@ -140,9 +140,8 @@ describe("restoreElements", () => {
|
|||||||
expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) });
|
expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when arrow element has defined endArrowHead", () => {
|
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => {
|
||||||
const arrowElement = API.createElement({ type: "arrow" });
|
const arrowElement = API.createElement({ type: "arrow" });
|
||||||
|
|
||||||
const restoredElements = restore.restoreElements([arrowElement], null);
|
const restoredElements = restore.restoreElements([arrowElement], null);
|
||||||
|
|
||||||
const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
|
const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
|
||||||
@ -150,7 +149,7 @@ describe("restoreElements", () => {
|
|||||||
expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead);
|
expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when arrow element has undefined endArrowHead", () => {
|
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is undefined', () => {
|
||||||
const arrowElement = API.createElement({ type: "arrow" });
|
const arrowElement = API.createElement({ type: "arrow" });
|
||||||
Object.defineProperty(arrowElement, "endArrowhead", {
|
Object.defineProperty(arrowElement, "endArrowhead", {
|
||||||
get: vi.fn(() => undefined),
|
get: vi.fn(() => undefined),
|
||||||
|
1
src/tests/fixtures/elementFixture.ts
vendored
1
src/tests/fixtures/elementFixture.ts
vendored
@ -34,6 +34,7 @@ export const rectangleFixture: ExcalidrawElement = {
|
|||||||
export const embeddableFixture: ExcalidrawElement = {
|
export const embeddableFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
type: "embeddable",
|
type: "embeddable",
|
||||||
|
validated: null,
|
||||||
};
|
};
|
||||||
export const ellipseFixture: ExcalidrawElement = {
|
export const ellipseFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
|
@ -15,7 +15,11 @@ import fs from "fs";
|
|||||||
import util from "util";
|
import util from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
import { newFreeDrawElement, newImageElement } from "../../element/newElement";
|
import {
|
||||||
|
newEmbeddableElement,
|
||||||
|
newFreeDrawElement,
|
||||||
|
newImageElement,
|
||||||
|
} from "../../element/newElement";
|
||||||
import { Point } from "../../types";
|
import { Point } from "../../types";
|
||||||
import { getSelectedElements } from "../../scene/selection";
|
import { getSelectedElements } from "../../scene/selection";
|
||||||
import { isLinearElementType } from "../../element/typeChecks";
|
import { isLinearElementType } from "../../element/typeChecks";
|
||||||
@ -178,14 +182,20 @@ export class API {
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
case "embeddable":
|
|
||||||
element = newElement({
|
element = newElement({
|
||||||
type: type as "rectangle" | "diamond" | "ellipse" | "embeddable",
|
type: type as "rectangle" | "diamond" | "ellipse",
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
...base,
|
...base,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "embeddable":
|
||||||
|
element = newEmbeddableElement({
|
||||||
|
type: "embeddable",
|
||||||
|
...base,
|
||||||
|
validated: null,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "text":
|
case "text":
|
||||||
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
|
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
|
||||||
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
|
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
|
||||||
|
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…
x
Reference in New Issue
Block a user