blob: d2b29705fdd23f4195eb9fd6d1980d71d5c7493b [file] [log] [blame]
JJ Allaireefa6ad42016-01-30 13:12:05 -05001/*!
2 * reveal.js
3 * http://lab.hakim.se/reveal-js
4 * MIT licensed
5 *
6 * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
7 */
8(function( root, factory ) {
9 if( typeof define === 'function' && define.amd ) {
10 // AMD. Register as an anonymous module.
11 define( function() {
12 root.Reveal = factory();
13 return root.Reveal;
14 } );
15 } else if( typeof exports === 'object' ) {
16 // Node. Does not work with strict CommonJS.
17 module.exports = factory();
18 } else {
19 // Browser globals.
20 root.Reveal = factory();
21 }
22}( this, function() {
23
24 'use strict';
25
26 var Reveal;
27
28 var SLIDES_SELECTOR = '.slides section',
29 HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
30 VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
31 HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
32
33 // Configuration defaults, can be overridden at initialization time
34 config = {
35
36 // The "normal" size of the presentation, aspect ratio will be preserved
37 // when the presentation is scaled to fit different resolutions
38 width: 960,
39 height: 700,
40
41 // Factor of the display size that should remain empty around the content
42 margin: 0.1,
43
44 // Bounds for smallest/largest possible scale to apply to content
45 minScale: 0.2,
46 maxScale: 1.5,
47
48 // Display controls in the bottom right corner
49 controls: true,
50
51 // Display a presentation progress bar
52 progress: true,
53
54 // Display the page number of the current slide
55 slideNumber: false,
56
57 // Push each slide change to the browser history
58 history: false,
59
60 // Enable keyboard shortcuts for navigation
61 keyboard: true,
62
63 // Optional function that blocks keyboard events when retuning false
64 keyboardCondition: null,
65
66 // Enable the slide overview mode
67 overview: true,
68
69 // Vertical centering of slides
70 center: true,
71
72 // Enables touch navigation on devices with touch input
73 touch: true,
74
75 // Loop the presentation
76 loop: false,
77
78 // Change the presentation direction to be RTL
79 rtl: false,
80
81 // Turns fragments on and off globally
82 fragments: true,
83
84 // Flags if the presentation is running in an embedded mode,
85 // i.e. contained within a limited portion of the screen
86 embedded: false,
87
88 // Flags if we should show a help overlay when the questionmark
89 // key is pressed
90 help: true,
91
92 // Flags if it should be possible to pause the presentation (blackout)
93 pause: true,
94
95 // Flags if speaker notes should be visible to all viewers
96 showNotes: false,
97
98 // Number of milliseconds between automatically proceeding to the
99 // next slide, disabled when set to 0, this value can be overwritten
100 // by using a data-autoslide attribute on your slides
101 autoSlide: 0,
102
103 // Stop auto-sliding after user input
104 autoSlideStoppable: true,
105
106 // Enable slide navigation via mouse wheel
107 mouseWheel: false,
108
109 // Apply a 3D roll to links on hover
110 rollingLinks: false,
111
112 // Hides the address bar on mobile devices
113 hideAddressBar: true,
114
115 // Opens links in an iframe preview overlay
116 previewLinks: false,
117
118 // Exposes the reveal.js API through window.postMessage
119 postMessage: true,
120
121 // Dispatches all reveal.js events to the parent window through postMessage
122 postMessageEvents: false,
123
124 // Focuses body when page changes visiblity to ensure keyboard shortcuts work
125 focusBodyOnPageVisibilityChange: true,
126
127 // Transition style
128 transition: 'slide', // none/fade/slide/convex/concave/zoom
129
130 // Transition speed
131 transitionSpeed: 'default', // default/fast/slow
132
133 // Transition style for full page slide backgrounds
134 backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
135
136 // Parallax background image
137 parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
138
139 // Parallax background size
140 parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
141
142 // Amount of pixels to move the parallax background per slide step
143 parallaxBackgroundHorizontal: null,
144 parallaxBackgroundVertical: null,
145
146 // Number of slides away from the current that are visible
147 viewDistance: 3,
148
149 // Script dependencies to load
150 dependencies: []
151
152 },
153
154 // Flags if reveal.js is loaded (has dispatched the 'ready' event)
155 loaded = false,
156
157 // Flags if the overview mode is currently active
158 overview = false,
159
160 // The horizontal and vertical index of the currently active slide
161 indexh,
162 indexv,
163
164 // The previous and current slide HTML elements
165 previousSlide,
166 currentSlide,
167
168 previousBackground,
169
170 // Slides may hold a data-state attribute which we pick up and apply
171 // as a class to the body. This list contains the combined state of
172 // all current slides.
173 state = [],
174
175 // The current scale of the presentation (see width/height config)
176 scale = 1,
177
178 // CSS transform that is currently applied to the slides container,
179 // split into two groups
180 slidesTransform = { layout: '', overview: '' },
181
182 // Cached references to DOM elements
183 dom = {},
184
185 // Features supported by the browser, see #checkCapabilities()
186 features = {},
187
188 // Client is a mobile device, see #checkCapabilities()
189 isMobileDevice,
190
191 // Throttles mouse wheel navigation
192 lastMouseWheelStep = 0,
193
194 // Delays updates to the URL due to a Chrome thumbnailer bug
195 writeURLTimeout = 0,
196
197 // Flags if the interaction event listeners are bound
198 eventsAreBound = false,
199
200 // The current auto-slide duration
201 autoSlide = 0,
202
203 // Auto slide properties
204 autoSlidePlayer,
205 autoSlideTimeout = 0,
206 autoSlideStartTime = -1,
207 autoSlidePaused = false,
208
209 // Holds information about the currently ongoing touch input
210 touch = {
211 startX: 0,
212 startY: 0,
213 startSpan: 0,
214 startCount: 0,
215 captured: false,
216 threshold: 40
217 },
218
219 // Holds information about the keyboard shortcuts
220 keyboardShortcuts = {
221 'N , SPACE': 'Next slide',
222 'P': 'Previous slide',
223 '← , H': 'Navigate left',
224 '→ , L': 'Navigate right',
225 '↑ , K': 'Navigate up',
226 '↓ , J': 'Navigate down',
227 'Home': 'First slide',
228 'End': 'Last slide',
229 'B , .': 'Pause',
230 'F': 'Fullscreen',
231 'ESC, O': 'Slide overview'
232 };
233
234 /**
235 * Starts up the presentation if the client is capable.
236 */
237 function initialize( options ) {
238
239 checkCapabilities();
240
241 if( !features.transforms2d && !features.transforms3d ) {
242 document.body.setAttribute( 'class', 'no-transforms' );
243
244 // Since JS won't be running any further, we load all lazy
245 // loading elements upfront
246 var images = toArray( document.getElementsByTagName( 'img' ) ),
247 iframes = toArray( document.getElementsByTagName( 'iframe' ) );
248
249 var lazyLoadable = images.concat( iframes );
250
251 for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
252 var element = lazyLoadable[i];
253 if( element.getAttribute( 'data-src' ) ) {
254 element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
255 element.removeAttribute( 'data-src' );
256 }
257 }
258
259 // If the browser doesn't support core features we won't be
260 // using JavaScript to control the presentation
261 return;
262 }
263
264 // Cache references to key DOM elements
265 dom.wrapper = document.querySelector( '.reveal' );
266 dom.slides = document.querySelector( '.reveal .slides' );
267
268 // Force a layout when the whole page, incl fonts, has loaded
269 window.addEventListener( 'load', layout, false );
270
271 var query = Reveal.getQueryHash();
272
273 // Do not accept new dependencies via query config to avoid
274 // the potential of malicious script injection
275 if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
276
277 // Copy options over to our config object
278 extend( config, options );
279 extend( config, query );
280
281 // Hide the address bar in mobile browsers
282 hideAddressBar();
283
284 // Loads the dependencies and continues to #start() once done
285 load();
286
287 }
288
289 /**
290 * Inspect the client to see what it's capable of, this
291 * should only happens once per runtime.
292 */
293 function checkCapabilities() {
294
295 features.transforms3d = 'WebkitPerspective' in document.body.style ||
296 'MozPerspective' in document.body.style ||
297 'msPerspective' in document.body.style ||
298 'OPerspective' in document.body.style ||
299 'perspective' in document.body.style;
300
301 features.transforms2d = 'WebkitTransform' in document.body.style ||
302 'MozTransform' in document.body.style ||
303 'msTransform' in document.body.style ||
304 'OTransform' in document.body.style ||
305 'transform' in document.body.style;
306
307 features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
308 features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
309
310 features.canvas = !!document.createElement( 'canvas' ).getContext;
311
312 features.touch = !!( 'ontouchstart' in window );
313
314 // Transitions in the overview are disabled in desktop and
315 // mobile Safari due to lag
316 features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( navigator.userAgent );
317
318 isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( navigator.userAgent );
319
320 }
321
322 /**
323 * Loads the dependencies of reveal.js. Dependencies are
324 * defined via the configuration option 'dependencies'
325 * and will be loaded prior to starting/binding reveal.js.
326 * Some dependencies may have an 'async' flag, if so they
327 * will load after reveal.js has been started up.
328 */
329 function load() {
330
331 var scripts = [],
332 scriptsAsync = [],
333 scriptsToPreload = 0;
334
335 // Called once synchronous scripts finish loading
336 function proceed() {
337 if( scriptsAsync.length ) {
338 // Load asynchronous scripts
339 head.js.apply( null, scriptsAsync );
340 }
341
342 start();
343 }
344
345 function loadScript( s ) {
346 head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() {
347 // Extension may contain callback functions
348 if( typeof s.callback === 'function' ) {
349 s.callback.apply( this );
350 }
351
352 if( --scriptsToPreload === 0 ) {
353 proceed();
354 }
355 });
356 }
357
358 for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
359 var s = config.dependencies[i];
360
361 // Load if there's no condition or the condition is truthy
362 if( !s.condition || s.condition() ) {
363 if( s.async ) {
364 scriptsAsync.push( s.src );
365 }
366 else {
367 scripts.push( s.src );
368 }
369
370 loadScript( s );
371 }
372 }
373
374 if( scripts.length ) {
375 scriptsToPreload = scripts.length;
376
377 // Load synchronous scripts
378 head.js.apply( null, scripts );
379 }
380 else {
381 proceed();
382 }
383
384 }
385
386 /**
387 * Starts up reveal.js by binding input events and navigating
388 * to the current URL deeplink if there is one.
389 */
390 function start() {
391
392 // Make sure we've got all the DOM elements we need
393 setupDOM();
394
395 // Listen to messages posted to this window
396 setupPostMessage();
397
398 // Prevent iframes from scrolling the slides out of view
399 setupIframeScrollPrevention();
400
401 // Resets all vertical slides so that only the first is visible
402 resetVerticalSlides();
403
404 // Updates the presentation to match the current configuration values
405 configure();
406
407 // Read the initial hash
408 readURL();
409
410 // Update all backgrounds
411 updateBackground( true );
412
413 // Notify listeners that the presentation is ready but use a 1ms
414 // timeout to ensure it's not fired synchronously after #initialize()
415 setTimeout( function() {
416 // Enable transitions now that we're loaded
417 dom.slides.classList.remove( 'no-transition' );
418
419 loaded = true;
420
421 dispatchEvent( 'ready', {
422 'indexh': indexh,
423 'indexv': indexv,
424 'currentSlide': currentSlide
425 } );
426 }, 1 );
427
428 // Special setup and config is required when printing to PDF
429 if( isPrintingPDF() ) {
430 removeEventListeners();
431
432 // The document needs to have loaded for the PDF layout
433 // measurements to be accurate
434 if( document.readyState === 'complete' ) {
435 setupPDF();
436 }
437 else {
438 window.addEventListener( 'load', setupPDF );
439 }
440 }
441
442 }
443
444 /**
445 * Finds and stores references to DOM elements which are
446 * required by the presentation. If a required element is
447 * not found, it is created.
448 */
449 function setupDOM() {
450
451 // Prevent transitions while we're loading
452 dom.slides.classList.add( 'no-transition' );
453
454 // Background element
455 dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
456
457 // Progress bar
458 dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
459 dom.progressbar = dom.progress.querySelector( 'span' );
460
461 // Arrow controls
462 createSingletonNode( dom.wrapper, 'aside', 'controls',
463 '<button class="navigate-left" aria-label="previous slide"></button>' +
464 '<button class="navigate-right" aria-label="next slide"></button>' +
465 '<button class="navigate-up" aria-label="above slide"></button>' +
466 '<button class="navigate-down" aria-label="below slide"></button>' );
467
468 // Slide number
469 dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
470
471 // Element containing notes that are visible to the audience
472 dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
473 dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
474
475 // Overlay graphic which is displayed during the paused mode
476 createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null );
477
478 // Cache references to elements
479 dom.controls = document.querySelector( '.reveal .controls' );
480 dom.theme = document.querySelector( '#theme' );
481
482 dom.wrapper.setAttribute( 'role', 'application' );
483
484 // There can be multiple instances of controls throughout the page
485 dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
486 dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
487 dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
488 dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
489 dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
490 dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
491
492 dom.statusDiv = createStatusDiv();
493 }
494
495 /**
496 * Creates a hidden div with role aria-live to announce the
497 * current slide content. Hide the div off-screen to make it
498 * available only to Assistive Technologies.
499 */
500 function createStatusDiv() {
501
502 var statusDiv = document.getElementById( 'aria-status-div' );
503 if( !statusDiv ) {
504 statusDiv = document.createElement( 'div' );
505 statusDiv.style.position = 'absolute';
506 statusDiv.style.height = '1px';
507 statusDiv.style.width = '1px';
508 statusDiv.style.overflow ='hidden';
509 statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
510 statusDiv.setAttribute( 'id', 'aria-status-div' );
511 statusDiv.setAttribute( 'aria-live', 'polite' );
512 statusDiv.setAttribute( 'aria-atomic','true' );
513 dom.wrapper.appendChild( statusDiv );
514 }
515 return statusDiv;
516
517 }
518
519 /**
520 * Configures the presentation for printing to a static
521 * PDF.
522 */
523 function setupPDF() {
524
525 var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
526
527 // Dimensions of the PDF pages
528 var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
529 pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
530
531 // Dimensions of slides within the pages
532 var slideWidth = slideSize.width,
533 slideHeight = slideSize.height;
534
535 // Let the browser know what page size we want to print
536 injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' );
537
538 // Limit the size of certain elements to the dimensions of the slide
539 injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
540
541 document.body.classList.add( 'print-pdf' );
542 document.body.style.width = pageWidth + 'px';
543 document.body.style.height = pageHeight + 'px';
544
545 // Add each slide's index as attributes on itself, we need these
546 // indices to generate slide numbers below
547 toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
548 hslide.setAttribute( 'data-index-h', h );
549
550 if( hslide.classList.contains( 'stack' ) ) {
551 toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
552 vslide.setAttribute( 'data-index-h', h );
553 vslide.setAttribute( 'data-index-v', v );
554 } );
555 }
556 } );
557
558 // Slide and slide background layout
559 toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
560
561 // Vertical stacks are not centred since their section
562 // children will be
563 if( slide.classList.contains( 'stack' ) === false ) {
564 // Center the slide inside of the page, giving the slide some margin
565 var left = ( pageWidth - slideWidth ) / 2,
566 top = ( pageHeight - slideHeight ) / 2;
567
568 var contentHeight = getAbsoluteHeight( slide );
569 var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
570
571 // Center slides vertically
572 if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
573 top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
574 }
575
576 // Position the slide inside of the page
577 slide.style.left = left + 'px';
578 slide.style.top = top + 'px';
579 slide.style.width = slideWidth + 'px';
580
581 // TODO Backgrounds need to be multiplied when the slide
582 // stretches over multiple pages
583 var background = slide.querySelector( '.slide-background' );
584 if( background ) {
585 background.style.width = pageWidth + 'px';
586 background.style.height = ( pageHeight * numberOfPages ) + 'px';
587 background.style.top = -top + 'px';
588 background.style.left = -left + 'px';
589 }
590
591 // Inject notes if `showNotes` is enabled
592 if( config.showNotes ) {
593 var notes = getSlideNotes( slide );
594 if( notes ) {
595 var notesSpacing = 8;
596 var notesElement = document.createElement( 'div' );
597 notesElement.classList.add( 'speaker-notes' );
598 notesElement.classList.add( 'speaker-notes-pdf' );
599 notesElement.innerHTML = notes;
600 notesElement.style.left = ( notesSpacing - left ) + 'px';
601 notesElement.style.bottom = ( notesSpacing - top ) + 'px';
602 notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
603 slide.appendChild( notesElement );
604 }
605 }
606
607 // Inject slide numbers if `slideNumbers` are enabled
608 if( config.slideNumber ) {
609 var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
610 slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
611
612 var numberElement = document.createElement( 'div' );
613 numberElement.classList.add( 'slide-number' );
614 numberElement.classList.add( 'slide-number-pdf' );
615 numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
616 background.appendChild( numberElement );
617 }
618 }
619
620 } );
621
622 // Show all fragments
623 toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) {
624 fragment.classList.add( 'visible' );
625 } );
626
627 }
628
629 /**
630 * This is an unfortunate necessity. Iframes can trigger the
631 * parent window to scroll, for example by focusing an input.
632 * This scrolling can not be prevented by hiding overflow in
633 * CSS so we have to resort to repeatedly checking if the
634 * browser has decided to offset our slides :(
635 */
636 function setupIframeScrollPrevention() {
637
638 if( dom.slides.querySelector( 'iframe' ) ) {
639 setInterval( function() {
640 if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
641 dom.wrapper.scrollTop = 0;
642 dom.wrapper.scrollLeft = 0;
643 }
644 }, 500 );
645 }
646
647 }
648
649 /**
650 * Creates an HTML element and returns a reference to it.
651 * If the element already exists the existing instance will
652 * be returned.
653 */
654 function createSingletonNode( container, tagname, classname, innerHTML ) {
655
656 // Find all nodes matching the description
657 var nodes = container.querySelectorAll( '.' + classname );
658
659 // Check all matches to find one which is a direct child of
660 // the specified container
661 for( var i = 0; i < nodes.length; i++ ) {
662 var testNode = nodes[i];
663 if( testNode.parentNode === container ) {
664 return testNode;
665 }
666 }
667
668 // If no node was found, create it now
669 var node = document.createElement( tagname );
670 node.classList.add( classname );
671 if( typeof innerHTML === 'string' ) {
672 node.innerHTML = innerHTML;
673 }
674 container.appendChild( node );
675
676 return node;
677
678 }
679
680 /**
681 * Creates the slide background elements and appends them
682 * to the background container. One element is created per
683 * slide no matter if the given slide has visible background.
684 */
685 function createBackgrounds() {
686
687 var printMode = isPrintingPDF();
688
689 // Clear prior backgrounds
690 dom.background.innerHTML = '';
691 dom.background.classList.add( 'no-transition' );
692
693 // Iterate over all horizontal slides
694 toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
695
696 var backgroundStack;
697
698 if( printMode ) {
699 backgroundStack = createBackground( slideh, slideh );
700 }
701 else {
702 backgroundStack = createBackground( slideh, dom.background );
703 }
704
705 // Iterate over all vertical slides
706 toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
707
708 if( printMode ) {
709 createBackground( slidev, slidev );
710 }
711 else {
712 createBackground( slidev, backgroundStack );
713 }
714
715 backgroundStack.classList.add( 'stack' );
716
717 } );
718
719 } );
720
721 // Add parallax background if specified
722 if( config.parallaxBackgroundImage ) {
723
724 dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
725 dom.background.style.backgroundSize = config.parallaxBackgroundSize;
726
727 // Make sure the below properties are set on the element - these properties are
728 // needed for proper transitions to be set on the element via CSS. To remove
729 // annoying background slide-in effect when the presentation starts, apply
730 // these properties after short time delay
731 setTimeout( function() {
732 dom.wrapper.classList.add( 'has-parallax-background' );
733 }, 1 );
734
735 }
736 else {
737
738 dom.background.style.backgroundImage = '';
739 dom.wrapper.classList.remove( 'has-parallax-background' );
740
741 }
742
743 }
744
745 /**
746 * Creates a background for the given slide.
747 *
748 * @param {HTMLElement} slide
749 * @param {HTMLElement} container The element that the background
750 * should be appended to
751 */
752 function createBackground( slide, container ) {
753
754 var data = {
755 background: slide.getAttribute( 'data-background' ),
756 backgroundSize: slide.getAttribute( 'data-background-size' ),
757 backgroundImage: slide.getAttribute( 'data-background-image' ),
758 backgroundVideo: slide.getAttribute( 'data-background-video' ),
759 backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
760 backgroundColor: slide.getAttribute( 'data-background-color' ),
761 backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
762 backgroundPosition: slide.getAttribute( 'data-background-position' ),
763 backgroundTransition: slide.getAttribute( 'data-background-transition' )
764 };
765
766 var element = document.createElement( 'div' );
767
768 // Carry over custom classes from the slide to the background
769 element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
770
771 if( data.background ) {
772 // Auto-wrap image urls in url(...)
773 if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) {
774 slide.setAttribute( 'data-background-image', data.background );
775 }
776 else {
777 element.style.background = data.background;
778 }
779 }
780
781 // Create a hash for this combination of background settings.
782 // This is used to determine when two slide backgrounds are
783 // the same.
784 if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
785 element.setAttribute( 'data-background-hash', data.background +
786 data.backgroundSize +
787 data.backgroundImage +
788 data.backgroundVideo +
789 data.backgroundIframe +
790 data.backgroundColor +
791 data.backgroundRepeat +
792 data.backgroundPosition +
793 data.backgroundTransition );
794 }
795
796 // Additional and optional background properties
797 if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize;
798 if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
799 if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat;
800 if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition;
801 if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
802
803 container.appendChild( element );
804
805 // If backgrounds are being recreated, clear old classes
806 slide.classList.remove( 'has-dark-background' );
807 slide.classList.remove( 'has-light-background' );
808
809 // If this slide has a background color, add a class that
810 // signals if it is light or dark. If the slide has no background
811 // color, no class will be set
812 var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor;
813 if( computedBackgroundColor ) {
814 var rgb = colorToRgb( computedBackgroundColor );
815
816 // Ignore fully transparent backgrounds. Some browsers return
817 // rgba(0,0,0,0) when reading the computed background color of
818 // an element with no background
819 if( rgb && rgb.a !== 0 ) {
820 if( colorBrightness( computedBackgroundColor ) < 128 ) {
821 slide.classList.add( 'has-dark-background' );
822 }
823 else {
824 slide.classList.add( 'has-light-background' );
825 }
826 }
827 }
828
829 return element;
830
831 }
832
833 /**
834 * Registers a listener to postMessage events, this makes it
835 * possible to call all reveal.js API methods from another
836 * window. For example:
837 *
838 * revealWindow.postMessage( JSON.stringify({
839 * method: 'slide',
840 * args: [ 2 ]
841 * }), '*' );
842 */
843 function setupPostMessage() {
844
845 if( config.postMessage ) {
846 window.addEventListener( 'message', function ( event ) {
847 var data = event.data;
848
849 // Make sure we're dealing with JSON
850 if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
851 data = JSON.parse( data );
852
853 // Check if the requested method can be found
854 if( data.method && typeof Reveal[data.method] === 'function' ) {
855 Reveal[data.method].apply( Reveal, data.args );
856 }
857 }
858 }, false );
859 }
860
861 }
862
863 /**
864 * Applies the configuration settings from the config
865 * object. May be called multiple times.
866 */
867 function configure( options ) {
868
869 var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
870
871 dom.wrapper.classList.remove( config.transition );
872
873 // New config options may be passed when this method
874 // is invoked through the API after initialization
875 if( typeof options === 'object' ) extend( config, options );
876
877 // Force linear transition based on browser capabilities
878 if( features.transforms3d === false ) config.transition = 'linear';
879
880 dom.wrapper.classList.add( config.transition );
881
882 dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
883 dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
884
885 dom.controls.style.display = config.controls ? 'block' : 'none';
886 dom.progress.style.display = config.progress ? 'block' : 'none';
887 dom.slideNumber.style.display = config.slideNumber && !isPrintingPDF() ? 'block' : 'none';
888
889 if( config.rtl ) {
890 dom.wrapper.classList.add( 'rtl' );
891 }
892 else {
893 dom.wrapper.classList.remove( 'rtl' );
894 }
895
896 if( config.center ) {
897 dom.wrapper.classList.add( 'center' );
898 }
899 else {
900 dom.wrapper.classList.remove( 'center' );
901 }
902
903 // Exit the paused mode if it was configured off
904 if( config.pause === false ) {
905 resume();
906 }
907
908 if( config.showNotes ) {
909 dom.speakerNotes.classList.add( 'visible' );
910 }
911 else {
912 dom.speakerNotes.classList.remove( 'visible' );
913 }
914
915 if( config.mouseWheel ) {
916 document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
917 document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
918 }
919 else {
920 document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
921 document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
922 }
923
924 // Rolling 3D links
925 if( config.rollingLinks ) {
926 enableRollingLinks();
927 }
928 else {
929 disableRollingLinks();
930 }
931
932 // Iframe link previews
933 if( config.previewLinks ) {
934 enablePreviewLinks();
935 }
936 else {
937 disablePreviewLinks();
938 enablePreviewLinks( '[data-preview-link]' );
939 }
940
941 // Remove existing auto-slide controls
942 if( autoSlidePlayer ) {
943 autoSlidePlayer.destroy();
944 autoSlidePlayer = null;
945 }
946
947 // Generate auto-slide controls if needed
948 if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
949 autoSlidePlayer = new Playback( dom.wrapper, function() {
950 return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
951 } );
952
953 autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
954 autoSlidePaused = false;
955 }
956
957 // When fragments are turned off they should be visible
958 if( config.fragments === false ) {
959 toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
960 element.classList.add( 'visible' );
961 element.classList.remove( 'current-fragment' );
962 } );
963 }
964
965 sync();
966
967 }
968
969 /**
970 * Binds all event listeners.
971 */
972 function addEventListeners() {
973
974 eventsAreBound = true;
975
976 window.addEventListener( 'hashchange', onWindowHashChange, false );
977 window.addEventListener( 'resize', onWindowResize, false );
978
979 if( config.touch ) {
980 dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
981 dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
982 dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
983
984 // Support pointer-style touch interaction as well
985 if( window.navigator.pointerEnabled ) {
986 // IE 11 uses un-prefixed version of pointer events
987 dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
988 dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
989 dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
990 }
991 else if( window.navigator.msPointerEnabled ) {
992 // IE 10 uses prefixed version of pointer events
993 dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
994 dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
995 dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
996 }
997 }
998
999 if( config.keyboard ) {
1000 document.addEventListener( 'keydown', onDocumentKeyDown, false );
1001 document.addEventListener( 'keypress', onDocumentKeyPress, false );
1002 }
1003
1004 if( config.progress && dom.progress ) {
1005 dom.progress.addEventListener( 'click', onProgressClicked, false );
1006 }
1007
1008 if( config.focusBodyOnPageVisibilityChange ) {
1009 var visibilityChange;
1010
1011 if( 'hidden' in document ) {
1012 visibilityChange = 'visibilitychange';
1013 }
1014 else if( 'msHidden' in document ) {
1015 visibilityChange = 'msvisibilitychange';
1016 }
1017 else if( 'webkitHidden' in document ) {
1018 visibilityChange = 'webkitvisibilitychange';
1019 }
1020
1021 if( visibilityChange ) {
1022 document.addEventListener( visibilityChange, onPageVisibilityChange, false );
1023 }
1024 }
1025
1026 // Listen to both touch and click events, in case the device
1027 // supports both
1028 var pointerEvents = [ 'touchstart', 'click' ];
1029
1030 // Only support touch for Android, fixes double navigations in
1031 // stock browser
1032 if( navigator.userAgent.match( /android/gi ) ) {
1033 pointerEvents = [ 'touchstart' ];
1034 }
1035
1036 pointerEvents.forEach( function( eventName ) {
1037 dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
1038 dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
1039 dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
1040 dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
1041 dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
1042 dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
1043 } );
1044
1045 }
1046
1047 /**
1048 * Unbinds all event listeners.
1049 */
1050 function removeEventListeners() {
1051
1052 eventsAreBound = false;
1053
1054 document.removeEventListener( 'keydown', onDocumentKeyDown, false );
1055 document.removeEventListener( 'keypress', onDocumentKeyPress, false );
1056 window.removeEventListener( 'hashchange', onWindowHashChange, false );
1057 window.removeEventListener( 'resize', onWindowResize, false );
1058
1059 dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
1060 dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
1061 dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
1062
1063 // IE11
1064 if( window.navigator.pointerEnabled ) {
1065 dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
1066 dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
1067 dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
1068 }
1069 // IE10
1070 else if( window.navigator.msPointerEnabled ) {
1071 dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
1072 dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
1073 dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
1074 }
1075
1076 if ( config.progress && dom.progress ) {
1077 dom.progress.removeEventListener( 'click', onProgressClicked, false );
1078 }
1079
1080 [ 'touchstart', 'click' ].forEach( function( eventName ) {
1081 dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
1082 dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
1083 dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
1084 dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
1085 dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
1086 dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
1087 } );
1088
1089 }
1090
1091 /**
1092 * Extend object a with the properties of object b.
1093 * If there's a conflict, object b takes precedence.
1094 */
1095 function extend( a, b ) {
1096
1097 for( var i in b ) {
1098 a[ i ] = b[ i ];
1099 }
1100
1101 }
1102
1103 /**
1104 * Converts the target object to an array.
1105 */
1106 function toArray( o ) {
1107
1108 return Array.prototype.slice.call( o );
1109
1110 }
1111
1112 /**
1113 * Utility for deserializing a value.
1114 */
1115 function deserialize( value ) {
1116
1117 if( typeof value === 'string' ) {
1118 if( value === 'null' ) return null;
1119 else if( value === 'true' ) return true;
1120 else if( value === 'false' ) return false;
1121 else if( value.match( /^\d+$/ ) ) return parseFloat( value );
1122 }
1123
1124 return value;
1125
1126 }
1127
1128 /**
1129 * Measures the distance in pixels between point a
1130 * and point b.
1131 *
1132 * @param {Object} a point with x/y properties
1133 * @param {Object} b point with x/y properties
1134 */
1135 function distanceBetween( a, b ) {
1136
1137 var dx = a.x - b.x,
1138 dy = a.y - b.y;
1139
1140 return Math.sqrt( dx*dx + dy*dy );
1141
1142 }
1143
1144 /**
1145 * Applies a CSS transform to the target element.
1146 */
1147 function transformElement( element, transform ) {
1148
1149 element.style.WebkitTransform = transform;
1150 element.style.MozTransform = transform;
1151 element.style.msTransform = transform;
1152 element.style.transform = transform;
1153
1154 }
1155
1156 /**
1157 * Applies CSS transforms to the slides container. The container
1158 * is transformed from two separate sources: layout and the overview
1159 * mode.
1160 */
1161 function transformSlides( transforms ) {
1162
1163 // Pick up new transforms from arguments
1164 if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
1165 if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
1166
1167 // Apply the transforms to the slides container
1168 if( slidesTransform.layout ) {
1169 transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
1170 }
1171 else {
1172 transformElement( dom.slides, slidesTransform.overview );
1173 }
1174
1175 }
1176
1177 /**
1178 * Injects the given CSS styles into the DOM.
1179 */
1180 function injectStyleSheet( value ) {
1181
1182 var tag = document.createElement( 'style' );
1183 tag.type = 'text/css';
1184 if( tag.styleSheet ) {
1185 tag.styleSheet.cssText = value;
1186 }
1187 else {
1188 tag.appendChild( document.createTextNode( value ) );
1189 }
1190 document.getElementsByTagName( 'head' )[0].appendChild( tag );
1191
1192 }
1193
1194 /**
1195 * Converts various color input formats to an {r:0,g:0,b:0} object.
1196 *
1197 * @param {String} color The string representation of a color,
1198 * the following formats are supported:
1199 * - #000
1200 * - #000000
1201 * - rgb(0,0,0)
1202 */
1203 function colorToRgb( color ) {
1204
1205 var hex3 = color.match( /^#([0-9a-f]{3})$/i );
1206 if( hex3 && hex3[1] ) {
1207 hex3 = hex3[1];
1208 return {
1209 r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
1210 g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
1211 b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
1212 };
1213 }
1214
1215 var hex6 = color.match( /^#([0-9a-f]{6})$/i );
1216 if( hex6 && hex6[1] ) {
1217 hex6 = hex6[1];
1218 return {
1219 r: parseInt( hex6.substr( 0, 2 ), 16 ),
1220 g: parseInt( hex6.substr( 2, 2 ), 16 ),
1221 b: parseInt( hex6.substr( 4, 2 ), 16 )
1222 };
1223 }
1224
1225 var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
1226 if( rgb ) {
1227 return {
1228 r: parseInt( rgb[1], 10 ),
1229 g: parseInt( rgb[2], 10 ),
1230 b: parseInt( rgb[3], 10 )
1231 };
1232 }
1233
1234 var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
1235 if( rgba ) {
1236 return {
1237 r: parseInt( rgba[1], 10 ),
1238 g: parseInt( rgba[2], 10 ),
1239 b: parseInt( rgba[3], 10 ),
1240 a: parseFloat( rgba[4] )
1241 };
1242 }
1243
1244 return null;
1245
1246 }
1247
1248 /**
1249 * Calculates brightness on a scale of 0-255.
1250 *
1251 * @param color See colorStringToRgb for supported formats.
1252 */
1253 function colorBrightness( color ) {
1254
1255 if( typeof color === 'string' ) color = colorToRgb( color );
1256
1257 if( color ) {
1258 return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
1259 }
1260
1261 return null;
1262
1263 }
1264
1265 /**
1266 * Retrieves the height of the given element by looking
1267 * at the position and height of its immediate children.
1268 */
1269 function getAbsoluteHeight( element ) {
1270
1271 var height = 0;
1272
1273 if( element ) {
1274 var absoluteChildren = 0;
1275
1276 toArray( element.childNodes ).forEach( function( child ) {
1277
1278 if( typeof child.offsetTop === 'number' && child.style ) {
1279 // Count # of abs children
1280 if( window.getComputedStyle( child ).position === 'absolute' ) {
1281 absoluteChildren += 1;
1282 }
1283
1284 height = Math.max( height, child.offsetTop + child.offsetHeight );
1285 }
1286
1287 } );
1288
1289 // If there are no absolute children, use offsetHeight
1290 if( absoluteChildren === 0 ) {
1291 height = element.offsetHeight;
1292 }
1293
1294 }
1295
1296 return height;
1297
1298 }
1299
1300 /**
1301 * Returns the remaining height within the parent of the
1302 * target element.
1303 *
1304 * remaining height = [ configured parent height ] - [ current parent height ]
1305 */
1306 function getRemainingHeight( element, height ) {
1307
1308 height = height || 0;
1309
1310 if( element ) {
1311 var newHeight, oldHeight = element.style.height;
1312
1313 // Change the .stretch element height to 0 in order find the height of all
1314 // the other elements
1315 element.style.height = '0px';
1316 newHeight = height - element.parentNode.offsetHeight;
1317
1318 // Restore the old height, just in case
1319 element.style.height = oldHeight + 'px';
1320
1321 return newHeight;
1322 }
1323
1324 return height;
1325
1326 }
1327
1328 /**
1329 * Checks if this instance is being used to print a PDF.
1330 */
1331 function isPrintingPDF() {
1332
1333 return ( /print-pdf/gi ).test( window.location.search );
1334
1335 }
1336
1337 /**
1338 * Hides the address bar if we're on a mobile device.
1339 */
1340 function hideAddressBar() {
1341
1342 if( config.hideAddressBar && isMobileDevice ) {
1343 // Events that should trigger the address bar to hide
1344 window.addEventListener( 'load', removeAddressBar, false );
1345 window.addEventListener( 'orientationchange', removeAddressBar, false );
1346 }
1347
1348 }
1349
1350 /**
1351 * Causes the address bar to hide on mobile devices,
1352 * more vertical space ftw.
1353 */
1354 function removeAddressBar() {
1355
1356 setTimeout( function() {
1357 window.scrollTo( 0, 1 );
1358 }, 10 );
1359
1360 }
1361
1362 /**
1363 * Dispatches an event of the specified type from the
1364 * reveal DOM element.
1365 */
1366 function dispatchEvent( type, args ) {
1367
1368 var event = document.createEvent( 'HTMLEvents', 1, 2 );
1369 event.initEvent( type, true, true );
1370 extend( event, args );
1371 dom.wrapper.dispatchEvent( event );
1372
1373 // If we're in an iframe, post each reveal.js event to the
1374 // parent window. Used by the notes plugin
1375 if( config.postMessageEvents && window.parent !== window.self ) {
1376 window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
1377 }
1378
1379 }
1380
1381 /**
1382 * Wrap all links in 3D goodness.
1383 */
1384 function enableRollingLinks() {
1385
1386 if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
1387 var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
1388
1389 for( var i = 0, len = anchors.length; i < len; i++ ) {
1390 var anchor = anchors[i];
1391
1392 if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
1393 var span = document.createElement('span');
1394 span.setAttribute('data-title', anchor.text);
1395 span.innerHTML = anchor.innerHTML;
1396
1397 anchor.classList.add( 'roll' );
1398 anchor.innerHTML = '';
1399 anchor.appendChild(span);
1400 }
1401 }
1402 }
1403
1404 }
1405
1406 /**
1407 * Unwrap all 3D links.
1408 */
1409 function disableRollingLinks() {
1410
1411 var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
1412
1413 for( var i = 0, len = anchors.length; i < len; i++ ) {
1414 var anchor = anchors[i];
1415 var span = anchor.querySelector( 'span' );
1416
1417 if( span ) {
1418 anchor.classList.remove( 'roll' );
1419 anchor.innerHTML = span.innerHTML;
1420 }
1421 }
1422
1423 }
1424
1425 /**
1426 * Bind preview frame links.
1427 */
1428 function enablePreviewLinks( selector ) {
1429
1430 var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
1431
1432 anchors.forEach( function( element ) {
1433 if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
1434 element.addEventListener( 'click', onPreviewLinkClicked, false );
1435 }
1436 } );
1437
1438 }
1439
1440 /**
1441 * Unbind preview frame links.
1442 */
1443 function disablePreviewLinks() {
1444
1445 var anchors = toArray( document.querySelectorAll( 'a' ) );
1446
1447 anchors.forEach( function( element ) {
1448 if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
1449 element.removeEventListener( 'click', onPreviewLinkClicked, false );
1450 }
1451 } );
1452
1453 }
1454
1455 /**
1456 * Opens a preview window for the target URL.
1457 */
1458 function showPreview( url ) {
1459
1460 closeOverlay();
1461
1462 dom.overlay = document.createElement( 'div' );
1463 dom.overlay.classList.add( 'overlay' );
1464 dom.overlay.classList.add( 'overlay-preview' );
1465 dom.wrapper.appendChild( dom.overlay );
1466
1467 dom.overlay.innerHTML = [
1468 '<header>',
1469 '<a class="close" href="#"><span class="icon"></span></a>',
1470 '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
1471 '</header>',
1472 '<div class="spinner"></div>',
1473 '<div class="viewport">',
1474 '<iframe src="'+ url +'"></iframe>',
1475 '</div>'
1476 ].join('');
1477
1478 dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
1479 dom.overlay.classList.add( 'loaded' );
1480 }, false );
1481
1482 dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
1483 closeOverlay();
1484 event.preventDefault();
1485 }, false );
1486
1487 dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
1488 closeOverlay();
1489 }, false );
1490
1491 setTimeout( function() {
1492 dom.overlay.classList.add( 'visible' );
1493 }, 1 );
1494
1495 }
1496
1497 /**
1498 * Opens a overlay window with help material.
1499 */
1500 function showHelp() {
1501
1502 if( config.help ) {
1503
1504 closeOverlay();
1505
1506 dom.overlay = document.createElement( 'div' );
1507 dom.overlay.classList.add( 'overlay' );
1508 dom.overlay.classList.add( 'overlay-help' );
1509 dom.wrapper.appendChild( dom.overlay );
1510
1511 var html = '<p class="title">Keyboard Shortcuts</p><br/>';
1512
1513 html += '<table><th>KEY</th><th>ACTION</th>';
1514 for( var key in keyboardShortcuts ) {
1515 html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
1516 }
1517
1518 html += '</table>';
1519
1520 dom.overlay.innerHTML = [
1521 '<header>',
1522 '<a class="close" href="#"><span class="icon"></span></a>',
1523 '</header>',
1524 '<div class="viewport">',
1525 '<div class="viewport-inner">'+ html +'</div>',
1526 '</div>'
1527 ].join('');
1528
1529 dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
1530 closeOverlay();
1531 event.preventDefault();
1532 }, false );
1533
1534 setTimeout( function() {
1535 dom.overlay.classList.add( 'visible' );
1536 }, 1 );
1537
1538 }
1539
1540 }
1541
1542 /**
1543 * Closes any currently open overlay.
1544 */
1545 function closeOverlay() {
1546
1547 if( dom.overlay ) {
1548 dom.overlay.parentNode.removeChild( dom.overlay );
1549 dom.overlay = null;
1550 }
1551
1552 }
1553
1554 /**
1555 * Applies JavaScript-controlled layout rules to the
1556 * presentation.
1557 */
1558 function layout() {
1559
1560 if( dom.wrapper && !isPrintingPDF() ) {
1561
1562 var size = getComputedSlideSize();
1563
1564 var slidePadding = 20; // TODO Dig this out of DOM
1565
1566 // Layout the contents of the slides
1567 layoutSlideContents( config.width, config.height, slidePadding );
1568
1569 dom.slides.style.width = size.width + 'px';
1570 dom.slides.style.height = size.height + 'px';
1571
1572 // Determine scale of content to fit within available space
1573 scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
1574
1575 // Respect max/min scale settings
1576 scale = Math.max( scale, config.minScale );
1577 scale = Math.min( scale, config.maxScale );
1578
1579 // Don't apply any scaling styles if scale is 1
1580 if( scale === 1 ) {
1581 dom.slides.style.zoom = '';
1582 dom.slides.style.left = '';
1583 dom.slides.style.top = '';
1584 dom.slides.style.bottom = '';
1585 dom.slides.style.right = '';
1586 transformSlides( { layout: '' } );
1587 }
1588 else {
1589 // Use zoom to scale up in desktop Chrome so that content
1590 // remains crisp. We don't use zoom to scale down since that
1591 // can lead to shifts in text layout/line breaks.
1592 if( scale > 1 && !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) {
1593 dom.slides.style.zoom = scale;
1594 dom.slides.style.left = '';
1595 dom.slides.style.top = '';
1596 dom.slides.style.bottom = '';
1597 dom.slides.style.right = '';
1598 transformSlides( { layout: '' } );
1599 }
1600 // Apply scale transform as a fallback
1601 else {
1602 dom.slides.style.zoom = '';
1603 dom.slides.style.left = '50%';
1604 dom.slides.style.top = '50%';
1605 dom.slides.style.bottom = 'auto';
1606 dom.slides.style.right = 'auto';
1607 transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
1608 }
1609 }
1610
1611 // Select all slides, vertical and horizontal
1612 var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
1613
1614 for( var i = 0, len = slides.length; i < len; i++ ) {
1615 var slide = slides[ i ];
1616
1617 // Don't bother updating invisible slides
1618 if( slide.style.display === 'none' ) {
1619 continue;
1620 }
1621
1622 if( config.center || slide.classList.contains( 'center' ) ) {
1623 // Vertical stacks are not centred since their section
1624 // children will be
1625 if( slide.classList.contains( 'stack' ) ) {
1626 slide.style.top = 0;
1627 }
1628 else {
1629 slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px';
1630 }
1631 }
1632 else {
1633 slide.style.top = '';
1634 }
1635
1636 }
1637
1638 updateProgress();
1639 updateParallax();
1640
1641 }
1642
1643 }
1644
1645 /**
1646 * Applies layout logic to the contents of all slides in
1647 * the presentation.
1648 */
1649 function layoutSlideContents( width, height, padding ) {
1650
1651 // Handle sizing of elements with the 'stretch' class
1652 toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
1653
1654 // Determine how much vertical space we can use
1655 var remainingHeight = getRemainingHeight( element, height );
1656
1657 // Consider the aspect ratio of media elements
1658 if( /(img|video)/gi.test( element.nodeName ) ) {
1659 var nw = element.naturalWidth || element.videoWidth,
1660 nh = element.naturalHeight || element.videoHeight;
1661
1662 var es = Math.min( width / nw, remainingHeight / nh );
1663
1664 element.style.width = ( nw * es ) + 'px';
1665 element.style.height = ( nh * es ) + 'px';
1666
1667 }
1668 else {
1669 element.style.width = width + 'px';
1670 element.style.height = remainingHeight + 'px';
1671 }
1672
1673 } );
1674
1675 }
1676
1677 /**
1678 * Calculates the computed pixel size of our slides. These
1679 * values are based on the width and height configuration
1680 * options.
1681 */
1682 function getComputedSlideSize( presentationWidth, presentationHeight ) {
1683
1684 var size = {
1685 // Slide size
1686 width: config.width,
1687 height: config.height,
1688
1689 // Presentation size
1690 presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
1691 presentationHeight: presentationHeight || dom.wrapper.offsetHeight
1692 };
1693
1694 // Reduce available space by margin
1695 size.presentationWidth -= ( size.presentationWidth * config.margin );
1696 size.presentationHeight -= ( size.presentationHeight * config.margin );
1697
1698 // Slide width may be a percentage of available width
1699 if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
1700 size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
1701 }
1702
1703 // Slide height may be a percentage of available height
1704 if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
1705 size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
1706 }
1707
1708 return size;
1709
1710 }
1711
1712 /**
1713 * Stores the vertical index of a stack so that the same
1714 * vertical slide can be selected when navigating to and
1715 * from the stack.
1716 *
1717 * @param {HTMLElement} stack The vertical stack element
1718 * @param {int} v Index to memorize
1719 */
1720 function setPreviousVerticalIndex( stack, v ) {
1721
1722 if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
1723 stack.setAttribute( 'data-previous-indexv', v || 0 );
1724 }
1725
1726 }
1727
1728 /**
1729 * Retrieves the vertical index which was stored using
1730 * #setPreviousVerticalIndex() or 0 if no previous index
1731 * exists.
1732 *
1733 * @param {HTMLElement} stack The vertical stack element
1734 */
1735 function getPreviousVerticalIndex( stack ) {
1736
1737 if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
1738 // Prefer manually defined start-indexv
1739 var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
1740
1741 return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
1742 }
1743
1744 return 0;
1745
1746 }
1747
1748 /**
1749 * Displays the overview of slides (quick nav) by scaling
1750 * down and arranging all slide elements.
1751 */
1752 function activateOverview() {
1753
1754 // Only proceed if enabled in config
1755 if( config.overview && !isOverview() ) {
1756
1757 overview = true;
1758
1759 dom.wrapper.classList.add( 'overview' );
1760 dom.wrapper.classList.remove( 'overview-deactivating' );
1761
1762 if( features.overviewTransitions ) {
1763 setTimeout( function() {
1764 dom.wrapper.classList.add( 'overview-animated' );
1765 }, 1 );
1766 }
1767
1768 // Don't auto-slide while in overview mode
1769 cancelAutoSlide();
1770
1771 // Move the backgrounds element into the slide container to
1772 // that the same scaling is applied
1773 dom.slides.appendChild( dom.background );
1774
1775 // Clicking on an overview slide navigates to it
1776 toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1777 if( !slide.classList.contains( 'stack' ) ) {
1778 slide.addEventListener( 'click', onOverviewSlideClicked, true );
1779 }
1780 } );
1781
1782 updateSlidesVisibility();
1783 layoutOverview();
1784 updateOverview();
1785
1786 layout();
1787
1788 // Notify observers of the overview showing
1789 dispatchEvent( 'overviewshown', {
1790 'indexh': indexh,
1791 'indexv': indexv,
1792 'currentSlide': currentSlide
1793 } );
1794
1795 }
1796
1797 }
1798
1799 /**
1800 * Uses CSS transforms to position all slides in a grid for
1801 * display inside of the overview mode.
1802 */
1803 function layoutOverview() {
1804
1805 var margin = 70;
1806 var slideWidth = config.width + margin,
1807 slideHeight = config.height + margin;
1808
1809 // Reverse in RTL mode
1810 if( config.rtl ) {
1811 slideWidth = -slideWidth;
1812 }
1813
1814 // Layout slides
1815 toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
1816 hslide.setAttribute( 'data-index-h', h );
1817 transformElement( hslide, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
1818
1819 if( hslide.classList.contains( 'stack' ) ) {
1820
1821 toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
1822 vslide.setAttribute( 'data-index-h', h );
1823 vslide.setAttribute( 'data-index-v', v );
1824
1825 transformElement( vslide, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
1826 } );
1827
1828 }
1829 } );
1830
1831 // Layout slide backgrounds
1832 toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
1833 transformElement( hbackground, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' );
1834
1835 toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
1836 transformElement( vbackground, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' );
1837 } );
1838 } );
1839
1840 }
1841
1842 /**
1843 * Moves the overview viewport to the current slides.
1844 * Called each time the current slide changes.
1845 */
1846 function updateOverview() {
1847
1848 var margin = 70;
1849 var slideWidth = config.width + margin,
1850 slideHeight = config.height + margin;
1851
1852 // Reverse in RTL mode
1853 if( config.rtl ) {
1854 slideWidth = -slideWidth;
1855 }
1856
1857 transformSlides( {
1858 overview: [
1859 'translateX('+ ( -indexh * slideWidth ) +'px)',
1860 'translateY('+ ( -indexv * slideHeight ) +'px)',
1861 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)'
1862 ].join( ' ' )
1863 } );
1864
1865 }
1866
1867 /**
1868 * Exits the slide overview and enters the currently
1869 * active slide.
1870 */
1871 function deactivateOverview() {
1872
1873 // Only proceed if enabled in config
1874 if( config.overview ) {
1875
1876 overview = false;
1877
1878 dom.wrapper.classList.remove( 'overview' );
1879 dom.wrapper.classList.remove( 'overview-animated' );
1880
1881 // Temporarily add a class so that transitions can do different things
1882 // depending on whether they are exiting/entering overview, or just
1883 // moving from slide to slide
1884 dom.wrapper.classList.add( 'overview-deactivating' );
1885
1886 setTimeout( function () {
1887 dom.wrapper.classList.remove( 'overview-deactivating' );
1888 }, 1 );
1889
1890 // Move the background element back out
1891 dom.wrapper.appendChild( dom.background );
1892
1893 // Clean up changes made to slides
1894 toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
1895 transformElement( slide, '' );
1896
1897 slide.removeEventListener( 'click', onOverviewSlideClicked, true );
1898 } );
1899
1900 // Clean up changes made to backgrounds
1901 toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
1902 transformElement( background, '' );
1903 } );
1904
1905 transformSlides( { overview: '' } );
1906
1907 slide( indexh, indexv );
1908
1909 layout();
1910
1911 cueAutoSlide();
1912
1913 // Notify observers of the overview hiding
1914 dispatchEvent( 'overviewhidden', {
1915 'indexh': indexh,
1916 'indexv': indexv,
1917 'currentSlide': currentSlide
1918 } );
1919
1920 }
1921 }
1922
1923 /**
1924 * Toggles the slide overview mode on and off.
1925 *
1926 * @param {Boolean} override Optional flag which overrides the
1927 * toggle logic and forcibly sets the desired state. True means
1928 * overview is open, false means it's closed.
1929 */
1930 function toggleOverview( override ) {
1931
1932 if( typeof override === 'boolean' ) {
1933 override ? activateOverview() : deactivateOverview();
1934 }
1935 else {
1936 isOverview() ? deactivateOverview() : activateOverview();
1937 }
1938
1939 }
1940
1941 /**
1942 * Checks if the overview is currently active.
1943 *
1944 * @return {Boolean} true if the overview is active,
1945 * false otherwise
1946 */
1947 function isOverview() {
1948
1949 return overview;
1950
1951 }
1952
1953 /**
1954 * Checks if the current or specified slide is vertical
1955 * (nested within another slide).
1956 *
1957 * @param {HTMLElement} slide [optional] The slide to check
1958 * orientation of
1959 */
1960 function isVerticalSlide( slide ) {
1961
1962 // Prefer slide argument, otherwise use current slide
1963 slide = slide ? slide : currentSlide;
1964
1965 return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
1966
1967 }
1968
1969 /**
1970 * Handling the fullscreen functionality via the fullscreen API
1971 *
1972 * @see http://fullscreen.spec.whatwg.org/
1973 * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
1974 */
1975 function enterFullscreen() {
1976
1977 var element = document.body;
1978
1979 // Check which implementation is available
1980 var requestMethod = element.requestFullScreen ||
1981 element.webkitRequestFullscreen ||
1982 element.webkitRequestFullScreen ||
1983 element.mozRequestFullScreen ||
1984 element.msRequestFullscreen;
1985
1986 if( requestMethod ) {
1987 requestMethod.apply( element );
1988 }
1989
1990 }
1991
1992 /**
1993 * Enters the paused mode which fades everything on screen to
1994 * black.
1995 */
1996 function pause() {
1997
1998 if( config.pause ) {
1999 var wasPaused = dom.wrapper.classList.contains( 'paused' );
2000
2001 cancelAutoSlide();
2002 dom.wrapper.classList.add( 'paused' );
2003
2004 if( wasPaused === false ) {
2005 dispatchEvent( 'paused' );
2006 }
2007 }
2008
2009 }
2010
2011 /**
2012 * Exits from the paused mode.
2013 */
2014 function resume() {
2015
2016 var wasPaused = dom.wrapper.classList.contains( 'paused' );
2017 dom.wrapper.classList.remove( 'paused' );
2018
2019 cueAutoSlide();
2020
2021 if( wasPaused ) {
2022 dispatchEvent( 'resumed' );
2023 }
2024
2025 }
2026
2027 /**
2028 * Toggles the paused mode on and off.
2029 */
2030 function togglePause( override ) {
2031
2032 if( typeof override === 'boolean' ) {
2033 override ? pause() : resume();
2034 }
2035 else {
2036 isPaused() ? resume() : pause();
2037 }
2038
2039 }
2040
2041 /**
2042 * Checks if we are currently in the paused mode.
2043 */
2044 function isPaused() {
2045
2046 return dom.wrapper.classList.contains( 'paused' );
2047
2048 }
2049
2050 /**
2051 * Toggles the auto slide mode on and off.
2052 *
2053 * @param {Boolean} override Optional flag which sets the desired state.
2054 * True means autoplay starts, false means it stops.
2055 */
2056
2057 function toggleAutoSlide( override ) {
2058
2059 if( typeof override === 'boolean' ) {
2060 override ? resumeAutoSlide() : pauseAutoSlide();
2061 }
2062
2063 else {
2064 autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
2065 }
2066
2067 }
2068
2069 /**
2070 * Checks if the auto slide mode is currently on.
2071 */
2072 function isAutoSliding() {
2073
2074 return !!( autoSlide && !autoSlidePaused );
2075
2076 }
2077
2078 /**
2079 * Steps from the current point in the presentation to the
2080 * slide which matches the specified horizontal and vertical
2081 * indices.
2082 *
2083 * @param {int} h Horizontal index of the target slide
2084 * @param {int} v Vertical index of the target slide
2085 * @param {int} f Optional index of a fragment within the
2086 * target slide to activate
2087 * @param {int} o Optional origin for use in multimaster environments
2088 */
2089 function slide( h, v, f, o ) {
2090
2091 // Remember where we were at before
2092 previousSlide = currentSlide;
2093
2094 // Query all horizontal slides in the deck
2095 var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
2096
2097 // If no vertical index is specified and the upcoming slide is a
2098 // stack, resume at its previous vertical index
2099 if( v === undefined && !isOverview() ) {
2100 v = getPreviousVerticalIndex( horizontalSlides[ h ] );
2101 }
2102
2103 // If we were on a vertical stack, remember what vertical index
2104 // it was on so we can resume at the same position when returning
2105 if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
2106 setPreviousVerticalIndex( previousSlide.parentNode, indexv );
2107 }
2108
2109 // Remember the state before this slide
2110 var stateBefore = state.concat();
2111
2112 // Reset the state array
2113 state.length = 0;
2114
2115 var indexhBefore = indexh || 0,
2116 indexvBefore = indexv || 0;
2117
2118 // Activate and transition to the new slide
2119 indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
2120 indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
2121
2122 // Update the visibility of slides now that the indices have changed
2123 updateSlidesVisibility();
2124
2125 layout();
2126
2127 // Apply the new state
2128 stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
2129 // Check if this state existed on the previous slide. If it
2130 // did, we will avoid adding it repeatedly
2131 for( var j = 0; j < stateBefore.length; j++ ) {
2132 if( stateBefore[j] === state[i] ) {
2133 stateBefore.splice( j, 1 );
2134 continue stateLoop;
2135 }
2136 }
2137
2138 document.documentElement.classList.add( state[i] );
2139
2140 // Dispatch custom event matching the state's name
2141 dispatchEvent( state[i] );
2142 }
2143
2144 // Clean up the remains of the previous state
2145 while( stateBefore.length ) {
2146 document.documentElement.classList.remove( stateBefore.pop() );
2147 }
2148
2149 // Update the overview if it's currently active
2150 if( isOverview() ) {
2151 updateOverview();
2152 }
2153
2154 // Find the current horizontal slide and any possible vertical slides
2155 // within it
2156 var currentHorizontalSlide = horizontalSlides[ indexh ],
2157 currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
2158
2159 // Store references to the previous and current slides
2160 currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
2161
2162 // Show fragment, if specified
2163 if( typeof f !== 'undefined' ) {
2164 navigateFragment( f );
2165 }
2166
2167 // Dispatch an event if the slide changed
2168 var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
2169 if( slideChanged ) {
2170 dispatchEvent( 'slidechanged', {
2171 'indexh': indexh,
2172 'indexv': indexv,
2173 'previousSlide': previousSlide,
2174 'currentSlide': currentSlide,
2175 'origin': o
2176 } );
2177 }
2178 else {
2179 // Ensure that the previous slide is never the same as the current
2180 previousSlide = null;
2181 }
2182
2183 // Solves an edge case where the previous slide maintains the
2184 // 'present' class when navigating between adjacent vertical
2185 // stacks
2186 if( previousSlide ) {
2187 previousSlide.classList.remove( 'present' );
2188 previousSlide.setAttribute( 'aria-hidden', 'true' );
2189
2190 // Reset all slides upon navigate to home
2191 // Issue: #285
2192 if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
2193 // Launch async task
2194 setTimeout( function () {
2195 var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
2196 for( i in slides ) {
2197 if( slides[i] ) {
2198 // Reset stack
2199 setPreviousVerticalIndex( slides[i], 0 );
2200 }
2201 }
2202 }, 0 );
2203 }
2204 }
2205
2206 // Handle embedded content
2207 if( slideChanged || !previousSlide ) {
2208 stopEmbeddedContent( previousSlide );
2209 startEmbeddedContent( currentSlide );
2210 }
2211
2212 // Announce the current slide contents, for screen readers
2213 dom.statusDiv.textContent = currentSlide.textContent;
2214
2215 updateControls();
2216 updateProgress();
2217 updateBackground();
2218 updateParallax();
2219 updateSlideNumber();
2220 updateNotes();
2221
2222 // Update the URL hash
2223 writeURL();
2224
2225 cueAutoSlide();
2226
2227 }
2228
2229 /**
2230 * Syncs the presentation with the current DOM. Useful
2231 * when new slides or control elements are added or when
2232 * the configuration has changed.
2233 */
2234 function sync() {
2235
2236 // Subscribe to input
2237 removeEventListeners();
2238 addEventListeners();
2239
2240 // Force a layout to make sure the current config is accounted for
2241 layout();
2242
2243 // Reflect the current autoSlide value
2244 autoSlide = config.autoSlide;
2245
2246 // Start auto-sliding if it's enabled
2247 cueAutoSlide();
2248
2249 // Re-create the slide backgrounds
2250 createBackgrounds();
2251
2252 // Write the current hash to the URL
2253 writeURL();
2254
2255 sortAllFragments();
2256
2257 updateControls();
2258 updateProgress();
2259 updateBackground( true );
2260 updateSlideNumber();
2261 updateSlidesVisibility();
2262 updateNotes();
2263
2264 formatEmbeddedContent();
2265 startEmbeddedContent( currentSlide );
2266
2267 if( isOverview() ) {
2268 layoutOverview();
2269 }
2270
2271 }
2272
2273 /**
2274 * Resets all vertical slides so that only the first
2275 * is visible.
2276 */
2277 function resetVerticalSlides() {
2278
2279 var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2280 horizontalSlides.forEach( function( horizontalSlide ) {
2281
2282 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
2283 verticalSlides.forEach( function( verticalSlide, y ) {
2284
2285 if( y > 0 ) {
2286 verticalSlide.classList.remove( 'present' );
2287 verticalSlide.classList.remove( 'past' );
2288 verticalSlide.classList.add( 'future' );
2289 verticalSlide.setAttribute( 'aria-hidden', 'true' );
2290 }
2291
2292 } );
2293
2294 } );
2295
2296 }
2297
2298 /**
2299 * Sorts and formats all of fragments in the
2300 * presentation.
2301 */
2302 function sortAllFragments() {
2303
2304 var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
2305 horizontalSlides.forEach( function( horizontalSlide ) {
2306
2307 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
2308 verticalSlides.forEach( function( verticalSlide, y ) {
2309
2310 sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
2311
2312 } );
2313
2314 if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
2315
2316 } );
2317
2318 }
2319
2320 /**
2321 * Updates one dimension of slides by showing the slide
2322 * with the specified index.
2323 *
2324 * @param {String} selector A CSS selector that will fetch
2325 * the group of slides we are working with
2326 * @param {Number} index The index of the slide that should be
2327 * shown
2328 *
2329 * @return {Number} The index of the slide that is now shown,
2330 * might differ from the passed in index if it was out of
2331 * bounds.
2332 */
2333 function updateSlides( selector, index ) {
2334
2335 // Select all slides and convert the NodeList result to
2336 // an array
2337 var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
2338 slidesLength = slides.length;
2339
2340 var printMode = isPrintingPDF();
2341
2342 if( slidesLength ) {
2343
2344 // Should the index loop?
2345 if( config.loop ) {
2346 index %= slidesLength;
2347
2348 if( index < 0 ) {
2349 index = slidesLength + index;
2350 }
2351 }
2352
2353 // Enforce max and minimum index bounds
2354 index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
2355
2356 for( var i = 0; i < slidesLength; i++ ) {
2357 var element = slides[i];
2358
2359 var reverse = config.rtl && !isVerticalSlide( element );
2360
2361 element.classList.remove( 'past' );
2362 element.classList.remove( 'present' );
2363 element.classList.remove( 'future' );
2364
2365 // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
2366 element.setAttribute( 'hidden', '' );
2367 element.setAttribute( 'aria-hidden', 'true' );
2368
2369 // If this element contains vertical slides
2370 if( element.querySelector( 'section' ) ) {
2371 element.classList.add( 'stack' );
2372 }
2373
2374 // If we're printing static slides, all slides are "present"
2375 if( printMode ) {
2376 element.classList.add( 'present' );
2377 continue;
2378 }
2379
2380 if( i < index ) {
2381 // Any element previous to index is given the 'past' class
2382 element.classList.add( reverse ? 'future' : 'past' );
2383
2384 if( config.fragments ) {
2385 var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
2386
2387 // Show all fragments on prior slides
2388 while( pastFragments.length ) {
2389 var pastFragment = pastFragments.pop();
2390 pastFragment.classList.add( 'visible' );
2391 pastFragment.classList.remove( 'current-fragment' );
2392 }
2393 }
2394 }
2395 else if( i > index ) {
2396 // Any element subsequent to index is given the 'future' class
2397 element.classList.add( reverse ? 'past' : 'future' );
2398
2399 if( config.fragments ) {
2400 var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
2401
2402 // No fragments in future slides should be visible ahead of time
2403 while( futureFragments.length ) {
2404 var futureFragment = futureFragments.pop();
2405 futureFragment.classList.remove( 'visible' );
2406 futureFragment.classList.remove( 'current-fragment' );
2407 }
2408 }
2409 }
2410 }
2411
2412 // Mark the current slide as present
2413 slides[index].classList.add( 'present' );
2414 slides[index].removeAttribute( 'hidden' );
2415 slides[index].removeAttribute( 'aria-hidden' );
2416
2417 // If this slide has a state associated with it, add it
2418 // onto the current state of the deck
2419 var slideState = slides[index].getAttribute( 'data-state' );
2420 if( slideState ) {
2421 state = state.concat( slideState.split( ' ' ) );
2422 }
2423
2424 }
2425 else {
2426 // Since there are no slides we can't be anywhere beyond the
2427 // zeroth index
2428 index = 0;
2429 }
2430
2431 return index;
2432
2433 }
2434
2435 /**
2436 * Optimization method; hide all slides that are far away
2437 * from the present slide.
2438 */
2439 function updateSlidesVisibility() {
2440
2441 // Select all slides and convert the NodeList result to
2442 // an array
2443 var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
2444 horizontalSlidesLength = horizontalSlides.length,
2445 distanceX,
2446 distanceY;
2447
2448 if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
2449
2450 // The number of steps away from the present slide that will
2451 // be visible
2452 var viewDistance = isOverview() ? 10 : config.viewDistance;
2453
2454 // Limit view distance on weaker devices
2455 if( isMobileDevice ) {
2456 viewDistance = isOverview() ? 6 : 2;
2457 }
2458
2459 // All slides need to be visible when exporting to PDF
2460 if( isPrintingPDF() ) {
2461 viewDistance = Number.MAX_VALUE;
2462 }
2463
2464 for( var x = 0; x < horizontalSlidesLength; x++ ) {
2465 var horizontalSlide = horizontalSlides[x];
2466
2467 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
2468 verticalSlidesLength = verticalSlides.length;
2469
2470 // Determine how far away this slide is from the present
2471 distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
2472
2473 // If the presentation is looped, distance should measure
2474 // 1 between the first and last slides
2475 if( config.loop ) {
2476 distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
2477 }
2478
2479 // Show the horizontal slide if it's within the view distance
2480 if( distanceX < viewDistance ) {
2481 showSlide( horizontalSlide );
2482 }
2483 else {
2484 hideSlide( horizontalSlide );
2485 }
2486
2487 if( verticalSlidesLength ) {
2488
2489 var oy = getPreviousVerticalIndex( horizontalSlide );
2490
2491 for( var y = 0; y < verticalSlidesLength; y++ ) {
2492 var verticalSlide = verticalSlides[y];
2493
2494 distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
2495
2496 if( distanceX + distanceY < viewDistance ) {
2497 showSlide( verticalSlide );
2498 }
2499 else {
2500 hideSlide( verticalSlide );
2501 }
2502 }
2503
2504 }
2505 }
2506
2507 }
2508
2509 }
2510
2511 /**
2512 * Pick up notes from the current slide and display tham
2513 * to the viewer.
2514 *
2515 * @see `showNotes` config value
2516 */
2517 function updateNotes() {
2518
2519 if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
2520
2521 dom.speakerNotes.innerHTML = getSlideNotes() || '';
2522
2523 }
2524
2525 }
2526
2527 /**
2528 * Updates the progress bar to reflect the current slide.
2529 */
2530 function updateProgress() {
2531
2532 // Update progress if enabled
2533 if( config.progress && dom.progressbar ) {
2534
2535 dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
2536
2537 }
2538
2539 }
2540
2541 /**
2542 * Updates the slide number div to reflect the current slide.
2543 *
2544 * The following slide number formats are available:
2545 * "h.v": horizontal . vertical slide number (default)
2546 * "h/v": horizontal / vertical slide number
2547 * "c": flattened slide number
2548 * "c/t": flattened slide number / total slides
2549 */
2550 function updateSlideNumber() {
2551
2552 // Update slide number if enabled
2553 if( config.slideNumber && dom.slideNumber ) {
2554
2555 var value = [];
2556 var format = 'h.v';
2557
2558 // Check if a custom number format is available
2559 if( typeof config.slideNumber === 'string' ) {
2560 format = config.slideNumber;
2561 }
2562
2563 switch( format ) {
2564 case 'c':
2565 value.push( getSlidePastCount() + 1 );
2566 break;
2567 case 'c/t':
2568 value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
2569 break;
2570 case 'h/v':
2571 value.push( indexh + 1 );
2572 if( isVerticalSlide() ) value.push( '/', indexv + 1 );
2573 break;
2574 default:
2575 value.push( indexh + 1 );
2576 if( isVerticalSlide() ) value.push( '.', indexv + 1 );
2577 }
2578
2579 dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
2580 }
2581
2582 }
2583
2584 /**
2585 * Applies HTML formatting to a slide number before it's
2586 * written to the DOM.
2587 */
2588 function formatSlideNumber( a, delimiter, b ) {
2589
2590 if( typeof b === 'number' && !isNaN( b ) ) {
2591 return '<span class="slide-number-a">'+ a +'</span>' +
2592 '<span class="slide-number-delimiter">'+ delimiter +'</span>' +
2593 '<span class="slide-number-b">'+ b +'</span>';
2594 }
2595 else {
2596 return '<span class="slide-number-a">'+ a +'</span>';
2597 }
2598
2599 }
2600
2601 /**
2602 * Updates the state of all control/navigation arrows.
2603 */
2604 function updateControls() {
2605
2606 var routes = availableRoutes();
2607 var fragments = availableFragments();
2608
2609 // Remove the 'enabled' class from all directions
2610 dom.controlsLeft.concat( dom.controlsRight )
2611 .concat( dom.controlsUp )
2612 .concat( dom.controlsDown )
2613 .concat( dom.controlsPrev )
2614 .concat( dom.controlsNext ).forEach( function( node ) {
2615 node.classList.remove( 'enabled' );
2616 node.classList.remove( 'fragmented' );
2617 } );
2618
2619 // Add the 'enabled' class to the available routes
2620 if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2621 if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2622 if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2623 if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2624
2625 // Prev/next buttons
2626 if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2627 if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } );
2628
2629 // Highlight fragment directions
2630 if( currentSlide ) {
2631
2632 // Always apply fragment decorator to prev/next buttons
2633 if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2634 if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2635
2636 // Apply fragment decorators to directional buttons based on
2637 // what slide axis they are in
2638 if( isVerticalSlide( currentSlide ) ) {
2639 if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2640 if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2641 }
2642 else {
2643 if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2644 if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } );
2645 }
2646
2647 }
2648
2649 }
2650
2651 /**
2652 * Updates the background elements to reflect the current
2653 * slide.
2654 *
2655 * @param {Boolean} includeAll If true, the backgrounds of
2656 * all vertical slides (not just the present) will be updated.
2657 */
2658 function updateBackground( includeAll ) {
2659
2660 var currentBackground = null;
2661
2662 // Reverse past/future classes when in RTL mode
2663 var horizontalPast = config.rtl ? 'future' : 'past',
2664 horizontalFuture = config.rtl ? 'past' : 'future';
2665
2666 // Update the classes of all backgrounds to match the
2667 // states of their slides (past/present/future)
2668 toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
2669
2670 backgroundh.classList.remove( 'past' );
2671 backgroundh.classList.remove( 'present' );
2672 backgroundh.classList.remove( 'future' );
2673
2674 if( h < indexh ) {
2675 backgroundh.classList.add( horizontalPast );
2676 }
2677 else if ( h > indexh ) {
2678 backgroundh.classList.add( horizontalFuture );
2679 }
2680 else {
2681 backgroundh.classList.add( 'present' );
2682
2683 // Store a reference to the current background element
2684 currentBackground = backgroundh;
2685 }
2686
2687 if( includeAll || h === indexh ) {
2688 toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
2689
2690 backgroundv.classList.remove( 'past' );
2691 backgroundv.classList.remove( 'present' );
2692 backgroundv.classList.remove( 'future' );
2693
2694 if( v < indexv ) {
2695 backgroundv.classList.add( 'past' );
2696 }
2697 else if ( v > indexv ) {
2698 backgroundv.classList.add( 'future' );
2699 }
2700 else {
2701 backgroundv.classList.add( 'present' );
2702
2703 // Only if this is the present horizontal and vertical slide
2704 if( h === indexh ) currentBackground = backgroundv;
2705 }
2706
2707 } );
2708 }
2709
2710 } );
2711
2712 // Stop any currently playing video background
2713 if( previousBackground ) {
2714
2715 var previousVideo = previousBackground.querySelector( 'video' );
2716 if( previousVideo ) previousVideo.pause();
2717
2718 }
2719
2720 if( currentBackground ) {
2721
2722 // Start video playback
2723 var currentVideo = currentBackground.querySelector( 'video' );
2724 if( currentVideo ) {
2725 if( currentVideo.currentTime > 0 ) currentVideo.currentTime = 0;
2726 currentVideo.play();
2727 }
2728
2729 var backgroundImageURL = currentBackground.style.backgroundImage || '';
2730
2731 // Restart GIFs (doesn't work in Firefox)
2732 if( /\.gif/i.test( backgroundImageURL ) ) {
2733 currentBackground.style.backgroundImage = '';
2734 window.getComputedStyle( currentBackground ).opacity;
2735 currentBackground.style.backgroundImage = backgroundImageURL;
2736 }
2737
2738 // Don't transition between identical backgrounds. This
2739 // prevents unwanted flicker.
2740 var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
2741 var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
2742 if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
2743 dom.background.classList.add( 'no-transition' );
2744 }
2745
2746 previousBackground = currentBackground;
2747
2748 }
2749
2750 // If there's a background brightness flag for this slide,
2751 // bubble it to the .reveal container
2752 if( currentSlide ) {
2753 [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
2754 if( currentSlide.classList.contains( classToBubble ) ) {
2755 dom.wrapper.classList.add( classToBubble );
2756 }
2757 else {
2758 dom.wrapper.classList.remove( classToBubble );
2759 }
2760 } );
2761 }
2762
2763 // Allow the first background to apply without transition
2764 setTimeout( function() {
2765 dom.background.classList.remove( 'no-transition' );
2766 }, 1 );
2767
2768 }
2769
2770 /**
2771 * Updates the position of the parallax background based
2772 * on the current slide index.
2773 */
2774 function updateParallax() {
2775
2776 if( config.parallaxBackgroundImage ) {
2777
2778 var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
2779 verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
2780
2781 var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
2782 backgroundWidth, backgroundHeight;
2783
2784 if( backgroundSize.length === 1 ) {
2785 backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
2786 }
2787 else {
2788 backgroundWidth = parseInt( backgroundSize[0], 10 );
2789 backgroundHeight = parseInt( backgroundSize[1], 10 );
2790 }
2791
2792 var slideWidth = dom.background.offsetWidth,
2793 horizontalSlideCount = horizontalSlides.length,
2794 horizontalOffsetMultiplier,
2795 horizontalOffset;
2796
2797 if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
2798 horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
2799 }
2800 else {
2801 horizontalOffsetMultiplier = ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 );
2802 }
2803
2804 horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
2805
2806 var slideHeight = dom.background.offsetHeight,
2807 verticalSlideCount = verticalSlides.length,
2808 verticalOffsetMultiplier,
2809 verticalOffset;
2810
2811 if( typeof config.parallaxBackgroundVertical === 'number' ) {
2812 verticalOffsetMultiplier = config.parallaxBackgroundVertical;
2813 }
2814 else {
2815 verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
2816 }
2817
2818 verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0;
2819
2820 dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
2821
2822 }
2823
2824 }
2825
2826 /**
2827 * Called when the given slide is within the configured view
2828 * distance. Shows the slide element and loads any content
2829 * that is set to load lazily (data-src).
2830 */
2831 function showSlide( slide ) {
2832
2833 // Show the slide element
2834 slide.style.display = 'block';
2835
2836 // Media elements with data-src attributes
2837 toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
2838 element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
2839 element.removeAttribute( 'data-src' );
2840 } );
2841
2842 // Media elements with <source> children
2843 toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
2844 var sources = 0;
2845
2846 toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
2847 source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
2848 source.removeAttribute( 'data-src' );
2849 sources += 1;
2850 } );
2851
2852 // If we rewrote sources for this video/audio element, we need
2853 // to manually tell it to load from its new origin
2854 if( sources > 0 ) {
2855 media.load();
2856 }
2857 } );
2858
2859
2860 // Show the corresponding background element
2861 var indices = getIndices( slide );
2862 var background = getSlideBackground( indices.h, indices.v );
2863 if( background ) {
2864 background.style.display = 'block';
2865
2866 // If the background contains media, load it
2867 if( background.hasAttribute( 'data-loaded' ) === false ) {
2868 background.setAttribute( 'data-loaded', 'true' );
2869
2870 var backgroundImage = slide.getAttribute( 'data-background-image' ),
2871 backgroundVideo = slide.getAttribute( 'data-background-video' ),
2872 backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
2873 backgroundIframe = slide.getAttribute( 'data-background-iframe' );
2874
2875 // Images
2876 if( backgroundImage ) {
2877 background.style.backgroundImage = 'url('+ backgroundImage +')';
2878 }
2879 // Videos
2880 else if ( backgroundVideo && !isSpeakerNotes() ) {
2881 var video = document.createElement( 'video' );
2882
2883 if( backgroundVideoLoop ) {
2884 video.setAttribute( 'loop', '' );
2885 }
2886
2887 // Support comma separated lists of video sources
2888 backgroundVideo.split( ',' ).forEach( function( source ) {
2889 video.innerHTML += '<source src="'+ source +'">';
2890 } );
2891
2892 background.appendChild( video );
2893 }
2894 // Iframes
2895 else if( backgroundIframe ) {
2896 var iframe = document.createElement( 'iframe' );
2897 iframe.setAttribute( 'src', backgroundIframe );
2898 iframe.style.width = '100%';
2899 iframe.style.height = '100%';
2900 iframe.style.maxHeight = '100%';
2901 iframe.style.maxWidth = '100%';
2902
2903 background.appendChild( iframe );
2904 }
2905 }
2906 }
2907
2908 }
2909
2910 /**
2911 * Called when the given slide is moved outside of the
2912 * configured view distance.
2913 */
2914 function hideSlide( slide ) {
2915
2916 // Hide the slide element
2917 slide.style.display = 'none';
2918
2919 // Hide the corresponding background element
2920 var indices = getIndices( slide );
2921 var background = getSlideBackground( indices.h, indices.v );
2922 if( background ) {
2923 background.style.display = 'none';
2924 }
2925
2926 }
2927
2928 /**
2929 * Determine what available routes there are for navigation.
2930 *
2931 * @return {Object} containing four booleans: left/right/up/down
2932 */
2933 function availableRoutes() {
2934
2935 var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
2936 verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
2937
2938 var routes = {
2939 left: indexh > 0 || config.loop,
2940 right: indexh < horizontalSlides.length - 1 || config.loop,
2941 up: indexv > 0,
2942 down: indexv < verticalSlides.length - 1
2943 };
2944
2945 // reverse horizontal controls for rtl
2946 if( config.rtl ) {
2947 var left = routes.left;
2948 routes.left = routes.right;
2949 routes.right = left;
2950 }
2951
2952 return routes;
2953
2954 }
2955
2956 /**
2957 * Returns an object describing the available fragment
2958 * directions.
2959 *
2960 * @return {Object} two boolean properties: prev/next
2961 */
2962 function availableFragments() {
2963
2964 if( currentSlide && config.fragments ) {
2965 var fragments = currentSlide.querySelectorAll( '.fragment' );
2966 var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
2967
2968 return {
2969 prev: fragments.length - hiddenFragments.length > 0,
2970 next: !!hiddenFragments.length
2971 };
2972 }
2973 else {
2974 return { prev: false, next: false };
2975 }
2976
2977 }
2978
2979 /**
2980 * Enforces origin-specific format rules for embedded media.
2981 */
2982 function formatEmbeddedContent() {
2983
2984 var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
2985 toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
2986 var src = el.getAttribute( sourceAttribute );
2987 if( src && src.indexOf( param ) === -1 ) {
2988 el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
2989 }
2990 });
2991 };
2992
2993 // YouTube frames must include "?enablejsapi=1"
2994 _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
2995 _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
2996
2997 // Vimeo frames must include "?api=1"
2998 _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
2999 _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
3000
3001 }
3002
3003 /**
3004 * Start playback of any embedded content inside of
3005 * the targeted slide.
3006 */
3007 function startEmbeddedContent( slide ) {
3008
3009 if( slide && !isSpeakerNotes() ) {
3010 // Restart GIFs
3011 toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
3012 // Setting the same unchanged source like this was confirmed
3013 // to work in Chrome, FF & Safari
3014 el.setAttribute( 'src', el.getAttribute( 'src' ) );
3015 } );
3016
3017 // HTML5 media elements
3018 toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3019 if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) {
3020 el.play();
3021 }
3022 } );
3023
3024 // Normal iframes
3025 toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
3026 startEmbeddedIframe( { target: el } );
3027 } );
3028
3029 // Lazy loading iframes
3030 toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3031 if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
3032 el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
3033 el.addEventListener( 'load', startEmbeddedIframe );
3034 el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
3035 }
3036 } );
3037 }
3038
3039 }
3040
3041 /**
3042 * "Starts" the content of an embedded iframe using the
3043 * postmessage API.
3044 */
3045 function startEmbeddedIframe( event ) {
3046
3047 var iframe = event.target;
3048
3049 // YouTube postMessage API
3050 if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
3051 iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
3052 }
3053 // Vimeo postMessage API
3054 else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) {
3055 iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
3056 }
3057 // Generic postMessage API
3058 else {
3059 iframe.contentWindow.postMessage( 'slide:start', '*' );
3060 }
3061
3062 }
3063
3064 /**
3065 * Stop playback of any embedded content inside of
3066 * the targeted slide.
3067 */
3068 function stopEmbeddedContent( slide ) {
3069
3070 if( slide && slide.parentNode ) {
3071 // HTML5 media elements
3072 toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3073 if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
3074 el.pause();
3075 }
3076 } );
3077
3078 // Generic postMessage API for non-lazy loaded iframes
3079 toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
3080 el.contentWindow.postMessage( 'slide:stop', '*' );
3081 el.removeEventListener( 'load', startEmbeddedIframe );
3082 });
3083
3084 // YouTube postMessage API
3085 toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
3086 if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3087 el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
3088 }
3089 });
3090
3091 // Vimeo postMessage API
3092 toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
3093 if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) {
3094 el.contentWindow.postMessage( '{"method":"pause"}', '*' );
3095 }
3096 });
3097
3098 // Lazy loading iframes
3099 toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
3100 // Only removing the src doesn't actually unload the frame
3101 // in all browsers (Firefox) so we set it to blank first
3102 el.setAttribute( 'src', 'about:blank' );
3103 el.removeAttribute( 'src' );
3104 } );
3105 }
3106
3107 }
3108
3109 /**
3110 * Returns the number of past slides. This can be used as a global
3111 * flattened index for slides.
3112 */
3113 function getSlidePastCount() {
3114
3115 var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3116
3117 // The number of past slides
3118 var pastCount = 0;
3119
3120 // Step through all slides and count the past ones
3121 mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
3122
3123 var horizontalSlide = horizontalSlides[i];
3124 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
3125
3126 for( var j = 0; j < verticalSlides.length; j++ ) {
3127
3128 // Stop as soon as we arrive at the present
3129 if( verticalSlides[j].classList.contains( 'present' ) ) {
3130 break mainLoop;
3131 }
3132
3133 pastCount++;
3134
3135 }
3136
3137 // Stop as soon as we arrive at the present
3138 if( horizontalSlide.classList.contains( 'present' ) ) {
3139 break;
3140 }
3141
3142 // Don't count the wrapping section for vertical slides
3143 if( horizontalSlide.classList.contains( 'stack' ) === false ) {
3144 pastCount++;
3145 }
3146
3147 }
3148
3149 return pastCount;
3150
3151 }
3152
3153 /**
3154 * Returns a value ranging from 0-1 that represents
3155 * how far into the presentation we have navigated.
3156 */
3157 function getProgress() {
3158
3159 // The number of past and total slides
3160 var totalCount = getTotalSlides();
3161 var pastCount = getSlidePastCount();
3162
3163 if( currentSlide ) {
3164
3165 var allFragments = currentSlide.querySelectorAll( '.fragment' );
3166
3167 // If there are fragments in the current slide those should be
3168 // accounted for in the progress.
3169 if( allFragments.length > 0 ) {
3170 var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
3171
3172 // This value represents how big a portion of the slide progress
3173 // that is made up by its fragments (0-1)
3174 var fragmentWeight = 0.9;
3175
3176 // Add fragment progress to the past slide count
3177 pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
3178 }
3179
3180 }
3181
3182 return pastCount / ( totalCount - 1 );
3183
3184 }
3185
3186 /**
3187 * Checks if this presentation is running inside of the
3188 * speaker notes window.
3189 */
3190 function isSpeakerNotes() {
3191
3192 return !!window.location.search.match( /receiver/gi );
3193
3194 }
3195
3196 /**
3197 * Reads the current URL (hash) and navigates accordingly.
3198 */
3199 function readURL() {
3200
3201 var hash = window.location.hash;
3202
3203 // Attempt to parse the hash as either an index or name
3204 var bits = hash.slice( 2 ).split( '/' ),
3205 name = hash.replace( /#|\//gi, '' );
3206
3207 // If the first bit is invalid and there is a name we can
3208 // assume that this is a named link
3209 if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
3210 var element;
3211
3212 // Ensure the named link is a valid HTML ID attribute
3213 if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) {
3214 // Find the slide with the specified ID
3215 element = document.getElementById( name );
3216 }
3217
3218 if( element ) {
3219 // Find the position of the named slide and navigate to it
3220 var indices = Reveal.getIndices( element );
3221 slide( indices.h, indices.v );
3222 }
3223 // If the slide doesn't exist, navigate to the current slide
3224 else {
3225 slide( indexh || 0, indexv || 0 );
3226 }
3227 }
3228 else {
3229 // Read the index components of the hash
3230 var h = parseInt( bits[0], 10 ) || 0,
3231 v = parseInt( bits[1], 10 ) || 0;
3232
3233 if( h !== indexh || v !== indexv ) {
3234 slide( h, v );
3235 }
3236 }
3237
3238 }
3239
3240 /**
3241 * Updates the page URL (hash) to reflect the current
3242 * state.
3243 *
3244 * @param {Number} delay The time in ms to wait before
3245 * writing the hash
3246 */
3247 function writeURL( delay ) {
3248
3249 if( config.history ) {
3250
3251 // Make sure there's never more than one timeout running
3252 clearTimeout( writeURLTimeout );
3253
3254 // If a delay is specified, timeout this call
3255 if( typeof delay === 'number' ) {
3256 writeURLTimeout = setTimeout( writeURL, delay );
3257 }
3258 else if( currentSlide ) {
3259 var url = '/';
3260
3261 // Attempt to create a named link based on the slide's ID
3262 var id = currentSlide.getAttribute( 'id' );
3263 if( id ) {
3264 id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' );
3265 }
3266
3267 // If the current slide has an ID, use that as a named link
3268 if( typeof id === 'string' && id.length ) {
3269 url = '/' + id;
3270 }
3271 // Otherwise use the /h/v index
3272 else {
3273 if( indexh > 0 || indexv > 0 ) url += indexh;
3274 if( indexv > 0 ) url += '/' + indexv;
3275 }
3276
3277 window.location.hash = url;
3278 }
3279 }
3280
3281 }
3282
3283 /**
3284 * Retrieves the h/v location of the current, or specified,
3285 * slide.
3286 *
3287 * @param {HTMLElement} slide If specified, the returned
3288 * index will be for this slide rather than the currently
3289 * active one
3290 *
3291 * @return {Object} { h: <int>, v: <int>, f: <int> }
3292 */
3293 function getIndices( slide ) {
3294
3295 // By default, return the current indices
3296 var h = indexh,
3297 v = indexv,
3298 f;
3299
3300 // If a slide is specified, return the indices of that slide
3301 if( slide ) {
3302 var isVertical = isVerticalSlide( slide );
3303 var slideh = isVertical ? slide.parentNode : slide;
3304
3305 // Select all horizontal slides
3306 var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
3307
3308 // Now that we know which the horizontal slide is, get its index
3309 h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
3310
3311 // Assume we're not vertical
3312 v = undefined;
3313
3314 // If this is a vertical slide, grab the vertical index
3315 if( isVertical ) {
3316 v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
3317 }
3318 }
3319
3320 if( !slide && currentSlide ) {
3321 var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
3322 if( hasFragments ) {
3323 var currentFragment = currentSlide.querySelector( '.current-fragment' );
3324 if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
3325 f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
3326 }
3327 else {
3328 f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
3329 }
3330 }
3331 }
3332
3333 return { h: h, v: v, f: f };
3334
3335 }
3336
3337 /**
3338 * Retrieves the total number of slides in this presentation.
3339 */
3340 function getTotalSlides() {
3341
3342 return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length;
3343
3344 }
3345
3346 /**
3347 * Returns the slide element matching the specified index.
3348 */
3349 function getSlide( x, y ) {
3350
3351 var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
3352 var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
3353
3354 if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
3355 return verticalSlides ? verticalSlides[ y ] : undefined;
3356 }
3357
3358 return horizontalSlide;
3359
3360 }
3361
3362 /**
3363 * Returns the background element for the given slide.
3364 * All slides, even the ones with no background properties
3365 * defined, have a background element so as long as the
3366 * index is valid an element will be returned.
3367 */
3368 function getSlideBackground( x, y ) {
3369
3370 // When printing to PDF the slide backgrounds are nested
3371 // inside of the slides
3372 if( isPrintingPDF() ) {
3373 var slide = getSlide( x, y );
3374 if( slide ) {
3375 var background = slide.querySelector( '.slide-background' );
3376 if( background && background.parentNode === slide ) {
3377 return background;
3378 }
3379 }
3380
3381 return undefined;
3382 }
3383
3384 var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ];
3385 var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' );
3386
3387 if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) {
3388 return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined;
3389 }
3390
3391 return horizontalBackground;
3392
3393 }
3394
3395 /**
3396 * Retrieves the speaker notes from a slide. Notes can be
3397 * defined in two ways:
3398 * 1. As a data-notes attribute on the slide <section>
3399 * 2. As an <aside class="notes"> inside of the slide
3400 */
3401 function getSlideNotes( slide ) {
3402
3403 // Default to the current slide
3404 slide = slide || currentSlide;
3405
3406 // Notes can be specified via the data-notes attribute...
3407 if( slide.hasAttribute( 'data-notes' ) ) {
3408 return slide.getAttribute( 'data-notes' );
3409 }
3410
3411 // ... or using an <aside class="notes"> element
3412 var notesElement = slide.querySelector( 'aside.notes' );
3413 if( notesElement ) {
3414 return notesElement.innerHTML;
3415 }
3416
3417 return null;
3418
3419 }
3420
3421 /**
3422 * Retrieves the current state of the presentation as
3423 * an object. This state can then be restored at any
3424 * time.
3425 */
3426 function getState() {
3427
3428 var indices = getIndices();
3429
3430 return {
3431 indexh: indices.h,
3432 indexv: indices.v,
3433 indexf: indices.f,
3434 paused: isPaused(),
3435 overview: isOverview()
3436 };
3437
3438 }
3439
3440 /**
3441 * Restores the presentation to the given state.
3442 *
3443 * @param {Object} state As generated by getState()
3444 */
3445 function setState( state ) {
3446
3447 if( typeof state === 'object' ) {
3448 slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
3449
3450 var pausedFlag = deserialize( state.paused ),
3451 overviewFlag = deserialize( state.overview );
3452
3453 if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
3454 togglePause( pausedFlag );
3455 }
3456
3457 if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
3458 toggleOverview( overviewFlag );
3459 }
3460 }
3461
3462 }
3463
3464 /**
3465 * Return a sorted fragments list, ordered by an increasing
3466 * "data-fragment-index" attribute.
3467 *
3468 * Fragments will be revealed in the order that they are returned by
3469 * this function, so you can use the index attributes to control the
3470 * order of fragment appearance.
3471 *
3472 * To maintain a sensible default fragment order, fragments are presumed
3473 * to be passed in document order. This function adds a "fragment-index"
3474 * attribute to each node if such an attribute is not already present,
3475 * and sets that attribute to an integer value which is the position of
3476 * the fragment within the fragments list.
3477 */
3478 function sortFragments( fragments ) {
3479
3480 fragments = toArray( fragments );
3481
3482 var ordered = [],
3483 unordered = [],
3484 sorted = [];
3485
3486 // Group ordered and unordered elements
3487 fragments.forEach( function( fragment, i ) {
3488 if( fragment.hasAttribute( 'data-fragment-index' ) ) {
3489 var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
3490
3491 if( !ordered[index] ) {
3492 ordered[index] = [];
3493 }
3494
3495 ordered[index].push( fragment );
3496 }
3497 else {
3498 unordered.push( [ fragment ] );
3499 }
3500 } );
3501
3502 // Append fragments without explicit indices in their
3503 // DOM order
3504 ordered = ordered.concat( unordered );
3505
3506 // Manually count the index up per group to ensure there
3507 // are no gaps
3508 var index = 0;
3509
3510 // Push all fragments in their sorted order to an array,
3511 // this flattens the groups
3512 ordered.forEach( function( group ) {
3513 group.forEach( function( fragment ) {
3514 sorted.push( fragment );
3515 fragment.setAttribute( 'data-fragment-index', index );
3516 } );
3517
3518 index ++;
3519 } );
3520
3521 return sorted;
3522
3523 }
3524
3525 /**
3526 * Navigate to the specified slide fragment.
3527 *
3528 * @param {Number} index The index of the fragment that
3529 * should be shown, -1 means all are invisible
3530 * @param {Number} offset Integer offset to apply to the
3531 * fragment index
3532 *
3533 * @return {Boolean} true if a change was made in any
3534 * fragments visibility as part of this call
3535 */
3536 function navigateFragment( index, offset ) {
3537
3538 if( currentSlide && config.fragments ) {
3539
3540 var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
3541 if( fragments.length ) {
3542
3543 // If no index is specified, find the current
3544 if( typeof index !== 'number' ) {
3545 var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
3546
3547 if( lastVisibleFragment ) {
3548 index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
3549 }
3550 else {
3551 index = -1;
3552 }
3553 }
3554
3555 // If an offset is specified, apply it to the index
3556 if( typeof offset === 'number' ) {
3557 index += offset;
3558 }
3559
3560 var fragmentsShown = [],
3561 fragmentsHidden = [];
3562
3563 toArray( fragments ).forEach( function( element, i ) {
3564
3565 if( element.hasAttribute( 'data-fragment-index' ) ) {
3566 i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
3567 }
3568
3569 // Visible fragments
3570 if( i <= index ) {
3571 if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
3572 element.classList.add( 'visible' );
3573 element.classList.remove( 'current-fragment' );
3574
3575 // Announce the fragments one by one to the Screen Reader
3576 dom.statusDiv.textContent = element.textContent;
3577
3578 if( i === index ) {
3579 element.classList.add( 'current-fragment' );
3580 }
3581 }
3582 // Hidden fragments
3583 else {
3584 if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
3585 element.classList.remove( 'visible' );
3586 element.classList.remove( 'current-fragment' );
3587 }
3588
3589
3590 } );
3591
3592 if( fragmentsHidden.length ) {
3593 dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
3594 }
3595
3596 if( fragmentsShown.length ) {
3597 dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
3598 }
3599
3600 updateControls();
3601 updateProgress();
3602
3603 return !!( fragmentsShown.length || fragmentsHidden.length );
3604
3605 }
3606
3607 }
3608
3609 return false;
3610
3611 }
3612
3613 /**
3614 * Navigate to the next slide fragment.
3615 *
3616 * @return {Boolean} true if there was a next fragment,
3617 * false otherwise
3618 */
3619 function nextFragment() {
3620
3621 return navigateFragment( null, 1 );
3622
3623 }
3624
3625 /**
3626 * Navigate to the previous slide fragment.
3627 *
3628 * @return {Boolean} true if there was a previous fragment,
3629 * false otherwise
3630 */
3631 function previousFragment() {
3632
3633 return navigateFragment( null, -1 );
3634
3635 }
3636
3637 /**
3638 * Cues a new automated slide if enabled in the config.
3639 */
3640 function cueAutoSlide() {
3641
3642 cancelAutoSlide();
3643
3644 if( currentSlide ) {
3645
3646 var currentFragment = currentSlide.querySelector( '.current-fragment' );
3647
3648 var fragmentAutoSlide = currentFragment ? currentFragment.getAttribute( 'data-autoslide' ) : null;
3649 var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
3650 var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
3651
3652 // Pick value in the following priority order:
3653 // 1. Current fragment's data-autoslide
3654 // 2. Current slide's data-autoslide
3655 // 3. Parent slide's data-autoslide
3656 // 4. Global autoSlide setting
3657 if( fragmentAutoSlide ) {
3658 autoSlide = parseInt( fragmentAutoSlide, 10 );
3659 }
3660 else if( slideAutoSlide ) {
3661 autoSlide = parseInt( slideAutoSlide, 10 );
3662 }
3663 else if( parentAutoSlide ) {
3664 autoSlide = parseInt( parentAutoSlide, 10 );
3665 }
3666 else {
3667 autoSlide = config.autoSlide;
3668 }
3669
3670 // If there are media elements with data-autoplay,
3671 // automatically set the autoSlide duration to the
3672 // length of that media. Not applicable if the slide
3673 // is divided up into fragments.
3674 if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
3675 toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
3676 if( el.hasAttribute( 'data-autoplay' ) ) {
3677 if( autoSlide && el.duration * 1000 > autoSlide ) {
3678 autoSlide = ( el.duration * 1000 ) + 1000;
3679 }
3680 }
3681 } );
3682 }
3683
3684 // Cue the next auto-slide if:
3685 // - There is an autoSlide value
3686 // - Auto-sliding isn't paused by the user
3687 // - The presentation isn't paused
3688 // - The overview isn't active
3689 // - The presentation isn't over
3690 if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
3691 autoSlideTimeout = setTimeout( navigateNext, autoSlide );
3692 autoSlideStartTime = Date.now();
3693 }
3694
3695 if( autoSlidePlayer ) {
3696 autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
3697 }
3698
3699 }
3700
3701 }
3702
3703 /**
3704 * Cancels any ongoing request to auto-slide.
3705 */
3706 function cancelAutoSlide() {
3707
3708 clearTimeout( autoSlideTimeout );
3709 autoSlideTimeout = -1;
3710
3711 }
3712
3713 function pauseAutoSlide() {
3714
3715 if( autoSlide && !autoSlidePaused ) {
3716 autoSlidePaused = true;
3717 dispatchEvent( 'autoslidepaused' );
3718 clearTimeout( autoSlideTimeout );
3719
3720 if( autoSlidePlayer ) {
3721 autoSlidePlayer.setPlaying( false );
3722 }
3723 }
3724
3725 }
3726
3727 function resumeAutoSlide() {
3728
3729 if( autoSlide && autoSlidePaused ) {
3730 autoSlidePaused = false;
3731 dispatchEvent( 'autoslideresumed' );
3732 cueAutoSlide();
3733 }
3734
3735 }
3736
3737 function navigateLeft() {
3738
3739 // Reverse for RTL
3740 if( config.rtl ) {
3741 if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
3742 slide( indexh + 1 );
3743 }
3744 }
3745 // Normal navigation
3746 else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
3747 slide( indexh - 1 );
3748 }
3749
3750 }
3751
3752 function navigateRight() {
3753
3754 // Reverse for RTL
3755 if( config.rtl ) {
3756 if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
3757 slide( indexh - 1 );
3758 }
3759 }
3760 // Normal navigation
3761 else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
3762 slide( indexh + 1 );
3763 }
3764
3765 }
3766
3767 function navigateUp() {
3768
3769 // Prioritize hiding fragments
3770 if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
3771 slide( indexh, indexv - 1 );
3772 }
3773
3774 }
3775
3776 function navigateDown() {
3777
3778 // Prioritize revealing fragments
3779 if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
3780 slide( indexh, indexv + 1 );
3781 }
3782
3783 }
3784
3785 /**
3786 * Navigates backwards, prioritized in the following order:
3787 * 1) Previous fragment
3788 * 2) Previous vertical slide
3789 * 3) Previous horizontal slide
3790 */
3791 function navigatePrev() {
3792
3793 // Prioritize revealing fragments
3794 if( previousFragment() === false ) {
3795 if( availableRoutes().up ) {
3796 navigateUp();
3797 }
3798 else {
3799 // Fetch the previous horizontal slide, if there is one
3800 var previousSlide;
3801
3802 if( config.rtl ) {
3803 previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
3804 }
3805 else {
3806 previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
3807 }
3808
3809 if( previousSlide ) {
3810 var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
3811 var h = indexh - 1;
3812 slide( h, v );
3813 }
3814 }
3815 }
3816
3817 }
3818
3819 /**
3820 * The reverse of #navigatePrev().
3821 */
3822 function navigateNext() {
3823
3824 // Prioritize revealing fragments
3825 if( nextFragment() === false ) {
3826 if( availableRoutes().down ) {
3827 navigateDown();
3828 }
3829 else if( config.rtl ) {
3830 navigateLeft();
3831 }
3832 else {
3833 navigateRight();
3834 }
3835 }
3836
3837 // If auto-sliding is enabled we need to cue up
3838 // another timeout
3839 cueAutoSlide();
3840
3841 }
3842
3843 /**
3844 * Checks if the target element prevents the triggering of
3845 * swipe navigation.
3846 */
3847 function isSwipePrevented( target ) {
3848
3849 while( target && typeof target.hasAttribute === 'function' ) {
3850 if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
3851 target = target.parentNode;
3852 }
3853
3854 return false;
3855
3856 }
3857
3858
3859 // --------------------------------------------------------------------//
3860 // ----------------------------- EVENTS -------------------------------//
3861 // --------------------------------------------------------------------//
3862
3863 /**
3864 * Called by all event handlers that are based on user
3865 * input.
3866 */
3867 function onUserInput( event ) {
3868
3869 if( config.autoSlideStoppable ) {
3870 pauseAutoSlide();
3871 }
3872
3873 }
3874
3875 /**
3876 * Handler for the document level 'keypress' event.
3877 */
3878 function onDocumentKeyPress( event ) {
3879
3880 // Check if the pressed key is question mark
3881 if( event.shiftKey && event.charCode === 63 ) {
3882 if( dom.overlay ) {
3883 closeOverlay();
3884 }
3885 else {
3886 showHelp( true );
3887 }
3888 }
3889
3890 }
3891
3892 /**
3893 * Handler for the document level 'keydown' event.
3894 */
3895 function onDocumentKeyDown( event ) {
3896
3897 // If there's a condition specified and it returns false,
3898 // ignore this event
3899 if( typeof config.keyboardCondition === 'function' && config.keyboardCondition() === false ) {
3900 return true;
3901 }
3902
3903 // Remember if auto-sliding was paused so we can toggle it
3904 var autoSlideWasPaused = autoSlidePaused;
3905
3906 onUserInput( event );
3907
3908 // Check if there's a focused element that could be using
3909 // the keyboard
3910 var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
3911 var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
3912
3913 // Disregard the event if there's a focused element or a
3914 // keyboard modifier key is present
3915 if( activeElementIsCE || activeElementIsInput || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
3916
3917 // While paused only allow resume keyboard events; 'b', '.''
3918 var resumeKeyCodes = [66,190,191];
3919 var key;
3920
3921 // Custom key bindings for togglePause should be able to resume
3922 if( typeof config.keyboard === 'object' ) {
3923 for( key in config.keyboard ) {
3924 if( config.keyboard[key] === 'togglePause' ) {
3925 resumeKeyCodes.push( parseInt( key, 10 ) );
3926 }
3927 }
3928 }
3929
3930 if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
3931 return false;
3932 }
3933
3934 var triggered = false;
3935
3936 // 1. User defined key bindings
3937 if( typeof config.keyboard === 'object' ) {
3938
3939 for( key in config.keyboard ) {
3940
3941 // Check if this binding matches the pressed key
3942 if( parseInt( key, 10 ) === event.keyCode ) {
3943
3944 var value = config.keyboard[ key ];
3945
3946 // Callback function
3947 if( typeof value === 'function' ) {
3948 value.apply( null, [ event ] );
3949 }
3950 // String shortcuts to reveal.js API
3951 else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
3952 Reveal[ value ].call();
3953 }
3954
3955 triggered = true;
3956
3957 }
3958
3959 }
3960
3961 }
3962
3963 // 2. System defined key bindings
3964 if( triggered === false ) {
3965
3966 // Assume true and try to prove false
3967 triggered = true;
3968
3969 switch( event.keyCode ) {
3970 // p, page up
3971 case 80: case 33: navigatePrev(); break;
3972 // n, page down
3973 case 78: case 34: navigateNext(); break;
3974 // h, left
3975 case 72: case 37: navigateLeft(); break;
3976 // l, right
3977 case 76: case 39: navigateRight(); break;
3978 // k, up
3979 case 75: case 38: navigateUp(); break;
3980 // j, down
3981 case 74: case 40: navigateDown(); break;
3982 // home
3983 case 36: slide( 0 ); break;
3984 // end
3985 case 35: slide( Number.MAX_VALUE ); break;
3986 // space
3987 case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
3988 // return
3989 case 13: isOverview() ? deactivateOverview() : triggered = false; break;
3990 // two-spot, semicolon, b, period, Logitech presenter tools "black screen" button
3991 case 58: case 59: case 66: case 190: case 191: togglePause(); break;
3992 // f
3993 case 70: enterFullscreen(); break;
3994 // a
3995 case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
3996 default:
3997 triggered = false;
3998 }
3999
4000 }
4001
4002 // If the input resulted in a triggered action we should prevent
4003 // the browsers default behavior
4004 if( triggered ) {
4005 event.preventDefault && event.preventDefault();
4006 }
4007 // ESC or O key
4008 else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
4009 if( dom.overlay ) {
4010 closeOverlay();
4011 }
4012 else {
4013 toggleOverview();
4014 }
4015
4016 event.preventDefault && event.preventDefault();
4017 }
4018
4019 // If auto-sliding is enabled we need to cue up
4020 // another timeout
4021 cueAutoSlide();
4022
4023 }
4024
4025 /**
4026 * Handler for the 'touchstart' event, enables support for
4027 * swipe and pinch gestures.
4028 */
4029 function onTouchStart( event ) {
4030
4031 if( isSwipePrevented( event.target ) ) return true;
4032
4033 touch.startX = event.touches[0].clientX;
4034 touch.startY = event.touches[0].clientY;
4035 touch.startCount = event.touches.length;
4036
4037 // If there's two touches we need to memorize the distance
4038 // between those two points to detect pinching
4039 if( event.touches.length === 2 && config.overview ) {
4040 touch.startSpan = distanceBetween( {
4041 x: event.touches[1].clientX,
4042 y: event.touches[1].clientY
4043 }, {
4044 x: touch.startX,
4045 y: touch.startY
4046 } );
4047 }
4048
4049 }
4050
4051 /**
4052 * Handler for the 'touchmove' event.
4053 */
4054 function onTouchMove( event ) {
4055
4056 if( isSwipePrevented( event.target ) ) return true;
4057
4058 // Each touch should only trigger one action
4059 if( !touch.captured ) {
4060 onUserInput( event );
4061
4062 var currentX = event.touches[0].clientX;
4063 var currentY = event.touches[0].clientY;
4064
4065 // If the touch started with two points and still has
4066 // two active touches; test for the pinch gesture
4067 if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
4068
4069 // The current distance in pixels between the two touch points
4070 var currentSpan = distanceBetween( {
4071 x: event.touches[1].clientX,
4072 y: event.touches[1].clientY
4073 }, {
4074 x: touch.startX,
4075 y: touch.startY
4076 } );
4077
4078 // If the span is larger than the desire amount we've got
4079 // ourselves a pinch
4080 if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
4081 touch.captured = true;
4082
4083 if( currentSpan < touch.startSpan ) {
4084 activateOverview();
4085 }
4086 else {
4087 deactivateOverview();
4088 }
4089 }
4090
4091 event.preventDefault();
4092
4093 }
4094 // There was only one touch point, look for a swipe
4095 else if( event.touches.length === 1 && touch.startCount !== 2 ) {
4096
4097 var deltaX = currentX - touch.startX,
4098 deltaY = currentY - touch.startY;
4099
4100 if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
4101 touch.captured = true;
4102 navigateLeft();
4103 }
4104 else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
4105 touch.captured = true;
4106 navigateRight();
4107 }
4108 else if( deltaY > touch.threshold ) {
4109 touch.captured = true;
4110 navigateUp();
4111 }
4112 else if( deltaY < -touch.threshold ) {
4113 touch.captured = true;
4114 navigateDown();
4115 }
4116
4117 // If we're embedded, only block touch events if they have
4118 // triggered an action
4119 if( config.embedded ) {
4120 if( touch.captured || isVerticalSlide( currentSlide ) ) {
4121 event.preventDefault();
4122 }
4123 }
4124 // Not embedded? Block them all to avoid needless tossing
4125 // around of the viewport in iOS
4126 else {
4127 event.preventDefault();
4128 }
4129
4130 }
4131 }
4132 // There's a bug with swiping on some Android devices unless
4133 // the default action is always prevented
4134 else if( navigator.userAgent.match( /android/gi ) ) {
4135 event.preventDefault();
4136 }
4137
4138 }
4139
4140 /**
4141 * Handler for the 'touchend' event.
4142 */
4143 function onTouchEnd( event ) {
4144
4145 touch.captured = false;
4146
4147 }
4148
4149 /**
4150 * Convert pointer down to touch start.
4151 */
4152 function onPointerDown( event ) {
4153
4154 if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
4155 event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
4156 onTouchStart( event );
4157 }
4158
4159 }
4160
4161 /**
4162 * Convert pointer move to touch move.
4163 */
4164 function onPointerMove( event ) {
4165
4166 if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
4167 event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
4168 onTouchMove( event );
4169 }
4170
4171 }
4172
4173 /**
4174 * Convert pointer up to touch end.
4175 */
4176 function onPointerUp( event ) {
4177
4178 if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
4179 event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
4180 onTouchEnd( event );
4181 }
4182
4183 }
4184
4185 /**
4186 * Handles mouse wheel scrolling, throttled to avoid skipping
4187 * multiple slides.
4188 */
4189 function onDocumentMouseScroll( event ) {
4190
4191 if( Date.now() - lastMouseWheelStep > 600 ) {
4192
4193 lastMouseWheelStep = Date.now();
4194
4195 var delta = event.detail || -event.wheelDelta;
4196 if( delta > 0 ) {
4197 navigateNext();
4198 }
4199 else {
4200 navigatePrev();
4201 }
4202
4203 }
4204
4205 }
4206
4207 /**
4208 * Clicking on the progress bar results in a navigation to the
4209 * closest approximate horizontal slide using this equation:
4210 *
4211 * ( clickX / presentationWidth ) * numberOfSlides
4212 */
4213 function onProgressClicked( event ) {
4214
4215 onUserInput( event );
4216
4217 event.preventDefault();
4218
4219 var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
4220 var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
4221
4222 if( config.rtl ) {
4223 slideIndex = slidesTotal - slideIndex;
4224 }
4225
4226 slide( slideIndex );
4227
4228 }
4229
4230 /**
4231 * Event handler for navigation control buttons.
4232 */
4233 function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
4234 function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
4235 function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
4236 function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
4237 function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
4238 function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
4239
4240 /**
4241 * Handler for the window level 'hashchange' event.
4242 */
4243 function onWindowHashChange( event ) {
4244
4245 readURL();
4246
4247 }
4248
4249 /**
4250 * Handler for the window level 'resize' event.
4251 */
4252 function onWindowResize( event ) {
4253
4254 layout();
4255
4256 }
4257
4258 /**
4259 * Handle for the window level 'visibilitychange' event.
4260 */
4261 function onPageVisibilityChange( event ) {
4262
4263 var isHidden = document.webkitHidden ||
4264 document.msHidden ||
4265 document.hidden;
4266
4267 // If, after clicking a link or similar and we're coming back,
4268 // focus the document.body to ensure we can use keyboard shortcuts
4269 if( isHidden === false && document.activeElement !== document.body ) {
4270 // Not all elements support .blur() - SVGs among them.
4271 if( typeof document.activeElement.blur === 'function' ) {
4272 document.activeElement.blur();
4273 }
4274 document.body.focus();
4275 }
4276
4277 }
4278
4279 /**
4280 * Invoked when a slide is and we're in the overview.
4281 */
4282 function onOverviewSlideClicked( event ) {
4283
4284 // TODO There's a bug here where the event listeners are not
4285 // removed after deactivating the overview.
4286 if( eventsAreBound && isOverview() ) {
4287 event.preventDefault();
4288
4289 var element = event.target;
4290
4291 while( element && !element.nodeName.match( /section/gi ) ) {
4292 element = element.parentNode;
4293 }
4294
4295 if( element && !element.classList.contains( 'disabled' ) ) {
4296
4297 deactivateOverview();
4298
4299 if( element.nodeName.match( /section/gi ) ) {
4300 var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
4301 v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
4302
4303 slide( h, v );
4304 }
4305
4306 }
4307 }
4308
4309 }
4310
4311 /**
4312 * Handles clicks on links that are set to preview in the
4313 * iframe overlay.
4314 */
4315 function onPreviewLinkClicked( event ) {
4316
4317 if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
4318 var url = event.currentTarget.getAttribute( 'href' );
4319 if( url ) {
4320 showPreview( url );
4321 event.preventDefault();
4322 }
4323 }
4324
4325 }
4326
4327 /**
4328 * Handles click on the auto-sliding controls element.
4329 */
4330 function onAutoSlidePlayerClick( event ) {
4331
4332 // Replay
4333 if( Reveal.isLastSlide() && config.loop === false ) {
4334 slide( 0, 0 );
4335 resumeAutoSlide();
4336 }
4337 // Resume
4338 else if( autoSlidePaused ) {
4339 resumeAutoSlide();
4340 }
4341 // Pause
4342 else {
4343 pauseAutoSlide();
4344 }
4345
4346 }
4347
4348
4349 // --------------------------------------------------------------------//
4350 // ------------------------ PLAYBACK COMPONENT ------------------------//
4351 // --------------------------------------------------------------------//
4352
4353
4354 /**
4355 * Constructor for the playback component, which displays
4356 * play/pause/progress controls.
4357 *
4358 * @param {HTMLElement} container The component will append
4359 * itself to this
4360 * @param {Function} progressCheck A method which will be
4361 * called frequently to get the current progress on a range
4362 * of 0-1
4363 */
4364 function Playback( container, progressCheck ) {
4365
4366 // Cosmetics
4367 this.diameter = 50;
4368 this.thickness = 3;
4369
4370 // Flags if we are currently playing
4371 this.playing = false;
4372
4373 // Current progress on a 0-1 range
4374 this.progress = 0;
4375
4376 // Used to loop the animation smoothly
4377 this.progressOffset = 1;
4378
4379 this.container = container;
4380 this.progressCheck = progressCheck;
4381
4382 this.canvas = document.createElement( 'canvas' );
4383 this.canvas.className = 'playback';
4384 this.canvas.width = this.diameter;
4385 this.canvas.height = this.diameter;
4386 this.context = this.canvas.getContext( '2d' );
4387
4388 this.container.appendChild( this.canvas );
4389
4390 this.render();
4391
4392 }
4393
4394 Playback.prototype.setPlaying = function( value ) {
4395
4396 var wasPlaying = this.playing;
4397
4398 this.playing = value;
4399
4400 // Start repainting if we weren't already
4401 if( !wasPlaying && this.playing ) {
4402 this.animate();
4403 }
4404 else {
4405 this.render();
4406 }
4407
4408 };
4409
4410 Playback.prototype.animate = function() {
4411
4412 var progressBefore = this.progress;
4413
4414 this.progress = this.progressCheck();
4415
4416 // When we loop, offset the progress so that it eases
4417 // smoothly rather than immediately resetting
4418 if( progressBefore > 0.8 && this.progress < 0.2 ) {
4419 this.progressOffset = this.progress;
4420 }
4421
4422 this.render();
4423
4424 if( this.playing ) {
4425 features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
4426 }
4427
4428 };
4429
4430 /**
4431 * Renders the current progress and playback state.
4432 */
4433 Playback.prototype.render = function() {
4434
4435 var progress = this.playing ? this.progress : 0,
4436 radius = ( this.diameter / 2 ) - this.thickness,
4437 x = this.diameter / 2,
4438 y = this.diameter / 2,
4439 iconSize = 14;
4440
4441 // Ease towards 1
4442 this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
4443
4444 var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
4445 var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
4446
4447 this.context.save();
4448 this.context.clearRect( 0, 0, this.diameter, this.diameter );
4449
4450 // Solid background color
4451 this.context.beginPath();
4452 this.context.arc( x, y, radius + 2, 0, Math.PI * 2, false );
4453 this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
4454 this.context.fill();
4455
4456 // Draw progress track
4457 this.context.beginPath();
4458 this.context.arc( x, y, radius, 0, Math.PI * 2, false );
4459 this.context.lineWidth = this.thickness;
4460 this.context.strokeStyle = '#666';
4461 this.context.stroke();
4462
4463 if( this.playing ) {
4464 // Draw progress on top of track
4465 this.context.beginPath();
4466 this.context.arc( x, y, radius, startAngle, endAngle, false );
4467 this.context.lineWidth = this.thickness;
4468 this.context.strokeStyle = '#fff';
4469 this.context.stroke();
4470 }
4471
4472 this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
4473
4474 // Draw play/pause icons
4475 if( this.playing ) {
4476 this.context.fillStyle = '#fff';
4477 this.context.fillRect( 0, 0, iconSize / 2 - 2, iconSize );
4478 this.context.fillRect( iconSize / 2 + 2, 0, iconSize / 2 - 2, iconSize );
4479 }
4480 else {
4481 this.context.beginPath();
4482 this.context.translate( 2, 0 );
4483 this.context.moveTo( 0, 0 );
4484 this.context.lineTo( iconSize - 2, iconSize / 2 );
4485 this.context.lineTo( 0, iconSize );
4486 this.context.fillStyle = '#fff';
4487 this.context.fill();
4488 }
4489
4490 this.context.restore();
4491
4492 };
4493
4494 Playback.prototype.on = function( type, listener ) {
4495 this.canvas.addEventListener( type, listener, false );
4496 };
4497
4498 Playback.prototype.off = function( type, listener ) {
4499 this.canvas.removeEventListener( type, listener, false );
4500 };
4501
4502 Playback.prototype.destroy = function() {
4503
4504 this.playing = false;
4505
4506 if( this.canvas.parentNode ) {
4507 this.container.removeChild( this.canvas );
4508 }
4509
4510 };
4511
4512
4513 // --------------------------------------------------------------------//
4514 // ------------------------------- API --------------------------------//
4515 // --------------------------------------------------------------------//
4516
4517
4518 Reveal = {
4519 initialize: initialize,
4520 configure: configure,
4521 sync: sync,
4522
4523 // Navigation methods
4524 slide: slide,
4525 left: navigateLeft,
4526 right: navigateRight,
4527 up: navigateUp,
4528 down: navigateDown,
4529 prev: navigatePrev,
4530 next: navigateNext,
4531
4532 // Fragment methods
4533 navigateFragment: navigateFragment,
4534 prevFragment: previousFragment,
4535 nextFragment: nextFragment,
4536
4537 // Deprecated aliases
4538 navigateTo: slide,
4539 navigateLeft: navigateLeft,
4540 navigateRight: navigateRight,
4541 navigateUp: navigateUp,
4542 navigateDown: navigateDown,
4543 navigatePrev: navigatePrev,
4544 navigateNext: navigateNext,
4545
4546 // Forces an update in slide layout
4547 layout: layout,
4548
4549 // Returns an object with the available routes as booleans (left/right/top/bottom)
4550 availableRoutes: availableRoutes,
4551
4552 // Returns an object with the available fragments as booleans (prev/next)
4553 availableFragments: availableFragments,
4554
4555 // Toggles the overview mode on/off
4556 toggleOverview: toggleOverview,
4557
4558 // Toggles the "black screen" mode on/off
4559 togglePause: togglePause,
4560
4561 // Toggles the auto slide mode on/off
4562 toggleAutoSlide: toggleAutoSlide,
4563
4564 // State checks
4565 isOverview: isOverview,
4566 isPaused: isPaused,
4567 isAutoSliding: isAutoSliding,
4568
4569 // Adds or removes all internal event listeners (such as keyboard)
4570 addEventListeners: addEventListeners,
4571 removeEventListeners: removeEventListeners,
4572
4573 // Facility for persisting and restoring the presentation state
4574 getState: getState,
4575 setState: setState,
4576
4577 // Presentation progress on range of 0-1
4578 getProgress: getProgress,
4579
4580 // Returns the indices of the current, or specified, slide
4581 getIndices: getIndices,
4582
4583 getTotalSlides: getTotalSlides,
4584
4585 // Returns the slide element at the specified index
4586 getSlide: getSlide,
4587
4588 // Returns the slide background element at the specified index
4589 getSlideBackground: getSlideBackground,
4590
4591 // Returns the speaker notes string for a slide, or null
4592 getSlideNotes: getSlideNotes,
4593
4594 // Returns the previous slide element, may be null
4595 getPreviousSlide: function() {
4596 return previousSlide;
4597 },
4598
4599 // Returns the current slide element
4600 getCurrentSlide: function() {
4601 return currentSlide;
4602 },
4603
4604 // Returns the current scale of the presentation content
4605 getScale: function() {
4606 return scale;
4607 },
4608
4609 // Returns the current configuration object
4610 getConfig: function() {
4611 return config;
4612 },
4613
4614 // Helper method, retrieves query string as a key/value hash
4615 getQueryHash: function() {
4616 var query = {};
4617
4618 location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
4619 query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
4620 } );
4621
4622 // Basic deserialization
4623 for( var i in query ) {
4624 var value = query[ i ];
4625
4626 query[ i ] = deserialize( unescape( value ) );
4627 }
4628
4629 return query;
4630 },
4631
4632 // Returns true if we're currently on the first slide
4633 isFirstSlide: function() {
4634 return ( indexh === 0 && indexv === 0 );
4635 },
4636
4637 // Returns true if we're currently on the last slide
4638 isLastSlide: function() {
4639 if( currentSlide ) {
4640 // Does this slide has next a sibling?
4641 if( currentSlide.nextElementSibling ) return false;
4642
4643 // If it's vertical, does its parent have a next sibling?
4644 if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
4645
4646 return true;
4647 }
4648
4649 return false;
4650 },
4651
4652 // Checks if reveal.js has been loaded and is ready for use
4653 isReady: function() {
4654 return loaded;
4655 },
4656
4657 // Forward event binding to the reveal DOM element
4658 addEventListener: function( type, listener, useCapture ) {
4659 if( 'addEventListener' in window ) {
4660 ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
4661 }
4662 },
4663 removeEventListener: function( type, listener, useCapture ) {
4664 if( 'addEventListener' in window ) {
4665 ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
4666 }
4667 },
4668
4669 // Programatically triggers a keyboard event
4670 triggerKey: function( keyCode ) {
4671 onDocumentKeyDown( { keyCode: keyCode } );
4672 }
4673 };
4674
4675 return Reveal;
4676
4677}));