presentations

Presentations
Log | Files | Refs

reveal.js (168667B)


      1 /*!
      2  * reveal.js
      3  * http://revealjs.com
      4  * MIT licensed
      5  *
      6  * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
      7  */
      8 (function( root, factory ) {
      9 	if( typeof define === 'function' && define.amd ) {
     10 		// AMD. Register as an anonymous module.
     11 		define( function() {
     12 			root.Reveal = factory();
     13 			return root.Reveal;
     14 		} );
     15 	} else if( typeof exports === 'object' ) {
     16 		// Node. Does not work with strict CommonJS.
     17 		module.exports = factory();
     18 	} else {
     19 		// Browser globals.
     20 		root.Reveal = factory();
     21 	}
     22 }( this, function() {
     23 
     24 	'use strict';
     25 
     26 	var Reveal;
     27 
     28 	// The reveal.js version
     29 	var VERSION = '3.8.0';
     30 
     31 	var SLIDES_SELECTOR = '.slides section',
     32 		HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
     33 		VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
     34 		HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
     35 		UA = navigator.userAgent,
     36 
     37 		// Configuration defaults, can be overridden at initialization time
     38 		config = {
     39 
     40 			// The "normal" size of the presentation, aspect ratio will be preserved
     41 			// when the presentation is scaled to fit different resolutions
     42 			width: 960,
     43 			height: 700,
     44 
     45 			// Factor of the display size that should remain empty around the content
     46 			margin: 0.04,
     47 
     48 			// Bounds for smallest/largest possible scale to apply to content
     49 			minScale: 0.2,
     50 			maxScale: 2.0,
     51 
     52 			// Display presentation control arrows
     53 			controls: true,
     54 
     55 			// Help the user learn the controls by providing hints, for example by
     56 			// bouncing the down arrow when they first encounter a vertical slide
     57 			controlsTutorial: true,
     58 
     59 			// Determines where controls appear, "edges" or "bottom-right"
     60 			controlsLayout: 'bottom-right',
     61 
     62 			// Visibility rule for backwards navigation arrows; "faded", "hidden"
     63 			// or "visible"
     64 			controlsBackArrows: 'faded',
     65 
     66 			// Display a presentation progress bar
     67 			progress: true,
     68 
     69 			// Display the page number of the current slide
     70 			// - true:    Show slide number
     71 			// - false:   Hide slide number
     72 			//
     73 			// Can optionally be set as a string that specifies the number formatting:
     74 			// - "h.v":	  Horizontal . vertical slide number (default)
     75 			// - "h/v":	  Horizontal / vertical slide number
     76 			// - "c":	  Flattened slide number
     77 			// - "c/t":	  Flattened slide number / total slides
     78 			//
     79 			// Alternatively, you can provide a function that returns the slide
     80 			// number for the current slide. The function needs to return an array
     81 			// with one string [slideNumber] or three strings [n1,delimiter,n2].
     82 			// See #formatSlideNumber().
     83 			slideNumber: false,
     84 
     85 			// Can be used to limit the contexts in which the slide number appears
     86 			// - "all":      Always show the slide number
     87 			// - "print":    Only when printing to PDF
     88 			// - "speaker":  Only in the speaker view
     89 			showSlideNumber: 'all',
     90 
     91 			// Use 1 based indexing for # links to match slide number (default is zero
     92 			// based)
     93 			hashOneBasedIndex: false,
     94 
     95 			// Add the current slide number to the URL hash so that reloading the
     96 			// page/copying the URL will return you to the same slide
     97 			hash: false,
     98 
     99 			// Push each slide change to the browser history.  Implies `hash: true`
    100 			history: false,
    101 
    102 			// Enable keyboard shortcuts for navigation
    103 			keyboard: true,
    104 
    105 			// Optional function that blocks keyboard events when retuning false
    106 			keyboardCondition: null,
    107 
    108 			// Enable the slide overview mode
    109 			overview: true,
    110 
    111 			// Disables the default reveal.js slide layout so that you can use
    112 			// custom CSS layout
    113 			disableLayout: false,
    114 
    115 			// Vertical centering of slides
    116 			center: true,
    117 
    118 			// Enables touch navigation on devices with touch input
    119 			touch: true,
    120 
    121 			// Loop the presentation
    122 			loop: false,
    123 
    124 			// Change the presentation direction to be RTL
    125 			rtl: false,
    126 
    127 			// Changes the behavior of our navigation directions.
    128 			//
    129 			// "default"
    130 			// Left/right arrow keys step between horizontal slides, up/down
    131 			// arrow keys step between vertical slides. Space key steps through
    132 			// all slides (both horizontal and vertical).
    133 			//
    134 			// "linear"
    135 			// Removes the up/down arrows. Left/right arrows step through all
    136 			// slides (both horizontal and vertical).
    137 			//
    138 			// "grid"
    139 			// When this is enabled, stepping left/right from a vertical stack
    140 			// to an adjacent vertical stack will land you at the same vertical
    141 			// index.
    142 			//
    143 			// Consider a deck with six slides ordered in two vertical stacks:
    144 			// 1.1    2.1
    145 			// 1.2    2.2
    146 			// 1.3    2.3
    147 			//
    148 			// If you're on slide 1.3 and navigate right, you will normally move
    149 			// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
    150 			// from 1.3 -> 2.3.
    151 			navigationMode: 'default',
    152 
    153 			// Randomizes the order of slides each time the presentation loads
    154 			shuffle: false,
    155 
    156 			// Turns fragments on and off globally
    157 			fragments: true,
    158 
    159 			// Flags whether to include the current fragment in the URL,
    160 			// so that reloading brings you to the same fragment position
    161 			fragmentInURL: false,
    162 
    163 			// Flags if the presentation is running in an embedded mode,
    164 			// i.e. contained within a limited portion of the screen
    165 			embedded: false,
    166 
    167 			// Flags if we should show a help overlay when the question-mark
    168 			// key is pressed
    169 			help: true,
    170 
    171 			// Flags if it should be possible to pause the presentation (blackout)
    172 			pause: true,
    173 
    174 			// Flags if speaker notes should be visible to all viewers
    175 			showNotes: false,
    176 
    177 			// Global override for autolaying embedded media (video/audio/iframe)
    178 			// - null:   Media will only autoplay if data-autoplay is present
    179 			// - true:   All media will autoplay, regardless of individual setting
    180 			// - false:  No media will autoplay, regardless of individual setting
    181 			autoPlayMedia: null,
    182 
    183 			// Global override for preloading lazy-loaded iframes
    184 			// - null:   Iframes with data-src AND data-preload will be loaded when within
    185 			//           the viewDistance, iframes with only data-src will be loaded when visible
    186 			// - true:   All iframes with data-src will be loaded when within the viewDistance
    187 			// - false:  All iframes with data-src will be loaded only when visible
    188 			preloadIframes: null,
    189 
    190 			// Controls automatic progression to the next slide
    191 			// - 0:      Auto-sliding only happens if the data-autoslide HTML attribute
    192 			//           is present on the current slide or fragment
    193 			// - 1+:     All slides will progress automatically at the given interval
    194 			// - false:  No auto-sliding, even if data-autoslide is present
    195 			autoSlide: 0,
    196 
    197 			// Stop auto-sliding after user input
    198 			autoSlideStoppable: true,
    199 
    200 			// Use this method for navigation when auto-sliding (defaults to navigateNext)
    201 			autoSlideMethod: null,
    202 
    203 			// Specify the average time in seconds that you think you will spend
    204 			// presenting each slide. This is used to show a pacing timer in the
    205 			// speaker view
    206 			defaultTiming: null,
    207 
    208 			// Enable slide navigation via mouse wheel
    209 			mouseWheel: false,
    210 
    211 			// Apply a 3D roll to links on hover
    212 			rollingLinks: false,
    213 
    214 			// Hides the address bar on mobile devices
    215 			hideAddressBar: true,
    216 
    217 			// Opens links in an iframe preview overlay
    218 			// Add `data-preview-link` and `data-preview-link="false"` to customise each link
    219 			// individually
    220 			previewLinks: false,
    221 
    222 			// Exposes the reveal.js API through window.postMessage
    223 			postMessage: true,
    224 
    225 			// Dispatches all reveal.js events to the parent window through postMessage
    226 			postMessageEvents: false,
    227 
    228 			// Focuses body when page changes visibility to ensure keyboard shortcuts work
    229 			focusBodyOnPageVisibilityChange: true,
    230 
    231 			// Transition style
    232 			transition: 'slide', // none/fade/slide/convex/concave/zoom
    233 
    234 			// Transition speed
    235 			transitionSpeed: 'default', // default/fast/slow
    236 
    237 			// Transition style for full page slide backgrounds
    238 			backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
    239 
    240 			// Parallax background image
    241 			parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
    242 
    243 			// Parallax background size
    244 			parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
    245 
    246 			// Parallax background repeat
    247 			parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
    248 
    249 			// Parallax background position
    250 			parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
    251 
    252 			// Amount of pixels to move the parallax background per slide step
    253 			parallaxBackgroundHorizontal: null,
    254 			parallaxBackgroundVertical: null,
    255 
    256 			// The maximum number of pages a single slide can expand onto when printing
    257 			// to PDF, unlimited by default
    258 			pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
    259 
    260 			// Prints each fragment on a separate slide
    261 			pdfSeparateFragments: true,
    262 
    263 			// Offset used to reduce the height of content within exported PDF pages.
    264 			// This exists to account for environment differences based on how you
    265 			// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
    266 			// on precisely the total height of the document whereas in-browser
    267 			// printing has to end one pixel before.
    268 			pdfPageHeightOffset: -1,
    269 
    270 			// Number of slides away from the current that are visible
    271 			viewDistance: 3,
    272 
    273 			// The display mode that will be used to show slides
    274 			display: 'block',
    275 
    276 			// Hide cursor if inactive
    277 			hideInactiveCursor: true,
    278 
    279 			// Time before the cursor is hidden (in ms)
    280 			hideCursorTime: 5000,
    281 
    282 			// Script dependencies to load
    283 			dependencies: []
    284 
    285 		},
    286 
    287 		// Flags if Reveal.initialize() has been called
    288 		initialized = false,
    289 
    290 		// Flags if reveal.js is loaded (has dispatched the 'ready' event)
    291 		loaded = false,
    292 
    293 		// Flags if the overview mode is currently active
    294 		overview = false,
    295 
    296 		// Holds the dimensions of our overview slides, including margins
    297 		overviewSlideWidth = null,
    298 		overviewSlideHeight = null,
    299 
    300 		// The horizontal and vertical index of the currently active slide
    301 		indexh,
    302 		indexv,
    303 
    304 		// The previous and current slide HTML elements
    305 		previousSlide,
    306 		currentSlide,
    307 
    308 		previousBackground,
    309 
    310 		// Remember which directions that the user has navigated towards
    311 		hasNavigatedRight = false,
    312 		hasNavigatedDown = false,
    313 
    314 		// Slides may hold a data-state attribute which we pick up and apply
    315 		// as a class to the body. This list contains the combined state of
    316 		// all current slides.
    317 		state = [],
    318 
    319 		// The current scale of the presentation (see width/height config)
    320 		scale = 1,
    321 
    322 		// CSS transform that is currently applied to the slides container,
    323 		// split into two groups
    324 		slidesTransform = { layout: '', overview: '' },
    325 
    326 		// Cached references to DOM elements
    327 		dom = {},
    328 
    329 		// A list of registered reveal.js plugins
    330 		plugins = {},
    331 
    332 		// List of asynchronously loaded reveal.js dependencies
    333 		asyncDependencies = [],
    334 
    335 		// Features supported by the browser, see #checkCapabilities()
    336 		features = {},
    337 
    338 		// Client is a mobile device, see #checkCapabilities()
    339 		isMobileDevice,
    340 
    341 		// Client is a desktop Chrome, see #checkCapabilities()
    342 		isChrome,
    343 
    344 		// Throttles mouse wheel navigation
    345 		lastMouseWheelStep = 0,
    346 
    347 		// Delays updates to the URL due to a Chrome thumbnailer bug
    348 		writeURLTimeout = 0,
    349 
    350 		// Is the mouse pointer currently hidden from view
    351 		cursorHidden = false,
    352 
    353 		// Timeout used to determine when the cursor is inactive
    354 		cursorInactiveTimeout = 0,
    355 
    356 		// Flags if the interaction event listeners are bound
    357 		eventsAreBound = false,
    358 
    359 		// The current auto-slide duration
    360 		autoSlide = 0,
    361 
    362 		// Auto slide properties
    363 		autoSlidePlayer,
    364 		autoSlideTimeout = 0,
    365 		autoSlideStartTime = -1,
    366 		autoSlidePaused = false,
    367 
    368 		// Holds information about the currently ongoing touch input
    369 		touch = {
    370 			startX: 0,
    371 			startY: 0,
    372 			startCount: 0,
    373 			captured: false,
    374 			threshold: 40
    375 		},
    376 
    377 		// A key:value map of shortcut keyboard keys and descriptions of
    378 		// the actions they trigger, generated in #configure()
    379 		keyboardShortcuts = {},
    380 
    381 		// Holds custom key code mappings
    382 		registeredKeyBindings = {};
    383 
    384 	/**
    385 	 * Starts up the presentation if the client is capable.
    386 	 */
    387 	function initialize( options ) {
    388 
    389 		// Make sure we only initialize once
    390 		if( initialized === true ) return;
    391 
    392 		initialized = true;
    393 
    394 		checkCapabilities();
    395 
    396 		if( !features.transforms2d && !features.transforms3d ) {
    397 			document.body.setAttribute( 'class', 'no-transforms' );
    398 
    399 			// Since JS won't be running any further, we load all lazy
    400 			// loading elements upfront
    401 			var images = toArray( document.getElementsByTagName( 'img' ) ),
    402 				iframes = toArray( document.getElementsByTagName( 'iframe' ) );
    403 
    404 			var lazyLoadable = images.concat( iframes );
    405 
    406 			for( var i = 0, len = lazyLoadable.length; i < len; i++ ) {
    407 				var element = lazyLoadable[i];
    408 				if( element.getAttribute( 'data-src' ) ) {
    409 					element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
    410 					element.removeAttribute( 'data-src' );
    411 				}
    412 			}
    413 
    414 			// If the browser doesn't support core features we won't be
    415 			// using JavaScript to control the presentation
    416 			return;
    417 		}
    418 
    419 		// Cache references to key DOM elements
    420 		dom.wrapper = document.querySelector( '.reveal' );
    421 		dom.slides = document.querySelector( '.reveal .slides' );
    422 
    423 		// Force a layout when the whole page, incl fonts, has loaded
    424 		window.addEventListener( 'load', layout, false );
    425 
    426 		var query = Reveal.getQueryHash();
    427 
    428 		// Do not accept new dependencies via query config to avoid
    429 		// the potential of malicious script injection
    430 		if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
    431 
    432 		// Copy options over to our config object
    433 		extend( config, options );
    434 		extend( config, query );
    435 
    436 		// Hide the address bar in mobile browsers
    437 		hideAddressBar();
    438 
    439 		// Loads dependencies and continues to #start() once done
    440 		load();
    441 
    442 	}
    443 
    444 	/**
    445 	 * Inspect the client to see what it's capable of, this
    446 	 * should only happens once per runtime.
    447 	 */
    448 	function checkCapabilities() {
    449 
    450 		isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( UA );
    451 		isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
    452 
    453 		var testElement = document.createElement( 'div' );
    454 
    455 		features.transforms3d = 'WebkitPerspective' in testElement.style ||
    456 								'MozPerspective' in testElement.style ||
    457 								'msPerspective' in testElement.style ||
    458 								'OPerspective' in testElement.style ||
    459 								'perspective' in testElement.style;
    460 
    461 		features.transforms2d = 'WebkitTransform' in testElement.style ||
    462 								'MozTransform' in testElement.style ||
    463 								'msTransform' in testElement.style ||
    464 								'OTransform' in testElement.style ||
    465 								'transform' in testElement.style;
    466 
    467 		features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
    468 		features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';
    469 
    470 		features.canvas = !!document.createElement( 'canvas' ).getContext;
    471 
    472 		// Transitions in the overview are disabled in desktop and
    473 		// Safari due to lag
    474 		features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( UA );
    475 
    476 		// Flags if we should use zoom instead of transform to scale
    477 		// up slides. Zoom produces crisper results but has a lot of
    478 		// xbrowser quirks so we only use it in whitelsited browsers.
    479 		features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
    480 						( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
    481 
    482 	}
    483 
    484 	/**
    485 	 * Loads the dependencies of reveal.js. Dependencies are
    486 	 * defined via the configuration option 'dependencies'
    487 	 * and will be loaded prior to starting/binding reveal.js.
    488 	 * Some dependencies may have an 'async' flag, if so they
    489 	 * will load after reveal.js has been started up.
    490 	 */
    491 	function load() {
    492 
    493 		var scripts = [],
    494 			scriptsToLoad = 0;
    495 
    496 		config.dependencies.forEach( function( s ) {
    497 			// Load if there's no condition or the condition is truthy
    498 			if( !s.condition || s.condition() ) {
    499 				if( s.async ) {
    500 					asyncDependencies.push( s );
    501 				}
    502 				else {
    503 					scripts.push( s );
    504 				}
    505 			}
    506 		} );
    507 
    508 		if( scripts.length ) {
    509 			scriptsToLoad = scripts.length;
    510 
    511 			// Load synchronous scripts
    512 			scripts.forEach( function( s ) {
    513 				loadScript( s.src, function() {
    514 
    515 					if( typeof s.callback === 'function' ) s.callback();
    516 
    517 					if( --scriptsToLoad === 0 ) {
    518 						initPlugins();
    519 					}
    520 
    521 				} );
    522 			} );
    523 		}
    524 		else {
    525 			initPlugins();
    526 		}
    527 
    528 	}
    529 
    530 	/**
    531 	 * Initializes our plugins and waits for them to be ready
    532 	 * before proceeding.
    533 	 */
    534 	function initPlugins() {
    535 
    536 		var pluginsToInitialize = Object.keys( plugins ).length;
    537 
    538 		// If there are no plugins, skip this step
    539 		if( pluginsToInitialize === 0 ) {
    540 			loadAsyncDependencies();
    541 		}
    542 		// ... otherwise initialize plugins
    543 		else {
    544 
    545 			var afterPlugInitialized = function() {
    546 				if( --pluginsToInitialize === 0 ) {
    547 					loadAsyncDependencies();
    548 				}
    549 			};
    550 
    551 			for( var i in plugins ) {
    552 
    553 				var plugin = plugins[i];
    554 
    555 				// If the plugin has an 'init' method, invoke it
    556 				if( typeof plugin.init === 'function' ) {
    557 					var callback = plugin.init();
    558 
    559 					// If the plugin returned a Promise, wait for it
    560 					if( callback && typeof callback.then === 'function' ) {
    561 						callback.then( afterPlugInitialized );
    562 					}
    563 					else {
    564 						afterPlugInitialized();
    565 					}
    566 				}
    567 				else {
    568 					afterPlugInitialized();
    569 				}
    570 
    571 			}
    572 
    573 		}
    574 
    575 	}
    576 
    577 	/**
    578 	 * Loads all async reveal.js dependencies.
    579 	 */
    580 	function loadAsyncDependencies() {
    581 
    582 		if( asyncDependencies.length ) {
    583 			asyncDependencies.forEach( function( s ) {
    584 				loadScript( s.src, s.callback );
    585 			} );
    586 		}
    587 
    588 		start();
    589 
    590 	}
    591 
    592 	/**
    593 	 * Loads a JavaScript file from the given URL and executes it.
    594 	 *
    595 	 * @param {string} url Address of the .js file to load
    596 	 * @param {function} callback Method to invoke when the script
    597 	 * has loaded and executed
    598 	 */
    599 	function loadScript( url, callback ) {
    600 
    601 		var script = document.createElement( 'script' );
    602 		script.type = 'text/javascript';
    603 		script.async = false;
    604 		script.defer = false;
    605 		script.src = url;
    606 
    607 		if( callback ) {
    608 
    609 			// Success callback
    610 			script.onload = script.onreadystatechange = function( event ) {
    611 				if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
    612 
    613 					// Kill event listeners
    614 					script.onload = script.onreadystatechange = script.onerror = null;
    615 
    616 					callback();
    617 
    618 				}
    619 			};
    620 
    621 			// Error callback
    622 			script.onerror = function( err ) {
    623 
    624 				// Kill event listeners
    625 				script.onload = script.onreadystatechange = script.onerror = null;
    626 
    627 				callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
    628 
    629 			};
    630 
    631 		}
    632 
    633 		// Append the script at the end of <head>
    634 		var head = document.querySelector( 'head' );
    635 		head.insertBefore( script, head.lastChild );
    636 
    637 	}
    638 
    639 	/**
    640 	 * Starts up reveal.js by binding input events and navigating
    641 	 * to the current URL deeplink if there is one.
    642 	 */
    643 	function start() {
    644 
    645 		loaded = true;
    646 
    647 		// Make sure we've got all the DOM elements we need
    648 		setupDOM();
    649 
    650 		// Listen to messages posted to this window
    651 		setupPostMessage();
    652 
    653 		// Prevent the slides from being scrolled out of view
    654 		setupScrollPrevention();
    655 
    656 		// Resets all vertical slides so that only the first is visible
    657 		resetVerticalSlides();
    658 
    659 		// Updates the presentation to match the current configuration values
    660 		configure();
    661 
    662 		// Read the initial hash
    663 		readURL();
    664 
    665 		// Update all backgrounds
    666 		updateBackground( true );
    667 
    668 		// Notify listeners that the presentation is ready but use a 1ms
    669 		// timeout to ensure it's not fired synchronously after #initialize()
    670 		setTimeout( function() {
    671 			// Enable transitions now that we're loaded
    672 			dom.slides.classList.remove( 'no-transition' );
    673 
    674 			dom.wrapper.classList.add( 'ready' );
    675 
    676 			dispatchEvent( 'ready', {
    677 				'indexh': indexh,
    678 				'indexv': indexv,
    679 				'currentSlide': currentSlide
    680 			} );
    681 		}, 1 );
    682 
    683 		// Special setup and config is required when printing to PDF
    684 		if( isPrintingPDF() ) {
    685 			removeEventListeners();
    686 
    687 			// The document needs to have loaded for the PDF layout
    688 			// measurements to be accurate
    689 			if( document.readyState === 'complete' ) {
    690 				setupPDF();
    691 			}
    692 			else {
    693 				window.addEventListener( 'load', setupPDF );
    694 			}
    695 		}
    696 
    697 	}
    698 
    699 	/**
    700 	 * Finds and stores references to DOM elements which are
    701 	 * required by the presentation. If a required element is
    702 	 * not found, it is created.
    703 	 */
    704 	function setupDOM() {
    705 
    706 		// Prevent transitions while we're loading
    707 		dom.slides.classList.add( 'no-transition' );
    708 
    709 		if( isMobileDevice ) {
    710 			dom.wrapper.classList.add( 'no-hover' );
    711 		}
    712 		else {
    713 			dom.wrapper.classList.remove( 'no-hover' );
    714 		}
    715 
    716 		if( /iphone/gi.test( UA ) ) {
    717 			dom.wrapper.classList.add( 'ua-iphone' );
    718 		}
    719 		else {
    720 			dom.wrapper.classList.remove( 'ua-iphone' );
    721 		}
    722 
    723 		// Background element
    724 		dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null );
    725 
    726 		// Progress bar
    727 		dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '<span></span>' );
    728 		dom.progressbar = dom.progress.querySelector( 'span' );
    729 
    730 		// Arrow controls
    731 		dom.controls = createSingletonNode( dom.wrapper, 'aside', 'controls',
    732 			'<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' +
    733 			'<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' +
    734 			'<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' +
    735 			'<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>' );
    736 
    737 		// Slide number
    738 		dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' );
    739 
    740 		// Element containing notes that are visible to the audience
    741 		dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
    742 		dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
    743 		dom.speakerNotes.setAttribute( 'tabindex', '0' );
    744 
    745 		// Overlay graphic which is displayed during the paused mode
    746 		dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
    747 
    748 		dom.wrapper.setAttribute( 'role', 'application' );
    749 
    750 		// There can be multiple instances of controls throughout the page
    751 		dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) );
    752 		dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) );
    753 		dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) );
    754 		dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) );
    755 		dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) );
    756 		dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) );
    757 
    758 		// The right and down arrows in the standard reveal.js controls
    759 		dom.controlsRightArrow = dom.controls.querySelector( '.navigate-right' );
    760 		dom.controlsDownArrow = dom.controls.querySelector( '.navigate-down' );
    761 
    762 		dom.statusDiv = createStatusDiv();
    763 	}
    764 
    765 	/**
    766 	 * Creates a hidden div with role aria-live to announce the
    767 	 * current slide content. Hide the div off-screen to make it
    768 	 * available only to Assistive Technologies.
    769 	 *
    770 	 * @return {HTMLElement}
    771 	 */
    772 	function createStatusDiv() {
    773 
    774 		var statusDiv = document.getElementById( 'aria-status-div' );
    775 		if( !statusDiv ) {
    776 			statusDiv = document.createElement( 'div' );
    777 			statusDiv.style.position = 'absolute';
    778 			statusDiv.style.height = '1px';
    779 			statusDiv.style.width = '1px';
    780 			statusDiv.style.overflow = 'hidden';
    781 			statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
    782 			statusDiv.setAttribute( 'id', 'aria-status-div' );
    783 			statusDiv.setAttribute( 'aria-live', 'polite' );
    784 			statusDiv.setAttribute( 'aria-atomic','true' );
    785 			dom.wrapper.appendChild( statusDiv );
    786 		}
    787 		return statusDiv;
    788 
    789 	}
    790 
    791 	/**
    792 	 * Converts the given HTML element into a string of text
    793 	 * that can be announced to a screen reader. Hidden
    794 	 * elements are excluded.
    795 	 */
    796 	function getStatusText( node ) {
    797 
    798 		var text = '';
    799 
    800 		// Text node
    801 		if( node.nodeType === 3 ) {
    802 			text += node.textContent;
    803 		}
    804 		// Element node
    805 		else if( node.nodeType === 1 ) {
    806 
    807 			var isAriaHidden = node.getAttribute( 'aria-hidden' );
    808 			var isDisplayHidden = window.getComputedStyle( node )['display'] === 'none';
    809 			if( isAriaHidden !== 'true' && !isDisplayHidden ) {
    810 
    811 				toArray( node.childNodes ).forEach( function( child ) {
    812 					text += getStatusText( child );
    813 				} );
    814 
    815 			}
    816 
    817 		}
    818 
    819 		return text;
    820 
    821 	}
    822 
    823 	/**
    824 	 * Configures the presentation for printing to a static
    825 	 * PDF.
    826 	 */
    827 	function setupPDF() {
    828 
    829 		var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight );
    830 
    831 		// Dimensions of the PDF pages
    832 		var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
    833 			pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
    834 
    835 		// Dimensions of slides within the pages
    836 		var slideWidth = slideSize.width,
    837 			slideHeight = slideSize.height;
    838 
    839 		// Let the browser know what page size we want to print
    840 		injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
    841 
    842 		// Limit the size of certain elements to the dimensions of the slide
    843 		injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
    844 
    845 		document.body.classList.add( 'print-pdf' );
    846 		document.body.style.width = pageWidth + 'px';
    847 		document.body.style.height = pageHeight + 'px';
    848 
    849 		// Make sure stretch elements fit on slide
    850 		layoutSlideContents( slideWidth, slideHeight );
    851 
    852 		// Add each slide's index as attributes on itself, we need these
    853 		// indices to generate slide numbers below
    854 		toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
    855 			hslide.setAttribute( 'data-index-h', h );
    856 
    857 			if( hslide.classList.contains( 'stack' ) ) {
    858 				toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
    859 					vslide.setAttribute( 'data-index-h', h );
    860 					vslide.setAttribute( 'data-index-v', v );
    861 				} );
    862 			}
    863 		} );
    864 
    865 		// Slide and slide background layout
    866 		toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
    867 
    868 			// Vertical stacks are not centred since their section
    869 			// children will be
    870 			if( slide.classList.contains( 'stack' ) === false ) {
    871 				// Center the slide inside of the page, giving the slide some margin
    872 				var left = ( pageWidth - slideWidth ) / 2,
    873 					top = ( pageHeight - slideHeight ) / 2;
    874 
    875 				var contentHeight = slide.scrollHeight;
    876 				var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
    877 
    878 				// Adhere to configured pages per slide limit
    879 				numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
    880 
    881 				// Center slides vertically
    882 				if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
    883 					top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
    884 				}
    885 
    886 				// Wrap the slide in a page element and hide its overflow
    887 				// so that no page ever flows onto another
    888 				var page = document.createElement( 'div' );
    889 				page.className = 'pdf-page';
    890 				page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
    891 				slide.parentNode.insertBefore( page, slide );
    892 				page.appendChild( slide );
    893 
    894 				// Position the slide inside of the page
    895 				slide.style.left = left + 'px';
    896 				slide.style.top = top + 'px';
    897 				slide.style.width = slideWidth + 'px';
    898 
    899 				if( slide.slideBackgroundElement ) {
    900 					page.insertBefore( slide.slideBackgroundElement, slide );
    901 				}
    902 
    903 				// Inject notes if `showNotes` is enabled
    904 				if( config.showNotes ) {
    905 
    906 					// Are there notes for this slide?
    907 					var notes = getSlideNotes( slide );
    908 					if( notes ) {
    909 
    910 						var notesSpacing = 8;
    911 						var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
    912 						var notesElement = document.createElement( 'div' );
    913 						notesElement.classList.add( 'speaker-notes' );
    914 						notesElement.classList.add( 'speaker-notes-pdf' );
    915 						notesElement.setAttribute( 'data-layout', notesLayout );
    916 						notesElement.innerHTML = notes;
    917 
    918 						if( notesLayout === 'separate-page' ) {
    919 							page.parentNode.insertBefore( notesElement, page.nextSibling );
    920 						}
    921 						else {
    922 							notesElement.style.left = notesSpacing + 'px';
    923 							notesElement.style.bottom = notesSpacing + 'px';
    924 							notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
    925 							page.appendChild( notesElement );
    926 						}
    927 
    928 					}
    929 
    930 				}
    931 
    932 				// Inject slide numbers if `slideNumbers` are enabled
    933 				if( config.slideNumber && /all|print/i.test( config.showSlideNumber ) ) {
    934 					var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1,
    935 						slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1;
    936 
    937 					var numberElement = document.createElement( 'div' );
    938 					numberElement.classList.add( 'slide-number' );
    939 					numberElement.classList.add( 'slide-number-pdf' );
    940 					numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV );
    941 					page.appendChild( numberElement );
    942 				}
    943 
    944 				// Copy page and show fragments one after another
    945 				if( config.pdfSeparateFragments ) {
    946 
    947 					// Each fragment 'group' is an array containing one or more
    948 					// fragments. Multiple fragments that appear at the same time
    949 					// are part of the same group.
    950 					var fragmentGroups = sortFragments( page.querySelectorAll( '.fragment' ), true );
    951 
    952 					var previousFragmentStep;
    953 					var previousPage;
    954 
    955 					fragmentGroups.forEach( function( fragments ) {
    956 
    957 						// Remove 'current-fragment' from the previous group
    958 						if( previousFragmentStep ) {
    959 							previousFragmentStep.forEach( function( fragment ) {
    960 								fragment.classList.remove( 'current-fragment' );
    961 							} );
    962 						}
    963 
    964 						// Show the fragments for the current index
    965 						fragments.forEach( function( fragment ) {
    966 							fragment.classList.add( 'visible', 'current-fragment' );
    967 						} );
    968 
    969 						// Create a separate page for the current fragment state
    970 						var clonedPage = page.cloneNode( true );
    971 						page.parentNode.insertBefore( clonedPage, ( previousPage || page ).nextSibling );
    972 
    973 						previousFragmentStep = fragments;
    974 						previousPage = clonedPage;
    975 
    976 					} );
    977 
    978 					// Reset the first/original page so that all fragments are hidden
    979 					fragmentGroups.forEach( function( fragments ) {
    980 						fragments.forEach( function( fragment ) {
    981 							fragment.classList.remove( 'visible', 'current-fragment' );
    982 						} );
    983 					} );
    984 
    985 				}
    986 				// Show all fragments
    987 				else {
    988 					toArray( page.querySelectorAll( '.fragment:not(.fade-out)' ) ).forEach( function( fragment ) {
    989 						fragment.classList.add( 'visible' );
    990 					} );
    991 				}
    992 
    993 			}
    994 
    995 		} );
    996 
    997 		// Notify subscribers that the PDF layout is good to go
    998 		dispatchEvent( 'pdf-ready' );
    999 
   1000 	}
   1001 
   1002 	/**
   1003 	 * This is an unfortunate necessity. Some actions – such as
   1004 	 * an input field being focused in an iframe or using the
   1005 	 * keyboard to expand text selection beyond the bounds of
   1006 	 * a slide – can trigger our content to be pushed out of view.
   1007 	 * This scrolling can not be prevented by hiding overflow in
   1008 	 * CSS (we already do) so we have to resort to repeatedly
   1009 	 * checking if the slides have been offset :(
   1010 	 */
   1011 	function setupScrollPrevention() {
   1012 
   1013 		setInterval( function() {
   1014 			if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) {
   1015 				dom.wrapper.scrollTop = 0;
   1016 				dom.wrapper.scrollLeft = 0;
   1017 			}
   1018 		}, 1000 );
   1019 
   1020 	}
   1021 
   1022 	/**
   1023 	 * Creates an HTML element and returns a reference to it.
   1024 	 * If the element already exists the existing instance will
   1025 	 * be returned.
   1026 	 *
   1027 	 * @param {HTMLElement} container
   1028 	 * @param {string} tagname
   1029 	 * @param {string} classname
   1030 	 * @param {string} innerHTML
   1031 	 *
   1032 	 * @return {HTMLElement}
   1033 	 */
   1034 	function createSingletonNode( container, tagname, classname, innerHTML ) {
   1035 
   1036 		// Find all nodes matching the description
   1037 		var nodes = container.querySelectorAll( '.' + classname );
   1038 
   1039 		// Check all matches to find one which is a direct child of
   1040 		// the specified container
   1041 		for( var i = 0; i < nodes.length; i++ ) {
   1042 			var testNode = nodes[i];
   1043 			if( testNode.parentNode === container ) {
   1044 				return testNode;
   1045 			}
   1046 		}
   1047 
   1048 		// If no node was found, create it now
   1049 		var node = document.createElement( tagname );
   1050 		node.className = classname;
   1051 		if( typeof innerHTML === 'string' ) {
   1052 			node.innerHTML = innerHTML;
   1053 		}
   1054 		container.appendChild( node );
   1055 
   1056 		return node;
   1057 
   1058 	}
   1059 
   1060 	/**
   1061 	 * Creates the slide background elements and appends them
   1062 	 * to the background container. One element is created per
   1063 	 * slide no matter if the given slide has visible background.
   1064 	 */
   1065 	function createBackgrounds() {
   1066 
   1067 		var printMode = isPrintingPDF();
   1068 
   1069 		// Clear prior backgrounds
   1070 		dom.background.innerHTML = '';
   1071 		dom.background.classList.add( 'no-transition' );
   1072 
   1073 		// Iterate over all horizontal slides
   1074 		toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) {
   1075 
   1076 			var backgroundStack = createBackground( slideh, dom.background );
   1077 
   1078 			// Iterate over all vertical slides
   1079 			toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) {
   1080 
   1081 				createBackground( slidev, backgroundStack );
   1082 
   1083 				backgroundStack.classList.add( 'stack' );
   1084 
   1085 			} );
   1086 
   1087 		} );
   1088 
   1089 		// Add parallax background if specified
   1090 		if( config.parallaxBackgroundImage ) {
   1091 
   1092 			dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
   1093 			dom.background.style.backgroundSize = config.parallaxBackgroundSize;
   1094 			dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat;
   1095 			dom.background.style.backgroundPosition = config.parallaxBackgroundPosition;
   1096 
   1097 			// Make sure the below properties are set on the element - these properties are
   1098 			// needed for proper transitions to be set on the element via CSS. To remove
   1099 			// annoying background slide-in effect when the presentation starts, apply
   1100 			// these properties after short time delay
   1101 			setTimeout( function() {
   1102 				dom.wrapper.classList.add( 'has-parallax-background' );
   1103 			}, 1 );
   1104 
   1105 		}
   1106 		else {
   1107 
   1108 			dom.background.style.backgroundImage = '';
   1109 			dom.wrapper.classList.remove( 'has-parallax-background' );
   1110 
   1111 		}
   1112 
   1113 	}
   1114 
   1115 	/**
   1116 	 * Creates a background for the given slide.
   1117 	 *
   1118 	 * @param {HTMLElement} slide
   1119 	 * @param {HTMLElement} container The element that the background
   1120 	 * should be appended to
   1121 	 * @return {HTMLElement} New background div
   1122 	 */
   1123 	function createBackground( slide, container ) {
   1124 
   1125 
   1126 		// Main slide background element
   1127 		var element = document.createElement( 'div' );
   1128 		element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
   1129 
   1130 		// Inner background element that wraps images/videos/iframes
   1131 		var contentElement = document.createElement( 'div' );
   1132 		contentElement.className = 'slide-background-content';
   1133 
   1134 		element.appendChild( contentElement );
   1135 		container.appendChild( element );
   1136 
   1137 		slide.slideBackgroundElement = element;
   1138 		slide.slideBackgroundContentElement = contentElement;
   1139 
   1140 		// Syncs the background to reflect all current background settings
   1141 		syncBackground( slide );
   1142 
   1143 		return element;
   1144 
   1145 	}
   1146 
   1147 	/**
   1148 	 * Renders all of the visual properties of a slide background
   1149 	 * based on the various background attributes.
   1150 	 *
   1151 	 * @param {HTMLElement} slide
   1152 	 */
   1153 	function syncBackground( slide ) {
   1154 
   1155 		var element = slide.slideBackgroundElement,
   1156 			contentElement = slide.slideBackgroundContentElement;
   1157 
   1158 		// Reset the prior background state in case this is not the
   1159 		// initial sync
   1160 		slide.classList.remove( 'has-dark-background' );
   1161 		slide.classList.remove( 'has-light-background' );
   1162 
   1163 		element.removeAttribute( 'data-loaded' );
   1164 		element.removeAttribute( 'data-background-hash' );
   1165 		element.removeAttribute( 'data-background-size' );
   1166 		element.removeAttribute( 'data-background-transition' );
   1167 		element.style.backgroundColor = '';
   1168 
   1169 		contentElement.style.backgroundSize = '';
   1170 		contentElement.style.backgroundRepeat = '';
   1171 		contentElement.style.backgroundPosition = '';
   1172 		contentElement.style.backgroundImage = '';
   1173 		contentElement.style.opacity = '';
   1174 		contentElement.innerHTML = '';
   1175 
   1176 		var data = {
   1177 			background: slide.getAttribute( 'data-background' ),
   1178 			backgroundSize: slide.getAttribute( 'data-background-size' ),
   1179 			backgroundImage: slide.getAttribute( 'data-background-image' ),
   1180 			backgroundVideo: slide.getAttribute( 'data-background-video' ),
   1181 			backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
   1182 			backgroundColor: slide.getAttribute( 'data-background-color' ),
   1183 			backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
   1184 			backgroundPosition: slide.getAttribute( 'data-background-position' ),
   1185 			backgroundTransition: slide.getAttribute( 'data-background-transition' ),
   1186 			backgroundOpacity: slide.getAttribute( 'data-background-opacity' )
   1187 		};
   1188 
   1189 		if( data.background ) {
   1190 			// Auto-wrap image urls in url(...)
   1191 			if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) {
   1192 				slide.setAttribute( 'data-background-image', data.background );
   1193 			}
   1194 			else {
   1195 				element.style.background = data.background;
   1196 			}
   1197 		}
   1198 
   1199 		// Create a hash for this combination of background settings.
   1200 		// This is used to determine when two slide backgrounds are
   1201 		// the same.
   1202 		if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
   1203 			element.setAttribute( 'data-background-hash', data.background +
   1204 															data.backgroundSize +
   1205 															data.backgroundImage +
   1206 															data.backgroundVideo +
   1207 															data.backgroundIframe +
   1208 															data.backgroundColor +
   1209 															data.backgroundRepeat +
   1210 															data.backgroundPosition +
   1211 															data.backgroundTransition +
   1212 															data.backgroundOpacity );
   1213 		}
   1214 
   1215 		// Additional and optional background properties
   1216 		if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
   1217 		if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
   1218 		if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
   1219 
   1220 		// Background image options are set on the content wrapper
   1221 		if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
   1222 		if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
   1223 		if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
   1224 		if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
   1225 
   1226 		// If this slide has a background color, we add a class that
   1227 		// signals if it is light or dark. If the slide has no background
   1228 		// color, no class will be added
   1229 		var contrastColor = data.backgroundColor;
   1230 
   1231 		// If no bg color was found, check the computed background
   1232 		if( !contrastColor ) {
   1233 			var computedBackgroundStyle = window.getComputedStyle( element );
   1234 			if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
   1235 				contrastColor = computedBackgroundStyle.backgroundColor;
   1236 			}
   1237 		}
   1238 
   1239 		if( contrastColor ) {
   1240 			var rgb = colorToRgb( contrastColor );
   1241 
   1242 			// Ignore fully transparent backgrounds. Some browsers return
   1243 			// rgba(0,0,0,0) when reading the computed background color of
   1244 			// an element with no background
   1245 			if( rgb && rgb.a !== 0 ) {
   1246 				if( colorBrightness( contrastColor ) < 128 ) {
   1247 					slide.classList.add( 'has-dark-background' );
   1248 				}
   1249 				else {
   1250 					slide.classList.add( 'has-light-background' );
   1251 				}
   1252 			}
   1253 		}
   1254 
   1255 	}
   1256 
   1257 	/**
   1258 	 * Registers a listener to postMessage events, this makes it
   1259 	 * possible to call all reveal.js API methods from another
   1260 	 * window. For example:
   1261 	 *
   1262 	 * revealWindow.postMessage( JSON.stringify({
   1263 	 *   method: 'slide',
   1264 	 *   args: [ 2 ]
   1265 	 * }), '*' );
   1266 	 */
   1267 	function setupPostMessage() {
   1268 
   1269 		if( config.postMessage ) {
   1270 			window.addEventListener( 'message', function ( event ) {
   1271 				var data = event.data;
   1272 
   1273 				// Make sure we're dealing with JSON
   1274 				if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) {
   1275 					data = JSON.parse( data );
   1276 
   1277 					// Check if the requested method can be found
   1278 					if( data.method && typeof Reveal[data.method] === 'function' ) {
   1279 						Reveal[data.method].apply( Reveal, data.args );
   1280 					}
   1281 				}
   1282 			}, false );
   1283 		}
   1284 
   1285 	}
   1286 
   1287 	/**
   1288 	 * Applies the configuration settings from the config
   1289 	 * object. May be called multiple times.
   1290 	 *
   1291 	 * @param {object} options
   1292 	 */
   1293 	function configure( options ) {
   1294 
   1295 		var oldTransition = config.transition;
   1296 
   1297 		// New config options may be passed when this method
   1298 		// is invoked through the API after initialization
   1299 		if( typeof options === 'object' ) extend( config, options );
   1300 
   1301 		// Abort if reveal.js hasn't finished loading, config
   1302 		// changes will be applied automatically once loading
   1303 		// finishes
   1304 		if( loaded === false ) return;
   1305 
   1306 		var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length;
   1307 
   1308 		// Remove the previously configured transition class
   1309 		dom.wrapper.classList.remove( oldTransition );
   1310 
   1311 		// Force linear transition based on browser capabilities
   1312 		if( features.transforms3d === false ) config.transition = 'linear';
   1313 
   1314 		dom.wrapper.classList.add( config.transition );
   1315 
   1316 		dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed );
   1317 		dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition );
   1318 
   1319 		dom.controls.style.display = config.controls ? 'block' : 'none';
   1320 		dom.progress.style.display = config.progress ? 'block' : 'none';
   1321 
   1322 		dom.controls.setAttribute( 'data-controls-layout', config.controlsLayout );
   1323 		dom.controls.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
   1324 
   1325 		if( config.shuffle ) {
   1326 			shuffle();
   1327 		}
   1328 
   1329 		if( config.rtl ) {
   1330 			dom.wrapper.classList.add( 'rtl' );
   1331 		}
   1332 		else {
   1333 			dom.wrapper.classList.remove( 'rtl' );
   1334 		}
   1335 
   1336 		if( config.center ) {
   1337 			dom.wrapper.classList.add( 'center' );
   1338 		}
   1339 		else {
   1340 			dom.wrapper.classList.remove( 'center' );
   1341 		}
   1342 
   1343 		// Exit the paused mode if it was configured off
   1344 		if( config.pause === false ) {
   1345 			resume();
   1346 		}
   1347 
   1348 		if( config.showNotes ) {
   1349 			dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
   1350 		}
   1351 
   1352 		if( config.mouseWheel ) {
   1353 			document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
   1354 			document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
   1355 		}
   1356 		else {
   1357 			document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
   1358 			document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false );
   1359 		}
   1360 
   1361 		// Rolling 3D links
   1362 		if( config.rollingLinks ) {
   1363 			enableRollingLinks();
   1364 		}
   1365 		else {
   1366 			disableRollingLinks();
   1367 		}
   1368 
   1369 		// Auto-hide the mouse pointer when its inactive
   1370 		if( config.hideInactiveCursor ) {
   1371 			document.addEventListener( 'mousemove', onDocumentCursorActive, false );
   1372 			document.addEventListener( 'mousedown', onDocumentCursorActive, false );
   1373 		}
   1374 		else {
   1375 			showCursor();
   1376 
   1377 			document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
   1378 			document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
   1379 		}
   1380 
   1381 		// Iframe link previews
   1382 		if( config.previewLinks ) {
   1383 			enablePreviewLinks();
   1384 			disablePreviewLinks( '[data-preview-link=false]' );
   1385 		}
   1386 		else {
   1387 			disablePreviewLinks();
   1388 			enablePreviewLinks( '[data-preview-link]:not([data-preview-link=false])' );
   1389 		}
   1390 
   1391 		// Remove existing auto-slide controls
   1392 		if( autoSlidePlayer ) {
   1393 			autoSlidePlayer.destroy();
   1394 			autoSlidePlayer = null;
   1395 		}
   1396 
   1397 		// Generate auto-slide controls if needed
   1398 		if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) {
   1399 			autoSlidePlayer = new Playback( dom.wrapper, function() {
   1400 				return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 );
   1401 			} );
   1402 
   1403 			autoSlidePlayer.on( 'click', onAutoSlidePlayerClick );
   1404 			autoSlidePaused = false;
   1405 		}
   1406 
   1407 		// When fragments are turned off they should be visible
   1408 		if( config.fragments === false ) {
   1409 			toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) {
   1410 				element.classList.add( 'visible' );
   1411 				element.classList.remove( 'current-fragment' );
   1412 			} );
   1413 		}
   1414 
   1415 		// Slide numbers
   1416 		var slideNumberDisplay = 'none';
   1417 		if( config.slideNumber && !isPrintingPDF() ) {
   1418 			if( config.showSlideNumber === 'all' ) {
   1419 				slideNumberDisplay = 'block';
   1420 			}
   1421 			else if( config.showSlideNumber === 'speaker' && isSpeakerNotes() ) {
   1422 				slideNumberDisplay = 'block';
   1423 			}
   1424 		}
   1425 
   1426 		dom.slideNumber.style.display = slideNumberDisplay;
   1427 
   1428 		// Add the navigation mode to the DOM so we can adjust styling
   1429 		if( config.navigationMode !== 'default' ) {
   1430 			dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
   1431 		}
   1432 		else {
   1433 			dom.wrapper.removeAttribute( 'data-navigation-mode' );
   1434 		}
   1435 
   1436 		// Define our contextual list of keyboard shortcuts
   1437 		if( config.navigationMode === 'linear' ) {
   1438 			keyboardShortcuts['&#8594;  ,  &#8595;  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide';
   1439 			keyboardShortcuts['&#8592;  ,  &#8593;  ,  P  ,  H  ,  K']           = 'Previous slide';
   1440 		}
   1441 		else {
   1442 			keyboardShortcuts['N  ,  SPACE']   = 'Next slide';
   1443 			keyboardShortcuts['P']             = 'Previous slide';
   1444 			keyboardShortcuts['&#8592;  ,  H'] = 'Navigate left';
   1445 			keyboardShortcuts['&#8594;  ,  L'] = 'Navigate right';
   1446 			keyboardShortcuts['&#8593;  ,  K'] = 'Navigate up';
   1447 			keyboardShortcuts['&#8595;  ,  J'] = 'Navigate down';
   1448 		}
   1449 
   1450 		keyboardShortcuts['Home  ,  &#8984;/CTRL &#8592;'] = 'First slide';
   1451 		keyboardShortcuts['End  ,  &#8984;/CTRL &#8594;']  = 'Last slide';
   1452 		keyboardShortcuts['B  ,  .']                       = 'Pause';
   1453 		keyboardShortcuts['F']                             = 'Fullscreen';
   1454 		keyboardShortcuts['ESC, O']                        = 'Slide overview';
   1455 
   1456 		sync();
   1457 
   1458 	}
   1459 
   1460 	/**
   1461 	 * Binds all event listeners.
   1462 	 */
   1463 	function addEventListeners() {
   1464 
   1465 		eventsAreBound = true;
   1466 
   1467 		window.addEventListener( 'hashchange', onWindowHashChange, false );
   1468 		window.addEventListener( 'resize', onWindowResize, false );
   1469 
   1470 		if( config.touch ) {
   1471 			if( 'onpointerdown' in window ) {
   1472 				// Use W3C pointer events
   1473 				dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false );
   1474 				dom.wrapper.addEventListener( 'pointermove', onPointerMove, false );
   1475 				dom.wrapper.addEventListener( 'pointerup', onPointerUp, false );
   1476 			}
   1477 			else if( window.navigator.msPointerEnabled ) {
   1478 				// IE 10 uses prefixed version of pointer events
   1479 				dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false );
   1480 				dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false );
   1481 				dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false );
   1482 			}
   1483 			else {
   1484 				// Fall back to touch events
   1485 				dom.wrapper.addEventListener( 'touchstart', onTouchStart, false );
   1486 				dom.wrapper.addEventListener( 'touchmove', onTouchMove, false );
   1487 				dom.wrapper.addEventListener( 'touchend', onTouchEnd, false );
   1488 			}
   1489 		}
   1490 
   1491 		if( config.keyboard ) {
   1492 			document.addEventListener( 'keydown', onDocumentKeyDown, false );
   1493 			document.addEventListener( 'keypress', onDocumentKeyPress, false );
   1494 		}
   1495 
   1496 		if( config.progress && dom.progress ) {
   1497 			dom.progress.addEventListener( 'click', onProgressClicked, false );
   1498 		}
   1499 
   1500 		dom.pauseOverlay.addEventListener( 'click', resume, false );
   1501 
   1502 		if( config.focusBodyOnPageVisibilityChange ) {
   1503 			var visibilityChange;
   1504 
   1505 			if( 'hidden' in document ) {
   1506 				visibilityChange = 'visibilitychange';
   1507 			}
   1508 			else if( 'msHidden' in document ) {
   1509 				visibilityChange = 'msvisibilitychange';
   1510 			}
   1511 			else if( 'webkitHidden' in document ) {
   1512 				visibilityChange = 'webkitvisibilitychange';
   1513 			}
   1514 
   1515 			if( visibilityChange ) {
   1516 				document.addEventListener( visibilityChange, onPageVisibilityChange, false );
   1517 			}
   1518 		}
   1519 
   1520 		// Listen to both touch and click events, in case the device
   1521 		// supports both
   1522 		var pointerEvents = [ 'touchstart', 'click' ];
   1523 
   1524 		// Only support touch for Android, fixes double navigations in
   1525 		// stock browser
   1526 		if( UA.match( /android/gi ) ) {
   1527 			pointerEvents = [ 'touchstart' ];
   1528 		}
   1529 
   1530 		pointerEvents.forEach( function( eventName ) {
   1531 			dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } );
   1532 			dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } );
   1533 			dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } );
   1534 			dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } );
   1535 			dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } );
   1536 			dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } );
   1537 		} );
   1538 
   1539 	}
   1540 
   1541 	/**
   1542 	 * Unbinds all event listeners.
   1543 	 */
   1544 	function removeEventListeners() {
   1545 
   1546 		eventsAreBound = false;
   1547 
   1548 		document.removeEventListener( 'keydown', onDocumentKeyDown, false );
   1549 		document.removeEventListener( 'keypress', onDocumentKeyPress, false );
   1550 		window.removeEventListener( 'hashchange', onWindowHashChange, false );
   1551 		window.removeEventListener( 'resize', onWindowResize, false );
   1552 
   1553 		dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false );
   1554 		dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false );
   1555 		dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false );
   1556 
   1557 		dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false );
   1558 		dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false );
   1559 		dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false );
   1560 
   1561 		dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false );
   1562 		dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
   1563 		dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
   1564 
   1565 		dom.pauseOverlay.removeEventListener( 'click', resume, false );
   1566 
   1567 		if ( config.progress && dom.progress ) {
   1568 			dom.progress.removeEventListener( 'click', onProgressClicked, false );
   1569 		}
   1570 
   1571 		[ 'touchstart', 'click' ].forEach( function( eventName ) {
   1572 			dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } );
   1573 			dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } );
   1574 			dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } );
   1575 			dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } );
   1576 			dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } );
   1577 			dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } );
   1578 		} );
   1579 
   1580 	}
   1581 
   1582 	/**
   1583 	 * Registers a new plugin with this reveal.js instance.
   1584 	 *
   1585 	 * reveal.js waits for all regisered plugins to initialize
   1586 	 * before considering itself ready, as long as the plugin
   1587 	 * is registered before calling `Reveal.initialize()`.
   1588 	 */
   1589 	function registerPlugin( id, plugin ) {
   1590 
   1591 		if( plugins[id] === undefined ) {
   1592 			plugins[id] = plugin;
   1593 
   1594 			// If a plugin is registered after reveal.js is loaded,
   1595 			// initialize it right away
   1596 			if( loaded && typeof plugin.init === 'function' ) {
   1597 				plugin.init();
   1598 			}
   1599 		}
   1600 		else {
   1601 			console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
   1602 		}
   1603 
   1604 	}
   1605 
   1606 	/**
   1607 	 * Checks if a specific plugin has been registered.
   1608 	 *
   1609 	 * @param {String} id Unique plugin identifier
   1610 	 */
   1611 	function hasPlugin( id ) {
   1612 
   1613 		return !!plugins[id];
   1614 
   1615 	}
   1616 
   1617 	/**
   1618 	 * Returns the specific plugin instance, if a plugin
   1619 	 * with the given ID has been registered.
   1620 	 *
   1621 	 * @param {String} id Unique plugin identifier
   1622 	 */
   1623 	function getPlugin( id ) {
   1624 
   1625 		return plugins[id];
   1626 
   1627 	}
   1628 
   1629 	/**
   1630 	 * Add a custom key binding with optional description to
   1631 	 * be added to the help screen.
   1632 	 */
   1633 	function addKeyBinding( binding, callback ) {
   1634 
   1635 		if( typeof binding === 'object' && binding.keyCode ) {
   1636 			registeredKeyBindings[binding.keyCode] = {
   1637 				callback: callback,
   1638 				key: binding.key,
   1639 				description: binding.description
   1640 			};
   1641 		}
   1642 		else {
   1643 			registeredKeyBindings[binding] = {
   1644 				callback: callback,
   1645 				key: null,
   1646 				description: null
   1647 			};
   1648 		}
   1649 
   1650 	}
   1651 
   1652 	/**
   1653 	 * Removes the specified custom key binding.
   1654 	 */
   1655 	function removeKeyBinding( keyCode ) {
   1656 
   1657 		delete registeredKeyBindings[keyCode];
   1658 
   1659 	}
   1660 
   1661 	/**
   1662 	 * Extend object a with the properties of object b.
   1663 	 * If there's a conflict, object b takes precedence.
   1664 	 *
   1665 	 * @param {object} a
   1666 	 * @param {object} b
   1667 	 */
   1668 	function extend( a, b ) {
   1669 
   1670 		for( var i in b ) {
   1671 			a[ i ] = b[ i ];
   1672 		}
   1673 
   1674 		return a;
   1675 
   1676 	}
   1677 
   1678 	/**
   1679 	 * Converts the target object to an array.
   1680 	 *
   1681 	 * @param {object} o
   1682 	 * @return {object[]}
   1683 	 */
   1684 	function toArray( o ) {
   1685 
   1686 		return Array.prototype.slice.call( o );
   1687 
   1688 	}
   1689 
   1690 	/**
   1691 	 * Utility for deserializing a value.
   1692 	 *
   1693 	 * @param {*} value
   1694 	 * @return {*}
   1695 	 */
   1696 	function deserialize( value ) {
   1697 
   1698 		if( typeof value === 'string' ) {
   1699 			if( value === 'null' ) return null;
   1700 			else if( value === 'true' ) return true;
   1701 			else if( value === 'false' ) return false;
   1702 			else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
   1703 		}
   1704 
   1705 		return value;
   1706 
   1707 	}
   1708 
   1709 	/**
   1710 	 * Measures the distance in pixels between point a
   1711 	 * and point b.
   1712 	 *
   1713 	 * @param {object} a point with x/y properties
   1714 	 * @param {object} b point with x/y properties
   1715 	 *
   1716 	 * @return {number}
   1717 	 */
   1718 	function distanceBetween( a, b ) {
   1719 
   1720 		var dx = a.x - b.x,
   1721 			dy = a.y - b.y;
   1722 
   1723 		return Math.sqrt( dx*dx + dy*dy );
   1724 
   1725 	}
   1726 
   1727 	/**
   1728 	 * Applies a CSS transform to the target element.
   1729 	 *
   1730 	 * @param {HTMLElement} element
   1731 	 * @param {string} transform
   1732 	 */
   1733 	function transformElement( element, transform ) {
   1734 
   1735 		element.style.WebkitTransform = transform;
   1736 		element.style.MozTransform = transform;
   1737 		element.style.msTransform = transform;
   1738 		element.style.transform = transform;
   1739 
   1740 	}
   1741 
   1742 	/**
   1743 	 * Applies CSS transforms to the slides container. The container
   1744 	 * is transformed from two separate sources: layout and the overview
   1745 	 * mode.
   1746 	 *
   1747 	 * @param {object} transforms
   1748 	 */
   1749 	function transformSlides( transforms ) {
   1750 
   1751 		// Pick up new transforms from arguments
   1752 		if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout;
   1753 		if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview;
   1754 
   1755 		// Apply the transforms to the slides container
   1756 		if( slidesTransform.layout ) {
   1757 			transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview );
   1758 		}
   1759 		else {
   1760 			transformElement( dom.slides, slidesTransform.overview );
   1761 		}
   1762 
   1763 	}
   1764 
   1765 	/**
   1766 	 * Injects the given CSS styles into the DOM.
   1767 	 *
   1768 	 * @param {string} value
   1769 	 */
   1770 	function injectStyleSheet( value ) {
   1771 
   1772 		var tag = document.createElement( 'style' );
   1773 		tag.type = 'text/css';
   1774 		if( tag.styleSheet ) {
   1775 			tag.styleSheet.cssText = value;
   1776 		}
   1777 		else {
   1778 			tag.appendChild( document.createTextNode( value ) );
   1779 		}
   1780 		document.getElementsByTagName( 'head' )[0].appendChild( tag );
   1781 
   1782 	}
   1783 
   1784 	/**
   1785 	 * Find the closest parent that matches the given
   1786 	 * selector.
   1787 	 *
   1788 	 * @param {HTMLElement} target The child element
   1789 	 * @param {String} selector The CSS selector to match
   1790 	 * the parents against
   1791 	 *
   1792 	 * @return {HTMLElement} The matched parent or null
   1793 	 * if no matching parent was found
   1794 	 */
   1795 	function closestParent( target, selector ) {
   1796 
   1797 		var parent = target.parentNode;
   1798 
   1799 		while( parent ) {
   1800 
   1801 			// There's some overhead doing this each time, we don't
   1802 			// want to rewrite the element prototype but should still
   1803 			// be enough to feature detect once at startup...
   1804 			var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;
   1805 
   1806 			// If we find a match, we're all set
   1807 			if( matchesMethod && matchesMethod.call( parent, selector ) ) {
   1808 				return parent;
   1809 			}
   1810 
   1811 			// Keep searching
   1812 			parent = parent.parentNode;
   1813 
   1814 		}
   1815 
   1816 		return null;
   1817 
   1818 	}
   1819 
   1820 	/**
   1821 	 * Converts various color input formats to an {r:0,g:0,b:0} object.
   1822 	 *
   1823 	 * @param {string} color The string representation of a color
   1824 	 * @example
   1825 	 * colorToRgb('#000');
   1826 	 * @example
   1827 	 * colorToRgb('#000000');
   1828 	 * @example
   1829 	 * colorToRgb('rgb(0,0,0)');
   1830 	 * @example
   1831 	 * colorToRgb('rgba(0,0,0)');
   1832 	 *
   1833 	 * @return {{r: number, g: number, b: number, [a]: number}|null}
   1834 	 */
   1835 	function colorToRgb( color ) {
   1836 
   1837 		var hex3 = color.match( /^#([0-9a-f]{3})$/i );
   1838 		if( hex3 && hex3[1] ) {
   1839 			hex3 = hex3[1];
   1840 			return {
   1841 				r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
   1842 				g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
   1843 				b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
   1844 			};
   1845 		}
   1846 
   1847 		var hex6 = color.match( /^#([0-9a-f]{6})$/i );
   1848 		if( hex6 && hex6[1] ) {
   1849 			hex6 = hex6[1];
   1850 			return {
   1851 				r: parseInt( hex6.substr( 0, 2 ), 16 ),
   1852 				g: parseInt( hex6.substr( 2, 2 ), 16 ),
   1853 				b: parseInt( hex6.substr( 4, 2 ), 16 )
   1854 			};
   1855 		}
   1856 
   1857 		var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
   1858 		if( rgb ) {
   1859 			return {
   1860 				r: parseInt( rgb[1], 10 ),
   1861 				g: parseInt( rgb[2], 10 ),
   1862 				b: parseInt( rgb[3], 10 )
   1863 			};
   1864 		}
   1865 
   1866 		var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
   1867 		if( rgba ) {
   1868 			return {
   1869 				r: parseInt( rgba[1], 10 ),
   1870 				g: parseInt( rgba[2], 10 ),
   1871 				b: parseInt( rgba[3], 10 ),
   1872 				a: parseFloat( rgba[4] )
   1873 			};
   1874 		}
   1875 
   1876 		return null;
   1877 
   1878 	}
   1879 
   1880 	/**
   1881 	 * Calculates brightness on a scale of 0-255.
   1882 	 *
   1883 	 * @param {string} color See colorToRgb for supported formats.
   1884 	 * @see {@link colorToRgb}
   1885 	 */
   1886 	function colorBrightness( color ) {
   1887 
   1888 		if( typeof color === 'string' ) color = colorToRgb( color );
   1889 
   1890 		if( color ) {
   1891 			return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
   1892 		}
   1893 
   1894 		return null;
   1895 
   1896 	}
   1897 
   1898 	/**
   1899 	 * Returns the remaining height within the parent of the
   1900 	 * target element.
   1901 	 *
   1902 	 * remaining height = [ configured parent height ] - [ current parent height ]
   1903 	 *
   1904 	 * @param {HTMLElement} element
   1905 	 * @param {number} [height]
   1906 	 */
   1907 	function getRemainingHeight( element, height ) {
   1908 
   1909 		height = height || 0;
   1910 
   1911 		if( element ) {
   1912 			var newHeight, oldHeight = element.style.height;
   1913 
   1914 			// Change the .stretch element height to 0 in order find the height of all
   1915 			// the other elements
   1916 			element.style.height = '0px';
   1917 
   1918 			// In Overview mode, the parent (.slide) height is set of 700px.
   1919 			// Restore it temporarily to its natural height.
   1920 			element.parentNode.style.height = 'auto';
   1921 
   1922 			newHeight = height - element.parentNode.offsetHeight;
   1923 
   1924 			// Restore the old height, just in case
   1925 			element.style.height = oldHeight + 'px';
   1926 
   1927 			// Clear the parent (.slide) height. .removeProperty works in IE9+
   1928 			element.parentNode.style.removeProperty('height');
   1929 
   1930 			return newHeight;
   1931 		}
   1932 
   1933 		return height;
   1934 
   1935 	}
   1936 
   1937 	/**
   1938 	 * Checks if this instance is being used to print a PDF.
   1939 	 */
   1940 	function isPrintingPDF() {
   1941 
   1942 		return ( /print-pdf/gi ).test( window.location.search );
   1943 
   1944 	}
   1945 
   1946 	/**
   1947 	 * Hides the address bar if we're on a mobile device.
   1948 	 */
   1949 	function hideAddressBar() {
   1950 
   1951 		if( config.hideAddressBar && isMobileDevice ) {
   1952 			// Events that should trigger the address bar to hide
   1953 			window.addEventListener( 'load', removeAddressBar, false );
   1954 			window.addEventListener( 'orientationchange', removeAddressBar, false );
   1955 		}
   1956 
   1957 	}
   1958 
   1959 	/**
   1960 	 * Causes the address bar to hide on mobile devices,
   1961 	 * more vertical space ftw.
   1962 	 */
   1963 	function removeAddressBar() {
   1964 
   1965 		setTimeout( function() {
   1966 			window.scrollTo( 0, 1 );
   1967 		}, 10 );
   1968 
   1969 	}
   1970 
   1971 	/**
   1972 	 * Dispatches an event of the specified type from the
   1973 	 * reveal DOM element.
   1974 	 */
   1975 	function dispatchEvent( type, args ) {
   1976 
   1977 		var event = document.createEvent( 'HTMLEvents', 1, 2 );
   1978 		event.initEvent( type, true, true );
   1979 		extend( event, args );
   1980 		dom.wrapper.dispatchEvent( event );
   1981 
   1982 		// If we're in an iframe, post each reveal.js event to the
   1983 		// parent window. Used by the notes plugin
   1984 		if( config.postMessageEvents && window.parent !== window.self ) {
   1985 			window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' );
   1986 		}
   1987 
   1988 	}
   1989 
   1990 	/**
   1991 	 * Wrap all links in 3D goodness.
   1992 	 */
   1993 	function enableRollingLinks() {
   1994 
   1995 		if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) {
   1996 			var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' );
   1997 
   1998 			for( var i = 0, len = anchors.length; i < len; i++ ) {
   1999 				var anchor = anchors[i];
   2000 
   2001 				if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) {
   2002 					var span = document.createElement('span');
   2003 					span.setAttribute('data-title', anchor.text);
   2004 					span.innerHTML = anchor.innerHTML;
   2005 
   2006 					anchor.classList.add( 'roll' );
   2007 					anchor.innerHTML = '';
   2008 					anchor.appendChild(span);
   2009 				}
   2010 			}
   2011 		}
   2012 
   2013 	}
   2014 
   2015 	/**
   2016 	 * Unwrap all 3D links.
   2017 	 */
   2018 	function disableRollingLinks() {
   2019 
   2020 		var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' );
   2021 
   2022 		for( var i = 0, len = anchors.length; i < len; i++ ) {
   2023 			var anchor = anchors[i];
   2024 			var span = anchor.querySelector( 'span' );
   2025 
   2026 			if( span ) {
   2027 				anchor.classList.remove( 'roll' );
   2028 				anchor.innerHTML = span.innerHTML;
   2029 			}
   2030 		}
   2031 
   2032 	}
   2033 
   2034 	/**
   2035 	 * Bind preview frame links.
   2036 	 *
   2037 	 * @param {string} [selector=a] - selector for anchors
   2038 	 */
   2039 	function enablePreviewLinks( selector ) {
   2040 
   2041 		var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
   2042 
   2043 		anchors.forEach( function( element ) {
   2044 			if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
   2045 				element.addEventListener( 'click', onPreviewLinkClicked, false );
   2046 			}
   2047 		} );
   2048 
   2049 	}
   2050 
   2051 	/**
   2052 	 * Unbind preview frame links.
   2053 	 */
   2054 	function disablePreviewLinks( selector ) {
   2055 
   2056 		var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) );
   2057 
   2058 		anchors.forEach( function( element ) {
   2059 			if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) {
   2060 				element.removeEventListener( 'click', onPreviewLinkClicked, false );
   2061 			}
   2062 		} );
   2063 
   2064 	}
   2065 
   2066 	/**
   2067 	 * Opens a preview window for the target URL.
   2068 	 *
   2069 	 * @param {string} url - url for preview iframe src
   2070 	 */
   2071 	function showPreview( url ) {
   2072 
   2073 		closeOverlay();
   2074 
   2075 		dom.overlay = document.createElement( 'div' );
   2076 		dom.overlay.classList.add( 'overlay' );
   2077 		dom.overlay.classList.add( 'overlay-preview' );
   2078 		dom.wrapper.appendChild( dom.overlay );
   2079 
   2080 		dom.overlay.innerHTML = [
   2081 			'<header>',
   2082 				'<a class="close" href="#"><span class="icon"></span></a>',
   2083 				'<a class="external" href="'+ url +'" target="_blank"><span class="icon"></span></a>',
   2084 			'</header>',
   2085 			'<div class="spinner"></div>',
   2086 			'<div class="viewport">',
   2087 				'<iframe src="'+ url +'"></iframe>',
   2088 				'<small class="viewport-inner">',
   2089 					'<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
   2090 				'</small>',
   2091 			'</div>'
   2092 		].join('');
   2093 
   2094 		dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) {
   2095 			dom.overlay.classList.add( 'loaded' );
   2096 		}, false );
   2097 
   2098 		dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
   2099 			closeOverlay();
   2100 			event.preventDefault();
   2101 		}, false );
   2102 
   2103 		dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) {
   2104 			closeOverlay();
   2105 		}, false );
   2106 
   2107 		setTimeout( function() {
   2108 			dom.overlay.classList.add( 'visible' );
   2109 		}, 1 );
   2110 
   2111 	}
   2112 
   2113 	/**
   2114 	 * Open or close help overlay window.
   2115 	 *
   2116 	 * @param {Boolean} [override] Flag which overrides the
   2117 	 * toggle logic and forcibly sets the desired state. True means
   2118 	 * help is open, false means it's closed.
   2119 	 */
   2120 	function toggleHelp( override ){
   2121 
   2122 		if( typeof override === 'boolean' ) {
   2123 			override ? showHelp() : closeOverlay();
   2124 		}
   2125 		else {
   2126 			if( dom.overlay ) {
   2127 				closeOverlay();
   2128 			}
   2129 			else {
   2130 				showHelp();
   2131 			}
   2132 		}
   2133 	}
   2134 
   2135 	/**
   2136 	 * Opens an overlay window with help material.
   2137 	 */
   2138 	function showHelp() {
   2139 
   2140 		if( config.help ) {
   2141 
   2142 			closeOverlay();
   2143 
   2144 			dom.overlay = document.createElement( 'div' );
   2145 			dom.overlay.classList.add( 'overlay' );
   2146 			dom.overlay.classList.add( 'overlay-help' );
   2147 			dom.wrapper.appendChild( dom.overlay );
   2148 
   2149 			var html = '<p class="title">Keyboard Shortcuts</p><br/>';
   2150 
   2151 			html += '<table><th>KEY</th><th>ACTION</th>';
   2152 			for( var key in keyboardShortcuts ) {
   2153 				html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[ key ] + '</td></tr>';
   2154 			}
   2155 
   2156 			// Add custom key bindings that have associated descriptions
   2157 			for( var binding in registeredKeyBindings ) {
   2158 				if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
   2159 					html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
   2160 				}
   2161 			}
   2162 
   2163 			html += '</table>';
   2164 
   2165 			dom.overlay.innerHTML = [
   2166 				'<header>',
   2167 					'<a class="close" href="#"><span class="icon"></span></a>',
   2168 				'</header>',
   2169 				'<div class="viewport">',
   2170 					'<div class="viewport-inner">'+ html +'</div>',
   2171 				'</div>'
   2172 			].join('');
   2173 
   2174 			dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) {
   2175 				closeOverlay();
   2176 				event.preventDefault();
   2177 			}, false );
   2178 
   2179 			setTimeout( function() {
   2180 				dom.overlay.classList.add( 'visible' );
   2181 			}, 1 );
   2182 
   2183 		}
   2184 
   2185 	}
   2186 
   2187 	/**
   2188 	 * Closes any currently open overlay.
   2189 	 */
   2190 	function closeOverlay() {
   2191 
   2192 		if( dom.overlay ) {
   2193 			dom.overlay.parentNode.removeChild( dom.overlay );
   2194 			dom.overlay = null;
   2195 		}
   2196 
   2197 	}
   2198 
   2199 	/**
   2200 	 * Applies JavaScript-controlled layout rules to the
   2201 	 * presentation.
   2202 	 */
   2203 	function layout() {
   2204 
   2205 		if( dom.wrapper && !isPrintingPDF() ) {
   2206 
   2207 			if( !config.disableLayout ) {
   2208 
   2209 				// On some mobile devices '100vh' is taller than the visible
   2210 				// viewport which leads to part of the presentation being
   2211 				// cut off. To work around this we define our own '--vh' custom
   2212 				// property where 100x adds up to the correct height.
   2213 				//
   2214 				// https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
   2215 				if( isMobileDevice ) {
   2216 					document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
   2217 				}
   2218 
   2219 				var size = getComputedSlideSize();
   2220 
   2221 				var oldScale = scale;
   2222 
   2223 				// Layout the contents of the slides
   2224 				layoutSlideContents( config.width, config.height );
   2225 
   2226 				dom.slides.style.width = size.width + 'px';
   2227 				dom.slides.style.height = size.height + 'px';
   2228 
   2229 				// Determine scale of content to fit within available space
   2230 				scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height );
   2231 
   2232 				// Respect max/min scale settings
   2233 				scale = Math.max( scale, config.minScale );
   2234 				scale = Math.min( scale, config.maxScale );
   2235 
   2236 				// Don't apply any scaling styles if scale is 1
   2237 				if( scale === 1 ) {
   2238 					dom.slides.style.zoom = '';
   2239 					dom.slides.style.left = '';
   2240 					dom.slides.style.top = '';
   2241 					dom.slides.style.bottom = '';
   2242 					dom.slides.style.right = '';
   2243 					transformSlides( { layout: '' } );
   2244 				}
   2245 				else {
   2246 					// Prefer zoom for scaling up so that content remains crisp.
   2247 					// Don't use zoom to scale down since that can lead to shifts
   2248 					// in text layout/line breaks.
   2249 					if( scale > 1 && features.zoom ) {
   2250 						dom.slides.style.zoom = scale;
   2251 						dom.slides.style.left = '';
   2252 						dom.slides.style.top = '';
   2253 						dom.slides.style.bottom = '';
   2254 						dom.slides.style.right = '';
   2255 						transformSlides( { layout: '' } );
   2256 					}
   2257 					// Apply scale transform as a fallback
   2258 					else {
   2259 						dom.slides.style.zoom = '';
   2260 						dom.slides.style.left = '50%';
   2261 						dom.slides.style.top = '50%';
   2262 						dom.slides.style.bottom = 'auto';
   2263 						dom.slides.style.right = 'auto';
   2264 						transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } );
   2265 					}
   2266 				}
   2267 
   2268 				// Select all slides, vertical and horizontal
   2269 				var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) );
   2270 
   2271 				for( var i = 0, len = slides.length; i < len; i++ ) {
   2272 					var slide = slides[ i ];
   2273 
   2274 					// Don't bother updating invisible slides
   2275 					if( slide.style.display === 'none' ) {
   2276 						continue;
   2277 					}
   2278 
   2279 					if( config.center || slide.classList.contains( 'center' ) ) {
   2280 						// Vertical stacks are not centred since their section
   2281 						// children will be
   2282 						if( slide.classList.contains( 'stack' ) ) {
   2283 							slide.style.top = 0;
   2284 						}
   2285 						else {
   2286 							slide.style.top = Math.max( ( size.height - slide.scrollHeight ) / 2, 0 ) + 'px';
   2287 						}
   2288 					}
   2289 					else {
   2290 						slide.style.top = '';
   2291 					}
   2292 
   2293 				}
   2294 
   2295 				if( oldScale !== scale ) {
   2296 					dispatchEvent( 'resize', {
   2297 						'oldScale': oldScale,
   2298 						'scale': scale,
   2299 						'size': size
   2300 					} );
   2301 				}
   2302 			}
   2303 
   2304 			updateProgress();
   2305 			updateParallax();
   2306 
   2307 			if( isOverview() ) {
   2308 				updateOverview();
   2309 			}
   2310 
   2311 		}
   2312 
   2313 	}
   2314 
   2315 	/**
   2316 	 * Applies layout logic to the contents of all slides in
   2317 	 * the presentation.
   2318 	 *
   2319 	 * @param {string|number} width
   2320 	 * @param {string|number} height
   2321 	 */
   2322 	function layoutSlideContents( width, height ) {
   2323 
   2324 		// Handle sizing of elements with the 'stretch' class
   2325 		toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) {
   2326 
   2327 			// Determine how much vertical space we can use
   2328 			var remainingHeight = getRemainingHeight( element, height );
   2329 
   2330 			// Consider the aspect ratio of media elements
   2331 			if( /(img|video)/gi.test( element.nodeName ) ) {
   2332 				var nw = element.naturalWidth || element.videoWidth,
   2333 					nh = element.naturalHeight || element.videoHeight;
   2334 
   2335 				var es = Math.min( width / nw, remainingHeight / nh );
   2336 
   2337 				element.style.width = ( nw * es ) + 'px';
   2338 				element.style.height = ( nh * es ) + 'px';
   2339 
   2340 			}
   2341 			else {
   2342 				element.style.width = width + 'px';
   2343 				element.style.height = remainingHeight + 'px';
   2344 			}
   2345 
   2346 		} );
   2347 
   2348 	}
   2349 
   2350 	/**
   2351 	 * Calculates the computed pixel size of our slides. These
   2352 	 * values are based on the width and height configuration
   2353 	 * options.
   2354 	 *
   2355 	 * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
   2356 	 * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
   2357 	 */
   2358 	function getComputedSlideSize( presentationWidth, presentationHeight ) {
   2359 
   2360 		var size = {
   2361 			// Slide size
   2362 			width: config.width,
   2363 			height: config.height,
   2364 
   2365 			// Presentation size
   2366 			presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
   2367 			presentationHeight: presentationHeight || dom.wrapper.offsetHeight
   2368 		};
   2369 
   2370 		// Reduce available space by margin
   2371 		size.presentationWidth -= ( size.presentationWidth * config.margin );
   2372 		size.presentationHeight -= ( size.presentationHeight * config.margin );
   2373 
   2374 		// Slide width may be a percentage of available width
   2375 		if( typeof size.width === 'string' && /%$/.test( size.width ) ) {
   2376 			size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth;
   2377 		}
   2378 
   2379 		// Slide height may be a percentage of available height
   2380 		if( typeof size.height === 'string' && /%$/.test( size.height ) ) {
   2381 			size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight;
   2382 		}
   2383 
   2384 		return size;
   2385 
   2386 	}
   2387 
   2388 	/**
   2389 	 * Stores the vertical index of a stack so that the same
   2390 	 * vertical slide can be selected when navigating to and
   2391 	 * from the stack.
   2392 	 *
   2393 	 * @param {HTMLElement} stack The vertical stack element
   2394 	 * @param {string|number} [v=0] Index to memorize
   2395 	 */
   2396 	function setPreviousVerticalIndex( stack, v ) {
   2397 
   2398 		if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) {
   2399 			stack.setAttribute( 'data-previous-indexv', v || 0 );
   2400 		}
   2401 
   2402 	}
   2403 
   2404 	/**
   2405 	 * Retrieves the vertical index which was stored using
   2406 	 * #setPreviousVerticalIndex() or 0 if no previous index
   2407 	 * exists.
   2408 	 *
   2409 	 * @param {HTMLElement} stack The vertical stack element
   2410 	 */
   2411 	function getPreviousVerticalIndex( stack ) {
   2412 
   2413 		if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) {
   2414 			// Prefer manually defined start-indexv
   2415 			var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv';
   2416 
   2417 			return parseInt( stack.getAttribute( attributeName ) || 0, 10 );
   2418 		}
   2419 
   2420 		return 0;
   2421 
   2422 	}
   2423 
   2424 	/**
   2425 	 * Displays the overview of slides (quick nav) by scaling
   2426 	 * down and arranging all slide elements.
   2427 	 */
   2428 	function activateOverview() {
   2429 
   2430 		// Only proceed if enabled in config
   2431 		if( config.overview && !isOverview() ) {
   2432 
   2433 			overview = true;
   2434 
   2435 			dom.wrapper.classList.add( 'overview' );
   2436 			dom.wrapper.classList.remove( 'overview-deactivating' );
   2437 
   2438 			if( features.overviewTransitions ) {
   2439 				setTimeout( function() {
   2440 					dom.wrapper.classList.add( 'overview-animated' );
   2441 				}, 1 );
   2442 			}
   2443 
   2444 			// Don't auto-slide while in overview mode
   2445 			cancelAutoSlide();
   2446 
   2447 			// Move the backgrounds element into the slide container to
   2448 			// that the same scaling is applied
   2449 			dom.slides.appendChild( dom.background );
   2450 
   2451 			// Clicking on an overview slide navigates to it
   2452 			toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
   2453 				if( !slide.classList.contains( 'stack' ) ) {
   2454 					slide.addEventListener( 'click', onOverviewSlideClicked, true );
   2455 				}
   2456 			} );
   2457 
   2458 			// Calculate slide sizes
   2459 			var margin = 70;
   2460 			var slideSize = getComputedSlideSize();
   2461 			overviewSlideWidth = slideSize.width + margin;
   2462 			overviewSlideHeight = slideSize.height + margin;
   2463 
   2464 			// Reverse in RTL mode
   2465 			if( config.rtl ) {
   2466 				overviewSlideWidth = -overviewSlideWidth;
   2467 			}
   2468 
   2469 			updateSlidesVisibility();
   2470 			layoutOverview();
   2471 			updateOverview();
   2472 
   2473 			layout();
   2474 
   2475 			// Notify observers of the overview showing
   2476 			dispatchEvent( 'overviewshown', {
   2477 				'indexh': indexh,
   2478 				'indexv': indexv,
   2479 				'currentSlide': currentSlide
   2480 			} );
   2481 
   2482 		}
   2483 
   2484 	}
   2485 
   2486 	/**
   2487 	 * Uses CSS transforms to position all slides in a grid for
   2488 	 * display inside of the overview mode.
   2489 	 */
   2490 	function layoutOverview() {
   2491 
   2492 		// Layout slides
   2493 		toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) {
   2494 			hslide.setAttribute( 'data-index-h', h );
   2495 			transformElement( hslide, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
   2496 
   2497 			if( hslide.classList.contains( 'stack' ) ) {
   2498 
   2499 				toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) {
   2500 					vslide.setAttribute( 'data-index-h', h );
   2501 					vslide.setAttribute( 'data-index-v', v );
   2502 
   2503 					transformElement( vslide, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
   2504 				} );
   2505 
   2506 			}
   2507 		} );
   2508 
   2509 		// Layout slide backgrounds
   2510 		toArray( dom.background.childNodes ).forEach( function( hbackground, h ) {
   2511 			transformElement( hbackground, 'translate3d(' + ( h * overviewSlideWidth ) + 'px, 0, 0)' );
   2512 
   2513 			toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) {
   2514 				transformElement( vbackground, 'translate3d(0, ' + ( v * overviewSlideHeight ) + 'px, 0)' );
   2515 			} );
   2516 		} );
   2517 
   2518 	}
   2519 
   2520 	/**
   2521 	 * Moves the overview viewport to the current slides.
   2522 	 * Called each time the current slide changes.
   2523 	 */
   2524 	function updateOverview() {
   2525 
   2526 		var vmin = Math.min( window.innerWidth, window.innerHeight );
   2527 		var scale = Math.max( vmin / 5, 150 ) / vmin;
   2528 
   2529 		transformSlides( {
   2530 			overview: [
   2531 				'scale('+ scale +')',
   2532 				'translateX('+ ( -indexh * overviewSlideWidth ) +'px)',
   2533 				'translateY('+ ( -indexv * overviewSlideHeight ) +'px)'
   2534 			].join( ' ' )
   2535 		} );
   2536 
   2537 	}
   2538 
   2539 	/**
   2540 	 * Exits the slide overview and enters the currently
   2541 	 * active slide.
   2542 	 */
   2543 	function deactivateOverview() {
   2544 
   2545 		// Only proceed if enabled in config
   2546 		if( config.overview ) {
   2547 
   2548 			overview = false;
   2549 
   2550 			dom.wrapper.classList.remove( 'overview' );
   2551 			dom.wrapper.classList.remove( 'overview-animated' );
   2552 
   2553 			// Temporarily add a class so that transitions can do different things
   2554 			// depending on whether they are exiting/entering overview, or just
   2555 			// moving from slide to slide
   2556 			dom.wrapper.classList.add( 'overview-deactivating' );
   2557 
   2558 			setTimeout( function () {
   2559 				dom.wrapper.classList.remove( 'overview-deactivating' );
   2560 			}, 1 );
   2561 
   2562 			// Move the background element back out
   2563 			dom.wrapper.appendChild( dom.background );
   2564 
   2565 			// Clean up changes made to slides
   2566 			toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) {
   2567 				transformElement( slide, '' );
   2568 
   2569 				slide.removeEventListener( 'click', onOverviewSlideClicked, true );
   2570 			} );
   2571 
   2572 			// Clean up changes made to backgrounds
   2573 			toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) {
   2574 				transformElement( background, '' );
   2575 			} );
   2576 
   2577 			transformSlides( { overview: '' } );
   2578 
   2579 			slide( indexh, indexv );
   2580 
   2581 			layout();
   2582 
   2583 			cueAutoSlide();
   2584 
   2585 			// Notify observers of the overview hiding
   2586 			dispatchEvent( 'overviewhidden', {
   2587 				'indexh': indexh,
   2588 				'indexv': indexv,
   2589 				'currentSlide': currentSlide
   2590 			} );
   2591 
   2592 		}
   2593 	}
   2594 
   2595 	/**
   2596 	 * Toggles the slide overview mode on and off.
   2597 	 *
   2598 	 * @param {Boolean} [override] Flag which overrides the
   2599 	 * toggle logic and forcibly sets the desired state. True means
   2600 	 * overview is open, false means it's closed.
   2601 	 */
   2602 	function toggleOverview( override ) {
   2603 
   2604 		if( typeof override === 'boolean' ) {
   2605 			override ? activateOverview() : deactivateOverview();
   2606 		}
   2607 		else {
   2608 			isOverview() ? deactivateOverview() : activateOverview();
   2609 		}
   2610 
   2611 	}
   2612 
   2613 	/**
   2614 	 * Checks if the overview is currently active.
   2615 	 *
   2616 	 * @return {Boolean} true if the overview is active,
   2617 	 * false otherwise
   2618 	 */
   2619 	function isOverview() {
   2620 
   2621 		return overview;
   2622 
   2623 	}
   2624 
   2625 	/**
   2626 	 * Return a hash URL that will resolve to the current slide location.
   2627 	 */
   2628 	function locationHash() {
   2629 
   2630 		var url = '/';
   2631 
   2632 		// Attempt to create a named link based on the slide's ID
   2633 		var id = currentSlide ? currentSlide.getAttribute( 'id' ) : null;
   2634 		if( id ) {
   2635 			id = encodeURIComponent( id );
   2636 		}
   2637 
   2638 		var indexf;
   2639 		if( config.fragmentInURL ) {
   2640 			indexf = getIndices().f;
   2641 		}
   2642 
   2643 		// If the current slide has an ID, use that as a named link,
   2644 		// but we don't support named links with a fragment index
   2645 		if( typeof id === 'string' && id.length && indexf === undefined ) {
   2646 			url = '/' + id;
   2647 		}
   2648 		// Otherwise use the /h/v index
   2649 		else {
   2650 			var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
   2651 			if( indexh > 0 || indexv > 0 || indexf !== undefined ) url += indexh + hashIndexBase;
   2652 			if( indexv > 0 || indexf !== undefined ) url += '/' + (indexv + hashIndexBase );
   2653 			if( indexf !== undefined ) url += '/' + indexf;
   2654 		}
   2655 
   2656 		return url;
   2657 
   2658 	}
   2659 
   2660 	/**
   2661 	 * Checks if the current or specified slide is vertical
   2662 	 * (nested within another slide).
   2663 	 *
   2664 	 * @param {HTMLElement} [slide=currentSlide] The slide to check
   2665 	 * orientation of
   2666 	 * @return {Boolean}
   2667 	 */
   2668 	function isVerticalSlide( slide ) {
   2669 
   2670 		// Prefer slide argument, otherwise use current slide
   2671 		slide = slide ? slide : currentSlide;
   2672 
   2673 		return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i );
   2674 
   2675 	}
   2676 
   2677 	/**
   2678 	 * Handling the fullscreen functionality via the fullscreen API
   2679 	 *
   2680 	 * @see http://fullscreen.spec.whatwg.org/
   2681 	 * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
   2682 	 */
   2683 	function enterFullscreen() {
   2684 
   2685 		var element = document.documentElement;
   2686 
   2687 		// Check which implementation is available
   2688 		var requestMethod = element.requestFullscreen ||
   2689 							element.webkitRequestFullscreen ||
   2690 							element.webkitRequestFullScreen ||
   2691 							element.mozRequestFullScreen ||
   2692 							element.msRequestFullscreen;
   2693 
   2694 		if( requestMethod ) {
   2695 			requestMethod.apply( element );
   2696 		}
   2697 
   2698 	}
   2699 
   2700 	/**
   2701 	 * Shows the mouse pointer after it has been hidden with
   2702 	 * #hideCursor.
   2703 	 */
   2704 	function showCursor() {
   2705 
   2706 		if( cursorHidden ) {
   2707 			cursorHidden = false;
   2708 			dom.wrapper.style.cursor = '';
   2709 		}
   2710 
   2711 	}
   2712 
   2713 	/**
   2714 	 * Hides the mouse pointer when it's on top of the .reveal
   2715 	 * container.
   2716 	 */
   2717 	function hideCursor() {
   2718 
   2719 		if( cursorHidden === false ) {
   2720 			cursorHidden = true;
   2721 			dom.wrapper.style.cursor = 'none';
   2722 		}
   2723 
   2724 	}
   2725 
   2726 	/**
   2727 	 * Enters the paused mode which fades everything on screen to
   2728 	 * black.
   2729 	 */
   2730 	function pause() {
   2731 
   2732 		if( config.pause ) {
   2733 			var wasPaused = dom.wrapper.classList.contains( 'paused' );
   2734 
   2735 			cancelAutoSlide();
   2736 			dom.wrapper.classList.add( 'paused' );
   2737 
   2738 			if( wasPaused === false ) {
   2739 				dispatchEvent( 'paused' );
   2740 			}
   2741 		}
   2742 
   2743 	}
   2744 
   2745 	/**
   2746 	 * Exits from the paused mode.
   2747 	 */
   2748 	function resume() {
   2749 
   2750 		var wasPaused = dom.wrapper.classList.contains( 'paused' );
   2751 		dom.wrapper.classList.remove( 'paused' );
   2752 
   2753 		cueAutoSlide();
   2754 
   2755 		if( wasPaused ) {
   2756 			dispatchEvent( 'resumed' );
   2757 		}
   2758 
   2759 	}
   2760 
   2761 	/**
   2762 	 * Toggles the paused mode on and off.
   2763 	 */
   2764 	function togglePause( override ) {
   2765 
   2766 		if( typeof override === 'boolean' ) {
   2767 			override ? pause() : resume();
   2768 		}
   2769 		else {
   2770 			isPaused() ? resume() : pause();
   2771 		}
   2772 
   2773 	}
   2774 
   2775 	/**
   2776 	 * Checks if we are currently in the paused mode.
   2777 	 *
   2778 	 * @return {Boolean}
   2779 	 */
   2780 	function isPaused() {
   2781 
   2782 		return dom.wrapper.classList.contains( 'paused' );
   2783 
   2784 	}
   2785 
   2786 	/**
   2787 	 * Toggles the auto slide mode on and off.
   2788 	 *
   2789 	 * @param {Boolean} [override] Flag which sets the desired state.
   2790 	 * True means autoplay starts, false means it stops.
   2791 	 */
   2792 
   2793 	function toggleAutoSlide( override ) {
   2794 
   2795 		if( typeof override === 'boolean' ) {
   2796 			override ? resumeAutoSlide() : pauseAutoSlide();
   2797 		}
   2798 
   2799 		else {
   2800 			autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
   2801 		}
   2802 
   2803 	}
   2804 
   2805 	/**
   2806 	 * Checks if the auto slide mode is currently on.
   2807 	 *
   2808 	 * @return {Boolean}
   2809 	 */
   2810 	function isAutoSliding() {
   2811 
   2812 		return !!( autoSlide && !autoSlidePaused );
   2813 
   2814 	}
   2815 
   2816 	/**
   2817 	 * Steps from the current point in the presentation to the
   2818 	 * slide which matches the specified horizontal and vertical
   2819 	 * indices.
   2820 	 *
   2821 	 * @param {number} [h=indexh] Horizontal index of the target slide
   2822 	 * @param {number} [v=indexv] Vertical index of the target slide
   2823 	 * @param {number} [f] Index of a fragment within the
   2824 	 * target slide to activate
   2825 	 * @param {number} [o] Origin for use in multimaster environments
   2826 	 */
   2827 	function slide( h, v, f, o ) {
   2828 
   2829 		// Remember where we were at before
   2830 		previousSlide = currentSlide;
   2831 
   2832 		// Query all horizontal slides in the deck
   2833 		var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
   2834 
   2835 		// Abort if there are no slides
   2836 		if( horizontalSlides.length === 0 ) return;
   2837 
   2838 		// If no vertical index is specified and the upcoming slide is a
   2839 		// stack, resume at its previous vertical index
   2840 		if( v === undefined && !isOverview() ) {
   2841 			v = getPreviousVerticalIndex( horizontalSlides[ h ] );
   2842 		}
   2843 
   2844 		// If we were on a vertical stack, remember what vertical index
   2845 		// it was on so we can resume at the same position when returning
   2846 		if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) {
   2847 			setPreviousVerticalIndex( previousSlide.parentNode, indexv );
   2848 		}
   2849 
   2850 		// Remember the state before this slide
   2851 		var stateBefore = state.concat();
   2852 
   2853 		// Reset the state array
   2854 		state.length = 0;
   2855 
   2856 		var indexhBefore = indexh || 0,
   2857 			indexvBefore = indexv || 0;
   2858 
   2859 		// Activate and transition to the new slide
   2860 		indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h );
   2861 		indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v );
   2862 
   2863 		// Update the visibility of slides now that the indices have changed
   2864 		updateSlidesVisibility();
   2865 
   2866 		layout();
   2867 
   2868 		// Update the overview if it's currently active
   2869 		if( isOverview() ) {
   2870 			updateOverview();
   2871 		}
   2872 
   2873 		// Find the current horizontal slide and any possible vertical slides
   2874 		// within it
   2875 		var currentHorizontalSlide = horizontalSlides[ indexh ],
   2876 			currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' );
   2877 
   2878 		// Store references to the previous and current slides
   2879 		currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide;
   2880 
   2881 		// Show fragment, if specified
   2882 		if( typeof f !== 'undefined' ) {
   2883 			navigateFragment( f );
   2884 		}
   2885 
   2886 		// Dispatch an event if the slide changed
   2887 		var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore );
   2888 		if (!slideChanged) {
   2889 			// Ensure that the previous slide is never the same as the current
   2890 			previousSlide = null;
   2891 		}
   2892 
   2893 		// Solves an edge case where the previous slide maintains the
   2894 		// 'present' class when navigating between adjacent vertical
   2895 		// stacks
   2896 		if( previousSlide && previousSlide !== currentSlide ) {
   2897 			previousSlide.classList.remove( 'present' );
   2898 			previousSlide.setAttribute( 'aria-hidden', 'true' );
   2899 
   2900 			// Reset all slides upon navigate to home
   2901 			// Issue: #285
   2902 			if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) {
   2903 				// Launch async task
   2904 				setTimeout( function () {
   2905 					var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i;
   2906 					for( i in slides ) {
   2907 						if( slides[i] ) {
   2908 							// Reset stack
   2909 							setPreviousVerticalIndex( slides[i], 0 );
   2910 						}
   2911 					}
   2912 				}, 0 );
   2913 			}
   2914 		}
   2915 
   2916 		// Apply the new state
   2917 		stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
   2918 			// Check if this state existed on the previous slide. If it
   2919 			// did, we will avoid adding it repeatedly
   2920 			for( var j = 0; j < stateBefore.length; j++ ) {
   2921 				if( stateBefore[j] === state[i] ) {
   2922 					stateBefore.splice( j, 1 );
   2923 					continue stateLoop;
   2924 				}
   2925 			}
   2926 
   2927 			document.documentElement.classList.add( state[i] );
   2928 
   2929 			// Dispatch custom event matching the state's name
   2930 			dispatchEvent( state[i] );
   2931 		}
   2932 
   2933 		// Clean up the remains of the previous state
   2934 		while( stateBefore.length ) {
   2935 			document.documentElement.classList.remove( stateBefore.pop() );
   2936 		}
   2937 
   2938 		if( slideChanged ) {
   2939 			dispatchEvent( 'slidechanged', {
   2940 				'indexh': indexh,
   2941 				'indexv': indexv,
   2942 				'previousSlide': previousSlide,
   2943 				'currentSlide': currentSlide,
   2944 				'origin': o
   2945 			} );
   2946 		}
   2947 
   2948 		// Handle embedded content
   2949 		if( slideChanged || !previousSlide ) {
   2950 			stopEmbeddedContent( previousSlide );
   2951 			startEmbeddedContent( currentSlide );
   2952 		}
   2953 
   2954 		// Announce the current slide contents, for screen readers
   2955 		dom.statusDiv.textContent = getStatusText( currentSlide );
   2956 
   2957 		updateControls();
   2958 		updateProgress();
   2959 		updateBackground();
   2960 		updateParallax();
   2961 		updateSlideNumber();
   2962 		updateNotes();
   2963 		updateFragments();
   2964 
   2965 		// Update the URL hash
   2966 		writeURL();
   2967 
   2968 		cueAutoSlide();
   2969 
   2970 	}
   2971 
   2972 	/**
   2973 	 * Syncs the presentation with the current DOM. Useful
   2974 	 * when new slides or control elements are added or when
   2975 	 * the configuration has changed.
   2976 	 */
   2977 	function sync() {
   2978 
   2979 		// Subscribe to input
   2980 		removeEventListeners();
   2981 		addEventListeners();
   2982 
   2983 		// Force a layout to make sure the current config is accounted for
   2984 		layout();
   2985 
   2986 		// Reflect the current autoSlide value
   2987 		autoSlide = config.autoSlide;
   2988 
   2989 		// Start auto-sliding if it's enabled
   2990 		cueAutoSlide();
   2991 
   2992 		// Re-create the slide backgrounds
   2993 		createBackgrounds();
   2994 
   2995 		// Write the current hash to the URL
   2996 		writeURL();
   2997 
   2998 		sortAllFragments();
   2999 
   3000 		updateControls();
   3001 		updateProgress();
   3002 		updateSlideNumber();
   3003 		updateSlidesVisibility();
   3004 		updateBackground( true );
   3005 		updateNotesVisibility();
   3006 		updateNotes();
   3007 
   3008 		formatEmbeddedContent();
   3009 
   3010 		// Start or stop embedded content depending on global config
   3011 		if( config.autoPlayMedia === false ) {
   3012 			stopEmbeddedContent( currentSlide, { unloadIframes: false } );
   3013 		}
   3014 		else {
   3015 			startEmbeddedContent( currentSlide );
   3016 		}
   3017 
   3018 		if( isOverview() ) {
   3019 			layoutOverview();
   3020 		}
   3021 
   3022 	}
   3023 
   3024 	/**
   3025 	 * Updates reveal.js to keep in sync with new slide attributes. For
   3026 	 * example, if you add a new `data-background-image` you can call
   3027 	 * this to have reveal.js render the new background image.
   3028 	 *
   3029 	 * Similar to #sync() but more efficient when you only need to
   3030 	 * refresh a specific slide.
   3031 	 *
   3032 	 * @param {HTMLElement} slide
   3033 	 */
   3034 	function syncSlide( slide ) {
   3035 
   3036 		// Default to the current slide
   3037 		slide = slide || currentSlide;
   3038 
   3039 		syncBackground( slide );
   3040 		syncFragments( slide );
   3041 
   3042 		updateBackground();
   3043 		updateNotes();
   3044 
   3045 		loadSlide( slide );
   3046 
   3047 	}
   3048 
   3049 	/**
   3050 	 * Formats the fragments on the given slide so that they have
   3051 	 * valid indices. Call this if fragments are changed in the DOM
   3052 	 * after reveal.js has already initialized.
   3053 	 *
   3054 	 * @param {HTMLElement} slide
   3055 	 * @return {Array} a list of the HTML fragments that were synced
   3056 	 */
   3057 	function syncFragments( slide ) {
   3058 
   3059 		// Default to the current slide
   3060 		slide = slide || currentSlide;
   3061 
   3062 		return sortFragments( slide.querySelectorAll( '.fragment' ) );
   3063 
   3064 	}
   3065 
   3066 	/**
   3067 	 * Resets all vertical slides so that only the first
   3068 	 * is visible.
   3069 	 */
   3070 	function resetVerticalSlides() {
   3071 
   3072 		var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
   3073 		horizontalSlides.forEach( function( horizontalSlide ) {
   3074 
   3075 			var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
   3076 			verticalSlides.forEach( function( verticalSlide, y ) {
   3077 
   3078 				if( y > 0 ) {
   3079 					verticalSlide.classList.remove( 'present' );
   3080 					verticalSlide.classList.remove( 'past' );
   3081 					verticalSlide.classList.add( 'future' );
   3082 					verticalSlide.setAttribute( 'aria-hidden', 'true' );
   3083 				}
   3084 
   3085 			} );
   3086 
   3087 		} );
   3088 
   3089 	}
   3090 
   3091 	/**
   3092 	 * Sorts and formats all of fragments in the
   3093 	 * presentation.
   3094 	 */
   3095 	function sortAllFragments() {
   3096 
   3097 		var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
   3098 		horizontalSlides.forEach( function( horizontalSlide ) {
   3099 
   3100 			var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
   3101 			verticalSlides.forEach( function( verticalSlide, y ) {
   3102 
   3103 				sortFragments( verticalSlide.querySelectorAll( '.fragment' ) );
   3104 
   3105 			} );
   3106 
   3107 			if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) );
   3108 
   3109 		} );
   3110 
   3111 	}
   3112 
   3113 	/**
   3114 	 * Randomly shuffles all slides in the deck.
   3115 	 */
   3116 	function shuffle() {
   3117 
   3118 		var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
   3119 
   3120 		slides.forEach( function( slide ) {
   3121 
   3122 			// Insert this slide next to another random slide. This may
   3123 			// cause the slide to insert before itself but that's fine.
   3124 			dom.slides.insertBefore( slide, slides[ Math.floor( Math.random() * slides.length ) ] );
   3125 
   3126 		} );
   3127 
   3128 	}
   3129 
   3130 	/**
   3131 	 * Updates one dimension of slides by showing the slide
   3132 	 * with the specified index.
   3133 	 *
   3134 	 * @param {string} selector A CSS selector that will fetch
   3135 	 * the group of slides we are working with
   3136 	 * @param {number} index The index of the slide that should be
   3137 	 * shown
   3138 	 *
   3139 	 * @return {number} The index of the slide that is now shown,
   3140 	 * might differ from the passed in index if it was out of
   3141 	 * bounds.
   3142 	 */
   3143 	function updateSlides( selector, index ) {
   3144 
   3145 		// Select all slides and convert the NodeList result to
   3146 		// an array
   3147 		var slides = toArray( dom.wrapper.querySelectorAll( selector ) ),
   3148 			slidesLength = slides.length;
   3149 
   3150 		var printMode = isPrintingPDF();
   3151 
   3152 		if( slidesLength ) {
   3153 
   3154 			// Should the index loop?
   3155 			if( config.loop ) {
   3156 				index %= slidesLength;
   3157 
   3158 				if( index < 0 ) {
   3159 					index = slidesLength + index;
   3160 				}
   3161 			}
   3162 
   3163 			// Enforce max and minimum index bounds
   3164 			index = Math.max( Math.min( index, slidesLength - 1 ), 0 );
   3165 
   3166 			for( var i = 0; i < slidesLength; i++ ) {
   3167 				var element = slides[i];
   3168 
   3169 				var reverse = config.rtl && !isVerticalSlide( element );
   3170 
   3171 				element.classList.remove( 'past' );
   3172 				element.classList.remove( 'present' );
   3173 				element.classList.remove( 'future' );
   3174 
   3175 				// http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
   3176 				element.setAttribute( 'hidden', '' );
   3177 				element.setAttribute( 'aria-hidden', 'true' );
   3178 
   3179 				// If this element contains vertical slides
   3180 				if( element.querySelector( 'section' ) ) {
   3181 					element.classList.add( 'stack' );
   3182 				}
   3183 
   3184 				// If we're printing static slides, all slides are "present"
   3185 				if( printMode ) {
   3186 					element.classList.add( 'present' );
   3187 					continue;
   3188 				}
   3189 
   3190 				if( i < index ) {
   3191 					// Any element previous to index is given the 'past' class
   3192 					element.classList.add( reverse ? 'future' : 'past' );
   3193 
   3194 					if( config.fragments ) {
   3195 						// Show all fragments in prior slides
   3196 						toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
   3197 							fragment.classList.add( 'visible' );
   3198 							fragment.classList.remove( 'current-fragment' );
   3199 						} );
   3200 					}
   3201 				}
   3202 				else if( i > index ) {
   3203 					// Any element subsequent to index is given the 'future' class
   3204 					element.classList.add( reverse ? 'past' : 'future' );
   3205 
   3206 					if( config.fragments ) {
   3207 						// Hide all fragments in future slides
   3208 						toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
   3209 							fragment.classList.remove( 'visible' );
   3210 							fragment.classList.remove( 'current-fragment' );
   3211 						} );
   3212 					}
   3213 				}
   3214 			}
   3215 
   3216 			// Mark the current slide as present
   3217 			slides[index].classList.add( 'present' );
   3218 			slides[index].removeAttribute( 'hidden' );
   3219 			slides[index].removeAttribute( 'aria-hidden' );
   3220 
   3221 			// If this slide has a state associated with it, add it
   3222 			// onto the current state of the deck
   3223 			var slideState = slides[index].getAttribute( 'data-state' );
   3224 			if( slideState ) {
   3225 				state = state.concat( slideState.split( ' ' ) );
   3226 			}
   3227 
   3228 		}
   3229 		else {
   3230 			// Since there are no slides we can't be anywhere beyond the
   3231 			// zeroth index
   3232 			index = 0;
   3233 		}
   3234 
   3235 		return index;
   3236 
   3237 	}
   3238 
   3239 	/**
   3240 	 * Optimization method; hide all slides that are far away
   3241 	 * from the present slide.
   3242 	 */
   3243 	function updateSlidesVisibility() {
   3244 
   3245 		// Select all slides and convert the NodeList result to
   3246 		// an array
   3247 		var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ),
   3248 			horizontalSlidesLength = horizontalSlides.length,
   3249 			distanceX,
   3250 			distanceY;
   3251 
   3252 		if( horizontalSlidesLength && typeof indexh !== 'undefined' ) {
   3253 
   3254 			// The number of steps away from the present slide that will
   3255 			// be visible
   3256 			var viewDistance = isOverview() ? 10 : config.viewDistance;
   3257 
   3258 			// Limit view distance on weaker devices
   3259 			if( isMobileDevice ) {
   3260 				viewDistance = isOverview() ? 6 : 2;
   3261 			}
   3262 
   3263 			// All slides need to be visible when exporting to PDF
   3264 			if( isPrintingPDF() ) {
   3265 				viewDistance = Number.MAX_VALUE;
   3266 			}
   3267 
   3268 			for( var x = 0; x < horizontalSlidesLength; x++ ) {
   3269 				var horizontalSlide = horizontalSlides[x];
   3270 
   3271 				var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ),
   3272 					verticalSlidesLength = verticalSlides.length;
   3273 
   3274 				// Determine how far away this slide is from the present
   3275 				distanceX = Math.abs( ( indexh || 0 ) - x ) || 0;
   3276 
   3277 				// If the presentation is looped, distance should measure
   3278 				// 1 between the first and last slides
   3279 				if( config.loop ) {
   3280 					distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0;
   3281 				}
   3282 
   3283 				// Show the horizontal slide if it's within the view distance
   3284 				if( distanceX < viewDistance ) {
   3285 					loadSlide( horizontalSlide );
   3286 				}
   3287 				else {
   3288 					unloadSlide( horizontalSlide );
   3289 				}
   3290 
   3291 				if( verticalSlidesLength ) {
   3292 
   3293 					var oy = getPreviousVerticalIndex( horizontalSlide );
   3294 
   3295 					for( var y = 0; y < verticalSlidesLength; y++ ) {
   3296 						var verticalSlide = verticalSlides[y];
   3297 
   3298 						distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy );
   3299 
   3300 						if( distanceX + distanceY < viewDistance ) {
   3301 							loadSlide( verticalSlide );
   3302 						}
   3303 						else {
   3304 							unloadSlide( verticalSlide );
   3305 						}
   3306 					}
   3307 
   3308 				}
   3309 			}
   3310 
   3311 			// Flag if there are ANY vertical slides, anywhere in the deck
   3312 			if( dom.wrapper.querySelectorAll( '.slides>section>section' ).length ) {
   3313 				dom.wrapper.classList.add( 'has-vertical-slides' );
   3314 			}
   3315 			else {
   3316 				dom.wrapper.classList.remove( 'has-vertical-slides' );
   3317 			}
   3318 
   3319 			// Flag if there are ANY horizontal slides, anywhere in the deck
   3320 			if( dom.wrapper.querySelectorAll( '.slides>section' ).length > 1 ) {
   3321 				dom.wrapper.classList.add( 'has-horizontal-slides' );
   3322 			}
   3323 			else {
   3324 				dom.wrapper.classList.remove( 'has-horizontal-slides' );
   3325 			}
   3326 
   3327 		}
   3328 
   3329 	}
   3330 
   3331 	/**
   3332 	 * Pick up notes from the current slide and display them
   3333 	 * to the viewer.
   3334 	 *
   3335 	 * @see {@link config.showNotes}
   3336 	 */
   3337 	function updateNotes() {
   3338 
   3339 		if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) {
   3340 
   3341 			dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
   3342 
   3343 		}
   3344 
   3345 	}
   3346 
   3347 	/**
   3348 	 * Updates the visibility of the speaker notes sidebar that
   3349 	 * is used to share annotated slides. The notes sidebar is
   3350 	 * only visible if showNotes is true and there are notes on
   3351 	 * one or more slides in the deck.
   3352 	 */
   3353 	function updateNotesVisibility() {
   3354 
   3355 		if( config.showNotes && hasNotes() ) {
   3356 			dom.wrapper.classList.add( 'show-notes' );
   3357 		}
   3358 		else {
   3359 			dom.wrapper.classList.remove( 'show-notes' );
   3360 		}
   3361 
   3362 	}
   3363 
   3364 	/**
   3365 	 * Checks if there are speaker notes for ANY slide in the
   3366 	 * presentation.
   3367 	 */
   3368 	function hasNotes() {
   3369 
   3370 		return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0;
   3371 
   3372 	}
   3373 
   3374 	/**
   3375 	 * Updates the progress bar to reflect the current slide.
   3376 	 */
   3377 	function updateProgress() {
   3378 
   3379 		// Update progress if enabled
   3380 		if( config.progress && dom.progressbar ) {
   3381 
   3382 			dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';
   3383 
   3384 		}
   3385 
   3386 	}
   3387 
   3388 
   3389 	/**
   3390 	 * Updates the slide number to match the current slide.
   3391 	 */
   3392 	function updateSlideNumber() {
   3393 
   3394 		// Update slide number if enabled
   3395 		if( config.slideNumber && dom.slideNumber ) {
   3396 
   3397 			var value;
   3398 			var format = 'h.v';
   3399 
   3400 			if( typeof config.slideNumber === 'function' ) {
   3401 				value = config.slideNumber();
   3402 			}
   3403 			else {
   3404 				// Check if a custom number format is available
   3405 				if( typeof config.slideNumber === 'string' ) {
   3406 					format = config.slideNumber;
   3407 				}
   3408 
   3409 				// If there are ONLY vertical slides in this deck, always use
   3410 				// a flattened slide number
   3411 				if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
   3412 					format = 'c';
   3413 				}
   3414 
   3415 				value = [];
   3416 				switch( format ) {
   3417 					case 'c':
   3418 						value.push( getSlidePastCount() + 1 );
   3419 						break;
   3420 					case 'c/t':
   3421 						value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
   3422 						break;
   3423 					case 'h/v':
   3424 						value.push( indexh + 1 );
   3425 						if( isVerticalSlide() ) value.push( '/', indexv + 1 );
   3426 						break;
   3427 					default:
   3428 						value.push( indexh + 1 );
   3429 						if( isVerticalSlide() ) value.push( '.', indexv + 1 );
   3430 				}
   3431 			}
   3432 
   3433 			dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
   3434 		}
   3435 
   3436 	}
   3437 
   3438 	/**
   3439 	 * Applies HTML formatting to a slide number before it's
   3440 	 * written to the DOM.
   3441 	 *
   3442 	 * @param {number} a Current slide
   3443 	 * @param {string} delimiter Character to separate slide numbers
   3444 	 * @param {(number|*)} b Total slides
   3445 	 * @return {string} HTML string fragment
   3446 	 */
   3447 	function formatSlideNumber( a, delimiter, b ) {
   3448 
   3449 		var url = '#' + locationHash();
   3450 		if( typeof b === 'number' && !isNaN( b ) ) {
   3451 			return  '<a href="' + url + '">' +
   3452 					'<span class="slide-number-a">'+ a +'</span>' +
   3453 					'<span class="slide-number-delimiter">'+ delimiter +'</span>' +
   3454 					'<span class="slide-number-b">'+ b +'</span>' +
   3455 					'</a>';
   3456 		}
   3457 		else {
   3458 			return '<a href="' + url + '">' +
   3459 			       '<span class="slide-number-a">'+ a +'</span>' +
   3460 			       '</a>';
   3461 		}
   3462 
   3463 	}
   3464 
   3465 	/**
   3466 	 * Updates the state of all control/navigation arrows.
   3467 	 */
   3468 	function updateControls() {
   3469 
   3470 		var routes = availableRoutes();
   3471 		var fragments = availableFragments();
   3472 
   3473 		// Remove the 'enabled' class from all directions
   3474 		dom.controlsLeft.concat( dom.controlsRight )
   3475 						.concat( dom.controlsUp )
   3476 						.concat( dom.controlsDown )
   3477 						.concat( dom.controlsPrev )
   3478 						.concat( dom.controlsNext ).forEach( function( node ) {
   3479 			node.classList.remove( 'enabled' );
   3480 			node.classList.remove( 'fragmented' );
   3481 
   3482 			// Set 'disabled' attribute on all directions
   3483 			node.setAttribute( 'disabled', 'disabled' );
   3484 		} );
   3485 
   3486 		// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
   3487 		if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3488 		if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3489 		if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3490 		if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3491 
   3492 		// Prev/next buttons
   3493 		if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3494 		if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3495 
   3496 		// Highlight fragment directions
   3497 		if( currentSlide ) {
   3498 
   3499 			// Always apply fragment decorator to prev/next buttons
   3500 			if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3501 			if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3502 
   3503 			// Apply fragment decorators to directional buttons based on
   3504 			// what slide axis they are in
   3505 			if( isVerticalSlide( currentSlide ) ) {
   3506 				if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3507 				if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3508 			}
   3509 			else {
   3510 				if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3511 				if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
   3512 			}
   3513 
   3514 		}
   3515 
   3516 		if( config.controlsTutorial ) {
   3517 
   3518 			// Highlight control arrows with an animation to ensure
   3519 			// that the viewer knows how to navigate
   3520 			if( !hasNavigatedDown && routes.down ) {
   3521 				dom.controlsDownArrow.classList.add( 'highlight' );
   3522 			}
   3523 			else {
   3524 				dom.controlsDownArrow.classList.remove( 'highlight' );
   3525 
   3526 				if( !hasNavigatedRight && routes.right && indexv === 0 ) {
   3527 					dom.controlsRightArrow.classList.add( 'highlight' );
   3528 				}
   3529 				else {
   3530 					dom.controlsRightArrow.classList.remove( 'highlight' );
   3531 				}
   3532 			}
   3533 
   3534 		}
   3535 
   3536 	}
   3537 
   3538 	/**
   3539 	 * Updates the background elements to reflect the current
   3540 	 * slide.
   3541 	 *
   3542 	 * @param {boolean} includeAll If true, the backgrounds of
   3543 	 * all vertical slides (not just the present) will be updated.
   3544 	 */
   3545 	function updateBackground( includeAll ) {
   3546 
   3547 		var currentBackground = null;
   3548 
   3549 		// Reverse past/future classes when in RTL mode
   3550 		var horizontalPast = config.rtl ? 'future' : 'past',
   3551 			horizontalFuture = config.rtl ? 'past' : 'future';
   3552 
   3553 		// Update the classes of all backgrounds to match the
   3554 		// states of their slides (past/present/future)
   3555 		toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) {
   3556 
   3557 			backgroundh.classList.remove( 'past' );
   3558 			backgroundh.classList.remove( 'present' );
   3559 			backgroundh.classList.remove( 'future' );
   3560 
   3561 			if( h < indexh ) {
   3562 				backgroundh.classList.add( horizontalPast );
   3563 			}
   3564 			else if ( h > indexh ) {
   3565 				backgroundh.classList.add( horizontalFuture );
   3566 			}
   3567 			else {
   3568 				backgroundh.classList.add( 'present' );
   3569 
   3570 				// Store a reference to the current background element
   3571 				currentBackground = backgroundh;
   3572 			}
   3573 
   3574 			if( includeAll || h === indexh ) {
   3575 				toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) {
   3576 
   3577 					backgroundv.classList.remove( 'past' );
   3578 					backgroundv.classList.remove( 'present' );
   3579 					backgroundv.classList.remove( 'future' );
   3580 
   3581 					if( v < indexv ) {
   3582 						backgroundv.classList.add( 'past' );
   3583 					}
   3584 					else if ( v > indexv ) {
   3585 						backgroundv.classList.add( 'future' );
   3586 					}
   3587 					else {
   3588 						backgroundv.classList.add( 'present' );
   3589 
   3590 						// Only if this is the present horizontal and vertical slide
   3591 						if( h === indexh ) currentBackground = backgroundv;
   3592 					}
   3593 
   3594 				} );
   3595 			}
   3596 
   3597 		} );
   3598 
   3599 		// Stop content inside of previous backgrounds
   3600 		if( previousBackground ) {
   3601 
   3602 			stopEmbeddedContent( previousBackground );
   3603 
   3604 		}
   3605 
   3606 		// Start content in the current background
   3607 		if( currentBackground ) {
   3608 
   3609 			startEmbeddedContent( currentBackground );
   3610 
   3611 			var currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
   3612 			if( currentBackgroundContent ) {
   3613 
   3614 				var backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
   3615 
   3616 				// Restart GIFs (doesn't work in Firefox)
   3617 				if( /\.gif/i.test( backgroundImageURL ) ) {
   3618 					currentBackgroundContent.style.backgroundImage = '';
   3619 					window.getComputedStyle( currentBackgroundContent ).opacity;
   3620 					currentBackgroundContent.style.backgroundImage = backgroundImageURL;
   3621 				}
   3622 
   3623 			}
   3624 
   3625 			// Don't transition between identical backgrounds. This
   3626 			// prevents unwanted flicker.
   3627 			var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null;
   3628 			var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
   3629 			if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) {
   3630 				dom.background.classList.add( 'no-transition' );
   3631 			}
   3632 
   3633 			previousBackground = currentBackground;
   3634 
   3635 		}
   3636 
   3637 		// If there's a background brightness flag for this slide,
   3638 		// bubble it to the .reveal container
   3639 		if( currentSlide ) {
   3640 			[ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) {
   3641 				if( currentSlide.classList.contains( classToBubble ) ) {
   3642 					dom.wrapper.classList.add( classToBubble );
   3643 				}
   3644 				else {
   3645 					dom.wrapper.classList.remove( classToBubble );
   3646 				}
   3647 			} );
   3648 		}
   3649 
   3650 		// Allow the first background to apply without transition
   3651 		setTimeout( function() {
   3652 			dom.background.classList.remove( 'no-transition' );
   3653 		}, 1 );
   3654 
   3655 	}
   3656 
   3657 	/**
   3658 	 * Updates the position of the parallax background based
   3659 	 * on the current slide index.
   3660 	 */
   3661 	function updateParallax() {
   3662 
   3663 		if( config.parallaxBackgroundImage ) {
   3664 
   3665 			var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
   3666 				verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
   3667 
   3668 			var backgroundSize = dom.background.style.backgroundSize.split( ' ' ),
   3669 				backgroundWidth, backgroundHeight;
   3670 
   3671 			if( backgroundSize.length === 1 ) {
   3672 				backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
   3673 			}
   3674 			else {
   3675 				backgroundWidth = parseInt( backgroundSize[0], 10 );
   3676 				backgroundHeight = parseInt( backgroundSize[1], 10 );
   3677 			}
   3678 
   3679 			var slideWidth = dom.background.offsetWidth,
   3680 				horizontalSlideCount = horizontalSlides.length,
   3681 				horizontalOffsetMultiplier,
   3682 				horizontalOffset;
   3683 
   3684 			if( typeof config.parallaxBackgroundHorizontal === 'number' ) {
   3685 				horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
   3686 			}
   3687 			else {
   3688 				horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
   3689 			}
   3690 
   3691 			horizontalOffset = horizontalOffsetMultiplier * indexh * -1;
   3692 
   3693 			var slideHeight = dom.background.offsetHeight,
   3694 				verticalSlideCount = verticalSlides.length,
   3695 				verticalOffsetMultiplier,
   3696 				verticalOffset;
   3697 
   3698 			if( typeof config.parallaxBackgroundVertical === 'number' ) {
   3699 				verticalOffsetMultiplier = config.parallaxBackgroundVertical;
   3700 			}
   3701 			else {
   3702 				verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
   3703 			}
   3704 
   3705 			verticalOffset = verticalSlideCount > 0 ?  verticalOffsetMultiplier * indexv : 0;
   3706 
   3707 			dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
   3708 
   3709 		}
   3710 
   3711 	}
   3712 
   3713 	/**
   3714 	 * Should the given element be preloaded?
   3715 	 * Decides based on local element attributes and global config.
   3716 	 *
   3717 	 * @param {HTMLElement} element
   3718 	 */
   3719 	function shouldPreload( element ) {
   3720 
   3721 		// Prefer an explicit global preload setting
   3722 		var preload = config.preloadIframes;
   3723 
   3724 		// If no global setting is available, fall back on the element's
   3725 		// own preload setting
   3726 		if( typeof preload !== 'boolean' ) {
   3727 			preload = element.hasAttribute( 'data-preload' );
   3728 		}
   3729 
   3730 		return preload;
   3731 	}
   3732 
   3733 	/**
   3734 	 * Called when the given slide is within the configured view
   3735 	 * distance. Shows the slide element and loads any content
   3736 	 * that is set to load lazily (data-src).
   3737 	 *
   3738 	 * @param {HTMLElement} slide Slide to show
   3739 	 */
   3740 	function loadSlide( slide, options ) {
   3741 
   3742 		options = options || {};
   3743 
   3744 		// Show the slide element
   3745 		slide.style.display = config.display;
   3746 
   3747 		// Media elements with data-src attributes
   3748 		toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
   3749 			if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
   3750 				element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
   3751 				element.setAttribute( 'data-lazy-loaded', '' );
   3752 				element.removeAttribute( 'data-src' );
   3753 			}
   3754 		} );
   3755 
   3756 		// Media elements with <source> children
   3757 		toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) {
   3758 			var sources = 0;
   3759 
   3760 			toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) {
   3761 				source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
   3762 				source.removeAttribute( 'data-src' );
   3763 				source.setAttribute( 'data-lazy-loaded', '' );
   3764 				sources += 1;
   3765 			} );
   3766 
   3767 			// If we rewrote sources for this video/audio element, we need
   3768 			// to manually tell it to load from its new origin
   3769 			if( sources > 0 ) {
   3770 				media.load();
   3771 			}
   3772 		} );
   3773 
   3774 
   3775 		// Show the corresponding background element
   3776 		var background = slide.slideBackgroundElement;
   3777 		if( background ) {
   3778 			background.style.display = 'block';
   3779 
   3780 			var backgroundContent = slide.slideBackgroundContentElement;
   3781 
   3782 			// If the background contains media, load it
   3783 			if( background.hasAttribute( 'data-loaded' ) === false ) {
   3784 				background.setAttribute( 'data-loaded', 'true' );
   3785 
   3786 				var backgroundImage = slide.getAttribute( 'data-background-image' ),
   3787 					backgroundVideo = slide.getAttribute( 'data-background-video' ),
   3788 					backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
   3789 					backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' ),
   3790 					backgroundIframe = slide.getAttribute( 'data-background-iframe' );
   3791 
   3792 				// Images
   3793 				if( backgroundImage ) {
   3794 					backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')';
   3795 				}
   3796 				// Videos
   3797 				else if ( backgroundVideo && !isSpeakerNotes() ) {
   3798 					var video = document.createElement( 'video' );
   3799 
   3800 					if( backgroundVideoLoop ) {
   3801 						video.setAttribute( 'loop', '' );
   3802 					}
   3803 
   3804 					if( backgroundVideoMuted ) {
   3805 						video.muted = true;
   3806 					}
   3807 
   3808 					// Inline video playback works (at least in Mobile Safari) as
   3809 					// long as the video is muted and the `playsinline` attribute is
   3810 					// present
   3811 					if( isMobileDevice ) {
   3812 						video.muted = true;
   3813 						video.autoplay = true;
   3814 						video.setAttribute( 'playsinline', '' );
   3815 					}
   3816 
   3817 					// Support comma separated lists of video sources
   3818 					backgroundVideo.split( ',' ).forEach( function( source ) {
   3819 						video.innerHTML += '<source src="'+ source +'">';
   3820 					} );
   3821 
   3822 					backgroundContent.appendChild( video );
   3823 				}
   3824 				// Iframes
   3825 				else if( backgroundIframe && options.excludeIframes !== true ) {
   3826 					var iframe = document.createElement( 'iframe' );
   3827 					iframe.setAttribute( 'allowfullscreen', '' );
   3828 					iframe.setAttribute( 'mozallowfullscreen', '' );
   3829 					iframe.setAttribute( 'webkitallowfullscreen', '' );
   3830 
   3831 					// Only load autoplaying content when the slide is shown to
   3832 					// avoid having it play in the background
   3833 					if( /autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
   3834 						iframe.setAttribute( 'data-src', backgroundIframe );
   3835 					}
   3836 					else {
   3837 						iframe.setAttribute( 'src', backgroundIframe );
   3838 					}
   3839 
   3840 					iframe.style.width  = '100%';
   3841 					iframe.style.height = '100%';
   3842 					iframe.style.maxHeight = '100%';
   3843 					iframe.style.maxWidth = '100%';
   3844 
   3845 					backgroundContent.appendChild( iframe );
   3846 				}
   3847 			}
   3848 
   3849 		}
   3850 
   3851 	}
   3852 
   3853 	/**
   3854 	 * Unloads and hides the given slide. This is called when the
   3855 	 * slide is moved outside of the configured view distance.
   3856 	 *
   3857 	 * @param {HTMLElement} slide
   3858 	 */
   3859 	function unloadSlide( slide ) {
   3860 
   3861 		// Hide the slide element
   3862 		slide.style.display = 'none';
   3863 
   3864 		// Hide the corresponding background element
   3865 		var background = getSlideBackground( slide );
   3866 		if( background ) {
   3867 			background.style.display = 'none';
   3868 		}
   3869 
   3870 		// Reset lazy-loaded media elements with src attributes
   3871 		toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
   3872 			element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
   3873 			element.removeAttribute( 'src' );
   3874 		} );
   3875 
   3876 		// Reset lazy-loaded media elements with <source> children
   3877 		toArray( slide.querySelectorAll( 'video[data-lazy-loaded] source[src], audio source[src]' ) ).forEach( function( source ) {
   3878 			source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
   3879 			source.removeAttribute( 'src' );
   3880 		} );
   3881 
   3882 	}
   3883 
   3884 	/**
   3885 	 * Determine what available routes there are for navigation.
   3886 	 *
   3887 	 * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
   3888 	 */
   3889 	function availableRoutes() {
   3890 
   3891 		var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ),
   3892 			verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
   3893 
   3894 		var routes = {
   3895 			left: indexh > 0,
   3896 			right: indexh < horizontalSlides.length - 1,
   3897 			up: indexv > 0,
   3898 			down: indexv < verticalSlides.length - 1
   3899 		};
   3900 
   3901 		// Looped presentations can always be navigated as long as
   3902 		// there are slides available
   3903 		if( config.loop ) {
   3904 			if( horizontalSlides.length > 1 ) {
   3905 				routes.left = true;
   3906 				routes.right = true;
   3907 			}
   3908 
   3909 			if( verticalSlides.length > 1 ) {
   3910 				routes.up = true;
   3911 				routes.down = true;
   3912 			}
   3913 		}
   3914 
   3915 		// Reverse horizontal controls for rtl
   3916 		if( config.rtl ) {
   3917 			var left = routes.left;
   3918 			routes.left = routes.right;
   3919 			routes.right = left;
   3920 		}
   3921 
   3922 		return routes;
   3923 
   3924 	}
   3925 
   3926 	/**
   3927 	 * Returns an object describing the available fragment
   3928 	 * directions.
   3929 	 *
   3930 	 * @return {{prev: boolean, next: boolean}}
   3931 	 */
   3932 	function availableFragments() {
   3933 
   3934 		if( currentSlide && config.fragments ) {
   3935 			var fragments = currentSlide.querySelectorAll( '.fragment' );
   3936 			var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
   3937 
   3938 			return {
   3939 				prev: fragments.length - hiddenFragments.length > 0,
   3940 				next: !!hiddenFragments.length
   3941 			};
   3942 		}
   3943 		else {
   3944 			return { prev: false, next: false };
   3945 		}
   3946 
   3947 	}
   3948 
   3949 	/**
   3950 	 * Enforces origin-specific format rules for embedded media.
   3951 	 */
   3952 	function formatEmbeddedContent() {
   3953 
   3954 		var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) {
   3955 			toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) {
   3956 				var src = el.getAttribute( sourceAttribute );
   3957 				if( src && src.indexOf( param ) === -1 ) {
   3958 					el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
   3959 				}
   3960 			});
   3961 		};
   3962 
   3963 		// YouTube frames must include "?enablejsapi=1"
   3964 		_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
   3965 		_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
   3966 
   3967 		// Vimeo frames must include "?api=1"
   3968 		_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
   3969 		_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
   3970 
   3971 	}
   3972 
   3973 	/**
   3974 	 * Start playback of any embedded content inside of
   3975 	 * the given element.
   3976 	 *
   3977 	 * @param {HTMLElement} element
   3978 	 */
   3979 	function startEmbeddedContent( element ) {
   3980 
   3981 		if( element && !isSpeakerNotes() ) {
   3982 
   3983 			// Restart GIFs
   3984 			toArray( element.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) {
   3985 				// Setting the same unchanged source like this was confirmed
   3986 				// to work in Chrome, FF & Safari
   3987 				el.setAttribute( 'src', el.getAttribute( 'src' ) );
   3988 			} );
   3989 
   3990 			// HTML5 media elements
   3991 			toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
   3992 				if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
   3993 					return;
   3994 				}
   3995 
   3996 				// Prefer an explicit global autoplay setting
   3997 				var autoplay = config.autoPlayMedia;
   3998 
   3999 				// If no global setting is available, fall back on the element's
   4000 				// own autoplay setting
   4001 				if( typeof autoplay !== 'boolean' ) {
   4002 					autoplay = el.hasAttribute( 'data-autoplay' ) || !!closestParent( el, '.slide-background' );
   4003 				}
   4004 
   4005 				if( autoplay && typeof el.play === 'function' ) {
   4006 
   4007 					// If the media is ready, start playback
   4008 					if( el.readyState > 1 ) {
   4009 						startEmbeddedMedia( { target: el } );
   4010 					}
   4011 					// Mobile devices never fire a loaded event so instead
   4012 					// of waiting, we initiate playback
   4013 					else if( isMobileDevice ) {
   4014 						var promise = el.play();
   4015 
   4016 						// If autoplay does not work, ensure that the controls are visible so
   4017 						// that the viewer can start the media on their own
   4018 						if( promise && typeof promise.catch === 'function' && el.controls === false ) {
   4019 							promise.catch( function() {
   4020 								el.controls = true;
   4021 
   4022 								// Once the video does start playing, hide the controls again
   4023 								el.addEventListener( 'play', function() {
   4024 									el.controls = false;
   4025 								} );
   4026 							} );
   4027 						}
   4028 					}
   4029 					// If the media isn't loaded, wait before playing
   4030 					else {
   4031 						el.removeEventListener( 'loadeddata', startEmbeddedMedia ); // remove first to avoid dupes
   4032 						el.addEventListener( 'loadeddata', startEmbeddedMedia );
   4033 					}
   4034 
   4035 				}
   4036 			} );
   4037 
   4038 			// Normal iframes
   4039 			toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) {
   4040 				if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
   4041 					return;
   4042 				}
   4043 
   4044 				startEmbeddedIframe( { target: el } );
   4045 			} );
   4046 
   4047 			// Lazy loading iframes
   4048 			toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
   4049 				if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) {
   4050 					return;
   4051 				}
   4052 
   4053 				if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
   4054 					el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes
   4055 					el.addEventListener( 'load', startEmbeddedIframe );
   4056 					el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
   4057 				}
   4058 			} );
   4059 
   4060 		}
   4061 
   4062 	}
   4063 
   4064 	/**
   4065 	 * Starts playing an embedded video/audio element after
   4066 	 * it has finished loading.
   4067 	 *
   4068 	 * @param {object} event
   4069 	 */
   4070 	function startEmbeddedMedia( event ) {
   4071 
   4072 		var isAttachedToDOM = !!closestParent( event.target, 'html' ),
   4073 			isVisible  		= !!closestParent( event.target, '.present' );
   4074 
   4075 		if( isAttachedToDOM && isVisible ) {
   4076 			event.target.currentTime = 0;
   4077 			event.target.play();
   4078 		}
   4079 
   4080 		event.target.removeEventListener( 'loadeddata', startEmbeddedMedia );
   4081 
   4082 	}
   4083 
   4084 	/**
   4085 	 * "Starts" the content of an embedded iframe using the
   4086 	 * postMessage API.
   4087 	 *
   4088 	 * @param {object} event
   4089 	 */
   4090 	function startEmbeddedIframe( event ) {
   4091 
   4092 		var iframe = event.target;
   4093 
   4094 		if( iframe && iframe.contentWindow ) {
   4095 
   4096 			var isAttachedToDOM = !!closestParent( event.target, 'html' ),
   4097 				isVisible  		= !!closestParent( event.target, '.present' );
   4098 
   4099 			if( isAttachedToDOM && isVisible ) {
   4100 
   4101 				// Prefer an explicit global autoplay setting
   4102 				var autoplay = config.autoPlayMedia;
   4103 
   4104 				// If no global setting is available, fall back on the element's
   4105 				// own autoplay setting
   4106 				if( typeof autoplay !== 'boolean' ) {
   4107 					autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closestParent( iframe, '.slide-background' );
   4108 				}
   4109 
   4110 				// YouTube postMessage API
   4111 				if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
   4112 					iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
   4113 				}
   4114 				// Vimeo postMessage API
   4115 				else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
   4116 					iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
   4117 				}
   4118 				// Generic postMessage API
   4119 				else {
   4120 					iframe.contentWindow.postMessage( 'slide:start', '*' );
   4121 				}
   4122 
   4123 			}
   4124 
   4125 		}
   4126 
   4127 	}
   4128 
   4129 	/**
   4130 	 * Stop playback of any embedded content inside of
   4131 	 * the targeted slide.
   4132 	 *
   4133 	 * @param {HTMLElement} element
   4134 	 */
   4135 	function stopEmbeddedContent( element, options ) {
   4136 
   4137 		options = extend( {
   4138 			// Defaults
   4139 			unloadIframes: true
   4140 		}, options || {} );
   4141 
   4142 		if( element && element.parentNode ) {
   4143 			// HTML5 media elements
   4144 			toArray( element.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
   4145 				if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
   4146 					el.setAttribute('data-paused-by-reveal', '');
   4147 					el.pause();
   4148 				}
   4149 			} );
   4150 
   4151 			// Generic postMessage API for non-lazy loaded iframes
   4152 			toArray( element.querySelectorAll( 'iframe' ) ).forEach( function( el ) {
   4153 				if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
   4154 				el.removeEventListener( 'load', startEmbeddedIframe );
   4155 			});
   4156 
   4157 			// YouTube postMessage API
   4158 			toArray( element.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) {
   4159 				if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
   4160 					el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
   4161 				}
   4162 			});
   4163 
   4164 			// Vimeo postMessage API
   4165 			toArray( element.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) {
   4166 				if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
   4167 					el.contentWindow.postMessage( '{"method":"pause"}', '*' );
   4168 				}
   4169 			});
   4170 
   4171 			if( options.unloadIframes === true ) {
   4172 				// Unload lazy-loaded iframes
   4173 				toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) {
   4174 					// Only removing the src doesn't actually unload the frame
   4175 					// in all browsers (Firefox) so we set it to blank first
   4176 					el.setAttribute( 'src', 'about:blank' );
   4177 					el.removeAttribute( 'src' );
   4178 				} );
   4179 			}
   4180 		}
   4181 
   4182 	}
   4183 
   4184 	/**
   4185 	 * Returns the number of past slides. This can be used as a global
   4186 	 * flattened index for slides.
   4187 	 *
   4188 	 * @return {number} Past slide count
   4189 	 */
   4190 	function getSlidePastCount() {
   4191 
   4192 		var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
   4193 
   4194 		// The number of past slides
   4195 		var pastCount = 0;
   4196 
   4197 		// Step through all slides and count the past ones
   4198 		mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) {
   4199 
   4200 			var horizontalSlide = horizontalSlides[i];
   4201 			var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
   4202 
   4203 			for( var j = 0; j < verticalSlides.length; j++ ) {
   4204 
   4205 				// Stop as soon as we arrive at the present
   4206 				if( verticalSlides[j].classList.contains( 'present' ) ) {
   4207 					break mainLoop;
   4208 				}
   4209 
   4210 				pastCount++;
   4211 
   4212 			}
   4213 
   4214 			// Stop as soon as we arrive at the present
   4215 			if( horizontalSlide.classList.contains( 'present' ) ) {
   4216 				break;
   4217 			}
   4218 
   4219 			// Don't count the wrapping section for vertical slides
   4220 			if( horizontalSlide.classList.contains( 'stack' ) === false ) {
   4221 				pastCount++;
   4222 			}
   4223 
   4224 		}
   4225 
   4226 		return pastCount;
   4227 
   4228 	}
   4229 
   4230 	/**
   4231 	 * Returns a value ranging from 0-1 that represents
   4232 	 * how far into the presentation we have navigated.
   4233 	 *
   4234 	 * @return {number}
   4235 	 */
   4236 	function getProgress() {
   4237 
   4238 		// The number of past and total slides
   4239 		var totalCount = getTotalSlides();
   4240 		var pastCount = getSlidePastCount();
   4241 
   4242 		if( currentSlide ) {
   4243 
   4244 			var allFragments = currentSlide.querySelectorAll( '.fragment' );
   4245 
   4246 			// If there are fragments in the current slide those should be
   4247 			// accounted for in the progress.
   4248 			if( allFragments.length > 0 ) {
   4249 				var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' );
   4250 
   4251 				// This value represents how big a portion of the slide progress
   4252 				// that is made up by its fragments (0-1)
   4253 				var fragmentWeight = 0.9;
   4254 
   4255 				// Add fragment progress to the past slide count
   4256 				pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight;
   4257 			}
   4258 
   4259 		}
   4260 
   4261 		return Math.min( pastCount / ( totalCount - 1 ), 1 );
   4262 
   4263 	}
   4264 
   4265 	/**
   4266 	 * Checks if this presentation is running inside of the
   4267 	 * speaker notes window.
   4268 	 *
   4269 	 * @return {boolean}
   4270 	 */
   4271 	function isSpeakerNotes() {
   4272 
   4273 		return !!window.location.search.match( /receiver/gi );
   4274 
   4275 	}
   4276 
   4277 	/**
   4278 	 * Reads the current URL (hash) and navigates accordingly.
   4279 	 */
   4280 	function readURL() {
   4281 
   4282 		var hash = window.location.hash;
   4283 
   4284 		// Attempt to parse the hash as either an index or name
   4285 		var bits = hash.slice( 2 ).split( '/' ),
   4286 			name = hash.replace( /#|\//gi, '' );
   4287 
   4288 		// If the first bit is not fully numeric and there is a name we
   4289 		// can assume that this is a named link
   4290 		if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
   4291 			var element;
   4292 
   4293 			// Ensure the named link is a valid HTML ID attribute
   4294 			try {
   4295 				element = document.getElementById( decodeURIComponent( name ) );
   4296 			}
   4297 			catch ( error ) { }
   4298 
   4299 			// Ensure that we're not already on a slide with the same name
   4300 			var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
   4301 
   4302 			if( element ) {
   4303 				// If the slide exists and is not the current slide...
   4304 				if ( !isSameNameAsCurrentSlide ) {
   4305 					// ...find the position of the named slide and navigate to it
   4306 					var indices = Reveal.getIndices(element);
   4307 					slide(indices.h, indices.v);
   4308 				}
   4309 			}
   4310 			// If the slide doesn't exist, navigate to the current slide
   4311 			else {
   4312 				slide( indexh || 0, indexv || 0 );
   4313 			}
   4314 		}
   4315 		else {
   4316 			var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
   4317 
   4318 			// Read the index components of the hash
   4319 			var h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
   4320 				v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
   4321 				f;
   4322 
   4323 			if( config.fragmentInURL ) {
   4324 				f = parseInt( bits[2], 10 );
   4325 				if( isNaN( f ) ) {
   4326 					f = undefined;
   4327 				}
   4328 			}
   4329 
   4330 			if( h !== indexh || v !== indexv || f !== undefined ) {
   4331 				slide( h, v, f );
   4332 			}
   4333 		}
   4334 
   4335 	}
   4336 
   4337 	/**
   4338 	 * Updates the page URL (hash) to reflect the current
   4339 	 * state.
   4340 	 *
   4341 	 * @param {number} delay The time in ms to wait before
   4342 	 * writing the hash
   4343 	 */
   4344 	function writeURL( delay ) {
   4345 
   4346 		// Make sure there's never more than one timeout running
   4347 		clearTimeout( writeURLTimeout );
   4348 
   4349 		// If a delay is specified, timeout this call
   4350 		if( typeof delay === 'number' ) {
   4351 			writeURLTimeout = setTimeout( writeURL, delay );
   4352 		}
   4353 		else if( currentSlide ) {
   4354 			// If we're configured to push to history OR the history
   4355 			// API is not avaialble.
   4356 			if( config.history || !window.history ) {
   4357 				window.location.hash = locationHash();
   4358 			}
   4359 			// If we're configured to reflect the current slide in the
   4360 			// URL without pushing to history.
   4361 			else if( config.hash ) {
   4362 				window.history.replaceState( null, null, '#' + locationHash() );
   4363 			}
   4364 			// If history and hash are both disabled, a hash may still
   4365 			// be added to the URL by clicking on a href with a hash
   4366 			// target. Counter this by always removing the hash.
   4367 			else {
   4368 				window.history.replaceState( null, null, window.location.pathname + window.location.search );
   4369 			}
   4370 		}
   4371 
   4372 	}
   4373 	/**
   4374 	 * Retrieves the h/v location and fragment of the current,
   4375 	 * or specified, slide.
   4376 	 *
   4377 	 * @param {HTMLElement} [slide] If specified, the returned
   4378 	 * index will be for this slide rather than the currently
   4379 	 * active one
   4380 	 *
   4381 	 * @return {{h: number, v: number, f: number}}
   4382 	 */
   4383 	function getIndices( slide ) {
   4384 
   4385 		// By default, return the current indices
   4386 		var h = indexh,
   4387 			v = indexv,
   4388 			f;
   4389 
   4390 		// If a slide is specified, return the indices of that slide
   4391 		if( slide ) {
   4392 			var isVertical = isVerticalSlide( slide );
   4393 			var slideh = isVertical ? slide.parentNode : slide;
   4394 
   4395 			// Select all horizontal slides
   4396 			var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) );
   4397 
   4398 			// Now that we know which the horizontal slide is, get its index
   4399 			h = Math.max( horizontalSlides.indexOf( slideh ), 0 );
   4400 
   4401 			// Assume we're not vertical
   4402 			v = undefined;
   4403 
   4404 			// If this is a vertical slide, grab the vertical index
   4405 			if( isVertical ) {
   4406 				v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 );
   4407 			}
   4408 		}
   4409 
   4410 		if( !slide && currentSlide ) {
   4411 			var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0;
   4412 			if( hasFragments ) {
   4413 				var currentFragment = currentSlide.querySelector( '.current-fragment' );
   4414 				if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) {
   4415 					f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 );
   4416 				}
   4417 				else {
   4418 					f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1;
   4419 				}
   4420 			}
   4421 		}
   4422 
   4423 		return { h: h, v: v, f: f };
   4424 
   4425 	}
   4426 
   4427 	/**
   4428 	 * Retrieves all slides in this presentation.
   4429 	 */
   4430 	function getSlides() {
   4431 
   4432 		return toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ));
   4433 
   4434 	}
   4435 
   4436 	/**
   4437 	 * Returns an array of objects where each object represents the
   4438 	 * attributes on its respective slide.
   4439 	 */
   4440 	function getSlidesAttributes() {
   4441 
   4442 		return getSlides().map( function( slide ) {
   4443 
   4444 			var attributes = {};
   4445 			for( var i = 0; i < slide.attributes.length; i++ ) {
   4446 				var attribute = slide.attributes[ i ];
   4447 				attributes[ attribute.name ] = attribute.value;
   4448 			}
   4449 			return attributes;
   4450 
   4451 		} );
   4452 
   4453 	}
   4454 
   4455 	/**
   4456 	 * Retrieves the total number of slides in this presentation.
   4457 	 *
   4458 	 * @return {number}
   4459 	 */
   4460 	function getTotalSlides() {
   4461 
   4462 		return getSlides().length;
   4463 
   4464 	}
   4465 
   4466 	/**
   4467 	 * Returns the slide element matching the specified index.
   4468 	 *
   4469 	 * @return {HTMLElement}
   4470 	 */
   4471 	function getSlide( x, y ) {
   4472 
   4473 		var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ];
   4474 		var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' );
   4475 
   4476 		if( verticalSlides && verticalSlides.length && typeof y === 'number' ) {
   4477 			return verticalSlides ? verticalSlides[ y ] : undefined;
   4478 		}
   4479 
   4480 		return horizontalSlide;
   4481 
   4482 	}
   4483 
   4484 	/**
   4485 	 * Returns the background element for the given slide.
   4486 	 * All slides, even the ones with no background properties
   4487 	 * defined, have a background element so as long as the
   4488 	 * index is valid an element will be returned.
   4489 	 *
   4490 	 * @param {mixed} x Horizontal background index OR a slide
   4491 	 * HTML element
   4492 	 * @param {number} y Vertical background index
   4493 	 * @return {(HTMLElement[]|*)}
   4494 	 */
   4495 	function getSlideBackground( x, y ) {
   4496 
   4497 		var slide = typeof x === 'number' ? getSlide( x, y ) : x;
   4498 		if( slide ) {
   4499 			return slide.slideBackgroundElement;
   4500 		}
   4501 
   4502 		return undefined;
   4503 
   4504 	}
   4505 
   4506 	/**
   4507 	 * Retrieves the speaker notes from a slide. Notes can be
   4508 	 * defined in two ways:
   4509 	 * 1. As a data-notes attribute on the slide <section>
   4510 	 * 2. As an <aside class="notes"> inside of the slide
   4511 	 *
   4512 	 * @param {HTMLElement} [slide=currentSlide]
   4513 	 * @return {(string|null)}
   4514 	 */
   4515 	function getSlideNotes( slide ) {
   4516 
   4517 		// Default to the current slide
   4518 		slide = slide || currentSlide;
   4519 
   4520 		// Notes can be specified via the data-notes attribute...
   4521 		if( slide.hasAttribute( 'data-notes' ) ) {
   4522 			return slide.getAttribute( 'data-notes' );
   4523 		}
   4524 
   4525 		// ... or using an <aside class="notes"> element
   4526 		var notesElement = slide.querySelector( 'aside.notes' );
   4527 		if( notesElement ) {
   4528 			return notesElement.innerHTML;
   4529 		}
   4530 
   4531 		return null;
   4532 
   4533 	}
   4534 
   4535 	/**
   4536 	 * Retrieves the current state of the presentation as
   4537 	 * an object. This state can then be restored at any
   4538 	 * time.
   4539 	 *
   4540 	 * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
   4541 	 */
   4542 	function getState() {
   4543 
   4544 		var indices = getIndices();
   4545 
   4546 		return {
   4547 			indexh: indices.h,
   4548 			indexv: indices.v,
   4549 			indexf: indices.f,
   4550 			paused: isPaused(),
   4551 			overview: isOverview()
   4552 		};
   4553 
   4554 	}
   4555 
   4556 	/**
   4557 	 * Restores the presentation to the given state.
   4558 	 *
   4559 	 * @param {object} state As generated by getState()
   4560 	 * @see {@link getState} generates the parameter `state`
   4561 	 */
   4562 	function setState( state ) {
   4563 
   4564 		if( typeof state === 'object' ) {
   4565 			slide( deserialize( state.indexh ), deserialize( state.indexv ), deserialize( state.indexf ) );
   4566 
   4567 			var pausedFlag = deserialize( state.paused ),
   4568 				overviewFlag = deserialize( state.overview );
   4569 
   4570 			if( typeof pausedFlag === 'boolean' && pausedFlag !== isPaused() ) {
   4571 				togglePause( pausedFlag );
   4572 			}
   4573 
   4574 			if( typeof overviewFlag === 'boolean' && overviewFlag !== isOverview() ) {
   4575 				toggleOverview( overviewFlag );
   4576 			}
   4577 		}
   4578 
   4579 	}
   4580 
   4581 	/**
   4582 	 * Return a sorted fragments list, ordered by an increasing
   4583 	 * "data-fragment-index" attribute.
   4584 	 *
   4585 	 * Fragments will be revealed in the order that they are returned by
   4586 	 * this function, so you can use the index attributes to control the
   4587 	 * order of fragment appearance.
   4588 	 *
   4589 	 * To maintain a sensible default fragment order, fragments are presumed
   4590 	 * to be passed in document order. This function adds a "fragment-index"
   4591 	 * attribute to each node if such an attribute is not already present,
   4592 	 * and sets that attribute to an integer value which is the position of
   4593 	 * the fragment within the fragments list.
   4594 	 *
   4595 	 * @param {object[]|*} fragments
   4596 	 * @param {boolean} grouped If true the returned array will contain
   4597 	 * nested arrays for all fragments with the same index
   4598 	 * @return {object[]} sorted Sorted array of fragments
   4599 	 */
   4600 	function sortFragments( fragments, grouped ) {
   4601 
   4602 		fragments = toArray( fragments );
   4603 
   4604 		var ordered = [],
   4605 			unordered = [],
   4606 			sorted = [];
   4607 
   4608 		// Group ordered and unordered elements
   4609 		fragments.forEach( function( fragment, i ) {
   4610 			if( fragment.hasAttribute( 'data-fragment-index' ) ) {
   4611 				var index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
   4612 
   4613 				if( !ordered[index] ) {
   4614 					ordered[index] = [];
   4615 				}
   4616 
   4617 				ordered[index].push( fragment );
   4618 			}
   4619 			else {
   4620 				unordered.push( [ fragment ] );
   4621 			}
   4622 		} );
   4623 
   4624 		// Append fragments without explicit indices in their
   4625 		// DOM order
   4626 		ordered = ordered.concat( unordered );
   4627 
   4628 		// Manually count the index up per group to ensure there
   4629 		// are no gaps
   4630 		var index = 0;
   4631 
   4632 		// Push all fragments in their sorted order to an array,
   4633 		// this flattens the groups
   4634 		ordered.forEach( function( group ) {
   4635 			group.forEach( function( fragment ) {
   4636 				sorted.push( fragment );
   4637 				fragment.setAttribute( 'data-fragment-index', index );
   4638 			} );
   4639 
   4640 			index ++;
   4641 		} );
   4642 
   4643 		return grouped === true ? ordered : sorted;
   4644 
   4645 	}
   4646 
   4647 	/**
   4648 	 * Refreshes the fragments on the current slide so that they
   4649 	 * have the appropriate classes (.visible + .current-fragment).
   4650 	 *
   4651 	 * @param {number} [index] The index of the current fragment
   4652 	 * @param {array} [fragments] Array containing all fragments
   4653 	 * in the current slide
   4654 	 *
   4655 	 * @return {{shown: array, hidden: array}}
   4656 	 */
   4657 	function updateFragments( index, fragments ) {
   4658 
   4659 		var changedFragments = {
   4660 			shown: [],
   4661 			hidden: []
   4662 		};
   4663 
   4664 		if( currentSlide && config.fragments ) {
   4665 
   4666 			fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
   4667 
   4668 			if( fragments.length ) {
   4669 
   4670 				if( typeof index !== 'number' ) {
   4671 					var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
   4672 					if( currentFragment ) {
   4673 						index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
   4674 					}
   4675 				}
   4676 
   4677 				toArray( fragments ).forEach( function( el, i ) {
   4678 
   4679 					if( el.hasAttribute( 'data-fragment-index' ) ) {
   4680 						i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
   4681 					}
   4682 
   4683 					// Visible fragments
   4684 					if( i <= index ) {
   4685 						if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
   4686 						el.classList.add( 'visible' );
   4687 						el.classList.remove( 'current-fragment' );
   4688 
   4689 						// Announce the fragments one by one to the Screen Reader
   4690 						dom.statusDiv.textContent = getStatusText( el );
   4691 
   4692 						if( i === index ) {
   4693 							el.classList.add( 'current-fragment' );
   4694 							startEmbeddedContent( el );
   4695 						}
   4696 					}
   4697 					// Hidden fragments
   4698 					else {
   4699 						if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
   4700 						el.classList.remove( 'visible' );
   4701 						el.classList.remove( 'current-fragment' );
   4702 					}
   4703 
   4704 				} );
   4705 
   4706 			}
   4707 
   4708 		}
   4709 
   4710 		return changedFragments;
   4711 
   4712 	}
   4713 
   4714 	/**
   4715 	 * Navigate to the specified slide fragment.
   4716 	 *
   4717 	 * @param {?number} index The index of the fragment that
   4718 	 * should be shown, -1 means all are invisible
   4719 	 * @param {number} offset Integer offset to apply to the
   4720 	 * fragment index
   4721 	 *
   4722 	 * @return {boolean} true if a change was made in any
   4723 	 * fragments visibility as part of this call
   4724 	 */
   4725 	function navigateFragment( index, offset ) {
   4726 
   4727 		if( currentSlide && config.fragments ) {
   4728 
   4729 			var fragments = sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
   4730 			if( fragments.length ) {
   4731 
   4732 				// If no index is specified, find the current
   4733 				if( typeof index !== 'number' ) {
   4734 					var lastVisibleFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
   4735 
   4736 					if( lastVisibleFragment ) {
   4737 						index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
   4738 					}
   4739 					else {
   4740 						index = -1;
   4741 					}
   4742 				}
   4743 
   4744 				// If an offset is specified, apply it to the index
   4745 				if( typeof offset === 'number' ) {
   4746 					index += offset;
   4747 				}
   4748 
   4749 				var changedFragments = updateFragments( index, fragments );
   4750 
   4751 				if( changedFragments.hidden.length ) {
   4752 					dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
   4753 				}
   4754 
   4755 				if( changedFragments.shown.length ) {
   4756 					dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
   4757 				}
   4758 
   4759 				updateControls();
   4760 				updateProgress();
   4761 
   4762 				if( config.fragmentInURL ) {
   4763 					writeURL();
   4764 				}
   4765 
   4766 				return !!( changedFragments.shown.length || changedFragments.hidden.length );
   4767 
   4768 			}
   4769 
   4770 		}
   4771 
   4772 		return false;
   4773 
   4774 	}
   4775 
   4776 	/**
   4777 	 * Navigate to the next slide fragment.
   4778 	 *
   4779 	 * @return {boolean} true if there was a next fragment,
   4780 	 * false otherwise
   4781 	 */
   4782 	function nextFragment() {
   4783 
   4784 		return navigateFragment( null, 1 );
   4785 
   4786 	}
   4787 
   4788 	/**
   4789 	 * Navigate to the previous slide fragment.
   4790 	 *
   4791 	 * @return {boolean} true if there was a previous fragment,
   4792 	 * false otherwise
   4793 	 */
   4794 	function previousFragment() {
   4795 
   4796 		return navigateFragment( null, -1 );
   4797 
   4798 	}
   4799 
   4800 	/**
   4801 	 * Cues a new automated slide if enabled in the config.
   4802 	 */
   4803 	function cueAutoSlide() {
   4804 
   4805 		cancelAutoSlide();
   4806 
   4807 		if( currentSlide && config.autoSlide !== false ) {
   4808 
   4809 			var fragment = currentSlide.querySelector( '.current-fragment' );
   4810 
   4811 			// When the slide first appears there is no "current" fragment so
   4812 			// we look for a data-autoslide timing on the first fragment
   4813 			if( !fragment ) fragment = currentSlide.querySelector( '.fragment' );
   4814 
   4815 			var fragmentAutoSlide = fragment ? fragment.getAttribute( 'data-autoslide' ) : null;
   4816 			var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute( 'data-autoslide' ) : null;
   4817 			var slideAutoSlide = currentSlide.getAttribute( 'data-autoslide' );
   4818 
   4819 			// Pick value in the following priority order:
   4820 			// 1. Current fragment's data-autoslide
   4821 			// 2. Current slide's data-autoslide
   4822 			// 3. Parent slide's data-autoslide
   4823 			// 4. Global autoSlide setting
   4824 			if( fragmentAutoSlide ) {
   4825 				autoSlide = parseInt( fragmentAutoSlide, 10 );
   4826 			}
   4827 			else if( slideAutoSlide ) {
   4828 				autoSlide = parseInt( slideAutoSlide, 10 );
   4829 			}
   4830 			else if( parentAutoSlide ) {
   4831 				autoSlide = parseInt( parentAutoSlide, 10 );
   4832 			}
   4833 			else {
   4834 				autoSlide = config.autoSlide;
   4835 			}
   4836 
   4837 			// If there are media elements with data-autoplay,
   4838 			// automatically set the autoSlide duration to the
   4839 			// length of that media. Not applicable if the slide
   4840 			// is divided up into fragments.
   4841 			// playbackRate is accounted for in the duration.
   4842 			if( currentSlide.querySelectorAll( '.fragment' ).length === 0 ) {
   4843 				toArray( currentSlide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
   4844 					if( el.hasAttribute( 'data-autoplay' ) ) {
   4845 						if( autoSlide && (el.duration * 1000 / el.playbackRate ) > autoSlide ) {
   4846 							autoSlide = ( el.duration * 1000 / el.playbackRate ) + 1000;
   4847 						}
   4848 					}
   4849 				} );
   4850 			}
   4851 
   4852 			// Cue the next auto-slide if:
   4853 			// - There is an autoSlide value
   4854 			// - Auto-sliding isn't paused by the user
   4855 			// - The presentation isn't paused
   4856 			// - The overview isn't active
   4857 			// - The presentation isn't over
   4858 			if( autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && ( !Reveal.isLastSlide() || availableFragments().next || config.loop === true ) ) {
   4859 				autoSlideTimeout = setTimeout( function() {
   4860 					typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
   4861 					cueAutoSlide();
   4862 				}, autoSlide );
   4863 				autoSlideStartTime = Date.now();
   4864 			}
   4865 
   4866 			if( autoSlidePlayer ) {
   4867 				autoSlidePlayer.setPlaying( autoSlideTimeout !== -1 );
   4868 			}
   4869 
   4870 		}
   4871 
   4872 	}
   4873 
   4874 	/**
   4875 	 * Cancels any ongoing request to auto-slide.
   4876 	 */
   4877 	function cancelAutoSlide() {
   4878 
   4879 		clearTimeout( autoSlideTimeout );
   4880 		autoSlideTimeout = -1;
   4881 
   4882 	}
   4883 
   4884 	function pauseAutoSlide() {
   4885 
   4886 		if( autoSlide && !autoSlidePaused ) {
   4887 			autoSlidePaused = true;
   4888 			dispatchEvent( 'autoslidepaused' );
   4889 			clearTimeout( autoSlideTimeout );
   4890 
   4891 			if( autoSlidePlayer ) {
   4892 				autoSlidePlayer.setPlaying( false );
   4893 			}
   4894 		}
   4895 
   4896 	}
   4897 
   4898 	function resumeAutoSlide() {
   4899 
   4900 		if( autoSlide && autoSlidePaused ) {
   4901 			autoSlidePaused = false;
   4902 			dispatchEvent( 'autoslideresumed' );
   4903 			cueAutoSlide();
   4904 		}
   4905 
   4906 	}
   4907 
   4908 	function navigateLeft() {
   4909 
   4910 		// Reverse for RTL
   4911 		if( config.rtl ) {
   4912 			if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
   4913 				slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
   4914 			}
   4915 		}
   4916 		// Normal navigation
   4917 		else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
   4918 			slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
   4919 		}
   4920 
   4921 	}
   4922 
   4923 	function navigateRight() {
   4924 
   4925 		hasNavigatedRight = true;
   4926 
   4927 		// Reverse for RTL
   4928 		if( config.rtl ) {
   4929 			if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
   4930 				slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
   4931 			}
   4932 		}
   4933 		// Normal navigation
   4934 		else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
   4935 			slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
   4936 		}
   4937 
   4938 	}
   4939 
   4940 	function navigateUp() {
   4941 
   4942 		// Prioritize hiding fragments
   4943 		if( ( isOverview() || previousFragment() === false ) && availableRoutes().up ) {
   4944 			slide( indexh, indexv - 1 );
   4945 		}
   4946 
   4947 	}
   4948 
   4949 	function navigateDown() {
   4950 
   4951 		hasNavigatedDown = true;
   4952 
   4953 		// Prioritize revealing fragments
   4954 		if( ( isOverview() || nextFragment() === false ) && availableRoutes().down ) {
   4955 			slide( indexh, indexv + 1 );
   4956 		}
   4957 
   4958 	}
   4959 
   4960 	/**
   4961 	 * Navigates backwards, prioritized in the following order:
   4962 	 * 1) Previous fragment
   4963 	 * 2) Previous vertical slide
   4964 	 * 3) Previous horizontal slide
   4965 	 */
   4966 	function navigatePrev() {
   4967 
   4968 		// Prioritize revealing fragments
   4969 		if( previousFragment() === false ) {
   4970 			if( availableRoutes().up ) {
   4971 				navigateUp();
   4972 			}
   4973 			else {
   4974 				// Fetch the previous horizontal slide, if there is one
   4975 				var previousSlide;
   4976 
   4977 				if( config.rtl ) {
   4978 					previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.future' ) ).pop();
   4979 				}
   4980 				else {
   4981 					previousSlide = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.past' ) ).pop();
   4982 				}
   4983 
   4984 				if( previousSlide ) {
   4985 					var v = ( previousSlide.querySelectorAll( 'section' ).length - 1 ) || undefined;
   4986 					var h = indexh - 1;
   4987 					slide( h, v );
   4988 				}
   4989 			}
   4990 		}
   4991 
   4992 	}
   4993 
   4994 	/**
   4995 	 * The reverse of #navigatePrev().
   4996 	 */
   4997 	function navigateNext() {
   4998 
   4999 		hasNavigatedRight = true;
   5000 		hasNavigatedDown = true;
   5001 
   5002 		// Prioritize revealing fragments
   5003 		if( nextFragment() === false ) {
   5004 
   5005 			var routes = availableRoutes();
   5006 
   5007 			// When looping is enabled `routes.down` is always available
   5008 			// so we need a separate check for when we've reached the
   5009 			// end of a stack and should move horizontally
   5010 			if( routes.down && routes.right && config.loop && Reveal.isLastVerticalSlide( currentSlide ) ) {
   5011 				routes.down = false;
   5012 			}
   5013 
   5014 			if( routes.down ) {
   5015 				navigateDown();
   5016 			}
   5017 			else if( config.rtl ) {
   5018 				navigateLeft();
   5019 			}
   5020 			else {
   5021 				navigateRight();
   5022 			}
   5023 		}
   5024 
   5025 	}
   5026 
   5027 	/**
   5028 	 * Checks if the target element prevents the triggering of
   5029 	 * swipe navigation.
   5030 	 */
   5031 	function isSwipePrevented( target ) {
   5032 
   5033 		while( target && typeof target.hasAttribute === 'function' ) {
   5034 			if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
   5035 			target = target.parentNode;
   5036 		}
   5037 
   5038 		return false;
   5039 
   5040 	}
   5041 
   5042 
   5043 	// --------------------------------------------------------------------//
   5044 	// ----------------------------- EVENTS -------------------------------//
   5045 	// --------------------------------------------------------------------//
   5046 
   5047 	/**
   5048 	 * Called by all event handlers that are based on user
   5049 	 * input.
   5050 	 *
   5051 	 * @param {object} [event]
   5052 	 */
   5053 	function onUserInput( event ) {
   5054 
   5055 		if( config.autoSlideStoppable ) {
   5056 			pauseAutoSlide();
   5057 		}
   5058 
   5059 	}
   5060 
   5061 	/**
   5062 	 * Called whenever there is mouse input at the document level
   5063 	 * to determine if the cursor is active or not.
   5064 	 *
   5065 	 * @param {object} event
   5066 	 */
   5067 	function onDocumentCursorActive( event ) {
   5068 
   5069 		showCursor();
   5070 
   5071 		clearTimeout( cursorInactiveTimeout );
   5072 
   5073 		cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
   5074 
   5075 	}
   5076 
   5077 	/**
   5078 	 * Handler for the document level 'keypress' event.
   5079 	 *
   5080 	 * @param {object} event
   5081 	 */
   5082 	function onDocumentKeyPress( event ) {
   5083 
   5084 		// Check if the pressed key is question mark
   5085 		if( event.shiftKey && event.charCode === 63 ) {
   5086 			toggleHelp();
   5087 		}
   5088 
   5089 	}
   5090 
   5091 	/**
   5092 	 * Handler for the document level 'keydown' event.
   5093 	 *
   5094 	 * @param {object} event
   5095 	 */
   5096 	function onDocumentKeyDown( event ) {
   5097 
   5098 		// If there's a condition specified and it returns false,
   5099 		// ignore this event
   5100 		if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
   5101 			return true;
   5102 		}
   5103 
   5104 		// Shorthand
   5105 		var keyCode = event.keyCode;
   5106 
   5107 		// Remember if auto-sliding was paused so we can toggle it
   5108 		var autoSlideWasPaused = autoSlidePaused;
   5109 
   5110 		onUserInput( event );
   5111 
   5112 		// Is there a focused element that could be using the keyboard?
   5113 		var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
   5114 		var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
   5115 		var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
   5116 
   5117 		// Whitelist specific modified + keycode combinations
   5118 		var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
   5119 		var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
   5120 		var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
   5121 
   5122 		// Prevent all other events when a modifier is pressed
   5123 		var unusedModifier = 	!prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
   5124 								( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
   5125 
   5126 		// Disregard the event if there's a focused element or a
   5127 		// keyboard modifier key is present
   5128 		if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
   5129 
   5130 		// While paused only allow resume keyboard events; 'b', 'v', '.'
   5131 		var resumeKeyCodes = [66,86,190,191];
   5132 		var key;
   5133 
   5134 		// Custom key bindings for togglePause should be able to resume
   5135 		if( typeof config.keyboard === 'object' ) {
   5136 			for( key in config.keyboard ) {
   5137 				if( config.keyboard[key] === 'togglePause' ) {
   5138 					resumeKeyCodes.push( parseInt( key, 10 ) );
   5139 				}
   5140 			}
   5141 		}
   5142 
   5143 		if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
   5144 			return false;
   5145 		}
   5146 
   5147 		var triggered = false;
   5148 
   5149 		// 1. User defined key bindings
   5150 		if( typeof config.keyboard === 'object' ) {
   5151 
   5152 			for( key in config.keyboard ) {
   5153 
   5154 				// Check if this binding matches the pressed key
   5155 				if( parseInt( key, 10 ) === keyCode ) {
   5156 
   5157 					var value = config.keyboard[ key ];
   5158 
   5159 					// Callback function
   5160 					if( typeof value === 'function' ) {
   5161 						value.apply( null, [ event ] );
   5162 					}
   5163 					// String shortcuts to reveal.js API
   5164 					else if( typeof value === 'string' && typeof Reveal[ value ] === 'function' ) {
   5165 						Reveal[ value ].call();
   5166 					}
   5167 
   5168 					triggered = true;
   5169 
   5170 				}
   5171 
   5172 			}
   5173 
   5174 		}
   5175 
   5176 		// 2. Registered custom key bindings
   5177 		if( triggered === false ) {
   5178 
   5179 			for( key in registeredKeyBindings ) {
   5180 
   5181 				// Check if this binding matches the pressed key
   5182 				if( parseInt( key, 10 ) === keyCode ) {
   5183 
   5184 					var action = registeredKeyBindings[ key ].callback;
   5185 
   5186 					// Callback function
   5187 					if( typeof action === 'function' ) {
   5188 						action.apply( null, [ event ] );
   5189 					}
   5190 					// String shortcuts to reveal.js API
   5191 					else if( typeof action === 'string' && typeof Reveal[ action ] === 'function' ) {
   5192 						Reveal[ action ].call();
   5193 					}
   5194 
   5195 					triggered = true;
   5196 				}
   5197 			}
   5198 		}
   5199 
   5200 		// 3. System defined key bindings
   5201 		if( triggered === false ) {
   5202 
   5203 			// Assume true and try to prove false
   5204 			triggered = true;
   5205 
   5206 			// P, PAGE UP
   5207 			if( keyCode === 80 || keyCode === 33 ) {
   5208 				navigatePrev();
   5209 			}
   5210 			// N, PAGE DOWN
   5211 			else if( keyCode === 78 || keyCode === 34 ) {
   5212 				navigateNext();
   5213 			}
   5214 			// H, LEFT
   5215 			else if( keyCode === 72 || keyCode === 37 ) {
   5216 				if( firstSlideShortcut ) {
   5217 					slide( 0 );
   5218 				}
   5219 				else if( !isOverview() && config.navigationMode === 'linear' ) {
   5220 					navigatePrev();
   5221 				}
   5222 				else {
   5223 					navigateLeft();
   5224 				}
   5225 			}
   5226 			// L, RIGHT
   5227 			else if( keyCode === 76 || keyCode === 39 ) {
   5228 				if( lastSlideShortcut ) {
   5229 					slide( Number.MAX_VALUE );
   5230 				}
   5231 				else if( !isOverview() && config.navigationMode === 'linear' ) {
   5232 					navigateNext();
   5233 				}
   5234 				else {
   5235 					navigateRight();
   5236 				}
   5237 			}
   5238 			// K, UP
   5239 			else if( keyCode === 75 || keyCode === 38 ) {
   5240 				if( !isOverview() && config.navigationMode === 'linear' ) {
   5241 					navigatePrev();
   5242 				}
   5243 				else {
   5244 					navigateUp();
   5245 				}
   5246 			}
   5247 			// J, DOWN
   5248 			else if( keyCode === 74 || keyCode === 40 ) {
   5249 				if( !isOverview() && config.navigationMode === 'linear' ) {
   5250 					navigateNext();
   5251 				}
   5252 				else {
   5253 					navigateDown();
   5254 				}
   5255 			}
   5256 			// HOME
   5257 			else if( keyCode === 36 ) {
   5258 				slide( 0 );
   5259 			}
   5260 			// END
   5261 			else if( keyCode === 35 ) {
   5262 				slide( Number.MAX_VALUE );
   5263 			}
   5264 			// SPACE
   5265 			else if( keyCode === 32 ) {
   5266 				if( isOverview() ) {
   5267 					deactivateOverview();
   5268 				}
   5269 				if( event.shiftKey ) {
   5270 					navigatePrev();
   5271 				}
   5272 				else {
   5273 					navigateNext();
   5274 				}
   5275 			}
   5276 			// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
   5277 			else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
   5278 				togglePause();
   5279 			}
   5280 			// F
   5281 			else if( keyCode === 70 ) {
   5282 				enterFullscreen();
   5283 			}
   5284 			// A
   5285 			else if( keyCode === 65 ) {
   5286 				if ( config.autoSlideStoppable ) {
   5287 					toggleAutoSlide( autoSlideWasPaused );
   5288 				}
   5289 			}
   5290 			else {
   5291 				triggered = false;
   5292 			}
   5293 
   5294 		}
   5295 
   5296 		// If the input resulted in a triggered action we should prevent
   5297 		// the browsers default behavior
   5298 		if( triggered ) {
   5299 			event.preventDefault && event.preventDefault();
   5300 		}
   5301 		// ESC or O key
   5302 		else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
   5303 			if( dom.overlay ) {
   5304 				closeOverlay();
   5305 			}
   5306 			else {
   5307 				toggleOverview();
   5308 			}
   5309 
   5310 			event.preventDefault && event.preventDefault();
   5311 		}
   5312 
   5313 		// If auto-sliding is enabled we need to cue up
   5314 		// another timeout
   5315 		cueAutoSlide();
   5316 
   5317 	}
   5318 
   5319 	/**
   5320 	 * Handler for the 'touchstart' event, enables support for
   5321 	 * swipe and pinch gestures.
   5322 	 *
   5323 	 * @param {object} event
   5324 	 */
   5325 	function onTouchStart( event ) {
   5326 
   5327 		if( isSwipePrevented( event.target ) ) return true;
   5328 
   5329 		touch.startX = event.touches[0].clientX;
   5330 		touch.startY = event.touches[0].clientY;
   5331 		touch.startCount = event.touches.length;
   5332 
   5333 	}
   5334 
   5335 	/**
   5336 	 * Handler for the 'touchmove' event.
   5337 	 *
   5338 	 * @param {object} event
   5339 	 */
   5340 	function onTouchMove( event ) {
   5341 
   5342 		if( isSwipePrevented( event.target ) ) return true;
   5343 
   5344 		// Each touch should only trigger one action
   5345 		if( !touch.captured ) {
   5346 			onUserInput( event );
   5347 
   5348 			var currentX = event.touches[0].clientX;
   5349 			var currentY = event.touches[0].clientY;
   5350 
   5351 			// There was only one touch point, look for a swipe
   5352 			if( event.touches.length === 1 && touch.startCount !== 2 ) {
   5353 
   5354 				var deltaX = currentX - touch.startX,
   5355 					deltaY = currentY - touch.startY;
   5356 
   5357 				if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
   5358 					touch.captured = true;
   5359 					navigateLeft();
   5360 				}
   5361 				else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
   5362 					touch.captured = true;
   5363 					navigateRight();
   5364 				}
   5365 				else if( deltaY > touch.threshold ) {
   5366 					touch.captured = true;
   5367 					navigateUp();
   5368 				}
   5369 				else if( deltaY < -touch.threshold ) {
   5370 					touch.captured = true;
   5371 					navigateDown();
   5372 				}
   5373 
   5374 				// If we're embedded, only block touch events if they have
   5375 				// triggered an action
   5376 				if( config.embedded ) {
   5377 					if( touch.captured || isVerticalSlide( currentSlide ) ) {
   5378 						event.preventDefault();
   5379 					}
   5380 				}
   5381 				// Not embedded? Block them all to avoid needless tossing
   5382 				// around of the viewport in iOS
   5383 				else {
   5384 					event.preventDefault();
   5385 				}
   5386 
   5387 			}
   5388 		}
   5389 		// There's a bug with swiping on some Android devices unless
   5390 		// the default action is always prevented
   5391 		else if( UA.match( /android/gi ) ) {
   5392 			event.preventDefault();
   5393 		}
   5394 
   5395 	}
   5396 
   5397 	/**
   5398 	 * Handler for the 'touchend' event.
   5399 	 *
   5400 	 * @param {object} event
   5401 	 */
   5402 	function onTouchEnd( event ) {
   5403 
   5404 		touch.captured = false;
   5405 
   5406 	}
   5407 
   5408 	/**
   5409 	 * Convert pointer down to touch start.
   5410 	 *
   5411 	 * @param {object} event
   5412 	 */
   5413 	function onPointerDown( event ) {
   5414 
   5415 		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
   5416 			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
   5417 			onTouchStart( event );
   5418 		}
   5419 
   5420 	}
   5421 
   5422 	/**
   5423 	 * Convert pointer move to touch move.
   5424 	 *
   5425 	 * @param {object} event
   5426 	 */
   5427 	function onPointerMove( event ) {
   5428 
   5429 		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
   5430 			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
   5431 			onTouchMove( event );
   5432 		}
   5433 
   5434 	}
   5435 
   5436 	/**
   5437 	 * Convert pointer up to touch end.
   5438 	 *
   5439 	 * @param {object} event
   5440 	 */
   5441 	function onPointerUp( event ) {
   5442 
   5443 		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
   5444 			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
   5445 			onTouchEnd( event );
   5446 		}
   5447 
   5448 	}
   5449 
   5450 	/**
   5451 	 * Handles mouse wheel scrolling, throttled to avoid skipping
   5452 	 * multiple slides.
   5453 	 *
   5454 	 * @param {object} event
   5455 	 */
   5456 	function onDocumentMouseScroll( event ) {
   5457 
   5458 		if( Date.now() - lastMouseWheelStep > 600 ) {
   5459 
   5460 			lastMouseWheelStep = Date.now();
   5461 
   5462 			var delta = event.detail || -event.wheelDelta;
   5463 			if( delta > 0 ) {
   5464 				navigateNext();
   5465 			}
   5466 			else if( delta < 0 ) {
   5467 				navigatePrev();
   5468 			}
   5469 
   5470 		}
   5471 
   5472 	}
   5473 
   5474 	/**
   5475 	 * Clicking on the progress bar results in a navigation to the
   5476 	 * closest approximate horizontal slide using this equation:
   5477 	 *
   5478 	 * ( clickX / presentationWidth ) * numberOfSlides
   5479 	 *
   5480 	 * @param {object} event
   5481 	 */
   5482 	function onProgressClicked( event ) {
   5483 
   5484 		onUserInput( event );
   5485 
   5486 		event.preventDefault();
   5487 
   5488 		var slidesTotal = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).length;
   5489 		var slideIndex = Math.floor( ( event.clientX / dom.wrapper.offsetWidth ) * slidesTotal );
   5490 
   5491 		if( config.rtl ) {
   5492 			slideIndex = slidesTotal - slideIndex;
   5493 		}
   5494 
   5495 		slide( slideIndex );
   5496 
   5497 	}
   5498 
   5499 	/**
   5500 	 * Event handler for navigation control buttons.
   5501 	 */
   5502 	function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
   5503 	function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
   5504 	function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
   5505 	function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
   5506 	function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
   5507 	function onNavigateNextClicked( event ) { event.preventDefault(); onUserInput(); navigateNext(); }
   5508 
   5509 	/**
   5510 	 * Handler for the window level 'hashchange' event.
   5511 	 *
   5512 	 * @param {object} [event]
   5513 	 */
   5514 	function onWindowHashChange( event ) {
   5515 
   5516 		readURL();
   5517 
   5518 	}
   5519 
   5520 	/**
   5521 	 * Handler for the window level 'resize' event.
   5522 	 *
   5523 	 * @param {object} [event]
   5524 	 */
   5525 	function onWindowResize( event ) {
   5526 
   5527 		layout();
   5528 
   5529 	}
   5530 
   5531 	/**
   5532 	 * Handle for the window level 'visibilitychange' event.
   5533 	 *
   5534 	 * @param {object} [event]
   5535 	 */
   5536 	function onPageVisibilityChange( event ) {
   5537 
   5538 		var isHidden =  document.webkitHidden ||
   5539 						document.msHidden ||
   5540 						document.hidden;
   5541 
   5542 		// If, after clicking a link or similar and we're coming back,
   5543 		// focus the document.body to ensure we can use keyboard shortcuts
   5544 		if( isHidden === false && document.activeElement !== document.body ) {
   5545 			// Not all elements support .blur() - SVGs among them.
   5546 			if( typeof document.activeElement.blur === 'function' ) {
   5547 				document.activeElement.blur();
   5548 			}
   5549 			document.body.focus();
   5550 		}
   5551 
   5552 	}
   5553 
   5554 	/**
   5555 	 * Invoked when a slide is and we're in the overview.
   5556 	 *
   5557 	 * @param {object} event
   5558 	 */
   5559 	function onOverviewSlideClicked( event ) {
   5560 
   5561 		// TODO There's a bug here where the event listeners are not
   5562 		// removed after deactivating the overview.
   5563 		if( eventsAreBound && isOverview() ) {
   5564 			event.preventDefault();
   5565 
   5566 			var element = event.target;
   5567 
   5568 			while( element && !element.nodeName.match( /section/gi ) ) {
   5569 				element = element.parentNode;
   5570 			}
   5571 
   5572 			if( element && !element.classList.contains( 'disabled' ) ) {
   5573 
   5574 				deactivateOverview();
   5575 
   5576 				if( element.nodeName.match( /section/gi ) ) {
   5577 					var h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
   5578 						v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
   5579 
   5580 					slide( h, v );
   5581 				}
   5582 
   5583 			}
   5584 		}
   5585 
   5586 	}
   5587 
   5588 	/**
   5589 	 * Handles clicks on links that are set to preview in the
   5590 	 * iframe overlay.
   5591 	 *
   5592 	 * @param {object} event
   5593 	 */
   5594 	function onPreviewLinkClicked( event ) {
   5595 
   5596 		if( event.currentTarget && event.currentTarget.hasAttribute( 'href' ) ) {
   5597 			var url = event.currentTarget.getAttribute( 'href' );
   5598 			if( url ) {
   5599 				showPreview( url );
   5600 				event.preventDefault();
   5601 			}
   5602 		}
   5603 
   5604 	}
   5605 
   5606 	/**
   5607 	 * Handles click on the auto-sliding controls element.
   5608 	 *
   5609 	 * @param {object} [event]
   5610 	 */
   5611 	function onAutoSlidePlayerClick( event ) {
   5612 
   5613 		// Replay
   5614 		if( Reveal.isLastSlide() && config.loop === false ) {
   5615 			slide( 0, 0 );
   5616 			resumeAutoSlide();
   5617 		}
   5618 		// Resume
   5619 		else if( autoSlidePaused ) {
   5620 			resumeAutoSlide();
   5621 		}
   5622 		// Pause
   5623 		else {
   5624 			pauseAutoSlide();
   5625 		}
   5626 
   5627 	}
   5628 
   5629 
   5630 	// --------------------------------------------------------------------//
   5631 	// ------------------------ PLAYBACK COMPONENT ------------------------//
   5632 	// --------------------------------------------------------------------//
   5633 
   5634 
   5635 	/**
   5636 	 * Constructor for the playback component, which displays
   5637 	 * play/pause/progress controls.
   5638 	 *
   5639 	 * @param {HTMLElement} container The component will append
   5640 	 * itself to this
   5641 	 * @param {function} progressCheck A method which will be
   5642 	 * called frequently to get the current progress on a range
   5643 	 * of 0-1
   5644 	 */
   5645 	function Playback( container, progressCheck ) {
   5646 
   5647 		// Cosmetics
   5648 		this.diameter = 100;
   5649 		this.diameter2 = this.diameter/2;
   5650 		this.thickness = 6;
   5651 
   5652 		// Flags if we are currently playing
   5653 		this.playing = false;
   5654 
   5655 		// Current progress on a 0-1 range
   5656 		this.progress = 0;
   5657 
   5658 		// Used to loop the animation smoothly
   5659 		this.progressOffset = 1;
   5660 
   5661 		this.container = container;
   5662 		this.progressCheck = progressCheck;
   5663 
   5664 		this.canvas = document.createElement( 'canvas' );
   5665 		this.canvas.className = 'playback';
   5666 		this.canvas.width = this.diameter;
   5667 		this.canvas.height = this.diameter;
   5668 		this.canvas.style.width = this.diameter2 + 'px';
   5669 		this.canvas.style.height = this.diameter2 + 'px';
   5670 		this.context = this.canvas.getContext( '2d' );
   5671 
   5672 		this.container.appendChild( this.canvas );
   5673 
   5674 		this.render();
   5675 
   5676 	}
   5677 
   5678 	/**
   5679 	 * @param value
   5680 	 */
   5681 	Playback.prototype.setPlaying = function( value ) {
   5682 
   5683 		var wasPlaying = this.playing;
   5684 
   5685 		this.playing = value;
   5686 
   5687 		// Start repainting if we weren't already
   5688 		if( !wasPlaying && this.playing ) {
   5689 			this.animate();
   5690 		}
   5691 		else {
   5692 			this.render();
   5693 		}
   5694 
   5695 	};
   5696 
   5697 	Playback.prototype.animate = function() {
   5698 
   5699 		var progressBefore = this.progress;
   5700 
   5701 		this.progress = this.progressCheck();
   5702 
   5703 		// When we loop, offset the progress so that it eases
   5704 		// smoothly rather than immediately resetting
   5705 		if( progressBefore > 0.8 && this.progress < 0.2 ) {
   5706 			this.progressOffset = this.progress;
   5707 		}
   5708 
   5709 		this.render();
   5710 
   5711 		if( this.playing ) {
   5712 			features.requestAnimationFrameMethod.call( window, this.animate.bind( this ) );
   5713 		}
   5714 
   5715 	};
   5716 
   5717 	/**
   5718 	 * Renders the current progress and playback state.
   5719 	 */
   5720 	Playback.prototype.render = function() {
   5721 
   5722 		var progress = this.playing ? this.progress : 0,
   5723 			radius = ( this.diameter2 ) - this.thickness,
   5724 			x = this.diameter2,
   5725 			y = this.diameter2,
   5726 			iconSize = 28;
   5727 
   5728 		// Ease towards 1
   5729 		this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
   5730 
   5731 		var endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
   5732 		var startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
   5733 
   5734 		this.context.save();
   5735 		this.context.clearRect( 0, 0, this.diameter, this.diameter );
   5736 
   5737 		// Solid background color
   5738 		this.context.beginPath();
   5739 		this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
   5740 		this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
   5741 		this.context.fill();
   5742 
   5743 		// Draw progress track
   5744 		this.context.beginPath();
   5745 		this.context.arc( x, y, radius, 0, Math.PI * 2, false );
   5746 		this.context.lineWidth = this.thickness;
   5747 		this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
   5748 		this.context.stroke();
   5749 
   5750 		if( this.playing ) {
   5751 			// Draw progress on top of track
   5752 			this.context.beginPath();
   5753 			this.context.arc( x, y, radius, startAngle, endAngle, false );
   5754 			this.context.lineWidth = this.thickness;
   5755 			this.context.strokeStyle = '#fff';
   5756 			this.context.stroke();
   5757 		}
   5758 
   5759 		this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
   5760 
   5761 		// Draw play/pause icons
   5762 		if( this.playing ) {
   5763 			this.context.fillStyle = '#fff';
   5764 			this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
   5765 			this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
   5766 		}
   5767 		else {
   5768 			this.context.beginPath();
   5769 			this.context.translate( 4, 0 );
   5770 			this.context.moveTo( 0, 0 );
   5771 			this.context.lineTo( iconSize - 4, iconSize / 2 );
   5772 			this.context.lineTo( 0, iconSize );
   5773 			this.context.fillStyle = '#fff';
   5774 			this.context.fill();
   5775 		}
   5776 
   5777 		this.context.restore();
   5778 
   5779 	};
   5780 
   5781 	Playback.prototype.on = function( type, listener ) {
   5782 		this.canvas.addEventListener( type, listener, false );
   5783 	};
   5784 
   5785 	Playback.prototype.off = function( type, listener ) {
   5786 		this.canvas.removeEventListener( type, listener, false );
   5787 	};
   5788 
   5789 	Playback.prototype.destroy = function() {
   5790 
   5791 		this.playing = false;
   5792 
   5793 		if( this.canvas.parentNode ) {
   5794 			this.container.removeChild( this.canvas );
   5795 		}
   5796 
   5797 	};
   5798 
   5799 
   5800 	// --------------------------------------------------------------------//
   5801 	// ------------------------------- API --------------------------------//
   5802 	// --------------------------------------------------------------------//
   5803 
   5804 
   5805 	Reveal = {
   5806 		VERSION: VERSION,
   5807 
   5808 		initialize: initialize,
   5809 		configure: configure,
   5810 
   5811 		sync: sync,
   5812 		syncSlide: syncSlide,
   5813 		syncFragments: syncFragments,
   5814 
   5815 		// Navigation methods
   5816 		slide: slide,
   5817 		left: navigateLeft,
   5818 		right: navigateRight,
   5819 		up: navigateUp,
   5820 		down: navigateDown,
   5821 		prev: navigatePrev,
   5822 		next: navigateNext,
   5823 
   5824 		// Fragment methods
   5825 		navigateFragment: navigateFragment,
   5826 		prevFragment: previousFragment,
   5827 		nextFragment: nextFragment,
   5828 
   5829 		// Deprecated aliases
   5830 		navigateTo: slide,
   5831 		navigateLeft: navigateLeft,
   5832 		navigateRight: navigateRight,
   5833 		navigateUp: navigateUp,
   5834 		navigateDown: navigateDown,
   5835 		navigatePrev: navigatePrev,
   5836 		navigateNext: navigateNext,
   5837 
   5838 		// Forces an update in slide layout
   5839 		layout: layout,
   5840 
   5841 		// Randomizes the order of slides
   5842 		shuffle: shuffle,
   5843 
   5844 		// Returns an object with the available routes as booleans (left/right/top/bottom)
   5845 		availableRoutes: availableRoutes,
   5846 
   5847 		// Returns an object with the available fragments as booleans (prev/next)
   5848 		availableFragments: availableFragments,
   5849 
   5850 		// Toggles a help overlay with keyboard shortcuts
   5851 		toggleHelp: toggleHelp,
   5852 
   5853 		// Toggles the overview mode on/off
   5854 		toggleOverview: toggleOverview,
   5855 
   5856 		// Toggles the "black screen" mode on/off
   5857 		togglePause: togglePause,
   5858 
   5859 		// Toggles the auto slide mode on/off
   5860 		toggleAutoSlide: toggleAutoSlide,
   5861 
   5862 		// State checks
   5863 		isOverview: isOverview,
   5864 		isPaused: isPaused,
   5865 		isAutoSliding: isAutoSliding,
   5866 		isSpeakerNotes: isSpeakerNotes,
   5867 
   5868 		// Slide preloading
   5869 		loadSlide: loadSlide,
   5870 		unloadSlide: unloadSlide,
   5871 
   5872 		// Adds or removes all internal event listeners (such as keyboard)
   5873 		addEventListeners: addEventListeners,
   5874 		removeEventListeners: removeEventListeners,
   5875 
   5876 		// Facility for persisting and restoring the presentation state
   5877 		getState: getState,
   5878 		setState: setState,
   5879 
   5880 		// Presentation progress
   5881 		getSlidePastCount: getSlidePastCount,
   5882 
   5883 		// Presentation progress on range of 0-1
   5884 		getProgress: getProgress,
   5885 
   5886 		// Returns the indices of the current, or specified, slide
   5887 		getIndices: getIndices,
   5888 
   5889 		// Returns an Array of all slides
   5890 		getSlides: getSlides,
   5891 
   5892 		// Returns an Array of objects representing the attributes on
   5893 		// the slides
   5894 		getSlidesAttributes: getSlidesAttributes,
   5895 
   5896 		// Returns the total number of slides
   5897 		getTotalSlides: getTotalSlides,
   5898 
   5899 		// Returns the slide element at the specified index
   5900 		getSlide: getSlide,
   5901 
   5902 		// Returns the slide background element at the specified index
   5903 		getSlideBackground: getSlideBackground,
   5904 
   5905 		// Returns the speaker notes string for a slide, or null
   5906 		getSlideNotes: getSlideNotes,
   5907 
   5908 		// Returns the previous slide element, may be null
   5909 		getPreviousSlide: function() {
   5910 			return previousSlide;
   5911 		},
   5912 
   5913 		// Returns the current slide element
   5914 		getCurrentSlide: function() {
   5915 			return currentSlide;
   5916 		},
   5917 
   5918 		// Returns the current scale of the presentation content
   5919 		getScale: function() {
   5920 			return scale;
   5921 		},
   5922 
   5923 		// Returns the current configuration object
   5924 		getConfig: function() {
   5925 			return config;
   5926 		},
   5927 
   5928 		// Helper method, retrieves query string as a key/value hash
   5929 		getQueryHash: function() {
   5930 			var query = {};
   5931 
   5932 			location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, function(a) {
   5933 				query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
   5934 			} );
   5935 
   5936 			// Basic deserialization
   5937 			for( var i in query ) {
   5938 				var value = query[ i ];
   5939 
   5940 				query[ i ] = deserialize( unescape( value ) );
   5941 			}
   5942 
   5943 			return query;
   5944 		},
   5945 
   5946 		// Returns the top-level DOM element
   5947 		getRevealElement: function() {
   5948 			return dom.wrapper || document.querySelector( '.reveal' );
   5949 		},
   5950 
   5951 		// Returns a hash with all registered plugins
   5952 		getPlugins: function() {
   5953 			return plugins;
   5954 		},
   5955 
   5956 		// Returns true if we're currently on the first slide
   5957 		isFirstSlide: function() {
   5958 			return ( indexh === 0 && indexv === 0 );
   5959 		},
   5960 
   5961 		// Returns true if we're currently on the last slide
   5962 		isLastSlide: function() {
   5963 			if( currentSlide ) {
   5964 				// Does this slide have a next sibling?
   5965 				if( currentSlide.nextElementSibling ) return false;
   5966 
   5967 				// If it's vertical, does its parent have a next sibling?
   5968 				if( isVerticalSlide( currentSlide ) && currentSlide.parentNode.nextElementSibling ) return false;
   5969 
   5970 				return true;
   5971 			}
   5972 
   5973 			return false;
   5974 		},
   5975 
   5976 		// Returns true if we're on the last slide in the current
   5977 		// vertical stack
   5978 		isLastVerticalSlide: function() {
   5979 			if( currentSlide && isVerticalSlide( currentSlide ) ) {
   5980 				// Does this slide have a next sibling?
   5981 				if( currentSlide.nextElementSibling ) return false;
   5982 
   5983 				return true;
   5984 			}
   5985 
   5986 			return false;
   5987 		},
   5988 
   5989 		// Checks if reveal.js has been loaded and is ready for use
   5990 		isReady: function() {
   5991 			return loaded;
   5992 		},
   5993 
   5994 		// Forward event binding to the reveal DOM element
   5995 		addEventListener: function( type, listener, useCapture ) {
   5996 			if( 'addEventListener' in window ) {
   5997 				Reveal.getRevealElement().addEventListener( type, listener, useCapture );
   5998 			}
   5999 		},
   6000 		removeEventListener: function( type, listener, useCapture ) {
   6001 			if( 'addEventListener' in window ) {
   6002 				Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
   6003 			}
   6004 		},
   6005 
   6006 		// Adds/removes a custom key binding
   6007 		addKeyBinding: addKeyBinding,
   6008 		removeKeyBinding: removeKeyBinding,
   6009 
   6010 		// API for registering and retrieving plugins
   6011 		registerPlugin: registerPlugin,
   6012 		hasPlugin: hasPlugin,
   6013 		getPlugin: getPlugin,
   6014 
   6015 		// Programmatically triggers a keyboard event
   6016 		triggerKey: function( keyCode ) {
   6017 			onDocumentKeyDown( { keyCode: keyCode } );
   6018 		},
   6019 
   6020 		// Registers a new shortcut to include in the help overlay
   6021 		registerKeyboardShortcut: function( key, value ) {
   6022 			keyboardShortcuts[key] = value;
   6023 		}
   6024 	};
   6025 
   6026 	return Reveal;
   6027 
   6028 }));