浏览代码

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>
Tom Dohnal 5 年之前
父节点
当前提交
f3ef93e9ce

+ 36 - 23
src/components/App.tsx

@@ -1818,29 +1818,6 @@ export class App extends React.Component<any, AppState> {
             );
             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 {
@@ -1990,6 +1967,8 @@ export class App extends React.Component<any, AppState> {
       resizeArrowFn = fn;
     };
 
+    let selectedElementWasDuplicated = false;
+
     const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
       const target = event.target;
       if (!(target instanceof HTMLElement)) {
@@ -2082,6 +2061,40 @@ export class App extends React.Component<any, AppState> {
           });
           lastX = x;
           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;
         }
       }

+ 3 - 3
src/index.tsx

@@ -7,12 +7,12 @@ import { IsMobileProvider } from "./is-mobile";
 import { App } from "./components/App";
 import "./styles.scss";
 
-const SentyEnvHostnameMap: { [key: string]: string } = {
+const SentryEnvHostnameMap: { [key: string]: string } = {
   "excalidraw.com": "production",
   "now.sh": "staging",
 };
 
-const onlineEnv = Object.keys(SentyEnvHostnameMap).find(
+const onlineEnv = Object.keys(SentryEnvHostnameMap).find(
   (item) => window.location.hostname.indexOf(item) >= 0,
 );
 
@@ -21,7 +21,7 @@ Sentry.init({
   dsn: onlineEnv
     ? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
     : undefined,
-  environment: onlineEnv ? SentyEnvHostnameMap[onlineEnv] : undefined,
+  environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
   release: process.env.REACT_APP_GIT_SHA,
   integrations: [
     new SentryIntegrations.CaptureConsole({

+ 8 - 8
src/tests/__snapshots__/move.test.tsx.snap

@@ -6,16 +6,16 @@ Object {
   "backgroundColor": "transparent",
   "fillStyle": "hachure",
   "height": 50,
-  "id": "id1",
+  "id": "id2",
   "isDeleted": false,
   "opacity": 100,
   "roughness": 1,
-  "seed": 453191,
+  "seed": 2019559783,
   "strokeColor": "#000000",
   "strokeWidth": 1,
   "type": "rectangle",
-  "version": 2,
-  "versionNonce": 1278240551,
+  "version": 4,
+  "versionNonce": 1150084233,
   "width": 30,
   "x": 30,
   "y": 20,
@@ -36,11 +36,11 @@ Object {
   "strokeColor": "#000000",
   "strokeWidth": 1,
   "type": "rectangle",
-  "version": 3,
-  "versionNonce": 2019559783,
+  "version": 5,
+  "versionNonce": 1014066025,
   "width": 30,
-  "x": 0,
-  "y": 40,
+  "x": -10,
+  "y": 60,
 }
 `;
 

+ 12 - 12
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -34,7 +34,7 @@ Object {
   "scrolledOutside": false,
   "selectedElementIds": Object {
     "id0": true,
-    "id2": true,
+    "id1": true,
   },
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
@@ -51,16 +51,16 @@ Object {
   "backgroundColor": "transparent",
   "fillStyle": "hachure",
   "height": 10,
-  "id": "id1",
+  "id": "id2",
   "isDeleted": false,
   "opacity": 100,
   "roughness": 1,
-  "seed": 453191,
+  "seed": 2019559783,
   "strokeColor": "#000000",
   "strokeWidth": 1,
   "type": "rectangle",
-  "version": 2,
-  "versionNonce": 1278240551,
+  "version": 4,
+  "versionNonce": 1150084233,
   "width": 10,
   "x": 10,
   "y": 10,
@@ -82,7 +82,7 @@ Object {
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 3,
-  "versionNonce": 2019559783,
+  "versionNonce": 401146281,
   "width": 10,
   "x": 20,
   "y": 20,
@@ -147,7 +147,7 @@ Object {
         "name": "Untitled-201933152653",
         "selectedElementIds": Object {
           "id0": true,
-          "id2": true,
+          "id1": true,
         },
         "viewBackgroundColor": "#ffffff",
       },
@@ -157,16 +157,16 @@ Object {
           "backgroundColor": "transparent",
           "fillStyle": "hachure",
           "height": 10,
-          "id": "id1",
+          "id": "id2",
           "isDeleted": false,
           "opacity": 100,
           "roughness": 1,
-          "seed": 453191,
+          "seed": 2019559783,
           "strokeColor": "#000000",
           "strokeWidth": 1,
           "type": "rectangle",
-          "version": 3,
-          "versionNonce": 1278240551,
+          "version": 5,
+          "versionNonce": 1150084233,
           "width": 10,
           "x": 10,
           "y": 10,
@@ -185,7 +185,7 @@ Object {
           "strokeWidth": 1,
           "type": "rectangle",
           "version": 4,
-          "versionNonce": 2019559783,
+          "versionNonce": 401146281,
           "width": 10,
           "x": 20,
           "y": 20,

+ 9 - 4
src/tests/move.test.tsx

@@ -74,17 +74,22 @@ describe("duplicate element on move when ALT is clicked", () => {
       renderScene.mockClear();
     }
 
-    fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20, altKey: true });
-    fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
+    fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
+    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);
 
-    expect(renderScene).toHaveBeenCalledTimes(3);
+    expect(renderScene).toHaveBeenCalledTimes(5);
     expect(h.state.selectionElement).toBeNull();
     expect(h.elements.length).toEqual(2);
 
     // previous element should stay intact
     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());
   });