Просмотр исходного кода

fix: cursor being leaked outside of canvas (#3161)

David Luzar 4 лет назад
Родитель
Сommit
c77c9ce65a

+ 2 - 2
src/actions/actionFinalize.tsx

@@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks";
 
 export const actionFinalize = register({
   name: "finalize",
-  perform: (elements, appState) => {
+  perform: (elements, appState, _, { canvas }) => {
     if (appState.editingLinearElement) {
       const {
         elementId,
@@ -126,7 +126,7 @@ export const actionFinalize = register({
       (!appState.elementLocked && appState.elementType !== "draw") ||
       !multiPointElement
     ) {
-      resetCursor();
+      resetCursor(canvas);
     }
     return {
       elements: newElements,

+ 3 - 1
src/components/Actions.tsx

@@ -151,10 +151,12 @@ const LIBRARY_ICON = (
 );
 
 export const ShapesSwitcher = ({
+  canvas,
   elementType,
   setAppState,
   isLibraryOpen,
 }: {
+  canvas: HTMLCanvasElement | null;
   elementType: ExcalidrawElement["type"];
   setAppState: React.Component<any, AppState>["setState"];
   isLibraryOpen: boolean;
@@ -185,7 +187,7 @@ export const ShapesSwitcher = ({
               multiElement: null,
               selectedElementIds: {},
             });
-            setCursorForShape(value);
+            setCursorForShape(canvas, value);
             setAppState({});
           }}
         />

+ 43 - 34
src/components/App.tsx

@@ -172,6 +172,7 @@ import {
   ResolvablePromise,
   resolvablePromise,
   sceneCoordsToViewportCoords,
+  setCursor,
   setCursorForShape,
   tupleToCoors,
   viewportCoordsToSceneCoords,
@@ -1440,16 +1441,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     }
     if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
       isHoldingSpace = true;
-      document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
+      setCursor(this.canvas, CURSOR_TYPE.GRABBING);
     }
   });
 
   private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
     if (event.key === KEYS.SPACE) {
       if (this.state.elementType === "selection") {
-        resetCursor();
+        resetCursor(this.canvas);
       } else {
-        setCursorForShape(this.state.elementType);
+        setCursorForShape(this.canvas, this.state.elementType);
         this.setState({
           selectedElementIds: {},
           selectedGroupIds: {},
@@ -1475,7 +1476,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
 
   private selectShapeTool(elementType: AppState["elementType"]) {
     if (!isHoldingSpace) {
-      setCursorForShape(elementType);
+      setCursorForShape(this.canvas, elementType);
     }
     if (isToolIcon(document.activeElement)) {
       document.activeElement.blur();
@@ -1601,7 +1602,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           editingElement: null,
         });
         if (this.state.elementLocked) {
-          setCursorForShape(this.state.elementType);
+          setCursorForShape(this.canvas, this.state.elementType);
         }
       }),
       element,
@@ -1782,7 +1783,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       return;
     }
 
-    resetCursor();
+    resetCursor(this.canvas);
 
     const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
       event,
@@ -1814,7 +1815,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       }
     }
 
-    resetCursor();
+    resetCursor(this.canvas);
 
     if (!event[KEYS.CTRL_OR_CMD]) {
       this.startTextEditing({
@@ -1880,9 +1881,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     const isOverScrollBar = isPointerOverScrollBars.isOverEither;
     if (!this.state.draggingElement && !this.state.multiElement) {
       if (isOverScrollBar) {
-        resetCursor();
+        resetCursor(this.canvas);
       } else {
-        setCursorForShape(this.state.elementType);
+        setCursorForShape(this.canvas, this.state.elementType);
       }
     }
 
@@ -1933,7 +1934,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       const { points, lastCommittedPoint } = multiElement;
       const lastPoint = points[points.length - 1];
 
-      setCursorForShape(this.state.elementType);
+      setCursorForShape(this.canvas, this.state.elementType);
 
       if (lastPoint === lastCommittedPoint) {
         // if we haven't yet created a temp point and we're beyond commit-zone
@@ -1950,7 +1951,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
             points: [...points, [scenePointerX - rx, scenePointerY - ry]],
           });
         } else {
-          document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
+          setCursor(this.canvas, CURSOR_TYPE.POINTER);
           // in this branch, we're inside the commit zone, and no uncommitted
           // point exists. Thus do nothing (don't add/remove points).
         }
@@ -1964,13 +1965,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           lastCommittedPoint[1],
         ) < LINE_CONFIRM_THRESHOLD
       ) {
-        document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
+        setCursor(this.canvas, CURSOR_TYPE.POINTER);
         mutateElement(multiElement, {
           points: points.slice(0, -1),
         });
       } else {
         if (isPathALoop(points, this.state.zoom.value)) {
-          document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
+          setCursor(this.canvas, CURSOR_TYPE.POINTER);
         }
         // update last uncommitted point
         mutateElement(multiElement, {
@@ -2013,8 +2014,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         elementWithTransformHandleType &&
         elementWithTransformHandleType.transformHandleType
       ) {
-        document.documentElement.style.cursor = getCursorForResizingElement(
-          elementWithTransformHandleType,
+        setCursor(
+          this.canvas,
+          getCursorForResizingElement(elementWithTransformHandleType),
         );
         return;
       }
@@ -2027,9 +2029,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         event.pointerType,
       );
       if (transformHandleType) {
-        document.documentElement.style.cursor = getCursorForResizingElement({
-          transformHandleType,
-        });
+        setCursor(
+          this.canvas,
+          getCursorForResizingElement({
+            transformHandleType,
+          }),
+        );
         return;
       }
     }
@@ -2039,11 +2044,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       scenePointer.y,
     );
     if (this.state.elementType === "text") {
-      document.documentElement.style.cursor = isTextElement(hitElement)
-        ? CURSOR_TYPE.TEXT
-        : CURSOR_TYPE.CROSSHAIR;
+      setCursor(
+        this.canvas,
+        isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
+      );
     } else if (isOverScrollBar) {
-      document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
+      setCursor(this.canvas, CURSOR_TYPE.AUTO);
     } else if (
       hitElement ||
       this.isHittingCommonBoundingBoxOfSelectedElements(
@@ -2051,9 +2057,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         selectedElements,
       )
     ) {
-      document.documentElement.style.cursor = CURSOR_TYPE.MOVE;
+      setCursor(this.canvas, CURSOR_TYPE.MOVE);
     } else {
-      document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
+      setCursor(this.canvas, CURSOR_TYPE.AUTO);
     }
   };
 
@@ -2226,7 +2232,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     let nextPastePrevented = false;
     const isLinux = /Linux/.test(window.navigator.platform);
 
-    document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
+    setCursor(this.canvas, CURSOR_TYPE.GRABBING);
     let { clientX: lastX, clientY: lastY } = event;
     const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
       const deltaX = lastX - event.clientX;
@@ -2278,7 +2284,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         lastPointerUp = null;
         isPanning = false;
         if (!isHoldingSpace) {
-          setCursorForShape(this.state.elementType);
+          setCursorForShape(this.canvas, this.state.elementType);
         }
         this.setState({
           cursorButton: "up",
@@ -2394,7 +2400,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
 
     const onPointerUp = withBatchedUpdates(() => {
       isDraggingScrollBar = false;
-      setCursorForShape(this.state.elementType);
+      setCursorForShape(this.canvas, this.state.elementType);
       lastPointerUp = null;
       this.setState({
         cursorButton: "up",
@@ -2457,9 +2463,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         );
       }
       if (pointerDownState.resize.handleType) {
-        document.documentElement.style.cursor = getCursorForResizingElement({
-          transformHandleType: pointerDownState.resize.handleType,
-        });
+        setCursor(
+          this.canvas,
+          getCursorForResizingElement({
+            transformHandleType: pointerDownState.resize.handleType,
+          }),
+        );
         pointerDownState.resize.isResizing = true;
         pointerDownState.resize.offset = tupleToCoors(
           getResizeOffsetXY(
@@ -2624,7 +2633,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       insertAtParentCenter: !event.altKey,
     });
 
-    resetCursor();
+    resetCursor(this.canvas);
     if (!this.state.elementLocked) {
       this.setState({
         elementType: "selection",
@@ -2681,7 +2690,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       mutateElement(multiElement, {
         lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
       });
-      document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
+      setCursor(this.canvas, CURSOR_TYPE.POINTER);
     } else {
       const [gridX, gridY] = getGridPoint(
         pointerDownState.origin.x,
@@ -3216,7 +3225,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           }
           this.setState({ suggestedBindings: [], startBoundElement: null });
           if (!elementLocked && elementType !== "draw") {
-            resetCursor();
+            resetCursor(this.canvas);
             this.setState((prevState) => ({
               draggingElement: null,
               elementType: "selection",
@@ -3387,7 +3396,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       }
 
       if (!elementLocked && elementType !== "draw") {
-        resetCursor();
+        resetCursor(this.canvas);
         this.setState({
           draggingElement: null,
           suggestedBindings: [],

+ 1 - 0
src/components/LayerUI.tsx

@@ -516,6 +516,7 @@ const LayerUI = ({
                       {heading}
                       <Stack.Row gap={1}>
                         <ShapesSwitcher
+                          canvas={canvas}
                           elementType={appState.elementType}
                           setAppState={setAppState}
                           isLibraryOpen={appState.isLibraryOpen}

+ 1 - 0
src/components/MobileMenu.tsx

@@ -57,6 +57,7 @@ export const MobileMenu = ({
                   {heading}
                   <Stack.Row gap={1}>
                     <ShapesSwitcher
+                      canvas={canvas}
                       elementType={appState.elementType}
                       setAppState={setAppState}
                       isLibraryOpen={appState.isLibraryOpen}

+ 0 - 2
src/components/TopErrorBoundary.tsx

@@ -1,6 +1,5 @@
 import React from "react";
 import * as Sentry from "@sentry/browser";
-import { resetCursor } from "../utils";
 import { t } from "../i18n";
 
 interface TopErrorBoundaryState {
@@ -24,7 +23,6 @@ export class TopErrorBoundary extends React.Component<
   }
 
   componentDidCatch(error: Error, errorInfo: any) {
-    resetCursor();
     const _localStorage: any = {};
     for (const [key, value] of Object.entries({ ...localStorage })) {
       try {

+ 0 - 8
src/element/resizeElements.ts

@@ -21,7 +21,6 @@ import {
 import { isLinearElement, isTextElement } from "./typeChecks";
 import { mutateElement } from "./mutateElement";
 import { getPerfectElementSize } from "./sizeHelpers";
-import { getCursorForResizingElement } from "./resizeTest";
 import { measureText, getFontString } from "../utils";
 import { updateBoundElements } from "./binding";
 import {
@@ -105,13 +104,6 @@ export const transformElements = (
       );
     }
 
-    // update cursor
-    // FIXME it is not very nice to have this here
-    document.documentElement.style.cursor = getCursorForResizingElement({
-      element,
-      transformHandleType,
-    });
-
     return true;
   } else if (selectedElements.length > 1) {
     if (transformHandleType === "rotation") {

+ 19 - 5
src/utils.ts

@@ -160,15 +160,29 @@ export const removeSelection = () => {
 
 export const distance = (x: number, y: number) => Math.abs(x - y);
 
-export const resetCursor = () => {
-  document.documentElement.style.cursor = "";
+export const resetCursor = (canvas: HTMLCanvasElement | null) => {
+  if (canvas) {
+    canvas.style.cursor = "";
+  }
+};
+
+export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
+  if (canvas) {
+    canvas.style.cursor = cursor;
+  }
 };
 
-export const setCursorForShape = (shape: string) => {
+export const setCursorForShape = (
+  canvas: HTMLCanvasElement | null,
+  shape: string,
+) => {
+  if (!canvas) {
+    return;
+  }
   if (shape === "selection") {
-    resetCursor();
+    resetCursor(canvas);
   } else {
-    document.documentElement.style.cursor = CURSOR_TYPE.CROSSHAIR;
+    canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
   }
 };