From cef6852d597ce67637466afb36c3498dc84211f6 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Wed, 8 Jan 2020 19:48:28 -0500
Subject: gestures are now overlayed and can span collections/panes!

---
 src/client/views/collections/CollectionSubView.tsx | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

(limited to 'src/client/views/collections/CollectionSubView.tsx')

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 062521690..73dc7edc6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -23,6 +23,7 @@ import { basename } from 'path';
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { Networking } from "../../Network";
+import { GestureUtils } from "../../../pen-gestures/GestureUtils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -47,15 +48,18 @@ export interface SubCollectionViewProps extends CollectionViewProps {
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
     class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
         private dropDisposer?: DragManager.DragDropDisposer;
+        private gestureDisposer?: GestureUtils.GestureEventDisposer;
         private _childLayoutDisposer?: IReactionDisposer;
-        protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+        protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
             this.dropDisposer && this.dropDisposer();
+            this.gestureDisposer && this.gestureDisposer();
             if (ele) {
                 this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+                this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
             }
         }
         protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
-            this.createDropTarget(ele);
+            this.createDropAndGestureTarget(ele);
         }
 
         componentDidMount() {
@@ -129,6 +133,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             }
         }
 
+        @undoBatch
+        protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
+
+        }
+
         @undoBatch
         @action
         protected drop(e: Event, de: DragManager.DropEvent): boolean {
-- 
cgit v1.2.3-70-g09d2


From de0c00ff0bb1f58a0736da24acc984f5a090d009 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 14 Jan 2020 18:35:28 -0500
Subject: ok we now use custom events for touch. im struggling with figuring
 out how to ignore a hand lol

---
 src/client/util/InteractionUtils.ts                |  50 ++++++-
 src/client/views/CollectionLinearView.tsx          |   4 +-
 src/client/views/GestureOverlay.tsx                | 160 +++++++++++++++++++--
 src/client/views/Touchable.tsx                     | 114 ++++++++++-----
 .../views/collections/CollectionStackingView.tsx   |   6 +-
 src/client/views/collections/CollectionSubView.tsx |   8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  80 ++++++-----
 src/client/views/nodes/DocumentView.tsx            |  32 +++--
 src/pen-gestures/GestureUtils.ts                   |   2 +-
 9 files changed, 344 insertions(+), 112 deletions(-)

(limited to 'src/client/views/collections/CollectionSubView.tsx')

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index 76b43da3c..63ee2fb92 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -8,13 +8,53 @@ export namespace InteractionUtils {
     const REACT_POINTER_PEN_BUTTON = 0;
     const ERASER_BUTTON = 5;
 
+    export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
+        constructor(
+            readonly fingers: number,
+            readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+            readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
+        ) { }
+    }
+
+    export interface MultiTouchEventDisposer { (): void; }
+
+    export function MakeMultiTouchTarget(
+        element: HTMLElement,
+        startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
+    ): MultiTouchEventDisposer {
+        const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
+        // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+        // if (onMultiTouchMoveHandler) {
+        //     element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+        // }
+        // if (onMultiTouchEndHandler) {
+        //     element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
+        // }
+        return () => {
+            element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+            // if (onMultiTouchMoveHandler) {
+            //     element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+            // }
+            // if (onMultiTouchEndHandler) {
+            //     element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
+            // }
+        };
+    }
+
     export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
         const myTouches = new Array<React.Touch>();
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt: any = e.targetTouches.item(i);
-            if (pt && prevPoints.has(pt.identifier)) {
-                if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
-                    myTouches.push(pt);
+        for (let i = 0; i < e.touches.length; i++) {
+            const pt: any = e.touches.item(i);
+            if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+                for (let j = 0; j < e.targetTouches.length; j++) {
+                    const tPt = e.targetTouches.item(j);
+                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+                        if (pt && prevPoints.has(pt.identifier)) {
+                            myTouches.push(pt);
+                        }
+                    }
                 }
             }
         }
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 5d6a58656..38f2decdd 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -36,7 +36,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
             { fireImmediately: true }
         );
     }
-    protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+    protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
         this._dropDisposer && this._dropDisposer();
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
@@ -55,7 +55,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     render() {
         const guid = Utils.GenerateGuid();
         return <div className="collectionLinearView-outer">
-            <div className="collectionLinearView" ref={this.createDropAndGestureTarget} >
+            <div className="collectionLinearView" ref={this.createDashEventsTarget} >
                 <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
                     onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 830c06f1f..a5fba362c 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,7 @@
 import React = require("react");
 import { Touchable } from "./Touchable";
 import { observer } from "mobx-react";
-import "./GestureOverlay.scss"
+import "./GestureOverlay.scss";
 import { computed, observable, action, runInAction } from "mobx";
 import { CreatePolyline } from "./InkingStroke";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
@@ -16,6 +16,7 @@ import { Scripting } from "../util/Scripting";
 import { FieldValue, Cast } from "../../new_fields/Types";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 import Palette from "./Palette";
+import { Utils } from "../../Utils";
 
 @observer
 export default class GestureOverlay extends Touchable {
@@ -28,6 +29,9 @@ export default class GestureOverlay extends Touchable {
 
     private _d1: Doc | undefined;
     private thumbIdentifier?: number;
+    private _hands: (React.Touch[])[] = [];
+
+    protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -35,11 +39,143 @@ export default class GestureOverlay extends Touchable {
         GestureOverlay.Instance = this;
     }
 
-    @action
+    getNewTouches(e: React.TouchEvent | TouchEvent) {
+        const ntt: (React.Touch | Touch)[] = [];
+        const nct: (React.Touch | Touch)[] = [];
+        const nt: (React.Touch | Touch)[] = [];
+        this._hands.forEach((hand) => {
+            for (let i = 0; i < e.targetTouches.length; i++) {
+                const pt = e.targetTouches.item(i);
+                if (pt && !hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    ntt.push(pt);
+                }
+            }
+
+            for (let i = 0; i < e.changedTouches.length; i++) {
+                const pt = e.changedTouches.item(i);
+                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nct.push(pt);
+                }
+            }
+
+            for (let i = 0; i < e.changedTouches.length; i++) {
+                const pt = e.touches.item(i);
+                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nt.push(pt);
+                }
+            }
+        });
+        return { ntt, nct, nt };
+    }
+
+    onReactTouchStart = (te: React.TouchEvent) => {
+        const actualPts: React.Touch[] = [];
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt: any = te.touches.item(i);
+            actualPts.push(pt);
+            // pen is also a touch, but with a radius of 0.5 (at least with the surface pens)
+            // and this seems to be the only way of differentiating pen and touch on touch events
+            if (pt.radiusX > 1 && pt.radiusY > 1) {
+                // if (typeof pt.identifier !== "string") {
+                //     pt.identifier = Utils.GenerateGuid();
+                // }
+                this.prevPoints.set(pt.identifier, pt);
+            }
+        }
+
+        const ptsToDelete: number[] = [];
+        this.prevPoints.forEach(pt => {
+            if (!actualPts.includes(pt)) {
+                ptsToDelete.push(pt.identifier);
+            }
+        });
+
+        ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+
+        if (this.prevPoints.size && this.prevPoints.size < 5) {
+            const nts: any = this.getNewTouches(te);
+            const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
+            target?.dispatchEvent(
+                new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
+                    {
+                        bubbles: true,
+                        detail: {
+                            fingers: this.prevPoints.size,
+                            points: te.targetTouches,
+                            touchEvent: new React.TouchEvent("start", {
+                                targetTouches: nts.ntt,
+                                touches: nts.nt,
+                                changedTouches: nts.nct
+                            });
+                        }
+                    }
+                )
+            );
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+            document.addEventListener("touchmove", this.onReactTouchMove);
+            document.addEventListener("touchend", this.onReactTouchEnd);
+        }
+        else {
+            this.handleHandDown(te);
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+        }
+    }
+
+    onReactTouchMove = (e: TouchEvent) => {
+        const nts: any = this.getNewTouches(e);
+        document.dispatchEvent(
+            new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove",
+                {
+                    bubbles: true,
+                    detail: {
+                        fingers: this.prevPoints.size,
+                        points: e.touches,
+                        touchEvent: new TouchEvent("move", {
+                            targetTouches: nts.ntt,
+                            changedTouches: nts.nct,
+                            touches: nts.nt,
+                        })
+                    }
+                })
+        );
+    }
+
+    onReactTouchEnd = (e: TouchEvent) => {
+        document.dispatchEvent(
+            new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd",
+                {
+                    bubbles: true,
+                    detail: {
+                        fingers: this.prevPoints.size,
+                        points: e.touches,
+                        touchEvent: e
+                    }
+                })
+        );
+        for (let i = 0; i < e.changedTouches.length; i++) {
+            const pt = e.changedTouches.item(i);
+            if (pt) {
+                if (this.prevPoints.has(pt.identifier)) {
+                    this.prevPoints.delete(pt.identifier);
+                }
+            }
+        }
+
+        if (this.prevPoints.size === 0) {
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+        }
+        e.stopPropagation();
+    }
+
     handleHandDown = (e: React.TouchEvent) => {
         const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
         const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
         this.thumbIdentifier = thumb?.identifier;
+        fingers.forEach((f) => this.prevPoints.delete(f.identifier));
+        this._hands.push(fingers);
         const others = fingers.filter(f => f !== thumb);
         const minX = Math.min(...others.map(f => f.clientX));
         const minY = Math.min(...others.map(f => f.clientY));
@@ -48,10 +184,12 @@ export default class GestureOverlay extends Touchable {
 
         const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
         if (thumbDoc) {
-            this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+            runInAction(() => {
+                this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+            });
         }
 
-        document.removeEventListener("touchmove", this.onTouch);
+        this.removeMoveListeners();
         document.removeEventListener("touchmove", this.handleHandMove);
         document.addEventListener("touchmove", this.handleHandMove);
         document.removeEventListener("touchend", this.handleHandUp);
@@ -69,11 +207,9 @@ export default class GestureOverlay extends Touchable {
 
     @action
     handleHandUp = (e: TouchEvent) => {
-        this.onTouchEnd(e);
-        if (this.prevPoints.size < 3) {
-            this._palette = undefined;
-            document.removeEventListener("touchend", this.handleHandUp);
-        }
+        // this.onTouchEnd(e);
+        this._palette = undefined;
+        document.removeEventListener("touchend", this.handleHandUp);
     }
 
     @action
@@ -115,7 +251,7 @@ export default class GestureOverlay extends Touchable {
                 DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
                 actionPerformed = true;
             }
-        }
+        };
         const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
             {
                 bubbles: true,
@@ -181,7 +317,7 @@ export default class GestureOverlay extends Touchable {
                             }
                         }
                     )
-                )
+                );
                 this._points = [];
             }
         }
@@ -215,7 +351,7 @@ export default class GestureOverlay extends Touchable {
 
     render() {
         return (
-            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart}>
+            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
                 {this.props.children}
                 {this._palette}
                 {this.currentStroke}
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 2a7599fbf..a71015b05 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -8,8 +8,11 @@ const HOLD_DURATION = 1000;
 
 export abstract class Touchable<T = {}> extends React.Component<T> {
     //private holdTimer: NodeJS.Timeout | undefined;
-    holdTimer: NodeJS.Timeout | undefined;
+    private holdTimer: NodeJS.Timeout | undefined;
+    private moveDisposer?: InteractionUtils.MultiTouchEventDisposer;
+    private endDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
+    protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     protected _touchDrag: boolean = false;
     protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
 
@@ -22,16 +25,27 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
      * When a touch even starts, we keep track of each touch that is associated with that event
      */
     @action
-    protected onTouchStart = (e: React.TouchEvent): void => {
+    protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
         const actualPts: React.Touch[] = [];
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt: any = e.targetTouches.item(i);
+        const te = me.touchEvent;
+        // loop through all touches on screen
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt: any = te.touches.item(i);
             actualPts.push(pt);
-            // pen is also a touch, but with a radius of 0.5 (at least with the surface pens)
-            // and this seems to be the only way of differentiating pen and touch on touch events
-            if (pt.radiusX > 1 && pt.radiusY > 1) {
+            if (this.prevPoints.has(pt.identifier)) {
                 this.prevPoints.set(pt.identifier, pt);
             }
+            // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives
+            for (let j = 0; j < te.changedTouches.length; j++) {
+                const tPt = te.changedTouches.item(j);
+                if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) {
+                    // pen is also a touch, but with a radius of 0.5 (at least with the surface pens)
+                    // and this seems to be the only way of differentiating pen and touch on touch events
+                    if (pt.radiusX > 1 && pt.radiusY > 1) {
+                        this.prevPoints.set(pt.identifier, pt);
+                    }
+                }
+            }
         }
 
         const ptsToDelete: number[] = [];
@@ -41,26 +55,30 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             }
         });
 
+        // console.log(ptsToDelete.length);
         ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
 
+        // console.log(this.prevPoints.size);
         if (this.prevPoints.size) {
             switch (this.prevPoints.size) {
                 case 1:
-                    this.handle1PointerDown(e);
-                    e.persist();
+                    this.handle1PointerDown(te);
+                    te.persist();
                     // if (this.holdTimer) {
                     //     clearTimeout(this.holdTimer)
                     //     this.holdTimer = undefined;
                     // }
-                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION);
+                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te), HOLD_DURATION);
+                    // e.stopPropagation();
                     // console.log(this.holdTimer);
                     break;
                 case 2:
-                    this.handle2PointersDown(e);
-                    break;
-                case 5:
-                    this.handleHandDown(e);
+                    this.handle2PointersDown(te);
+                    // e.stopPropagation();
                     break;
+                // case 5:
+                //     this.handleHandDown(te);
+                //     break;
             }
         }
     }
@@ -69,28 +87,30 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     * Handle touch move event
     */
     @action
-    protected onTouch = (e: TouchEvent): void => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+    protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+        const te = me.touchEvent;
+        const myTouches = InteractionUtils.GetMyTargetTouches(te, this.prevPoints, true);
 
         // if we're not actually moving a lot, don't consider it as dragging yet
         if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
         this._touchDrag = true;
         if (this.holdTimer) {
-            console.log("CLEAR")
+            console.log("CLEAR");
             clearTimeout(this.holdTimer);
             // this.holdTimer = undefined;
         }
+        // console.log(myTouches.length);
         switch (myTouches.length) {
             case 1:
-                this.handle1PointerMove(e);
+                this.handle1PointerMove(te);
                 break;
             case 2:
-                this.handle2PointersMove(e);
+                this.handle2PointersMove(te);
                 break;
         }
 
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt = e.targetTouches.item(i);
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt = te.touches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.set(pt.identifier, pt);
@@ -100,11 +120,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     }
 
     @action
-    protected onTouchEnd = (e: TouchEvent): void => {
+    protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
         // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up");
         // remove all the touches associated with the event
-        for (let i = 0; i < e.changedTouches.length; i++) {
-            const pt = e.changedTouches.item(i);
+        const te = me.touchEvent;
+        for (let i = 0; i < te.changedTouches.length; i++) {
+            const pt = te.changedTouches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.delete(pt.identifier);
@@ -116,7 +137,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             console.log("clear");
         }
         this._touchDrag = false;
-        e.stopPropagation();
+        te.stopPropagation();
 
 
         // if (e.targetTouches.length === 0) {
@@ -126,11 +147,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         if (this.prevPoints.size === 0) {
             this.cleanUpInteractions();
         }
+        e.stopPropagation();
     }
 
     cleanUpInteractions = (): void => {
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     handle1PointerMove = (e: TouchEvent): any => {
@@ -144,23 +166,43 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     }
 
     handle1PointerDown = (e: React.TouchEvent): any => {
-        document.removeEventListener("touchmove", this.onTouch);
-        document.addEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
-        document.addEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.addMoveListeners();
+        this.removeEndListeners();
+        this.addEndListeners();
     }
 
     handle2PointersDown = (e: React.TouchEvent): any => {
-        document.removeEventListener("touchmove", this.onTouch);
-        document.addEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
-        document.addEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.addMoveListeners();
+        this.removeEndListeners();
+        this.addEndListeners();
     }
 
     handle1PointerHoldStart = (e: React.TouchEvent): any => {
         e.stopPropagation();
         e.preventDefault();
-        document.removeEventListener("touchmove", this.onTouch);
+        this.removeMoveListeners();
+    }
+
+    addMoveListeners = () => {
+        const handler = (e: Event) => this.onTouch(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+        document.addEventListener("dashOnTouchMove", handler);
+        this.moveDisposer = () => document.removeEventListener("dashOnTouchMove", handler);
+    }
+
+    removeMoveListeners = () => {
+        this.moveDisposer && this.moveDisposer();
+    }
+
+    addEndListeners = () => {
+        const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+        document.addEventListener("dashOnTouchEnd", handler);
+        this.endDisposer = () => document.removeEventListener("dashOnTouchEnd", handler);
+    }
+
+    removeEndListeners = () => {
+        this.endDisposer && this.endDisposer();
     }
 
     handleHandDown = (e: React.TouchEvent) => {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 992820fc7..b8423af20 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -153,7 +153,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     createRef = (ele: HTMLDivElement | null) => {
         this._masonryGridRef = ele;
-        this.createDropAndGestureTarget(ele!); //so the whole grid is the drop target?
+        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
     }
 
     overlays = (doc: Doc) => {
@@ -309,7 +309,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             docList={docList}
             parent={this}
             type={type}
-            createDropTarget={this.createDropAndGestureTarget}
+            createDropTarget={this.createDashEventsTarget}
             screenToLocalTransform={this.props.ScreenToLocalTransform}
         />;
     }
@@ -342,7 +342,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             docList={docList}
             parent={this}
             type={type}
-            createDropTarget={this.createDropAndGestureTarget}
+            createDropTarget={this.createDashEventsTarget}
             screenToLocalTransform={this.props.ScreenToLocalTransform}
             setDocHeight={this.setDocHeight}
         />;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 73dc7edc6..4133956fc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -24,6 +24,7 @@ import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { Networking } from "../../Network";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -49,17 +50,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
     class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
         private dropDisposer?: DragManager.DragDropDisposer;
         private gestureDisposer?: GestureUtils.GestureEventDisposer;
+        protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
         private _childLayoutDisposer?: IReactionDisposer;
-        protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+        protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
             this.dropDisposer && this.dropDisposer();
             this.gestureDisposer && this.gestureDisposer();
+            this.multiTouchDisposer && this.multiTouchDisposer();
             if (ele) {
                 this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
                 this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
+                this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
             }
         }
         protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
-            this.createDropAndGestureTarget(ele);
+            this.createDashEventsTarget(ele);
         }
 
         componentDidMount() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 84945c6e6..3894e9d63 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -326,29 +326,30 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     handle1PointerDown = (e: React.TouchEvent) => {
-        const pt = e.targetTouches.item(0);
-        if (pt) {
-            this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
-            if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
-                document.removeEventListener("touchmove", this.onTouch);
-                document.addEventListener("touchmove", this.onTouch);
-                document.removeEventListener("touchend", this.onTouchEnd);
-                document.addEventListener("touchend", this.onTouchEnd);
-                // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
-                //     e.stopPropagation();
-                //     e.preventDefault();
-                //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
-                //     this._points.push({ X: point[0], Y: point[1] });
-                // }
-                if (InkingControl.Instance.selectedTool === InkTool.None) {
-                    this._lastX = pt.pageX;
-                    this._lastY = pt.pageY;
-                    e.stopPropagation();
-                    e.preventDefault();
-                }
-                else {
-                    e.stopPropagation();
-                    e.preventDefault();
+        if (!e.nativeEvent.cancelBubble) {
+            // const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const pt = e.changedTouches.item(0);
+            if (pt) {
+                this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
+                if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
+                    this.removeMoveListeners();
+                    this.addMoveListeners();
+                    this.removeEndListeners();
+                    this.addEndListeners();
+                    // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                    //     e.stopPropagation();
+                    //     e.preventDefault();
+                    //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
+                    //     this._points.push({ X: point[0], Y: point[1] });
+                    // }
+                    if (InkingControl.Instance.selectedTool === InkTool.None) {
+                        this._lastX = pt.pageX;
+                        this._lastY = pt.pageY;
+                        e.preventDefault();
+                    }
+                    else {
+                        e.preventDefault();
+                    }
                 }
             }
         }
@@ -395,8 +396,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     @action
@@ -482,7 +483,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     this.pan(pt);
                 }
             }
-            e.stopPropagation();
+            // e.stopPropagation();
             e.preventDefault();
         }
     }
@@ -527,7 +528,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     }
                 }
             }
-            e.stopPropagation();
+            // e.stopPropagation();
             e.preventDefault();
         }
     }
@@ -535,18 +536,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     handle2PointersDown = (e: React.TouchEvent) => {
         if (!e.nativeEvent.cancelBubble && this.props.active(true)) {
-            const pt1: React.Touch | null = e.targetTouches.item(0);
-            const pt2: React.Touch | null = e.targetTouches.item(1);
-            if (!pt1 || !pt2) return;
+            // const pt1: React.Touch | null = e.targetTouches.item(0);
+            // const pt2: React.Touch | null = e.targetTouches.item(1);
+            // // if (!pt1 || !pt2) return;
+            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const pt1 = myTouches[0];
+            const pt2 = myTouches[1];
 
             const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
             const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
             this._lastX = centerX;
             this._lastY = centerY;
-            document.removeEventListener("touchmove", this.onTouch);
-            document.addEventListener("touchmove", this.onTouch);
-            document.removeEventListener("touchend", this.onTouchEnd);
-            document.addEventListener("touchend", this.onTouchEnd);
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
             e.stopPropagation();
         }
     }
@@ -554,8 +558,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     cleanUpInteractions = () => {
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     @action
@@ -989,9 +993,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
         if (!this.extensionDoc) return (null);
         // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
-        return <div className={"collectionfreeformview-container"} ref={this.createDropAndGestureTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+        return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
             style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
-            onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
+            onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
             {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2e0ae09ba..b33bebe7d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -103,6 +103,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
     private _titleRef = React.createRef<EditableView>();
 
+    protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
     public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
     public get ContentDiv() { return this._mainCont.current; }
     @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
@@ -177,6 +179,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     componentDidMount() {
         this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
         this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+        this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
 
         !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this);
     }
@@ -184,7 +187,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     componentDidUpdate() {
         this._dropDisposer && this._dropDisposer();
+        this._gestureEventDisposer && this._gestureEventDisposer();
+        this.multiTouchDisposer && this.multiTouchDisposer();
         this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
+        this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+        this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
     }
 
     @action
@@ -319,25 +326,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 }
             }
             if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation();
-            document.removeEventListener("touchmove", this.onTouch);
-            document.addEventListener("touchmove", this.onTouch);
-            document.removeEventListener("touchend", this.onTouchEnd);
-            document.addEventListener("touchend", this.onTouchEnd);
-            if ((e.nativeEvent as any).formattedHandled) e.stopPropagation();
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
+            e.stopPropagation();
         }
     }
 
     handle1PointerMove = (e: TouchEvent) => {
         if ((e as any).formattedHandled) { e.stopPropagation; return; }
         if (e.cancelBubble && this.active) {
-            document.removeEventListener("touchmove", this.onTouch);
+            this.removeMoveListeners();
         }
         else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
             const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
             if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
-                    document.removeEventListener("touchmove", this.onTouch);
-                    document.removeEventListener("touchend", this.onTouchEnd);
+                    this.cleanUpInteractions();
                     this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
                 }
             }
@@ -352,10 +358,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             e.stopPropagation();
             e.preventDefault();
 
-            document.removeEventListener("touchmove", this.onTouch);
-            document.addEventListener("touchmove", this.onTouch);
-            document.removeEventListener("touchend", this.onTouchEnd);
-            document.addEventListener("touchend", this.onTouchEnd);
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
         }
     }
 
@@ -970,7 +976,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 width: animwidth,
                 height: animheight,
                 opacity: this.Document.opacity
-            }} onTouchStart={this.onTouchStart}>
+            }}>
             {this.innards}
         </div>;
     }
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 062604458..4b5ad6684 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -31,7 +31,7 @@ export namespace GestureUtils {
         element.addEventListener("dashOnGesture", handler);
         return () => {
             element.removeEventListener("dashOnGesture", handler);
-        }
+        };
     }
 
     export enum Gestures {
-- 
cgit v1.2.3-70-g09d2

