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