blob: 556bcf083d1d242beacef7d9810081a792702d41 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { queryAll } from '../utils/util.js'
2import { isAndroid } from '../utils/device.js'
3
4/**
5 * Manages our presentation controls. This includes both
6 * the built-in control arrows as well as event monitoring
7 * of any elements within the presentation with either of the
8 * following helper classes:
9 * - .navigate-up
10 * - .navigate-right
11 * - .navigate-down
12 * - .navigate-left
13 * - .navigate-next
14 * - .navigate-prev
15 */
16export default class Controls {
17
18 constructor( Reveal ) {
19
20 this.Reveal = Reveal;
21
22 this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
23 this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
24 this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
25 this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
26 this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
27 this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
28
29 }
30
31 render() {
32
33 const rtl = this.Reveal.getConfig().rtl;
34 const revealElement = this.Reveal.getRevealElement();
35
36 this.element = document.createElement( 'aside' );
37 this.element.className = 'controls';
38 this.element.innerHTML =
39 `<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
40 <button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
41 <button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
42 <button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
43
44 this.Reveal.getRevealElement().appendChild( this.element );
45
46 // There can be multiple instances of controls throughout the page
47 this.controlsLeft = queryAll( revealElement, '.navigate-left' );
48 this.controlsRight = queryAll( revealElement, '.navigate-right' );
49 this.controlsUp = queryAll( revealElement, '.navigate-up' );
50 this.controlsDown = queryAll( revealElement, '.navigate-down' );
51 this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
52 this.controlsNext = queryAll( revealElement, '.navigate-next' );
53
54 // The left, right and down arrows in the standard reveal.js controls
55 this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
56 this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
57 this.controlsDownArrow = this.element.querySelector( '.navigate-down' );
58
59 }
60
61 /**
62 * Called when the reveal.js config is updated.
63 */
64 configure( config, oldConfig ) {
65
66 this.element.style.display = config.controls ? 'block' : 'none';
67
68 this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
69 this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
70
71 }
72
73 bind() {
74
75 // Listen to both touch and click events, in case the device
76 // supports both
77 let pointerEvents = [ 'touchstart', 'click' ];
78
79 // Only support touch for Android, fixes double navigations in
80 // stock browser
81 if( isAndroid ) {
82 pointerEvents = [ 'touchstart' ];
83 }
84
85 pointerEvents.forEach( eventName => {
86 this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
87 this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
88 this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
89 this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
90 this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
91 this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
92 } );
93
94 }
95
96 unbind() {
97
98 [ 'touchstart', 'click' ].forEach( eventName => {
99 this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
100 this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
101 this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
102 this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
103 this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
104 this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
105 } );
106
107 }
108
109 /**
110 * Updates the state of all control/navigation arrows.
111 */
112 update() {
113
114 let routes = this.Reveal.availableRoutes();
115
116 // Remove the 'enabled' class from all directions
117 [...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
118 node.classList.remove( 'enabled', 'fragmented' );
119
120 // Set 'disabled' attribute on all directions
121 node.setAttribute( 'disabled', 'disabled' );
122 } );
123
124 // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
125 if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
126 if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
127 if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
128 if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
129
130 // Prev/next buttons
131 if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
132 if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
133
134 // Highlight fragment directions
135 let currentSlide = this.Reveal.getCurrentSlide();
136 if( currentSlide ) {
137
138 let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
139
140 // Always apply fragment decorator to prev/next buttons
141 if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
142 if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
143
144 // Apply fragment decorators to directional buttons based on
145 // what slide axis they are in
146 if( this.Reveal.isVerticalSlide( currentSlide ) ) {
147 if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
148 if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
149 }
150 else {
151 if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
152 if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
153 }
154
155 }
156
157 if( this.Reveal.getConfig().controlsTutorial ) {
158
159 let indices = this.Reveal.getIndices();
160
161 // Highlight control arrows with an animation to ensure
162 // that the viewer knows how to navigate
163 if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
164 this.controlsDownArrow.classList.add( 'highlight' );
165 }
166 else {
167 this.controlsDownArrow.classList.remove( 'highlight' );
168
169 if( this.Reveal.getConfig().rtl ) {
170
171 if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
172 this.controlsLeftArrow.classList.add( 'highlight' );
173 }
174 else {
175 this.controlsLeftArrow.classList.remove( 'highlight' );
176 }
177
178 } else {
179
180 if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
181 this.controlsRightArrow.classList.add( 'highlight' );
182 }
183 else {
184 this.controlsRightArrow.classList.remove( 'highlight' );
185 }
186 }
187 }
188 }
189 }
190
191 /**
192 * Event handlers for navigation control buttons.
193 */
194 onNavigateLeftClicked( event ) {
195
196 event.preventDefault();
197 this.Reveal.onUserInput();
198
199 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
200 this.Reveal.prev();
201 }
202 else {
203 this.Reveal.left();
204 }
205
206 }
207
208 onNavigateRightClicked( event ) {
209
210 event.preventDefault();
211 this.Reveal.onUserInput();
212
213 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
214 this.Reveal.next();
215 }
216 else {
217 this.Reveal.right();
218 }
219
220 }
221
222 onNavigateUpClicked( event ) {
223
224 event.preventDefault();
225 this.Reveal.onUserInput();
226
227 this.Reveal.up();
228
229 }
230
231 onNavigateDownClicked( event ) {
232
233 event.preventDefault();
234 this.Reveal.onUserInput();
235
236 this.Reveal.down();
237
238 }
239
240 onNavigatePrevClicked( event ) {
241
242 event.preventDefault();
243 this.Reveal.onUserInput();
244
245 this.Reveal.prev();
246
247 }
248
249 onNavigateNextClicked( event ) {
250
251 event.preventDefault();
252 this.Reveal.onUserInput();
253
254 this.Reveal.next();
255
256 }
257
258
259}