PageSourceSearch

https://pydelhi.org/pydelhi-intro/js/reveal.js

js pydelhi.org collected 2026-10-03 22:51:25 UTC 124,376 bytes, 4,508 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.