Browse Source

feat: stop storing selection element into `appState.draggingElement`

dwelle 1 năm trước cách đây
mục cha
commit
add575a419

+ 4 - 1
src/actions/actionFinalize.tsx

@@ -170,6 +170,7 @@ export const actionFinalize = register({
             : activeTool,
         activeEmbeddable: null,
         draggingElement: null,
+        selectionElement: null,
         multiElement: null,
         editingElement: null,
         startBoundElement: null,
@@ -196,7 +197,9 @@ export const actionFinalize = register({
   keyTest: (event, appState) =>
     (event.key === KEYS.ESCAPE &&
       (appState.editingLinearElement !== null ||
-        (!appState.draggingElement && appState.multiElement === null))) ||
+        (!appState.selectionElement &&
+          !appState.draggingElement &&
+          appState.multiElement === null))) ||
     ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
       appState.multiElement !== null),
   PanelComponent: ({ appState, updateData, data }) => (

+ 1 - 0
src/actions/actionHistory.tsx

@@ -21,6 +21,7 @@ const writeData = (
     !appState.multiElement &&
     !appState.resizingElement &&
     !appState.editingElement &&
+    !appState.selectionElement &&
     !appState.draggingElement
   ) {
     const data = updater();

+ 326 - 292
src/components/App.tsx

@@ -3014,7 +3014,8 @@ class App extends React.Component<AppProps, AppState> {
         !event.ctrlKey &&
         !event.altKey &&
         !event.metaKey &&
-        this.state.draggingElement === null
+        !this.state.draggingElement &&
+        !this.state.selectionElement
       ) {
         const shape = findShapeByKey(event.key);
         if (shape) {
@@ -3343,6 +3344,7 @@ class App extends React.Component<AppProps, AppState> {
 
         this.setState({
           draggingElement: null,
+          selectionElement: null,
           editingElement: null,
         });
         if (this.state.activeTool.locked) {
@@ -4421,8 +4423,7 @@ class App extends React.Component<AppProps, AppState> {
     // finger is lifted
     if (
       event.pointerType === "touch" &&
-      this.state.draggingElement &&
-      this.state.draggingElement.type === "freedraw"
+      this.state.draggingElement?.type === "freedraw"
     ) {
       const element = this.state.draggingElement as ExcalidrawFreeDrawElement;
       this.updateScene({
@@ -4434,6 +4435,7 @@ class App extends React.Component<AppProps, AppState> {
             }
           : {}),
         appState: {
+          selectionElement: null,
           draggingElement: null,
           editingElement: null,
           startBoundElement: null,
@@ -4561,13 +4563,16 @@ class App extends React.Component<AppProps, AppState> {
       // retrieve the latest element as the state may be stale
       const pendingImageElement =
         this.state.pendingImageElementId &&
-        this.scene.getElement(this.state.pendingImageElementId);
+        this.scene.getElement<ExcalidrawImageElement>(
+          this.state.pendingImageElementId,
+        );
 
       if (!pendingImageElement) {
         return;
       }
 
       this.setState({
+        selectionElement: null,
         draggingElement: pendingImageElement,
         editingElement: pendingImageElement,
         pendingImageElementId: null,
@@ -5374,6 +5379,7 @@ class App extends React.Component<AppProps, AppState> {
     );
     this.scene.addNewElement(element);
     this.setState({
+      selectionElement: null,
       draggingElement: element,
       editingElement: element,
       startBoundElement: boundElement,
@@ -5593,6 +5599,7 @@ class App extends React.Component<AppProps, AppState> {
 
       this.scene.addNewElement(element);
       this.setState({
+        selectionElement: null,
         draggingElement: element,
         editingElement: element,
         startBoundElement: boundElement,
@@ -5667,12 +5674,13 @@ class App extends React.Component<AppProps, AppState> {
     if (element.type === "selection") {
       this.setState({
         selectionElement: element,
-        draggingElement: element,
+        draggingElement: null,
       });
     } else {
       this.scene.addNewElement(element);
       this.setState({
         multiElement: null,
+        selectionElement: null,
         draggingElement: element,
         editingElement: element,
       });
@@ -5705,6 +5713,7 @@ class App extends React.Component<AppProps, AppState> {
 
     this.setState({
       multiElement: null,
+      selectionElement: null,
       draggingElement: frame,
       editingElement: frame,
     });
@@ -5763,7 +5772,9 @@ class App extends React.Component<AppProps, AppState> {
       if (this.maybeHandleResize(pointerDownState, event)) {
         return;
       }
-      this.maybeDragNewGenericElement(pointerDownState, event);
+      if (!this.maybeUpdateSelectionElement(pointerDownState, event)) {
+        this.maybeDragNewGenericElement(pointerDownState, event);
+      }
     });
   }
 
@@ -5776,7 +5787,9 @@ class App extends React.Component<AppProps, AppState> {
       if (this.maybeHandleResize(pointerDownState, event)) {
         return;
       }
-      this.maybeDragNewGenericElement(pointerDownState, event);
+      if (!this.maybeUpdateSelectionElement(pointerDownState, event)) {
+        this.maybeDragNewGenericElement(pointerDownState, event);
+      }
     });
   }
 
@@ -6132,6 +6145,13 @@ class App extends React.Component<AppProps, AppState> {
         }
       }
 
+      pointerDownState.lastCoords.x = pointerCoords.x;
+      pointerDownState.lastCoords.y = pointerCoords.y;
+
+      if (this.maybeHandleBoxSelection(pointerDownState, event)) {
+        return;
+      }
+
       // It is very important to read this.state within each move event,
       // otherwise we would read a stale one!
       const draggingElement = this.state.draggingElement;
@@ -6199,105 +6219,6 @@ class App extends React.Component<AppProps, AppState> {
         pointerDownState.lastCoords.y = pointerCoords.y;
         this.maybeDragNewGenericElement(pointerDownState, event);
       }
-
-      if (this.state.activeTool.type === "selection") {
-        pointerDownState.boxSelection.hasOccurred = true;
-
-        const elements = this.scene.getNonDeletedElements();
-
-        // box-select line editor points
-        if (this.state.editingLinearElement) {
-          LinearElementEditor.handleBoxSelection(
-            event,
-            this.state,
-            this.setState.bind(this),
-          );
-          // regular box-select
-        } else {
-          let shouldReuseSelection = true;
-
-          if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
-            if (
-              pointerDownState.withCmdOrCtrl &&
-              pointerDownState.hit.element
-            ) {
-              this.setState((prevState) =>
-                selectGroupsForSelectedElements(
-                  {
-                    ...prevState,
-                    selectedElementIds: {
-                      [pointerDownState.hit.element!.id]: true,
-                    },
-                  },
-                  this.scene.getNonDeletedElements(),
-                  prevState,
-                  this,
-                ),
-              );
-            } else {
-              shouldReuseSelection = false;
-            }
-          }
-          const elementsWithinSelection = getElementsWithinSelection(
-            elements,
-            draggingElement,
-          );
-
-          this.setState((prevState) => {
-            const nextSelectedElementIds = {
-              ...(shouldReuseSelection && prevState.selectedElementIds),
-              ...elementsWithinSelection.reduce(
-                (acc: Record<ExcalidrawElement["id"], true>, element) => {
-                  acc[element.id] = true;
-                  return acc;
-                },
-                {},
-              ),
-            };
-
-            if (pointerDownState.hit.element) {
-              // if using ctrl/cmd, select the hitElement only if we
-              // haven't box-selected anything else
-              if (!elementsWithinSelection.length) {
-                nextSelectedElementIds[pointerDownState.hit.element.id] = true;
-              } else {
-                delete nextSelectedElementIds[pointerDownState.hit.element.id];
-              }
-            }
-
-            prevState = !shouldReuseSelection
-              ? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
-              : prevState;
-
-            return {
-              ...selectGroupsForSelectedElements(
-                {
-                  editingGroupId: prevState.editingGroupId,
-                  selectedElementIds: nextSelectedElementIds,
-                },
-                this.scene.getNonDeletedElements(),
-                prevState,
-                this,
-              ),
-              // select linear element only when we haven't box-selected anything else
-              selectedLinearElement:
-                elementsWithinSelection.length === 1 &&
-                isLinearElement(elementsWithinSelection[0])
-                  ? new LinearElementEditor(
-                      elementsWithinSelection[0],
-                      this.scene,
-                    )
-                  : null,
-              showHyperlinkPopup:
-                elementsWithinSelection.length === 1 &&
-                (elementsWithinSelection[0].link ||
-                  isEmbeddableElement(elementsWithinSelection[0]))
-                  ? "info"
-                  : false,
-            };
-          });
-        }
-      }
     });
   }
 
@@ -6558,6 +6479,7 @@ class App extends React.Component<AppProps, AppState> {
             resetCursor(this.interactiveCanvas);
             this.setState((prevState) => ({
               draggingElement: null,
+              selectionElement: null,
               activeTool: updateActiveTool(this.state, {
                 type: "selection",
               }),
@@ -6576,6 +6498,7 @@ class App extends React.Component<AppProps, AppState> {
           } else {
             this.setState((prevState) => ({
               draggingElement: null,
+              selectionElement: null,
             }));
           }
         }
@@ -6595,140 +6518,137 @@ class App extends React.Component<AppProps, AppState> {
         );
         this.setState({
           draggingElement: null,
+          selectionElement: null,
         });
         return;
       }
 
-      if (draggingElement) {
-        if (pointerDownState.drag.hasOccurred) {
-          const sceneCoords = viewportCoordsToSceneCoords(
-            childEvent,
-            this.state,
-          );
+      if (pointerDownState.drag.hasOccurred) {
+        const sceneCoords = viewportCoordsToSceneCoords(childEvent, this.state);
 
-          // when editing the points of a linear element, we check if the
-          // linear element still is in the frame afterwards
-          // if not, the linear element will be removed from its frame (if any)
-          if (
-            this.state.selectedLinearElement &&
-            this.state.selectedLinearElement.isDragging
-          ) {
-            const linearElement = this.scene.getElement(
-              this.state.selectedLinearElement.elementId,
-            );
+        // when editing the points of a linear element, we check if the
+        // linear element still is in the frame afterwards
+        // if not, the linear element will be removed from its frame (if any)
+        if (
+          this.state.selectedLinearElement &&
+          this.state.selectedLinearElement.isDragging
+        ) {
+          const linearElement = this.scene.getElement(
+            this.state.selectedLinearElement.elementId,
+          );
 
-            if (linearElement?.frameId) {
-              const frame = getContainingFrame(linearElement);
+          if (linearElement?.frameId) {
+            const frame = getContainingFrame(linearElement);
 
-              if (frame && linearElement) {
-                if (!elementOverlapsWithFrame(linearElement, frame)) {
-                  // remove the linear element from all groups
-                  // before removing it from the frame as well
-                  mutateElement(linearElement, {
-                    groupIds: [],
-                  });
+            if (frame && linearElement) {
+              if (!elementOverlapsWithFrame(linearElement, frame)) {
+                // remove the linear element from all groups
+                // before removing it from the frame as well
+                mutateElement(linearElement, {
+                  groupIds: [],
+                });
 
-                  this.scene.replaceAllElements(
-                    removeElementsFromFrame(
-                      this.scene.getElementsIncludingDeleted(),
-                      [linearElement],
-                      this.state,
-                    ),
-                  );
-                }
+                this.scene.replaceAllElements(
+                  removeElementsFromFrame(
+                    this.scene.getElementsIncludingDeleted(),
+                    [linearElement],
+                    this.state,
+                  ),
+                );
               }
             }
-          } else {
-            // update the relationships between selected elements and frames
-            const topLayerFrame =
-              this.getTopLayerFrameAtSceneCoords(sceneCoords);
-
-            const selectedElements = this.scene.getSelectedElements(this.state);
-            let nextElements = this.scene.getElementsIncludingDeleted();
-
-            const updateGroupIdsAfterEditingGroup = (
-              elements: ExcalidrawElement[],
-            ) => {
-              if (elements.length > 0) {
-                for (const element of elements) {
-                  const index = element.groupIds.indexOf(
-                    this.state.editingGroupId!,
-                  );
+          }
+        } else {
+          // update the relationships between selected elements and frames
+          const topLayerFrame = this.getTopLayerFrameAtSceneCoords(sceneCoords);
+
+          const selectedElements = this.scene.getSelectedElements(this.state);
+          let nextElements = this.scene.getElementsIncludingDeleted();
+
+          const updateGroupIdsAfterEditingGroup = (
+            elements: ExcalidrawElement[],
+          ) => {
+            if (elements.length > 0) {
+              for (const element of elements) {
+                const index = element.groupIds.indexOf(
+                  this.state.editingGroupId!,
+                );
+
+                mutateElement(
+                  element,
+                  {
+                    groupIds: element.groupIds.slice(0, index),
+                  },
+                  false,
+                );
+              }
 
+              nextElements.forEach((element) => {
+                if (
+                  element.groupIds.length &&
+                  getElementsInGroup(
+                    nextElements,
+                    element.groupIds[element.groupIds.length - 1],
+                  ).length < 2
+                ) {
                   mutateElement(
                     element,
                     {
-                      groupIds: element.groupIds.slice(0, index),
+                      groupIds: [],
                     },
                     false,
                   );
                 }
+              });
 
-                nextElements.forEach((element) => {
-                  if (
-                    element.groupIds.length &&
-                    getElementsInGroup(
-                      nextElements,
-                      element.groupIds[element.groupIds.length - 1],
-                    ).length < 2
-                  ) {
-                    mutateElement(
-                      element,
-                      {
-                        groupIds: [],
-                      },
-                      false,
-                    );
-                  }
-                });
-
-                this.setState({
-                  editingGroupId: null,
-                });
-              }
-            };
-
-            if (
-              topLayerFrame &&
-              !this.state.selectedElementIds[topLayerFrame.id]
-            ) {
-              const elementsToAdd = selectedElements.filter(
-                (element) =>
-                  element.frameId !== topLayerFrame.id &&
-                  isElementInFrame(element, nextElements, this.state),
-              );
-
-              if (this.state.editingGroupId) {
-                updateGroupIdsAfterEditingGroup(elementsToAdd);
-              }
+              this.setState({
+                editingGroupId: null,
+              });
+            }
+          };
 
-              nextElements = addElementsToFrame(
-                nextElements,
-                elementsToAdd,
-                topLayerFrame,
-              );
-            } else if (!topLayerFrame) {
-              if (this.state.editingGroupId) {
-                const elementsToRemove = selectedElements.filter(
-                  (element) =>
-                    element.frameId &&
-                    !isElementInFrame(element, nextElements, this.state),
-                );
+          if (
+            topLayerFrame &&
+            !this.state.selectedElementIds[topLayerFrame.id]
+          ) {
+            const elementsToAdd = selectedElements.filter(
+              (element) =>
+                element.frameId !== topLayerFrame.id &&
+                isElementInFrame(element, nextElements, this.state),
+            );
 
-                updateGroupIdsAfterEditingGroup(elementsToRemove);
-              }
+            if (this.state.editingGroupId) {
+              updateGroupIdsAfterEditingGroup(elementsToAdd);
             }
 
-            nextElements = updateFrameMembershipOfSelectedElements(
+            nextElements = addElementsToFrame(
               nextElements,
-              this.state,
-              this,
+              elementsToAdd,
+              topLayerFrame,
             );
+          } else if (!topLayerFrame) {
+            if (this.state.editingGroupId) {
+              const elementsToRemove = selectedElements.filter(
+                (element) =>
+                  element.frameId &&
+                  !isElementInFrame(element, nextElements, this.state),
+              );
 
-            this.scene.replaceAllElements(nextElements);
+              updateGroupIdsAfterEditingGroup(elementsToRemove);
+            }
           }
+
+          nextElements = updateFrameMembershipOfSelectedElements(
+            nextElements,
+            this.state,
+            this,
+          );
+
+          this.scene.replaceAllElements(nextElements);
         }
+      }
 
+      if (draggingElement) {
         if (draggingElement.type === "frame") {
           const elementsInsideFrame = getElementsInNewFrame(
             this.scene.getElementsIncludingDeleted(),
@@ -7032,8 +6952,7 @@ class App extends React.Component<AppProps, AppState> {
       if (
         !activeTool.locked &&
         activeTool.type !== "freedraw" &&
-        draggingElement &&
-        draggingElement.type !== "selection"
+        draggingElement
       ) {
         this.setState((prevState) => ({
           selectedElementIds: makeNextSelectedElementIds(
@@ -7072,12 +6991,14 @@ class App extends React.Component<AppProps, AppState> {
         resetCursor(this.interactiveCanvas);
         this.setState({
           draggingElement: null,
+          selectionElement: null,
           suggestedBindings: [],
           activeTool: updateActiveTool(this.state, { type: "selection" }),
         });
       } else {
         this.setState({
           draggingElement: null,
+          selectionElement: null,
           suggestedBindings: [],
         });
       }
@@ -7876,6 +7797,139 @@ class App extends React.Component<AppProps, AppState> {
     );
   };
 
+  private maybeUpdateSelectionElement = (
+    pointerDownState: PointerDownState,
+    event: PointerEvent | KeyboardEvent,
+  ): boolean => {
+    const { selectionElement } = this.state;
+
+    if (!selectionElement || this.state.activeTool.type !== "selection") {
+      return false;
+    }
+
+    const pointerCoords = pointerDownState.lastCoords;
+
+    dragNewElement(
+      selectionElement,
+      this.state.activeTool.type,
+      pointerDownState.origin.x,
+      pointerDownState.origin.y,
+      pointerCoords.x,
+      pointerCoords.y,
+      distance(pointerDownState.origin.x, pointerCoords.x),
+      distance(pointerDownState.origin.y, pointerCoords.y),
+      shouldMaintainAspectRatio(event),
+      shouldResizeFromCenter(event),
+    );
+
+    return true;
+  };
+
+  private maybeHandleBoxSelection = (
+    pointerDownState: PointerDownState,
+    event: PointerEvent,
+  ): boolean => {
+    const { selectionElement } = this.state;
+
+    if (!selectionElement || this.state.activeTool.type !== "selection") {
+      return false;
+    }
+
+    this.maybeUpdateSelectionElement(pointerDownState, event);
+
+    pointerDownState.boxSelection.hasOccurred = true;
+
+    const elements = this.scene.getNonDeletedElements();
+
+    // box-select line editor points
+    if (this.state.editingLinearElement) {
+      LinearElementEditor.handleBoxSelection(
+        event,
+        this.state,
+        this.setState.bind(this),
+      );
+      // regular box-select
+    } else {
+      let shouldReuseSelection = true;
+
+      if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
+        if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
+          this.setState((prevState) =>
+            selectGroupsForSelectedElements(
+              {
+                ...prevState,
+                selectedElementIds: {
+                  [pointerDownState.hit.element!.id]: true,
+                },
+              },
+              this.scene.getNonDeletedElements(),
+              prevState,
+              this,
+            ),
+          );
+        } else {
+          shouldReuseSelection = false;
+        }
+      }
+      const elementsWithinSelection = getElementsWithinSelection(
+        elements,
+        selectionElement,
+      );
+
+      this.setState((prevState) => {
+        const nextSelectedElementIds = {
+          ...(shouldReuseSelection && prevState.selectedElementIds),
+          ...elementsWithinSelection.reduce(
+            (acc: Record<ExcalidrawElement["id"], true>, element) => {
+              acc[element.id] = true;
+              return acc;
+            },
+            {},
+          ),
+        };
+
+        if (pointerDownState.hit.element) {
+          // if using ctrl/cmd, select the hitElement only if we
+          // haven't box-selected anything else
+          if (!elementsWithinSelection.length) {
+            nextSelectedElementIds[pointerDownState.hit.element.id] = true;
+          } else {
+            delete nextSelectedElementIds[pointerDownState.hit.element.id];
+          }
+        }
+
+        prevState = !shouldReuseSelection
+          ? { ...prevState, selectedGroupIds: {}, editingGroupId: null }
+          : prevState;
+
+        return {
+          ...selectGroupsForSelectedElements(
+            {
+              editingGroupId: prevState.editingGroupId,
+              selectedElementIds: nextSelectedElementIds,
+            },
+            this.scene.getNonDeletedElements(),
+            prevState,
+            this,
+          ),
+          // select linear element only when we haven't box-selected anything else
+          selectedLinearElement:
+            elementsWithinSelection.length === 1 &&
+            isLinearElement(elementsWithinSelection[0])
+              ? new LinearElementEditor(elementsWithinSelection[0], this.scene)
+              : null,
+          showHyperlinkPopup:
+            elementsWithinSelection.length === 1 &&
+            (elementsWithinSelection[0].link ||
+              isEmbeddableElement(elementsWithinSelection[0]))
+              ? "info"
+              : false,
+        };
+      });
+    }
+    return true;
+  };
+
   private maybeDragNewGenericElement = (
     pointerDownState: PointerDownState,
     event: MouseEvent | KeyboardEvent,
@@ -7885,93 +7939,73 @@ class App extends React.Component<AppProps, AppState> {
     if (!draggingElement) {
       return;
     }
-    if (
-      draggingElement.type === "selection" &&
-      this.state.activeTool.type !== "eraser"
-    ) {
-      dragNewElement(
-        draggingElement,
-        this.state.activeTool.type,
-        pointerDownState.origin.x,
-        pointerDownState.origin.y,
-        pointerCoords.x,
-        pointerCoords.y,
-        distance(pointerDownState.origin.x, pointerCoords.x),
-        distance(pointerDownState.origin.y, pointerCoords.y),
-        shouldMaintainAspectRatio(event),
-        shouldResizeFromCenter(event),
-      );
-    } else {
-      let [gridX, gridY] = getGridPoint(
-        pointerCoords.x,
-        pointerCoords.y,
-        event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
-      );
+    let [gridX, gridY] = getGridPoint(
+      pointerCoords.x,
+      pointerCoords.y,
+      event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
+    );
 
-      const image =
-        isInitializedImageElement(draggingElement) &&
-        this.imageCache.get(draggingElement.fileId)?.image;
-      const aspectRatio =
-        image && !(image instanceof Promise)
-          ? image.width / image.height
-          : null;
+    const image =
+      isInitializedImageElement(draggingElement) &&
+      this.imageCache.get(draggingElement.fileId)?.image;
+    const aspectRatio =
+      image && !(image instanceof Promise) ? image.width / image.height : null;
 
-      this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
+    this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
 
-      const { snapOffset, snapLines } = snapNewElement(
-        draggingElement,
-        this.state,
-        event,
-        {
-          x:
-            pointerDownState.originInGrid.x +
-            (this.state.originSnapOffset?.x ?? 0),
-          y:
-            pointerDownState.originInGrid.y +
-            (this.state.originSnapOffset?.y ?? 0),
-        },
-        {
-          x: gridX - pointerDownState.originInGrid.x,
-          y: gridY - pointerDownState.originInGrid.y,
-        },
-      );
+    const { snapOffset, snapLines } = snapNewElement(
+      draggingElement,
+      this.state,
+      event,
+      {
+        x:
+          pointerDownState.originInGrid.x +
+          (this.state.originSnapOffset?.x ?? 0),
+        y:
+          pointerDownState.originInGrid.y +
+          (this.state.originSnapOffset?.y ?? 0),
+      },
+      {
+        x: gridX - pointerDownState.originInGrid.x,
+        y: gridY - pointerDownState.originInGrid.y,
+      },
+    );
 
-      gridX += snapOffset.x;
-      gridY += snapOffset.y;
+    gridX += snapOffset.x;
+    gridY += snapOffset.y;
 
-      this.setState({
-        snapLines,
-      });
+    this.setState({
+      snapLines,
+    });
 
-      dragNewElement(
-        draggingElement,
-        this.state.activeTool.type,
-        pointerDownState.originInGrid.x,
-        pointerDownState.originInGrid.y,
-        gridX,
-        gridY,
-        distance(pointerDownState.originInGrid.x, gridX),
-        distance(pointerDownState.originInGrid.y, gridY),
-        isImageElement(draggingElement)
-          ? !shouldMaintainAspectRatio(event)
-          : shouldMaintainAspectRatio(event),
-        shouldResizeFromCenter(event),
-        aspectRatio,
-        this.state.originSnapOffset,
-      );
+    dragNewElement(
+      draggingElement,
+      this.state.activeTool.type,
+      pointerDownState.originInGrid.x,
+      pointerDownState.originInGrid.y,
+      gridX,
+      gridY,
+      distance(pointerDownState.originInGrid.x, gridX),
+      distance(pointerDownState.originInGrid.y, gridY),
+      isImageElement(draggingElement)
+        ? !shouldMaintainAspectRatio(event)
+        : shouldMaintainAspectRatio(event),
+      shouldResizeFromCenter(event),
+      aspectRatio,
+      this.state.originSnapOffset,
+    );
 
-      this.maybeSuggestBindingForAll([draggingElement]);
+    this.maybeSuggestBindingForAll([draggingElement]);
 
-      // highlight elements that are to be added to frames on frames creation
-      if (this.state.activeTool.type === "frame") {
-        this.setState({
-          elementsToHighlight: getElementsInResizingFrame(
-            this.scene.getNonDeletedElements(),
-            draggingElement as ExcalidrawFrameElement,
-            this.state,
-          ),
-        });
-      }
+    // highlight elements that are to be added to frames on frames creation
+    if (this.state.activeTool.type === "frame") {
+      this.setState({
+        elementsToHighlight: getElementsInResizingFrame(
+          this.scene.getNonDeletedElements(),
+          draggingElement as ExcalidrawFrameElement,
+          this.state,
+        ),
+      });
     }
   };
 

+ 2 - 1
src/components/HintViewer.tsx

@@ -82,8 +82,9 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
 
   if (activeTool.type === "selection") {
     if (
-      appState.draggingElement?.type === "selection" &&
+      appState.selectionElement &&
       !selectedElements.length &&
+      !appState.draggingElement &&
       !appState.editingElement &&
       !appState.editingLinearElement
     ) {

+ 1 - 0
src/element/Hyperlink.tsx

@@ -210,6 +210,7 @@ export const Hyperlink = ({
   };
   const { x, y } = getCoordsForPopover(element, appState);
   if (
+    appState.selectionElement ||
     appState.draggingElement ||
     appState.resizingElement ||
     appState.isRotating ||

+ 2 - 5
src/element/linearElementEditor.ts

@@ -134,10 +134,7 @@ export class LinearElementEditor {
     appState: AppState,
     setState: React.Component<any, AppState>["setState"],
   ) {
-    if (
-      !appState.editingLinearElement ||
-      appState.draggingElement?.type !== "selection"
-    ) {
+    if (!appState.editingLinearElement || !appState.selectionElement) {
       return false;
     }
     const { editingLinearElement } = appState;
@@ -149,7 +146,7 @@ export class LinearElementEditor {
     }
 
     const [selectionX1, selectionY1, selectionX2, selectionY2] =
-      getElementAbsoluteCoords(appState.draggingElement);
+      getElementAbsoluteCoords(appState.selectionElement);
 
     const pointsSceneCoords =
       LinearElementEditor.getPointsGlobalCoordinates(element);

+ 5 - 145
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -5152,35 +5152,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
   "currentItemTextAlign": "left",
   "cursorButton": "down",
   "defaultSidebarDockedPreference": false,
-  "draggingElement": {
-    "angle": 0,
-    "backgroundColor": "transparent",
-    "boundElements": null,
-    "fillStyle": "hachure",
-    "frameId": null,
-    "groupIds": [],
-    "height": 0,
-    "id": "id3",
-    "isDeleted": false,
-    "link": null,
-    "locked": false,
-    "opacity": 100,
-    "roughness": 1,
-    "roundness": {
-      "type": 2,
-    },
-    "seed": 400692809,
-    "strokeColor": "#1e1e1e",
-    "strokeStyle": "solid",
-    "strokeWidth": 1,
-    "type": "selection",
-    "updated": 1,
-    "version": 1,
-    "versionNonce": 0,
-    "width": 0,
-    "x": 500,
-    "y": 500,
-  },
+  "draggingElement": null,
   "editingElement": null,
   "editingFrame": null,
   "editingGroupId": null,
@@ -5449,35 +5421,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
   "currentItemTextAlign": "left",
   "cursorButton": "up",
   "defaultSidebarDockedPreference": false,
-  "draggingElement": {
-    "angle": 0,
-    "backgroundColor": "transparent",
-    "boundElements": null,
-    "fillStyle": "hachure",
-    "frameId": null,
-    "groupIds": [],
-    "height": 0,
-    "id": "id3",
-    "isDeleted": false,
-    "link": null,
-    "locked": false,
-    "opacity": 100,
-    "roughness": 1,
-    "roundness": {
-      "type": 2,
-    },
-    "seed": 400692809,
-    "strokeColor": "#1e1e1e",
-    "strokeStyle": "solid",
-    "strokeWidth": 1,
-    "type": "selection",
-    "updated": 1,
-    "version": 1,
-    "versionNonce": 0,
-    "width": 0,
-    "x": 50,
-    "y": 50,
-  },
+  "draggingElement": null,
   "editingElement": null,
   "editingFrame": null,
   "editingGroupId": null,
@@ -5718,35 +5662,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
   "currentItemTextAlign": "left",
   "cursorButton": "down",
   "defaultSidebarDockedPreference": false,
-  "draggingElement": {
-    "angle": 0,
-    "backgroundColor": "transparent",
-    "boundElements": null,
-    "fillStyle": "hachure",
-    "frameId": null,
-    "groupIds": [],
-    "height": 0,
-    "id": "id1",
-    "isDeleted": false,
-    "link": null,
-    "locked": false,
-    "opacity": 100,
-    "roughness": 1,
-    "roundness": {
-      "type": 2,
-    },
-    "seed": 2019559783,
-    "strokeColor": "#1e1e1e",
-    "strokeStyle": "solid",
-    "strokeWidth": 1,
-    "type": "selection",
-    "updated": 1,
-    "version": 1,
-    "versionNonce": 0,
-    "width": 0,
-    "x": 110,
-    "y": 110,
-  },
+  "draggingElement": null,
   "editingElement": null,
   "editingFrame": null,
   "editingGroupId": null,
@@ -16262,35 +16178,7 @@ exports[`regression tests > switches from group of selected elements to another
   "currentItemTextAlign": "left",
   "cursorButton": "down",
   "defaultSidebarDockedPreference": false,
-  "draggingElement": {
-    "angle": 0,
-    "backgroundColor": "transparent",
-    "boundElements": null,
-    "fillStyle": "hachure",
-    "frameId": null,
-    "groupIds": [],
-    "height": 0,
-    "id": "id4",
-    "isDeleted": false,
-    "link": null,
-    "locked": false,
-    "opacity": 100,
-    "roughness": 1,
-    "roundness": {
-      "type": 2,
-    },
-    "seed": 493213705,
-    "strokeColor": "#1e1e1e",
-    "strokeStyle": "solid",
-    "strokeWidth": 1,
-    "type": "selection",
-    "updated": 1,
-    "version": 1,
-    "versionNonce": 0,
-    "width": 0,
-    "x": 0,
-    "y": 0,
-  },
+  "draggingElement": null,
   "editingElement": null,
   "editingFrame": null,
   "editingGroupId": null,
@@ -16662,35 +16550,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
   "currentItemTextAlign": "left",
   "cursorButton": "down",
   "defaultSidebarDockedPreference": false,
-  "draggingElement": {
-    "angle": 0,
-    "backgroundColor": "transparent",
-    "boundElements": null,
-    "fillStyle": "hachure",
-    "frameId": null,
-    "groupIds": [],
-    "height": 0,
-    "id": "id2",
-    "isDeleted": false,
-    "link": null,
-    "locked": false,
-    "opacity": 100,
-    "roughness": 1,
-    "roundness": {
-      "type": 2,
-    },
-    "seed": 238820263,
-    "strokeColor": "#1e1e1e",
-    "strokeStyle": "solid",
-    "strokeWidth": 1,
-    "type": "selection",
-    "updated": 1,
-    "version": 1,
-    "versionNonce": 0,
-    "width": 0,
-    "x": 0,
-    "y": 0,
-  },
+  "draggingElement": null,
   "editingElement": null,
   "editingFrame": null,
   "editingGroupId": null,

+ 25 - 4
src/types.ts

@@ -17,6 +17,7 @@ import {
   StrokeRoundness,
   ExcalidrawFrameElement,
   ExcalidrawEmbeddableElement,
+  ExcalidrawSelectionElement,
 } from "./element/types";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
 import { LinearElementEditor } from "./element/linearElementEditor";
@@ -182,10 +183,25 @@ export type AppState = {
     element: NonDeletedExcalidrawElement;
     state: "hover" | "active";
   } | null;
-  draggingElement: NonDeletedExcalidrawElement | null;
+  /** element that's being dragged or created */
+  draggingElement: Exclude<
+    NonDeletedExcalidrawElement,
+    ExcalidrawSelectionElement
+  > | null;
+  /**
+   * Element that's being resized.
+   * NOTE not set when resizing a group or linear element
+   */
   resizingElement: NonDeletedExcalidrawElement | null;
+  /** multi-point linear element when it's being created */
   multiElement: NonDeleted<ExcalidrawLinearElement> | null;
-  selectionElement: NonDeletedExcalidrawElement | null;
+  /**
+   * The selection box (we currently use an excalidraw element).
+   *
+   * Checking for this attribute is a good way to determine whether the user is
+   * selecting.
+   */
+  selectionElement: ExcalidrawSelectionElement | null;
   isBindingEnabled: boolean;
   startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
   suggestedBindings: SuggestedBinding[];
@@ -198,9 +214,14 @@ export type AppState = {
   };
   editingFrame: string | null;
   elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
-  // element being edited, but not necessarily added to elements array yet
-  // (e.g. text element when typing into the input)
+  /**
+   * Text that's being element, or new element being created.
+   */
   editingElement: NonDeletedExcalidrawElement | null;
+  /**
+   * Linear element that's being edited (when in the linear element editor).
+   * Not set when creating multi-point linear element.
+   */
   editingLinearElement: LinearElementEditor | null;
   activeTool: {
     /**