blob: 7c52a0fcbbfcf0a2699ec3fef6073bacf833fde3 [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
Marc Kupietzcf6e9982026-08-15 15:37:40 +020069 this.element.style.display = (
70 config.controls &&
71 (config.controls !== 'speaker-only' || this.Reveal.isSpeakerNotes())
72 ) ? 'block' : 'none';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020073
74 this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
75 this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
76
77 }
78
79 bind() {
80
81 // Listen to both touch and click events, in case the device
82 // supports both
83 let pointerEvents = [ 'touchstart', 'click' ];
84
85 // Only support touch for Android, fixes double navigations in
86 // stock browser
87 if( isAndroid ) {
88 pointerEvents = [ 'touchstart' ];
89 }
90
91 pointerEvents.forEach( eventName => {
92 this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
93 this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
94 this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
95 this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
96 this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
97 this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
Marc Kupietz9c036a42024-05-14 13:17:25 +020098 this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, false ) );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020099 } );
100
101 }
102
103 unbind() {
104
105 [ 'touchstart', 'click' ].forEach( eventName => {
106 this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
107 this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
108 this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
109 this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
110 this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
111 this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
Marc Kupietz9c036a42024-05-14 13:17:25 +0200112 this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, false ) );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200113 } );
114
115 }
116
117 /**
118 * Updates the state of all control/navigation arrows.
119 */
120 update() {
121
122 let routes = this.Reveal.availableRoutes();
123
124 // Remove the 'enabled' class from all directions
125 [...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
126 node.classList.remove( 'enabled', 'fragmented' );
127
128 // Set 'disabled' attribute on all directions
129 node.setAttribute( 'disabled', 'disabled' );
130 } );
131
132 // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
133 if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
134 if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
135 if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
136 if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
137
138 // Prev/next buttons
139 if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
140 if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
141
142 // Highlight fragment directions
143 let currentSlide = this.Reveal.getCurrentSlide();
144 if( currentSlide ) {
145
146 let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
147
148 // Always apply fragment decorator to prev/next buttons
149 if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
150 if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
151
Marc Kupietzcf6e9982026-08-15 15:37:40 +0200152 const isVerticalStack = this.Reveal.isVerticalSlide( currentSlide );
153 const hasVerticalSiblings = isVerticalStack &&
154 currentSlide.parentElement &&
155 currentSlide.parentElement.querySelectorAll( ':scope > section' ).length > 1;
156
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200157 // Apply fragment decorators to directional buttons based on
158 // what slide axis they are in
Marc Kupietzcf6e9982026-08-15 15:37:40 +0200159 if( isVerticalStack && hasVerticalSiblings ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200160 if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
161 if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
162 }
163 else {
164 if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
165 if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
166 }
167
168 }
169
170 if( this.Reveal.getConfig().controlsTutorial ) {
171
172 let indices = this.Reveal.getIndices();
173
174 // Highlight control arrows with an animation to ensure
175 // that the viewer knows how to navigate
176 if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
177 this.controlsDownArrow.classList.add( 'highlight' );
178 }
179 else {
180 this.controlsDownArrow.classList.remove( 'highlight' );
181
182 if( this.Reveal.getConfig().rtl ) {
183
184 if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
185 this.controlsLeftArrow.classList.add( 'highlight' );
186 }
187 else {
188 this.controlsLeftArrow.classList.remove( 'highlight' );
189 }
190
191 } else {
192
193 if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
194 this.controlsRightArrow.classList.add( 'highlight' );
195 }
196 else {
197 this.controlsRightArrow.classList.remove( 'highlight' );
198 }
199 }
200 }
201 }
202 }
203
Marc Kupietz09b75752023-10-07 09:32:19 +0200204 destroy() {
205
206 this.unbind();
207 this.element.remove();
208
209 }
210
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200211 /**
212 * Event handlers for navigation control buttons.
213 */
214 onNavigateLeftClicked( event ) {
215
216 event.preventDefault();
217 this.Reveal.onUserInput();
218
219 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
220 this.Reveal.prev();
221 }
222 else {
223 this.Reveal.left();
224 }
225
226 }
227
228 onNavigateRightClicked( event ) {
229
230 event.preventDefault();
231 this.Reveal.onUserInput();
232
233 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
234 this.Reveal.next();
235 }
236 else {
237 this.Reveal.right();
238 }
239
240 }
241
242 onNavigateUpClicked( event ) {
243
244 event.preventDefault();
245 this.Reveal.onUserInput();
246
247 this.Reveal.up();
248
249 }
250
251 onNavigateDownClicked( event ) {
252
253 event.preventDefault();
254 this.Reveal.onUserInput();
255
256 this.Reveal.down();
257
258 }
259
260 onNavigatePrevClicked( event ) {
261
262 event.preventDefault();
263 this.Reveal.onUserInput();
264
265 this.Reveal.prev();
266
267 }
268
269 onNavigateNextClicked( event ) {
270
271 event.preventDefault();
272 this.Reveal.onUserInput();
273
274 this.Reveal.next();
275
276 }
277
Marc Kupietz9c036a42024-05-14 13:17:25 +0200278 onEnterFullscreen( event ) {
279
280 const config = this.Reveal.getConfig();
281 const viewport = this.Reveal.getViewportElement();
282
283 enterFullscreen( config.embedded ? viewport : viewport.parentElement );
284
285 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200286
287}