mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
fix: deselected hit element being duplicated + incorrect re-seeding (#9333)
* fix: deselected hit element being duplicated + incorrect re-seeding * snapshots * Fix alt-drag binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Add alt-drag bound arrow test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
ce267aa0d3
commit
c2caf78e95
5 changed files with 259 additions and 12 deletions
|
@ -55,6 +55,7 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBindableElement,
|
isBindableElement,
|
||||||
|
isBindingElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
|
@ -1422,7 +1423,7 @@ const getLinearElementEdgeCoors = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fixBindingsAfterDuplication = (
|
export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
newElements: ExcalidrawElement[],
|
newElements: ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
||||||
|
@ -1493,6 +1494,196 @@ export const fixBindingsAfterDuplication = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fixReversedBindingsForBindables = (
|
||||||
|
original: ExcalidrawBindableElement,
|
||||||
|
duplicate: ExcalidrawBindableElement,
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
elementsWithClones: ExcalidrawElement[],
|
||||||
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
|
) => {
|
||||||
|
original.boundElements?.forEach((binding, idx) => {
|
||||||
|
if (binding.type !== "arrow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldArrow = elementsWithClones.find((el) => el.id === binding.id);
|
||||||
|
|
||||||
|
if (!isBindingElement(oldArrow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalElements.has(binding.id)) {
|
||||||
|
// Linked arrow is in the selection, so find the duplicate pair
|
||||||
|
const newArrowId = oldIdToDuplicatedId.get(binding.id) ?? binding.id;
|
||||||
|
const newArrow = elementsWithClones.find(
|
||||||
|
(el) => el.id === newArrowId,
|
||||||
|
)! as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
mutateElement(newArrow, {
|
||||||
|
startBinding:
|
||||||
|
oldArrow.startBinding?.elementId === binding.id
|
||||||
|
? {
|
||||||
|
...oldArrow.startBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: newArrow.startBinding,
|
||||||
|
endBinding:
|
||||||
|
oldArrow.endBinding?.elementId === binding.id
|
||||||
|
? {
|
||||||
|
...oldArrow.endBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: newArrow.endBinding,
|
||||||
|
});
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
boundElements: [
|
||||||
|
...(duplicate.boundElements ?? []).filter(
|
||||||
|
(el) => el.id !== binding.id && el.id !== newArrowId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: newArrowId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Linked arrow is outside the selection,
|
||||||
|
// so we move the binding to the duplicate
|
||||||
|
mutateElement(oldArrow, {
|
||||||
|
startBinding:
|
||||||
|
oldArrow.startBinding?.elementId === original.id
|
||||||
|
? {
|
||||||
|
...oldArrow.startBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: oldArrow.startBinding,
|
||||||
|
endBinding:
|
||||||
|
oldArrow.endBinding?.elementId === original.id
|
||||||
|
? {
|
||||||
|
...oldArrow.endBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: oldArrow.endBinding,
|
||||||
|
});
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
boundElements: [
|
||||||
|
...(duplicate.boundElements ?? []),
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: oldArrow.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mutateElement(original, {
|
||||||
|
boundElements:
|
||||||
|
original.boundElements?.filter((_, i) => i !== idx) ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fixReversedBindingsForArrows = (
|
||||||
|
original: ExcalidrawArrowElement,
|
||||||
|
duplicate: ExcalidrawArrowElement,
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
bindingProp: "startBinding" | "endBinding",
|
||||||
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
|
elementsWithClones: ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const oldBindableId = original[bindingProp]?.elementId;
|
||||||
|
|
||||||
|
if (oldBindableId) {
|
||||||
|
if (originalElements.has(oldBindableId)) {
|
||||||
|
// Linked element is in the selection
|
||||||
|
const newBindableId =
|
||||||
|
oldIdToDuplicatedId.get(oldBindableId) ?? oldBindableId;
|
||||||
|
const newBindable = elementsWithClones.find(
|
||||||
|
(el) => el.id === newBindableId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
[bindingProp]: {
|
||||||
|
...original[bindingProp],
|
||||||
|
elementId: newBindableId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mutateElement(newBindable, {
|
||||||
|
boundElements: [
|
||||||
|
...(newBindable.boundElements ?? []).filter(
|
||||||
|
(el) => el.id !== original.id && el.id !== duplicate.id,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: duplicate.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Linked element is outside the selection
|
||||||
|
const originalBindable = elementsWithClones.find(
|
||||||
|
(el) => el.id === oldBindableId,
|
||||||
|
);
|
||||||
|
if (originalBindable) {
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
[bindingProp]: original[bindingProp],
|
||||||
|
});
|
||||||
|
mutateElement(original, {
|
||||||
|
[bindingProp]: null,
|
||||||
|
});
|
||||||
|
mutateElement(originalBindable, {
|
||||||
|
boundElements: [
|
||||||
|
...(originalBindable.boundElements?.filter(
|
||||||
|
(el) => el.id !== original.id,
|
||||||
|
) ?? []),
|
||||||
|
{
|
||||||
|
id: duplicate.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fixReversedBindings = (
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
elementsWithClones: ExcalidrawElement[],
|
||||||
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
|
) => {
|
||||||
|
for (const original of originalElements.values()) {
|
||||||
|
const duplicate = elementsWithClones.find(
|
||||||
|
(el) => el.id === oldIdToDuplicatedId.get(original.id),
|
||||||
|
)!;
|
||||||
|
|
||||||
|
if (isBindableElement(original) && isBindableElement(duplicate)) {
|
||||||
|
fixReversedBindingsForBindables(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
elementsWithClones,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
} else if (isArrowElement(original) && isArrowElement(duplicate)) {
|
||||||
|
fixReversedBindingsForArrows(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
"startBinding",
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
elementsWithClones,
|
||||||
|
);
|
||||||
|
fixReversedBindingsForArrows(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
"endBinding",
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
elementsWithClones,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const fixBindingsAfterDeletion = (
|
export const fixBindingsAfterDeletion = (
|
||||||
sceneElements: readonly ExcalidrawElement[],
|
sceneElements: readonly ExcalidrawElement[],
|
||||||
deletedElements: readonly ExcalidrawElement[],
|
deletedElements: readonly ExcalidrawElement[],
|
||||||
|
|
|
@ -36,7 +36,10 @@ import {
|
||||||
|
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
|
||||||
import { fixBindingsAfterDuplication } from "./binding";
|
import {
|
||||||
|
fixDuplicatedBindingsAfterDuplication,
|
||||||
|
fixReversedBindings,
|
||||||
|
} from "./binding";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -381,12 +384,20 @@ export const duplicateElements = (
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fixBindingsAfterDuplication(
|
fixDuplicatedBindingsAfterDuplication(
|
||||||
newElements,
|
newElements,
|
||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (reverseOrder) {
|
||||||
|
fixReversedBindings(
|
||||||
|
_idsOfElementsToDuplicate,
|
||||||
|
elementsWithClones,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bindElementsToFramesAfterDuplication(
|
bindElementsToFramesAfterDuplication(
|
||||||
elementsWithClones,
|
elementsWithClones,
|
||||||
oldElements,
|
oldElements,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
|
@ -699,4 +699,34 @@ describe("duplication z-order", () => {
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
{ id: text.id, containerId: arrow.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating bindable element with bound arrow should keep the arrow on the duplicate", () => {
|
||||||
|
const rect = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: -100,
|
||||||
|
y: 50,
|
||||||
|
width: 95,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(5, 5);
|
||||||
|
mouse.up(15, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.h.elements).toHaveLength(3);
|
||||||
|
|
||||||
|
const newRect = window.h.elements[0];
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(newRect.id);
|
||||||
|
expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -99,6 +99,7 @@ import {
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
type EXPORT_IMAGE_TYPES,
|
type EXPORT_IMAGE_TYPES,
|
||||||
|
randomInteger,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -8521,20 +8522,26 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
|
// hit element may not end up being selected
|
||||||
|
// if we're alt-dragging a common bounding box
|
||||||
|
// over the hit element
|
||||||
|
pointerDownState.hit.wasAddedToSelection &&
|
||||||
!selectedElements.find((el) => el.id === hitElement.id)
|
!selectedElements.find((el) => el.id === hitElement.id)
|
||||||
) {
|
) {
|
||||||
selectedElements.push(hitElement);
|
selectedElements.push(hitElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const idsOfElementsToDuplicate = new Map(
|
||||||
|
selectedElements.map((el) => [el.id, el]),
|
||||||
|
);
|
||||||
|
|
||||||
const { newElements: clonedElements, elementsWithClones } =
|
const { newElements: clonedElements, elementsWithClones } =
|
||||||
duplicateElements({
|
duplicateElements({
|
||||||
type: "in-place",
|
type: "in-place",
|
||||||
elements,
|
elements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
idsOfElementsToDuplicate: new Map(
|
idsOfElementsToDuplicate,
|
||||||
selectedElements.map((el) => [el.id, el]),
|
|
||||||
),
|
|
||||||
overrides: (el) => {
|
overrides: (el) => {
|
||||||
const origEl = pointerDownState.originalElements.get(el.id);
|
const origEl = pointerDownState.originalElements.get(el.id);
|
||||||
|
|
||||||
|
@ -8542,6 +8549,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return {
|
return {
|
||||||
x: origEl.x,
|
x: origEl.x,
|
||||||
y: origEl.y,
|
y: origEl.y,
|
||||||
|
seed: origEl.seed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8561,7 +8569,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const nextSceneElements = syncMovedIndices(
|
const nextSceneElements = syncMovedIndices(
|
||||||
mappedNewSceneElements || elementsWithClones,
|
mappedNewSceneElements || elementsWithClones,
|
||||||
arrayToMap(clonedElements),
|
arrayToMap(clonedElements),
|
||||||
);
|
).map((el) => {
|
||||||
|
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||||
|
return newElementWith(el, {
|
||||||
|
seed: randomInteger(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextSceneElements);
|
this.scene.replaceAllElements(nextSceneElements);
|
||||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
|
|
|
@ -20,7 +20,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 238820263,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
|
@ -54,14 +54,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 1505387817,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 6,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 915032327,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
|
|
Loading…
Add table
Reference in a new issue