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
|
@ -7,26 +7,17 @@ import {
|
|||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectedElements,
|
||||
getSelectionStateForElements,
|
||||
} from "@excalidraw/element/selection";
|
||||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { DuplicateIcon } from "../components/icons";
|
||||
|
||||
|
@ -65,52 +56,49 @@ export const actionDuplicateSelection = register({
|
|||
}
|
||||
}
|
||||
|
||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
),
|
||||
appState,
|
||||
randomizeSeed: true,
|
||||
overrides: (element) => ({
|
||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
reverseOrder: false,
|
||||
});
|
||||
),
|
||||
appState,
|
||||
randomizeSeed: true,
|
||||
overrides: ({ origElement, origIdToDuplicateId }) => {
|
||||
const duplicateFrameId =
|
||||
origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
|
||||
return {
|
||||
x: origElement.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: origElement.y + DEFAULT_GRID_SIZE / 2,
|
||||
frameId: duplicateFrameId ?? origElement.frameId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
if (app.props.onDuplicate && nextElements) {
|
||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
||||
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||
const mappedElements = app.props.onDuplicate(
|
||||
elementsWithDuplicates,
|
||||
elements,
|
||||
);
|
||||
if (mappedElements) {
|
||||
nextElements = mappedElements;
|
||||
elementsWithDuplicates = mappedElements;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
||||
elements: syncMovedIndices(
|
||||
elementsWithDuplicates,
|
||||
arrayToMap(duplicatedElements),
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...updateLinearElementEditors(duplicatedElements),
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||
duplicatedElements,
|
||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
getNonDeletedElements(nextElements),
|
||||
...getSelectionStateForElements(
|
||||
duplicatedElements,
|
||||
getNonDeletedElements(elementsWithDuplicates),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
@ -130,24 +118,3 @@ export const actionDuplicateSelection = register({
|
|||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
|
||||
const linears = clonedElements.filter(isLinearElement);
|
||||
if (linears.length === 1) {
|
||||
const linear = linears[0];
|
||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||
const onlySingleLinearSelected = clonedElements.every(
|
||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||
);
|
||||
|
||||
if (onlySingleLinearSelected) {
|
||||
return {
|
||||
selectedLinearElement: new LinearElementEditor(linear),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedLinearElement: null,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
|||
type: "everything",
|
||||
elements: item.elements,
|
||||
randomizeSeed: true,
|
||||
}).newElements,
|
||||
}).duplicatedElements,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,40 +1,6 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 50,
|
||||
"id": "id2",
|
||||
"index": "Zz",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
|
@ -54,13 +20,47 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1505387817,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"version": 5,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 50,
|
||||
"id": "id2",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1604849351,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 915032327,
|
||||
"width": 30,
|
||||
"x": -10,
|
||||
|
|
|
@ -2038,7 +2038,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||
"scrolledOutside": false,
|
||||
"searchMatches": [],
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
|
@ -2128,8 +2128,16 @@ History {
|
|||
HistoryEntry {
|
||||
"appStateChange": AppStateChange {
|
||||
"delta": Delta {
|
||||
"deleted": {},
|
||||
"inserted": {},
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id2": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"elementsChange": ElementsChange {
|
||||
|
@ -2145,7 +2153,7 @@ History {
|
|||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 10,
|
||||
"index": "Zz",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -2159,26 +2167,15 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {
|
||||
"id0" => Delta {
|
||||
"deleted": {
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -10378,13 +10375,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||
"scrolledOutside": false,
|
||||
"searchMatches": [],
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
"id6": true,
|
||||
"id8": true,
|
||||
"id9": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {
|
||||
"id4": true,
|
||||
"id7": true,
|
||||
},
|
||||
"selectedLinearElement": null,
|
||||
"selectionElement": null,
|
||||
|
@ -10648,8 +10645,26 @@ History {
|
|||
HistoryEntry {
|
||||
"appStateChange": AppStateChange {
|
||||
"delta": Delta {
|
||||
"deleted": {},
|
||||
"inserted": {},
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id6": true,
|
||||
"id8": true,
|
||||
"id9": true,
|
||||
},
|
||||
"selectedGroupIds": {
|
||||
"id7": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
"id1": true,
|
||||
"id2": true,
|
||||
},
|
||||
"selectedGroupIds": {
|
||||
"id4": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"elementsChange": ElementsChange {
|
||||
|
@ -10667,7 +10682,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zx",
|
||||
"index": "a3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10681,8 +10696,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -10700,7 +10715,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zy",
|
||||
"index": "a4",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10714,8 +10729,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
"x": 40,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -10733,7 +10748,7 @@ History {
|
|||
"id7",
|
||||
],
|
||||
"height": 10,
|
||||
"index": "Zz",
|
||||
"index": "a5",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
|
@ -10747,46 +10762,15 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
"x": 60,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {
|
||||
"id0" => Delta {
|
||||
"deleted": {
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
"id1" => Delta {
|
||||
"deleted": {
|
||||
"x": 40,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 30,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
"id2" => Delta {
|
||||
"deleted": {
|
||||
"x": 60,
|
||||
"y": 20,
|
||||
},
|
||||
"inserted": {
|
||||
"x": 50,
|
||||
"y": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": Map {},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -307,6 +307,41 @@ describe("pasting & frames", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should remove element from frame when pasted outside", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
frameId: frame.id,
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
});
|
||||
|
||||
API.setElements([frame]);
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(150, 150);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(2);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out elements not overlapping frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
|
|
|
@ -218,7 +218,7 @@ describe("Cropping and other features", async () => {
|
|||
initialHeight / 2,
|
||||
]);
|
||||
Keyboard.keyDown(KEYS.ESCAPE);
|
||||
const duplicatedImage = duplicateElement(null, new Map(), image, {});
|
||||
const duplicatedImage = duplicateElement(null, new Map(), image);
|
||||
act(() => {
|
||||
h.app.scene.insertElement(duplicatedImage);
|
||||
});
|
||||
|
|
|
@ -724,7 +724,8 @@ export type PointerDownState = Readonly<{
|
|||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
||||
// The previous pointer position
|
||||
lastCoords: { x: number; y: number };
|
||||
// map of original elements data
|
||||
// original element frozen snapshots so we can access the original
|
||||
// element attribute values at time of pointerdown
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||
resize: {
|
||||
// Handle when resizing, might change during the pointer interaction
|
||||
|
@ -758,6 +759,9 @@ export type PointerDownState = Readonly<{
|
|||
hasOccurred: boolean;
|
||||
// Might change during the pointer interaction
|
||||
offset: { x: number; y: number } | null;
|
||||
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||
// to current pointer position at time of duplication.
|
||||
origin: { x: number; y: number };
|
||||
};
|
||||
// We need to have these in the state so that we can unsubscribe them
|
||||
eventListeners: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue