mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-08-21 01:07:02 +02:00
Compare commits
5 Commits
aakansha/s
...
feat-add-e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
48c1f93e3e | ||
![]() |
3dd446dc15 | ||
![]() |
56c21529db | ||
![]() |
a13aed92f2 | ||
![]() |
134df7bfbb |
75
dev-docs/docs/codebase/json-schema.mdx
Normal file
75
dev-docs/docs/codebase/json-schema.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
# JSON Schema
|
||||
|
||||
The Excalidraw data format uses plaintext JSON.
|
||||
|
||||
## Excalidraw files
|
||||
|
||||
When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) is using the below format.
|
||||
|
||||
### Attributes
|
||||
|
||||
| Attribute | Description | Value |
|
||||
| --- | --- | --- |
|
||||
| `type` | The type of the Excalidraw schema | `"excalidraw"` |
|
||||
| `version` | The version of the Excalidraw schema | number |
|
||||
| `source` | The source URL of the Excalidraw application | `"https://excalidraw.com"` |
|
||||
| `elements` | An array of objects representing excalidraw elements on canvas | Array containing excalidraw element objects |
|
||||
| `appState` | Additional application state/configuration | Object containing application state properties |
|
||||
| `files` | Data for excalidraw `image` elements | Object containing image data |
|
||||
|
||||
### JSON Schema example
|
||||
|
||||
```json
|
||||
{
|
||||
// schema information
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
|
||||
// elements on canvas
|
||||
"elements": [
|
||||
// example element
|
||||
{
|
||||
"id": "pologsyG-tAraPgiN9xP9b",
|
||||
"type": "rectangle",
|
||||
"x": 928,
|
||||
"y": 319,
|
||||
"width": 134,
|
||||
"height": 90
|
||||
/* ...other element properties */
|
||||
}
|
||||
/* other elements */
|
||||
],
|
||||
|
||||
// editor state (canvas config, preferences, ...)
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
|
||||
// files data for "image" elements, using format `{ [fileId]: fileData }`
|
||||
"files": {
|
||||
// example of an image data object
|
||||
"3cebd7720911620a3938ce77243696149da03861": {
|
||||
"mimeType": "image/png",
|
||||
"id": "3cebd7720911620a3938c.77243626149da03861",
|
||||
"dataURL": "data:image/png;base64,iVBORWOKGgoAAAANSUhEUgA=",
|
||||
"created": 1690295874454,
|
||||
"lastRetrieved": 1690295874454
|
||||
}
|
||||
/* ...other image data objects */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Excalidraw clipboard format
|
||||
|
||||
When copying selected excalidraw elements to clipboard, the JSON schema is similar to `.excalidraw` format, except it differs in attributes.
|
||||
|
||||
### Attributes
|
||||
|
||||
| Attribute | Description | Example Value |
|
||||
| --- | --- | --- |
|
||||
| `type` | The type of the Excalidraw document. | "excalidraw/clipboard" |
|
||||
| `elements` | An array of objects representing excalidraw elements on canvas. | Array containing excalidraw element objects (see example below) |
|
||||
| `files` | Data for excalidraw `image` elements. | Object containing image data |
|
@@ -23,7 +23,6 @@ const sidebars = {
|
||||
},
|
||||
items: ["introduction/development", "introduction/contributing"],
|
||||
},
|
||||
|
||||
{
|
||||
type: "category",
|
||||
label: "@excalidraw/excalidraw",
|
||||
@@ -92,6 +91,11 @@ const sidebars = {
|
||||
"@excalidraw/excalidraw/development",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Codebase",
|
||||
items: ["codebase/json-schema"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@@ -6501,7 +6501,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
nextElements = updateFrameMembershipOfSelectedElements(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
nextElements,
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
|
@@ -92,7 +92,8 @@ const repairBinding = (binding: PointBinding | null) => {
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
/** @deprecated */
|
||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||
@@ -158,6 +159,9 @@ const restoreElementWithProperties = <
|
||||
locked: element.locked ?? false,
|
||||
};
|
||||
|
||||
if ("subtype" in element) {
|
||||
base.subtype = element.subtype;
|
||||
}
|
||||
if ("customData" in element) {
|
||||
base.customData = element.customData;
|
||||
}
|
||||
|
@@ -65,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
subtype?: string;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import {
|
||||
convertToExcalidrawElements,
|
||||
Excalidraw,
|
||||
} from "./packages/excalidraw/index";
|
||||
import { API } from "./tests/helpers/api";
|
||||
import { Pointer } from "./tests/helpers/ui";
|
||||
import { Keyboard, Pointer } from "./tests/helpers/ui";
|
||||
import { render } from "./tests/test-utils";
|
||||
|
||||
const { h } = window;
|
||||
@@ -28,83 +29,301 @@ describe("adding elements to frames", () => {
|
||||
}, []);
|
||||
};
|
||||
|
||||
describe("resizing frame over elements", () => {
|
||||
const testElements = async (
|
||||
containerType: "arrow" | "rectangle",
|
||||
initialOrder: ElementType[],
|
||||
expectedOrder: ElementType[],
|
||||
) => {
|
||||
await render(<Excalidraw />);
|
||||
function resizeFrameOverElement(
|
||||
frame: ExcalidrawElement,
|
||||
element: ExcalidrawElement,
|
||||
) {
|
||||
mouse.clickAt(0, 0);
|
||||
mouse.downAt(frame.x + frame.width, frame.y + frame.height);
|
||||
mouse.moveTo(
|
||||
element.x + element.width + 50,
|
||||
element.y + element.height + 50,
|
||||
);
|
||||
mouse.up();
|
||||
}
|
||||
|
||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||
function dragElementIntoFrame(
|
||||
frame: ExcalidrawElement,
|
||||
element: ExcalidrawElement,
|
||||
) {
|
||||
mouse.clickAt(element.x, element.y);
|
||||
mouse.downAt(element.x + element.width / 2, element.y + element.height / 2);
|
||||
mouse.moveTo(frame.x + frame.width / 2, frame.y + frame.height / 2);
|
||||
mouse.up();
|
||||
}
|
||||
|
||||
h.elements = reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
);
|
||||
function selectElementAndDuplicate(
|
||||
element: ExcalidrawElement,
|
||||
moveTo: [number, number] = [element.x + 25, element.y + 25],
|
||||
) {
|
||||
const [x, y] = [
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
];
|
||||
|
||||
assertOrder(h.elements, initialOrder);
|
||||
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
expect(h.elements[2].frameId).toBe(null);
|
||||
|
||||
const container = h.elements[1];
|
||||
|
||||
mouse.clickAt(0, 0);
|
||||
mouse.downAt(frame.x + frame.width, frame.y + frame.height);
|
||||
mouse.moveTo(
|
||||
container.x + container.width + 100,
|
||||
container.y + container.height + 100,
|
||||
);
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.downAt(x, y);
|
||||
mouse.moveTo(moveTo[0], moveTo[1]);
|
||||
mouse.up();
|
||||
assertOrder(h.elements, expectedOrder);
|
||||
});
|
||||
}
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
};
|
||||
function expectEqualIds(expected: ExcalidrawElement[]) {
|
||||
expect(h.elements.map((x) => x.id)).toEqual(expected.map((x) => x.id));
|
||||
}
|
||||
|
||||
it("resizing over text containers / labelled arrows", async () => {
|
||||
await testElements(
|
||||
let frame: ExcalidrawElement;
|
||||
let rect1: ExcalidrawElement;
|
||||
let rect2: ExcalidrawElement;
|
||||
let rect3: ExcalidrawElement;
|
||||
let rect4: ExcalidrawElement;
|
||||
let text: ExcalidrawElement;
|
||||
let arrow: ExcalidrawElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
|
||||
frame = API.createElement({ id: "id0", type: "frame", x: 0, width: 150 });
|
||||
rect1 = API.createElement({
|
||||
id: "id1",
|
||||
type: "rectangle",
|
||||
x: -1000,
|
||||
});
|
||||
rect2 = API.createElement({
|
||||
id: "id2",
|
||||
type: "rectangle",
|
||||
x: 200,
|
||||
width: 50,
|
||||
});
|
||||
rect3 = API.createElement({
|
||||
id: "id3",
|
||||
type: "rectangle",
|
||||
x: 400,
|
||||
width: 50,
|
||||
});
|
||||
rect4 = API.createElement({
|
||||
id: "id4",
|
||||
type: "rectangle",
|
||||
x: 1000,
|
||||
width: 50,
|
||||
});
|
||||
text = API.createElement({
|
||||
id: "id5",
|
||||
type: "text",
|
||||
x: 100,
|
||||
});
|
||||
arrow = API.createElement({
|
||||
id: "id6",
|
||||
type: "arrow",
|
||||
x: 100,
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
});
|
||||
|
||||
const commonTestCases = async (
|
||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||
) => {
|
||||
describe("when frame is in a layer below", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
h.elements = [frame, rect2, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [frame, rect1, rect2, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect1, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [frame, rect3, rect4, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when frame is in a layer above", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [rect2, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
h.elements = [rect2, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect4, rect1, rect3, rect2, frame]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when frame is in an inner layer", async () => {
|
||||
it("should add elements", async () => {
|
||||
h.elements = [rect2, frame, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect4, rect3, rect2, frame, rect1]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const resizingTest = async (
|
||||
containerType: "arrow" | "rectangle",
|
||||
initialOrder: ElementType[],
|
||||
expectedOrder: ElementType[],
|
||||
) => {
|
||||
await render(<Excalidraw />);
|
||||
|
||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||
|
||||
h.elements = reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
);
|
||||
|
||||
assertOrder(h.elements, initialOrder);
|
||||
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
expect(h.elements[2].frameId).toBe(null);
|
||||
|
||||
const container = h.elements[1];
|
||||
|
||||
resizeFrameOverElement(frame, container);
|
||||
assertOrder(h.elements, expectedOrder);
|
||||
|
||||
expect(h.elements[0].frameId).toBe(frame.id);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
};
|
||||
|
||||
describe("resizing frame over elements", async () => {
|
||||
await commonTestCases(resizeFrameOverElement);
|
||||
|
||||
it("resizing over text containers and labelled arrows", async () => {
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["frame", "rectangle", "text"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["frame", "text", "rectangle"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["rectangle", "text", "frame"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["text", "rectangle", "frame"],
|
||||
["text", "rectangle", "frame"],
|
||||
["rectangle", "text", "frame"],
|
||||
);
|
||||
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["frame", "arrow", "text"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
await testElements(
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["text", "arrow", "frame"],
|
||||
["text", "arrow", "frame"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
await resizingTest(
|
||||
"arrow",
|
||||
["frame", "arrow", "text"],
|
||||
["arrow", "text", "frame"],
|
||||
);
|
||||
|
||||
// FIXME failing in tests (it fails to add elements to frame for some
|
||||
@@ -118,11 +337,104 @@ describe("adding elements to frames", () => {
|
||||
// ["arrow", "text", "frame"],
|
||||
// ["arrow", "text", "frame"],
|
||||
// );
|
||||
// await testElements(
|
||||
// "arrow",
|
||||
// ["frame", "text", "arrow"],
|
||||
// ["text", "arrow", "frame"],
|
||||
// );
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer below", async () => {
|
||||
h.elements = [frame, arrow, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer above", async () => {
|
||||
h.elements = [arrow, text, frame];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||
h.elements = [arrow, frame, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
expect(arrow.frameId).toBe(frame.id);
|
||||
expect(text.frameId).toBe(frame.id);
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resizing frame over elements but downwards", async () => {
|
||||
it("should add elements when frame is in a layer below", async () => {
|
||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in a layer above", async () => {
|
||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in an inner layer", async () => {
|
||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expect(rect3.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dragging elements into the frame", async () => {
|
||||
await commonTestCases(dragElementIntoFrame);
|
||||
|
||||
it("should drag element inside, duplicate it and keep it in frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
selectElementAndDuplicate(rect2);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(frame.id);
|
||||
expectEqualIds([rect2_copy, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should drag element inside, duplicate it and remove it from frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
const rect2_copy = { ...rect2, id: `${rect2.id}_copy` };
|
||||
|
||||
// move the rect2 outside the frame
|
||||
selectElementAndDuplicate(rect2, [-1000, -1000]);
|
||||
|
||||
expect(rect2_copy.frameId).toBe(frame.id);
|
||||
expect(rect2.frameId).toBe(null);
|
||||
expectEqualIds([rect2_copy, frame, rect2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
77
src/frame.ts
77
src/frame.ts
@@ -468,14 +468,39 @@ export const addElementsToFrame = (
|
||||
}
|
||||
}
|
||||
|
||||
let nextElements = allElements.slice();
|
||||
const allElementsIndex = allElements.reduce(
|
||||
(acc: Record<string, number>, element, index) => {
|
||||
acc[element.id] = index;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const frameIndex = allElementsIndex[frame.id];
|
||||
// need to be calculated before the mutation below occurs
|
||||
const leftFrameBoundaryIndex = findIndex(
|
||||
allElements,
|
||||
(e) => e.frameId === frame.id,
|
||||
);
|
||||
|
||||
const existingFrameChildren = allElements.filter(
|
||||
(element) => element.frameId === frame.id,
|
||||
);
|
||||
|
||||
const addedFrameChildren_left: ExcalidrawElement[] = [];
|
||||
const addedFrameChildren_right: ExcalidrawElement[] = [];
|
||||
|
||||
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
||||
for (const element of omitGroupsContainingFrames(
|
||||
allElements,
|
||||
_elementsToAdd,
|
||||
)) {
|
||||
if (element.frameId !== frame.id && !isFrameElement(element)) {
|
||||
if (allElementsIndex[element.id] > frameIndex) {
|
||||
addedFrameChildren_right.push(element);
|
||||
} else {
|
||||
addedFrameChildren_left.push(element);
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
@@ -483,28 +508,35 @@ export const addElementsToFrame = (
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
|
||||
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
|
||||
|
||||
if (elementIndex < frameBoundary) {
|
||||
nextElements = [
|
||||
...nextElements.slice(0, elementIndex),
|
||||
...nextElements.slice(elementIndex + 1, frameBoundary),
|
||||
element,
|
||||
...nextElements.slice(frameBoundary),
|
||||
];
|
||||
} else if (elementIndex > frameIndex) {
|
||||
nextElements = [
|
||||
...nextElements.slice(0, frameIndex),
|
||||
element,
|
||||
...nextElements.slice(frameIndex, elementIndex),
|
||||
...nextElements.slice(elementIndex + 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frameElement = allElements[frameIndex];
|
||||
const nextFrameChildren = addedFrameChildren_left
|
||||
.concat(existingFrameChildren)
|
||||
.concat(addedFrameChildren_right);
|
||||
|
||||
const nextFrameChildrenMap = nextFrameChildren.reduce(
|
||||
(acc: Record<string, boolean>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const nextOtherElements_left = allElements
|
||||
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
|
||||
const nextOtherElement_right = allElements
|
||||
.slice(frameIndex + 1)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
|
||||
const nextElements = nextOtherElements_left
|
||||
.concat(nextFrameChildren)
|
||||
.concat([frameElement])
|
||||
.concat(nextOtherElement_right);
|
||||
|
||||
return nextElements;
|
||||
};
|
||||
|
||||
@@ -518,6 +550,7 @@ export const removeElementsFromFrame = (
|
||||
for (const element of elementsToRemove) {
|
||||
if (element.frameId) {
|
||||
_elementsToRemove.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.push(boundTextElement);
|
||||
@@ -566,7 +599,7 @@ export const replaceAllElementsInFrame = (
|
||||
);
|
||||
};
|
||||
|
||||
/** does not mutate elements, but return new ones */
|
||||
/** does not mutate elements, but returns new ones */
|
||||
export const updateFrameMembershipOfSelectedElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
appState: AppState,
|
||||
|
Reference in New Issue
Block a user