diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index c84d80577..7a67cf0a1 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -55,6 +55,7 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { isArrowElement, isBindableElement, + isBindingElement, isBoundToContainer, isElbowArrow, isFixedPointBinding, @@ -1422,7 +1423,7 @@ const getLinearElementEdgeCoors = ( ); }; -export const fixBindingsAfterDuplication = ( +export const fixDuplicatedBindingsAfterDuplication = ( newElements: ExcalidrawElement[], oldIdToDuplicatedId: Map, duplicatedElementsMap: NonDeletedSceneElementsMap, @@ -1493,6 +1494,196 @@ export const fixBindingsAfterDuplication = ( } }; +const fixReversedBindingsForBindables = ( + original: ExcalidrawBindableElement, + duplicate: ExcalidrawBindableElement, + originalElements: Map, + elementsWithClones: ExcalidrawElement[], + oldIdToDuplicatedId: Map, +) => { + 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, + bindingProp: "startBinding" | "endBinding", + oldIdToDuplicatedId: Map, + 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, + elementsWithClones: ExcalidrawElement[], + oldIdToDuplicatedId: Map, +) => { + 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 = ( sceneElements: readonly ExcalidrawElement[], deletedElements: readonly ExcalidrawElement[], diff --git a/packages/element/src/duplicate.ts b/packages/element/src/duplicate.ts index 5b95f9085..ded1fd26c 100644 --- a/packages/element/src/duplicate.ts +++ b/packages/element/src/duplicate.ts @@ -36,7 +36,10 @@ import { import { getBoundTextElement, getContainerElement } from "./textElement"; -import { fixBindingsAfterDuplication } from "./binding"; +import { + fixDuplicatedBindingsAfterDuplication, + fixReversedBindings, +} from "./binding"; import type { ElementsMap, @@ -381,12 +384,20 @@ export const duplicateElements = ( // --------------------------------------------------------------------------- - fixBindingsAfterDuplication( + fixDuplicatedBindingsAfterDuplication( newElements, oldIdToDuplicatedId, duplicatedElementsMap as NonDeletedSceneElementsMap, ); + if (reverseOrder) { + fixReversedBindings( + _idsOfElementsToDuplicate, + elementsWithClones, + oldIdToDuplicatedId, + ); + } + bindElementsToFramesAfterDuplication( elementsWithClones, oldElements,