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