import { isAndroid } from '../utils/device.js' import { matches } from '../utils/util.js' const SWIPE_THRESHOLD = 40; /** * Controls all touch interactions and navigations for * a presentation. */ export default class Touch { constructor( Reveal ) { this.Reveal = Reveal; // Holds information about the currently ongoing touch interaction this.touchStartX = 0; this.touchStartY = 0; this.touchStartCount = 0; this.touchCaptured = false; this.onPointerDown = this.onPointerDown.bind( this ); this.onPointerMove = this.onPointerMove.bind( this ); this.onPointerUp = this.onPointerUp.bind( this ); this.onTouchStart = this.onTouchStart.bind( this ); this.onTouchMove = this.onTouchMove.bind( this ); this.onTouchEnd = this.onTouchEnd.bind( this ); } /** * */ bind() { let revealElement = this.Reveal.getRevealElement(); if( 'onpointerdown' in window ) { // Use W3C pointer events revealElement.addEventListener( 'pointerdown', this.onPointerDown, false ); revealElement.addEventListener( 'pointermove', this.onPointerMove, false ); revealElement.addEventListener( 'pointerup', this.onPointerUp, false ); } else if( window.navigator.msPointerEnabled ) { // IE 10 uses prefixed version of pointer events revealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false ); revealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false ); revealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false ); } else { // Fall back to touch events revealElement.addEventListener( 'touchstart', this.onTouchStart, false ); revealElement.addEventListener( 'touchmove', this.onTouchMove, false ); revealElement.addEventListener( 'touchend', this.onTouchEnd, false ); } } /** * */ unbind() { let revealElement = this.Reveal.getRevealElement(); revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false ); revealElement.removeEventListener( 'pointermove', this.onPointerMove, false ); revealElement.removeEventListener( 'pointerup', this.onPointerUp, false ); revealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false ); revealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false ); revealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false ); revealElement.removeEventListener( 'touchstart', this.onTouchStart, false ); revealElement.removeEventListener( 'touchmove', this.onTouchMove, false ); revealElement.removeEventListener( 'touchend', this.onTouchEnd, false ); } /** * Checks if the target element prevents the triggering of * swipe navigation. */ isSwipePrevented( target ) { // Prevent accidental swipes when scrubbing timelines if( matches( target, 'video[controls], audio[controls]' ) ) return true; while( target && typeof target.hasAttribute === 'function' ) { if( target.hasAttribute( 'data-prevent-swipe' ) ) return true; target = target.parentNode; } return false; } /** * Handler for the 'touchstart' event, enables support for * swipe and pinch gestures. * * @param {object} event */ onTouchStart( event ) { this.touchCaptured = false; if( this.isSwipePrevented( event.target ) ) return true; this.touchStartX = event.touches[0].clientX; this.touchStartY = event.touches[0].clientY; this.touchStartCount = event.touches.length; } /** * Handler for the 'touchmove' event. * * @param {object} event */ onTouchMove( event ) { if( this.isSwipePrevented( event.target ) ) return true; let config = this.Reveal.getConfig(); // Each touch should only trigger one action if( !this.touchCaptured ) { this.Reveal.onUserInput( event ); let currentX = event.touches[0].clientX; let currentY = event.touches[0].clientY; // There was only one touch point, look for a swipe if( event.touches.length === 1 && this.touchStartCount !== 2 ) { let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true }); let deltaX = currentX - this.touchStartX, deltaY = currentY - this.touchStartY; if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) { this.touchCaptured = true; if( config.navigationMode === 'linear' ) { if( config.rtl ) { this.Reveal.next(); } else { this.Reveal.prev(); } } else { this.Reveal.left(); } } else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) { this.touchCaptured = true; if( config.navigationMode === 'linear' ) { if( config.rtl ) { this.Reveal.prev(); } else { this.Reveal.next(); } } else { this.Reveal.right(); } } else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) { this.touchCaptured = true; if( config.navigationMode === 'linear' ) { this.Reveal.prev(); } else { this.Reveal.up(); } } else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) { this.touchCaptured = true; if( config.navigationMode === 'linear' ) { this.Reveal.next(); } else { this.Reveal.down(); } } // If we're embedded, only block touch events if they have // triggered an action if( config.embedded ) { if( this.touchCaptured || this.Reveal.isVerticalSlide() ) { event.preventDefault(); } } // Not embedded? Block them all to avoid needless tossing // around of the viewport in iOS else { event.preventDefault(); } } } // There's a bug with swiping on some Android devices unless // the default action is always prevented else if( isAndroid ) { event.preventDefault(); } } /** * Handler for the 'touchend' event. * * @param {object} event */ onTouchEnd( event ) { this.touchCaptured = false; } /** * Convert pointer down to touch start. * * @param {object} event */ onPointerDown( event ) { if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchStart( event ); } } /** * Convert pointer move to touch move. * * @param {object} event */ onPointerMove( event ) { if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchMove( event ); } } /** * Convert pointer up to touch end. * * @param {object} event */ onPointerUp( event ) { if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchEnd( event ); } } }