You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

259 lines
6.5KB

  1. import { isAndroid } from '../utils/device.js'
  2. const SWIPE_THRESHOLD = 40;
  3. /**
  4. * Controls all touch interactions and navigations for
  5. * a presentation.
  6. */
  7. export default class Touch {
  8. constructor( Reveal ) {
  9. this.Reveal = Reveal;
  10. // Holds information about the currently ongoing touch interaction
  11. this.touchStartX = 0;
  12. this.touchStartY = 0;
  13. this.touchStartCount = 0;
  14. this.touchCaptured = false;
  15. this.onPointerDown = this.onPointerDown.bind( this );
  16. this.onPointerMove = this.onPointerMove.bind( this );
  17. this.onPointerUp = this.onPointerUp.bind( this );
  18. this.onTouchStart = this.onTouchStart.bind( this );
  19. this.onTouchMove = this.onTouchMove.bind( this );
  20. this.onTouchEnd = this.onTouchEnd.bind( this );
  21. }
  22. /**
  23. *
  24. */
  25. bind() {
  26. let revealElement = this.Reveal.getRevealElement();
  27. if( 'onpointerdown' in window ) {
  28. // Use W3C pointer events
  29. revealElement.addEventListener( 'pointerdown', this.onPointerDown, false );
  30. revealElement.addEventListener( 'pointermove', this.onPointerMove, false );
  31. revealElement.addEventListener( 'pointerup', this.onPointerUp, false );
  32. }
  33. else if( window.navigator.msPointerEnabled ) {
  34. // IE 10 uses prefixed version of pointer events
  35. revealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false );
  36. revealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false );
  37. revealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false );
  38. }
  39. else {
  40. // Fall back to touch events
  41. revealElement.addEventListener( 'touchstart', this.onTouchStart, false );
  42. revealElement.addEventListener( 'touchmove', this.onTouchMove, false );
  43. revealElement.addEventListener( 'touchend', this.onTouchEnd, false );
  44. }
  45. }
  46. /**
  47. *
  48. */
  49. unbind() {
  50. let revealElement = this.Reveal.getRevealElement();
  51. revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );
  52. revealElement.removeEventListener( 'pointermove', this.onPointerMove, false );
  53. revealElement.removeEventListener( 'pointerup', this.onPointerUp, false );
  54. revealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false );
  55. revealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false );
  56. revealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false );
  57. revealElement.removeEventListener( 'touchstart', this.onTouchStart, false );
  58. revealElement.removeEventListener( 'touchmove', this.onTouchMove, false );
  59. revealElement.removeEventListener( 'touchend', this.onTouchEnd, false );
  60. }
  61. /**
  62. * Checks if the target element prevents the triggering of
  63. * swipe navigation.
  64. */
  65. isSwipePrevented( target ) {
  66. while( target && typeof target.hasAttribute === 'function' ) {
  67. if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
  68. target = target.parentNode;
  69. }
  70. return false;
  71. }
  72. /**
  73. * Handler for the 'touchstart' event, enables support for
  74. * swipe and pinch gestures.
  75. *
  76. * @param {object} event
  77. */
  78. onTouchStart( event ) {
  79. if( this.isSwipePrevented( event.target ) ) return true;
  80. this.touchStartX = event.touches[0].clientX;
  81. this.touchStartY = event.touches[0].clientY;
  82. this.touchStartCount = event.touches.length;
  83. }
  84. /**
  85. * Handler for the 'touchmove' event.
  86. *
  87. * @param {object} event
  88. */
  89. onTouchMove( event ) {
  90. if( this.isSwipePrevented( event.target ) ) return true;
  91. let config = this.Reveal.getConfig();
  92. // Each touch should only trigger one action
  93. if( !this.touchCaptured ) {
  94. this.Reveal.onUserInput( event );
  95. let currentX = event.touches[0].clientX;
  96. let currentY = event.touches[0].clientY;
  97. // There was only one touch point, look for a swipe
  98. if( event.touches.length === 1 && this.touchStartCount !== 2 ) {
  99. let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });
  100. let deltaX = currentX - this.touchStartX,
  101. deltaY = currentY - this.touchStartY;
  102. if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
  103. this.touchCaptured = true;
  104. if( config.navigationMode === 'linear' ) {
  105. if( config.rtl ) {
  106. this.Reveal.next();
  107. }
  108. else {
  109. this.Reveal.prev();
  110. }
  111. }
  112. else {
  113. this.Reveal.left();
  114. }
  115. }
  116. else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
  117. this.touchCaptured = true;
  118. if( config.navigationMode === 'linear' ) {
  119. if( config.rtl ) {
  120. this.Reveal.prev();
  121. }
  122. else {
  123. this.Reveal.next();
  124. }
  125. }
  126. else {
  127. this.Reveal.right();
  128. }
  129. }
  130. else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {
  131. this.touchCaptured = true;
  132. if( config.navigationMode === 'linear' ) {
  133. this.Reveal.prev();
  134. }
  135. else {
  136. this.Reveal.up();
  137. }
  138. }
  139. else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {
  140. this.touchCaptured = true;
  141. if( config.navigationMode === 'linear' ) {
  142. this.Reveal.next();
  143. }
  144. else {
  145. this.Reveal.down();
  146. }
  147. }
  148. // If we're embedded, only block touch events if they have
  149. // triggered an action
  150. if( config.embedded ) {
  151. if( this.touchCaptured || this.Reveal.isVerticalSlide() ) {
  152. event.preventDefault();
  153. }
  154. }
  155. // Not embedded? Block them all to avoid needless tossing
  156. // around of the viewport in iOS
  157. else {
  158. event.preventDefault();
  159. }
  160. }
  161. }
  162. // There's a bug with swiping on some Android devices unless
  163. // the default action is always prevented
  164. else if( isAndroid ) {
  165. event.preventDefault();
  166. }
  167. }
  168. /**
  169. * Handler for the 'touchend' event.
  170. *
  171. * @param {object} event
  172. */
  173. onTouchEnd( event ) {
  174. this.touchCaptured = false;
  175. }
  176. /**
  177. * Convert pointer down to touch start.
  178. *
  179. * @param {object} event
  180. */
  181. onPointerDown( event ) {
  182. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  183. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  184. this.onTouchStart( event );
  185. }
  186. }
  187. /**
  188. * Convert pointer move to touch move.
  189. *
  190. * @param {object} event
  191. */
  192. onPointerMove( event ) {
  193. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  194. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  195. this.onTouchMove( event );
  196. }
  197. }
  198. /**
  199. * Convert pointer up to touch end.
  200. *
  201. * @param {object} event
  202. */
  203. onPointerUp( event ) {
  204. if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
  205. event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
  206. this.onTouchEnd( event );
  207. }
  208. }
  209. }