blob: 7078cf250f2b868fd56fd438b70d116f913a8fb4 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { isAndroid } from '../utils/device.js'
2import { matches } from '../utils/util.js'
3
4const SWIPE_THRESHOLD = 40;
5
6/**
7 * Controls all touch interactions and navigations for
8 * a presentation.
9 */
10export 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 Kupietz9c036a42024-05-14 13:17:25 +020087 if( matches( target, 'video[controls], audio[controls]' ) ) return true;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020088
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 Kupietz9c036a42024-05-14 13:17:25 +0200106 this.touchCaptured = false;
107
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200108 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}