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