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