| /*! | 
|  * reveal.js | 
|  * http://lab.hakim.se/reveal-js | 
|  * MIT licensed | 
|  * | 
|  * Copyright (C) 2013 Hakim El Hattab, http://hakim.se | 
|  */ | 
| var Reveal = (function(){ | 
|   | 
|     'use strict'; | 
|   | 
|     var SLIDES_SELECTOR = '.reveal .slides section', | 
|         HORIZONTAL_SLIDES_SELECTOR = '.reveal .slides>section', | 
|         VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section', | 
|         HOME_SLIDE_SELECTOR = '.reveal .slides>section:first-child', | 
|   | 
|         // Configurations defaults, can be overridden at initialization time | 
|         config = { | 
|   | 
|             // The "normal" size of the presentation, aspect ratio will be preserved | 
|             // when the presentation is scaled to fit different resolutions | 
|             width: 960, | 
|             height: 700, | 
|   | 
|             // Factor of the display size that should remain empty around the content | 
|             margin: 0.1, | 
|   | 
|             // Bounds for smallest/largest possible scale to apply to content | 
|             minScale: 0.2, | 
|             maxScale: 1.0, | 
|   | 
|             // Display controls in the bottom right corner | 
|             controls: true, | 
|   | 
|             // Display a presentation progress bar | 
|             progress: true, | 
|   | 
|             // Push each slide change to the browser history | 
|             history: false, | 
|   | 
|             // Enable keyboard shortcuts for navigation | 
|             keyboard: true, | 
|   | 
|             // Enable the slide overview mode | 
|             overview: true, | 
|   | 
|             // Vertical centring of slides | 
|             center: true, | 
|   | 
|             // Enables touch navigation on devices with touch input | 
|             touch: true, | 
|   | 
|             // Loop the presentation | 
|             loop: false, | 
|   | 
|             // Change the presentation direction to be RTL | 
|             rtl: false, | 
|   | 
|             // Turns fragments on and off globally | 
|             fragments: true, | 
|   | 
|             // Number of milliseconds between automatically proceeding to the | 
|             // next slide, disabled when set to 0, this value can be overwritten | 
|             // by using a data-autoslide attribute on your slides | 
|             autoSlide: 0, | 
|   | 
|             // Enable slide navigation via mouse wheel | 
|             mouseWheel: false, | 
|   | 
|             // Apply a 3D roll to links on hover | 
|             rollingLinks: true, | 
|   | 
|             // Opens links in an iframe preview overlay | 
|             previewLinks: false, | 
|   | 
|             // Theme (see /css/theme) | 
|             theme: null, | 
|   | 
|             // Transition style | 
|             transition: 'default', // default/cube/page/concave/zoom/linear/fade/none | 
|   | 
|             // Transition speed | 
|             transitionSpeed: 'default', // default/fast/slow | 
|   | 
|             // Transition style for full page slide backgrounds | 
|             backgroundTransition: 'default', // default/linear | 
|   | 
|             // Script dependencies to load | 
|             dependencies: [] | 
|         }, | 
|   | 
|         // The current auto-slide duration | 
|         autoSlide = 0, | 
|   | 
|         // The horizontal and vertical index of the currently active slide | 
|         indexh = 0, | 
|         indexv = 0, | 
|   | 
|         // The previous and current slide HTML elements | 
|         previousSlide, | 
|         currentSlide, | 
|   | 
|         // Slides may hold a data-state attribute which we pick up and apply | 
|         // as a class to the body. This list contains the combined state of | 
|         // all current slides. | 
|         state = [], | 
|   | 
|         // The current scale of the presentation (see width/height config) | 
|         scale = 1, | 
|   | 
|         // Cached references to DOM elements | 
|         dom = {}, | 
|   | 
|         // Detect support for CSS 3D transforms | 
|         supports3DTransforms =  'WebkitPerspective' in document.body.style || | 
|                                 'MozPerspective' in document.body.style || | 
|                                 'msPerspective' in document.body.style || | 
|                                 'OPerspective' in document.body.style || | 
|                                 'perspective' in document.body.style, | 
|   | 
|         // Detect support for CSS 2D transforms | 
|         supports2DTransforms =  'WebkitTransform' in document.body.style || | 
|                                 'MozTransform' in document.body.style || | 
|                                 'msTransform' in document.body.style || | 
|                                 'OTransform' in document.body.style || | 
|                                 'transform' in document.body.style, | 
|   | 
|         // Throttles mouse wheel navigation | 
|         lastMouseWheelStep = 0, | 
|   | 
|         // An interval used to automatically move on to the next slide | 
|         autoSlideTimeout = 0, | 
|   | 
|         // Delays updates to the URL due to a Chrome thumbnailer bug | 
|         writeURLTimeout = 0, | 
|   | 
|         // A delay used to activate the overview mode | 
|         activateOverviewTimeout = 0, | 
|   | 
|         // A delay used to deactivate the overview mode | 
|         deactivateOverviewTimeout = 0, | 
|   | 
|         // Flags if the interaction event listeners are bound | 
|         eventsAreBound = false, | 
|   | 
|         // Holds information about the currently ongoing touch input | 
|         touch = { | 
|             startX: 0, | 
|             startY: 0, | 
|             startSpan: 0, | 
|             startCount: 0, | 
|             handled: false, | 
|             threshold: 80 | 
|         }; | 
|   | 
|     /** | 
|      * Starts up the presentation if the client is capable. | 
|      */ | 
|     function initialize( options ) { | 
|   | 
|         if( !supports2DTransforms && !supports3DTransforms ) { | 
|             document.body.setAttribute( 'class', 'no-transforms' ); | 
|   | 
|             // If the browser doesn't support core features we won't be | 
|             // using JavaScript to control the presentation | 
|             return; | 
|         } | 
|   | 
|         // Force a layout when the whole page, incl fonts, has loaded | 
|         window.addEventListener( 'load', layout, false ); | 
|   | 
|         // Copy options over to our config object | 
|         extend( config, options ); | 
|   | 
|         // Hide the address bar in mobile browsers | 
|         hideAddressBar(); | 
|   | 
|         // Loads the dependencies and continues to #start() once done | 
|         load(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Finds and stores references to DOM elements which are | 
|      * required by the presentation. If a required element is | 
|      * not found, it is created. | 
|      */ | 
|     function setupDOM() { | 
|   | 
|         // Cache references to key DOM elements | 
|         dom.theme = document.querySelector( '#theme' ); | 
|         dom.wrapper = document.querySelector( '.reveal' ); | 
|         dom.slides = document.querySelector( '.reveal .slides' ); | 
|   | 
|         // Background element | 
|         if( !document.querySelector( '.reveal .backgrounds' ) ) { | 
|             dom.background = document.createElement( 'div' ); | 
|             dom.background.classList.add( 'backgrounds' ); | 
|             dom.wrapper.appendChild( dom.background ); | 
|         } | 
|   | 
|         // Progress bar | 
|         if( !dom.wrapper.querySelector( '.progress' ) ) { | 
|             var progressElement = document.createElement( 'div' ); | 
|             progressElement.classList.add( 'progress' ); | 
|             progressElement.innerHTML = '<span></span>'; | 
|             dom.wrapper.appendChild( progressElement ); | 
|         } | 
|   | 
|         // Arrow controls | 
|         if( !dom.wrapper.querySelector( '.controls' ) ) { | 
|             var controlsElement = document.createElement( 'aside' ); | 
|             controlsElement.classList.add( 'controls' ); | 
|             controlsElement.innerHTML = '<div class="navigate-left"></div>' + | 
|                                         '<div class="navigate-right"></div>' + | 
|                                         '<div class="navigate-up"></div>' + | 
|                                         '<div class="navigate-down"></div>'; | 
|             dom.wrapper.appendChild( controlsElement ); | 
|         } | 
|   | 
|         // State background element [DEPRECATED] | 
|         if( !dom.wrapper.querySelector( '.state-background' ) ) { | 
|             var stateBackgroundElement = document.createElement( 'div' ); | 
|             stateBackgroundElement.classList.add( 'state-background' ); | 
|             dom.wrapper.appendChild( stateBackgroundElement ); | 
|         } | 
|   | 
|         // Overlay graphic which is displayed during the paused mode | 
|         if( !dom.wrapper.querySelector( '.pause-overlay' ) ) { | 
|             var pausedElement = document.createElement( 'div' ); | 
|             pausedElement.classList.add( 'pause-overlay' ); | 
|             dom.wrapper.appendChild( pausedElement ); | 
|         } | 
|   | 
|         // Cache references to elements | 
|         dom.progress = document.querySelector( '.reveal .progress' ); | 
|         dom.progressbar = document.querySelector( '.reveal .progress span' ); | 
|   | 
|         if ( config.controls ) { | 
|             dom.controls = document.querySelector( '.reveal .controls' ); | 
|   | 
|             // There can be multiple instances of controls throughout the page | 
|             dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) ); | 
|             dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) ); | 
|             dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) ); | 
|             dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) ); | 
|             dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) ); | 
|             dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Creates the slide background elements and appends them | 
|      * to the background container. One element is created per | 
|      * slide no matter if the given slide has visible background. | 
|      */ | 
|     function createBackgrounds() { | 
|   | 
|         if( isPrintingPDF() ) { | 
|             document.body.classList.add( 'print-pdf' ); | 
|         } | 
|   | 
|         // Clear prior backgrounds | 
|         dom.background.innerHTML = ''; | 
|         dom.background.classList.add( 'no-transition' ); | 
|   | 
|         // Helper method for creating a background element for the | 
|         // given slide | 
|         function _createBackground( slide, container ) { | 
|   | 
|             var data = { | 
|                 background: slide.getAttribute( 'data-background' ), | 
|                 backgroundSize: slide.getAttribute( 'data-background-size' ), | 
|                 backgroundColor: slide.getAttribute( 'data-background-color' ), | 
|                 backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), | 
|                 backgroundPosition: slide.getAttribute( 'data-background-position' ), | 
|                 backgroundTransition: slide.getAttribute( 'data-background-transition' ) | 
|             }; | 
|   | 
|             var element = document.createElement( 'div' ); | 
|             element.className = 'slide-background'; | 
|   | 
|             if( data.background ) { | 
|                 // Auto-wrap image urls in url(...) | 
|                 if( /^(http|file|\/\/)/gi.test( data.background ) ) { | 
|                     element.style.backgroundImage = 'url('+ data.background +')'; | 
|                 } | 
|                 else { | 
|                     element.style.background = data.background; | 
|                 } | 
|             } | 
|   | 
|             // Additional and optional background properties | 
|             if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; | 
|             if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; | 
|             if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat; | 
|             if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition; | 
|             if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); | 
|   | 
|             container.appendChild( element ); | 
|   | 
|             return element; | 
|   | 
|         } | 
|   | 
|         // Iterate over all horizontal slides | 
|         toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) { | 
|   | 
|             var backgroundStack; | 
|   | 
|             if( isPrintingPDF() ) { | 
|                 backgroundStack = _createBackground( slideh, slideh ); | 
|             } | 
|             else { | 
|                 backgroundStack = _createBackground( slideh, dom.background ); | 
|             } | 
|   | 
|             // Iterate over all vertical slides | 
|             toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) { | 
|   | 
|                 if( isPrintingPDF() ) { | 
|                     _createBackground( slidev, slidev ); | 
|                 } | 
|                 else { | 
|                     _createBackground( slidev, backgroundStack ); | 
|                 } | 
|   | 
|             } ); | 
|   | 
|         } ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Hides the address bar if we're on a mobile device. | 
|      */ | 
|     function hideAddressBar() { | 
|   | 
|         if( /iphone|ipod|android/gi.test( navigator.userAgent ) && !/crios/gi.test( navigator.userAgent ) ) { | 
|             // Events that should trigger the address bar to hide | 
|             window.addEventListener( 'load', removeAddressBar, false ); | 
|             window.addEventListener( 'orientationchange', removeAddressBar, false ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Loads the dependencies of reveal.js. Dependencies are | 
|      * defined via the configuration option 'dependencies' | 
|      * and will be loaded prior to starting/binding reveal.js. | 
|      * Some dependencies may have an 'async' flag, if so they | 
|      * will load after reveal.js has been started up. | 
|      */ | 
|     function load() { | 
|   | 
|         var scripts = [], | 
|             scriptsAsync = []; | 
|   | 
|         for( var i = 0, len = config.dependencies.length; i < len; i++ ) { | 
|             var s = config.dependencies[i]; | 
|   | 
|             // Load if there's no condition or the condition is truthy | 
|             if( !s.condition || s.condition() ) { | 
|                 if( s.async ) { | 
|                     scriptsAsync.push( s.src ); | 
|                 } | 
|                 else { | 
|                     scripts.push( s.src ); | 
|                 } | 
|   | 
|                 // Extension may contain callback functions | 
|                 if( typeof s.callback === 'function' ) { | 
|                     head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], s.callback ); | 
|                 } | 
|             } | 
|         } | 
|   | 
|         // Called once synchronous scripts finish loading | 
|         function proceed() { | 
|             if( scriptsAsync.length ) { | 
|                 // Load asynchronous scripts | 
|                 head.js.apply( null, scriptsAsync ); | 
|             } | 
|   | 
|             start(); | 
|         } | 
|   | 
|         if( scripts.length ) { | 
|             head.ready( proceed ); | 
|   | 
|             // Load synchronous scripts | 
|             head.js.apply( null, scripts ); | 
|         } | 
|         else { | 
|             proceed(); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Starts up reveal.js by binding input events and navigating | 
|      * to the current URL deeplink if there is one. | 
|      */ | 
|     function start() { | 
|   | 
|         // Make sure we've got all the DOM elements we need | 
|         setupDOM(); | 
|   | 
|         // Updates the presentation to match the current configuration values | 
|         configure(); | 
|   | 
|         // Read the initial hash | 
|         readURL(); | 
|   | 
|         // Notify listeners that the presentation is ready but use a 1ms | 
|         // timeout to ensure it's not fired synchronously after #initialize() | 
|         setTimeout( function() { | 
|             dispatchEvent( 'ready', { | 
|                 'indexh': indexh, | 
|                 'indexv': indexv, | 
|                 'currentSlide': currentSlide | 
|             } ); | 
|         }, 1 ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Applies the configuration settings from the config | 
|      * object. May be called multiple times. | 
|      */ | 
|     function configure( options ) { | 
|   | 
|         dom.wrapper.classList.remove( config.transition ); | 
|   | 
|         // New config options may be passed when this method | 
|         // is invoked through the API after initialization | 
|         if( typeof options === 'object' ) extend( config, options ); | 
|   | 
|         // Force linear transition based on browser capabilities | 
|         if( supports3DTransforms === false ) config.transition = 'linear'; | 
|   | 
|         dom.wrapper.classList.add( config.transition ); | 
|   | 
|         dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed ); | 
|         dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition ); | 
|   | 
|         if( dom.controls ) { | 
|             dom.controls.style.display = ( config.controls && dom.controls ) ? 'block' : 'none'; | 
|         } | 
|   | 
|         if( dom.progress ) { | 
|             dom.progress.style.display = ( config.progress && dom.progress ) ? 'block' : 'none'; | 
|         } | 
|   | 
|         if( config.rtl ) { | 
|             dom.wrapper.classList.add( 'rtl' ); | 
|         } | 
|         else { | 
|             dom.wrapper.classList.remove( 'rtl' ); | 
|         } | 
|   | 
|         if( config.center ) { | 
|             dom.wrapper.classList.add( 'center' ); | 
|         } | 
|         else { | 
|             dom.wrapper.classList.remove( 'center' ); | 
|         } | 
|   | 
|         if( config.mouseWheel ) { | 
|             document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF | 
|             document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); | 
|         } | 
|         else { | 
|             document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF | 
|             document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false ); | 
|         } | 
|   | 
|         // Rolling 3D links | 
|         if( config.rollingLinks ) { | 
|             enableRollingLinks(); | 
|         } | 
|         else { | 
|             disableRollingLinks(); | 
|         } | 
|   | 
|         // Iframe link previews | 
|         if( config.previewLinks ) { | 
|             enablePreviewLinks(); | 
|         } | 
|         else { | 
|             disablePreviewLinks(); | 
|             enablePreviewLinks( '[data-preview-link]' ); | 
|         } | 
|   | 
|         // Load the theme in the config, if it's not already loaded | 
|         if( config.theme && dom.theme ) { | 
|             var themeURL = dom.theme.getAttribute( 'href' ); | 
|             var themeFinder = /[^\/]*?(?=\.css)/; | 
|             var themeName = themeURL.match(themeFinder)[0]; | 
|   | 
|             if(  config.theme !== themeName ) { | 
|                 themeURL = themeURL.replace(themeFinder, config.theme); | 
|                 dom.theme.setAttribute( 'href', themeURL ); | 
|             } | 
|         } | 
|   | 
|         sync(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Binds all event listeners. | 
|      */ | 
|     function addEventListeners() { | 
|   | 
|         eventsAreBound = true; | 
|   | 
|         window.addEventListener( 'hashchange', onWindowHashChange, false ); | 
|         window.addEventListener( 'resize', onWindowResize, false ); | 
|   | 
|         if( config.touch ) { | 
|             dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); | 
|             dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); | 
|             dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); | 
|   | 
|             // Support pointer-style touch interaction as well | 
|             if( window.navigator.msPointerEnabled ) { | 
|                 dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false ); | 
|                 dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); | 
|                 dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); | 
|             } | 
|         } | 
|   | 
|         if( config.keyboard ) { | 
|             document.addEventListener( 'keydown', onDocumentKeyDown, false ); | 
|         } | 
|   | 
|         if ( config.progress && dom.progress ) { | 
|             dom.progress.addEventListener( 'click', onProgressClicked, false ); | 
|         } | 
|   | 
|         if ( config.controls && dom.controls ) { | 
|             [ 'touchstart', 'click' ].forEach( function( eventName ) { | 
|                 dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } ); | 
|                 dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } ); | 
|                 dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } ); | 
|                 dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } ); | 
|                 dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } ); | 
|                 dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } ); | 
|             } ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Unbinds all event listeners. | 
|      */ | 
|     function removeEventListeners() { | 
|   | 
|         eventsAreBound = false; | 
|   | 
|         document.removeEventListener( 'keydown', onDocumentKeyDown, false ); | 
|         window.removeEventListener( 'hashchange', onWindowHashChange, false ); | 
|         window.removeEventListener( 'resize', onWindowResize, false ); | 
|   | 
|         dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false ); | 
|         dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); | 
|         dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); | 
|   | 
|         if( window.navigator.msPointerEnabled ) { | 
|             dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); | 
|             dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); | 
|             dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); | 
|         } | 
|   | 
|         if ( config.progress && dom.progress ) { | 
|             dom.progress.removeEventListener( 'click', onProgressClicked, false ); | 
|         } | 
|   | 
|         if ( config.controls && dom.controls ) { | 
|             [ 'touchstart', 'click' ].forEach( function( eventName ) { | 
|                 dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } ); | 
|                 dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } ); | 
|                 dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } ); | 
|                 dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } ); | 
|                 dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } ); | 
|                 dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } ); | 
|             } ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Extend object a with the properties of object b. | 
|      * If there's a conflict, object b takes precedence. | 
|      */ | 
|     function extend( a, b ) { | 
|   | 
|         for( var i in b ) { | 
|             a[ i ] = b[ i ]; | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Converts the target object to an array. | 
|      */ | 
|     function toArray( o ) { | 
|   | 
|         return Array.prototype.slice.call( o ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Measures the distance in pixels between point a | 
|      * and point b. | 
|      * | 
|      * @param {Object} a point with x/y properties | 
|      * @param {Object} b point with x/y properties | 
|      */ | 
|     function distanceBetween( a, b ) { | 
|   | 
|         var dx = a.x - b.x, | 
|             dy = a.y - b.y; | 
|   | 
|         return Math.sqrt( dx*dx + dy*dy ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Retrieves the height of the given element by looking | 
|      * at the position and height of its immediate children. | 
|      */ | 
|     function getAbsoluteHeight( element ) { | 
|   | 
|         var height = 0; | 
|   | 
|         if( element ) { | 
|             var absoluteChildren = 0; | 
|   | 
|             toArray( element.childNodes ).forEach( function( child ) { | 
|   | 
|                 if( typeof child.offsetTop === 'number' && child.style ) { | 
|                     // Count # of abs children | 
|                     if( child.style.position === 'absolute' ) { | 
|                         absoluteChildren += 1; | 
|                     } | 
|   | 
|                     height = Math.max( height, child.offsetTop + child.offsetHeight ); | 
|                 } | 
|   | 
|             } ); | 
|   | 
|             // If there are no absolute children, use offsetHeight | 
|             if( absoluteChildren === 0 ) { | 
|                 height = element.offsetHeight; | 
|             } | 
|   | 
|         } | 
|   | 
|         return height; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Checks if this instance is being used to print a PDF. | 
|      */ | 
|     function isPrintingPDF() { | 
|   | 
|         return ( /print-pdf/gi ).test( window.location.search ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Causes the address bar to hide on mobile devices, | 
|      * more vertical space ftw. | 
|      */ | 
|     function removeAddressBar() { | 
|   | 
|         if( window.orientation === 0 ) { | 
|             document.documentElement.style.overflow = 'scroll'; | 
|             document.body.style.height = '120%'; | 
|         } | 
|         else { | 
|             document.documentElement.style.overflow = ''; | 
|             document.body.style.height = '100%'; | 
|         } | 
|   | 
|         setTimeout( function() { | 
|             window.scrollTo( 0, 1 ); | 
|         }, 10 ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Dispatches an event of the specified type from the | 
|      * reveal DOM element. | 
|      */ | 
|     function dispatchEvent( type, properties ) { | 
|   | 
|         var event = document.createEvent( "HTMLEvents", 1, 2 ); | 
|         event.initEvent( type, true, true ); | 
|         extend( event, properties ); | 
|         dom.wrapper.dispatchEvent( event ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Wrap all links in 3D goodness. | 
|      */ | 
|     function enableRollingLinks() { | 
|   | 
|         if( supports3DTransforms && !( 'msPerspective' in document.body.style ) ) { | 
|             var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a:not(.image)' ); | 
|   | 
|             for( var i = 0, len = anchors.length; i < len; i++ ) { | 
|                 var anchor = anchors[i]; | 
|   | 
|                 if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) { | 
|                     var span = document.createElement('span'); | 
|                     span.setAttribute('data-title', anchor.text); | 
|                     span.innerHTML = anchor.innerHTML; | 
|   | 
|                     anchor.classList.add( 'roll' ); | 
|                     anchor.innerHTML = ''; | 
|                     anchor.appendChild(span); | 
|                 } | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Unwrap all 3D links. | 
|      */ | 
|     function disableRollingLinks() { | 
|   | 
|         var anchors = document.querySelectorAll( SLIDES_SELECTOR + ' a.roll' ); | 
|   | 
|         for( var i = 0, len = anchors.length; i < len; i++ ) { | 
|             var anchor = anchors[i]; | 
|             var span = anchor.querySelector( 'span' ); | 
|   | 
|             if( span ) { | 
|                 anchor.classList.remove( 'roll' ); | 
|                 anchor.innerHTML = span.innerHTML; | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Bind preview frame links. | 
|      */ | 
|     function enablePreviewLinks( selector ) { | 
|   | 
|         var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) ); | 
|   | 
|         anchors.forEach( function( element ) { | 
|             if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { | 
|                 element.addEventListener( 'click', onPreviewLinkClicked, false ); | 
|             } | 
|         } ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Unbind preview frame links. | 
|      */ | 
|     function disablePreviewLinks() { | 
|   | 
|         var anchors = toArray( document.querySelectorAll( 'a' ) ); | 
|   | 
|         anchors.forEach( function( element ) { | 
|             if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { | 
|                 element.removeEventListener( 'click', onPreviewLinkClicked, false ); | 
|             } | 
|         } ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Opens a preview window for the target URL. | 
|      */ | 
|     function openPreview( url ) { | 
|   | 
|         closePreview(); | 
|   | 
|         dom.preview = document.createElement( 'div' ); | 
|         dom.preview.classList.add( 'preview-link-overlay' ); | 
|         dom.wrapper.appendChild( dom.preview ); | 
|   | 
|         dom.preview.innerHTML = [ | 
|             '<header>', | 
|                 '<a class="close" href="#"><span class="icon"></span></a>', | 
|                 '<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>', | 
|             '</header>', | 
|             '<div class="spinner"></div>', | 
|             '<div class="viewport">', | 
|                 '<iframe src="'+ url +'"></iframe>', | 
|             '</div>' | 
|         ].join(''); | 
|   | 
|         dom.preview.querySelector( 'iframe' ).addEventListener( 'load', function( event ) { | 
|             dom.preview.classList.add( 'loaded' ); | 
|         }, false ); | 
|   | 
|         dom.preview.querySelector( '.close' ).addEventListener( 'click', function( event ) { | 
|             closePreview(); | 
|             event.preventDefault(); | 
|         }, false ); | 
|   | 
|         dom.preview.querySelector( '.external' ).addEventListener( 'click', function( event ) { | 
|             closePreview(); | 
|         }, false ); | 
|   | 
|         setTimeout( function() { | 
|             dom.preview.classList.add( 'visible' ); | 
|         }, 1 ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Closes the iframe preview window. | 
|      */ | 
|     function closePreview() { | 
|   | 
|         if( dom.preview ) { | 
|             dom.preview.setAttribute( 'src', '' ); | 
|             dom.preview.parentNode.removeChild( dom.preview ); | 
|             dom.preview = null; | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Return a sorted fragments list, ordered by an increasing | 
|      * "data-fragment-index" attribute. | 
|      * | 
|      * Fragments will be revealed in the order that they are returned by | 
|      * this function, so you can use the index attributes to control the | 
|      * order of fragment appearance. | 
|      * | 
|      * To maintain a sensible default fragment order, fragments are presumed | 
|      * to be passed in document order. This function adds a "fragment-index" | 
|      * attribute to each node if such an attribute is not already present, | 
|      * and sets that attribute to an integer value which is the position of | 
|      * the fragment within the fragments list. | 
|      */ | 
|     function sortFragments( fragments ) { | 
|   | 
|         var a = toArray( fragments ); | 
|   | 
|         a.forEach( function( el, idx ) { | 
|             if( !el.hasAttribute( 'data-fragment-index' ) ) { | 
|                 el.setAttribute( 'data-fragment-index', idx ); | 
|             } | 
|         } ); | 
|   | 
|         a.sort( function( l, r ) { | 
|             return l.getAttribute( 'data-fragment-index' ) - r.getAttribute( 'data-fragment-index'); | 
|         } ); | 
|   | 
|         return a; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Applies JavaScript-controlled layout rules to the | 
|      * presentation. | 
|      */ | 
|     function layout() { | 
|   | 
|         if( dom.wrapper && !isPrintingPDF() ) { | 
|   | 
|             // Available space to scale within | 
|             var availableWidth = dom.wrapper.offsetWidth, | 
|                 availableHeight = dom.wrapper.offsetHeight; | 
|   | 
|             // Reduce available space by margin | 
|             availableWidth -= ( availableHeight * config.margin ); | 
|             availableHeight -= ( availableHeight * config.margin ); | 
|   | 
|             // Dimensions of the content | 
|             var slideWidth = config.width, | 
|                 slideHeight = config.height; | 
|   | 
|             // Slide width may be a percentage of available width | 
|             if( typeof slideWidth === 'string' && /%$/.test( slideWidth ) ) { | 
|                 slideWidth = parseInt( slideWidth, 10 ) / 100 * availableWidth; | 
|             } | 
|   | 
|             // Slide height may be a percentage of available height | 
|             if( typeof slideHeight === 'string' && /%$/.test( slideHeight ) ) { | 
|                 slideHeight = parseInt( slideHeight, 10 ) / 100 * availableHeight; | 
|             } | 
|   | 
|             dom.slides.style.width = slideWidth + 'px'; | 
|             dom.slides.style.height = slideHeight + 'px'; | 
|   | 
|             // Determine scale of content to fit within available space | 
|             scale = Math.min( availableWidth / slideWidth, availableHeight / slideHeight ); | 
|   | 
|             // Respect max/min scale settings | 
|             scale = Math.max( scale, config.minScale ); | 
|             scale = Math.min( scale, config.maxScale ); | 
|   | 
|             // Prefer applying scale via zoom since Chrome blurs scaled content | 
|             // with nested transforms | 
|             if( typeof dom.slides.style.zoom !== 'undefined' && !navigator.userAgent.match( /(iphone|ipod|ipad|android)/gi ) ) { | 
|                 dom.slides.style.zoom = scale; | 
|             } | 
|             // Apply scale transform as a fallback | 
|             else { | 
|                 var transform = 'translate(-50%, -50%) scale('+ scale +') translate(50%, 50%)'; | 
|   | 
|                 dom.slides.style.WebkitTransform = transform; | 
|                 dom.slides.style.MozTransform = transform; | 
|                 dom.slides.style.msTransform = transform; | 
|                 dom.slides.style.OTransform = transform; | 
|                 dom.slides.style.transform = transform; | 
|             } | 
|   | 
|             // Select all slides, vertical and horizontal | 
|             var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) ); | 
|   | 
|             for( var i = 0, len = slides.length; i < len; i++ ) { | 
|                 var slide = slides[ i ]; | 
|   | 
|                 // Don't bother updating invisible slides | 
|                 if( slide.style.display === 'none' ) { | 
|                     continue; | 
|                 } | 
|   | 
|                 if( config.center ) { | 
|                     // Vertical stacks are not centred since their section | 
|                     // children will be | 
|                     if( slide.classList.contains( 'stack' ) ) { | 
|                         slide.style.top = 0; | 
|                     } | 
|                     else { | 
|                         slide.style.top = Math.max( - ( getAbsoluteHeight( slide ) / 2 ) - 20, -slideHeight / 2 ) + 'px'; | 
|                     } | 
|                 } | 
|                 else { | 
|                     slide.style.top = ''; | 
|                 } | 
|   | 
|             } | 
|   | 
|             updateProgress(); | 
|   | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Stores the vertical index of a stack so that the same | 
|      * vertical slide can be selected when navigating to and | 
|      * from the stack. | 
|      * | 
|      * @param {HTMLElement} stack The vertical stack element | 
|      * @param {int} v Index to memorize | 
|      */ | 
|     function setPreviousVerticalIndex( stack, v ) { | 
|   | 
|         if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) { | 
|             stack.setAttribute( 'data-previous-indexv', v || 0 ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Retrieves the vertical index which was stored using | 
|      * #setPreviousVerticalIndex() or 0 if no previous index | 
|      * exists. | 
|      * | 
|      * @param {HTMLElement} stack The vertical stack element | 
|      */ | 
|     function getPreviousVerticalIndex( stack ) { | 
|   | 
|         if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) { | 
|             // Prefer manually defined start-indexv | 
|             var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv'; | 
|   | 
|             return parseInt( stack.getAttribute( attributeName ) || 0, 10 ); | 
|         } | 
|   | 
|         return 0; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Displays the overview of slides (quick nav) by | 
|      * scaling down and arranging all slide elements. | 
|      * | 
|      * Experimental feature, might be dropped if perf | 
|      * can't be improved. | 
|      */ | 
|     function activateOverview() { | 
|   | 
|         // Only proceed if enabled in config | 
|         if( config.overview ) { | 
|   | 
|             // Don't auto-slide while in overview mode | 
|             cancelAutoSlide(); | 
|   | 
|             var wasActive = dom.wrapper.classList.contains( 'overview' ); | 
|   | 
|             dom.wrapper.classList.add( 'overview' ); | 
|             dom.wrapper.classList.remove( 'exit-overview' ); | 
|   | 
|             clearTimeout( activateOverviewTimeout ); | 
|             clearTimeout( deactivateOverviewTimeout ); | 
|   | 
|             // Not the pretties solution, but need to let the overview | 
|             // class apply first so that slides are measured accurately | 
|             // before we can position them | 
|             activateOverviewTimeout = setTimeout( function(){ | 
|   | 
|                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); | 
|   | 
|                 for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { | 
|                     var hslide = horizontalSlides[i], | 
|                         hoffset = config.rtl ? -105 : 105, | 
|                         htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * hoffset ) + '%, 0%)'; | 
|   | 
|                     hslide.setAttribute( 'data-index-h', i ); | 
|                     hslide.style.display = 'block'; | 
|                     hslide.style.WebkitTransform = htransform; | 
|                     hslide.style.MozTransform = htransform; | 
|                     hslide.style.msTransform = htransform; | 
|                     hslide.style.OTransform = htransform; | 
|                     hslide.style.transform = htransform; | 
|   | 
|                     if( hslide.classList.contains( 'stack' ) ) { | 
|   | 
|                         var verticalSlides = hslide.querySelectorAll( 'section' ); | 
|   | 
|                         for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { | 
|                             var verticalIndex = i === indexh ? indexv : getPreviousVerticalIndex( hslide ); | 
|   | 
|                             var vslide = verticalSlides[j], | 
|                                 vtransform = 'translate(0%, ' + ( ( j - verticalIndex ) * 105 ) + '%)'; | 
|   | 
|                             vslide.setAttribute( 'data-index-h', i ); | 
|                             vslide.setAttribute( 'data-index-v', j ); | 
|                             vslide.style.display = 'block'; | 
|                             vslide.style.WebkitTransform = vtransform; | 
|                             vslide.style.MozTransform = vtransform; | 
|                             vslide.style.msTransform = vtransform; | 
|                             vslide.style.OTransform = vtransform; | 
|                             vslide.style.transform = vtransform; | 
|   | 
|                             // Navigate to this slide on click | 
|                             vslide.addEventListener( 'click', onOverviewSlideClicked, true ); | 
|                         } | 
|   | 
|                     } | 
|                     else { | 
|   | 
|                         // Navigate to this slide on click | 
|                         hslide.addEventListener( 'click', onOverviewSlideClicked, true ); | 
|   | 
|                     } | 
|                 } | 
|   | 
|                 layout(); | 
|   | 
|                 if( !wasActive ) { | 
|                     // Notify observers of the overview showing | 
|                     dispatchEvent( 'overviewshown', { | 
|                         'indexh': indexh, | 
|                         'indexv': indexv, | 
|                         'currentSlide': currentSlide | 
|                     } ); | 
|                 } | 
|   | 
|             }, 10 ); | 
|   | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Exits the slide overview and enters the currently | 
|      * active slide. | 
|      */ | 
|     function deactivateOverview() { | 
|   | 
|         // Only proceed if enabled in config | 
|         if( config.overview ) { | 
|   | 
|             clearTimeout( activateOverviewTimeout ); | 
|             clearTimeout( deactivateOverviewTimeout ); | 
|   | 
|             dom.wrapper.classList.remove( 'overview' ); | 
|   | 
|             // Temporarily add a class so that transitions can do different things | 
|             // depending on whether they are exiting/entering overview, or just | 
|             // moving from slide to slide | 
|             dom.wrapper.classList.add( 'exit-overview' ); | 
|   | 
|             deactivateOverviewTimeout = setTimeout( function () { | 
|                 dom.wrapper.classList.remove( 'exit-overview' ); | 
|             }, 10); | 
|   | 
|             // Select all slides | 
|             var slides = toArray( document.querySelectorAll( SLIDES_SELECTOR ) ); | 
|   | 
|             for( var i = 0, len = slides.length; i < len; i++ ) { | 
|                 var element = slides[i]; | 
|   | 
|                 element.style.display = ''; | 
|   | 
|                 // Resets all transforms to use the external styles | 
|                 element.style.WebkitTransform = ''; | 
|                 element.style.MozTransform = ''; | 
|                 element.style.msTransform = ''; | 
|                 element.style.OTransform = ''; | 
|                 element.style.transform = ''; | 
|   | 
|                 element.removeEventListener( 'click', onOverviewSlideClicked, true ); | 
|             } | 
|   | 
|             slide( indexh, indexv ); | 
|   | 
|             cueAutoSlide(); | 
|   | 
|             // Notify observers of the overview hiding | 
|             dispatchEvent( 'overviewhidden', { | 
|                 'indexh': indexh, | 
|                 'indexv': indexv, | 
|                 'currentSlide': currentSlide | 
|             } ); | 
|   | 
|         } | 
|     } | 
|   | 
|     /** | 
|      * Toggles the slide overview mode on and off. | 
|      * | 
|      * @param {Boolean} override Optional flag which overrides the | 
|      * toggle logic and forcibly sets the desired state. True means | 
|      * overview is open, false means it's closed. | 
|      */ | 
|     function toggleOverview( override ) { | 
|   | 
|         if( typeof override === 'boolean' ) { | 
|             override ? activateOverview() : deactivateOverview(); | 
|         } | 
|         else { | 
|             isOverview() ? deactivateOverview() : activateOverview(); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Checks if the overview is currently active. | 
|      * | 
|      * @return {Boolean} true if the overview is active, | 
|      * false otherwise | 
|      */ | 
|     function isOverview() { | 
|   | 
|         return dom.wrapper.classList.contains( 'overview' ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Checks if the current or specified slide is vertical | 
|      * (nested within another slide). | 
|      * | 
|      * @param {HTMLElement} slide [optional] The slide to check | 
|      * orientation of | 
|      */ | 
|     function isVerticalSlide( slide ) { | 
|   | 
|         // Prefer slide argument, otherwise use current slide | 
|         slide = slide ? slide : currentSlide; | 
|   | 
|         return slide && !!slide.parentNode.nodeName.match( /section/i ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handling the fullscreen functionality via the fullscreen API | 
|      * | 
|      * @see http://fullscreen.spec.whatwg.org/ | 
|      * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode | 
|      */ | 
|     function enterFullscreen() { | 
|   | 
|         var element = document.body; | 
|   | 
|         // Check which implementation is available | 
|         var requestMethod = element.requestFullScreen || | 
|                             element.webkitRequestFullscreen || | 
|                             element.webkitRequestFullScreen || | 
|                             element.mozRequestFullScreen || | 
|                             element.msRequestFullScreen; | 
|   | 
|         if( requestMethod ) { | 
|             requestMethod.apply( element ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Enters the paused mode which fades everything on screen to | 
|      * black. | 
|      */ | 
|     function pause() { | 
|   | 
|         var wasPaused = dom.wrapper.classList.contains( 'paused' ); | 
|   | 
|         cancelAutoSlide(); | 
|         dom.wrapper.classList.add( 'paused' ); | 
|   | 
|         if( wasPaused === false ) { | 
|             dispatchEvent( 'paused' ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Exits from the paused mode. | 
|      */ | 
|     function resume() { | 
|   | 
|         var wasPaused = dom.wrapper.classList.contains( 'paused' ); | 
|         dom.wrapper.classList.remove( 'paused' ); | 
|   | 
|         cueAutoSlide(); | 
|   | 
|         if( wasPaused ) { | 
|             dispatchEvent( 'resumed' ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Toggles the paused mode on and off. | 
|      */ | 
|     function togglePause() { | 
|   | 
|         if( isPaused() ) { | 
|             resume(); | 
|         } | 
|         else { | 
|             pause(); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Checks if we are currently in the paused mode. | 
|      */ | 
|     function isPaused() { | 
|   | 
|         return dom.wrapper.classList.contains( 'paused' ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Steps from the current point in the presentation to the | 
|      * slide which matches the specified horizontal and vertical | 
|      * indices. | 
|      * | 
|      * @param {int} h Horizontal index of the target slide | 
|      * @param {int} v Vertical index of the target slide | 
|      * @param {int} f Optional index of a fragment within the | 
|      * target slide to activate | 
|      * @param {int} o Optional origin for use in multimaster environments | 
|      */ | 
|     function slide( h, v, f, o ) { | 
|   | 
|         // Remember where we were at before | 
|         previousSlide = currentSlide; | 
|   | 
|         // Query all horizontal slides in the deck | 
|         var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); | 
|   | 
|         // If no vertical index is specified and the upcoming slide is a | 
|         // stack, resume at its previous vertical index | 
|         if( v === undefined ) { | 
|             v = getPreviousVerticalIndex( horizontalSlides[ h ] ); | 
|         } | 
|   | 
|         // If we were on a vertical stack, remember what vertical index | 
|         // it was on so we can resume at the same position when returning | 
|         if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) { | 
|             setPreviousVerticalIndex( previousSlide.parentNode, indexv ); | 
|         } | 
|   | 
|         // Remember the state before this slide | 
|         var stateBefore = state.concat(); | 
|   | 
|         // Reset the state array | 
|         state.length = 0; | 
|   | 
|         var indexhBefore = indexh, | 
|             indexvBefore = indexv; | 
|   | 
|         // Activate and transition to the new slide | 
|         indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); | 
|         indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); | 
|   | 
|         layout(); | 
|   | 
|         // Apply the new state | 
|         stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { | 
|             // Check if this state existed on the previous slide. If it | 
|             // did, we will avoid adding it repeatedly | 
|             for( var j = 0; j < stateBefore.length; j++ ) { | 
|                 if( stateBefore[j] === state[i] ) { | 
|                     stateBefore.splice( j, 1 ); | 
|                     continue stateLoop; | 
|                 } | 
|             } | 
|   | 
|             document.documentElement.classList.add( state[i] ); | 
|   | 
|             // Dispatch custom event matching the state's name | 
|             dispatchEvent( state[i] ); | 
|         } | 
|   | 
|         // Clean up the remains of the previous state | 
|         while( stateBefore.length ) { | 
|             document.documentElement.classList.remove( stateBefore.pop() ); | 
|         } | 
|   | 
|         // If the overview is active, re-activate it to update positions | 
|         if( isOverview() ) { | 
|             activateOverview(); | 
|         } | 
|   | 
|         // Update the URL hash after a delay since updating it mid-transition | 
|         // is likely to cause visual lag | 
|         writeURL( 1500 ); | 
|   | 
|         // Find the current horizontal slide and any possible vertical slides | 
|         // within it | 
|         var currentHorizontalSlide = horizontalSlides[ indexh ], | 
|             currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); | 
|   | 
|         // Store references to the previous and current slides | 
|         currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; | 
|   | 
|   | 
|         // Show fragment, if specified | 
|         if( typeof f !== 'undefined' ) { | 
|             var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); | 
|   | 
|             toArray( fragments ).forEach( function( fragment, indexf ) { | 
|                 if( indexf < f ) { | 
|                     fragment.classList.add( 'visible' ); | 
|                 } | 
|                 else { | 
|                     fragment.classList.remove( 'visible' ); | 
|                 } | 
|             } ); | 
|         } | 
|   | 
|         // Dispatch an event if the slide changed | 
|         var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); | 
|         if( slideChanged ) { | 
|             dispatchEvent( 'slidechanged', { | 
|                 'indexh': indexh, | 
|                 'indexv': indexv, | 
|                 'previousSlide': previousSlide, | 
|                 'currentSlide': currentSlide, | 
|                 'origin': o | 
|             } ); | 
|         } | 
|         else { | 
|             // Ensure that the previous slide is never the same as the current | 
|             previousSlide = null; | 
|         } | 
|   | 
|         // Solves an edge case where the previous slide maintains the | 
|         // 'present' class when navigating between adjacent vertical | 
|         // stacks | 
|         if( previousSlide ) { | 
|             previousSlide.classList.remove( 'present' ); | 
|   | 
|             // Reset all slides upon navigate to home | 
|             // Issue: #285 | 
|             if ( document.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) { | 
|                 // Launch async task | 
|                 setTimeout( function () { | 
|                     var slides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i; | 
|                     for( i in slides ) { | 
|                         if( slides[i] ) { | 
|                             // Reset stack | 
|                             setPreviousVerticalIndex( slides[i], 0 ); | 
|                         } | 
|                     } | 
|                 }, 0 ); | 
|             } | 
|         } | 
|   | 
|         // Handle embedded content | 
|         if( slideChanged ) { | 
|             stopEmbeddedContent( previousSlide ); | 
|             startEmbeddedContent( currentSlide ); | 
|         } | 
|   | 
|         updateControls(); | 
|         updateProgress(); | 
|         updateBackground(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Syncs the presentation with the current DOM. Useful | 
|      * when new slides or control elements are added or when | 
|      * the configuration has changed. | 
|      */ | 
|     function sync() { | 
|   | 
|         // Subscribe to input | 
|         removeEventListeners(); | 
|         addEventListeners(); | 
|   | 
|         // Force a layout to make sure the current config is accounted for | 
|         layout(); | 
|   | 
|         // Reflect the current autoSlide value | 
|         autoSlide = config.autoSlide; | 
|   | 
|         // Start auto-sliding if it's enabled | 
|         cueAutoSlide(); | 
|   | 
|         // Re-create the slide backgrounds | 
|         createBackgrounds(); | 
|   | 
|         updateControls(); | 
|         updateProgress(); | 
|         updateBackground(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Updates one dimension of slides by showing the slide | 
|      * with the specified index. | 
|      * | 
|      * @param {String} selector A CSS selector that will fetch | 
|      * the group of slides we are working with | 
|      * @param {Number} index The index of the slide that should be | 
|      * shown | 
|      * | 
|      * @return {Number} The index of the slide that is now shown, | 
|      * might differ from the passed in index if it was out of | 
|      * bounds. | 
|      */ | 
|     function updateSlides( selector, index ) { | 
|   | 
|         // Select all slides and convert the NodeList result to | 
|         // an array | 
|         var slides = toArray( document.querySelectorAll( selector ) ), | 
|             slidesLength = slides.length; | 
|   | 
|         if( slidesLength ) { | 
|   | 
|             // Should the index loop? | 
|             if( config.loop ) { | 
|                 index %= slidesLength; | 
|   | 
|                 if( index < 0 ) { | 
|                     index = slidesLength + index; | 
|                 } | 
|             } | 
|   | 
|             // Enforce max and minimum index bounds | 
|             index = Math.max( Math.min( index, slidesLength - 1 ), 0 ); | 
|   | 
|             for( var i = 0; i < slidesLength; i++ ) { | 
|                 var element = slides[i]; | 
|   | 
|                 // Optimization; hide all slides that are three or more steps | 
|                 // away from the present slide | 
|                 if( isOverview() === false ) { | 
|                     // The distance loops so that it measures 1 between the first | 
|                     // and last slides | 
|                     var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0; | 
|   | 
|                     element.style.display = distance > 3 ? 'none' : 'block'; | 
|                 } | 
|   | 
|                 var reverse = config.rtl && !isVerticalSlide( element ); | 
|   | 
|                 element.classList.remove( 'past' ); | 
|                 element.classList.remove( 'present' ); | 
|                 element.classList.remove( 'future' ); | 
|   | 
|                 // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute | 
|                 element.setAttribute( 'hidden', '' ); | 
|   | 
|                 if( i < index ) { | 
|                     // Any element previous to index is given the 'past' class | 
|                     element.classList.add( reverse ? 'future' : 'past' ); | 
|                 } | 
|                 else if( i > index ) { | 
|                     // Any element subsequent to index is given the 'future' class | 
|                     element.classList.add( reverse ? 'past' : 'future' ); | 
|                 } | 
|   | 
|                 // If this element contains vertical slides | 
|                 if( element.querySelector( 'section' ) ) { | 
|                     element.classList.add( 'stack' ); | 
|                 } | 
|             } | 
|   | 
|             // Mark the current slide as present | 
|             slides[index].classList.add( 'present' ); | 
|             slides[index].removeAttribute( 'hidden' ); | 
|   | 
|             // If this slide has a state associated with it, add it | 
|             // onto the current state of the deck | 
|             var slideState = slides[index].getAttribute( 'data-state' ); | 
|             if( slideState ) { | 
|                 state = state.concat( slideState.split( ' ' ) ); | 
|             } | 
|   | 
|             // If this slide has a data-autoslide attribtue associated use this as | 
|             // autoSlide value otherwise use the global configured time | 
|             var slideAutoSlide = slides[index].getAttribute( 'data-autoslide' ); | 
|             if( slideAutoSlide ) { | 
|                 autoSlide = parseInt( slideAutoSlide, 10 ); | 
|             } | 
|             else { | 
|                 autoSlide = config.autoSlide; | 
|             } | 
|   | 
|         } | 
|         else { | 
|             // Since there are no slides we can't be anywhere beyond the | 
|             // zeroth index | 
|             index = 0; | 
|         } | 
|   | 
|         return index; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Updates the progress bar to reflect the current slide. | 
|      */ | 
|     function updateProgress() { | 
|   | 
|         // Update progress if enabled | 
|         if( config.progress && dom.progress ) { | 
|   | 
|             var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); | 
|   | 
|             // The number of past and total slides | 
|             var totalCount = document.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; | 
|             var pastCount = 0; | 
|   | 
|             // Step through all slides and count the past ones | 
|             mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { | 
|   | 
|                 var horizontalSlide = horizontalSlides[i]; | 
|                 var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); | 
|   | 
|                 for( var j = 0; j < verticalSlides.length; j++ ) { | 
|   | 
|                     // Stop as soon as we arrive at the present | 
|                     if( verticalSlides[j].classList.contains( 'present' ) ) { | 
|                         break mainLoop; | 
|                     } | 
|   | 
|                     pastCount++; | 
|   | 
|                 } | 
|   | 
|                 // Stop as soon as we arrive at the present | 
|                 if( horizontalSlide.classList.contains( 'present' ) ) { | 
|                     break; | 
|                 } | 
|   | 
|                 // Don't count the wrapping section for vertical slides | 
|                 if( horizontalSlide.classList.contains( 'stack' ) === false ) { | 
|                     pastCount++; | 
|                 } | 
|   | 
|             } | 
|   | 
|             dom.progressbar.style.width = ( pastCount / ( totalCount - 1 ) ) * window.innerWidth + 'px'; | 
|   | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Updates the state of all control/navigation arrows. | 
|      */ | 
|     function updateControls() { | 
|   | 
|         if ( config.controls && dom.controls ) { | 
|   | 
|             var routes = availableRoutes(); | 
|             var fragments = availableFragments(); | 
|   | 
|             // Remove the 'enabled' class from all directions | 
|             dom.controlsLeft.concat( dom.controlsRight ) | 
|                             .concat( dom.controlsUp ) | 
|                             .concat( dom.controlsDown ) | 
|                             .concat( dom.controlsPrev ) | 
|                             .concat( dom.controlsNext ).forEach( function( node ) { | 
|                 node.classList.remove( 'enabled' ); | 
|                 node.classList.remove( 'fragmented' ); | 
|             } ); | 
|   | 
|             // Add the 'enabled' class to the available routes | 
|             if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' );    } ); | 
|             if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } ); | 
|             if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' );    } ); | 
|             if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } ); | 
|   | 
|             // Prev/next buttons | 
|             if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } ); | 
|             if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } ); | 
|   | 
|             // Highlight fragment directions | 
|             if( currentSlide ) { | 
|   | 
|                 // Always apply fragment decorator to prev/next buttons | 
|                 if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|                 if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|   | 
|                 // Apply fragment decorators to directional buttons based on | 
|                 // what slide axis they are in | 
|                 if( isVerticalSlide( currentSlide ) ) { | 
|                     if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|                     if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|                 } | 
|                 else { | 
|                     if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|                     if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); | 
|                 } | 
|             } | 
|   | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Updates the background elements to reflect the current  | 
|      * slide. | 
|      */ | 
|     function updateBackground() { | 
|   | 
|         // Update the classes of all backgrounds to match the  | 
|         // states of their slides (past/present/future) | 
|         toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { | 
|   | 
|             // Reverse past/future classes when in RTL mode | 
|             var horizontalPast = config.rtl ? 'future' : 'past', | 
|                 horizontalFuture = config.rtl ? 'past' : 'future'; | 
|   | 
|             backgroundh.className = 'slide-background ' + ( h < indexh ? horizontalPast : h > indexh ? horizontalFuture : 'present' ); | 
|   | 
|             toArray( backgroundh.childNodes ).forEach( function( backgroundv, v ) { | 
|   | 
|                 backgroundv.className = 'slide-background ' + ( v < indexv ? 'past' : v > indexv ? 'future' : 'present' ); | 
|   | 
|             } ); | 
|   | 
|         } ); | 
|   | 
|         // Allow the first background to apply without transition | 
|         setTimeout( function() { | 
|             dom.background.classList.remove( 'no-transition' ); | 
|         }, 1 ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Determine what available routes there are for navigation. | 
|      * | 
|      * @return {Object} containing four booleans: left/right/up/down | 
|      */ | 
|     function availableRoutes() { | 
|   | 
|         var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), | 
|             verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); | 
|   | 
|         var routes = { | 
|             left: indexh > 0 || config.loop, | 
|             right: indexh < horizontalSlides.length - 1 || config.loop, | 
|             up: indexv > 0, | 
|             down: indexv < verticalSlides.length - 1 | 
|         }; | 
|   | 
|         // reverse horizontal controls for rtl | 
|         if( config.rtl ) { | 
|             var left = routes.left; | 
|             routes.left = routes.right; | 
|             routes.right = left; | 
|         } | 
|   | 
|         return routes; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Returns an object describing the available fragment | 
|      * directions. | 
|      * | 
|      * @return {Object} two boolean properties: prev/next | 
|      */ | 
|     function availableFragments() { | 
|   | 
|         if( currentSlide && config.fragments ) { | 
|             var fragments = currentSlide.querySelectorAll( '.fragment' ); | 
|             var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' ); | 
|   | 
|             return { | 
|                 prev: fragments.length - hiddenFragments.length > 0, | 
|                 next: !!hiddenFragments.length | 
|             }; | 
|         } | 
|         else { | 
|             return { prev: false, next: false }; | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Start playback of any embedded content inside of | 
|      * the targeted slide. | 
|      */ | 
|     function startEmbeddedContent( slide ) { | 
|   | 
|         if( slide ) { | 
|             // HTML5 media elements | 
|             toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { | 
|                 if( el.hasAttribute( 'data-autoplay' ) ) { | 
|                     el.play(); | 
|                 } | 
|             } ); | 
|   | 
|             // YouTube embeds | 
|             toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { | 
|                 if( el.hasAttribute( 'data-autoplay' ) ) { | 
|                     el.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); | 
|                 } | 
|             }); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Stop playback of any embedded content inside of | 
|      * the targeted slide. | 
|      */ | 
|     function stopEmbeddedContent( slide ) { | 
|   | 
|         if( slide ) { | 
|             // HTML5 media elements | 
|             toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { | 
|                 if( !el.hasAttribute( 'data-ignore' ) ) { | 
|                     el.pause(); | 
|                 } | 
|             } ); | 
|   | 
|             // YouTube embeds | 
|             toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { | 
|                 if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { | 
|                     el.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); | 
|                 } | 
|             }); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Reads the current URL (hash) and navigates accordingly. | 
|      */ | 
|     function readURL() { | 
|   | 
|         var hash = window.location.hash; | 
|   | 
|         // Attempt to parse the hash as either an index or name | 
|         var bits = hash.slice( 2 ).split( '/' ), | 
|             name = hash.replace( /#|\//gi, '' ); | 
|   | 
|         // If the first bit is invalid and there is a name we can | 
|         // assume that this is a named link | 
|         if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) { | 
|             // Find the slide with the specified name | 
|             var element = document.querySelector( '#' + name ); | 
|   | 
|             if( element ) { | 
|                 // Find the position of the named slide and navigate to it | 
|                 var indices = Reveal.getIndices( element ); | 
|                 slide( indices.h, indices.v ); | 
|             } | 
|             // If the slide doesn't exist, navigate to the current slide | 
|             else { | 
|                 slide( indexh, indexv ); | 
|             } | 
|         } | 
|         else { | 
|             // Read the index components of the hash | 
|             var h = parseInt( bits[0], 10 ) || 0, | 
|                 v = parseInt( bits[1], 10 ) || 0; | 
|   | 
|             slide( h, v ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Updates the page URL (hash) to reflect the current | 
|      * state. | 
|      * | 
|      * @param {Number} delay The time in ms to wait before | 
|      * writing the hash | 
|      */ | 
|     function writeURL( delay ) { | 
|   | 
|         if( config.history ) { | 
|   | 
|             // Make sure there's never more than one timeout running | 
|             clearTimeout( writeURLTimeout ); | 
|   | 
|             // If a delay is specified, timeout this call | 
|             if( typeof delay === 'number' ) { | 
|                 writeURLTimeout = setTimeout( writeURL, delay ); | 
|             } | 
|             else { | 
|                 var url = '/'; | 
|   | 
|                 // If the current slide has an ID, use that as a named link | 
|                 if( currentSlide && typeof currentSlide.getAttribute( 'id' ) === 'string' ) { | 
|                     url = '/' + currentSlide.getAttribute( 'id' ); | 
|                 } | 
|                 // Otherwise use the /h/v index | 
|                 else { | 
|                     if( indexh > 0 || indexv > 0 ) url += indexh; | 
|                     if( indexv > 0 ) url += '/' + indexv; | 
|                 } | 
|   | 
|                 window.location.hash = url; | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Retrieves the h/v location of the current, or specified, | 
|      * slide. | 
|      * | 
|      * @param {HTMLElement} slide If specified, the returned | 
|      * index will be for this slide rather than the currently | 
|      * active one | 
|      * | 
|      * @return {Object} { h: <int>, v: <int>, f: <int> } | 
|      */ | 
|     function getIndices( slide ) { | 
|   | 
|         // By default, return the current indices | 
|         var h = indexh, | 
|             v = indexv, | 
|             f; | 
|   | 
|         // If a slide is specified, return the indices of that slide | 
|         if( slide ) { | 
|             var isVertical = isVerticalSlide( slide ); | 
|             var slideh = isVertical ? slide.parentNode : slide; | 
|   | 
|             // Select all horizontal slides | 
|             var horizontalSlides = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); | 
|   | 
|             // Now that we know which the horizontal slide is, get its index | 
|             h = Math.max( horizontalSlides.indexOf( slideh ), 0 ); | 
|   | 
|             // If this is a vertical slide, grab the vertical index | 
|             if( isVertical ) { | 
|                 v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 ); | 
|             } | 
|         } | 
|   | 
|         if( !slide && currentSlide ) { | 
|             var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); | 
|             if( visibleFragments.length ) { | 
|                 f = visibleFragments.length; | 
|             } | 
|         } | 
|   | 
|         return { h: h, v: v, f: f }; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Navigate to the next slide fragment. | 
|      * | 
|      * @return {Boolean} true if there was a next fragment, | 
|      * false otherwise | 
|      */ | 
|     function nextFragment() { | 
|   | 
|         if( currentSlide && config.fragments ) { | 
|             var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment:not(.visible)' ) ); | 
|   | 
|             if( fragments.length ) { | 
|                 // Find the index of the next fragment | 
|                 var index = fragments[0].getAttribute( 'data-fragment-index' ); | 
|   | 
|                 // Find all fragments with the same index | 
|                 fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); | 
|   | 
|                 toArray( fragments ).forEach( function( element ) { | 
|                     element.classList.add( 'visible' ); | 
|   | 
|                     // Notify subscribers of the change | 
|                     dispatchEvent( 'fragmentshown', { fragment: element } ); | 
|                 } ); | 
|   | 
|                 updateControls(); | 
|                 return true; | 
|             } | 
|         } | 
|   | 
|         return false; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Navigate to the previous slide fragment. | 
|      * | 
|      * @return {Boolean} true if there was a previous fragment, | 
|      * false otherwise | 
|      */ | 
|     function previousFragment() { | 
|   | 
|         if( currentSlide && config.fragments ) { | 
|             var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ); | 
|   | 
|             if( fragments.length ) { | 
|                 // Find the index of the previous fragment | 
|                 var index = fragments[ fragments.length - 1 ].getAttribute( 'data-fragment-index' ); | 
|   | 
|                 // Find all fragments with the same index | 
|                 fragments = currentSlide.querySelectorAll( '.fragment[data-fragment-index="'+ index +'"]' ); | 
|   | 
|                 toArray( fragments ).forEach( function( f ) { | 
|                     f.classList.remove( 'visible' ); | 
|   | 
|                     // Notify subscribers of the change | 
|                     dispatchEvent( 'fragmenthidden', { fragment: f } ); | 
|                 } ); | 
|   | 
|                 updateControls(); | 
|                 return true; | 
|             } | 
|         } | 
|   | 
|         return false; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Cues a new automated slide if enabled in the config. | 
|      */ | 
|     function cueAutoSlide() { | 
|   | 
|         clearTimeout( autoSlideTimeout ); | 
|   | 
|         // Cue the next auto-slide if enabled | 
|         if( autoSlide && !isPaused() && !isOverview() ) { | 
|             autoSlideTimeout = setTimeout( navigateNext, autoSlide ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Cancels any ongoing request to auto-slide. | 
|      */ | 
|     function cancelAutoSlide() { | 
|   | 
|         clearTimeout( autoSlideTimeout ); | 
|   | 
|     } | 
|   | 
|     function navigateLeft() { | 
|   | 
|         // Reverse for RTL | 
|         if( config.rtl ) { | 
|             if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) { | 
|                 slide( indexh + 1 ); | 
|             } | 
|         } | 
|         // Normal navigation | 
|         else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) { | 
|             slide( indexh - 1 ); | 
|         } | 
|   | 
|     } | 
|   | 
|     function navigateRight() { | 
|   | 
|         // Reverse for RTL | 
|         if( config.rtl ) { | 
|             if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) { | 
|                 slide( indexh - 1 ); | 
|             } | 
|         } | 
|         // Normal navigation | 
|         else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) { | 
|             slide( indexh + 1 ); | 
|         } | 
|   | 
|     } | 
|   | 
|     function navigateUp() { | 
|   | 
|         // Prioritize hiding fragments | 
|         if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) { | 
|             slide( indexh, indexv - 1 ); | 
|         } | 
|   | 
|     } | 
|   | 
|     function navigateDown() { | 
|   | 
|         // Prioritize revealing fragments | 
|         if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) { | 
|             slide( indexh, indexv + 1 ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Navigates backwards, prioritized in the following order: | 
|      * 1) Previous fragment | 
|      * 2) Previous vertical slide | 
|      * 3) Previous horizontal slide | 
|      */ | 
|     function navigatePrev() { | 
|   | 
|         // Prioritize revealing fragments | 
|         if( previousFragment() === false ) { | 
|             if( availableRoutes().up ) { | 
|                 navigateUp(); | 
|             } | 
|             else { | 
|                 // Fetch the previous horizontal slide, if there is one | 
|                 var previousSlide = document.querySelector( HORIZONTAL_SLIDES_SELECTOR + '.past:nth-child(' + indexh + ')' ); | 
|   | 
|                 if( previousSlide ) { | 
|                     var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined; | 
|                     var h = indexh - 1; | 
|                     slide( h, v ); | 
|                 } | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Same as #navigatePrev() but navigates forwards. | 
|      */ | 
|     function navigateNext() { | 
|   | 
|         // Prioritize revealing fragments | 
|         if( nextFragment() === false ) { | 
|             availableRoutes().down ? navigateDown() : navigateRight(); | 
|         } | 
|   | 
|         // If auto-sliding is enabled we need to cue up | 
|         // another timeout | 
|         cueAutoSlide(); | 
|   | 
|     } | 
|   | 
|   | 
|     // --------------------------------------------------------------------// | 
|     // ----------------------------- EVENTS -------------------------------// | 
|     // --------------------------------------------------------------------// | 
|   | 
|   | 
|     /** | 
|      * Handler for the document level 'keydown' event. | 
|      * | 
|      * @param {Object} event | 
|      */ | 
|     function onDocumentKeyDown( event ) { | 
|   | 
|         // Check if there's a focused element that could be using | 
|         // the keyboard | 
|         var activeElement = document.activeElement; | 
|         var hasFocus = !!( document.activeElement && ( document.activeElement.type || document.activeElement.href || document.activeElement.contentEditable !== 'inherit' ) ); | 
|   | 
|         // Disregard the event if there's a focused element or a | 
|         // keyboard modifier key is present | 
|         if( hasFocus || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return; | 
|   | 
|         // While paused only allow "unpausing" keyboard events (b and .) | 
|         if( isPaused() && [66,190,191].indexOf( event.keyCode ) === -1 ) { | 
|             return false; | 
|         } | 
|   | 
|         var triggered = false; | 
|   | 
|         // 1. User defined key bindings | 
|         if( typeof config.keyboard === 'object' ) { | 
|   | 
|             for( var key in config.keyboard ) { | 
|   | 
|                 // Check if this binding matches the pressed key | 
|                 if( parseInt( key, 10 ) === event.keyCode ) { | 
|   | 
|                     var value = config.keyboard[ key ]; | 
|   | 
|                     // Calback function | 
|                     if( typeof value === 'function' ) { | 
|                         value.apply( null, [ event ] ); | 
|                     } | 
|                     // String shortcuts to reveal.js API | 
|                     else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) { | 
|                         Reveal[ value ].call(); | 
|                     } | 
|   | 
|                     triggered = true; | 
|   | 
|                 } | 
|   | 
|             } | 
|   | 
|         } | 
|   | 
|         // 2. System defined key bindings | 
|         if( triggered === false ) { | 
|   | 
|             // Assume true and try to prove false | 
|             triggered = true; | 
|   | 
|             switch( event.keyCode ) { | 
|                 // p, page up | 
|                 case 80: case 33: navigatePrev(); break; | 
|                 // n, page down | 
|                 case 78: case 34: navigateNext(); break; | 
|                 // h, left | 
|                 case 72: case 37: navigateLeft(); break; | 
|                 // l, right | 
|                 case 76: case 39: navigateRight(); break; | 
|                 // k, up | 
|                 case 75: case 38: navigateUp(); break; | 
|                 // j, down | 
|                 case 74: case 40: navigateDown(); break; | 
|                 // home | 
|                 case 36: slide( 0 ); break; | 
|                 // end | 
|                 case 35: slide( Number.MAX_VALUE ); break; | 
|                 // space | 
|                 case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break; | 
|                 // return | 
|                 case 13: isOverview() ? deactivateOverview() : triggered = false; break; | 
|                 // b, period, Logitech presenter tools "black screen" button | 
|                 case 66: case 190: case 191: togglePause(); break; | 
|                 // f | 
|                 case 70: enterFullscreen(); break; | 
|                 default: | 
|                     triggered = false; | 
|             } | 
|   | 
|         } | 
|   | 
|         // If the input resulted in a triggered action we should prevent | 
|         // the browsers default behavior | 
|         if( triggered ) { | 
|             event.preventDefault(); | 
|         } | 
|         else if ( event.keyCode === 27 && supports3DTransforms ) { | 
|             toggleOverview(); | 
|   | 
|             event.preventDefault(); | 
|         } | 
|   | 
|         // If auto-sliding is enabled we need to cue up | 
|         // another timeout | 
|         cueAutoSlide(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handler for the 'touchstart' event, enables support for | 
|      * swipe and pinch gestures. | 
|      */ | 
|     function onTouchStart( event ) { | 
|   | 
|         touch.startX = event.touches[0].clientX; | 
|         touch.startY = event.touches[0].clientY; | 
|         touch.startCount = event.touches.length; | 
|   | 
|         // If there's two touches we need to memorize the distance | 
|         // between those two points to detect pinching | 
|         if( event.touches.length === 2 && config.overview ) { | 
|             touch.startSpan = distanceBetween( { | 
|                 x: event.touches[1].clientX, | 
|                 y: event.touches[1].clientY | 
|             }, { | 
|                 x: touch.startX, | 
|                 y: touch.startY | 
|             } ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handler for the 'touchmove' event. | 
|      */ | 
|     function onTouchMove( event ) { | 
|   | 
|         // Each touch should only trigger one action | 
|         if( !touch.handled ) { | 
|             var currentX = event.touches[0].clientX; | 
|             var currentY = event.touches[0].clientY; | 
|   | 
|             // If the touch started off with two points and still has | 
|             // two active touches; test for the pinch gesture | 
|             if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) { | 
|   | 
|                 // The current distance in pixels between the two touch points | 
|                 var currentSpan = distanceBetween( { | 
|                     x: event.touches[1].clientX, | 
|                     y: event.touches[1].clientY | 
|                 }, { | 
|                     x: touch.startX, | 
|                     y: touch.startY | 
|                 } ); | 
|   | 
|                 // If the span is larger than the desire amount we've got | 
|                 // ourselves a pinch | 
|                 if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) { | 
|                     touch.handled = true; | 
|   | 
|                     if( currentSpan < touch.startSpan ) { | 
|                         activateOverview(); | 
|                     } | 
|                     else { | 
|                         deactivateOverview(); | 
|                     } | 
|                 } | 
|   | 
|                 event.preventDefault(); | 
|   | 
|             } | 
|             // There was only one touch point, look for a swipe | 
|             else if( event.touches.length === 1 && touch.startCount !== 2 ) { | 
|   | 
|                 var deltaX = currentX - touch.startX, | 
|                     deltaY = currentY - touch.startY; | 
|   | 
|                 if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { | 
|                     touch.handled = true; | 
|                     navigateLeft(); | 
|                 } | 
|                 else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { | 
|                     touch.handled = true; | 
|                     navigateRight(); | 
|                 } | 
|                 else if( deltaY > touch.threshold ) { | 
|                     touch.handled = true; | 
|                     navigateUp(); | 
|                 } | 
|                 else if( deltaY < -touch.threshold ) { | 
|                     touch.handled = true; | 
|                     navigateDown(); | 
|                 } | 
|   | 
|                 event.preventDefault(); | 
|   | 
|             } | 
|         } | 
|         // There's a bug with swiping on some Android devices unless | 
|         // the default action is always prevented | 
|         else if( navigator.userAgent.match( /android/gi ) ) { | 
|             event.preventDefault(); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handler for the 'touchend' event. | 
|      */ | 
|     function onTouchEnd( event ) { | 
|   | 
|         touch.handled = false; | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Convert pointer down to touch start. | 
|      */ | 
|     function onPointerDown( event ) { | 
|   | 
|         if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { | 
|             event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; | 
|             onTouchStart( event ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Convert pointer move to touch move. | 
|      */ | 
|     function onPointerMove( event ) { | 
|   | 
|         if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { | 
|             event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; | 
|             onTouchMove( event ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Convert pointer up to touch end. | 
|      */ | 
|     function onPointerUp( event ) { | 
|   | 
|         if( event.pointerType === event.MSPOINTER_TYPE_TOUCH ) { | 
|             event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; | 
|             onTouchEnd( event ); | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handles mouse wheel scrolling, throttled to avoid skipping | 
|      * multiple slides. | 
|      */ | 
|     function onDocumentMouseScroll( event ) { | 
|   | 
|         if( Date.now() - lastMouseWheelStep > 600 ) { | 
|   | 
|             lastMouseWheelStep = Date.now(); | 
|   | 
|             var delta = event.detail || -event.wheelDelta; | 
|             if( delta > 0 ) { | 
|                 navigateNext(); | 
|             } | 
|             else { | 
|                 navigatePrev(); | 
|             } | 
|   | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Clicking on the progress bar results in a navigation to the | 
|      * closest approximate horizontal slide using this equation: | 
|      * | 
|      * ( clickX / presentationWidth ) * numberOfSlides | 
|      */ | 
|     function onProgressClicked( event ) { | 
|   | 
|         event.preventDefault(); | 
|   | 
|         var slidesTotal = toArray( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length; | 
|         var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal ); | 
|   | 
|         slide( slideIndex ); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Event handler for navigation control buttons. | 
|      */ | 
|     function onNavigateLeftClicked( event ) { event.preventDefault(); navigateLeft(); } | 
|     function onNavigateRightClicked( event ) { event.preventDefault(); navigateRight(); } | 
|     function onNavigateUpClicked( event ) { event.preventDefault(); navigateUp(); } | 
|     function onNavigateDownClicked( event ) { event.preventDefault(); navigateDown(); } | 
|     function onNavigatePrevClicked( event ) { event.preventDefault(); navigatePrev(); } | 
|     function onNavigateNextClicked( event ) { event.preventDefault(); navigateNext(); } | 
|   | 
|     /** | 
|      * Handler for the window level 'hashchange' event. | 
|      */ | 
|     function onWindowHashChange( event ) { | 
|   | 
|         readURL(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handler for the window level 'resize' event. | 
|      */ | 
|     function onWindowResize( event ) { | 
|   | 
|         layout(); | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Invoked when a slide is and we're in the overview. | 
|      */ | 
|     function onOverviewSlideClicked( event ) { | 
|   | 
|         // TODO There's a bug here where the event listeners are not | 
|         // removed after deactivating the overview. | 
|         if( eventsAreBound && isOverview() ) { | 
|             event.preventDefault(); | 
|   | 
|             var element = event.target; | 
|   | 
|             while( element && !element.nodeName.match( /section/gi ) ) { | 
|                 element = element.parentNode; | 
|             } | 
|   | 
|             if( element && !element.classList.contains( 'disabled' ) ) { | 
|   | 
|                 deactivateOverview(); | 
|   | 
|                 if( element.nodeName.match( /section/gi ) ) { | 
|                     var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ), | 
|                         v = parseInt( element.getAttribute( 'data-index-v' ), 10 ); | 
|   | 
|                     slide( h, v ); | 
|                 } | 
|   | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     /** | 
|      * Handles clicks on links that are set to preview in the | 
|      * iframe overlay. | 
|      */ | 
|     function onPreviewLinkClicked( event ) { | 
|   | 
|         var url = event.target.getAttribute( 'href' ); | 
|         if( url ) { | 
|             openPreview( url ); | 
|             event.preventDefault(); | 
|         } | 
|   | 
|     } | 
|   | 
|   | 
|     // --------------------------------------------------------------------// | 
|     // ------------------------------- API --------------------------------// | 
|     // --------------------------------------------------------------------// | 
|   | 
|   | 
|     return { | 
|         initialize: initialize, | 
|         configure: configure, | 
|         sync: sync, | 
|   | 
|         // Navigation methods | 
|         slide: slide, | 
|         left: navigateLeft, | 
|         right: navigateRight, | 
|         up: navigateUp, | 
|         down: navigateDown, | 
|         prev: navigatePrev, | 
|         next: navigateNext, | 
|         prevFragment: previousFragment, | 
|         nextFragment: nextFragment, | 
|   | 
|         // Deprecated aliases | 
|         navigateTo: slide, | 
|         navigateLeft: navigateLeft, | 
|         navigateRight: navigateRight, | 
|         navigateUp: navigateUp, | 
|         navigateDown: navigateDown, | 
|         navigatePrev: navigatePrev, | 
|         navigateNext: navigateNext, | 
|   | 
|         // Forces an update in slide layout | 
|         layout: layout, | 
|   | 
|         // Returns an object with the available routes as booleans (left/right/top/bottom) | 
|         availableRoutes: availableRoutes, | 
|   | 
|         // Returns an object with the available fragments as booleans (prev/next) | 
|         availableFragments: availableFragments, | 
|   | 
|         // Toggles the overview mode on/off | 
|         toggleOverview: toggleOverview, | 
|   | 
|         // Toggles the "black screen" mode on/off | 
|         togglePause: togglePause, | 
|   | 
|         // State checks | 
|         isOverview: isOverview, | 
|         isPaused: isPaused, | 
|   | 
|         // Adds or removes all internal event listeners (such as keyboard) | 
|         addEventListeners: addEventListeners, | 
|         removeEventListeners: removeEventListeners, | 
|   | 
|         // Returns the indices of the current, or specified, slide | 
|         getIndices: getIndices, | 
|   | 
|         // Returns the slide at the specified index, y is optional | 
|         getSlide: function( x, y ) { | 
|             var horizontalSlide = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; | 
|             var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' ); | 
|   | 
|             if( typeof y !== 'undefined' ) { | 
|                 return verticalSlides ? verticalSlides[ y ] : undefined; | 
|             } | 
|   | 
|             return horizontalSlide; | 
|         }, | 
|   | 
|         // Returns the previous slide element, may be null | 
|         getPreviousSlide: function() { | 
|             return previousSlide; | 
|         }, | 
|   | 
|         // Returns the current slide element | 
|         getCurrentSlide: function() { | 
|             return currentSlide; | 
|         }, | 
|   | 
|         // Returns the current scale of the presentation content | 
|         getScale: function() { | 
|             return scale; | 
|         }, | 
|   | 
|         // Returns the current configuration object | 
|         getConfig: function() { | 
|             return config; | 
|         }, | 
|   | 
|         // Helper method, retrieves query string as a key/value hash | 
|         getQueryHash: function() { | 
|             var query = {}; | 
|   | 
|             location.search.replace( /[A-Z0-9]+?=(\w*)/gi, function(a) { | 
|                 query[ a.split( '=' ).shift() ] = a.split( '=' ).pop(); | 
|             } ); | 
|   | 
|             return query; | 
|         }, | 
|   | 
|         // Returns true if we're currently on the first slide | 
|         isFirstSlide: function() { | 
|             return document.querySelector( SLIDES_SELECTOR + '.past' ) == null ? true : false; | 
|         }, | 
|   | 
|         // Returns true if we're currently on the last slide | 
|         isLastSlide: function() { | 
|             if( currentSlide && currentSlide.classList.contains( '.stack' ) ) { | 
|                 return currentSlide.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; | 
|             } | 
|             else { | 
|                 return document.querySelector( SLIDES_SELECTOR + '.future' ) == null ? true : false; | 
|             } | 
|         }, | 
|   | 
|         // Forward event binding to the reveal DOM element | 
|         addEventListener: function( type, listener, useCapture ) { | 
|             if( 'addEventListener' in window ) { | 
|                 ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture ); | 
|             } | 
|         }, | 
|         removeEventListener: function( type, listener, useCapture ) { | 
|             if( 'addEventListener' in window ) { | 
|                 ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture ); | 
|             } | 
|         } | 
|     }; | 
|   | 
| })(); |