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