Pārlūkot izejas kodu

Panning with space key (#579)

* Panning with space key

* prevent panning when selecting/dragging & add more checks

* Fix changing current tool via shortcut while panning

* Fix order of statements

* teardown on blur event

* Refactor cursor setting

Co-authored-by: David Luzar <luzar.david@gmail.com>
Guillermo Peralta Scura 5 gadi atpakaļ
vecāks
revīzija
35750d8d09
2 mainītis faili ar 70 papildinājumiem un 12 dzēšanām
  1. 69 12
      src/index.tsx
  2. 1 0
      src/keys.ts

+ 69 - 12
src/index.tsx

@@ -100,12 +100,27 @@ function resetCursor() {
   document.documentElement.style.cursor = "";
 }
 
+function setCursorForShape(shape: string) {
+  if (shape === "selection") {
+    resetCursor();
+  } else {
+    document.documentElement.style.cursor =
+      shape === "text" ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR;
+  }
+}
+
 const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
 const ELEMENT_TRANSLATE_AMOUNT = 1;
 const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
 const CURSOR_TYPE = {
   TEXT: "text",
   CROSSHAIR: "crosshair",
+  GRABBING: "grabbing",
+};
+const MOUSE_BUTTON = {
+  MAIN: 0,
+  WHEEL: 1,
+  SECONDARY: 2,
 };
 
 let lastCanvasWidth = -1;
@@ -141,6 +156,9 @@ function pickAppStatePropertiesForHistory(
 
 let cursorX = 0;
 let cursorY = 0;
+let isHoldingSpace: boolean = false;
+let isPanning: boolean = false;
+let isHoldingMouseButton: boolean = false;
 
 export class App extends React.Component<any, AppState> {
   canvas: HTMLCanvasElement | null = null;
@@ -225,6 +243,7 @@ export class App extends React.Component<any, AppState> {
   };
 
   private onUnload = () => {
+    isHoldingSpace = false;
     this.saveDebounced();
     this.saveDebounced.flush();
   };
@@ -269,9 +288,11 @@ export class App extends React.Component<any, AppState> {
     document.addEventListener("cut", this.onCut);
 
     document.addEventListener("keydown", this.onKeyDown, false);
+    document.addEventListener("keyup", this.onKeyUp, { passive: true });
     document.addEventListener("mousemove", this.updateCurrentCursorPosition);
     window.addEventListener("resize", this.onResize, false);
     window.addEventListener("unload", this.onUnload, false);
+    window.addEventListener("blur", this.onUnload, false);
 
     const searchParams = new URLSearchParams(window.location.search);
     const id = searchParams.get("id");
@@ -292,6 +313,7 @@ export class App extends React.Component<any, AppState> {
     );
     window.removeEventListener("resize", this.onResize, false);
     window.removeEventListener("unload", this.onUnload, false);
+    window.removeEventListener("blur", this.onUnload, false);
   }
 
   public state: AppState = getDefaultAppState();
@@ -356,11 +378,11 @@ export class App extends React.Component<any, AppState> {
       !event.metaKey &&
       this.state.draggingElement === null
     ) {
-      if (shape === "text") {
-        document.documentElement.style.cursor = CURSOR_TYPE.TEXT;
-      } else {
-        document.documentElement.style.cursor = CURSOR_TYPE.CROSSHAIR;
+      if (!isHoldingSpace) {
+        document.documentElement.style.cursor =
+          shape === "text" ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR;
       }
+      elements = clearSelection(elements);
       this.setState({ elementType: shape });
     } else if (event[KEYS.META] && event.code === "KeyZ") {
       event.preventDefault();
@@ -380,6 +402,25 @@ export class App extends React.Component<any, AppState> {
           this.setState(data.appState);
         }
       }
+    } else if (event.key === KEYS.SPACE && !isHoldingMouseButton) {
+      isHoldingSpace = true;
+      document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
+    }
+  };
+
+  private onKeyUp = (event: KeyboardEvent) => {
+    if (event.key === KEYS.SPACE) {
+      if (this.state.elementType === "selection") {
+        resetCursor();
+      } else {
+        elements = clearSelection(elements);
+        document.documentElement.style.cursor =
+          this.state.elementType === "text"
+            ? CURSOR_TYPE.TEXT
+            : CURSOR_TYPE.CROSSHAIR;
+        this.setState({});
+      }
+      isHoldingSpace = false;
     }
   };
 
@@ -783,11 +824,19 @@ export class App extends React.Component<any, AppState> {
                 lastMouseUp(e);
               }
 
-              // pan canvas on wheel button drag
-              if (e.button === 1) {
+              if (isPanning) return;
+
+              // pan canvas on wheel button drag or space+drag
+              if (
+                !isHoldingMouseButton &&
+                (e.button === MOUSE_BUTTON.WHEEL ||
+                  (e.button === MOUSE_BUTTON.MAIN && isHoldingSpace))
+              ) {
+                isHoldingMouseButton = true;
+                isPanning = true;
+                document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
                 let { clientX: lastX, clientY: lastY } = e;
                 const onMouseMove = (e: MouseEvent) => {
-                  document.documentElement.style.cursor = `grabbing`;
                   let deltaX = lastX - e.clientX;
                   let deltaY = lastY - e.clientY;
                   lastX = e.clientX;
@@ -799,22 +848,28 @@ export class App extends React.Component<any, AppState> {
                     scrollY: state.scrollY - deltaY,
                   }));
                 };
-                const onMouseUp = (lastMouseUp = (e: MouseEvent) => {
+                const teardown = (lastMouseUp = () => {
                   lastMouseUp = null;
-                  resetCursor();
+                  isPanning = false;
+                  isHoldingMouseButton = false;
+                  if (!isHoldingSpace) {
+                    setCursorForShape(this.state.elementType);
+                  }
                   history.resumeRecording();
                   window.removeEventListener("mousemove", onMouseMove);
-                  window.removeEventListener("mouseup", onMouseUp);
+                  window.removeEventListener("mouseup", teardown);
+                  window.removeEventListener("blur", teardown);
                 });
+                window.addEventListener("blur", teardown);
                 window.addEventListener("mousemove", onMouseMove, {
                   passive: true,
                 });
-                window.addEventListener("mouseup", onMouseUp);
+                window.addEventListener("mouseup", teardown);
                 return;
               }
 
               // only handle left mouse button
-              if (e.button !== 0) return;
+              if (e.button !== MOUSE_BUTTON.MAIN) return;
               // fixes mousemove causing selection of UI texts #32
               e.preventDefault();
               // Preventing the event above disables default behavior
@@ -1208,6 +1263,7 @@ export class App extends React.Component<any, AppState> {
                 } = this.state;
 
                 lastMouseUp = null;
+                isHoldingMouseButton = false;
                 window.removeEventListener("mousemove", onMouseMove);
                 window.removeEventListener("mouseup", onMouseUp);
 
@@ -1393,6 +1449,7 @@ export class App extends React.Component<any, AppState> {
               });
             }}
             onMouseMove={e => {
+              if (isHoldingSpace || isPanning) return;
               const hasDeselectedButton = Boolean(e.buttons);
               if (
                 hasDeselectedButton ||

+ 1 - 0
src/keys.ts

@@ -13,6 +13,7 @@ export const KEYS = {
       : "ctrlKey";
   },
   TAB: "Tab",
+  SPACE: " ",
 };
 
 export function isArrowKey(keyCode: string) {