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.
 
 
 

263 lines
6.7 KiB

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