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