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