Bladeren bron

Pull onPointerDown, onDoubleClick, onPointerMove into instance methods (#876)

* Pull onPointerDown, onDoubleClick, onPointerMove into instance methods

* Use bound instance methods
Pete Hunt 5 jaren geleden
bovenliggende
commit
92ba401da8
1 gewijzigde bestanden met toevoegingen van 1124 en 1191 verwijderingen
  1. 1124 1191
      src/components/App.tsx

+ 1124 - 1191
src/components/App.tsx

@@ -571,1271 +571,1204 @@ export class App extends React.Component<any, AppState> {
                 left: event.clientX,
               });
             }}
-            onPointerDown={event => {
-              if (lastPointerUp !== null) {
-                // Unfortunately, sometimes we don't get a pointerup after a pointerdown,
-                // this can happen when a contextual menu or alert is triggered. In order to avoid
-                // being in a weird state, we clean up on the next pointerdown
-                lastPointerUp(event);
-              }
-
-              if (isPanning) {
-                return;
-              }
-
-              this.setState({ lastPointerDownWith: event.pointerType });
-
-              // pan canvas on wheel button drag or space+drag
-              if (
-                gesture.pointers.length === 0 &&
-                (event.button === POINTER_BUTTON.WHEEL ||
-                  (event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
-              ) {
-                isPanning = true;
-                document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
-                let { clientX: lastX, clientY: lastY } = event;
-                const onPointerMove = (event: PointerEvent) => {
-                  const deltaX = lastX - event.clientX;
-                  const deltaY = lastY - event.clientY;
-                  lastX = event.clientX;
-                  lastY = event.clientY;
-
-                  this.setState({
-                    scrollX: normalizeScroll(
-                      this.state.scrollX - deltaX / this.state.zoom,
-                    ),
-                    scrollY: normalizeScroll(
-                      this.state.scrollY - deltaY / this.state.zoom,
-                    ),
-                  });
-                };
-                const teardown = (lastPointerUp = () => {
-                  lastPointerUp = null;
-                  isPanning = false;
-                  if (!isHoldingSpace) {
-                    setCursorForShape(this.state.elementType);
-                  }
-                  window.removeEventListener("pointermove", onPointerMove);
-                  window.removeEventListener("pointerup", teardown);
-                  window.removeEventListener("blur", teardown);
-                });
-                window.addEventListener("blur", teardown);
-                window.addEventListener("pointermove", onPointerMove, {
-                  passive: true,
-                });
-                window.addEventListener("pointerup", teardown);
-                return;
-              }
-
-              // only handle left mouse button or touch
+            onPointerDown={this.handleCanvasPointerDown}
+            onDoubleClick={this.handleCanvasDoubleClick}
+            onPointerMove={this.handleCanvasPointerMove}
+            onPointerUp={this.removePointer}
+            onPointerCancel={this.removePointer}
+            onDrop={event => {
+              const file = event.dataTransfer.files[0];
               if (
-                event.button !== POINTER_BUTTON.MAIN &&
-                event.button !== POINTER_BUTTON.TOUCH
+                file?.type === "application/json" ||
+                file?.name.endsWith(".excalidraw")
               ) {
-                return;
+                loadFromBlob(file)
+                  .then(({ elements, appState }) =>
+                    this.syncActionResult({ elements, appState }),
+                  )
+                  .catch(error => console.error(error));
               }
+            }}
+          >
+            {t("labels.drawingCanvas")}
+          </canvas>
+        </main>
+      </div>
+    );
+  }
 
-              gesture.pointers.push({
-                id: event.pointerId,
-                x: event.clientX,
-                y: event.clientY,
-              });
-              if (gesture.pointers.length === 2) {
-                gesture.lastCenter = getCenter(gesture.pointers);
-                gesture.initialScale = this.state.zoom;
-                gesture.initialDistance = getDistance(gesture.pointers);
-              }
+  private handleCanvasDoubleClick = (
+    event: React.MouseEvent<HTMLCanvasElement>,
+  ) => {
+    resetCursor();
 
-              // fixes pointermove causing selection of UI texts #32
-              event.preventDefault();
-              // Preventing the event above disables default behavior
-              //  of defocusing potentially focused element, which is what we
-              //  want when clicking inside the canvas.
-              if (document.activeElement instanceof HTMLElement) {
-                document.activeElement.blur();
-              }
+    const { x, y } = viewportCoordsToSceneCoords(
+      event,
+      this.state,
+      this.canvas,
+    );
 
-              // don't select while panning
-              if (gesture.pointers.length > 1) {
-                return;
-              }
+    const elementAtPosition = getElementAtPosition(
+      elements,
+      this.state,
+      x,
+      y,
+      this.state.zoom,
+    );
 
-              // Handle scrollbars dragging
-              const {
-                isOverHorizontalScrollBar,
-                isOverVerticalScrollBar,
-              } = isOverScrollBars(
-                currentScrollBars,
-                event.clientX,
-                event.clientY,
-              );
+    const element =
+      elementAtPosition && isTextElement(elementAtPosition)
+        ? elementAtPosition
+        : newTextElement(
+            newElement(
+              "text",
+              x,
+              y,
+              this.state.currentItemStrokeColor,
+              this.state.currentItemBackgroundColor,
+              this.state.currentItemFillStyle,
+              this.state.currentItemStrokeWidth,
+              this.state.currentItemRoughness,
+              this.state.currentItemOpacity,
+            ),
+            "", // default text
+            this.state.currentItemFont, // default font
+          );
+
+    this.setState({ editingElement: element });
+
+    let textX = event.clientX;
+    let textY = event.clientY;
+
+    if (elementAtPosition && isTextElement(elementAtPosition)) {
+      elements = elements.filter(
+        element => element.id !== elementAtPosition.id,
+      );
+      this.setState({});
 
-              const { x, y } = viewportCoordsToSceneCoords(
-                event,
-                this.state,
-                this.canvas,
-              );
-              let lastX = x;
-              let lastY = y;
+      const centerElementX = elementAtPosition.x + elementAtPosition.width / 2;
+      const centerElementY = elementAtPosition.y + elementAtPosition.height / 2;
 
-              if (
-                (isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
-                !this.state.multiElement
-              ) {
-                isDraggingScrollBar = true;
-                lastX = event.clientX;
-                lastY = event.clientY;
-                const onPointerMove = (event: PointerEvent) => {
-                  const target = event.target;
-                  if (!(target instanceof HTMLElement)) {
-                    return;
-                  }
+      const {
+        x: centerElementXInViewport,
+        y: centerElementYInViewport,
+      } = sceneCoordsToViewportCoords(
+        { sceneX: centerElementX, sceneY: centerElementY },
+        this.state,
+        this.canvas,
+      );
 
-                  if (isOverHorizontalScrollBar) {
-                    const x = event.clientX;
-                    const dx = x - lastX;
-                    this.setState({
-                      scrollX: normalizeScroll(
-                        this.state.scrollX - dx / this.state.zoom,
-                      ),
-                    });
-                    lastX = x;
-                    return;
-                  }
+      textX = centerElementXInViewport;
+      textY = centerElementYInViewport;
 
-                  if (isOverVerticalScrollBar) {
-                    const y = event.clientY;
-                    const dy = y - lastY;
-                    this.setState({
-                      scrollY: normalizeScroll(
-                        this.state.scrollY - dy / this.state.zoom,
-                      ),
-                    });
-                    lastY = y;
-                  }
-                };
+      // x and y will change after calling newTextElement function
+      element.x = centerElementX;
+      element.y = centerElementY;
+    } else if (!event.altKey) {
+      const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
+        x,
+        y,
+      );
 
-                const onPointerUp = () => {
-                  isDraggingScrollBar = false;
-                  setCursorForShape(this.state.elementType);
-                  lastPointerUp = null;
-                  window.removeEventListener("pointermove", onPointerMove);
-                  window.removeEventListener("pointerup", onPointerUp);
-                };
+      if (snappedToCenterPosition) {
+        element.x = snappedToCenterPosition.elementCenterX;
+        element.y = snappedToCenterPosition.elementCenterY;
+        textX = snappedToCenterPosition.wysiwygX;
+        textY = snappedToCenterPosition.wysiwygY;
+      }
+    }
 
-                lastPointerUp = onPointerUp;
+    const resetSelection = () => {
+      this.setState({
+        draggingElement: null,
+        editingElement: null,
+      });
+    };
 
-                window.addEventListener("pointermove", onPointerMove);
-                window.addEventListener("pointerup", onPointerUp);
-                return;
-              }
+    textWysiwyg({
+      initText: element.text,
+      x: textX,
+      y: textY,
+      strokeColor: element.strokeColor,
+      font: element.font,
+      opacity: this.state.currentItemOpacity,
+      zoom: this.state.zoom,
+      onSubmit: text => {
+        if (text) {
+          elements = [
+            ...elements,
+            {
+              // we need to recreate the element to update dimensions &
+              //  position
+              ...newTextElement(element, text, element.font),
+            },
+          ];
+        }
+        this.setState(prevState => ({
+          selectedElementIds: {
+            ...prevState.selectedElementIds,
+            [element.id]: true,
+          },
+        }));
+        history.resumeRecording();
+        resetSelection();
+      },
+      onCancel: () => {
+        resetSelection();
+      },
+    });
+  };
 
-              const originX = x;
-              const originY = y;
+  private handleCanvasPointerMove = (
+    event: React.PointerEvent<HTMLCanvasElement>,
+  ) => {
+    gesture.pointers = gesture.pointers.map(pointer =>
+      pointer.id === event.pointerId
+        ? {
+            id: event.pointerId,
+            x: event.clientX,
+            y: event.clientY,
+          }
+        : pointer,
+    );
 
-              let element = newElement(
-                this.state.elementType,
-                x,
-                y,
-                this.state.currentItemStrokeColor,
-                this.state.currentItemBackgroundColor,
-                this.state.currentItemFillStyle,
-                this.state.currentItemStrokeWidth,
-                this.state.currentItemRoughness,
-                this.state.currentItemOpacity,
-              );
+    if (gesture.pointers.length === 2) {
+      const center = getCenter(gesture.pointers);
+      const deltaX = center.x - gesture.lastCenter!.x;
+      const deltaY = center.y - gesture.lastCenter!.y;
+      gesture.lastCenter = center;
 
-              if (isTextElement(element)) {
-                element = newTextElement(
-                  element,
-                  "",
-                  this.state.currentItemFont,
-                );
-              }
+      const distance = getDistance(gesture.pointers);
+      const scaleFactor = distance / gesture.initialDistance!;
 
-              type ResizeTestType = ReturnType<typeof resizeTest>;
-              let resizeHandle: ResizeTestType = false;
-              let isResizingElements = false;
-              let draggingOccurred = false;
-              let hitElement: ExcalidrawElement | null = null;
-              let elementIsAddedToSelection = false;
-              if (this.state.elementType === "selection") {
-                const resizeElement = getElementWithResizeHandler(
-                  elements,
-                  this.state,
-                  { x, y },
-                  this.state.zoom,
-                  event.pointerType,
-                );
+      this.setState({
+        scrollX: normalizeScroll(this.state.scrollX + deltaX / this.state.zoom),
+        scrollY: normalizeScroll(this.state.scrollY + deltaY / this.state.zoom),
+        zoom: getNormalizedZoom(gesture.initialScale! * scaleFactor),
+      });
+    } else {
+      gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
+    }
 
-                const selectedElements = getSelectedElements(
-                  elements,
-                  this.state,
-                );
-                if (selectedElements.length === 1 && resizeElement) {
-                  this.setState({
-                    resizingElement: resizeElement
-                      ? resizeElement.element
-                      : null,
-                  });
-
-                  resizeHandle = resizeElement.resizeHandle;
-                  document.documentElement.style.cursor = getCursorForResizingElement(
-                    resizeElement,
-                  );
-                  isResizingElements = true;
-                } else {
-                  hitElement = getElementAtPosition(
-                    elements,
-                    this.state,
-                    x,
-                    y,
-                    this.state.zoom,
-                  );
-                  // clear selection if shift is not clicked
-                  if (
-                    !(
-                      hitElement && this.state.selectedElementIds[hitElement.id]
-                    ) &&
-                    !event.shiftKey
-                  ) {
-                    this.setState({ selectedElementIds: {} });
-                  }
+    if (isHoldingSpace || isPanning || isDraggingScrollBar) {
+      return;
+    }
+    const {
+      isOverHorizontalScrollBar,
+      isOverVerticalScrollBar,
+    } = isOverScrollBars(currentScrollBars, event.clientX, event.clientY);
+    const isOverScrollBar =
+      isOverVerticalScrollBar || isOverHorizontalScrollBar;
+    if (!this.state.draggingElement && !this.state.multiElement) {
+      if (isOverScrollBar) {
+        resetCursor();
+      } else {
+        setCursorForShape(this.state.elementType);
+      }
+    }
 
-                  // If we click on something
-                  if (hitElement) {
-                    // deselect if item is selected
-                    // if shift is not clicked, this will always return true
-                    // otherwise, it will trigger selection based on current
-                    // state of the box
-                    if (!this.state.selectedElementIds[hitElement.id]) {
-                      this.setState(prevState => ({
-                        selectedElementIds: {
-                          ...prevState.selectedElementIds,
-                          [hitElement!.id]: true,
-                        },
-                      }));
-                      elements = elements.slice();
-                      elementIsAddedToSelection = true;
-                    }
-
-                    // We duplicate the selected element if alt is pressed on pointer down
-                    if (event.altKey) {
-                      // Move the currently selected elements to the top of the z index stack, and
-                      // put the duplicates where the selected elements used to be.
-                      const nextElements = [];
-                      const elementsToAppend = [];
-                      for (const element of elements) {
-                        if (this.state.selectedElementIds[element.id]) {
-                          nextElements.push(duplicateElement(element));
-                          elementsToAppend.push(element);
-                        } else {
-                          nextElements.push(element);
-                        }
-                      }
-                      elements = [...nextElements, ...elementsToAppend];
-                    }
-                  }
-                }
-              } else {
-                this.setState({ selectedElementIds: {} });
-              }
+    const { x, y } = viewportCoordsToSceneCoords(
+      event,
+      this.state,
+      this.canvas,
+    );
+    if (this.state.multiElement) {
+      const { multiElement } = this.state;
+      const originX = multiElement.x;
+      const originY = multiElement.y;
+      const points = multiElement.points;
+      const pnt = points[points.length - 1];
+      pnt[0] = x - originX;
+      pnt[1] = y - originY;
+      invalidateShapeForElement(multiElement);
+      this.setState({});
+      return;
+    }
 
-              if (isTextElement(element)) {
-                // if we're currently still editing text, clicking outside
-                //  should only finalize it, not create another (irrespective
-                //  of state.elementLocked)
-                if (this.state.editingElement?.type === "text") {
-                  return;
-                }
-                if (elementIsAddedToSelection) {
-                  element = hitElement!;
-                }
-                let textX = event.clientX;
-                let textY = event.clientY;
-                if (!event.altKey) {
-                  const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
-                    x,
-                    y,
-                  );
-                  if (snappedToCenterPosition) {
-                    element.x = snappedToCenterPosition.elementCenterX;
-                    element.y = snappedToCenterPosition.elementCenterY;
-                    textX = snappedToCenterPosition.wysiwygX;
-                    textY = snappedToCenterPosition.wysiwygY;
-                  }
-                }
+    const hasDeselectedButton = Boolean(event.buttons);
+    if (hasDeselectedButton || this.state.elementType !== "selection") {
+      return;
+    }
 
-                const resetSelection = () => {
-                  this.setState({
-                    draggingElement: null,
-                    editingElement: null,
-                  });
-                };
-
-                textWysiwyg({
-                  initText: "",
-                  x: textX,
-                  y: textY,
-                  strokeColor: this.state.currentItemStrokeColor,
-                  opacity: this.state.currentItemOpacity,
-                  font: this.state.currentItemFont,
-                  zoom: this.state.zoom,
-                  onSubmit: text => {
-                    if (text) {
-                      elements = [
-                        ...elements,
-                        {
-                          ...newTextElement(
-                            element,
-                            text,
-                            this.state.currentItemFont,
-                          ),
-                        },
-                      ];
-                    }
-                    this.setState(prevState => ({
-                      selectedElementIds: {
-                        ...prevState.selectedElementIds,
-                        [element.id]: true,
-                      },
-                    }));
-                    if (this.state.elementLocked) {
-                      setCursorForShape(this.state.elementType);
-                    }
-                    history.resumeRecording();
-                    resetSelection();
-                  },
-                  onCancel: () => {
-                    resetSelection();
-                  },
-                });
-                resetCursor();
-                if (!this.state.elementLocked) {
-                  this.setState({
-                    editingElement: element,
-                    elementType: "selection",
-                  });
-                } else {
-                  this.setState({
-                    editingElement: element,
-                  });
-                }
-                return;
-              } else if (
-                this.state.elementType === "arrow" ||
-                this.state.elementType === "line"
-              ) {
-                if (this.state.multiElement) {
-                  const { multiElement } = this.state;
-                  const { x: rx, y: ry } = multiElement;
-                  this.setState(prevState => ({
-                    selectedElementIds: {
-                      ...prevState.selectedElementIds,
-                      [multiElement.id]: true,
-                    },
-                  }));
-                  multiElement.points.push([x - rx, y - ry]);
-                  invalidateShapeForElement(multiElement);
-                } else {
-                  this.setState(prevState => ({
-                    selectedElementIds: {
-                      ...prevState.selectedElementIds,
-                      [element.id]: false,
-                    },
-                  }));
-                  element.points.push([0, 0]);
-                  invalidateShapeForElement(element);
-                  elements = [...elements, element];
-                  this.setState({
-                    draggingElement: element,
-                  });
-                }
-              } else if (element.type === "selection") {
-                this.setState({
-                  selectionElement: element,
-                  draggingElement: element,
-                });
-              } else {
-                elements = [...elements, element];
-                this.setState({ multiElement: null, draggingElement: element });
-              }
+    const selectedElements = getSelectedElements(elements, this.state);
+    if (selectedElements.length === 1 && !isOverScrollBar) {
+      const resizeElement = getElementWithResizeHandler(
+        elements,
+        this.state,
+        { x, y },
+        this.state.zoom,
+        event.pointerType,
+      );
+      if (resizeElement && resizeElement.resizeHandle) {
+        document.documentElement.style.cursor = getCursorForResizingElement(
+          resizeElement,
+        );
+        return;
+      }
+    }
+    const hitElement = getElementAtPosition(
+      elements,
+      this.state,
+      x,
+      y,
+      this.state.zoom,
+    );
+    document.documentElement.style.cursor =
+      hitElement && !isOverScrollBar ? "move" : "";
+  };
 
-              let resizeArrowFn:
-                | ((
-                    element: ExcalidrawElement,
-                    p1: Point,
-                    deltaX: number,
-                    deltaY: number,
-                    pointerX: number,
-                    pointerY: number,
-                    perfect: boolean,
-                  ) => void)
-                | null = null;
-
-              const arrowResizeOrigin = (
-                element: ExcalidrawElement,
-                p1: Point,
-                deltaX: number,
-                deltaY: number,
-                pointerX: number,
-                pointerY: number,
-                perfect: boolean,
-              ) => {
-                if (perfect) {
-                  const absPx = p1[0] + element.x;
-                  const absPy = p1[1] + element.y;
-
-                  const { width, height } = getPerfectElementSize(
-                    element.type,
-                    pointerX - element.x - p1[0],
-                    pointerY - element.y - p1[1],
-                  );
-
-                  const dx = element.x + width + p1[0];
-                  const dy = element.y + height + p1[1];
-                  element.x = dx;
-                  element.y = dy;
-                  p1[0] = absPx - element.x;
-                  p1[1] = absPy - element.y;
-                } else {
-                  element.x += deltaX;
-                  element.y += deltaY;
-                  p1[0] -= deltaX;
-                  p1[1] -= deltaY;
-                }
-              };
-
-              const arrowResizeEnd = (
-                element: ExcalidrawElement,
-                p1: Point,
-                deltaX: number,
-                deltaY: number,
-                pointerX: number,
-                pointerY: number,
-                perfect: boolean,
-              ) => {
-                if (perfect) {
-                  const { width, height } = getPerfectElementSize(
-                    element.type,
-                    pointerX - element.x,
-                    pointerY - element.y,
-                  );
-                  p1[0] = width;
-                  p1[1] = height;
-                } else {
-                  p1[0] += deltaX;
-                  p1[1] += deltaY;
-                }
-              };
+  private handleCanvasPointerDown = (
+    event: React.PointerEvent<HTMLCanvasElement>,
+  ) => {
+    if (lastPointerUp !== null) {
+      // Unfortunately, sometimes we don't get a pointerup after a pointerdown,
+      // this can happen when a contextual menu or alert is triggered. In order to avoid
+      // being in a weird state, we clean up on the next pointerdown
+      lastPointerUp(event);
+    }
 
-              const onPointerMove = (event: PointerEvent) => {
-                const target = event.target;
-                if (!(target instanceof HTMLElement)) {
-                  return;
-                }
+    if (isPanning) {
+      return;
+    }
 
-                if (isOverHorizontalScrollBar) {
-                  const x = event.clientX;
-                  const dx = x - lastX;
-                  this.setState({
-                    scrollX: normalizeScroll(
-                      this.state.scrollX - dx / this.state.zoom,
-                    ),
-                  });
-                  lastX = x;
-                  return;
-                }
+    this.setState({ lastPointerDownWith: event.pointerType });
 
-                if (isOverVerticalScrollBar) {
-                  const y = event.clientY;
-                  const dy = y - lastY;
-                  this.setState({
-                    scrollY: normalizeScroll(
-                      this.state.scrollY - dy / this.state.zoom,
-                    ),
-                  });
-                  lastY = y;
-                  return;
-                }
+    // pan canvas on wheel button drag or space+drag
+    if (
+      gesture.pointers.length === 0 &&
+      (event.button === POINTER_BUTTON.WHEEL ||
+        (event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
+    ) {
+      isPanning = true;
+      document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
+      let { clientX: lastX, clientY: lastY } = event;
+      const onPointerMove = (event: PointerEvent) => {
+        const deltaX = lastX - event.clientX;
+        const deltaY = lastY - event.clientY;
+        lastX = event.clientX;
+        lastY = event.clientY;
+
+        this.setState({
+          scrollX: normalizeScroll(
+            this.state.scrollX - deltaX / this.state.zoom,
+          ),
+          scrollY: normalizeScroll(
+            this.state.scrollY - deltaY / this.state.zoom,
+          ),
+        });
+      };
+      const teardown = (lastPointerUp = () => {
+        lastPointerUp = null;
+        isPanning = false;
+        if (!isHoldingSpace) {
+          setCursorForShape(this.state.elementType);
+        }
+        window.removeEventListener("pointermove", onPointerMove);
+        window.removeEventListener("pointerup", teardown);
+        window.removeEventListener("blur", teardown);
+      });
+      window.addEventListener("blur", teardown);
+      window.addEventListener("pointermove", onPointerMove, {
+        passive: true,
+      });
+      window.addEventListener("pointerup", teardown);
+      return;
+    }
 
-                // for arrows, don't start dragging until a given threshold
-                //  to ensure we don't create a 2-point arrow by mistake when
-                //  user clicks mouse in a way that it moves a tiny bit (thus
-                //  triggering pointermove)
-                if (
-                  !draggingOccurred &&
-                  (this.state.elementType === "arrow" ||
-                    this.state.elementType === "line")
-                ) {
-                  const { x, y } = viewportCoordsToSceneCoords(
-                    event,
-                    this.state,
-                    this.canvas,
-                  );
-                  if (distance2d(x, y, originX, originY) < DRAGGING_THRESHOLD) {
-                    return;
-                  }
-                }
+    // only handle left mouse button or touch
+    if (
+      event.button !== POINTER_BUTTON.MAIN &&
+      event.button !== POINTER_BUTTON.TOUCH
+    ) {
+      return;
+    }
 
-                if (isResizingElements && this.state.resizingElement) {
-                  this.setState({ isResizing: true });
-                  const el = this.state.resizingElement;
-                  const selectedElements = getSelectedElements(
-                    elements,
-                    this.state,
-                  );
-                  if (selectedElements.length === 1) {
-                    const { x, y } = viewportCoordsToSceneCoords(
-                      event,
-                      this.state,
-                      this.canvas,
-                    );
-                    const deltaX = x - lastX;
-                    const deltaY = y - lastY;
-                    const element = selectedElements[0];
-                    const isLinear =
-                      element.type === "line" || element.type === "arrow";
-                    switch (resizeHandle) {
-                      case "nw":
-                        if (isLinear && element.points.length === 2) {
-                          const [, p1] = element.points;
-
-                          if (!resizeArrowFn) {
-                            if (p1[0] < 0 || p1[1] < 0) {
-                              resizeArrowFn = arrowResizeEnd;
-                            } else {
-                              resizeArrowFn = arrowResizeOrigin;
-                            }
-                          }
-                          resizeArrowFn(
-                            element,
-                            p1,
-                            deltaX,
-                            deltaY,
-                            x,
-                            y,
-                            event.shiftKey,
-                          );
-                        } else {
-                          element.width -= deltaX;
-                          element.x += deltaX;
-
-                          if (event.shiftKey) {
-                            element.y += element.height - element.width;
-                            element.height = element.width;
-                          } else {
-                            element.height -= deltaY;
-                            element.y += deltaY;
-                          }
-                        }
-                        break;
-                      case "ne":
-                        if (isLinear && element.points.length === 2) {
-                          const [, p1] = element.points;
-                          if (!resizeArrowFn) {
-                            if (p1[0] >= 0) {
-                              resizeArrowFn = arrowResizeEnd;
-                            } else {
-                              resizeArrowFn = arrowResizeOrigin;
-                            }
-                          }
-                          resizeArrowFn(
-                            element,
-                            p1,
-                            deltaX,
-                            deltaY,
-                            x,
-                            y,
-                            event.shiftKey,
-                          );
-                        } else {
-                          element.width += deltaX;
-                          if (event.shiftKey) {
-                            element.y += element.height - element.width;
-                            element.height = element.width;
-                          } else {
-                            element.height -= deltaY;
-                            element.y += deltaY;
-                          }
-                        }
-                        break;
-                      case "sw":
-                        if (isLinear && element.points.length === 2) {
-                          const [, p1] = element.points;
-                          if (!resizeArrowFn) {
-                            if (p1[0] <= 0) {
-                              resizeArrowFn = arrowResizeEnd;
-                            } else {
-                              resizeArrowFn = arrowResizeOrigin;
-                            }
-                          }
-                          resizeArrowFn(
-                            element,
-                            p1,
-                            deltaX,
-                            deltaY,
-                            x,
-                            y,
-                            event.shiftKey,
-                          );
-                        } else {
-                          element.width -= deltaX;
-                          element.x += deltaX;
-                          if (event.shiftKey) {
-                            element.height = element.width;
-                          } else {
-                            element.height += deltaY;
-                          }
-                        }
-                        break;
-                      case "se":
-                        if (isLinear && element.points.length === 2) {
-                          const [, p1] = element.points;
-                          if (!resizeArrowFn) {
-                            if (p1[0] > 0 || p1[1] > 0) {
-                              resizeArrowFn = arrowResizeEnd;
-                            } else {
-                              resizeArrowFn = arrowResizeOrigin;
-                            }
-                          }
-                          resizeArrowFn(
-                            element,
-                            p1,
-                            deltaX,
-                            deltaY,
-                            x,
-                            y,
-                            event.shiftKey,
-                          );
-                        } else {
-                          if (event.shiftKey) {
-                            element.width += deltaX;
-                            element.height = element.width;
-                          } else {
-                            element.width += deltaX;
-                            element.height += deltaY;
-                          }
-                        }
-                        break;
-                      case "n": {
-                        element.height -= deltaY;
-                        element.y += deltaY;
-
-                        if (element.points.length > 0) {
-                          const len = element.points.length;
-
-                          const points = [...element.points].sort(
-                            (a, b) => a[1] - b[1],
-                          );
-
-                          for (let i = 1; i < points.length; ++i) {
-                            const pnt = points[i];
-                            pnt[1] -= deltaY / (len - i);
-                          }
-                        }
-                        break;
-                      }
-                      case "w": {
-                        element.width -= deltaX;
-                        element.x += deltaX;
-
-                        if (element.points.length > 0) {
-                          const len = element.points.length;
-                          const points = [...element.points].sort(
-                            (a, b) => a[0] - b[0],
-                          );
-
-                          for (let i = 0; i < points.length; ++i) {
-                            const pnt = points[i];
-                            pnt[0] -= deltaX / (len - i);
-                          }
-                        }
-                        break;
-                      }
-                      case "s": {
-                        element.height += deltaY;
-                        if (element.points.length > 0) {
-                          const len = element.points.length;
-                          const points = [...element.points].sort(
-                            (a, b) => a[1] - b[1],
-                          );
-
-                          for (let i = 1; i < points.length; ++i) {
-                            const pnt = points[i];
-                            pnt[1] += deltaY / (len - i);
-                          }
-                        }
-                        break;
-                      }
-                      case "e": {
-                        element.width += deltaX;
-                        if (element.points.length > 0) {
-                          const len = element.points.length;
-                          const points = [...element.points].sort(
-                            (a, b) => a[0] - b[0],
-                          );
-
-                          for (let i = 1; i < points.length; ++i) {
-                            const pnt = points[i];
-                            pnt[0] += deltaX / (len - i);
-                          }
-                        }
-                        break;
-                      }
-                    }
-
-                    if (resizeHandle) {
-                      resizeHandle = normalizeResizeHandle(
-                        element,
-                        resizeHandle,
-                      );
-                    }
-                    normalizeDimensions(element);
-
-                    document.documentElement.style.cursor = getCursorForResizingElement(
-                      { element, resizeHandle },
-                    );
-                    el.x = element.x;
-                    el.y = element.y;
-                    invalidateShapeForElement(el);
-
-                    lastX = x;
-                    lastY = y;
-                    this.setState({});
-                    return;
-                  }
-                }
+    gesture.pointers.push({
+      id: event.pointerId,
+      x: event.clientX,
+      y: event.clientY,
+    });
+    if (gesture.pointers.length === 2) {
+      gesture.lastCenter = getCenter(gesture.pointers);
+      gesture.initialScale = this.state.zoom;
+      gesture.initialDistance = getDistance(gesture.pointers);
+    }
 
-                if (
-                  hitElement &&
-                  this.state.selectedElementIds[hitElement.id]
-                ) {
-                  // Marking that click was used for dragging to check
-                  // if elements should be deselected on pointerup
-                  draggingOccurred = true;
-                  const selectedElements = getSelectedElements(
-                    elements,
-                    this.state,
-                  );
-                  if (selectedElements.length > 0) {
-                    const { x, y } = viewportCoordsToSceneCoords(
-                      event,
-                      this.state,
-                      this.canvas,
-                    );
-
-                    selectedElements.forEach(element => {
-                      element.x += x - lastX;
-                      element.y += y - lastY;
-                    });
-                    lastX = x;
-                    lastY = y;
-                    this.setState({});
-                    return;
-                  }
-                }
+    // fixes pointermove causing selection of UI texts #32
+    event.preventDefault();
+    // Preventing the event above disables default behavior
+    //  of defocusing potentially focused element, which is what we
+    //  want when clicking inside the canvas.
+    if (document.activeElement instanceof HTMLElement) {
+      document.activeElement.blur();
+    }
 
-                // It is very important to read this.state within each move event,
-                // otherwise we would read a stale one!
-                const draggingElement = this.state.draggingElement;
-                if (!draggingElement) {
-                  return;
-                }
+    // don't select while panning
+    if (gesture.pointers.length > 1) {
+      return;
+    }
 
-                const { x, y } = viewportCoordsToSceneCoords(
-                  event,
-                  this.state,
-                  this.canvas,
-                );
+    // Handle scrollbars dragging
+    const {
+      isOverHorizontalScrollBar,
+      isOverVerticalScrollBar,
+    } = isOverScrollBars(currentScrollBars, event.clientX, event.clientY);
 
-                let width = distance(originX, x);
-                let height = distance(originY, y);
-
-                const isLinear =
-                  this.state.elementType === "line" ||
-                  this.state.elementType === "arrow";
-
-                if (isLinear) {
-                  draggingOccurred = true;
-                  const points = draggingElement.points;
-                  let dx = x - draggingElement.x;
-                  let dy = y - draggingElement.y;
-
-                  if (event.shiftKey && points.length === 2) {
-                    ({ width: dx, height: dy } = getPerfectElementSize(
-                      this.state.elementType,
-                      dx,
-                      dy,
-                    ));
-                  }
+    const { x, y } = viewportCoordsToSceneCoords(
+      event,
+      this.state,
+      this.canvas,
+    );
+    let lastX = x;
+    let lastY = y;
 
-                  if (points.length === 1) {
-                    points.push([dx, dy]);
-                  } else if (points.length > 1) {
-                    const pnt = points[points.length - 1];
-                    pnt[0] = dx;
-                    pnt[1] = dy;
-                  }
-                } else {
-                  if (event.shiftKey) {
-                    ({ width, height } = getPerfectElementSize(
-                      this.state.elementType,
-                      width,
-                      y < originY ? -height : height,
-                    ));
-
-                    if (height < 0) {
-                      height = -height;
-                    }
-                  }
+    if (
+      (isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
+      !this.state.multiElement
+    ) {
+      isDraggingScrollBar = true;
+      lastX = event.clientX;
+      lastY = event.clientY;
+      const onPointerMove = (event: PointerEvent) => {
+        const target = event.target;
+        if (!(target instanceof HTMLElement)) {
+          return;
+        }
 
-                  draggingElement.x = x < originX ? originX - width : originX;
-                  draggingElement.y = y < originY ? originY - height : originY;
+        if (isOverHorizontalScrollBar) {
+          const x = event.clientX;
+          const dx = x - lastX;
+          this.setState({
+            scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
+          });
+          lastX = x;
+          return;
+        }
 
-                  draggingElement.width = width;
-                  draggingElement.height = height;
-                }
+        if (isOverVerticalScrollBar) {
+          const y = event.clientY;
+          const dy = y - lastY;
+          this.setState({
+            scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
+          });
+          lastY = y;
+        }
+      };
 
-                invalidateShapeForElement(draggingElement);
+      const onPointerUp = () => {
+        isDraggingScrollBar = false;
+        setCursorForShape(this.state.elementType);
+        lastPointerUp = null;
+        window.removeEventListener("pointermove", onPointerMove);
+        window.removeEventListener("pointerup", onPointerUp);
+      };
 
-                if (this.state.elementType === "selection") {
-                  if (
-                    !event.shiftKey &&
-                    isSomeElementSelected(elements, this.state)
-                  ) {
-                    this.setState({ selectedElementIds: {} });
-                  }
-                  const elementsWithinSelection = getElementsWithinSelection(
-                    elements,
-                    draggingElement,
-                  );
-                  this.setState(prevState => ({
-                    selectedElementIds: {
-                      ...prevState.selectedElementIds,
-                      ...Object.fromEntries(
-                        elementsWithinSelection.map(element => [
-                          element.id,
-                          true,
-                        ]),
-                      ),
-                    },
-                  }));
-                }
-                this.setState({});
-              };
-
-              const onPointerUp = (event: PointerEvent) => {
-                const {
-                  draggingElement,
-                  resizingElement,
-                  multiElement,
-                  elementType,
-                  elementLocked,
-                } = this.state;
-
-                this.setState({
-                  isResizing: false,
-                  resizingElement: null,
-                  selectionElement: null,
-                });
+      lastPointerUp = onPointerUp;
 
-                resizeArrowFn = null;
-                lastPointerUp = null;
-                window.removeEventListener("pointermove", onPointerMove);
-                window.removeEventListener("pointerup", onPointerUp);
+      window.addEventListener("pointermove", onPointerMove);
+      window.addEventListener("pointerup", onPointerUp);
+      return;
+    }
 
-                if (elementType === "arrow" || elementType === "line") {
-                  if (draggingElement!.points.length > 1) {
-                    history.resumeRecording();
-                    this.setState({});
-                  }
-                  if (!draggingOccurred && draggingElement && !multiElement) {
-                    const { x, y } = viewportCoordsToSceneCoords(
-                      event,
-                      this.state,
-                      this.canvas,
-                    );
-                    draggingElement.points.push([
-                      x - draggingElement.x,
-                      y - draggingElement.y,
-                    ]);
-                    invalidateShapeForElement(draggingElement);
-                    this.setState({ multiElement: this.state.draggingElement });
-                  } else if (draggingOccurred && !multiElement) {
-                    if (!elementLocked) {
-                      resetCursor();
-                      this.setState(prevState => ({
-                        draggingElement: null,
-                        elementType: "selection",
-                        selectedElementIds: {
-                          ...prevState.selectedElementIds,
-                          [this.state.draggingElement!.id]: true,
-                        },
-                      }));
-                    } else {
-                      this.setState(prevState => ({
-                        draggingElement: null,
-                        selectedElementIds: {
-                          ...prevState.selectedElementIds,
-                          [this.state.draggingElement!.id]: true,
-                        },
-                      }));
-                    }
-                  }
-                  return;
-                }
+    const originX = x;
+    const originY = y;
+
+    let element = newElement(
+      this.state.elementType,
+      x,
+      y,
+      this.state.currentItemStrokeColor,
+      this.state.currentItemBackgroundColor,
+      this.state.currentItemFillStyle,
+      this.state.currentItemStrokeWidth,
+      this.state.currentItemRoughness,
+      this.state.currentItemOpacity,
+    );
 
-                if (
-                  elementType !== "selection" &&
-                  draggingElement &&
-                  isInvisiblySmallElement(draggingElement)
-                ) {
-                  // remove invisible element which was added in onPointerDown
-                  elements = elements.slice(0, -1);
-                  this.setState({
-                    draggingElement: null,
-                  });
-                  return;
-                }
+    if (isTextElement(element)) {
+      element = newTextElement(element, "", this.state.currentItemFont);
+    }
 
-                if (normalizeDimensions(draggingElement)) {
-                  this.setState({});
-                }
+    type ResizeTestType = ReturnType<typeof resizeTest>;
+    let resizeHandle: ResizeTestType = false;
+    let isResizingElements = false;
+    let draggingOccurred = false;
+    let hitElement: ExcalidrawElement | null = null;
+    let elementIsAddedToSelection = false;
+    if (this.state.elementType === "selection") {
+      const resizeElement = getElementWithResizeHandler(
+        elements,
+        this.state,
+        { x, y },
+        this.state.zoom,
+        event.pointerType,
+      );
 
-                if (resizingElement) {
-                  history.resumeRecording();
-                  this.setState({});
-                }
+      const selectedElements = getSelectedElements(elements, this.state);
+      if (selectedElements.length === 1 && resizeElement) {
+        this.setState({
+          resizingElement: resizeElement ? resizeElement.element : null,
+        });
 
-                if (
-                  resizingElement &&
-                  isInvisiblySmallElement(resizingElement)
-                ) {
-                  elements = elements.filter(
-                    el => el.id !== resizingElement.id,
-                  );
-                }
+        resizeHandle = resizeElement.resizeHandle;
+        document.documentElement.style.cursor = getCursorForResizingElement(
+          resizeElement,
+        );
+        isResizingElements = true;
+      } else {
+        hitElement = getElementAtPosition(
+          elements,
+          this.state,
+          x,
+          y,
+          this.state.zoom,
+        );
+        // clear selection if shift is not clicked
+        if (
+          !(hitElement && this.state.selectedElementIds[hitElement.id]) &&
+          !event.shiftKey
+        ) {
+          this.setState({ selectedElementIds: {} });
+        }
 
-                // If click occurred on already selected element
-                // it is needed to remove selection from other elements
-                // or if SHIFT or META key pressed remove selection
-                // from hitted element
-                //
-                // If click occurred and elements were dragged or some element
-                // was added to selection (on pointerdown phase) we need to keep
-                // selection unchanged
-                if (
-                  hitElement &&
-                  !draggingOccurred &&
-                  !elementIsAddedToSelection
-                ) {
-                  if (event.shiftKey) {
-                    this.setState(prevState => ({
-                      selectedElementIds: {
-                        ...prevState.selectedElementIds,
-                        [hitElement!.id]: false,
-                      },
-                    }));
-                  } else {
-                    this.setState(prevState => ({
-                      selectedElementIds: { [hitElement!.id]: true },
-                    }));
-                  }
-                }
+        // If we click on something
+        if (hitElement) {
+          // deselect if item is selected
+          // if shift is not clicked, this will always return true
+          // otherwise, it will trigger selection based on current
+          // state of the box
+          if (!this.state.selectedElementIds[hitElement.id]) {
+            this.setState(prevState => ({
+              selectedElementIds: {
+                ...prevState.selectedElementIds,
+                [hitElement!.id]: true,
+              },
+            }));
+            elements = elements.slice();
+            elementIsAddedToSelection = true;
+          }
 
-                if (draggingElement === null) {
-                  // if no element is clicked, clear the selection and redraw
-                  this.setState({ selectedElementIds: {} });
-                  return;
-                }
+          // We duplicate the selected element if alt is pressed on pointer down
+          if (event.altKey) {
+            // Move the currently selected elements to the top of the z index stack, and
+            // put the duplicates where the selected elements used to be.
+            const nextElements = [];
+            const elementsToAppend = [];
+            for (const element of elements) {
+              if (this.state.selectedElementIds[element.id]) {
+                nextElements.push(duplicateElement(element));
+                elementsToAppend.push(element);
+              } else {
+                nextElements.push(element);
+              }
+            }
+            elements = [...nextElements, ...elementsToAppend];
+          }
+        }
+      }
+    } else {
+      this.setState({ selectedElementIds: {} });
+    }
 
-                if (!elementLocked) {
-                  this.setState(prevState => ({
-                    selectedElementIds: {
-                      ...prevState.selectedElementIds,
-                      [draggingElement.id]: true,
-                    },
-                  }));
-                }
+    if (isTextElement(element)) {
+      // if we're currently still editing text, clicking outside
+      //  should only finalize it, not create another (irrespective
+      //  of state.elementLocked)
+      if (this.state.editingElement?.type === "text") {
+        return;
+      }
+      if (elementIsAddedToSelection) {
+        element = hitElement!;
+      }
+      let textX = event.clientX;
+      let textY = event.clientY;
+      if (!event.altKey) {
+        const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
+          x,
+          y,
+        );
+        if (snappedToCenterPosition) {
+          element.x = snappedToCenterPosition.elementCenterX;
+          element.y = snappedToCenterPosition.elementCenterY;
+          textX = snappedToCenterPosition.wysiwygX;
+          textY = snappedToCenterPosition.wysiwygY;
+        }
+      }
 
-                if (
-                  elementType !== "selection" ||
-                  isSomeElementSelected(elements, this.state)
-                ) {
-                  history.resumeRecording();
-                }
+      const resetSelection = () => {
+        this.setState({
+          draggingElement: null,
+          editingElement: null,
+        });
+      };
+
+      textWysiwyg({
+        initText: "",
+        x: textX,
+        y: textY,
+        strokeColor: this.state.currentItemStrokeColor,
+        opacity: this.state.currentItemOpacity,
+        font: this.state.currentItemFont,
+        zoom: this.state.zoom,
+        onSubmit: text => {
+          if (text) {
+            elements = [
+              ...elements,
+              {
+                ...newTextElement(element, text, this.state.currentItemFont),
+              },
+            ];
+          }
+          this.setState(prevState => ({
+            selectedElementIds: {
+              ...prevState.selectedElementIds,
+              [element.id]: true,
+            },
+          }));
+          if (this.state.elementLocked) {
+            setCursorForShape(this.state.elementType);
+          }
+          history.resumeRecording();
+          resetSelection();
+        },
+        onCancel: () => {
+          resetSelection();
+        },
+      });
+      resetCursor();
+      if (!this.state.elementLocked) {
+        this.setState({
+          editingElement: element,
+          elementType: "selection",
+        });
+      } else {
+        this.setState({
+          editingElement: element,
+        });
+      }
+      return;
+    } else if (
+      this.state.elementType === "arrow" ||
+      this.state.elementType === "line"
+    ) {
+      if (this.state.multiElement) {
+        const { multiElement } = this.state;
+        const { x: rx, y: ry } = multiElement;
+        this.setState(prevState => ({
+          selectedElementIds: {
+            ...prevState.selectedElementIds,
+            [multiElement.id]: true,
+          },
+        }));
+        multiElement.points.push([x - rx, y - ry]);
+        invalidateShapeForElement(multiElement);
+      } else {
+        this.setState(prevState => ({
+          selectedElementIds: {
+            ...prevState.selectedElementIds,
+            [element.id]: false,
+          },
+        }));
+        element.points.push([0, 0]);
+        invalidateShapeForElement(element);
+        elements = [...elements, element];
+        this.setState({
+          draggingElement: element,
+        });
+      }
+    } else if (element.type === "selection") {
+      this.setState({
+        selectionElement: element,
+        draggingElement: element,
+      });
+    } else {
+      elements = [...elements, element];
+      this.setState({ multiElement: null, draggingElement: element });
+    }
 
-                if (!elementLocked) {
-                  resetCursor();
-                  this.setState({
-                    draggingElement: null,
-                    elementType: "selection",
-                  });
-                } else {
-                  this.setState({
-                    draggingElement: null,
-                  });
-                }
-              };
+    let resizeArrowFn:
+      | ((
+          element: ExcalidrawElement,
+          p1: Point,
+          deltaX: number,
+          deltaY: number,
+          pointerX: number,
+          pointerY: number,
+          perfect: boolean,
+        ) => void)
+      | null = null;
+
+    const arrowResizeOrigin = (
+      element: ExcalidrawElement,
+      p1: Point,
+      deltaX: number,
+      deltaY: number,
+      pointerX: number,
+      pointerY: number,
+      perfect: boolean,
+    ) => {
+      if (perfect) {
+        const absPx = p1[0] + element.x;
+        const absPy = p1[1] + element.y;
+
+        const { width, height } = getPerfectElementSize(
+          element.type,
+          pointerX - element.x - p1[0],
+          pointerY - element.y - p1[1],
+        );
 
-              lastPointerUp = onPointerUp;
+        const dx = element.x + width + p1[0];
+        const dy = element.y + height + p1[1];
+        element.x = dx;
+        element.y = dy;
+        p1[0] = absPx - element.x;
+        p1[1] = absPy - element.y;
+      } else {
+        element.x += deltaX;
+        element.y += deltaY;
+        p1[0] -= deltaX;
+        p1[1] -= deltaY;
+      }
+    };
 
-              window.addEventListener("pointermove", onPointerMove);
-              window.addEventListener("pointerup", onPointerUp);
-            }}
-            onDoubleClick={event => {
-              resetCursor();
+    const arrowResizeEnd = (
+      element: ExcalidrawElement,
+      p1: Point,
+      deltaX: number,
+      deltaY: number,
+      pointerX: number,
+      pointerY: number,
+      perfect: boolean,
+    ) => {
+      if (perfect) {
+        const { width, height } = getPerfectElementSize(
+          element.type,
+          pointerX - element.x,
+          pointerY - element.y,
+        );
+        p1[0] = width;
+        p1[1] = height;
+      } else {
+        p1[0] += deltaX;
+        p1[1] += deltaY;
+      }
+    };
 
-              const { x, y } = viewportCoordsToSceneCoords(
-                event,
-                this.state,
-                this.canvas,
-              );
+    const onPointerMove = (event: PointerEvent) => {
+      const target = event.target;
+      if (!(target instanceof HTMLElement)) {
+        return;
+      }
 
-              const elementAtPosition = getElementAtPosition(
-                elements,
-                this.state,
-                x,
-                y,
-                this.state.zoom,
-              );
+      if (isOverHorizontalScrollBar) {
+        const x = event.clientX;
+        const dx = x - lastX;
+        this.setState({
+          scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
+        });
+        lastX = x;
+        return;
+      }
 
-              const element =
-                elementAtPosition && isTextElement(elementAtPosition)
-                  ? elementAtPosition
-                  : newTextElement(
-                      newElement(
-                        "text",
-                        x,
-                        y,
-                        this.state.currentItemStrokeColor,
-                        this.state.currentItemBackgroundColor,
-                        this.state.currentItemFillStyle,
-                        this.state.currentItemStrokeWidth,
-                        this.state.currentItemRoughness,
-                        this.state.currentItemOpacity,
-                      ),
-                      "", // default text
-                      this.state.currentItemFont, // default font
-                    );
-
-              this.setState({ editingElement: element });
-
-              let textX = event.clientX;
-              let textY = event.clientY;
-
-              if (elementAtPosition && isTextElement(elementAtPosition)) {
-                elements = elements.filter(
-                  element => element.id !== elementAtPosition.id,
-                );
-                this.setState({});
-
-                const centerElementX =
-                  elementAtPosition.x + elementAtPosition.width / 2;
-                const centerElementY =
-                  elementAtPosition.y + elementAtPosition.height / 2;
-
-                const {
-                  x: centerElementXInViewport,
-                  y: centerElementYInViewport,
-                } = sceneCoordsToViewportCoords(
-                  { sceneX: centerElementX, sceneY: centerElementY },
-                  this.state,
-                  this.canvas,
-                );
+      if (isOverVerticalScrollBar) {
+        const y = event.clientY;
+        const dy = y - lastY;
+        this.setState({
+          scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
+        });
+        lastY = y;
+        return;
+      }
 
-                textX = centerElementXInViewport;
-                textY = centerElementYInViewport;
+      // for arrows, don't start dragging until a given threshold
+      //  to ensure we don't create a 2-point arrow by mistake when
+      //  user clicks mouse in a way that it moves a tiny bit (thus
+      //  triggering pointermove)
+      if (
+        !draggingOccurred &&
+        (this.state.elementType === "arrow" ||
+          this.state.elementType === "line")
+      ) {
+        const { x, y } = viewportCoordsToSceneCoords(
+          event,
+          this.state,
+          this.canvas,
+        );
+        if (distance2d(x, y, originX, originY) < DRAGGING_THRESHOLD) {
+          return;
+        }
+      }
 
-                // x and y will change after calling newTextElement function
-                element.x = centerElementX;
-                element.y = centerElementY;
-              } else if (!event.altKey) {
-                const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
+      if (isResizingElements && this.state.resizingElement) {
+        this.setState({ isResizing: true });
+        const el = this.state.resizingElement;
+        const selectedElements = getSelectedElements(elements, this.state);
+        if (selectedElements.length === 1) {
+          const { x, y } = viewportCoordsToSceneCoords(
+            event,
+            this.state,
+            this.canvas,
+          );
+          const deltaX = x - lastX;
+          const deltaY = y - lastY;
+          const element = selectedElements[0];
+          const isLinear = element.type === "line" || element.type === "arrow";
+          switch (resizeHandle) {
+            case "nw":
+              if (isLinear && element.points.length === 2) {
+                const [, p1] = element.points;
+
+                if (!resizeArrowFn) {
+                  if (p1[0] < 0 || p1[1] < 0) {
+                    resizeArrowFn = arrowResizeEnd;
+                  } else {
+                    resizeArrowFn = arrowResizeOrigin;
+                  }
+                }
+                resizeArrowFn(
+                  element,
+                  p1,
+                  deltaX,
+                  deltaY,
                   x,
                   y,
+                  event.shiftKey,
                 );
+              } else {
+                element.width -= deltaX;
+                element.x += deltaX;
 
-                if (snappedToCenterPosition) {
-                  element.x = snappedToCenterPosition.elementCenterX;
-                  element.y = snappedToCenterPosition.elementCenterY;
-                  textX = snappedToCenterPosition.wysiwygX;
-                  textY = snappedToCenterPosition.wysiwygY;
+                if (event.shiftKey) {
+                  element.y += element.height - element.width;
+                  element.height = element.width;
+                } else {
+                  element.height -= deltaY;
+                  element.y += deltaY;
                 }
               }
-
-              const resetSelection = () => {
-                this.setState({
-                  draggingElement: null,
-                  editingElement: null,
-                });
-              };
-
-              textWysiwyg({
-                initText: element.text,
-                x: textX,
-                y: textY,
-                strokeColor: element.strokeColor,
-                font: element.font,
-                opacity: this.state.currentItemOpacity,
-                zoom: this.state.zoom,
-                onSubmit: text => {
-                  if (text) {
-                    elements = [
-                      ...elements,
-                      {
-                        // we need to recreate the element to update dimensions &
-                        //  position
-                        ...newTextElement(element, text, element.font),
-                      },
-                    ];
+              break;
+            case "ne":
+              if (isLinear && element.points.length === 2) {
+                const [, p1] = element.points;
+                if (!resizeArrowFn) {
+                  if (p1[0] >= 0) {
+                    resizeArrowFn = arrowResizeEnd;
+                  } else {
+                    resizeArrowFn = arrowResizeOrigin;
                   }
-                  this.setState(prevState => ({
-                    selectedElementIds: {
-                      ...prevState.selectedElementIds,
-                      [element.id]: true,
-                    },
-                  }));
-                  history.resumeRecording();
-                  resetSelection();
-                },
-                onCancel: () => {
-                  resetSelection();
-                },
-              });
-            }}
-            onPointerMove={event => {
-              gesture.pointers = gesture.pointers.map(pointer =>
-                pointer.id === event.pointerId
-                  ? {
-                      id: event.pointerId,
-                      x: event.clientX,
-                      y: event.clientY,
-                    }
-                  : pointer,
-              );
-
-              if (gesture.pointers.length === 2) {
-                const center = getCenter(gesture.pointers);
-                const deltaX = center.x - gesture.lastCenter!.x;
-                const deltaY = center.y - gesture.lastCenter!.y;
-                gesture.lastCenter = center;
-
-                const distance = getDistance(gesture.pointers);
-                const scaleFactor = distance / gesture.initialDistance!;
-
-                this.setState({
-                  scrollX: normalizeScroll(
-                    this.state.scrollX + deltaX / this.state.zoom,
-                  ),
-                  scrollY: normalizeScroll(
-                    this.state.scrollY + deltaY / this.state.zoom,
-                  ),
-                  zoom: getNormalizedZoom(gesture.initialScale! * scaleFactor),
-                });
+                }
+                resizeArrowFn(
+                  element,
+                  p1,
+                  deltaX,
+                  deltaY,
+                  x,
+                  y,
+                  event.shiftKey,
+                );
               } else {
-                gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
+                element.width += deltaX;
+                if (event.shiftKey) {
+                  element.y += element.height - element.width;
+                  element.height = element.width;
+                } else {
+                  element.height -= deltaY;
+                  element.y += deltaY;
+                }
               }
-
-              if (isHoldingSpace || isPanning || isDraggingScrollBar) {
-                return;
+              break;
+            case "sw":
+              if (isLinear && element.points.length === 2) {
+                const [, p1] = element.points;
+                if (!resizeArrowFn) {
+                  if (p1[0] <= 0) {
+                    resizeArrowFn = arrowResizeEnd;
+                  } else {
+                    resizeArrowFn = arrowResizeOrigin;
+                  }
+                }
+                resizeArrowFn(
+                  element,
+                  p1,
+                  deltaX,
+                  deltaY,
+                  x,
+                  y,
+                  event.shiftKey,
+                );
+              } else {
+                element.width -= deltaX;
+                element.x += deltaX;
+                if (event.shiftKey) {
+                  element.height = element.width;
+                } else {
+                  element.height += deltaY;
+                }
               }
-              const {
-                isOverHorizontalScrollBar,
-                isOverVerticalScrollBar,
-              } = isOverScrollBars(
-                currentScrollBars,
-                event.clientX,
-                event.clientY,
-              );
-              const isOverScrollBar =
-                isOverVerticalScrollBar || isOverHorizontalScrollBar;
-              if (!this.state.draggingElement && !this.state.multiElement) {
-                if (isOverScrollBar) {
-                  resetCursor();
+              break;
+            case "se":
+              if (isLinear && element.points.length === 2) {
+                const [, p1] = element.points;
+                if (!resizeArrowFn) {
+                  if (p1[0] > 0 || p1[1] > 0) {
+                    resizeArrowFn = arrowResizeEnd;
+                  } else {
+                    resizeArrowFn = arrowResizeOrigin;
+                  }
+                }
+                resizeArrowFn(
+                  element,
+                  p1,
+                  deltaX,
+                  deltaY,
+                  x,
+                  y,
+                  event.shiftKey,
+                );
+              } else {
+                if (event.shiftKey) {
+                  element.width += deltaX;
+                  element.height = element.width;
                 } else {
-                  setCursorForShape(this.state.elementType);
+                  element.width += deltaX;
+                  element.height += deltaY;
                 }
               }
+              break;
+            case "n": {
+              element.height -= deltaY;
+              element.y += deltaY;
 
-              const { x, y } = viewportCoordsToSceneCoords(
-                event,
-                this.state,
-                this.canvas,
-              );
-              if (this.state.multiElement) {
-                const { multiElement } = this.state;
-                const originX = multiElement.x;
-                const originY = multiElement.y;
-                const points = multiElement.points;
-                const pnt = points[points.length - 1];
-                pnt[0] = x - originX;
-                pnt[1] = y - originY;
-                invalidateShapeForElement(multiElement);
-                this.setState({});
-                return;
-              }
+              if (element.points.length > 0) {
+                const len = element.points.length;
 
-              const hasDeselectedButton = Boolean(event.buttons);
-              if (
-                hasDeselectedButton ||
-                this.state.elementType !== "selection"
-              ) {
-                return;
-              }
+                const points = [...element.points].sort((a, b) => a[1] - b[1]);
 
-              const selectedElements = getSelectedElements(
-                elements,
-                this.state,
-              );
-              if (selectedElements.length === 1 && !isOverScrollBar) {
-                const resizeElement = getElementWithResizeHandler(
-                  elements,
-                  this.state,
-                  { x, y },
-                  this.state.zoom,
-                  event.pointerType,
-                );
-                if (resizeElement && resizeElement.resizeHandle) {
-                  document.documentElement.style.cursor = getCursorForResizingElement(
-                    resizeElement,
-                  );
-                  return;
+                for (let i = 1; i < points.length; ++i) {
+                  const pnt = points[i];
+                  pnt[1] -= deltaY / (len - i);
                 }
               }
-              const hitElement = getElementAtPosition(
-                elements,
-                this.state,
-                x,
-                y,
-                this.state.zoom,
-              );
-              document.documentElement.style.cursor =
-                hitElement && !isOverScrollBar ? "move" : "";
-            }}
-            onPointerUp={this.removePointer}
-            onPointerCancel={this.removePointer}
-            onDrop={event => {
-              const file = event.dataTransfer.files[0];
-              if (
-                file?.type === "application/json" ||
-                file?.name.endsWith(".excalidraw")
-              ) {
-                loadFromBlob(file)
-                  .then(({ elements, appState }) =>
-                    this.syncActionResult({ elements, appState }),
-                  )
-                  .catch(error => console.error(error));
+              break;
+            }
+            case "w": {
+              element.width -= deltaX;
+              element.x += deltaX;
+
+              if (element.points.length > 0) {
+                const len = element.points.length;
+                const points = [...element.points].sort((a, b) => a[0] - b[0]);
+
+                for (let i = 0; i < points.length; ++i) {
+                  const pnt = points[i];
+                  pnt[0] -= deltaX / (len - i);
+                }
               }
-            }}
-          >
-            {t("labels.drawingCanvas")}
-          </canvas>
-        </main>
-      </div>
-    );
-  }
+              break;
+            }
+            case "s": {
+              element.height += deltaY;
+              if (element.points.length > 0) {
+                const len = element.points.length;
+                const points = [...element.points].sort((a, b) => a[1] - b[1]);
+
+                for (let i = 1; i < points.length; ++i) {
+                  const pnt = points[i];
+                  pnt[1] += deltaY / (len - i);
+                }
+              }
+              break;
+            }
+            case "e": {
+              element.width += deltaX;
+              if (element.points.length > 0) {
+                const len = element.points.length;
+                const points = [...element.points].sort((a, b) => a[0] - b[0]);
+
+                for (let i = 1; i < points.length; ++i) {
+                  const pnt = points[i];
+                  pnt[0] += deltaX / (len - i);
+                }
+              }
+              break;
+            }
+          }
+
+          if (resizeHandle) {
+            resizeHandle = normalizeResizeHandle(element, resizeHandle);
+          }
+          normalizeDimensions(element);
+
+          document.documentElement.style.cursor = getCursorForResizingElement({
+            element,
+            resizeHandle,
+          });
+          el.x = element.x;
+          el.y = element.y;
+          invalidateShapeForElement(el);
+
+          lastX = x;
+          lastY = y;
+          this.setState({});
+          return;
+        }
+      }
+
+      if (hitElement && this.state.selectedElementIds[hitElement.id]) {
+        // Marking that click was used for dragging to check
+        // if elements should be deselected on pointerup
+        draggingOccurred = true;
+        const selectedElements = getSelectedElements(elements, this.state);
+        if (selectedElements.length > 0) {
+          const { x, y } = viewportCoordsToSceneCoords(
+            event,
+            this.state,
+            this.canvas,
+          );
+
+          selectedElements.forEach(element => {
+            element.x += x - lastX;
+            element.y += y - lastY;
+          });
+          lastX = x;
+          lastY = y;
+          this.setState({});
+          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;
+      if (!draggingElement) {
+        return;
+      }
+
+      const { x, y } = viewportCoordsToSceneCoords(
+        event,
+        this.state,
+        this.canvas,
+      );
+
+      let width = distance(originX, x);
+      let height = distance(originY, y);
+
+      const isLinear =
+        this.state.elementType === "line" || this.state.elementType === "arrow";
+
+      if (isLinear) {
+        draggingOccurred = true;
+        const points = draggingElement.points;
+        let dx = x - draggingElement.x;
+        let dy = y - draggingElement.y;
+
+        if (event.shiftKey && points.length === 2) {
+          ({ width: dx, height: dy } = getPerfectElementSize(
+            this.state.elementType,
+            dx,
+            dy,
+          ));
+        }
+
+        if (points.length === 1) {
+          points.push([dx, dy]);
+        } else if (points.length > 1) {
+          const pnt = points[points.length - 1];
+          pnt[0] = dx;
+          pnt[1] = dy;
+        }
+      } else {
+        if (event.shiftKey) {
+          ({ width, height } = getPerfectElementSize(
+            this.state.elementType,
+            width,
+            y < originY ? -height : height,
+          ));
+
+          if (height < 0) {
+            height = -height;
+          }
+        }
+
+        draggingElement.x = x < originX ? originX - width : originX;
+        draggingElement.y = y < originY ? originY - height : originY;
+
+        draggingElement.width = width;
+        draggingElement.height = height;
+      }
+
+      invalidateShapeForElement(draggingElement);
+
+      if (this.state.elementType === "selection") {
+        if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
+          this.setState({ selectedElementIds: {} });
+        }
+        const elementsWithinSelection = getElementsWithinSelection(
+          elements,
+          draggingElement,
+        );
+        this.setState(prevState => ({
+          selectedElementIds: {
+            ...prevState.selectedElementIds,
+            ...Object.fromEntries(
+              elementsWithinSelection.map(element => [element.id, true]),
+            ),
+          },
+        }));
+      }
+      this.setState({});
+    };
+
+    const onPointerUp = (event: PointerEvent) => {
+      const {
+        draggingElement,
+        resizingElement,
+        multiElement,
+        elementType,
+        elementLocked,
+      } = this.state;
+
+      this.setState({
+        isResizing: false,
+        resizingElement: null,
+        selectionElement: null,
+      });
+
+      resizeArrowFn = null;
+      lastPointerUp = null;
+      window.removeEventListener("pointermove", onPointerMove);
+      window.removeEventListener("pointerup", onPointerUp);
+
+      if (elementType === "arrow" || elementType === "line") {
+        if (draggingElement!.points.length > 1) {
+          history.resumeRecording();
+          this.setState({});
+        }
+        if (!draggingOccurred && draggingElement && !multiElement) {
+          const { x, y } = viewportCoordsToSceneCoords(
+            event,
+            this.state,
+            this.canvas,
+          );
+          draggingElement.points.push([
+            x - draggingElement.x,
+            y - draggingElement.y,
+          ]);
+          invalidateShapeForElement(draggingElement);
+          this.setState({ multiElement: this.state.draggingElement });
+        } else if (draggingOccurred && !multiElement) {
+          if (!elementLocked) {
+            resetCursor();
+            this.setState(prevState => ({
+              draggingElement: null,
+              elementType: "selection",
+              selectedElementIds: {
+                ...prevState.selectedElementIds,
+                [this.state.draggingElement!.id]: true,
+              },
+            }));
+          } else {
+            this.setState(prevState => ({
+              draggingElement: null,
+              selectedElementIds: {
+                ...prevState.selectedElementIds,
+                [this.state.draggingElement!.id]: true,
+              },
+            }));
+          }
+        }
+        return;
+      }
+
+      if (
+        elementType !== "selection" &&
+        draggingElement &&
+        isInvisiblySmallElement(draggingElement)
+      ) {
+        // remove invisible element which was added in onPointerDown
+        elements = elements.slice(0, -1);
+        this.setState({
+          draggingElement: null,
+        });
+        return;
+      }
+
+      if (normalizeDimensions(draggingElement)) {
+        this.setState({});
+      }
+
+      if (resizingElement) {
+        history.resumeRecording();
+        this.setState({});
+      }
+
+      if (resizingElement && isInvisiblySmallElement(resizingElement)) {
+        elements = elements.filter(el => el.id !== resizingElement.id);
+      }
+
+      // If click occurred on already selected element
+      // it is needed to remove selection from other elements
+      // or if SHIFT or META key pressed remove selection
+      // from hitted element
+      //
+      // If click occurred and elements were dragged or some element
+      // was added to selection (on pointerdown phase) we need to keep
+      // selection unchanged
+      if (hitElement && !draggingOccurred && !elementIsAddedToSelection) {
+        if (event.shiftKey) {
+          this.setState(prevState => ({
+            selectedElementIds: {
+              ...prevState.selectedElementIds,
+              [hitElement!.id]: false,
+            },
+          }));
+        } else {
+          this.setState(prevState => ({
+            selectedElementIds: { [hitElement!.id]: true },
+          }));
+        }
+      }
+
+      if (draggingElement === null) {
+        // if no element is clicked, clear the selection and redraw
+        this.setState({ selectedElementIds: {} });
+        return;
+      }
+
+      if (!elementLocked) {
+        this.setState(prevState => ({
+          selectedElementIds: {
+            ...prevState.selectedElementIds,
+            [draggingElement.id]: true,
+          },
+        }));
+      }
+
+      if (
+        elementType !== "selection" ||
+        isSomeElementSelected(elements, this.state)
+      ) {
+        history.resumeRecording();
+      }
+
+      if (!elementLocked) {
+        resetCursor();
+        this.setState({
+          draggingElement: null,
+          elementType: "selection",
+        });
+      } else {
+        this.setState({
+          draggingElement: null,
+        });
+      }
+    };
+
+    lastPointerUp = onPointerUp;
+
+    window.addEventListener("pointermove", onPointerMove);
+    window.addEventListener("pointerup", onPointerUp);
+  };
 
   private handleWheel = (event: WheelEvent) => {
     event.preventDefault();