소스 검색

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
Aakansha Doshi 1 년 전
부모
커밋
f5c91c3a0f
5개의 변경된 파일1240개의 추가작업 그리고 29개의 파일을 삭제
  1. 1073 21
      src/data/__snapshots__/transform.test.ts.snap
  2. 84 0
      src/data/transform.test.ts
  3. 72 6
      src/data/transform.ts
  4. 4 2
      src/element/newElement.ts
  5. 7 0
      src/packages/excalidraw/example/initialData.tsx

+ 1073 - 21
src/data/__snapshots__/transform.test.ts.snap

@@ -1,16 +1,151 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "fillStyle": "solid",
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 100,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "#fff3bf",
+  "boundElements": [
+    {
+      "id": "id34",
+      "type": "text",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 96,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "diamond",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 340,
+  "x": 120,
+  "y": 20,
+}
+`;
+
+exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 126,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "name": "My frame",
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "frame",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "width": 470,
+  "x": 0,
+  "y": 0,
+}
+`;
+
+exports[`Test Transform > Test Frames > should transform frames and update frame ids when regenerated 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": null,
+  "containerId": "2",
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 30,
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 37.5,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO EXCALIDRAW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#099268",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HELLO EXCALIDRAW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 160,
+  "x": 210,
+  "y": 49.25,
+}
+`;
+
 exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = `
 {
   "angle": 0,
   "backgroundColor": "#d8f5a2",
   "boundElements": [
     {
-      "id": "id41",
+      "id": "id45",
       "type": "arrow",
     },
     {
-      "id": "id42",
+      "id": "id46",
       "type": "arrow",
     },
   ],
@@ -45,7 +180,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id42",
+      "id": "id46",
       "type": "arrow",
     },
   ],
@@ -110,7 +245,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "seed": Any<Number>,
   "startArrowhead": null,
   "startBinding": {
-    "elementId": "id43",
+    "elementId": "id47",
     "focus": -0.08139534883720931,
     "gap": 1,
   },
@@ -186,7 +321,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id41",
+      "id": "id45",
       "type": "arrow",
     },
   ],
@@ -222,7 +357,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "baseline": 0,
   "boundElements": [
     {
-      "id": "id44",
+      "id": "id48",
       "type": "arrow",
     },
   ],
@@ -266,7 +401,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "baseline": 0,
   "boundElements": [
     {
-      "id": "id44",
+      "id": "id48",
       "type": "arrow",
     },
   ],
@@ -309,7 +444,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id45",
+      "id": "id49",
       "type": "text",
     },
   ],
@@ -367,7 +502,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
   "backgroundColor": "transparent",
   "baseline": 0,
   "boundElements": null,
-  "containerId": "id44",
+  "containerId": "id48",
   "fillStyle": "solid",
   "fontFamily": 1,
   "fontSize": 20,
@@ -406,13 +541,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id34",
+      "id": "id38",
       "type": "text",
     },
   ],
   "endArrowhead": "arrow",
   "endBinding": {
-    "elementId": "id36",
+    "elementId": "id40",
     "focus": 0,
     "gap": 1,
   },
@@ -441,7 +576,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "seed": Any<Number>,
   "startArrowhead": null,
   "startBinding": {
-    "elementId": "id35",
+    "elementId": "id39",
     "focus": 0,
     "gap": 1,
   },
@@ -464,7 +599,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "backgroundColor": "transparent",
   "baseline": 0,
   "boundElements": null,
-  "containerId": "id33",
+  "containerId": "id37",
   "fillStyle": "solid",
   "fontFamily": 1,
   "fontSize": 20,
@@ -503,7 +638,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id33",
+      "id": "id37",
       "type": "arrow",
     },
   ],
@@ -538,7 +673,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id33",
+      "id": "id37",
       "type": "arrow",
     },
   ],
@@ -573,13 +708,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "backgroundColor": "transparent",
   "boundElements": [
     {
-      "id": "id38",
+      "id": "id42",
       "type": "text",
     },
   ],
   "endArrowhead": "arrow",
   "endBinding": {
-    "elementId": "id40",
+    "elementId": "id44",
     "focus": 0,
     "gap": 1,
   },
@@ -608,7 +743,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "seed": Any<Number>,
   "startArrowhead": null,
   "startBinding": {
-    "elementId": "id39",
+    "elementId": "id43",
     "focus": 0,
     "gap": 1,
   },
@@ -631,7 +766,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "backgroundColor": "transparent",
   "baseline": 0,
   "boundElements": null,
-  "containerId": "id37",
+  "containerId": "id41",
   "fillStyle": "solid",
   "fontFamily": 1,
   "fontSize": 20,
@@ -671,7 +806,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "baseline": 0,
   "boundElements": [
     {
-      "id": "id37",
+      "id": "id41",
       "type": "arrow",
     },
   ],
@@ -715,7 +850,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
   "baseline": 0,
   "boundElements": [
     {
-      "id": "id37",
+      "id": "id41",
       "type": "arrow",
     },
   ],
@@ -782,6 +917,141 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
 }
 `;
 
+exports[`Test Transform > should transform frames and update frame ids when regenerated 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "fillStyle": "solid",
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 100,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 10,
+  "y": 10,
+}
+`;
+
+exports[`Test Transform > should transform frames and update frame ids when regenerated 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "#fff3bf",
+  "boundElements": [
+    {
+      "id": "id34",
+      "type": "text",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 96,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "diamond",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 340,
+  "x": 120,
+  "y": 20,
+}
+`;
+
+exports[`Test Transform > should transform frames and update frame ids when regenerated 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 126,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "name": "My frame",
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "frame",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "width": 470,
+  "x": 0,
+  "y": 0,
+}
+`;
+
+exports[`Test Transform > should transform frames and update frame ids when regenerated 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": null,
+  "containerId": "2",
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 30,
+  "frameId": "id33",
+  "groupIds": [],
+  "height": 37.5,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO EXCALIDRAW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#099268",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HELLO EXCALIDRAW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 160,
+  "x": 210,
+  "y": 49.25,
+}
+`;
+
 exports[`Test Transform > should transform linear elements 1`] = `
 {
   "angle": 0,
@@ -2030,3 +2300,785 @@ CONTAINER",
   "y": 522.5735931288071,
 }
 `;
+
+exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "#d8f5a2",
+  "boundElements": [
+    {
+      "id": "id43",
+      "type": "arrow",
+    },
+    {
+      "id": "id44",
+      "type": "arrow",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 300,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#66a80f",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 300,
+  "x": 630,
+  "y": 316,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id44",
+      "type": "arrow",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 100,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#9c36b5",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "diamond",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "width": 140,
+  "x": 96,
+  "y": 400,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "ellipse-1",
+    "focus": -0.008153707962747813,
+    "gap": 1,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 35,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0.5,
+      0.5,
+    ],
+    [
+      394.5,
+      34.5,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id45",
+    "focus": -0.08139534883720931,
+    "gap": 1,
+  },
+  "strokeColor": "#1864ab",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 395,
+  "x": 247,
+  "y": 420,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "ellipse-1",
+    "focus": 0.10666666666666667,
+    "gap": 3.834326468444573,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 0,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0.5,
+      0,
+    ],
+    [
+      399.5,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "diamond-1",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#e67700",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 400,
+  "x": 227,
+  "y": 450,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id43",
+      "type": "arrow",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 300,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "width": 300,
+  "x": -53,
+  "y": 270,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": [
+    {
+      "id": "id46",
+      "type": "arrow",
+    },
+  ],
+  "containerId": null,
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HEYYYYY",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#c2255c",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HEYYYYY",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "top",
+  "width": 70,
+  "x": 185,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": [
+    {
+      "id": "id46",
+      "type": "arrow",
+    },
+  ],
+  "containerId": null,
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "Whats up ?",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "Whats up ?",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "top",
+  "width": 100,
+  "x": 560,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id47",
+      "type": "text",
+    },
+  ],
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "text-2",
+    "focus": 0,
+    "gap": 205,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 0,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0.5,
+      0,
+    ],
+    [
+      99.5,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "text-1",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 255,
+  "y": 239,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": null,
+  "containerId": "id46",
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 130,
+  "x": 240,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id36",
+      "type": "text",
+    },
+  ],
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "id38",
+    "focus": 0,
+    "gap": 1,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 0,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0.5,
+      0,
+    ],
+    [
+      99.5,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id37",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 255,
+  "y": 239,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": null,
+  "containerId": "id35",
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 130,
+  "x": 240,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id35",
+      "type": "arrow",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 100,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 155,
+  "y": 189,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id35",
+      "type": "arrow",
+    },
+  ],
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 100,
+  "id": Any<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 355,
+  "y": 189,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id40",
+      "type": "text",
+    },
+  ],
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "id42",
+    "focus": 0,
+    "gap": 1,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 0,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0.5,
+      0,
+    ],
+    [
+      99.5,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id41",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 255,
+  "y": 239,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": null,
+  "containerId": "id39",
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 130,
+  "x": 240,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": [
+    {
+      "id": "id39",
+      "type": "arrow",
+    },
+  ],
+  "containerId": null,
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HEYYYYY",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "HEYYYYY",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "top",
+  "width": 70,
+  "x": 185,
+  "y": 226.5,
+}
+`;
+
+exports[`Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "baseline": 0,
+  "boundElements": [
+    {
+      "id": "id39",
+      "type": "arrow",
+    },
+  ],
+  "containerId": null,
+  "fillStyle": "solid",
+  "fontFamily": 1,
+  "fontSize": 20,
+  "frameId": null,
+  "groupIds": [],
+  "height": 25,
+  "id": Any<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "WHATS UP ?",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "text": "WHATS UP ?",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "top",
+  "width": 100,
+  "x": 355,
+  "y": 226.5,
+}
+`;
+
+exports[`should not allow duplicate ids 1`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 200,
+  "id": "rect-1",
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "width": 100,
+  "x": 300,
+  "y": 100,
+}
+`;

+ 84 - 0
src/data/transform.test.ts

@@ -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 = [

+ 72 - 6
src/data/transform.ts

@@ -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();
 };

+ 4 - 2
src/element/newElement.ts

@@ -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 - 0
src/packages/excalidraw/example/initialData.tsx

@@ -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,