mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: filter out elements not overlapping frame on paste (#7591)
This commit is contained in:
parent
4997624a3a
commit
740a165452
3 changed files with 198 additions and 7 deletions
|
@ -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);
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue