1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2025-02-18 13:29:36 +01:00

fix: filter out elements not overlapping frame on paste (#7591)

This commit is contained in:
David Luzar 2024-01-21 20:55:57 +01:00 committed by GitHub
parent 4997624a3a
commit 740a165452
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 198 additions and 7 deletions

@ -349,6 +349,7 @@ import {
isElementInFrame,
getFrameLikeTitle,
getElementsOverlappingFrame,
filterElementsEligibleAsFrameChildren,
} from "../frame";
import {
excludeElementsInFramesFromSelection,
@ -3107,7 +3108,11 @@ class App extends React.Component<AppProps, AppState> {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
if (topLayerFrame) {
addElementsToFrame(allElements, newElements, topLayerFrame);
const eligibleElements = filterElementsEligibleAsFrameChildren(
newElements,
topLayerFrame,
);
addElementsToFrame(allElements, eligibleElements, topLayerFrame);
}
this.scene.replaceAllElements(allElements);

@ -107,17 +107,16 @@ export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(frame);
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return (
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2
frameX1 <= elementX1 &&
frameY1 <= elementY1 &&
frameX2 >= elementX2 &&
frameY2 >= elementY2
);
};
@ -372,6 +371,56 @@ export const getContainingFrame = (
// --------------------------- Frame Operations -------------------------------
/** */
export const filterElementsEligibleAsFrameChildren = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement,
) => {
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
elements = omitGroupsContainingFrameLikes(elements);
for (const element of elements) {
if (isFrameLikeElement(element) && element.id !== frame.id) {
otherFrames.add(element.id);
}
}
const processedGroups = new Set<ExcalidrawElement["id"]>();
const eligibleElements: ExcalidrawElement[] = [];
for (const element of elements) {
// don't add frames or their children
if (
isFrameLikeElement(element) ||
(element.frameId && otherFrames.has(element.frameId))
) {
continue;
}
if (element.groupIds.length) {
const shallowestGroupId = element.groupIds.at(-1)!;
if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId);
const groupElements = getElementsInGroup(elements, shallowestGroupId);
if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) {
for (const child of groupElements) {
eligibleElements.push(child);
}
}
}
} else {
const overlaps = elementOverlapsWithFrame(element, frame);
if (overlaps) {
eligibleElements.push(element);
}
}
}
return eligibleElements;
};
/**
* Retains (or repairs for target frame) the ordering invriant where children
* elements come right before the parent frame:

@ -292,4 +292,141 @@ describe("pasting & frames", () => {
expect(h.elements[1].frameId).toBe(frame.id);
});
});
it("should filter out elements not overlapping frame", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 100,
y: 100,
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(3);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(null);
});
});
it("should not filter out elements not overlapping frame if part of group", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
groupIds: ["g1"],
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 100,
y: 100,
groupIds: ["g1"],
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(3);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(frame.id);
});
});
it("should not filter out other frames and their children", async () => {
const frame = API.createElement({
type: "frame",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rect = API.createElement({
type: "rectangle",
width: 50,
height: 50,
groupIds: ["g1"],
});
const frame2 = API.createElement({
type: "frame",
width: 75,
height: 75,
x: 0,
y: 0,
});
const rect2 = API.createElement({
type: "rectangle",
width: 50,
height: 50,
x: 55,
y: 55,
frameId: frame2.id,
});
h.elements = [frame];
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2, frame2],
files: null,
});
mouse.moveTo(90, 90);
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(4);
expect(h.elements[1].type).toBe(rect.type);
expect(h.elements[1].frameId).toBe(frame.id);
expect(h.elements[2].type).toBe(rect2.type);
expect(h.elements[2].frameId).toBe(h.elements[3].id);
expect(h.elements[3].type).toBe(frame2.type);
expect(h.elements[3].frameId).toBe(null);
});
});
});