瀏覽代碼

alternatvie: keep lasso drag to only mobile

Ryan Di 1 月之前
父節點
當前提交
85dc55c718
共有 2 個文件被更改,包括 53 次插入13 次删除
  1. 12 0
      packages/common/src/constants.ts
  2. 41 13
      packages/excalidraw/components/App.tsx

+ 12 - 0
packages/common/src/constants.ts

@@ -25,6 +25,18 @@ export const isIOS =
 export const isBrave = () =>
   (navigator as any).brave?.isBrave?.name === "isBrave";
 
+// Mobile user agent detection
+export const isMobileUA =
+  /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
+    navigator.userAgent.toLowerCase(),
+  );
+
+// Mobile platform detection
+export const isMobilePlatform =
+  /android|ios|iphone|ipad|ipod|blackberry|windows phone/i.test(
+    navigator.platform.toLowerCase(),
+  );
+
 export const supportsResizeObserver =
   typeof window !== "undefined" && "ResizeObserver" in window;
 

+ 41 - 13
packages/excalidraw/components/App.tsx

@@ -100,6 +100,8 @@ import {
   randomInteger,
   CLASSES,
   Emitter,
+  isMobileUA,
+  isMobilePlatform,
 } from "@excalidraw/common";
 
 import {
@@ -2386,6 +2388,19 @@ class App extends React.Component<AppProps, AppState> {
     }
   };
 
+  private isMobileOrTablet = (): boolean => {
+    // Touch + pointer accuracy
+    const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
+    const hasCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
+    const isTouchMobile = hasTouch && hasCoarsePointer;
+
+    // At least two indicators should be true
+    const indicators = [isMobileUA, isTouchMobile, isMobilePlatform];
+    const hasMultipleIndicators = indicators.filter(Boolean).length >= 2;
+
+    return hasMultipleIndicators;
+  };
+
   private isMobileBreakpoint = (width: number, height: number) => {
     return (
       width < MQ_MAX_WIDTH_PORTRAIT ||
@@ -6013,7 +6028,7 @@ class App extends React.Component<AppProps, AppState> {
     if (
       hasDeselectedButton ||
       (this.state.activeTool.type !== "selection" &&
-        this.state.activeTool.type !== "lasso" &&
+        (this.state.activeTool.type !== "lasso" || !this.isMobileOrTablet()) &&
         this.state.activeTool.type !== "text" &&
         this.state.activeTool.type !== "eraser")
     ) {
@@ -6187,7 +6202,7 @@ class App extends React.Component<AppProps, AppState> {
           ) {
             if (
               this.state.activeTool.type !== "lasso" ||
-              selectedElements.length > 0
+              (selectedElements.length > 0 && this.isMobileOrTablet())
             ) {
               setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
             }
@@ -6301,7 +6316,8 @@ class App extends React.Component<AppProps, AppState> {
           ) {
             if (
               this.state.activeTool.type !== "lasso" ||
-              Object.keys(this.state.selectedElementIds).length > 0
+              (Object.keys(this.state.selectedElementIds).length > 0 &&
+                this.isMobileOrTablet())
             ) {
               setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
             }
@@ -6315,7 +6331,8 @@ class App extends React.Component<AppProps, AppState> {
         ) {
           if (
             this.state.activeTool.type !== "lasso" ||
-            Object.keys(this.state.selectedElementIds).length > 0
+            (Object.keys(this.state.selectedElementIds).length > 0 &&
+              this.isMobileOrTablet())
           ) {
             setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
           }
@@ -6583,13 +6600,16 @@ class App extends React.Component<AppProps, AppState> {
       const hitSelectedElement =
         pointerDownState.hit.element &&
         this.isASelectedElement(pointerDownState.hit.element);
+      const isMobileOrTablet = this.isMobileOrTablet();
 
-      // Start a new lasso ONLY if we're not interacting with an existing
+      // On PCs, we always want to start a new lasso path even when we're hitting some elements
+      // Otherwise, start a new lasso ONLY if we're not interacting with an existing
       // selection (move/resize/rotate).
       if (
-        !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
-        !pointerDownState.resize.handleType &&
-        !hitSelectedElement
+        !isMobileOrTablet ||
+        (!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
+          !pointerDownState.resize.handleType &&
+          !hitSelectedElement)
       ) {
         this.lassoTrail.startPath(
           pointerDownState.origin.x,
@@ -6598,8 +6618,13 @@ class App extends React.Component<AppProps, AppState> {
         );
       }
 
-      // For lasso tool, if we hit an element, select it immediately like normal selection
-      if (pointerDownState.hit.element && !hitSelectedElement) {
+      // When mobile & tablet, for lasso tool
+      // if we hit an element, select it immediately like normal selection
+      if (
+        isMobileOrTablet &&
+        pointerDownState.hit.element &&
+        !hitSelectedElement
+      ) {
         this.setState((prevState) => {
           const nextSelectedElementIds: { [id: string]: true } = {
             ...prevState.selectedElementIds,
@@ -7141,7 +7166,7 @@ class App extends React.Component<AppProps, AppState> {
   ): boolean => {
     if (
       this.state.activeTool.type === "selection" ||
-      this.state.activeTool.type === "lasso"
+      (this.state.activeTool.type === "lasso" && this.isMobileOrTablet())
     ) {
       const elements = this.scene.getNonDeletedElements();
       const elementsMap = this.scene.getNonDeletedElementsMap();
@@ -8387,7 +8412,8 @@ class App extends React.Component<AppProps, AppState> {
         (hasHitASelectedElement ||
           pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements ||
           (this.state.activeTool.type === "lasso" &&
-            pointerDownState.hit.element)) &&
+            pointerDownState.hit.element &&
+            this.isMobileOrTablet())) &&
         !isSelectingPointsInLineEditor &&
         (this.state.activeTool.type !== "lasso" ||
           pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements ||
@@ -8437,7 +8463,9 @@ class App extends React.Component<AppProps, AppState> {
           selectedElements.length > 0 &&
           !pointerDownState.withCmdOrCtrl &&
           !this.state.editingTextElement &&
-          this.state.activeEmbeddable?.state !== "active"
+          this.state.activeEmbeddable?.state !== "active" &&
+          // for lasso tool, only allow dragging on mobile or tablet devices
+          (this.state.activeTool.type !== "lasso" || this.isMobileOrTablet())
         ) {
           const dragOffset = {
             x: pointerCoords.x - pointerDownState.drag.origin.x,