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