Allow to drag THEN press alt to duplicate (#1373)
* fix typo * duplicate elements when alt is pressed on pointer move * document use case Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
5ca763cdbb
commit
f3ef93e9ce
@ -1818,29 +1818,6 @@ export class App extends React.Component<any, AppState> {
|
|||||||
);
|
);
|
||||||
hitElementWasAddedToSelection = true;
|
hitElementWasAddedToSelection = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We duplicate the selected element if alt is pressed on pointer down
|
|
||||||
if (event.altKey) {
|
|
||||||
// Move the currently selected elements to the top of the z index stack, and
|
|
||||||
// put the duplicates where the selected elements used to be.
|
|
||||||
const nextElements = [];
|
|
||||||
const elementsToAppend = [];
|
|
||||||
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
|
||||||
if (
|
|
||||||
this.state.selectedElementIds[element.id] ||
|
|
||||||
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
|
||||||
) {
|
|
||||||
nextElements.push(duplicateElement(element));
|
|
||||||
elementsToAppend.push(element);
|
|
||||||
} else {
|
|
||||||
nextElements.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalSceneState.replaceAllElements([
|
|
||||||
...nextElements,
|
|
||||||
...elementsToAppend,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1990,6 +1967,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
resizeArrowFn = fn;
|
resizeArrowFn = fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let selectedElementWasDuplicated = false;
|
||||||
|
|
||||||
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
@ -2082,6 +2061,40 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
lastX = x;
|
lastX = x;
|
||||||
lastY = y;
|
lastY = y;
|
||||||
|
|
||||||
|
// We duplicate the selected element if alt is pressed on pointer move
|
||||||
|
if (event.altKey && !selectedElementWasDuplicated) {
|
||||||
|
// Move the currently selected elements to the top of the z index stack, and
|
||||||
|
// put the duplicates where the selected elements used to be.
|
||||||
|
// (the origin point where the dragging started)
|
||||||
|
|
||||||
|
selectedElementWasDuplicated = true;
|
||||||
|
|
||||||
|
const nextElements = [];
|
||||||
|
const elementsToAppend = [];
|
||||||
|
for (const element of globalSceneState.getElementsIncludingDeleted()) {
|
||||||
|
if (
|
||||||
|
this.state.selectedElementIds[element.id] ||
|
||||||
|
// case: the state.selectedElementIds might not have been
|
||||||
|
// updated yet by the time this mousemove event is fired
|
||||||
|
(element.id === hitElement.id && hitElementWasAddedToSelection)
|
||||||
|
) {
|
||||||
|
const duplicatedElement = duplicateElement(element);
|
||||||
|
mutateElement(duplicatedElement, {
|
||||||
|
x: duplicatedElement.x + (originX - lastX),
|
||||||
|
y: duplicatedElement.y + (originY - lastY),
|
||||||
|
});
|
||||||
|
nextElements.push(duplicatedElement);
|
||||||
|
elementsToAppend.push(element);
|
||||||
|
} else {
|
||||||
|
nextElements.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalSceneState.replaceAllElements([
|
||||||
|
...nextElements,
|
||||||
|
...elementsToAppend,
|
||||||
|
]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,12 @@ import { IsMobileProvider } from "./is-mobile";
|
|||||||
import { App } from "./components/App";
|
import { App } from "./components/App";
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
|
||||||
const SentyEnvHostnameMap: { [key: string]: string } = {
|
const SentryEnvHostnameMap: { [key: string]: string } = {
|
||||||
"excalidraw.com": "production",
|
"excalidraw.com": "production",
|
||||||
"now.sh": "staging",
|
"now.sh": "staging",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onlineEnv = Object.keys(SentyEnvHostnameMap).find(
|
const onlineEnv = Object.keys(SentryEnvHostnameMap).find(
|
||||||
(item) => window.location.hostname.indexOf(item) >= 0,
|
(item) => window.location.hostname.indexOf(item) >= 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ Sentry.init({
|
|||||||
dsn: onlineEnv
|
dsn: onlineEnv
|
||||||
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
||||||
: undefined,
|
: undefined,
|
||||||
environment: onlineEnv ? SentyEnvHostnameMap[onlineEnv] : undefined,
|
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
||||||
release: process.env.REACT_APP_GIT_SHA,
|
release: process.env.REACT_APP_GIT_SHA,
|
||||||
integrations: [
|
integrations: [
|
||||||
new SentryIntegrations.CaptureConsole({
|
new SentryIntegrations.CaptureConsole({
|
||||||
|
@ -6,16 +6,16 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id1",
|
"id": "id2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 453191,
|
"seed": 2019559783,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 2,
|
"version": 4,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 1150084233,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -36,11 +36,11 @@ Object {
|
|||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 3,
|
"version": 5,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 1014066025,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 0,
|
"x": -10,
|
||||||
"y": 40,
|
"y": 60,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ Object {
|
|||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
"shouldCacheIgnoreZoom": false,
|
"shouldCacheIgnoreZoom": false,
|
||||||
@ -51,16 +51,16 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"id": "id1",
|
"id": "id2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 453191,
|
"seed": 2019559783,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 2,
|
"version": 4,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 1150084233,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -82,7 +82,7 @@ Object {
|
|||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -147,7 +147,7 @@ Object {
|
|||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
"selectedElementIds": Object {
|
"selectedElementIds": Object {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
"id2": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
"viewBackgroundColor": "#ffffff",
|
"viewBackgroundColor": "#ffffff",
|
||||||
},
|
},
|
||||||
@ -157,16 +157,16 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"id": "id1",
|
"id": "id2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 453191,
|
"seed": 2019559783,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 3,
|
"version": 5,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 1150084233,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -185,7 +185,7 @@ Object {
|
|||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
|
@ -74,17 +74,22 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
renderScene.mockClear();
|
renderScene.mockClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20, altKey: true });
|
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40, altKey: true });
|
||||||
|
|
||||||
|
// firing another pointerMove event with alt key pressed should NOT trigger
|
||||||
|
// another duplication
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40, altKey: true });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: 10, clientY: 60 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
expect(renderScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
|
|
||||||
// previous element should stay intact
|
// previous element should stay intact
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toEqual([0, 40]);
|
expect([h.elements[1].x, h.elements[1].y]).toEqual([-10, 60]);
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user