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