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

View file

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

View file

@ -107,17 +107,16 @@ export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
getElementAbsoluteCoords(frame);
const [elementX1, elementY1, elementX2, elementY2] = const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements); getCommonBounds(elements);
return ( return (
selectionX1 <= elementX1 && frameX1 <= elementX1 &&
selectionY1 <= elementY1 && frameY1 <= elementY1 &&
selectionX2 >= elementX2 && frameX2 >= elementX2 &&
selectionY2 >= elementY2 frameY2 >= elementY2
); );
}; };
@ -372,6 +371,56 @@ export const getContainingFrame = (
// --------------------------- Frame Operations ------------------------------- // --------------------------- 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 * Retains (or repairs for target frame) the ordering invriant where children
* elements come right before the parent frame: * elements come right before the parent frame:

View file

@ -292,4 +292,141 @@ describe("pasting & frames", () => {
expect(h.elements[1].frameId).toBe(frame.id); 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);
});
});
}); });