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:
Tom Dohnal 2020-04-11 13:37:43 +02:00 committed by GitHub
parent 5ca763cdbb
commit f3ef93e9ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 68 additions and 50 deletions

View File

@ -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;
} }
} }

View File

@ -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({

View File

@ -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,
} }
`; `;

View File

@ -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,

View File

@ -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());
}); });