|  | @@ -41,7 +41,7 @@ import {
 | 
	
		
			
				|  |  |  } from "./scene";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import { renderScene } from "./renderer";
 | 
	
		
			
				|  |  | -import { AppState, FlooredNumber } from "./types";
 | 
	
		
			
				|  |  | +import { AppState, FlooredNumber, Gesture } from "./types";
 | 
	
		
			
				|  |  |  import { ExcalidrawElement } from "./element/types";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import {
 | 
	
	
		
			
				|  | @@ -108,6 +108,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import { copyToAppClipboard, getClipboardContent } from "./clipboard";
 | 
	
		
			
				|  |  |  import { normalizeScroll } from "./scene/data";
 | 
	
		
			
				|  |  | +import { getCenter, getDistance } from "./gesture";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  let { elements } = createScene();
 | 
	
		
			
				|  |  |  const { history } = createHistory();
 | 
	
	
		
			
				|  | @@ -130,10 +131,11 @@ const CURSOR_TYPE = {
 | 
	
		
			
				|  |  |    CROSSHAIR: "crosshair",
 | 
	
		
			
				|  |  |    GRABBING: "grabbing",
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  | -const MOUSE_BUTTON = {
 | 
	
		
			
				|  |  | +const POINTER_BUTTON = {
 | 
	
		
			
				|  |  |    MAIN: 0,
 | 
	
		
			
				|  |  |    WHEEL: 1,
 | 
	
		
			
				|  |  |    SECONDARY: 2,
 | 
	
		
			
				|  |  | +  TOUCH: -1,
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  // Block pinch-zooming on iOS outside of the content area
 | 
	
	
		
			
				|  | @@ -148,7 +150,13 @@ document.addEventListener(
 | 
	
		
			
				|  |  |    { passive: false },
 | 
	
		
			
				|  |  |  );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -let lastMouseUp: ((e: any) => void) | null = null;
 | 
	
		
			
				|  |  | +let lastPointerUp: ((e: any) => void) | null = null;
 | 
	
		
			
				|  |  | +const gesture: Gesture = {
 | 
	
		
			
				|  |  | +  pointers: [],
 | 
	
		
			
				|  |  | +  lastCenter: null,
 | 
	
		
			
				|  |  | +  initialDistance: null,
 | 
	
		
			
				|  |  | +  initialScale: null,
 | 
	
		
			
				|  |  | +};
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  export function viewportCoordsToSceneCoords(
 | 
	
		
			
				|  |  |    { clientX, clientY }: { clientX: number; clientY: number },
 | 
	
	
		
			
				|  | @@ -202,7 +210,6 @@ let cursorX = 0;
 | 
	
		
			
				|  |  |  let cursorY = 0;
 | 
	
		
			
				|  |  |  let isHoldingSpace: boolean = false;
 | 
	
		
			
				|  |  |  let isPanning: boolean = false;
 | 
	
		
			
				|  |  | -let isHoldingMouseButton: boolean = false;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  interface LayerUIProps {
 | 
	
		
			
				|  |  |    actionManager: ActionManager;
 | 
	
	
		
			
				|  | @@ -279,17 +286,15 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |        );
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    function renderSelectedShapeActions(
 | 
	
		
			
				|  |  | -      elements: readonly ExcalidrawElement[],
 | 
	
		
			
				|  |  | -    ) {
 | 
	
		
			
				|  |  | +    const showSelectedShapeActions =
 | 
	
		
			
				|  |  | +      (appState.editingElement || getSelectedElements(elements).length) &&
 | 
	
		
			
				|  |  | +      appState.elementType === "selection";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    function renderSelectedShapeActions() {
 | 
	
		
			
				|  |  |        const { elementType, editingElement } = appState;
 | 
	
		
			
				|  |  |        const targetElements = editingElement
 | 
	
		
			
				|  |  |          ? [editingElement]
 | 
	
		
			
				|  |  |          : getSelectedElements(elements);
 | 
	
		
			
				|  |  | -      if (!targetElements.length && elementType === "selection") {
 | 
	
		
			
				|  |  | -        return null;
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |        return (
 | 
	
		
			
				|  |  |          <div className="panelColumn">
 | 
	
		
			
				|  |  |            {actionManager.renderAction("changeStrokeColor")}
 | 
	
	
		
			
				|  | @@ -331,8 +336,6 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                {actionManager.renderAction("bringForward")}
 | 
	
		
			
				|  |  |              </div>
 | 
	
		
			
				|  |  |            </fieldset>
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -          {actionManager.renderAction("deleteSelectedElements")}
 | 
	
		
			
				|  |  |          </div>
 | 
	
		
			
				|  |  |        );
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -418,7 +421,7 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                </Stack.Col>
 | 
	
		
			
				|  |  |              </div>
 | 
	
		
			
				|  |  |            </section>
 | 
	
		
			
				|  |  | -        ) : appState.openedMenu === "shape" ? (
 | 
	
		
			
				|  |  | +        ) : appState.openedMenu === "shape" && showSelectedShapeActions ? (
 | 
	
		
			
				|  |  |            <section
 | 
	
		
			
				|  |  |              className="App-mobile-menu"
 | 
	
		
			
				|  |  |              aria-labelledby="selected-shape-title"
 | 
	
	
		
			
				|  | @@ -427,7 +430,7 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                {t("headings.selectedShapeActions")}
 | 
	
		
			
				|  |  |              </h2>
 | 
	
		
			
				|  |  |              <div className="App-mobile-menu-scroller">
 | 
	
		
			
				|  |  | -              {renderSelectedShapeActions(elements)}
 | 
	
		
			
				|  |  | +              {renderSelectedShapeActions()}
 | 
	
		
			
				|  |  |              </div>
 | 
	
		
			
				|  |  |            </section>
 | 
	
		
			
				|  |  |          ) : null}
 | 
	
	
		
			
				|  | @@ -444,6 +447,12 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                </Stack.Row>
 | 
	
		
			
				|  |  |              </Stack.Col>
 | 
	
		
			
				|  |  |            </section>
 | 
	
		
			
				|  |  | +          <HintViewer
 | 
	
		
			
				|  |  | +            elementType={appState.elementType}
 | 
	
		
			
				|  |  | +            multiMode={appState.multiElement !== null}
 | 
	
		
			
				|  |  | +            isResizing={appState.isResizing}
 | 
	
		
			
				|  |  | +            elements={elements}
 | 
	
		
			
				|  |  | +          />
 | 
	
		
			
				|  |  |          </FixedSideContainer>
 | 
	
		
			
				|  |  |          <footer className="App-toolbar">
 | 
	
		
			
				|  |  |            <div className="App-toolbar-content">
 | 
	
	
		
			
				|  | @@ -459,7 +468,18 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                  }))
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  |              />
 | 
	
		
			
				|  |  | +            <div
 | 
	
		
			
				|  |  | +              style={{
 | 
	
		
			
				|  |  | +                visibility: isSomeElementSelected(elements)
 | 
	
		
			
				|  |  | +                  ? "visible"
 | 
	
		
			
				|  |  | +                  : "hidden",
 | 
	
		
			
				|  |  | +              }}
 | 
	
		
			
				|  |  | +            >
 | 
	
		
			
				|  |  | +              {" "}
 | 
	
		
			
				|  |  | +              {actionManager.renderAction("deleteSelectedElements")}
 | 
	
		
			
				|  |  | +            </div>
 | 
	
		
			
				|  |  |              {lockButton}
 | 
	
		
			
				|  |  | +            {actionManager.renderAction("finalize")}
 | 
	
		
			
				|  |  |              <div
 | 
	
		
			
				|  |  |                style={{
 | 
	
		
			
				|  |  |                  visibility: isSomeElementSelected(elements)
 | 
	
	
		
			
				|  | @@ -482,12 +502,6 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                />
 | 
	
		
			
				|  |  |              </div>
 | 
	
		
			
				|  |  | -            <HintViewer
 | 
	
		
			
				|  |  | -              elementType={appState.elementType}
 | 
	
		
			
				|  |  | -              multiMode={appState.multiElement !== null}
 | 
	
		
			
				|  |  | -              isResizing={appState.isResizing}
 | 
	
		
			
				|  |  | -              elements={elements}
 | 
	
		
			
				|  |  | -            />
 | 
	
		
			
				|  |  |              {appState.scrolledOutside && (
 | 
	
		
			
				|  |  |                <button
 | 
	
		
			
				|  |  |                  className="scroll-back-to-content"
 | 
	
	
		
			
				|  | @@ -525,17 +539,17 @@ const LayerUI = React.memo(
 | 
	
		
			
				|  |  |                    </Stack.Col>
 | 
	
		
			
				|  |  |                  </Island>
 | 
	
		
			
				|  |  |                </section>
 | 
	
		
			
				|  |  | -              <section
 | 
	
		
			
				|  |  | -                className="App-right-menu"
 | 
	
		
			
				|  |  | -                aria-labelledby="selected-shape-title"
 | 
	
		
			
				|  |  | -              >
 | 
	
		
			
				|  |  | -                <h2 className="visually-hidden" id="selected-shape-title">
 | 
	
		
			
				|  |  | -                  {t("headings.selectedShapeActions")}
 | 
	
		
			
				|  |  | -                </h2>
 | 
	
		
			
				|  |  | -                <Island padding={4}>
 | 
	
		
			
				|  |  | -                  {renderSelectedShapeActions(elements)}
 | 
	
		
			
				|  |  | -                </Island>
 | 
	
		
			
				|  |  | -              </section>
 | 
	
		
			
				|  |  | +              {showSelectedShapeActions ? (
 | 
	
		
			
				|  |  | +                <section
 | 
	
		
			
				|  |  | +                  className="App-right-menu"
 | 
	
		
			
				|  |  | +                  aria-labelledby="selected-shape-title"
 | 
	
		
			
				|  |  | +                >
 | 
	
		
			
				|  |  | +                  <h2 className="visually-hidden" id="selected-shape-title">
 | 
	
		
			
				|  |  | +                    {t("headings.selectedShapeActions")}
 | 
	
		
			
				|  |  | +                  </h2>
 | 
	
		
			
				|  |  | +                  <Island padding={4}>{renderSelectedShapeActions()}</Island>
 | 
	
		
			
				|  |  | +                </section>
 | 
	
		
			
				|  |  | +              ) : null}
 | 
	
		
			
				|  |  |              </Stack.Col>
 | 
	
		
			
				|  |  |              <section aria-labelledby="shapes-title">
 | 
	
		
			
				|  |  |                <Stack.Col gap={4} align="start">
 | 
	
	
		
			
				|  | @@ -858,7 +872,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |            this.setState({ ...data.appState });
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -    } else if (event.key === KEYS.SPACE && !isHoldingMouseButton) {
 | 
	
		
			
				|  |  | +    } else if (event.key === KEYS.SPACE && gesture.pointers.length === 0) {
 | 
	
		
			
				|  |  |        isHoldingSpace = true;
 | 
	
		
			
				|  |  |        document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -953,6 +967,10 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |      this.setState({});
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  removePointer = (e: React.PointerEvent<HTMLElement>) => {
 | 
	
		
			
				|  |  | +    gesture.pointers = gesture.pointers.filter(p => p.id !== e.pointerId);
 | 
	
		
			
				|  |  | +  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    public render() {
 | 
	
		
			
				|  |  |      const canvasDOMWidth = window.innerWidth;
 | 
	
		
			
				|  |  |      const canvasDOMHeight = window.innerHeight;
 | 
	
	
		
			
				|  | @@ -1055,12 +1073,12 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  left: e.clientX,
 | 
	
		
			
				|  |  |                });
 | 
	
		
			
				|  |  |              }}
 | 
	
		
			
				|  |  | -            onMouseDown={e => {
 | 
	
		
			
				|  |  | -              if (lastMouseUp !== null) {
 | 
	
		
			
				|  |  | -                // Unfortunately, sometimes we don't get a mouseup after a mousedown,
 | 
	
		
			
				|  |  | +            onPointerDown={e => {
 | 
	
		
			
				|  |  | +              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 mousedown
 | 
	
		
			
				|  |  | -                lastMouseUp(e);
 | 
	
		
			
				|  |  | +                // being in a weird state, we clean up on the next pointerdown
 | 
	
		
			
				|  |  | +                lastPointerUp(e);
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                if (isPanning) {
 | 
	
	
		
			
				|  | @@ -1069,15 +1087,14 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                // pan canvas on wheel button drag or space+drag
 | 
	
		
			
				|  |  |                if (
 | 
	
		
			
				|  |  | -                !isHoldingMouseButton &&
 | 
	
		
			
				|  |  | -                (e.button === MOUSE_BUTTON.WHEEL ||
 | 
	
		
			
				|  |  | -                  (e.button === MOUSE_BUTTON.MAIN && isHoldingSpace))
 | 
	
		
			
				|  |  | +                gesture.pointers.length === 0 &&
 | 
	
		
			
				|  |  | +                (e.button === POINTER_BUTTON.WHEEL ||
 | 
	
		
			
				|  |  | +                  (e.button === POINTER_BUTTON.MAIN && isHoldingSpace))
 | 
	
		
			
				|  |  |                ) {
 | 
	
		
			
				|  |  | -                isHoldingMouseButton = true;
 | 
	
		
			
				|  |  |                  isPanning = true;
 | 
	
		
			
				|  |  |                  document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
 | 
	
		
			
				|  |  |                  let { clientX: lastX, clientY: lastY } = e;
 | 
	
		
			
				|  |  | -                const onMouseMove = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | +                const onPointerMove = (e: PointerEvent) => {
 | 
	
		
			
				|  |  |                    const deltaX = lastX - e.clientX;
 | 
	
		
			
				|  |  |                    const deltaY = lastY - e.clientY;
 | 
	
		
			
				|  |  |                    lastX = e.clientX;
 | 
	
	
		
			
				|  | @@ -1092,30 +1109,44 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                      ),
 | 
	
		
			
				|  |  |                    });
 | 
	
		
			
				|  |  |                  };
 | 
	
		
			
				|  |  | -                const teardown = (lastMouseUp = () => {
 | 
	
		
			
				|  |  | -                  lastMouseUp = null;
 | 
	
		
			
				|  |  | +                const teardown = (lastPointerUp = () => {
 | 
	
		
			
				|  |  | +                  lastPointerUp = null;
 | 
	
		
			
				|  |  |                    isPanning = false;
 | 
	
		
			
				|  |  | -                  isHoldingMouseButton = false;
 | 
	
		
			
				|  |  |                    if (!isHoldingSpace) {
 | 
	
		
			
				|  |  |                      setCursorForShape(this.state.elementType);
 | 
	
		
			
				|  |  |                    }
 | 
	
		
			
				|  |  | -                  window.removeEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | -                  window.removeEventListener("mouseup", teardown);
 | 
	
		
			
				|  |  | +                  window.removeEventListener("pointermove", onPointerMove);
 | 
	
		
			
				|  |  | +                  window.removeEventListener("pointerup", teardown);
 | 
	
		
			
				|  |  |                    window.removeEventListener("blur", teardown);
 | 
	
		
			
				|  |  |                  });
 | 
	
		
			
				|  |  |                  window.addEventListener("blur", teardown);
 | 
	
		
			
				|  |  | -                window.addEventListener("mousemove", onMouseMove, {
 | 
	
		
			
				|  |  | +                window.addEventListener("pointermove", onPointerMove, {
 | 
	
		
			
				|  |  |                    passive: true,
 | 
	
		
			
				|  |  |                  });
 | 
	
		
			
				|  |  | -                window.addEventListener("mouseup", teardown);
 | 
	
		
			
				|  |  | +                window.addEventListener("pointerup", teardown);
 | 
	
		
			
				|  |  |                  return;
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              // only handle left mouse button
 | 
	
		
			
				|  |  | -              if (e.button !== MOUSE_BUTTON.MAIN) {
 | 
	
		
			
				|  |  | +              // only handle left mouse button or touch
 | 
	
		
			
				|  |  | +              if (
 | 
	
		
			
				|  |  | +                e.button !== POINTER_BUTTON.MAIN &&
 | 
	
		
			
				|  |  | +                e.button !== POINTER_BUTTON.TOUCH
 | 
	
		
			
				|  |  | +              ) {
 | 
	
		
			
				|  |  |                  return;
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  | -              // fixes mousemove causing selection of UI texts #32
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              gesture.pointers.push({
 | 
	
		
			
				|  |  | +                id: e.pointerId,
 | 
	
		
			
				|  |  | +                x: e.clientX,
 | 
	
		
			
				|  |  | +                y: e.clientY,
 | 
	
		
			
				|  |  | +              });
 | 
	
		
			
				|  |  | +              if (gesture.pointers.length === 2) {
 | 
	
		
			
				|  |  | +                gesture.lastCenter = getCenter(gesture.pointers);
 | 
	
		
			
				|  |  | +                gesture.initialScale = this.state.zoom;
 | 
	
		
			
				|  |  | +                gesture.initialDistance = getDistance(gesture.pointers);
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              // fixes pointermove causing selection of UI texts #32
 | 
	
		
			
				|  |  |                e.preventDefault();
 | 
	
		
			
				|  |  |                // Preventing the event above disables default behavior
 | 
	
		
			
				|  |  |                //  of defocusing potentially focused element, which is what we
 | 
	
	
		
			
				|  | @@ -1124,6 +1155,11 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  document.activeElement.blur();
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +              // don't select while panning
 | 
	
		
			
				|  |  | +              if (gesture.pointers.length > 1) {
 | 
	
		
			
				|  |  | +                return;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                // Handle scrollbars dragging
 | 
	
		
			
				|  |  |                const {
 | 
	
		
			
				|  |  |                  isOverHorizontalScrollBar,
 | 
	
	
		
			
				|  | @@ -1216,7 +1252,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                        elementIsAddedToSelection = true;
 | 
	
		
			
				|  |  |                      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                    // We duplicate the selected element if alt is pressed on Mouse down
 | 
	
		
			
				|  |  | +                    // We duplicate the selected element if alt is pressed on pointer down
 | 
	
		
			
				|  |  |                      if (e.altKey) {
 | 
	
		
			
				|  |  |                        elements = [
 | 
	
		
			
				|  |  |                          ...elements.map(element => ({
 | 
	
	
		
			
				|  | @@ -1352,8 +1388,8 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                      p1: Point,
 | 
	
		
			
				|  |  |                      deltaX: number,
 | 
	
		
			
				|  |  |                      deltaY: number,
 | 
	
		
			
				|  |  | -                    mouseX: number,
 | 
	
		
			
				|  |  | -                    mouseY: number,
 | 
	
		
			
				|  |  | +                    pointerX: number,
 | 
	
		
			
				|  |  | +                    pointerY: number,
 | 
	
		
			
				|  |  |                      perfect: boolean,
 | 
	
		
			
				|  |  |                    ) => void)
 | 
	
		
			
				|  |  |                  | null = null;
 | 
	
	
		
			
				|  | @@ -1363,8 +1399,8 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  p1: Point,
 | 
	
		
			
				|  |  |                  deltaX: number,
 | 
	
		
			
				|  |  |                  deltaY: number,
 | 
	
		
			
				|  |  | -                mouseX: number,
 | 
	
		
			
				|  |  | -                mouseY: number,
 | 
	
		
			
				|  |  | +                pointerX: number,
 | 
	
		
			
				|  |  | +                pointerY: number,
 | 
	
		
			
				|  |  |                  perfect: boolean,
 | 
	
		
			
				|  |  |                ) => {
 | 
	
		
			
				|  |  |                  if (perfect) {
 | 
	
	
		
			
				|  | @@ -1373,8 +1409,8 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                    const { width, height } = getPerfectElementSize(
 | 
	
		
			
				|  |  |                      element.type,
 | 
	
		
			
				|  |  | -                    mouseX - element.x - p1[0],
 | 
	
		
			
				|  |  | -                    mouseY - element.y - p1[1],
 | 
	
		
			
				|  |  | +                    pointerX - element.x - p1[0],
 | 
	
		
			
				|  |  | +                    pointerY - element.y - p1[1],
 | 
	
		
			
				|  |  |                    );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                    const dx = element.x + width + p1[0];
 | 
	
	
		
			
				|  | @@ -1396,15 +1432,15 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  p1: Point,
 | 
	
		
			
				|  |  |                  deltaX: number,
 | 
	
		
			
				|  |  |                  deltaY: number,
 | 
	
		
			
				|  |  | -                mouseX: number,
 | 
	
		
			
				|  |  | -                mouseY: number,
 | 
	
		
			
				|  |  | +                pointerX: number,
 | 
	
		
			
				|  |  | +                pointerY: number,
 | 
	
		
			
				|  |  |                  perfect: boolean,
 | 
	
		
			
				|  |  |                ) => {
 | 
	
		
			
				|  |  |                  if (perfect) {
 | 
	
		
			
				|  |  |                    const { width, height } = getPerfectElementSize(
 | 
	
		
			
				|  |  |                      element.type,
 | 
	
		
			
				|  |  | -                    mouseX - element.x,
 | 
	
		
			
				|  |  | -                    mouseY - element.y,
 | 
	
		
			
				|  |  | +                    pointerX - element.x,
 | 
	
		
			
				|  |  | +                    pointerY - element.y,
 | 
	
		
			
				|  |  |                    );
 | 
	
		
			
				|  |  |                    p1[0] = width;
 | 
	
		
			
				|  |  |                    p1[1] = height;
 | 
	
	
		
			
				|  | @@ -1414,7 +1450,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              const onMouseMove = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | +              const onPointerMove = (e: PointerEvent) => {
 | 
	
		
			
				|  |  |                  const target = e.target;
 | 
	
		
			
				|  |  |                  if (!(target instanceof HTMLElement)) {
 | 
	
		
			
				|  |  |                    return;
 | 
	
	
		
			
				|  | @@ -1447,7 +1483,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  // 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 mousemove)
 | 
	
		
			
				|  |  | +                //  triggering pointermove)
 | 
	
		
			
				|  |  |                  if (
 | 
	
		
			
				|  |  |                    !draggingOccurred &&
 | 
	
		
			
				|  |  |                    (this.state.elementType === "arrow" ||
 | 
	
	
		
			
				|  | @@ -1691,7 +1727,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  if (hitElement?.isSelected) {
 | 
	
		
			
				|  |  |                    // Marking that click was used for dragging to check
 | 
	
		
			
				|  |  | -                  // if elements should be deselected on mouseup
 | 
	
		
			
				|  |  | +                  // if elements should be deselected on pointerup
 | 
	
		
			
				|  |  |                    draggingOccurred = true;
 | 
	
		
			
				|  |  |                    const selectedElements = getSelectedElements(elements);
 | 
	
		
			
				|  |  |                    if (selectedElements.length > 0) {
 | 
	
	
		
			
				|  | @@ -1790,7 +1826,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  this.setState({});
 | 
	
		
			
				|  |  |                };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              const onMouseUp = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | +              const onPointerUp = (e: PointerEvent) => {
 | 
	
		
			
				|  |  |                  const {
 | 
	
		
			
				|  |  |                    draggingElement,
 | 
	
		
			
				|  |  |                    resizingElement,
 | 
	
	
		
			
				|  | @@ -1806,10 +1842,9 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  resizeArrowFn = null;
 | 
	
		
			
				|  |  | -                lastMouseUp = null;
 | 
	
		
			
				|  |  | -                isHoldingMouseButton = false;
 | 
	
		
			
				|  |  | -                window.removeEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | -                window.removeEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  | +                lastPointerUp = null;
 | 
	
		
			
				|  |  | +                window.removeEventListener("pointermove", onPointerMove);
 | 
	
		
			
				|  |  | +                window.removeEventListener("pointerup", onPointerUp);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  if (elementType === "arrow" || elementType === "line") {
 | 
	
		
			
				|  |  |                    if (draggingElement!.points.length > 1) {
 | 
	
	
		
			
				|  | @@ -1850,7 +1885,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                    draggingElement &&
 | 
	
		
			
				|  |  |                    isInvisiblySmallElement(draggingElement)
 | 
	
		
			
				|  |  |                  ) {
 | 
	
		
			
				|  |  | -                  // remove invisible element which was added in onMouseDown
 | 
	
		
			
				|  |  | +                  // remove invisible element which was added in onPointerDown
 | 
	
		
			
				|  |  |                    elements = elements.slice(0, -1);
 | 
	
		
			
				|  |  |                    this.setState({
 | 
	
		
			
				|  |  |                      draggingElement: null,
 | 
	
	
		
			
				|  | @@ -1882,7 +1917,7 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  // from hitted element
 | 
	
		
			
				|  |  |                  //
 | 
	
		
			
				|  |  |                  // If click occurred and elements were dragged or some element
 | 
	
		
			
				|  |  | -                // was added to selection (on mousedown phase) we need to keep
 | 
	
		
			
				|  |  | +                // was added to selection (on pointerdown phase) we need to keep
 | 
	
		
			
				|  |  |                  // selection unchanged
 | 
	
		
			
				|  |  |                  if (
 | 
	
		
			
				|  |  |                    hitElement &&
 | 
	
	
		
			
				|  | @@ -1928,10 +1963,10 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              lastMouseUp = onMouseUp;
 | 
	
		
			
				|  |  | +              lastPointerUp = onPointerUp;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              window.addEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | -              window.addEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  | +              window.addEventListener("pointermove", onPointerMove);
 | 
	
		
			
				|  |  | +              window.addEventListener("pointerup", onPointerUp);
 | 
	
		
			
				|  |  |              }}
 | 
	
		
			
				|  |  |              onDoubleClick={e => {
 | 
	
		
			
				|  |  |                resetCursor();
 | 
	
	
		
			
				|  | @@ -2048,7 +2083,39 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                  },
 | 
	
		
			
				|  |  |                });
 | 
	
		
			
				|  |  |              }}
 | 
	
		
			
				|  |  | -            onMouseMove={e => {
 | 
	
		
			
				|  |  | +            onPointerMove={e => {
 | 
	
		
			
				|  |  | +              gesture.pointers = gesture.pointers.map(p =>
 | 
	
		
			
				|  |  | +                p.id === e.pointerId
 | 
	
		
			
				|  |  | +                  ? {
 | 
	
		
			
				|  |  | +                      id: e.pointerId,
 | 
	
		
			
				|  |  | +                      x: e.clientX,
 | 
	
		
			
				|  |  | +                      y: e.clientY,
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                  : p,
 | 
	
		
			
				|  |  | +              );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              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),
 | 
	
		
			
				|  |  | +                });
 | 
	
		
			
				|  |  | +              } else {
 | 
	
		
			
				|  |  | +                gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                if (isHoldingSpace || isPanning) {
 | 
	
		
			
				|  |  |                  return;
 | 
	
		
			
				|  |  |                }
 | 
	
	
		
			
				|  | @@ -2101,6 +2168,8 @@ export class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |                );
 | 
	
		
			
				|  |  |                document.documentElement.style.cursor = hitElement ? "move" : "";
 | 
	
		
			
				|  |  |              }}
 | 
	
		
			
				|  |  | +            onPointerUp={this.removePointer}
 | 
	
		
			
				|  |  | +            onPointerCancel={this.removePointer}
 | 
	
		
			
				|  |  |              onDrop={e => {
 | 
	
		
			
				|  |  |                const file = e.dataTransfer.files[0];
 | 
	
		
			
				|  |  |                if (file?.type === "application/json") {
 |