mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: keep orig elem in place on alt-duplication (#9403)
* fix: keep orig elem in place on alt-duplication * clarify comment * fix: incorrect selection on duplicating labeled containers * fix: duplicating within group outside frame should remove from group
This commit is contained in:
parent
0cf36d6b30
commit
a5d6939826
13 changed files with 603 additions and 579 deletions
|
@ -279,6 +279,7 @@ import {
|
|||
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectionStateForElements,
|
||||
makeNextSelectedElementIds,
|
||||
} from "@excalidraw/element/selection";
|
||||
|
||||
|
@ -3267,7 +3268,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||
|
||||
const { newElements } = duplicateElements({
|
||||
const { duplicatedElements } = duplicateElements({
|
||||
type: "everything",
|
||||
elements: elements.map((element) => {
|
||||
return newElementWith(element, {
|
||||
|
@ -3279,7 +3280,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
|
||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = [...prevElements, ...newElements];
|
||||
let nextElements = [...prevElements, ...duplicatedElements];
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
nextElements,
|
||||
|
@ -3288,13 +3289,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
nextElements = mappedNewSceneElements || nextElements;
|
||||
|
||||
syncMovedIndices(nextElements, arrayToMap(newElements));
|
||||
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
|
||||
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
if (topLayerFrame) {
|
||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||
newElements,
|
||||
duplicatedElements,
|
||||
topLayerFrame,
|
||||
);
|
||||
addElementsToFrame(
|
||||
|
@ -3307,7 +3308,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
|
||||
newElements.forEach((newElement) => {
|
||||
duplicatedElements.forEach((newElement) => {
|
||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||
const container = getContainerElement(
|
||||
newElement,
|
||||
|
@ -3323,7 +3324,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
||||
if (isSafari) {
|
||||
Fonts.loadElementsFonts(newElements).then((fontFaces) => {
|
||||
Fonts.loadElementsFonts(duplicatedElements).then((fontFaces) => {
|
||||
this.fonts.onLoaded(fontFaces);
|
||||
});
|
||||
}
|
||||
|
@ -3335,7 +3336,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.store.shouldCaptureIncrement();
|
||||
|
||||
const nextElementsToSelect =
|
||||
excludeElementsInFramesFromSelection(newElements);
|
||||
excludeElementsInFramesFromSelection(duplicatedElements);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
|
@ -3378,7 +3379,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setActiveTool({ type: "selection" });
|
||||
|
||||
if (opts.fitToContent) {
|
||||
this.scrollToContent(newElements, {
|
||||
this.scrollToContent(duplicatedElements, {
|
||||
fitToContent: true,
|
||||
canvasOffsets: this.getEditorUIOffsets(),
|
||||
});
|
||||
|
@ -6942,6 +6943,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
drag: {
|
||||
hasOccurred: false,
|
||||
offset: null,
|
||||
origin: { ...origin },
|
||||
},
|
||||
eventListeners: {
|
||||
onMove: null,
|
||||
|
@ -8236,8 +8238,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.activeEmbeddable?.state !== "active"
|
||||
) {
|
||||
const dragOffset = {
|
||||
x: pointerCoords.x - pointerDownState.origin.x,
|
||||
y: pointerCoords.y - pointerDownState.origin.y,
|
||||
x: pointerCoords.x - pointerDownState.drag.origin.x,
|
||||
y: pointerCoords.y - pointerDownState.drag.origin.y,
|
||||
};
|
||||
|
||||
const originalElements = [
|
||||
|
@ -8432,52 +8434,112 @@ class App extends React.Component<AppProps, AppState> {
|
|||
selectedElements.map((el) => [el.id, el]),
|
||||
);
|
||||
|
||||
const { newElements: clonedElements, elementsWithClones } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate,
|
||||
overrides: (el) => {
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
|
||||
if (origEl) {
|
||||
return {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
seed: origEl.seed,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
reverseOrder: true,
|
||||
});
|
||||
clonedElements.forEach((element) => {
|
||||
pointerDownState.originalElements.set(element.id, element);
|
||||
const {
|
||||
duplicatedElements,
|
||||
duplicateElementsMap,
|
||||
elementsWithDuplicates,
|
||||
origIdToDuplicateId,
|
||||
} = duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
appState: this.state,
|
||||
randomizeSeed: true,
|
||||
idsOfElementsToDuplicate,
|
||||
overrides: ({ duplicateElement, origElement }) => {
|
||||
return {
|
||||
// reset to the original element's frameId (unless we've
|
||||
// duplicated alongside a frame in which case we need to
|
||||
// keep the duplicate frame's id) so that the element
|
||||
// frame membership is refreshed on pointerup
|
||||
// NOTE this is a hacky solution and should be done
|
||||
// differently
|
||||
frameId: duplicateElement.frameId ?? origElement.frameId,
|
||||
seed: randomInteger(),
|
||||
};
|
||||
},
|
||||
});
|
||||
duplicatedElements.forEach((element) => {
|
||||
pointerDownState.originalElements.set(
|
||||
element.id,
|
||||
deepCopyElement(element),
|
||||
);
|
||||
});
|
||||
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
elementsWithClones,
|
||||
elements,
|
||||
);
|
||||
|
||||
const nextSceneElements = syncMovedIndices(
|
||||
mappedNewSceneElements || elementsWithClones,
|
||||
arrayToMap(clonedElements),
|
||||
).map((el) => {
|
||||
const mappedClonedElements = elementsWithDuplicates.map((el) => {
|
||||
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||
return newElementWith(el, {
|
||||
seed: randomInteger(),
|
||||
});
|
||||
const origEl = pointerDownState.originalElements.get(el.id);
|
||||
|
||||
if (origEl) {
|
||||
return newElementWith(el, {
|
||||
x: origEl.x,
|
||||
y: origEl.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
return el;
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(nextSceneElements);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||
mappedClonedElements,
|
||||
elements,
|
||||
);
|
||||
|
||||
const elementsWithIndices = syncMovedIndices(
|
||||
mappedNewSceneElements || mappedClonedElements,
|
||||
arrayToMap(duplicatedElements),
|
||||
);
|
||||
|
||||
// we need to update synchronously so as to keep pointerDownState,
|
||||
// appState, and scene elements in sync
|
||||
flushSync(() => {
|
||||
// swap hit element with the duplicated one
|
||||
if (pointerDownState.hit.element) {
|
||||
const cloneId = origIdToDuplicateId.get(
|
||||
pointerDownState.hit.element.id,
|
||||
);
|
||||
const clonedElement =
|
||||
cloneId && duplicateElementsMap.get(cloneId);
|
||||
pointerDownState.hit.element = clonedElement || null;
|
||||
}
|
||||
// swap hit elements with the duplicated ones
|
||||
pointerDownState.hit.allHitElements =
|
||||
pointerDownState.hit.allHitElements.reduce(
|
||||
(
|
||||
acc: typeof pointerDownState.hit.allHitElements,
|
||||
origHitElement,
|
||||
) => {
|
||||
const cloneId = origIdToDuplicateId.get(origHitElement.id);
|
||||
const clonedElement =
|
||||
cloneId && duplicateElementsMap.get(cloneId);
|
||||
if (clonedElement) {
|
||||
acc.push(clonedElement);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// update drag origin to the position at which we started
|
||||
// the duplication so that the drag offset is correct
|
||||
pointerDownState.drag.origin = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
);
|
||||
|
||||
// switch selected elements to the duplicated ones
|
||||
this.setState((prevState) => ({
|
||||
...getSelectionStateForElements(
|
||||
duplicatedElements,
|
||||
this.scene.getNonDeletedElements(),
|
||||
prevState,
|
||||
),
|
||||
}));
|
||||
|
||||
this.scene.replaceAllElements(elementsWithIndices);
|
||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue