diff --git a/src/index.tsx b/src/index.tsx index 2d74deb2..6787ed7c 100644 --- a/src/index.tsx +++ b/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 { canvas: HTMLCanvasElement | null = null; @@ -225,6 +243,7 @@ export class App extends React.Component { }; private onUnload = () => { + isHoldingSpace = false; this.saveDebounced(); this.saveDebounced.flush(); }; @@ -269,9 +288,11 @@ export class App extends React.Component { 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 { ); 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 { !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 { 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 { 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 { 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 { } = this.state; lastMouseUp = null; + isHoldingMouseButton = false; window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); @@ -1393,6 +1449,7 @@ export class App extends React.Component { }); }} onMouseMove={e => { + if (isHoldingSpace || isPanning) return; const hasDeselectedButton = Boolean(e.buttons); if ( hasDeselectedButton || diff --git a/src/keys.ts b/src/keys.ts index ad9c938d..ef859be7 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -13,6 +13,7 @@ export const KEYS = { : "ctrlKey"; }, TAB: "Tab", + SPACE: " ", }; export function isArrowKey(keyCode: string) {