blob: 1d191164a1955a10116fd65c152fa354ef07bf57 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import SlideContent from './controllers/slidecontent.js'
2import SlideNumber from './controllers/slidenumber.js'
Marc Kupietz09b75752023-10-07 09:32:19 +02003import JumpToSlide from './controllers/jumptoslide.js'
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02004import Backgrounds from './controllers/backgrounds.js'
5import AutoAnimate from './controllers/autoanimate.js'
6import Fragments from './controllers/fragments.js'
7import Overview from './controllers/overview.js'
8import Keyboard from './controllers/keyboard.js'
9import Location from './controllers/location.js'
10import Controls from './controllers/controls.js'
11import Progress from './controllers/progress.js'
12import Pointer from './controllers/pointer.js'
13import Plugins from './controllers/plugins.js'
14import Print from './controllers/print.js'
15import Touch from './controllers/touch.js'
16import Focus from './controllers/focus.js'
17import Notes from './controllers/notes.js'
18import Playback from './components/playback.js'
19import defaultConfig from './config.js'
20import * as Util from './utils/util.js'
21import * as Device from './utils/device.js'
22import {
23 SLIDES_SELECTOR,
24 HORIZONTAL_SLIDES_SELECTOR,
25 VERTICAL_SLIDES_SELECTOR,
26 POST_MESSAGE_METHOD_BLACKLIST
27} from './utils/constants.js'
28
29// The reveal.js version
Marc Kupietz09b75752023-10-07 09:32:19 +020030export const VERSION = '4.6.0';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020031
32/**
33 * reveal.js
34 * https://revealjs.com
35 * MIT licensed
36 *
Marc Kupietz09b75752023-10-07 09:32:19 +020037 * Copyright (C) 2011-2022 Hakim El Hattab, https://hakim.se
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020038 */
39export default function( revealElement, options ) {
40
41 // Support initialization with no args, one arg
42 // [options] or two args [revealElement, options]
43 if( arguments.length < 2 ) {
44 options = arguments[0];
45 revealElement = document.querySelector( '.reveal' );
46 }
47
48 const Reveal = {};
49
50 // Configuration defaults, can be overridden at initialization time
51 let config = {},
52
53 // Flags if reveal.js is loaded (has dispatched the 'ready' event)
54 ready = false,
55
56 // The horizontal and vertical index of the currently active slide
57 indexh,
58 indexv,
59
60 // The previous and current slide HTML elements
61 previousSlide,
62 currentSlide,
63
64 // Remember which directions that the user has navigated towards
65 navigationHistory = {
66 hasNavigatedHorizontally: false,
67 hasNavigatedVertically: false
68 },
69
70 // Slides may have a data-state attribute which we pick up and apply
71 // as a class to the body. This list contains the combined state of
72 // all current slides.
73 state = [],
74
75 // The current scale of the presentation (see width/height config)
76 scale = 1,
77
78 // CSS transform that is currently applied to the slides container,
79 // split into two groups
80 slidesTransform = { layout: '', overview: '' },
81
82 // Cached references to DOM elements
83 dom = {},
84
85 // Flags if the interaction event listeners are bound
86 eventsAreBound = false,
87
88 // The current slide transition state; idle or running
89 transition = 'idle',
90
91 // The current auto-slide duration
92 autoSlide = 0,
93
94 // Auto slide properties
95 autoSlidePlayer,
96 autoSlideTimeout = 0,
97 autoSlideStartTime = -1,
98 autoSlidePaused = false,
99
100 // Controllers for different aspects of our presentation. They're
101 // all given direct references to this Reveal instance since there
102 // may be multiple presentations running in parallel.
103 slideContent = new SlideContent( Reveal ),
104 slideNumber = new SlideNumber( Reveal ),
Marc Kupietz09b75752023-10-07 09:32:19 +0200105 jumpToSlide = new JumpToSlide( Reveal ),
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200106 autoAnimate = new AutoAnimate( Reveal ),
107 backgrounds = new Backgrounds( Reveal ),
108 fragments = new Fragments( Reveal ),
109 overview = new Overview( Reveal ),
110 keyboard = new Keyboard( Reveal ),
111 location = new Location( Reveal ),
112 controls = new Controls( Reveal ),
113 progress = new Progress( Reveal ),
114 pointer = new Pointer( Reveal ),
115 plugins = new Plugins( Reveal ),
116 print = new Print( Reveal ),
117 focus = new Focus( Reveal ),
118 touch = new Touch( Reveal ),
119 notes = new Notes( Reveal );
120
121 /**
122 * Starts up the presentation.
123 */
124 function initialize( initOptions ) {
125
Christophe Dervieux8afae132021-12-06 15:16:42 +0100126 if( !revealElement ) throw 'Unable to find presentation root (<div class="reveal">).';
127
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200128 // Cache references to key DOM elements
129 dom.wrapper = revealElement;
130 dom.slides = revealElement.querySelector( '.slides' );
131
Christophe Dervieux8afae132021-12-06 15:16:42 +0100132 if( !dom.slides ) throw 'Unable to find slides container (<div class="slides">).';
133
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200134 // Compose our config object in order of increasing precedence:
135 // 1. Default reveal.js options
136 // 2. Options provided via Reveal.configure() prior to
137 // initialization
138 // 3. Options passed to the Reveal constructor
139 // 4. Options passed to Reveal.initialize
140 // 5. Query params
141 config = { ...defaultConfig, ...config, ...options, ...initOptions, ...Util.getQueryHash() };
142
143 setViewport();
144
145 // Force a layout when the whole page, incl fonts, has loaded
146 window.addEventListener( 'load', layout, false );
147
148 // Register plugins and load dependencies, then move on to #start()
149 plugins.load( config.plugins, config.dependencies ).then( start );
150
151 return new Promise( resolve => Reveal.on( 'ready', resolve ) );
152
153 }
154
155 /**
156 * Encase the presentation in a reveal.js viewport. The
157 * extent of the viewport differs based on configuration.
158 */
159 function setViewport() {
160
161 // Embedded decks use the reveal element as their viewport
162 if( config.embedded === true ) {
163 dom.viewport = Util.closest( revealElement, '.reveal-viewport' ) || revealElement;
164 }
165 // Full-page decks use the body as their viewport
166 else {
167 dom.viewport = document.body;
168 document.documentElement.classList.add( 'reveal-full-page' );
169 }
170
171 dom.viewport.classList.add( 'reveal-viewport' );
172
173 }
174
175 /**
176 * Starts up reveal.js by binding input events and navigating
177 * to the current URL deeplink if there is one.
178 */
179 function start() {
180
181 ready = true;
182
183 // Remove slides hidden with data-visibility
184 removeHiddenSlides();
185
186 // Make sure we've got all the DOM elements we need
187 setupDOM();
188
189 // Listen to messages posted to this window
190 setupPostMessage();
191
192 // Prevent the slides from being scrolled out of view
193 setupScrollPrevention();
194
Marc Kupietz09b75752023-10-07 09:32:19 +0200195 // Adds bindings for fullscreen mode
196 setupFullscreen();
197
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200198 // Resets all vertical slides so that only the first is visible
199 resetVerticalSlides();
200
201 // Updates the presentation to match the current configuration values
202 configure();
203
204 // Read the initial hash
205 location.readURL();
206
207 // Create slide backgrounds
208 backgrounds.update( true );
209
210 // Notify listeners that the presentation is ready but use a 1ms
211 // timeout to ensure it's not fired synchronously after #initialize()
212 setTimeout( () => {
213 // Enable transitions now that we're loaded
214 dom.slides.classList.remove( 'no-transition' );
215
216 dom.wrapper.classList.add( 'ready' );
217
218 dispatchEvent({
219 type: 'ready',
220 data: {
221 indexh,
222 indexv,
223 currentSlide
224 }
225 });
226 }, 1 );
227
228 // Special setup and config is required when printing to PDF
229 if( print.isPrintingPDF() ) {
230 removeEventListeners();
231
232 // The document needs to have loaded for the PDF layout
233 // measurements to be accurate
234 if( document.readyState === 'complete' ) {
235 print.setupPDF();
236 }
237 else {
238 window.addEventListener( 'load', () => {
239 print.setupPDF();
240 } );
241 }
242 }
243
244 }
245
246 /**
247 * Removes all slides with data-visibility="hidden". This
248 * is done right before the rest of the presentation is
249 * initialized.
250 *
251 * If you want to show all hidden slides, initialize
252 * reveal.js with showHiddenSlides set to true.
253 */
254 function removeHiddenSlides() {
255
256 if( !config.showHiddenSlides ) {
257 Util.queryAll( dom.wrapper, 'section[data-visibility="hidden"]' ).forEach( slide => {
258 slide.parentNode.removeChild( slide );
259 } );
260 }
261
262 }
263
264 /**
265 * Finds and stores references to DOM elements which are
266 * required by the presentation. If a required element is
267 * not found, it is created.
268 */
269 function setupDOM() {
270
271 // Prevent transitions while we're loading
272 dom.slides.classList.add( 'no-transition' );
273
274 if( Device.isMobile ) {
275 dom.wrapper.classList.add( 'no-hover' );
276 }
277 else {
278 dom.wrapper.classList.remove( 'no-hover' );
279 }
280
281 backgrounds.render();
282 slideNumber.render();
Marc Kupietz09b75752023-10-07 09:32:19 +0200283 jumpToSlide.render();
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200284 controls.render();
285 progress.render();
286 notes.render();
287
288 // Overlay graphic which is displayed during the paused mode
289 dom.pauseOverlay = Util.createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
290
291 dom.statusElement = createStatusElement();
292
293 dom.wrapper.setAttribute( 'role', 'application' );
294 }
295
296 /**
297 * Creates a hidden div with role aria-live to announce the
298 * current slide content. Hide the div off-screen to make it
299 * available only to Assistive Technologies.
300 *
301 * @return {HTMLElement}
302 */
303 function createStatusElement() {
304
305 let statusElement = dom.wrapper.querySelector( '.aria-status' );
306 if( !statusElement ) {
307 statusElement = document.createElement( 'div' );
308 statusElement.style.position = 'absolute';
309 statusElement.style.height = '1px';
310 statusElement.style.width = '1px';
311 statusElement.style.overflow = 'hidden';
312 statusElement.style.clip = 'rect( 1px, 1px, 1px, 1px )';
313 statusElement.classList.add( 'aria-status' );
314 statusElement.setAttribute( 'aria-live', 'polite' );
315 statusElement.setAttribute( 'aria-atomic','true' );
316 dom.wrapper.appendChild( statusElement );
317 }
318 return statusElement;
319
320 }
321
322 /**
323 * Announces the given text to screen readers.
324 */
325 function announceStatus( value ) {
326
327 dom.statusElement.textContent = value;
328
329 }
330
331 /**
332 * Converts the given HTML element into a string of text
333 * that can be announced to a screen reader. Hidden
334 * elements are excluded.
335 */
336 function getStatusText( node ) {
337
338 let text = '';
339
340 // Text node
341 if( node.nodeType === 3 ) {
342 text += node.textContent;
343 }
344 // Element node
345 else if( node.nodeType === 1 ) {
346
347 let isAriaHidden = node.getAttribute( 'aria-hidden' );
348 let isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
349 if( isAriaHidden !== 'true' && !isDisplayHidden ) {
350
351 Array.from( node.childNodes ).forEach( child => {
352 text += getStatusText( child );
353 } );
354
355 }
356
357 }
358
359 text = text.trim();
360
361 return text === '' ? '' : text + ' ';
362
363 }
364
365 /**
366 * This is an unfortunate necessity. Some actions – such as
367 * an input field being focused in an iframe or using the
368 * keyboard to expand text selection beyond the bounds of
369 * a slide – can trigger our content to be pushed out of view.
370 * This scrolling can not be prevented by hiding overflow in
371 * CSS (we already do) so we have to resort to repeatedly
372 * checking if the slides have been offset :(
373 */
374 function setupScrollPrevention() {
375
376 setInterval( () => {
377 if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
378 dom.wrapper.scrollTop = 0;
379 dom.wrapper.scrollLeft = 0;
380 }
381 }, 1000 );
382
383 }
384
385 /**
Marc Kupietz09b75752023-10-07 09:32:19 +0200386 * After entering fullscreen we need to force a layout to
387 * get our presentations to scale correctly. This behavior
388 * is inconsistent across browsers but a force layout seems
389 * to normalize it.
390 */
391 function setupFullscreen() {
392
393 document.addEventListener( 'fullscreenchange', onFullscreenChange );
394 document.addEventListener( 'webkitfullscreenchange', onFullscreenChange );
395
396 }
397
398 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200399 * Registers a listener to postMessage events, this makes it
400 * possible to call all reveal.js API methods from another
401 * window. For example:
402 *
403 * revealWindow.postMessage( JSON.stringify({
404 * method: 'slide',
405 * args: [ 2 ]
406 * }), '*' );
407 */
408 function setupPostMessage() {
409
410 if( config.postMessage ) {
Marc Kupietz09b75752023-10-07 09:32:19 +0200411 window.addEventListener( 'message', onPostMessage, false );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200412 }
413
414 }
415
416 /**
417 * Applies the configuration settings from the config
418 * object. May be called multiple times.
419 *
420 * @param {object} options
421 */
422 function configure( options ) {
423
424 const oldConfig = { ...config }
425
426 // New config options may be passed when this method
427 // is invoked through the API after initialization
428 if( typeof options === 'object' ) Util.extend( config, options );
429
430 // Abort if reveal.js hasn't finished loading, config
431 // changes will be applied automatically once ready
432 if( Reveal.isReady() === false ) return;
433
434 const numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
435
436 // The transition is added as a class on the .reveal element
437 dom.wrapper.classList.remove( oldConfig.transition );
438 dom.wrapper.classList.add( config.transition );
439
440 dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
441 dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
442
443 // Expose our configured slide dimensions as custom props
444 dom.viewport.style.setProperty( '--slide-width', config.width + 'px' );
445 dom.viewport.style.setProperty( '--slide-height', config.height + 'px' );
446
447 if( config.shuffle ) {
448 shuffle();
449 }
450
451 Util.toggleClass( dom.wrapper, 'embedded', config.embedded );
452 Util.toggleClass( dom.wrapper, 'rtl', config.rtl );
453 Util.toggleClass( dom.wrapper, 'center', config.center );
454
455 // Exit the paused mode if it was configured off
456 if( config.pause === false ) {
457 resume();
458 }
459
460 // Iframe link previews
461 if( config.previewLinks ) {
462 enablePreviewLinks();
463 disablePreviewLinks( '[data-preview-link=false]' );
464 }
465 else {
466 disablePreviewLinks();
467 enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
468 }
469
470 // Reset all changes made by auto-animations
471 autoAnimate.reset();
472
473 // Remove existing auto-slide controls
474 if( autoSlidePlayer ) {
475 autoSlidePlayer.destroy();
476 autoSlidePlayer = null;
477 }
478
479 // Generate auto-slide controls if needed
480 if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable ) {
481 autoSlidePlayer = new Playback( dom.wrapper, () => {
482 return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
483 } );
484
485 autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
486 autoSlidePaused = false;
487 }
488
489 // Add the navigation mode to the DOM so we can adjust styling
490 if( config.navigationMode !== 'default' ) {
491 dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
492 }
493 else {
494 dom.wrapper.removeAttribute( 'data-navigation-mode' );
495 }
496
497 notes.configure( config, oldConfig );
498 focus.configure( config, oldConfig );
499 pointer.configure( config, oldConfig );
500 controls.configure( config, oldConfig );
501 progress.configure( config, oldConfig );
502 keyboard.configure( config, oldConfig );
503 fragments.configure( config, oldConfig );
504 slideNumber.configure( config, oldConfig );
505
506 sync();
507
508 }
509
510 /**
511 * Binds all event listeners.
512 */
513 function addEventListeners() {
514
515 eventsAreBound = true;
516
517 window.addEventListener( 'resize', onWindowResize, false );
518
519 if( config.touch ) touch.bind();
520 if( config.keyboard ) keyboard.bind();
521 if( config.progress ) progress.bind();
522 if( config.respondToHashChanges ) location.bind();
523 controls.bind();
524 focus.bind();
525
Christophe Dervieux8afae132021-12-06 15:16:42 +0100526 dom.slides.addEventListener( 'click', onSlidesClicked, false );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200527 dom.slides.addEventListener( 'transitionend', onTransitionEnd, false );
528 dom.pauseOverlay.addEventListener( 'click', resume, false );
529
530 if( config.focusBodyOnPageVisibilityChange ) {
531 document.addEventListener( 'visibilitychange', onPageVisibilityChange, false );
532 }
533
534 }
535
536 /**
537 * Unbinds all event listeners.
538 */
539 function removeEventListeners() {
540
541 eventsAreBound = false;
542
543 touch.unbind();
544 focus.unbind();
545 keyboard.unbind();
546 controls.unbind();
547 progress.unbind();
548 location.unbind();
549
550 window.removeEventListener( 'resize', onWindowResize, false );
551
Christophe Dervieux8afae132021-12-06 15:16:42 +0100552 dom.slides.removeEventListener( 'click', onSlidesClicked, false );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200553 dom.slides.removeEventListener( 'transitionend', onTransitionEnd, false );
554 dom.pauseOverlay.removeEventListener( 'click', resume, false );
555
556 }
557
558 /**
Marc Kupietz09b75752023-10-07 09:32:19 +0200559 * Uninitializes reveal.js by undoing changes made to the
560 * DOM and removing all event listeners.
561 */
562 function destroy() {
563
564 removeEventListeners();
565 cancelAutoSlide();
566 disablePreviewLinks();
567
568 // Destroy controllers
569 notes.destroy();
570 focus.destroy();
571 plugins.destroy();
572 pointer.destroy();
573 controls.destroy();
574 progress.destroy();
575 backgrounds.destroy();
576 slideNumber.destroy();
577 jumpToSlide.destroy();
578
579 // Remove event listeners
580 document.removeEventListener( 'fullscreenchange', onFullscreenChange );
581 document.removeEventListener( 'webkitfullscreenchange', onFullscreenChange );
582 document.removeEventListener( 'visibilitychange', onPageVisibilityChange, false );
583 window.removeEventListener( 'message', onPostMessage, false );
584 window.removeEventListener( 'load', layout, false );
585
586 // Undo DOM changes
587 if( dom.pauseOverlay ) dom.pauseOverlay.remove();
588 if( dom.statusElement ) dom.statusElement.remove();
589
590 document.documentElement.classList.remove( 'reveal-full-page' );
591
592 dom.wrapper.classList.remove( 'ready', 'center', 'has-horizontal-slides', 'has-vertical-slides' );
593 dom.wrapper.removeAttribute( 'data-transition-speed' );
594 dom.wrapper.removeAttribute( 'data-background-transition' );
595
596 dom.viewport.classList.remove( 'reveal-viewport' );
597 dom.viewport.style.removeProperty( '--slide-width' );
598 dom.viewport.style.removeProperty( '--slide-height' );
599
600 dom.slides.style.removeProperty( 'width' );
601 dom.slides.style.removeProperty( 'height' );
602 dom.slides.style.removeProperty( 'zoom' );
603 dom.slides.style.removeProperty( 'left' );
604 dom.slides.style.removeProperty( 'top' );
605 dom.slides.style.removeProperty( 'bottom' );
606 dom.slides.style.removeProperty( 'right' );
607 dom.slides.style.removeProperty( 'transform' );
608
609 Array.from( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( slide => {
610 slide.style.removeProperty( 'display' );
611 slide.style.removeProperty( 'top' );
612 slide.removeAttribute( 'hidden' );
613 slide.removeAttribute( 'aria-hidden' );
614 } );
615
616 }
617
618 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200619 * Adds a listener to one of our custom reveal.js events,
620 * like slidechanged.
621 */
622 function on( type, listener, useCapture ) {
623
624 revealElement.addEventListener( type, listener, useCapture );
625
626 }
627
628 /**
629 * Unsubscribes from a reveal.js event.
630 */
631 function off( type, listener, useCapture ) {
632
633 revealElement.removeEventListener( type, listener, useCapture );
634
635 }
636
637 /**
638 * Applies CSS transforms to the slides container. The container
639 * is transformed from two separate sources: layout and the overview
640 * mode.
641 *
642 * @param {object} transforms
643 */
644 function transformSlides( transforms ) {
645
646 // Pick up new transforms from arguments
647 if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
648 if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
649
650 // Apply the transforms to the slides container
651 if( slidesTransform.layout ) {
652 Util.transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
653 }
654 else {
655 Util.transformElement( dom.slides, slidesTransform.overview );
656 }
657
658 }
659
660 /**
661 * Dispatches an event of the specified type from the
662 * reveal DOM element.
663 */
664 function dispatchEvent({ target=dom.wrapper, type, data, bubbles=true }) {
665
666 let event = document.createEvent( 'HTMLEvents', 1, 2 );
667 event.initEvent( type, bubbles, true );
668 Util.extend( event, data );
669 target.dispatchEvent( event );
670
671 if( target === dom.wrapper ) {
672 // If we're in an iframe, post each reveal.js event to the
673 // parent window. Used by the notes plugin
674 dispatchPostMessage( type );
675 }
676
Christophe Dervieux8afae132021-12-06 15:16:42 +0100677 return event;
678
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200679 }
680
681 /**
682 * Dispatched a postMessage of the given type from our window.
683 */
684 function dispatchPostMessage( type, data ) {
685
686 if( config.postMessageEvents && window.parent !== window.self ) {
687 let message = {
688 namespace: 'reveal',
689 eventName: type,
690 state: getState()
691 };
692
693 Util.extend( message, data );
694
695 window.parent.postMessage( JSON.stringify( message ), '*' );
696 }
697
698 }
699
700 /**
701 * Bind preview frame links.
702 *
703 * @param {string} [selector=a] - selector for anchors
704 */
705 function enablePreviewLinks( selector = 'a' ) {
706
707 Array.from( dom.wrapper.querySelectorAll( selector ) ).forEach( element => {
708 if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
709 element.addEventListener( 'click', onPreviewLinkClicked, false );
710 }
711 } );
712
713 }
714
715 /**
716 * Unbind preview frame links.
717 */
718 function disablePreviewLinks( selector = 'a' ) {
719
720 Array.from( dom.wrapper.querySelectorAll( selector ) ).forEach( element => {
721 if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
722 element.removeEventListener( 'click', onPreviewLinkClicked, false );
723 }
724 } );
725
726 }
727
728 /**
729 * Opens a preview window for the target URL.
730 *
731 * @param {string} url - url for preview iframe src
732 */
733 function showPreview( url ) {
734
735 closeOverlay();
736
737 dom.overlay = document.createElement( 'div' );
738 dom.overlay.classList.add( 'overlay' );
739 dom.overlay.classList.add( 'overlay-preview' );
740 dom.wrapper.appendChild( dom.overlay );
741
742 dom.overlay.innerHTML =
743 `<header>
744 <a class="close" href="#"><span class="icon"></span></a>
745 <a class="external" href="${url}" target="_blank"><span class="icon"></span></a>
746 </header>
747 <div class="spinner"></div>
748 <div class="viewport">
749 <iframe src="${url}"></iframe>
750 <small class="viewport-inner">
751 <span class="x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
752 </small>
753 </div>`;
754
755 dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', event => {
756 dom.overlay.classList.add( 'loaded' );
757 }, false );
758
759 dom.overlay.querySelector( '.close' ).addEventListener( 'click', event => {
760 closeOverlay();
761 event.preventDefault();
762 }, false );
763
764 dom.overlay.querySelector( '.external' ).addEventListener( 'click', event => {
765 closeOverlay();
766 }, false );
767
768 }
769
770 /**
771 * Open or close help overlay window.
772 *
773 * @param {Boolean} [override] Flag which overrides the
774 * toggle logic and forcibly sets the desired state. True means
775 * help is open, false means it's closed.
776 */
777 function toggleHelp( override ){
778
779 if( typeof override === 'boolean' ) {
780 override ? showHelp() : closeOverlay();
781 }
782 else {
783 if( dom.overlay ) {
784 closeOverlay();
785 }
786 else {
787 showHelp();
788 }
789 }
790 }
791
792 /**
793 * Opens an overlay window with help material.
794 */
795 function showHelp() {
796
797 if( config.help ) {
798
799 closeOverlay();
800
801 dom.overlay = document.createElement( 'div' );
802 dom.overlay.classList.add( 'overlay' );
803 dom.overlay.classList.add( 'overlay-help' );
804 dom.wrapper.appendChild( dom.overlay );
805
806 let html = '<p class="title">Keyboard Shortcuts</p><br/>';
807
808 let shortcuts = keyboard.getShortcuts(),
809 bindings = keyboard.getBindings();
810
811 html += '<table><th>KEY</th><th>ACTION</th>';
812 for( let key in shortcuts ) {
813 html += `<tr><td>${key}</td><td>${shortcuts[ key ]}</td></tr>`;
814 }
815
816 // Add custom key bindings that have associated descriptions
817 for( let binding in bindings ) {
818 if( bindings[binding].key && bindings[binding].description ) {
819 html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
820 }
821 }
822
823 html += '</table>';
824
825 dom.overlay.innerHTML = `
826 <header>
827 <a class="close" href="#"><span class="icon"></span></a>
828 </header>
829 <div class="viewport">
830 <div class="viewport-inner">${html}</div>
831 </div>
832 `;
833
834 dom.overlay.querySelector( '.close' ).addEventListener( 'click', event => {
835 closeOverlay();
836 event.preventDefault();
837 }, false );
838
839 }
840
841 }
842
843 /**
844 * Closes any currently open overlay.
845 */
846 function closeOverlay() {
847
848 if( dom.overlay ) {
849 dom.overlay.parentNode.removeChild( dom.overlay );
850 dom.overlay = null;
851 return true;
852 }
853
854 return false;
855
856 }
857
858 /**
859 * Applies JavaScript-controlled layout rules to the
860 * presentation.
861 */
862 function layout() {
863
864 if( dom.wrapper && !print.isPrintingPDF() ) {
865
866 if( !config.disableLayout ) {
867
868 // On some mobile devices '100vh' is taller than the visible
869 // viewport which leads to part of the presentation being
870 // cut off. To work around this we define our own '--vh' custom
871 // property where 100x adds up to the correct height.
872 //
873 // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
874 if( Device.isMobile && !config.embedded ) {
875 document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
876 }
877
878 const size = getComputedSlideSize();
879
880 const oldScale = scale;
881
882 // Layout the contents of the slides
883 layoutSlideContents( config.width, config.height );
884
885 dom.slides.style.width = size.width + 'px';
886 dom.slides.style.height = size.height + 'px';
887
888 // Determine scale of content to fit within available space
889 scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
890
891 // Respect max/min scale settings
892 scale = Math.max( scale, config.minScale );
893 scale = Math.min( scale, config.maxScale );
894
895 // Don't apply any scaling styles if scale is 1
896 if( scale === 1 ) {
897 dom.slides.style.zoom = '';
898 dom.slides.style.left = '';
899 dom.slides.style.top = '';
900 dom.slides.style.bottom = '';
901 dom.slides.style.right = '';
902 transformSlides( { layout: '' } );
903 }
904 else {
Marc Kupietz09b75752023-10-07 09:32:19 +0200905 dom.slides.style.zoom = '';
906 dom.slides.style.left = '50%';
907 dom.slides.style.top = '50%';
908 dom.slides.style.bottom = 'auto';
909 dom.slides.style.right = 'auto';
910 transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200911 }
912
913 // Select all slides, vertical and horizontal
914 const slides = Array.from( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
915
916 for( let i = 0, len = slides.length; i < len; i++ ) {
917 const slide = slides[ i ];
918
919 // Don't bother updating invisible slides
920 if( slide.style.display === 'none' ) {
921 continue;
922 }
923
924 if( config.center || slide.classList.contains( 'center' ) ) {
925 // Vertical stacks are not centred since their section
926 // children will be
927 if( slide.classList.contains( 'stack' ) ) {
928 slide.style.top = 0;
929 }
930 else {
931 slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
932 }
933 }
934 else {
935 slide.style.top = '';
936 }
937
938 }
939
940 if( oldScale !== scale ) {
941 dispatchEvent({
942 type: 'resize',
943 data: {
944 oldScale,
945 scale,
946 size
947 }
948 });
949 }
950 }
951
Marc Kupietz09b75752023-10-07 09:32:19 +0200952 dom.viewport.style.setProperty( '--slide-scale', scale );
953
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200954 progress.update();
955 backgrounds.updateParallax();
956
957 if( overview.isActive() ) {
958 overview.update();
959 }
960
961 }
962
963 }
964
965 /**
966 * Applies layout logic to the contents of all slides in
967 * the presentation.
968 *
969 * @param {string|number} width
970 * @param {string|number} height
971 */
972 function layoutSlideContents( width, height ) {
973
974 // Handle sizing of elements with the 'r-stretch' class
975 Util.queryAll( dom.slides, 'section > .stretch, section > .r-stretch' ).forEach( element => {
976
977 // Determine how much vertical space we can use
978 let remainingHeight = Util.getRemainingHeight( element, height );
979
980 // Consider the aspect ratio of media elements
981 if( /(img|video)/gi.test( element.nodeName ) ) {
982 const nw = element.naturalWidth || element.videoWidth,
983 nh = element.naturalHeight || element.videoHeight;
984
985 const es = Math.min( width / nw, remainingHeight / nh );
986
987 element.style.width = ( nw * es ) + 'px';
988 element.style.height = ( nh * es ) + 'px';
989
990 }
991 else {
992 element.style.width = width + 'px';
993 element.style.height = remainingHeight + 'px';
994 }
995
996 } );
997
998 }
999
1000 /**
1001 * Calculates the computed pixel size of our slides. These
1002 * values are based on the width and height configuration
1003 * options.
1004 *
1005 * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
1006 * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
1007 */
1008 function getComputedSlideSize( presentationWidth, presentationHeight ) {
Marc Kupietz09b75752023-10-07 09:32:19 +02001009 let width = config.width;
1010 let height = config.height;
1011
1012 if( config.disableLayout ) {
1013 width = dom.slides.offsetWidth;
1014 height = dom.slides.offsetHeight;
1015 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001016
1017 const size = {
1018 // Slide size
Marc Kupietz09b75752023-10-07 09:32:19 +02001019 width: width,
1020 height: height,
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001021
1022 // Presentation size
1023 presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
1024 presentationHeight: presentationHeight || dom.wrapper.offsetHeight
1025 };
1026
1027 // Reduce available space by margin
1028 size.presentationWidth -= ( size.presentationWidth * config.margin );
1029 size.presentationHeight -= ( size.presentationHeight * config.margin );
1030
1031 // Slide width may be a percentage of available width
1032 if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
1033 size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
1034 }
1035
1036 // Slide height may be a percentage of available height
1037 if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
1038 size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
1039 }
1040
1041 return size;
1042
1043 }
1044
1045 /**
1046 * Stores the vertical index of a stack so that the same
1047 * vertical slide can be selected when navigating to and
1048 * from the stack.
1049 *
1050 * @param {HTMLElement} stack The vertical stack element
1051 * @param {string|number} [v=0] Index to memorize
1052 */
1053 function setPreviousVerticalIndex( stack, v ) {
1054
1055 if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
1056 stack.setAttribute( 'data-previous-indexv', v || 0 );
1057 }
1058
1059 }
1060
1061 /**
1062 * Retrieves the vertical index which was stored using
1063 * #setPreviousVerticalIndex() or 0 if no previous index
1064 * exists.
1065 *
1066 * @param {HTMLElement} stack The vertical stack element
1067 */
1068 function getPreviousVerticalIndex( stack ) {
1069
1070 if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
1071 // Prefer manually defined start-indexv
1072 const attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
1073
1074 return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
1075 }
1076
1077 return 0;
1078
1079 }
1080
1081 /**
1082 * Checks if the current or specified slide is vertical
1083 * (nested within another slide).
1084 *
1085 * @param {HTMLElement} [slide=currentSlide] The slide to check
1086 * orientation of
1087 * @return {Boolean}
1088 */
1089 function isVerticalSlide( slide = currentSlide ) {
1090
1091 return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
1092
1093 }
1094
1095 /**
1096 * Returns true if we're on the last slide in the current
1097 * vertical stack.
1098 */
1099 function isLastVerticalSlide() {
1100
1101 if( currentSlide && isVerticalSlide( currentSlide ) ) {
1102 // Does this slide have a next sibling?
1103 if( currentSlide.nextElementSibling ) return false;
1104
1105 return true;
1106 }
1107
1108 return false;
1109
1110 }
1111
1112 /**
1113 * Returns true if we're currently on the first slide in
1114 * the presentation.
1115 */
1116 function isFirstSlide() {
1117
1118 return indexh === 0 && indexv === 0;
1119
1120 }
1121
1122 /**
1123 * Returns true if we're currently on the last slide in
1124 * the presenation. If the last slide is a stack, we only
1125 * consider this the last slide if it's at the end of the
1126 * stack.
1127 */
1128 function isLastSlide() {
1129
1130 if( currentSlide ) {
1131 // Does this slide have a next sibling?
1132 if( currentSlide.nextElementSibling ) return false;
1133
1134 // If it's vertical, does its parent have a next sibling?
1135 if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
1136
1137 return true;
1138 }
1139
1140 return false;
1141
1142 }
1143
1144 /**
1145 * Enters the paused mode which fades everything on screen to
1146 * black.
1147 */
1148 function pause() {
1149
1150 if( config.pause ) {
1151 const wasPaused = dom.wrapper.classList.contains( 'paused' );
1152
1153 cancelAutoSlide();
1154 dom.wrapper.classList.add( 'paused' );
1155
1156 if( wasPaused === false ) {
1157 dispatchEvent({ type: 'paused' });
1158 }
1159 }
1160
1161 }
1162
1163 /**
1164 * Exits from the paused mode.
1165 */
1166 function resume() {
1167
1168 const wasPaused = dom.wrapper.classList.contains( 'paused' );
1169 dom.wrapper.classList.remove( 'paused' );
1170
1171 cueAutoSlide();
1172
1173 if( wasPaused ) {
1174 dispatchEvent({ type: 'resumed' });
1175 }
1176
1177 }
1178
1179 /**
1180 * Toggles the paused mode on and off.
1181 */
1182 function togglePause( override ) {
1183
1184 if( typeof override === 'boolean' ) {
1185 override ? pause() : resume();
1186 }
1187 else {
1188 isPaused() ? resume() : pause();
1189 }
1190
1191 }
1192
1193 /**
1194 * Checks if we are currently in the paused mode.
1195 *
1196 * @return {Boolean}
1197 */
1198 function isPaused() {
1199
1200 return dom.wrapper.classList.contains( 'paused' );
1201
1202 }
1203
1204 /**
Marc Kupietz09b75752023-10-07 09:32:19 +02001205 * Toggles visibility of the jump-to-slide UI.
1206 */
1207 function toggleJumpToSlide( override ) {
1208
1209 if( typeof override === 'boolean' ) {
1210 override ? jumpToSlide.show() : jumpToSlide.hide();
1211 }
1212 else {
1213 jumpToSlide.isVisible() ? jumpToSlide.hide() : jumpToSlide.show();
1214 }
1215
1216 }
1217
1218 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001219 * Toggles the auto slide mode on and off.
1220 *
1221 * @param {Boolean} [override] Flag which sets the desired state.
1222 * True means autoplay starts, false means it stops.
1223 */
1224
1225 function toggleAutoSlide( override ) {
1226
1227 if( typeof override === 'boolean' ) {
1228 override ? resumeAutoSlide() : pauseAutoSlide();
1229 }
1230
1231 else {
1232 autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
1233 }
1234
1235 }
1236
1237 /**
1238 * Checks if the auto slide mode is currently on.
1239 *
1240 * @return {Boolean}
1241 */
1242 function isAutoSliding() {
1243
1244 return !!( autoSlide && !autoSlidePaused );
1245
1246 }
1247
1248 /**
1249 * Steps from the current point in the presentation to the
1250 * slide which matches the specified horizontal and vertical
1251 * indices.
1252 *
1253 * @param {number} [h=indexh] Horizontal index of the target slide
1254 * @param {number} [v=indexv] Vertical index of the target slide
1255 * @param {number} [f] Index of a fragment within the
1256 * target slide to activate
Christophe Dervieux8afae132021-12-06 15:16:42 +01001257 * @param {number} [origin] Origin for use in multimaster environments
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001258 */
Christophe Dervieux8afae132021-12-06 15:16:42 +01001259 function slide( h, v, f, origin ) {
1260
Marc Kupietz09b75752023-10-07 09:32:19 +02001261 // Dispatch an event before the slide
Christophe Dervieux8afae132021-12-06 15:16:42 +01001262 const slidechange = dispatchEvent({
1263 type: 'beforeslidechange',
1264 data: {
1265 indexh: h === undefined ? indexh : h,
1266 indexv: v === undefined ? indexv : v,
1267 origin
1268 }
1269 });
1270
1271 // Abort if this slide change was prevented by an event listener
1272 if( slidechange.defaultPrevented ) return;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001273
1274 // Remember where we were at before
1275 previousSlide = currentSlide;
1276
1277 // Query all horizontal slides in the deck
1278 const horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
1279
1280 // Abort if there are no slides
1281 if( horizontalSlides.length === 0 ) return;
1282
1283 // If no vertical index is specified and the upcoming slide is a
1284 // stack, resume at its previous vertical index
1285 if( v === undefined && !overview.isActive() ) {
1286 v = getPreviousVerticalIndex( horizontalSlides[ h ] );
1287 }
1288
1289 // If we were on a vertical stack, remember what vertical index
1290 // it was on so we can resume at the same position when returning
1291 if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
1292 setPreviousVerticalIndex( previousSlide.parentNode, indexv );
1293 }
1294
1295 // Remember the state before this slide
1296 const stateBefore = state.concat();
1297
1298 // Reset the state array
1299 state.length = 0;
1300
1301 let indexhBefore = indexh || 0,
1302 indexvBefore = indexv || 0;
1303
1304 // Activate and transition to the new slide
1305 indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
1306 indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
1307
1308 // Dispatch an event if the slide changed
1309 let slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
1310
1311 // Ensure that the previous slide is never the same as the current
1312 if( !slideChanged ) previousSlide = null;
1313
1314 // Find the current horizontal slide and any possible vertical slides
1315 // within it
1316 let currentHorizontalSlide = horizontalSlides[ indexh ],
1317 currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
1318
1319 // Store references to the previous and current slides
1320 currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
1321
1322 let autoAnimateTransition = false;
1323
1324 // Detect if we're moving between two auto-animated slides
1325 if( slideChanged && previousSlide && currentSlide && !overview.isActive() ) {
1326
1327 // If this is an auto-animated transition, we disable the
1328 // regular slide transition
1329 //
1330 // Note 20-03-2020:
1331 // This needs to happen before we update slide visibility,
1332 // otherwise transitions will still run in Safari.
1333 if( previousSlide.hasAttribute( 'data-auto-animate' ) && currentSlide.hasAttribute( 'data-auto-animate' )
1334 && previousSlide.getAttribute( 'data-auto-animate-id' ) === currentSlide.getAttribute( 'data-auto-animate-id' )
1335 && !( ( indexh > indexhBefore || indexv > indexvBefore ) ? currentSlide : previousSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {
1336
1337 autoAnimateTransition = true;
1338 dom.slides.classList.add( 'disable-slide-transitions' );
1339 }
1340
1341 transition = 'running';
1342
1343 }
1344
1345 // Update the visibility of slides now that the indices have changed
1346 updateSlidesVisibility();
1347
1348 layout();
1349
1350 // Update the overview if it's currently active
1351 if( overview.isActive() ) {
1352 overview.update();
1353 }
1354
1355 // Show fragment, if specified
1356 if( typeof f !== 'undefined' ) {
1357 fragments.goto( f );
1358 }
1359
1360 // Solves an edge case where the previous slide maintains the
1361 // 'present' class when navigating between adjacent vertical
1362 // stacks
1363 if( previousSlide && previousSlide !== currentSlide ) {
1364 previousSlide.classList.remove( 'present' );
1365 previousSlide.setAttribute( 'aria-hidden', 'true' );
1366
1367 // Reset all slides upon navigate to home
1368 if( isFirstSlide() ) {
1369 // Launch async task
1370 setTimeout( () => {
1371 getVerticalStacks().forEach( slide => {
1372 setPreviousVerticalIndex( slide, 0 );
1373 } );
1374 }, 0 );
1375 }
1376 }
1377
1378 // Apply the new state
1379 stateLoop: for( let i = 0, len = state.length; i < len; i++ ) {
1380 // Check if this state existed on the previous slide. If it
1381 // did, we will avoid adding it repeatedly
1382 for( let j = 0; j < stateBefore.length; j++ ) {
1383 if( stateBefore[j] === state[i] ) {
1384 stateBefore.splice( j, 1 );
1385 continue stateLoop;
1386 }
1387 }
1388
1389 dom.viewport.classList.add( state[i] );
1390
1391 // Dispatch custom event matching the state's name
1392 dispatchEvent({ type: state[i] });
1393 }
1394
1395 // Clean up the remains of the previous state
1396 while( stateBefore.length ) {
1397 dom.viewport.classList.remove( stateBefore.pop() );
1398 }
1399
1400 if( slideChanged ) {
1401 dispatchEvent({
1402 type: 'slidechanged',
1403 data: {
1404 indexh,
1405 indexv,
1406 previousSlide,
1407 currentSlide,
Christophe Dervieux8afae132021-12-06 15:16:42 +01001408 origin
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001409 }
1410 });
1411 }
1412
1413 // Handle embedded content
1414 if( slideChanged || !previousSlide ) {
1415 slideContent.stopEmbeddedContent( previousSlide );
1416 slideContent.startEmbeddedContent( currentSlide );
1417 }
1418
1419 // Announce the current slide contents to screen readers
1420 // Use animation frame to prevent getComputedStyle in getStatusText
1421 // from triggering layout mid-frame
1422 requestAnimationFrame( () => {
1423 announceStatus( getStatusText( currentSlide ) );
1424 });
1425
1426 progress.update();
1427 controls.update();
1428 notes.update();
1429 backgrounds.update();
1430 backgrounds.updateParallax();
1431 slideNumber.update();
1432 fragments.update();
1433
1434 // Update the URL hash
1435 location.writeURL();
1436
1437 cueAutoSlide();
1438
1439 // Auto-animation
1440 if( autoAnimateTransition ) {
1441
1442 setTimeout( () => {
1443 dom.slides.classList.remove( 'disable-slide-transitions' );
1444 }, 0 );
1445
1446 if( config.autoAnimate ) {
1447 // Run the auto-animation between our slides
1448 autoAnimate.run( previousSlide, currentSlide );
1449 }
1450
1451 }
1452
1453 }
1454
1455 /**
1456 * Syncs the presentation with the current DOM. Useful
1457 * when new slides or control elements are added or when
1458 * the configuration has changed.
1459 */
1460 function sync() {
1461
1462 // Subscribe to input
1463 removeEventListeners();
1464 addEventListeners();
1465
1466 // Force a layout to make sure the current config is accounted for
1467 layout();
1468
1469 // Reflect the current autoSlide value
1470 autoSlide = config.autoSlide;
1471
1472 // Start auto-sliding if it's enabled
1473 cueAutoSlide();
1474
1475 // Re-create all slide backgrounds
1476 backgrounds.create();
1477
1478 // Write the current hash to the URL
1479 location.writeURL();
1480
Marc Kupietz09b75752023-10-07 09:32:19 +02001481 if( config.sortFragmentsOnSync === true ) {
1482 fragments.sortAll();
1483 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001484
1485 controls.update();
1486 progress.update();
1487
1488 updateSlidesVisibility();
1489
1490 notes.update();
1491 notes.updateVisibility();
1492 backgrounds.update( true );
1493 slideNumber.update();
1494 slideContent.formatEmbeddedContent();
1495
1496 // Start or stop embedded content depending on global config
1497 if( config.autoPlayMedia === false ) {
1498 slideContent.stopEmbeddedContent( currentSlide, { unloadIframes: false } );
1499 }
1500 else {
1501 slideContent.startEmbeddedContent( currentSlide );
1502 }
1503
1504 if( overview.isActive() ) {
1505 overview.layout();
1506 }
1507
1508 }
1509
1510 /**
1511 * Updates reveal.js to keep in sync with new slide attributes. For
1512 * example, if you add a new `data-background-image` you can call
1513 * this to have reveal.js render the new background image.
1514 *
1515 * Similar to #sync() but more efficient when you only need to
1516 * refresh a specific slide.
1517 *
1518 * @param {HTMLElement} slide
1519 */
1520 function syncSlide( slide = currentSlide ) {
1521
1522 backgrounds.sync( slide );
1523 fragments.sync( slide );
1524
1525 slideContent.load( slide );
1526
1527 backgrounds.update();
1528 notes.update();
1529
1530 }
1531
1532 /**
1533 * Resets all vertical slides so that only the first
1534 * is visible.
1535 */
1536 function resetVerticalSlides() {
1537
1538 getHorizontalSlides().forEach( horizontalSlide => {
1539
1540 Util.queryAll( horizontalSlide, 'section' ).forEach( ( verticalSlide, y ) => {
1541
1542 if( y > 0 ) {
1543 verticalSlide.classList.remove( 'present' );
1544 verticalSlide.classList.remove( 'past' );
1545 verticalSlide.classList.add( 'future' );
1546 verticalSlide.setAttribute( 'aria-hidden', 'true' );
1547 }
1548
1549 } );
1550
1551 } );
1552
1553 }
1554
1555 /**
1556 * Randomly shuffles all slides in the deck.
1557 */
1558 function shuffle( slides = getHorizontalSlides() ) {
1559
1560 slides.forEach( ( slide, i ) => {
1561
1562 // Insert the slide next to a randomly picked sibling slide
1563 // slide. This may cause the slide to insert before itself,
1564 // but that's not an issue.
1565 let beforeSlide = slides[ Math.floor( Math.random() * slides.length ) ];
1566 if( beforeSlide.parentNode === slide.parentNode ) {
1567 slide.parentNode.insertBefore( slide, beforeSlide );
1568 }
1569
1570 // Randomize the order of vertical slides (if there are any)
1571 let verticalSlides = slide.querySelectorAll( 'section' );
1572 if( verticalSlides.length ) {
1573 shuffle( verticalSlides );
1574 }
1575
1576 } );
1577
1578 }
1579
1580 /**
1581 * Updates one dimension of slides by showing the slide
1582 * with the specified index.
1583 *
1584 * @param {string} selector A CSS selector that will fetch
1585 * the group of slides we are working with
1586 * @param {number} index The index of the slide that should be
1587 * shown
1588 *
1589 * @return {number} The index of the slide that is now shown,
1590 * might differ from the passed in index if it was out of
1591 * bounds.
1592 */
1593 function updateSlides( selector, index ) {
1594
1595 // Select all slides and convert the NodeList result to
1596 // an array
1597 let slides = Util.queryAll( dom.wrapper, selector ),
1598 slidesLength = slides.length;
1599
1600 let printMode = print.isPrintingPDF();
Marc Kupietz09b75752023-10-07 09:32:19 +02001601 let loopedForwards = false;
1602 let loopedBackwards = false;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001603
1604 if( slidesLength ) {
1605
1606 // Should the index loop?
1607 if( config.loop ) {
Marc Kupietz09b75752023-10-07 09:32:19 +02001608 if( index >= slidesLength ) loopedForwards = true;
1609
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001610 index %= slidesLength;
1611
1612 if( index < 0 ) {
1613 index = slidesLength + index;
Marc Kupietz09b75752023-10-07 09:32:19 +02001614 loopedBackwards = true;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001615 }
1616 }
1617
1618 // Enforce max and minimum index bounds
1619 index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
1620
1621 for( let i = 0; i < slidesLength; i++ ) {
1622 let element = slides[i];
1623
1624 let reverse = config.rtl && !isVerticalSlide( element );
1625
1626 // Avoid .remove() with multiple args for IE11 support
1627 element.classList.remove( 'past' );
1628 element.classList.remove( 'present' );
1629 element.classList.remove( 'future' );
1630
1631 // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
1632 element.setAttribute( 'hidden', '' );
1633 element.setAttribute( 'aria-hidden', 'true' );
1634
1635 // If this element contains vertical slides
1636 if( element.querySelector( 'section' ) ) {
1637 element.classList.add( 'stack' );
1638 }
1639
1640 // If we're printing static slides, all slides are "present"
1641 if( printMode ) {
1642 element.classList.add( 'present' );
1643 continue;
1644 }
1645
1646 if( i < index ) {
1647 // Any element previous to index is given the 'past' class
1648 element.classList.add( reverse ? 'future' : 'past' );
1649
1650 if( config.fragments ) {
1651 // Show all fragments in prior slides
Marc Kupietz09b75752023-10-07 09:32:19 +02001652 showFragmentsIn( element );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001653 }
1654 }
1655 else if( i > index ) {
1656 // Any element subsequent to index is given the 'future' class
1657 element.classList.add( reverse ? 'past' : 'future' );
1658
1659 if( config.fragments ) {
1660 // Hide all fragments in future slides
Marc Kupietz09b75752023-10-07 09:32:19 +02001661 hideFragmentsIn( element );
1662 }
1663 }
1664 // Update the visibility of fragments when a presentation loops
1665 // in either direction
1666 else if( i === index && config.fragments ) {
1667 if( loopedForwards ) {
1668 hideFragmentsIn( element );
1669 }
1670 else if( loopedBackwards ) {
1671 showFragmentsIn( element );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001672 }
1673 }
1674 }
1675
1676 let slide = slides[index];
1677 let wasPresent = slide.classList.contains( 'present' );
1678
1679 // Mark the current slide as present
1680 slide.classList.add( 'present' );
1681 slide.removeAttribute( 'hidden' );
1682 slide.removeAttribute( 'aria-hidden' );
1683
1684 if( !wasPresent ) {
1685 // Dispatch an event indicating the slide is now visible
1686 dispatchEvent({
1687 target: slide,
1688 type: 'visible',
1689 bubbles: false
1690 });
1691 }
1692
1693 // If this slide has a state associated with it, add it
1694 // onto the current state of the deck
1695 let slideState = slide.getAttribute( 'data-state' );
1696 if( slideState ) {
1697 state = state.concat( slideState.split( ' ' ) );
1698 }
1699
1700 }
1701 else {
1702 // Since there are no slides we can't be anywhere beyond the
1703 // zeroth index
1704 index = 0;
1705 }
1706
1707 return index;
1708
1709 }
1710
1711 /**
Marc Kupietz09b75752023-10-07 09:32:19 +02001712 * Shows all fragment elements within the given contaienr.
1713 */
1714 function showFragmentsIn( container ) {
1715
1716 Util.queryAll( container, '.fragment' ).forEach( fragment => {
1717 fragment.classList.add( 'visible' );
1718 fragment.classList.remove( 'current-fragment' );
1719 } );
1720
1721 }
1722
1723 /**
1724 * Hides all fragment elements within the given contaienr.
1725 */
1726 function hideFragmentsIn( container ) {
1727
1728 Util.queryAll( container, '.fragment.visible' ).forEach( fragment => {
1729 fragment.classList.remove( 'visible', 'current-fragment' );
1730 } );
1731
1732 }
1733
1734 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001735 * Optimization method; hide all slides that are far away
1736 * from the present slide.
1737 */
1738 function updateSlidesVisibility() {
1739
1740 // Select all slides and convert the NodeList result to
1741 // an array
1742 let horizontalSlides = getHorizontalSlides(),
1743 horizontalSlidesLength = horizontalSlides.length,
1744 distanceX,
1745 distanceY;
1746
1747 if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
1748
1749 // The number of steps away from the present slide that will
1750 // be visible
1751 let viewDistance = overview.isActive() ? 10 : config.viewDistance;
1752
1753 // Shorten the view distance on devices that typically have
1754 // less resources
1755 if( Device.isMobile ) {
1756 viewDistance = overview.isActive() ? 6 : config.mobileViewDistance;
1757 }
1758
1759 // All slides need to be visible when exporting to PDF
1760 if( print.isPrintingPDF() ) {
1761 viewDistance = Number.MAX_VALUE;
1762 }
1763
1764 for( let x = 0; x < horizontalSlidesLength; x++ ) {
1765 let horizontalSlide = horizontalSlides[x];
1766
1767 let verticalSlides = Util.queryAll( horizontalSlide, 'section' ),
1768 verticalSlidesLength = verticalSlides.length;
1769
1770 // Determine how far away this slide is from the present
1771 distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
1772
1773 // If the presentation is looped, distance should measure
1774 // 1 between the first and last slides
1775 if( config.loop ) {
1776 distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
1777 }
1778
1779 // Show the horizontal slide if it's within the view distance
1780 if( distanceX < viewDistance ) {
1781 slideContent.load( horizontalSlide );
1782 }
1783 else {
1784 slideContent.unload( horizontalSlide );
1785 }
1786
1787 if( verticalSlidesLength ) {
1788
1789 let oy = getPreviousVerticalIndex( horizontalSlide );
1790
1791 for( let y = 0; y < verticalSlidesLength; y++ ) {
1792 let verticalSlide = verticalSlides[y];
1793
1794 distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
1795
1796 if( distanceX + distanceY < viewDistance ) {
1797 slideContent.load( verticalSlide );
1798 }
1799 else {
1800 slideContent.unload( verticalSlide );
1801 }
1802 }
1803
1804 }
1805 }
1806
1807 // Flag if there are ANY vertical slides, anywhere in the deck
1808 if( hasVerticalSlides() ) {
1809 dom.wrapper.classList.add( 'has-vertical-slides' );
1810 }
1811 else {
1812 dom.wrapper.classList.remove( 'has-vertical-slides' );
1813 }
1814
1815 // Flag if there are ANY horizontal slides, anywhere in the deck
1816 if( hasHorizontalSlides() ) {
1817 dom.wrapper.classList.add( 'has-horizontal-slides' );
1818 }
1819 else {
1820 dom.wrapper.classList.remove( 'has-horizontal-slides' );
1821 }
1822
1823 }
1824
1825 }
1826
1827 /**
1828 * Determine what available routes there are for navigation.
1829 *
1830 * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
1831 */
1832 function availableRoutes({ includeFragments = false } = {}) {
1833
1834 let horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
1835 verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
1836
1837 let routes = {
1838 left: indexh > 0,
1839 right: indexh < horizontalSlides.length - 1,
1840 up: indexv > 0,
1841 down: indexv < verticalSlides.length - 1
1842 };
1843
1844 // Looped presentations can always be navigated as long as
1845 // there are slides available
1846 if( config.loop ) {
1847 if( horizontalSlides.length > 1 ) {
1848 routes.left = true;
1849 routes.right = true;
1850 }
1851
1852 if( verticalSlides.length > 1 ) {
1853 routes.up = true;
1854 routes.down = true;
1855 }
1856 }
1857
1858 if ( horizontalSlides.length > 1 && config.navigationMode === 'linear' ) {
1859 routes.right = routes.right || routes.down;
1860 routes.left = routes.left || routes.up;
1861 }
1862
1863 // If includeFragments is set, a route will be considered
Marc Kupietz09b75752023-10-07 09:32:19 +02001864 // available if either a slid OR fragment is available in
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001865 // the given direction
1866 if( includeFragments === true ) {
1867 let fragmentRoutes = fragments.availableRoutes();
1868 routes.left = routes.left || fragmentRoutes.prev;
1869 routes.up = routes.up || fragmentRoutes.prev;
1870 routes.down = routes.down || fragmentRoutes.next;
1871 routes.right = routes.right || fragmentRoutes.next;
1872 }
1873
1874 // Reverse horizontal controls for rtl
1875 if( config.rtl ) {
1876 let left = routes.left;
1877 routes.left = routes.right;
1878 routes.right = left;
1879 }
1880
1881 return routes;
1882
1883 }
1884
1885 /**
1886 * Returns the number of past slides. This can be used as a global
1887 * flattened index for slides.
1888 *
1889 * @param {HTMLElement} [slide=currentSlide] The slide we're counting before
1890 *
1891 * @return {number} Past slide count
1892 */
1893 function getSlidePastCount( slide = currentSlide ) {
1894
1895 let horizontalSlides = getHorizontalSlides();
1896
1897 // The number of past slides
1898 let pastCount = 0;
1899
1900 // Step through all slides and count the past ones
1901 mainLoop: for( let i = 0; i < horizontalSlides.length; i++ ) {
1902
1903 let horizontalSlide = horizontalSlides[i];
1904 let verticalSlides = horizontalSlide.querySelectorAll( 'section' );
1905
1906 for( let j = 0; j < verticalSlides.length; j++ ) {
1907
1908 // Stop as soon as we arrive at the present
1909 if( verticalSlides[j] === slide ) {
1910 break mainLoop;
1911 }
1912
1913 // Don't count slides with the "uncounted" class
1914 if( verticalSlides[j].dataset.visibility !== 'uncounted' ) {
1915 pastCount++;
1916 }
1917
1918 }
1919
1920 // Stop as soon as we arrive at the present
1921 if( horizontalSlide === slide ) {
1922 break;
1923 }
1924
1925 // Don't count the wrapping section for vertical slides and
1926 // slides marked as uncounted
1927 if( horizontalSlide.classList.contains( 'stack' ) === false && horizontalSlide.dataset.visibility !== 'uncounted' ) {
1928 pastCount++;
1929 }
1930
1931 }
1932
1933 return pastCount;
1934
1935 }
1936
1937 /**
1938 * Returns a value ranging from 0-1 that represents
1939 * how far into the presentation we have navigated.
1940 *
1941 * @return {number}
1942 */
1943 function getProgress() {
1944
1945 // The number of past and total slides
1946 let totalCount = getTotalSlides();
1947 let pastCount = getSlidePastCount();
1948
1949 if( currentSlide ) {
1950
1951 let allFragments = currentSlide.querySelectorAll( '.fragment' );
1952
1953 // If there are fragments in the current slide those should be
1954 // accounted for in the progress.
1955 if( allFragments.length > 0 ) {
1956 let visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
1957
1958 // This value represents how big a portion of the slide progress
1959 // that is made up by its fragments (0-1)
1960 let fragmentWeight = 0.9;
1961
1962 // Add fragment progress to the past slide count
1963 pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
1964 }
1965
1966 }
1967
1968 return Math.min( pastCount / ( totalCount - 1 ), 1 );
1969
1970 }
1971
1972 /**
1973 * Retrieves the h/v location and fragment of the current,
1974 * or specified, slide.
1975 *
1976 * @param {HTMLElement} [slide] If specified, the returned
1977 * index will be for this slide rather than the currently
1978 * active one
1979 *
1980 * @return {{h: number, v: number, f: number}}
1981 */
1982 function getIndices( slide ) {
1983
1984 // By default, return the current indices
1985 let h = indexh,
1986 v = indexv,
1987 f;
1988
1989 // If a slide is specified, return the indices of that slide
1990 if( slide ) {
1991 let isVertical = isVerticalSlide( slide );
1992 let slideh = isVertical ? slide.parentNode : slide;
1993
1994 // Select all horizontal slides
1995 let horizontalSlides = getHorizontalSlides();
1996
1997 // Now that we know which the horizontal slide is, get its index
1998 h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
1999
2000 // Assume we're not vertical
2001 v = undefined;
2002
2003 // If this is a vertical slide, grab the vertical index
2004 if( isVertical ) {
2005 v = Math.max( Util.queryAll( slide.parentNode, 'section' ).indexOf( slide ), 0 );
2006 }
2007 }
2008
2009 if( !slide && currentSlide ) {
2010 let hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
2011 if( hasFragments ) {
2012 let currentFragment = currentSlide.querySelector( '.current-fragment' );
2013 if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
2014 f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
2015 }
2016 else {
2017 f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
2018 }
2019 }
2020 }
2021
2022 return { h, v, f };
2023
2024 }
2025
2026 /**
2027 * Retrieves all slides in this presentation.
2028 */
2029 function getSlides() {
2030
2031 return Util.queryAll( dom.wrapper, SLIDES_SELECTOR + ':not(.stack):not([data-visibility="uncounted"])' );
2032
2033 }
2034
2035 /**
2036 * Returns a list of all horizontal slides in the deck. Each
2037 * vertical stack is included as one horizontal slide in the
2038 * resulting array.
2039 */
2040 function getHorizontalSlides() {
2041
2042 return Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR );
2043
2044 }
2045
2046 /**
2047 * Returns all vertical slides that exist within this deck.
2048 */
2049 function getVerticalSlides() {
2050
2051 return Util.queryAll( dom.wrapper, '.slides>section>section' );
2052
2053 }
2054
2055 /**
2056 * Returns all vertical stacks (each stack can contain multiple slides).
2057 */
2058 function getVerticalStacks() {
2059
2060 return Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + '.stack');
2061
2062 }
2063
2064 /**
2065 * Returns true if there are at least two horizontal slides.
2066 */
2067 function hasHorizontalSlides() {
2068
2069 return getHorizontalSlides().length > 1;
2070 }
2071
2072 /**
2073 * Returns true if there are at least two vertical slides.
2074 */
2075 function hasVerticalSlides() {
2076
2077 return getVerticalSlides().length > 1;
2078
2079 }
2080
2081 /**
2082 * Returns an array of objects where each object represents the
2083 * attributes on its respective slide.
2084 */
2085 function getSlidesAttributes() {
2086
2087 return getSlides().map( slide => {
2088
2089 let attributes = {};
2090 for( let i = 0; i < slide.attributes.length; i++ ) {
2091 let attribute = slide.attributes[ i ];
2092 attributes[ attribute.name ] = attribute.value;
2093 }
2094 return attributes;
2095
2096 } );
2097
2098 }
2099
2100 /**
2101 * Retrieves the total number of slides in this presentation.
2102 *
2103 * @return {number}
2104 */
2105 function getTotalSlides() {
2106
2107 return getSlides().length;
2108
2109 }
2110
2111 /**
2112 * Returns the slide element matching the specified index.
2113 *
2114 * @return {HTMLElement}
2115 */
2116 function getSlide( x, y ) {
2117
2118 let horizontalSlide = getHorizontalSlides()[ x ];
2119 let verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
2120
2121 if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
2122 return verticalSlides ? verticalSlides[ y ] : undefined;
2123 }
2124
2125 return horizontalSlide;
2126
2127 }
2128
2129 /**
2130 * Returns the background element for the given slide.
2131 * All slides, even the ones with no background properties
2132 * defined, have a background element so as long as the
2133 * index is valid an element will be returned.
2134 *
2135 * @param {mixed} x Horizontal background index OR a slide
2136 * HTML element
2137 * @param {number} y Vertical background index
2138 * @return {(HTMLElement[]|*)}
2139 */
2140 function getSlideBackground( x, y ) {
2141
2142 let slide = typeof x === 'number' ? getSlide( x, y ) : x;
2143 if( slide ) {
2144 return slide.slideBackgroundElement;
2145 }
2146
2147 return undefined;
2148
2149 }
2150
2151 /**
2152 * Retrieves the current state of the presentation as
2153 * an object. This state can then be restored at any
2154 * time.
2155 *
2156 * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
2157 */
2158 function getState() {
2159
2160 let indices = getIndices();
2161
2162 return {
2163 indexh: indices.h,
2164 indexv: indices.v,
2165 indexf: indices.f,
2166 paused: isPaused(),
2167 overview: overview.isActive()
2168 };
2169
2170 }
2171
2172 /**
2173 * Restores the presentation to the given state.
2174 *
2175 * @param {object} state As generated by getState()
2176 * @see {@link getState} generates the parameter `state`
2177 */
2178 function setState( state ) {
2179
2180 if( typeof state === 'object' ) {
2181 slide( Util.deserialize( state.indexh ), Util.deserialize( state.indexv ), Util.deserialize( state.indexf ) );
2182
2183 let pausedFlag = Util.deserialize( state.paused ),
2184 overviewFlag = Util.deserialize( state.overview );
2185
2186 if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
2187 togglePause( pausedFlag );
2188 }
2189
2190 if( typeof overviewFlag === 'boolean' && overviewFlag !== overview.isActive() ) {
2191 overview.toggle( overviewFlag );
2192 }
2193 }
2194
2195 }
2196
2197 /**
2198 * Cues a new automated slide if enabled in the config.
2199 */
2200 function cueAutoSlide() {
2201
2202 cancelAutoSlide();
2203
2204 if( currentSlide && config.autoSlide !== false ) {
2205
Marc Kupietz09b75752023-10-07 09:32:19 +02002206 let fragment = currentSlide.querySelector( '.current-fragment[data-autoslide]' );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002207
2208 let fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
2209 let parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
2210 let slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
2211
2212 // Pick value in the following priority order:
2213 // 1. Current fragment's data-autoslide
2214 // 2. Current slide's data-autoslide
2215 // 3. Parent slide's data-autoslide
2216 // 4. Global autoSlide setting
2217 if( fragmentAutoSlide ) {
2218 autoSlide = parseInt( fragmentAutoSlide, 10 );
2219 }
2220 else if( slideAutoSlide ) {
2221 autoSlide = parseInt( slideAutoSlide, 10 );
2222 }
2223 else if( parentAutoSlide ) {
2224 autoSlide = parseInt( parentAutoSlide, 10 );
2225 }
2226 else {
2227 autoSlide = config.autoSlide;
2228
2229 // If there are media elements with data-autoplay,
2230 // automatically set the autoSlide duration to the
2231 // length of that media. Not applicable if the slide
2232 // is divided up into fragments.
2233 // playbackRate is accounted for in the duration.
2234 if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
2235 Util.queryAll( currentSlide, 'video, audio' ).forEach( el => {
2236 if( el.hasAttribute( 'data-autoplay' ) ) {
2237 if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
2238 autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
2239 }
2240 }
2241 } );
2242 }
2243 }
2244
2245 // Cue the next auto-slide if:
2246 // - There is an autoSlide value
2247 // - Auto-sliding isn't paused by the user
2248 // - The presentation isn't paused
2249 // - The overview isn't active
2250 // - The presentation isn't over
2251 if( autoSlide && !autoSlidePaused && !isPaused() && !overview.isActive() && ( !isLastSlide() || fragments.availableRoutes().next || config.loop === true ) ) {
2252 autoSlideTimeout = setTimeout( () => {
2253 if( typeof config.autoSlideMethod === 'function' ) {
2254 config.autoSlideMethod()
2255 }
2256 else {
2257 navigateNext();
2258 }
2259 cueAutoSlide();
2260 }, autoSlide );
2261 autoSlideStartTime = Date.now();
2262 }
2263
2264 if( autoSlidePlayer ) {
2265 autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
2266 }
2267
2268 }
2269
2270 }
2271
2272 /**
2273 * Cancels any ongoing request to auto-slide.
2274 */
2275 function cancelAutoSlide() {
2276
2277 clearTimeout( autoSlideTimeout );
2278 autoSlideTimeout = -1;
2279
2280 }
2281
2282 function pauseAutoSlide() {
2283
2284 if( autoSlide && !autoSlidePaused ) {
2285 autoSlidePaused = true;
2286 dispatchEvent({ type: 'autoslidepaused' });
2287 clearTimeout( autoSlideTimeout );
2288
2289 if( autoSlidePlayer ) {
2290 autoSlidePlayer.setPlaying( false );
2291 }
2292 }
2293
2294 }
2295
2296 function resumeAutoSlide() {
2297
2298 if( autoSlide && autoSlidePaused ) {
2299 autoSlidePaused = false;
2300 dispatchEvent({ type: 'autoslideresumed' });
2301 cueAutoSlide();
2302 }
2303
2304 }
2305
Christophe Dervieux8afae132021-12-06 15:16:42 +01002306 function navigateLeft({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002307
2308 navigationHistory.hasNavigatedHorizontally = true;
2309
2310 // Reverse for RTL
2311 if( config.rtl ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002312 if( ( overview.isActive() || skipFragments || fragments.next() === false ) && availableRoutes().left ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002313 slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
2314 }
2315 }
2316 // Normal navigation
Christophe Dervieux8afae132021-12-06 15:16:42 +01002317 else if( ( overview.isActive() || skipFragments || fragments.prev() === false ) && availableRoutes().left ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002318 slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
2319 }
2320
2321 }
2322
Christophe Dervieux8afae132021-12-06 15:16:42 +01002323 function navigateRight({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002324
2325 navigationHistory.hasNavigatedHorizontally = true;
2326
2327 // Reverse for RTL
2328 if( config.rtl ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002329 if( ( overview.isActive() || skipFragments || fragments.prev() === false ) && availableRoutes().right ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002330 slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
2331 }
2332 }
2333 // Normal navigation
Christophe Dervieux8afae132021-12-06 15:16:42 +01002334 else if( ( overview.isActive() || skipFragments || fragments.next() === false ) && availableRoutes().right ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002335 slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
2336 }
2337
2338 }
2339
Christophe Dervieux8afae132021-12-06 15:16:42 +01002340 function navigateUp({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002341
2342 // Prioritize hiding fragments
Christophe Dervieux8afae132021-12-06 15:16:42 +01002343 if( ( overview.isActive() || skipFragments || fragments.prev() === false ) && availableRoutes().up ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002344 slide( indexh, indexv - 1 );
2345 }
2346
2347 }
2348
Christophe Dervieux8afae132021-12-06 15:16:42 +01002349 function navigateDown({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002350
2351 navigationHistory.hasNavigatedVertically = true;
2352
2353 // Prioritize revealing fragments
Christophe Dervieux8afae132021-12-06 15:16:42 +01002354 if( ( overview.isActive() || skipFragments || fragments.next() === false ) && availableRoutes().down ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002355 slide( indexh, indexv + 1 );
2356 }
2357
2358 }
2359
2360 /**
2361 * Navigates backwards, prioritized in the following order:
2362 * 1) Previous fragment
2363 * 2) Previous vertical slide
2364 * 3) Previous horizontal slide
2365 */
Christophe Dervieux8afae132021-12-06 15:16:42 +01002366 function navigatePrev({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002367
2368 // Prioritize revealing fragments
Christophe Dervieux8afae132021-12-06 15:16:42 +01002369 if( skipFragments || fragments.prev() === false ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002370 if( availableRoutes().up ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002371 navigateUp({skipFragments});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002372 }
2373 else {
2374 // Fetch the previous horizontal slide, if there is one
2375 let previousSlide;
2376
2377 if( config.rtl ) {
2378 previousSlide = Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + '.future' ).pop();
2379 }
2380 else {
2381 previousSlide = Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + '.past' ).pop();
2382 }
2383
Christophe Dervieux8afae132021-12-06 15:16:42 +01002384 // When going backwards and arriving on a stack we start
2385 // at the bottom of the stack
2386 if( previousSlide && previousSlide.classList.contains( 'stack' ) ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002387 let v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
2388 let h = indexh - 1;
2389 slide( h, v );
2390 }
Christophe Dervieux8afae132021-12-06 15:16:42 +01002391 else {
2392 navigateLeft({skipFragments});
2393 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002394 }
2395 }
2396
2397 }
2398
2399 /**
2400 * The reverse of #navigatePrev().
2401 */
Christophe Dervieux8afae132021-12-06 15:16:42 +01002402 function navigateNext({skipFragments=false}={}) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002403
2404 navigationHistory.hasNavigatedHorizontally = true;
2405 navigationHistory.hasNavigatedVertically = true;
2406
2407 // Prioritize revealing fragments
Christophe Dervieux8afae132021-12-06 15:16:42 +01002408 if( skipFragments || fragments.next() === false ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002409
2410 let routes = availableRoutes();
2411
2412 // When looping is enabled `routes.down` is always available
2413 // so we need a separate check for when we've reached the
2414 // end of a stack and should move horizontally
2415 if( routes.down && routes.right && config.loop && isLastVerticalSlide() ) {
2416 routes.down = false;
2417 }
2418
2419 if( routes.down ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002420 navigateDown({skipFragments});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002421 }
2422 else if( config.rtl ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002423 navigateLeft({skipFragments});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002424 }
2425 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +01002426 navigateRight({skipFragments});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002427 }
2428 }
2429
2430 }
2431
2432
2433 // --------------------------------------------------------------------//
2434 // ----------------------------- EVENTS -------------------------------//
2435 // --------------------------------------------------------------------//
2436
2437 /**
2438 * Called by all event handlers that are based on user
2439 * input.
2440 *
2441 * @param {object} [event]
2442 */
2443 function onUserInput( event ) {
2444
2445 if( config.autoSlideStoppable ) {
2446 pauseAutoSlide();
2447 }
2448
2449 }
2450
2451 /**
Marc Kupietz09b75752023-10-07 09:32:19 +02002452 * Listener for post message events posted to this window.
2453 */
2454 function onPostMessage( event ) {
2455
2456 let data = event.data;
2457
2458 // Make sure we're dealing with JSON
2459 if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
2460 data = JSON.parse( data );
2461
2462 // Check if the requested method can be found
2463 if( data.method && typeof Reveal[data.method] === 'function' ) {
2464
2465 if( POST_MESSAGE_METHOD_BLACKLIST.test( data.method ) === false ) {
2466
2467 const result = Reveal[data.method].apply( Reveal, data.args );
2468
2469 // Dispatch a postMessage event with the returned value from
2470 // our method invocation for getter functions
2471 dispatchPostMessage( 'callback', { method: data.method, result: result } );
2472
2473 }
2474 else {
2475 console.warn( 'reveal.js: "'+ data.method +'" is is blacklisted from the postMessage API' );
2476 }
2477
2478 }
2479 }
2480
2481 }
2482
2483 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002484 * Event listener for transition end on the current slide.
2485 *
2486 * @param {object} [event]
2487 */
2488 function onTransitionEnd( event ) {
2489
2490 if( transition === 'running' && /section/gi.test( event.target.nodeName ) ) {
2491 transition = 'idle';
2492 dispatchEvent({
2493 type: 'slidetransitionend',
2494 data: { indexh, indexv, previousSlide, currentSlide }
2495 });
2496 }
2497
2498 }
2499
2500 /**
Christophe Dervieux8afae132021-12-06 15:16:42 +01002501 * A global listener for all click events inside of the
2502 * .slides container.
2503 *
2504 * @param {object} [event]
2505 */
2506 function onSlidesClicked( event ) {
2507
2508 const anchor = Util.closest( event.target, 'a[href^="#"]' );
2509
2510 // If a hash link is clicked, we find the target slide
2511 // and navigate to it. We previously relied on 'hashchange'
2512 // for links like these but that prevented media with
2513 // audio tracks from playing in mobile browsers since it
2514 // wasn't considered a direct interaction with the document.
2515 if( anchor ) {
2516 const hash = anchor.getAttribute( 'href' );
2517 const indices = location.getIndicesFromHash( hash );
2518
2519 if( indices ) {
2520 Reveal.slide( indices.h, indices.v, indices.f );
2521 event.preventDefault();
2522 }
2523 }
2524
2525 }
2526
2527 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002528 * Handler for the window level 'resize' event.
2529 *
2530 * @param {object} [event]
2531 */
2532 function onWindowResize( event ) {
2533
2534 layout();
2535
2536 }
2537
2538 /**
2539 * Handle for the window level 'visibilitychange' event.
2540 *
2541 * @param {object} [event]
2542 */
2543 function onPageVisibilityChange( event ) {
2544
2545 // If, after clicking a link or similar and we're coming back,
2546 // focus the document.body to ensure we can use keyboard shortcuts
2547 if( document.hidden === false && document.activeElement !== document.body ) {
2548 // Not all elements support .blur() - SVGs among them.
2549 if( typeof document.activeElement.blur === 'function' ) {
2550 document.activeElement.blur();
2551 }
2552 document.body.focus();
2553 }
2554
2555 }
2556
2557 /**
Marc Kupietz09b75752023-10-07 09:32:19 +02002558 * Handler for the document level 'fullscreenchange' event.
2559 *
2560 * @param {object} [event]
2561 */
2562 function onFullscreenChange( event ) {
2563
2564 let element = document.fullscreenElement || document.webkitFullscreenElement;
2565 if( element === dom.wrapper ) {
2566 event.stopImmediatePropagation();
2567
2568 // Timeout to avoid layout shift in Safari
2569 setTimeout( () => {
2570 Reveal.layout();
2571 Reveal.focus.focus(); // focus.focus :'(
2572 }, 1 );
2573 }
2574
2575 }
2576
2577 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002578 * Handles clicks on links that are set to preview in the
2579 * iframe overlay.
2580 *
2581 * @param {object} event
2582 */
2583 function onPreviewLinkClicked( event ) {
2584
2585 if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
2586 let url = event.currentTarget.getAttribute( 'href' );
2587 if( url ) {
2588 showPreview( url );
2589 event.preventDefault();
2590 }
2591 }
2592
2593 }
2594
2595 /**
2596 * Handles click on the auto-sliding controls element.
2597 *
2598 * @param {object} [event]
2599 */
2600 function onAutoSlidePlayerClick( event ) {
2601
2602 // Replay
2603 if( isLastSlide() && config.loop === false ) {
2604 slide( 0, 0 );
2605 resumeAutoSlide();
2606 }
2607 // Resume
2608 else if( autoSlidePaused ) {
2609 resumeAutoSlide();
2610 }
2611 // Pause
2612 else {
2613 pauseAutoSlide();
2614 }
2615
2616 }
2617
2618
2619 // --------------------------------------------------------------------//
2620 // ------------------------------- API --------------------------------//
2621 // --------------------------------------------------------------------//
2622
2623 // The public reveal.js API
2624 const API = {
2625 VERSION,
2626
2627 initialize,
2628 configure,
Marc Kupietz09b75752023-10-07 09:32:19 +02002629 destroy,
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002630
2631 sync,
2632 syncSlide,
2633 syncFragments: fragments.sync.bind( fragments ),
2634
2635 // Navigation methods
2636 slide,
2637 left: navigateLeft,
2638 right: navigateRight,
2639 up: navigateUp,
2640 down: navigateDown,
2641 prev: navigatePrev,
2642 next: navigateNext,
2643
2644 // Navigation aliases
2645 navigateLeft, navigateRight, navigateUp, navigateDown, navigatePrev, navigateNext,
2646
2647 // Fragment methods
2648 navigateFragment: fragments.goto.bind( fragments ),
2649 prevFragment: fragments.prev.bind( fragments ),
2650 nextFragment: fragments.next.bind( fragments ),
2651
2652 // Event binding
2653 on,
2654 off,
2655
2656 // Legacy event binding methods left in for backwards compatibility
2657 addEventListener: on,
2658 removeEventListener: off,
2659
2660 // Forces an update in slide layout
2661 layout,
2662
2663 // Randomizes the order of slides
2664 shuffle,
2665
2666 // Returns an object with the available routes as booleans (left/right/top/bottom)
2667 availableRoutes,
2668
2669 // Returns an object with the available fragments as booleans (prev/next)
2670 availableFragments: fragments.availableRoutes.bind( fragments ),
2671
2672 // Toggles a help overlay with keyboard shortcuts
2673 toggleHelp,
2674
2675 // Toggles the overview mode on/off
2676 toggleOverview: overview.toggle.bind( overview ),
2677
2678 // Toggles the "black screen" mode on/off
2679 togglePause,
2680
2681 // Toggles the auto slide mode on/off
2682 toggleAutoSlide,
2683
Marc Kupietz09b75752023-10-07 09:32:19 +02002684 // Toggles visibility of the jump-to-slide UI
2685 toggleJumpToSlide,
2686
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002687 // Slide navigation checks
2688 isFirstSlide,
2689 isLastSlide,
2690 isLastVerticalSlide,
2691 isVerticalSlide,
2692
2693 // State checks
2694 isPaused,
2695 isAutoSliding,
2696 isSpeakerNotes: notes.isSpeakerNotesWindow.bind( notes ),
2697 isOverview: overview.isActive.bind( overview ),
2698 isFocused: focus.isFocused.bind( focus ),
2699 isPrintingPDF: print.isPrintingPDF.bind( print ),
2700
2701 // Checks if reveal.js has been loaded and is ready for use
2702 isReady: () => ready,
2703
2704 // Slide preloading
2705 loadSlide: slideContent.load.bind( slideContent ),
2706 unloadSlide: slideContent.unload.bind( slideContent ),
2707
Marc Kupietz09b75752023-10-07 09:32:19 +02002708 // Media playback
2709 startEmbeddedContent: () => slideContent.startEmbeddedContent( currentSlide ),
2710 stopEmbeddedContent: () => slideContent.stopEmbeddedContent( currentSlide, { unloadIframes: false } ),
2711
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002712 // Preview management
2713 showPreview,
2714 hidePreview: closeOverlay,
2715
2716 // Adds or removes all internal event listeners
2717 addEventListeners,
2718 removeEventListeners,
2719 dispatchEvent,
2720
2721 // Facility for persisting and restoring the presentation state
2722 getState,
2723 setState,
2724
2725 // Presentation progress on range of 0-1
2726 getProgress,
2727
2728 // Returns the indices of the current, or specified, slide
2729 getIndices,
2730
2731 // Returns an Array of key:value maps of the attributes of each
2732 // slide in the deck
2733 getSlidesAttributes,
2734
2735 // Returns the number of slides that we have passed
2736 getSlidePastCount,
2737
2738 // Returns the total number of slides
2739 getTotalSlides,
2740
2741 // Returns the slide element at the specified index
2742 getSlide,
2743
2744 // Returns the previous slide element, may be null
2745 getPreviousSlide: () => previousSlide,
2746
2747 // Returns the current slide element
2748 getCurrentSlide: () => currentSlide,
2749
2750 // Returns the slide background element at the specified index
2751 getSlideBackground,
2752
2753 // Returns the speaker notes string for a slide, or null
2754 getSlideNotes: notes.getSlideNotes.bind( notes ),
2755
2756 // Returns an Array of all slides
2757 getSlides,
2758
2759 // Returns an array with all horizontal/vertical slides in the deck
2760 getHorizontalSlides,
2761 getVerticalSlides,
2762
2763 // Checks if the presentation contains two or more horizontal
2764 // and vertical slides
2765 hasHorizontalSlides,
2766 hasVerticalSlides,
2767
2768 // Checks if the deck has navigated on either axis at least once
2769 hasNavigatedHorizontally: () => navigationHistory.hasNavigatedHorizontally,
2770 hasNavigatedVertically: () => navigationHistory.hasNavigatedVertically,
2771
2772 // Adds/removes a custom key binding
2773 addKeyBinding: keyboard.addKeyBinding.bind( keyboard ),
2774 removeKeyBinding: keyboard.removeKeyBinding.bind( keyboard ),
2775
2776 // Programmatically triggers a keyboard event
2777 triggerKey: keyboard.triggerKey.bind( keyboard ),
2778
2779 // Registers a new shortcut to include in the help overlay
2780 registerKeyboardShortcut: keyboard.registerKeyboardShortcut.bind( keyboard ),
2781
2782 getComputedSlideSize,
2783
2784 // Returns the current scale of the presentation content
2785 getScale: () => scale,
2786
2787 // Returns the current configuration object
2788 getConfig: () => config,
2789
2790 // Helper method, retrieves query string as a key:value map
2791 getQueryHash: Util.getQueryHash,
2792
Marc Kupietz09b75752023-10-07 09:32:19 +02002793 // Returns the path to the current slide as represented in the URL
2794 getSlidePath: location.getHash.bind( location ),
2795
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02002796 // Returns reveal.js DOM elements
2797 getRevealElement: () => revealElement,
2798 getSlidesElement: () => dom.slides,
2799 getViewportElement: () => dom.viewport,
2800 getBackgroundsElement: () => backgrounds.element,
2801
2802 // API for registering and retrieving plugins
2803 registerPlugin: plugins.registerPlugin.bind( plugins ),
2804 hasPlugin: plugins.hasPlugin.bind( plugins ),
2805 getPlugin: plugins.getPlugin.bind( plugins ),
2806 getPlugins: plugins.getRegisteredPlugins.bind( plugins )
2807
2808 };
2809
2810 // Our internal API which controllers have access to
2811 Util.extend( Reveal, {
2812 ...API,
2813
2814 // Methods for announcing content to screen readers
2815 announceStatus,
2816 getStatusText,
2817
2818 // Controllers
2819 print,
2820 focus,
2821 progress,
2822 controls,
2823 location,
2824 overview,
2825 fragments,
2826 slideContent,
2827 slideNumber,
2828
2829 onUserInput,
2830 closeOverlay,
2831 updateSlidesVisibility,
2832 layoutSlideContents,
2833 transformSlides,
2834 cueAutoSlide,
2835 cancelAutoSlide
2836 } );
2837
2838 return API;
2839
2840};