From ba9e8cb1849cbcb6b82b14778f44e84ed9bff47f Mon Sep 17 00:00:00 2001
From: Kartikaya Gupta <kgupta@mozilla.com>
Date: Wed, 21 Sep 2016 09:44:56 -0400
Subject: [PATCH] Add support for touch-swiping between pages in presentation
 mode (bug 1300878).

---
 web/pdf_presentation_mode.js | 121 ++++++++++++++++++++++++++++++-----
 1 file changed, 106 insertions(+), 15 deletions(-)

diff --git a/web/pdf_presentation_mode.js b/web/pdf_presentation_mode.js
index bfc3b535c..87e26f4fa 100644
--- a/web/pdf_presentation_mode.js
+++ b/web/pdf_presentation_mode.js
@@ -62,6 +62,7 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
     this.contextMenuOpen = false;
     this.mouseScrollTimeStamp = 0;
     this.mouseScrollDelta = 0;
+    this.touchSwipeState = null;
 
     if (contextMenuItems) {
       contextMenuItems.contextFirstPage.addEventListener('click',
@@ -135,10 +136,6 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
 
       var MOUSE_SCROLL_COOLDOWN_TIME = 50;
       var PAGE_SWITCH_THRESHOLD = 0.1;
-      var PageSwitchDirection = {
-        UP: -1,
-        DOWN: 1
-      };
 
       var currentTime = (new Date()).getTime();
       var storedTime = this.mouseScrollTimeStamp;
@@ -156,19 +153,13 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
       this.mouseScrollDelta += delta;
 
       if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) {
-        var pageSwitchDirection = (this.mouseScrollDelta > 0) ?
-          PageSwitchDirection.UP : PageSwitchDirection.DOWN;
-        var page = this.pdfViewer.currentPageNumber;
+        var totalDelta = this.mouseScrollDelta;
         this._resetMouseScrollState();
-
-        // If we're at the first/last page, we don't need to do anything.
-        if ((page === 1 && pageSwitchDirection === PageSwitchDirection.UP) ||
-            (page === this.pdfViewer.pagesCount &&
-             pageSwitchDirection === PageSwitchDirection.DOWN)) {
-          return;
+        var success = totalDelta > 0 ? this._goToPreviousPage()
+                                     : this._goToNextPage();
+        if (success) {
+          this.mouseScrollTimeStamp = currentTime;
         }
-        this.pdfViewer.currentPageNumber = (page + pageSwitchDirection);
-        this.mouseScrollTimeStamp = currentTime;
       }
     },
 
@@ -179,6 +170,32 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
                 document.msFullscreenElement);
     },
 
+    /**
+     * @private
+     */
+    _goToPreviousPage: function PDFPresentationMode_goToPreviousPage() {
+      var page = this.pdfViewer.currentPageNumber;
+      // If we're at the first page, we don't need to do anything.
+      if (page <= 1) {
+        return false;
+      }
+      this.pdfViewer.currentPageNumber = (page - 1);
+      return true;
+    },
+
+    /**
+     * @private
+     */
+    _goToNextPage: function PDFPresentationMode_goToNextPage() {
+      var page = this.pdfViewer.currentPageNumber;
+      // If we're at the last page, we don't need to do anything.
+      if (page >= this.pdfViewer.pagesCount) {
+        return false;
+      }
+      this.pdfViewer.currentPageNumber = (page + 1);
+      return true;
+    },
+
     /**
      * @private
      */
@@ -339,6 +356,72 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
       this.mouseScrollDelta = 0;
     },
 
+    /**
+     * @private
+     */
+    _touchSwipe: function PDFPresentationMode_touchSwipe(evt) {
+      if (!this.active) {
+        return;
+      }
+
+      // Must move at least these many CSS pixels for it to count as a swipe
+      var SWIPE_MIN_DISTANCE_THRESHOLD = 50;
+      // The swipe angle is allowed to deviate from the x or y axis by this much
+      // before it is not considered a swipe in that direction any more.
+      var SWIPE_ANGLE_THRESHOLD = Math.PI / 6;
+
+      if (evt.touches.length > 1) {
+        // Multiple touch points detected, cancel the swipe.
+        this.touchSwipeState = null;
+        return;
+      }
+      switch (evt.type) {
+        case 'touchstart':
+          this.touchSwipeState = {
+            startX: evt.touches[0].pageX,
+            startY: evt.touches[0].pageY,
+            endX: evt.touches[0].pageX,
+            endY: evt.touches[0].pageY
+          };
+          break;
+        case 'touchmove':
+          if (this.touchSwipeState === null) {
+            return;
+          }
+          this.touchSwipeState.endX = evt.touches[0].pageX;
+          this.touchSwipeState.endY = evt.touches[0].pageY;
+          // Do a preventDefault to avoid the swipe from triggering browser
+          // gestures (Chrome in particular has some sort of swipe gesture in
+          // fullscreen mode).
+          evt.preventDefault();
+          break;
+        case 'touchend':
+          if (this.touchSwipeState === null) {
+            return;
+          }
+          var delta = 0;
+          var dx = this.touchSwipeState.endX - this.touchSwipeState.startX;
+          var dy = this.touchSwipeState.endY - this.touchSwipeState.startY;
+          var absAngle = Math.abs(Math.atan2(dy, dx));
+          if (Math.abs(dx) > SWIPE_MIN_DISTANCE_THRESHOLD &&
+              (absAngle <= SWIPE_ANGLE_THRESHOLD ||
+               absAngle >= (Math.PI - SWIPE_ANGLE_THRESHOLD))) {
+            // horizontal swipe
+            delta = dx;
+          } else if (Math.abs(dy) > SWIPE_MIN_DISTANCE_THRESHOLD &&
+              Math.abs(absAngle - (Math.PI / 2)) <= SWIPE_ANGLE_THRESHOLD) {
+            // vertical swipe
+            delta = dy;
+          }
+          if (delta > 0) {
+            this._goToPreviousPage();
+          } else if (delta < 0) {
+            this._goToNextPage();
+          }
+          break;
+      }
+    },
+
     /**
      * @private
      */
@@ -348,12 +431,16 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
       this.mouseWheelBind = this._mouseWheel.bind(this);
       this.resetMouseScrollStateBind = this._resetMouseScrollState.bind(this);
       this.contextMenuBind = this._contextMenu.bind(this);
+      this.touchSwipeBind = this._touchSwipe.bind(this);
 
       window.addEventListener('mousemove', this.showControlsBind);
       window.addEventListener('mousedown', this.mouseDownBind);
       window.addEventListener('wheel', this.mouseWheelBind);
       window.addEventListener('keydown', this.resetMouseScrollStateBind);
       window.addEventListener('contextmenu', this.contextMenuBind);
+      window.addEventListener('touchstart', this.touchSwipeBind);
+      window.addEventListener('touchmove', this.touchSwipeBind);
+      window.addEventListener('touchend', this.touchSwipeBind);
     },
 
     /**
@@ -366,12 +453,16 @@ var PDFPresentationMode = (function PDFPresentationModeClosure() {
       window.removeEventListener('wheel', this.mouseWheelBind);
       window.removeEventListener('keydown', this.resetMouseScrollStateBind);
       window.removeEventListener('contextmenu', this.contextMenuBind);
+      window.removeEventListener('touchstart', this.touchSwipeBind);
+      window.removeEventListener('touchmove', this.touchSwipeBind);
+      window.removeEventListener('touchend', this.touchSwipeBind);
 
       delete this.showControlsBind;
       delete this.mouseDownBind;
       delete this.mouseWheelBind;
       delete this.resetMouseScrollStateBind;
       delete this.contextMenuBind;
+      delete this.touchSwipeBind;
     },
 
     /**