返回 reveal.js
touch.js
根目录 / js / controllers / touch.js
1 import { isAndroid } from '../utils/device'
2 import { matches } from '../utils/util'
3
4 const SWIPE_THRESHOLD = 40;
5 const ZOOM_GESTURE_THRESHOLD = 1.03;
6
7 /**
8 * Controls all touch interactions and navigations for
9 * a presentation.
10 */
11 export default class Touch {
12
13 constructor( Reveal ) {
14
15 this.Reveal = Reveal;
16
17 // Holds information about the currently ongoing touch interaction
18 this.touchStartX = 0;
19 this.touchStartY = 0;
20 this.touchStartCount = 0;
21 this.touchCaptured = false;
22 this.activePointers = new Map();
23
24 this.onPointerDown = this.onPointerDown.bind( this );
25 this.onPointerMove = this.onPointerMove.bind( this );
26 this.onPointerUp = this.onPointerUp.bind( this );
27 this.onPointerCancel = this.onPointerCancel.bind( this );
28 this.onTouchStart = this.onTouchStart.bind( this );
29 this.onTouchMove = this.onTouchMove.bind( this );
30 this.onTouchEnd = this.onTouchEnd.bind( this );
31
32 }
33
34 /**
35 *
36 */
37 bind() {
38
39 let revealElement = this.Reveal.getRevealElement();
40
41 if( 'onpointerdown' in window ) {
42 revealElement.addEventListener( 'pointerdown', this.onPointerDown, false );
43 revealElement.addEventListener( 'pointermove', this.onPointerMove, false );
44 revealElement.addEventListener( 'pointerup', this.onPointerUp, false );
45 revealElement.addEventListener( 'pointercancel', this.onPointerCancel, false );
46 }
47 // Fall back to touch events
48 else {
49 revealElement.addEventListener( 'touchstart', this.onTouchStart, false );
50 revealElement.addEventListener( 'touchmove', this.onTouchMove, false );
51 revealElement.addEventListener( 'touchend', this.onTouchEnd, false );
52 }
53
54 }
55
56 /**
57 *
58 */
59 unbind() {
60
61 let revealElement = this.Reveal.getRevealElement();
62
63 revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );
64 revealElement.removeEventListener( 'pointermove', this.onPointerMove, false );
65 revealElement.removeEventListener( 'pointerup', this.onPointerUp, false );
66 revealElement.removeEventListener( 'pointercancel', this.onPointerCancel, false );
67
68 revealElement.removeEventListener( 'touchstart', this.onTouchStart, false );
69 revealElement.removeEventListener( 'touchmove', this.onTouchMove, false );
70 revealElement.removeEventListener( 'touchend', this.onTouchEnd, false );
71
72 }
73
74 /**
75 * Checks if the target element prevents the triggering of
76 * swipe navigation.
77 */
78 isSwipePrevented( target ) {
79
80 // Prevent accidental swipes when scrubbing timelines
81 if( matches( target, 'video[controls], audio[controls]' ) ) return true;
82
83 while( target && typeof target.hasAttribute === 'function' ) {
84 if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
85 target = target.parentNode;
86 }
87
88 return false;
89
90 }
91
92 /**
93 * Returns true when browser-level zoom is active enough that
94 * we should not trigger reveal.js touch gestures.
95 */
96 isViewportZoomed() {
97
98 if( !window.visualViewport || typeof window.visualViewport.scale !== 'number' ) {
99 return false;
100 }
101
102 const currentScale = window.visualViewport.scale;
103
104 // Fixed-width viewport configurations may start below 1.
105 // Compare against the smallest observed "resting" scale.
106 if( typeof this.visualViewportBaseScale === 'undefined' ) {
107 this.visualViewportBaseScale = currentScale;
108 } else {
109 this.visualViewportBaseScale = Math.min( this.visualViewportBaseScale, currentScale );
110 }
111
112 return ( currentScale / this.visualViewportBaseScale ) > ZOOM_GESTURE_THRESHOLD;
113
114 }
115
116 /**
117 * Handler for the 'touchstart' event, enables support for
118 * swipe and pinch gestures.
119 *
120 * @param {object} event
121 */
122 onTouchStart( event ) {
123
124 this.touchCaptured = false;
125
126 // if( this.isViewportZoomed() ) return true;
127 if( this.isSwipePrevented( event.target ) ) return true;
128
129 this.touchStartX = event.touches[0].clientX;
130 this.touchStartY = event.touches[0].clientY;
131 this.touchStartCount = event.touches.length;
132
133 }
134
135 /**
136 * Handler for the 'touchmove' event.
137 *
138 * @param {object} event
139 */
140 onTouchMove( event ) {
141
142 // if( this.isViewportZoomed() ) return true;
143 if( this.isSwipePrevented( event.target ) ) return true;
144
145 let config = this.Reveal.getConfig();
146
147 // Each touch should only trigger one action
148 if( !this.touchCaptured ) {
149 this.Reveal.onUserInput( event );
150
151 let currentX = event.touches[0].clientX;
152 let currentY = event.touches[0].clientY;
153
154 // There was only one touch point, look for a swipe
155 if( event.touches.length === 1 && this.touchStartCount !== 2 ) {
156
157 let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });
158
159 let deltaX = currentX - this.touchStartX,
160 deltaY = currentY - this.touchStartY;
161
162 if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
163 this.touchCaptured = true;
164 if( config.navigationMode === 'linear' ) {
165 if( config.rtl ) {
166 this.Reveal.next();
167 }
168 else {
169 this.Reveal.prev();
170 }
171 }
172 else {
173 this.Reveal.left();
174 }
175 }
176 else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
177 this.touchCaptured = true;
178 if( config.navigationMode === 'linear' ) {
179 if( config.rtl ) {
180 this.Reveal.prev();
181 }
182 else {
183 this.Reveal.next();
184 }
185 }
186 else {
187 this.Reveal.right();
188 }
189 }
190 else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {
191 this.touchCaptured = true;
192 if( config.navigationMode === 'linear' ) {
193 this.Reveal.prev();
194 }
195 else {
196 this.Reveal.up();
197 }
198 }
199 else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {
200 this.touchCaptured = true;
201 if( config.navigationMode === 'linear' ) {
202 this.Reveal.next();
203 }
204 else {
205 this.Reveal.down();
206 }
207 }
208
209 // If we're embedded, only block touch events if they have
210 // triggered an action
211 if( config.embedded ) {
212 if( this.touchCaptured || this.Reveal.isVerticalSlide() ) {
213 event.preventDefault();
214 }
215 }
216 // Not embedded? Block them all to avoid needless tossing
217 // around of the viewport in iOS
218 else {
219 event.preventDefault();
220 }
221
222 }
223 }
224 // There's a bug with swiping on some Android devices unless
225 // the default action is always prevented
226 else if( isAndroid ) {
227 event.preventDefault();
228 }
229
230 }
231
232 /**
233 * Handler for the 'touchend' event.
234 *
235 * @param {object} event
236 */
237 onTouchEnd( event ) {
238
239 // Media playback is only allowed as a direct result of a
240 // user interaction. Some mobile devices do not consider a
241 // 'touchmove' to be a direct user action. If this is the
242 // case, we fall back to starting playback here instead.
243 if( this.touchCaptured && !this.Reveal.slideContent.isAllowedToPlayAudio() ) {
244 this.Reveal.startEmbeddedContent( this.Reveal.getCurrentSlide() );
245 }
246
247 this.touchCaptured = false;
248
249 }
250
251 /**
252 * Returns all active touch pointers in touch-like format.
253 *
254 * @return {Array}
255 */
256 getActiveTouches() {
257
258 return Array.from( this.activePointers.values(), pointer => ({
259 clientX: pointer.clientX,
260 clientY: pointer.clientY
261 }) );
262
263 }
264
265 /**
266 * Convert pointer down to touch start.
267 *
268 * @param {object} event
269 */
270 onPointerDown( event ) {
271
272 if( event.pointerType === "touch" ) {
273 this.activePointers.set( event.pointerId, event );
274 event.touches = this.getActiveTouches();
275 this.onTouchStart( event );
276 }
277
278 }
279
280 /**
281 * Convert pointer move to touch move.
282 *
283 * @param {object} event
284 */
285 onPointerMove( event ) {
286
287 if( event.pointerType === "touch" ) {
288 this.activePointers.set( event.pointerId, event );
289 event.touches = this.getActiveTouches();
290 this.onTouchMove( event );
291 }
292
293 }
294
295 /**
296 * Convert pointer up to touch end.
297 *
298 * @param {object} event
299 */
300 onPointerUp( event ) {
301
302 if( event.pointerType === "touch" ) {
303 this.activePointers.delete( event.pointerId );
304 event.touches = this.getActiveTouches();
305 this.onTouchEnd( event );
306 }
307
308 }
309
310 /**
311 * Convert pointer cancel to touch end.
312 *
313 * @param {object} event
314 */
315 onPointerCancel( event ) {
316
317 if( event.pointerType === "touch" ) {
318 this.activePointers.delete( event.pointerId );
319 event.touches = this.getActiveTouches();
320 this.onTouchEnd( event );
321 }
322
323 }
324
325 }
325 lines JAVASCRIPT