← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~tomasgroth/openlp/revealjs-380 into lp:openlp

 

Tomas Groth has proposed merging lp:~tomasgroth/openlp/revealjs-380 into lp:openlp.

Commit message:
Update Reveal.js to 3.8.0.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/revealjs-380/+merge/365698
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/revealjs-380 into lp:openlp.
=== modified file 'openlp/core/display/html/reveal.js'
--- openlp/core/display/html/reveal.js	2019-02-11 20:34:20 +0000
+++ openlp/core/display/html/reveal.js	2019-04-08 19:57:45 +0000
@@ -3,7 +3,7 @@
  * http://revealjs.com
  * MIT licensed
  *
- * Copyright (C) 2018 Hakim El Hattab, http://hakim.se
+ * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
  */
 (function( root, factory ) {
 	if( typeof define === 'function' && define.amd ) {
@@ -26,7 +26,7 @@
 	var Reveal;
 
 	// The reveal.js version
-	var VERSION = '3.7.0';
+	var VERSION = '3.8.0';
 
 	var SLIDES_SELECTOR = '.slides section',
 		HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
@@ -67,16 +67,36 @@
 			progress: true,
 
 			// Display the page number of the current slide
+			// - true:    Show slide number
+			// - false:   Hide slide number
+			//
+			// Can optionally be set as a string that specifies the number formatting:
+			// - "h.v":	  Horizontal . vertical slide number (default)
+			// - "h/v":	  Horizontal / vertical slide number
+			// - "c":	  Flattened slide number
+			// - "c/t":	  Flattened slide number / total slides
+			//
+			// Alternatively, you can provide a function that returns the slide
+			// number for the current slide. The function needs to return an array
+			// with one string [slideNumber] or three strings [n1,delimiter,n2].
+			// See #formatSlideNumber().
 			slideNumber: false,
 
+			// Can be used to limit the contexts in which the slide number appears
+			// - "all":      Always show the slide number
+			// - "print":    Only when printing to PDF
+			// - "speaker":  Only in the speaker view
+			showSlideNumber: 'all',
+
 			// Use 1 based indexing for # links to match slide number (default is zero
 			// based)
 			hashOneBasedIndex: false,
 
-			// Determine which displays to show the slide number on
-			showSlideNumber: 'all',
+			// Add the current slide number to the URL hash so that reloading the
+			// page/copying the URL will return you to the same slide
+			hash: false,
 
-			// Push each slide change to the browser history
+			// Push each slide change to the browser history.  Implies `hash: true`
 			history: false,
 
 			// Enable keyboard shortcuts for navigation
@@ -104,6 +124,32 @@
 			// Change the presentation direction to be RTL
 			rtl: false,
 
+			// Changes the behavior of our navigation directions.
+			//
+			// "default"
+			// Left/right arrow keys step between horizontal slides, up/down
+			// arrow keys step between vertical slides. Space key steps through
+			// all slides (both horizontal and vertical).
+			//
+			// "linear"
+			// Removes the up/down arrows. Left/right arrows step through all
+			// slides (both horizontal and vertical).
+			//
+			// "grid"
+			// When this is enabled, stepping left/right from a vertical stack
+			// to an adjacent vertical stack will land you at the same vertical
+			// index.
+			//
+			// Consider a deck with six slides ordered in two vertical stacks:
+			// 1.1    2.1
+			// 1.2    2.2
+			// 1.3    2.3
+			//
+			// If you're on slide 1.3 and navigate right, you will normally move
+			// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
+			// from 1.3 -> 2.3.
+			navigationMode: 'default',
+
 			// Randomizes the order of slides each time the presentation loads
 			shuffle: false,
 
@@ -134,6 +180,13 @@
 			// - false:  No media will autoplay, regardless of individual setting
 			autoPlayMedia: null,
 
+			// Global override for preloading lazy-loaded iframes
+			// - null:   Iframes with data-src AND data-preload will be loaded when within
+			//           the viewDistance, iframes with only data-src will be loaded when visible
+			// - true:   All iframes with data-src will be loaded when within the viewDistance
+			// - false:  All iframes with data-src will be loaded only when visible
+			preloadIframes: null,
+
 			// Controls automatic progression to the next slide
 			// - 0:      Auto-sliding only happens if the data-autoslide HTML attribute
 			//           is present on the current slide or fragment
@@ -220,6 +273,12 @@
 			// The display mode that will be used to show slides
 			display: 'block',
 
+			// Hide cursor if inactive
+			hideInactiveCursor: true,
+
+			// Time before the cursor is hidden (in ms)
+			hideCursorTime: 5000,
+
 			// Script dependencies to load
 			dependencies: []
 
@@ -267,6 +326,12 @@
 		// Cached references to DOM elements
 		dom = {},
 
+		// A list of registered reveal.js plugins
+		plugins = {},
+
+		// List of asynchronously loaded reveal.js dependencies
+		asyncDependencies = [],
+
 		// Features supported by the browser, see #checkCapabilities()
 		features = {},
 
@@ -282,6 +347,12 @@
 		// Delays updates to the URL due to a Chrome thumbnailer bug
 		writeURLTimeout = 0,
 
+		// Is the mouse pointer currently hidden from view
+		cursorHidden = false,
+
+		// Timeout used to determine when the cursor is inactive
+		cursorInactiveTimeout = 0,
+
 		// Flags if the interaction event listeners are bound
 		eventsAreBound = false,
 
@@ -298,26 +369,14 @@
 		touch = {
 			startX: 0,
 			startY: 0,
-			startSpan: 0,
 			startCount: 0,
 			captured: false,
 			threshold: 40
 		},
 
-		// Holds information about the keyboard shortcuts
-		keyboardShortcuts = {
-			'N  ,  SPACE':			'Next slide',
-			'P':					'Previous slide',
-			'←  ,  H':		'Navigate left',
-			'→  ,  L':		'Navigate right',
-			'↑  ,  K':		'Navigate up',
-			'↓  ,  J':		'Navigate down',
-			'Home':					'First slide',
-			'End':					'Last slide',
-			'B  ,  .':				'Pause',
-			'F':					'Fullscreen',
-			'ESC, O':				'Slide overview'
-		},
+		// A key:value map of shortcut keyboard keys and descriptions of
+		// the actions they trigger, generated in #configure()
+		keyboardShortcuts = {},
 
 		// Holds custom key code mappings
 		registeredKeyBindings = {};
@@ -377,7 +436,7 @@
 		// Hide the address bar in mobile browsers
 		hideAddressBar();
 
-		// Loads the dependencies and continues to #start() once done
+		// Loads dependencies and continues to #start() once done
 		load();
 
 	}
@@ -440,57 +499,148 @@
 	function load() {
 
 		var scripts = [],
-			scriptsAsync = [],
-			scriptsToPreload = 0;
-
-		// Called once synchronous scripts finish loading
-		function proceed() {
-			if( scriptsAsync.length ) {
-				// Load asynchronous scripts
-				head.js.apply( null, scriptsAsync );
-			}
-
-			start();
-		}
-
-		function loadScript( s ) {
-			head.ready( s.src.match( /([\w\d_\-]*)\.?js(\?[\w\d.=&]*)?$|[^\\\/]*$/i )[0], function() {
-				// Extension may contain callback functions
-				if( typeof s.callback === 'function' ) {
-					s.callback.apply( this );
-				}
-
-				if( --scriptsToPreload === 0 ) {
-					proceed();
-				}
-			});
-		}
-
-		for( var i = 0, len = config.dependencies.length; i < len; i++ ) {
-			var s = config.dependencies[i];
-
+			scriptsToLoad = 0;
+
+		config.dependencies.forEach( function( s ) {
 			// Load if there's no condition or the condition is truthy
 			if( !s.condition || s.condition() ) {
 				if( s.async ) {
-					scriptsAsync.push( s.src );
+					asyncDependencies.push( s );
 				}
 				else {
-					scripts.push( s.src );
+					scripts.push( s );
 				}
-
-				loadScript( s );
 			}
-		}
+		} );
 
 		if( scripts.length ) {
-			scriptsToPreload = scripts.length;
+			scriptsToLoad = scripts.length;
 
 			// Load synchronous scripts
-			head.js.apply( null, scripts );
-		}
-		else {
-			proceed();
-		}
+			scripts.forEach( function( s ) {
+				loadScript( s.src, function() {
+
+					if( typeof s.callback === 'function' ) s.callback();
+
+					if( --scriptsToLoad === 0 ) {
+						initPlugins();
+					}
+
+				} );
+			} );
+		}
+		else {
+			initPlugins();
+		}
+
+	}
+
+	/**
+	 * Initializes our plugins and waits for them to be ready
+	 * before proceeding.
+	 */
+	function initPlugins() {
+
+		var pluginsToInitialize = Object.keys( plugins ).length;
+
+		// If there are no plugins, skip this step
+		if( pluginsToInitialize === 0 ) {
+			loadAsyncDependencies();
+		}
+		// ... otherwise initialize plugins
+		else {
+
+			var afterPlugInitialized = function() {
+				if( --pluginsToInitialize === 0 ) {
+					loadAsyncDependencies();
+				}
+			};
+
+			for( var i in plugins ) {
+
+				var plugin = plugins[i];
+
+				// If the plugin has an 'init' method, invoke it
+				if( typeof plugin.init === 'function' ) {
+					var callback = plugin.init();
+
+					// If the plugin returned a Promise, wait for it
+					if( callback && typeof callback.then === 'function' ) {
+						callback.then( afterPlugInitialized );
+					}
+					else {
+						afterPlugInitialized();
+					}
+				}
+				else {
+					afterPlugInitialized();
+				}
+
+			}
+
+		}
+
+	}
+
+	/**
+	 * Loads all async reveal.js dependencies.
+	 */
+	function loadAsyncDependencies() {
+
+		if( asyncDependencies.length ) {
+			asyncDependencies.forEach( function( s ) {
+				loadScript( s.src, s.callback );
+			} );
+		}
+
+		start();
+
+	}
+
+	/**
+	 * Loads a JavaScript file from the given URL and executes it.
+	 *
+	 * @param {string} url Address of the .js file to load
+	 * @param {function} callback Method to invoke when the script
+	 * has loaded and executed
+	 */
+	function loadScript( url, callback ) {
+
+		var script = document.createElement( 'script' );
+		script.type = 'text/javascript';
+		script.async = false;
+		script.defer = false;
+		script.src = url;
+
+		if( callback ) {
+
+			// Success callback
+			script.onload = script.onreadystatechange = function( event ) {
+				if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) {
+
+					// Kill event listeners
+					script.onload = script.onreadystatechange = script.onerror = null;
+
+					callback();
+
+				}
+			};
+
+			// Error callback
+			script.onerror = function( err ) {
+
+				// Kill event listeners
+				script.onload = script.onreadystatechange = script.onerror = null;
+
+				callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) );
+
+			};
+
+		}
+
+		// Append the script at the end of <head>
+		var head = document.querySelector( 'head' );
+		head.insertBefore( script, head.lastChild );
 
 	}
 
@@ -601,8 +751,7 @@
 		dom.speakerNotes.setAttribute( 'tabindex', '0' );
 
 		// Overlay graphic which is displayed during the paused mode
-		dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', '<button class="resume-button">Resume presentation</button>' );
-		dom.resumeButton = dom.pauseOverlay.querySelector( '.resume-button' );
+		dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
 
 		dom.wrapper.setAttribute( 'role', 'application' );
 
@@ -1082,18 +1231,27 @@
 		if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
 		if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
 
-		// If this slide has a background color, add a class that
+		// If this slide has a background color, we add a class that
 		// signals if it is light or dark. If the slide has no background
-		// color, no class will be set
-		var computedBackgroundStyle = window.getComputedStyle( element );
-		if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
-			var rgb = colorToRgb( computedBackgroundStyle.backgroundColor );
+		// color, no class will be added
+		var contrastColor = data.backgroundColor;
+
+		// If no bg color was found, check the computed background
+		if( !contrastColor ) {
+			var computedBackgroundStyle = window.getComputedStyle( element );
+			if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
+				contrastColor = computedBackgroundStyle.backgroundColor;
+			}
+		}
+
+		if( contrastColor ) {
+			var rgb = colorToRgb( contrastColor );
 
 			// Ignore fully transparent backgrounds. Some browsers return
 			// rgba(0,0,0,0) when reading the computed background color of
 			// an element with no background
 			if( rgb && rgb.a !== 0 ) {
-				if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) {
+				if( colorBrightness( contrastColor ) < 128 ) {
 					slide.classList.add( 'has-dark-background' );
 				}
 				else {
@@ -1216,6 +1374,18 @@
 			disableRollingLinks();
 		}
 
+		// Auto-hide the mouse pointer when its inactive
+		if( config.hideInactiveCursor ) {
+			document.addEventListener( 'mousemove', onDocumentCursorActive, false );
+			document.addEventListener( 'mousedown', onDocumentCursorActive, false );
+		}
+		else {
+			showCursor();
+
+			document.removeEventListener( 'mousemove', onDocumentCursorActive, false );
+			document.removeEventListener( 'mousedown', onDocumentCursorActive, false );
+		}
+
 		// Iframe link previews
 		if( config.previewLinks ) {
 			enablePreviewLinks();
@@ -1263,6 +1433,34 @@
 
 		dom.slideNumber.style.display = slideNumberDisplay;
 
+		// Add the navigation mode to the DOM so we can adjust styling
+		if( config.navigationMode !== 'default' ) {
+			dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
+		}
+		else {
+			dom.wrapper.removeAttribute( 'data-navigation-mode' );
+		}
+
+		// Define our contextual list of keyboard shortcuts
+		if( config.navigationMode === 'linear' ) {
+			keyboardShortcuts['&#8594;  ,  &#8595;  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide';
+			keyboardShortcuts['&#8592;  ,  &#8593;  ,  P  ,  H  ,  K']           = 'Previous slide';
+		}
+		else {
+			keyboardShortcuts['N  ,  SPACE']   = 'Next slide';
+			keyboardShortcuts['P']             = 'Previous slide';
+			keyboardShortcuts['&#8592;  ,  H'] = 'Navigate left';
+			keyboardShortcuts['&#8594;  ,  L'] = 'Navigate right';
+			keyboardShortcuts['&#8593;  ,  K'] = 'Navigate up';
+			keyboardShortcuts['&#8595;  ,  J'] = 'Navigate down';
+		}
+
+		keyboardShortcuts['Home  ,  &#8984;/CTRL &#8592;'] = 'First slide';
+		keyboardShortcuts['End  ,  &#8984;/CTRL &#8594;']  = 'Last slide';
+		keyboardShortcuts['B  ,  .']                       = 'Pause';
+		keyboardShortcuts['F']                             = 'Fullscreen';
+		keyboardShortcuts['ESC, O']                        = 'Slide overview';
+
 		sync();
 
 	}
@@ -1307,7 +1505,7 @@
 			dom.progress.addEventListener( 'click', onProgressClicked, false );
 		}
 
-		dom.resumeButton.addEventListener( 'click', resume, false );
+		dom.pauseOverlay.addEventListener( 'click', resume, false );
 
 		if( config.focusBodyOnPageVisibilityChange ) {
 			var visibilityChange;
@@ -1372,7 +1570,7 @@
 		dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false );
 		dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false );
 
-		dom.resumeButton.removeEventListener( 'click', resume, false );
+		dom.pauseOverlay.removeEventListener( 'click', resume, false );
 
 		if ( config.progress && dom.progress ) {
 			dom.progress.removeEventListener( 'click', onProgressClicked, false );
@@ -1390,6 +1588,53 @@
 	}
 
 	/**
+	 * Registers a new plugin with this reveal.js instance.
+	 *
+	 * reveal.js waits for all regisered plugins to initialize
+	 * before considering itself ready, as long as the plugin
+	 * is registered before calling `Reveal.initialize()`.
+	 */
+	function registerPlugin( id, plugin ) {
+
+		if( plugins[id] === undefined ) {
+			plugins[id] = plugin;
+
+			// If a plugin is registered after reveal.js is loaded,
+			// initialize it right away
+			if( loaded && typeof plugin.init === 'function' ) {
+				plugin.init();
+			}
+		}
+		else {
+			console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
+		}
+
+	}
+
+	/**
+	 * Checks if a specific plugin has been registered.
+	 *
+	 * @param {String} id Unique plugin identifier
+	 */
+	function hasPlugin( id ) {
+
+		return !!plugins[id];
+
+	}
+
+	/**
+	 * Returns the specific plugin instance, if a plugin
+	 * with the given ID has been registered.
+	 *
+	 * @param {String} id Unique plugin identifier
+	 */
+	function getPlugin( id ) {
+
+		return plugins[id];
+
+	}
+
+	/**
 	 * Add a custom key binding with optional description to
 	 * be added to the help screen.
 	 */
@@ -1677,11 +1922,19 @@
 			// Change the .stretch element height to 0 in order find the height of all
 			// the other elements
 			element.style.height = '0px';
+
+			// In Overview mode, the parent (.slide) height is set of 700px.
+			// Restore it temporarily to its natural height.
+			element.parentNode.style.height = 'auto';
+
 			newHeight = height - element.parentNode.offsetHeight;
 
 			// Restore the old height, just in case
 			element.style.height = oldHeight + 'px';
 
+			// Clear the parent (.slide) height. .removeProperty works in IE9+
+			element.parentNode.style.removeProperty('height');
+
 			return newHeight;
 		}
 
@@ -1699,15 +1952,6 @@
 	}
 
 	/**
-	 * Check if this instance is being used to print a PDF with fragments.
-	 */
-	function isPrintingPDFFragments() {
-
-		return ( /print-pdf-fragments/gi ).test( window.location.search );
-
-	}
-
-	/**
 	 * Hides the address bar if we're on a mobile device.
 	 */
 	function hideAddressBar() {
@@ -1970,8 +2214,20 @@
 
 			if( !config.disableLayout ) {
 
+				// On some mobile devices '100vh' is taller than the visible
+				// viewport which leads to part of the presentation being
+				// cut off. To work around this we define our own '--vh' custom
+				// property where 100x adds up to the correct height.
+				//
+				// https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+				if( isMobileDevice ) {
+					document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' );
+				}
+
 				var size = getComputedSlideSize();
 
+				var oldScale = scale;
+
 				// Layout the contents of the slides
 				layoutSlideContents( config.width, config.height );
 
@@ -2044,6 +2300,13 @@
 
 				}
 
+				if( oldScale !== scale ) {
+					dispatchEvent( 'resize', {
+						'oldScale': oldScale,
+						'scale': scale,
+						'size': size
+					} );
+				}
 			}
 
 			updateProgress();
@@ -2443,6 +2706,32 @@
 	}
 
 	/**
+	 * Shows the mouse pointer after it has been hidden with
+	 * #hideCursor.
+	 */
+	function showCursor() {
+
+		if( cursorHidden ) {
+			cursorHidden = false;
+			dom.wrapper.style.cursor = '';
+		}
+
+	}
+
+	/**
+	 * Hides the mouse pointer when it's on top of the .reveal
+	 * container.
+	 */
+	function hideCursor() {
+
+		if( cursorHidden === false ) {
+			cursorHidden = true;
+			dom.wrapper.style.cursor = 'none';
+		}
+
+	}
+
+	/**
 	 * Enters the paused mode which fades everything on screen to
 	 * black.
 	 */
@@ -2584,28 +2873,6 @@
 
 		layout();
 
-		// Apply the new state
-		stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
-			// Check if this state existed on the previous slide. If it
-			// did, we will avoid adding it repeatedly
-			for( var j = 0; j < stateBefore.length; j++ ) {
-				if( stateBefore[j] === state[i] ) {
-					stateBefore.splice( j, 1 );
-					continue stateLoop;
-				}
-			}
-
-			document.documentElement.classList.add( state[i] );
-
-			// Dispatch custom event matching the state's name
-			dispatchEvent( state[i] );
-		}
-
-		// Clean up the remains of the previous state
-		while( stateBefore.length ) {
-			document.documentElement.classList.remove( stateBefore.pop() );
-		}
-
 		// Update the overview if it's currently active
 		if( isOverview() ) {
 			updateOverview();
@@ -2654,6 +2921,28 @@
 			}
 		}
 
+		// Apply the new state
+		stateLoop: for( var i = 0, len = state.length; i < len; i++ ) {
+			// Check if this state existed on the previous slide. If it
+			// did, we will avoid adding it repeatedly
+			for( var j = 0; j < stateBefore.length; j++ ) {
+				if( stateBefore[j] === state[i] ) {
+					stateBefore.splice( j, 1 );
+					continue stateLoop;
+				}
+			}
+
+			document.documentElement.classList.add( state[i] );
+
+			// Dispatch custom event matching the state's name
+			dispatchEvent( state[i] );
+		}
+
+		// Clean up the remains of the previous state
+		while( stateBefore.length ) {
+			document.documentElement.classList.remove( stateBefore.pop() );
+		}
+
 		if( slideChanged ) {
 			dispatchEvent( 'slidechanged', {
 				'indexh': indexh,
@@ -2679,6 +2968,7 @@
 		updateParallax();
 		updateSlideNumber();
 		updateNotes();
+		updateFragments();
 
 		// Update the URL hash
 		writeURL();
@@ -2751,6 +3041,9 @@
 	 */
 	function syncSlide( slide ) {
 
+		// Default to the current slide
+		slide = slide || currentSlide;
+
 		syncBackground( slide );
 		syncFragments( slide );
 
@@ -2767,10 +3060,14 @@
 	 * after reveal.js has already initialized.
 	 *
 	 * @param {HTMLElement} slide
+	 * @return {Array} a list of the HTML fragments that were synced
 	 */
 	function syncFragments( slide ) {
 
-		sortFragments( slide.querySelectorAll( '.fragment' ) );
+		// Default to the current slide
+		slide = slide || currentSlide;
+
+		return sortFragments( slide.querySelectorAll( '.fragment' ) );
 
 	}
 
@@ -2903,14 +3200,11 @@
 					element.classList.add( reverse ? 'future' : 'past' );
 
 					if( config.fragments ) {
-						var pastFragments = toArray( element.querySelectorAll( '.fragment' ) );
-
-						// Show all fragments on prior slides
-						while( pastFragments.length ) {
-							var pastFragment = pastFragments.pop();
-							pastFragment.classList.add( 'visible' );
-							pastFragment.classList.remove( 'current-fragment' );
-						}
+						// Show all fragments in prior slides
+						toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) {
+							fragment.classList.add( 'visible' );
+							fragment.classList.remove( 'current-fragment' );
+						} );
 					}
 				}
 				else if( i > index ) {
@@ -2918,14 +3212,11 @@
 					element.classList.add( reverse ? 'past' : 'future' );
 
 					if( config.fragments ) {
-						var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) );
-
-						// No fragments in future slides should be visible ahead of time
-						while( futureFragments.length ) {
-							var futureFragment = futureFragments.pop();
-							futureFragment.classList.remove( 'visible' );
-							futureFragment.classList.remove( 'current-fragment' );
-						}
+						// Hide all fragments in future slides
+						toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) {
+							fragment.classList.remove( 'visible' );
+							fragment.classList.remove( 'current-fragment' );
+						} );
 					}
 				}
 			}
@@ -3104,47 +3395,47 @@
 
 
 	/**
-	 * Updates the slide number div to reflect the current slide.
-	 *
-	 * The following slide number formats are available:
-	 *  "h.v":	horizontal . vertical slide number (default)
-	 *  "h/v":	horizontal / vertical slide number
-	 *    "c":	flattened slide number
-	 *  "c/t":	flattened slide number / total slides
+	 * Updates the slide number to match the current slide.
 	 */
 	function updateSlideNumber() {
 
 		// Update slide number if enabled
 		if( config.slideNumber && dom.slideNumber ) {
 
-			var value = [];
+			var value;
 			var format = 'h.v';
 
-			// Check if a custom number format is available
-			if( typeof config.slideNumber === 'string' ) {
-				format = config.slideNumber;
-			}
-
-			// If there are ONLY vertical slides in this deck, always use
-			// a flattened slide number
-			if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
-				format = 'c';
-			}
-
-			switch( format ) {
-				case 'c':
-					value.push( getSlidePastCount() + 1 );
-					break;
-				case 'c/t':
-					value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
-					break;
-				case 'h/v':
-					value.push( indexh + 1 );
-					if( isVerticalSlide() ) value.push( '/', indexv + 1 );
-					break;
-				default:
-					value.push( indexh + 1 );
-					if( isVerticalSlide() ) value.push( '.', indexv + 1 );
+			if( typeof config.slideNumber === 'function' ) {
+				value = config.slideNumber();
+			}
+			else {
+				// Check if a custom number format is available
+				if( typeof config.slideNumber === 'string' ) {
+					format = config.slideNumber;
+				}
+
+				// If there are ONLY vertical slides in this deck, always use
+				// a flattened slide number
+				if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) {
+					format = 'c';
+				}
+
+				value = [];
+				switch( format ) {
+					case 'c':
+						value.push( getSlidePastCount() + 1 );
+						break;
+					case 'c/t':
+						value.push( getSlidePastCount() + 1, '/', getTotalSlides() );
+						break;
+					case 'h/v':
+						value.push( indexh + 1 );
+						if( isVerticalSlide() ) value.push( '/', indexv + 1 );
+						break;
+					default:
+						value.push( indexh + 1 );
+						if( isVerticalSlide() ) value.push( '.', indexv + 1 );
+				}
 			}
 
 			dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] );
@@ -3428,6 +3719,26 @@
 	}
 
 	/**
+	 * Should the given element be preloaded?
+	 * Decides based on local element attributes and global config.
+	 *
+	 * @param {HTMLElement} element
+	 */
+	function shouldPreload( element ) {
+
+		// Prefer an explicit global preload setting
+		var preload = config.preloadIframes;
+
+		// If no global setting is available, fall back on the element's
+		// own preload setting
+		if( typeof preload !== 'boolean' ) {
+			preload = element.hasAttribute( 'data-preload' );
+		}
+
+		return preload;
+	}
+
+	/**
 	 * Called when the given slide is within the configured view
 	 * distance. Shows the slide element and loads any content
 	 * that is set to load lazily (data-src).
@@ -3442,10 +3753,12 @@
 		slide.style.display = config.display;
 
 		// Media elements with data-src attributes
-		toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) {
-			element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
-			element.setAttribute( 'data-lazy-loaded', '' );
-			element.removeAttribute( 'data-src' );
+		toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) {
+			if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
+				element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
+				element.setAttribute( 'data-lazy-loaded', '' );
+				element.removeAttribute( 'data-src' );
+			}
 		} );
 
 		// Media elements with <source> children
@@ -3563,7 +3876,7 @@
 		}
 
 		// Reset lazy-loaded media elements with src attributes
-		toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) {
+		toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) {
 			element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
 			element.removeAttribute( 'src' );
 		} );
@@ -3663,13 +3976,6 @@
 		_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
 		_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
 
-		// Always show media controls on mobile devices
-		if( isMobileDevice ) {
-			toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) {
-				el.controls = true;
-			} );
-		}
-
 	}
 
 	/**
@@ -3713,7 +4019,20 @@
 					// Mobile devices never fire a loaded event so instead
 					// of waiting, we initiate playback
 					else if( isMobileDevice ) {
-						el.play();
+						var promise = el.play();
+
+						// If autoplay does not work, ensure that the controls are visible so
+						// that the viewer can start the media on their own
+						if( promise && typeof promise.catch === 'function' && el.controls === false ) {
+							promise.catch( function() {
+								el.controls = true;
+
+								// Once the video does start playing, hide the controls again
+								el.addEventListener( 'play', function() {
+									el.controls = false;
+								} );
+							} );
+						}
 					}
 					// If the media isn't loaded, wait before playing
 					else {
@@ -3947,7 +4266,7 @@
 
 		}
 
-		return pastCount / ( totalCount - 1 );
+		return Math.min( pastCount / ( totalCount - 1 ), 1 );
 
 	}
 
@@ -3974,9 +4293,9 @@
 		var bits = hash.slice( 2 ).split( '/' ),
 			name = hash.replace( /#|\//gi, '' );
 
-		// If the first bit is invalid and there is a name we can
-		// assume that this is a named link
-		if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) {
+		// If the first bit is not fully numeric and there is a name we
+		// can assume that this is a named link
+		if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
 			var element;
 
 			// Ensure the named link is a valid HTML ID attribute
@@ -3988,10 +4307,13 @@
 			// Ensure that we're not already on a slide with the same name
 			var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
 
-			if( element && !isSameNameAsCurrentSlide ) {
-				// Find the position of the named slide and navigate to it
-				var indices = Reveal.getIndices( element );
-				slide( indices.h, indices.v );
+			if( element ) {
+				// If the slide exists and is not the current slide...
+				if ( !isSameNameAsCurrentSlide ) {
+					// ...find the position of the named slide and navigate to it
+					var indices = Reveal.getIndices(element);
+					slide(indices.h, indices.v);
+				}
 			}
 			// If the slide doesn't exist, navigate to the current slide
 			else {
@@ -4029,18 +4351,30 @@
 	 */
 	function writeURL( delay ) {
 
-		if( config.history ) {
-
-			// Make sure there's never more than one timeout running
-			clearTimeout( writeURLTimeout );
-
-			// If a delay is specified, timeout this call
-			if( typeof delay === 'number' ) {
-				writeURLTimeout = setTimeout( writeURL, delay );
-			}
-			else if( currentSlide ) {
+		// Make sure there's never more than one timeout running
+		clearTimeout( writeURLTimeout );
+
+		// If a delay is specified, timeout this call
+		if( typeof delay === 'number' ) {
+			writeURLTimeout = setTimeout( writeURL, delay );
+		}
+		else if( currentSlide ) {
+			// If we're configured to push to history OR the history
+			// API is not avaialble.
+			if( config.history || !window.history ) {
 				window.location.hash = locationHash();
 			}
+			// If we're configured to reflect the current slide in the
+			// URL without pushing to history.
+			else if( config.hash ) {
+				window.history.replaceState( null, null, '#' + locationHash() );
+			}
+			// If history and hash are both disabled, a hash may still
+			// be added to the URL by clicking on a href with a hash
+			// target. Counter this by always removing the hash.
+			else {
+				window.history.replaceState( null, null, window.location.pathname + window.location.search );
+			}
 		}
 
 	}
@@ -4108,6 +4442,25 @@
 	}
 
 	/**
+	 * Returns an array of objects where each object represents the
+	 * attributes on its respective slide.
+	 */
+	function getSlidesAttributes() {
+
+		return getSlides().map( function( slide ) {
+
+			var attributes = {};
+			for( var i = 0; i < slide.attributes.length; i++ ) {
+				var attribute = slide.attributes[ i ];
+				attributes[ attribute.name ] = attribute.value;
+			}
+			return attributes;
+
+		} );
+
+	}
+
+	/**
 	 * Retrieves the total number of slides in this presentation.
 	 *
 	 * @return {number}
@@ -4300,6 +4653,73 @@
 	}
 
 	/**
+	 * Refreshes the fragments on the current slide so that they
+	 * have the appropriate classes (.visible + .current-fragment).
+	 *
+	 * @param {number} [index] The index of the current fragment
+	 * @param {array} [fragments] Array containing all fragments
+	 * in the current slide
+	 *
+	 * @return {{shown: array, hidden: array}}
+	 */
+	function updateFragments( index, fragments ) {
+
+		var changedFragments = {
+			shown: [],
+			hidden: []
+		};
+
+		if( currentSlide && config.fragments ) {
+
+			fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) );
+
+			if( fragments.length ) {
+
+				if( typeof index !== 'number' ) {
+					var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
+					if( currentFragment ) {
+						index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
+					}
+				}
+
+				toArray( fragments ).forEach( function( el, i ) {
+
+					if( el.hasAttribute( 'data-fragment-index' ) ) {
+						i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
+					}
+
+					// Visible fragments
+					if( i <= index ) {
+						if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
+						el.classList.add( 'visible' );
+						el.classList.remove( 'current-fragment' );
+
+						// Announce the fragments one by one to the Screen Reader
+						dom.statusDiv.textContent = getStatusText( el );
+
+						if( i === index ) {
+							el.classList.add( 'current-fragment' );
+							startEmbeddedContent( el );
+						}
+					}
+					// Hidden fragments
+					else {
+						if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
+						el.classList.remove( 'visible' );
+						el.classList.remove( 'current-fragment' );
+					}
+
+				} );
+
+			}
+
+		}
+
+		return changedFragments;
+
+	}
+
+	/**
 	 * Navigate to the specified slide fragment.
 	 *
 	 * @param {?number} index The index of the fragment that
@@ -4334,53 +4754,24 @@
 					index += offset;
 				}
 
-				var fragmentsShown = [],
-					fragmentsHidden = [];
-
-				toArray( fragments ).forEach( function( element, i ) {
-
-					if( element.hasAttribute( 'data-fragment-index' ) ) {
-						i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 );
-					}
-
-					// Visible fragments
-					if( i <= index ) {
-						if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element );
-						element.classList.add( 'visible' );
-						element.classList.remove( 'current-fragment' );
-
-						// Announce the fragments one by one to the Screen Reader
-						dom.statusDiv.textContent = getStatusText( element );
-
-						if( i === index ) {
-							element.classList.add( 'current-fragment' );
-							startEmbeddedContent( element );
-						}
-					}
-					// Hidden fragments
-					else {
-						if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element );
-						element.classList.remove( 'visible' );
-						element.classList.remove( 'current-fragment' );
-					}
-
-				} );
-
-				if( fragmentsHidden.length ) {
-					dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } );
+				var changedFragments = updateFragments( index, fragments );
+
+				if( changedFragments.hidden.length ) {
+					dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
 				}
 
-				if( fragmentsShown.length ) {
-					dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } );
+				if( changedFragments.shown.length ) {
+					dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
 				}
 
 				updateControls();
 				updateProgress();
+
 				if( config.fragmentInURL ) {
 					writeURL();
 				}
 
-				return !!( fragmentsShown.length || fragmentsHidden.length );
+				return !!( changedFragments.shown.length || changedFragments.hidden.length );
 
 			}
 
@@ -4527,12 +4918,12 @@
 		// Reverse for RTL
 		if( config.rtl ) {
 			if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) {
-				slide( indexh + 1 );
+				slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
 			}
 		}
 		// Normal navigation
 		else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) {
-			slide( indexh - 1 );
+			slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
 		}
 
 	}
@@ -4544,12 +4935,12 @@
 		// Reverse for RTL
 		if( config.rtl ) {
 			if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) {
-				slide( indexh - 1 );
+				slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined );
 			}
 		}
 		// Normal navigation
 		else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) {
-			slide( indexh + 1 );
+			slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined );
 		}
 
 	}
@@ -4676,6 +5067,22 @@
 	}
 
 	/**
+	 * Called whenever there is mouse input at the document level
+	 * to determine if the cursor is active or not.
+	 *
+	 * @param {object} event
+	 */
+	function onDocumentCursorActive( event ) {
+
+		showCursor();
+
+		clearTimeout( cursorInactiveTimeout );
+
+		cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime );
+
+	}
+
+	/**
 	 * Handler for the document level 'keypress' event.
 	 *
 	 * @param {object} event
@@ -4702,20 +5109,31 @@
 			return true;
 		}
 
+		// Shorthand
+		var keyCode = event.keyCode;
+
 		// Remember if auto-sliding was paused so we can toggle it
 		var autoSlideWasPaused = autoSlidePaused;
 
 		onUserInput( event );
 
-		// Check if there's a focused element that could be using
-		// the keyboard
+		// Is there a focused element that could be using the keyboard?
 		var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
 		var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
 		var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
 
+		// Whitelist specific modified + keycode combinations
+		var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
+		var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37;
+		var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39;
+
+		// Prevent all other events when a modifier is pressed
+		var unusedModifier = 	!prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
+								( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
+
 		// Disregard the event if there's a focused element or a
 		// keyboard modifier key is present
-		if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return;
+		if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
 
 		// While paused only allow resume keyboard events; 'b', 'v', '.'
 		var resumeKeyCodes = [66,86,190,191];
@@ -4730,7 +5148,7 @@
 			}
 		}
 
-		if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) {
+		if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
 			return false;
 		}
 
@@ -4742,7 +5160,7 @@
 			for( key in config.keyboard ) {
 
 				// Check if this binding matches the pressed key
-				if( parseInt( key, 10 ) === event.keyCode ) {
+				if( parseInt( key, 10 ) === keyCode ) {
 
 					var value = config.keyboard[ key ];
 
@@ -4769,7 +5187,7 @@
 			for( key in registeredKeyBindings ) {
 
 				// Check if this binding matches the pressed key
-				if( parseInt( key, 10 ) === event.keyCode ) {
+				if( parseInt( key, 10 ) === keyCode ) {
 
 					var action = registeredKeyBindings[ key ].callback;
 
@@ -4793,35 +5211,92 @@
 			// Assume true and try to prove false
 			triggered = true;
 
-			switch( event.keyCode ) {
-				// p, page up
-				case 80: case 33: navigatePrev(); break;
-				// n, page down
-				case 78: case 34: navigateNext(); break;
-				// h, left
-				case 72: case 37: navigateLeft(); break;
-				// l, right
-				case 76: case 39: navigateRight(); break;
-				// k, up
-				case 75: case 38: navigateUp(); break;
-				// j, down
-				case 74: case 40: navigateDown(); break;
-				// home
-				case 36: slide( 0 ); break;
-				// end
-				case 35: slide( Number.MAX_VALUE ); break;
-				// space
-				case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break;
-				// return
-				case 13: isOverview() ? deactivateOverview() : triggered = false; break;
-				// two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button
-				case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break;
-				// f
-				case 70: enterFullscreen(); break;
-				// a
-				case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break;
-				default:
-					triggered = false;
+			// P, PAGE UP
+			if( keyCode === 80 || keyCode === 33 ) {
+				navigatePrev();
+			}
+			// N, PAGE DOWN
+			else if( keyCode === 78 || keyCode === 34 ) {
+				navigateNext();
+			}
+			// H, LEFT
+			else if( keyCode === 72 || keyCode === 37 ) {
+				if( firstSlideShortcut ) {
+					slide( 0 );
+				}
+				else if( !isOverview() && config.navigationMode === 'linear' ) {
+					navigatePrev();
+				}
+				else {
+					navigateLeft();
+				}
+			}
+			// L, RIGHT
+			else if( keyCode === 76 || keyCode === 39 ) {
+				if( lastSlideShortcut ) {
+					slide( Number.MAX_VALUE );
+				}
+				else if( !isOverview() && config.navigationMode === 'linear' ) {
+					navigateNext();
+				}
+				else {
+					navigateRight();
+				}
+			}
+			// K, UP
+			else if( keyCode === 75 || keyCode === 38 ) {
+				if( !isOverview() && config.navigationMode === 'linear' ) {
+					navigatePrev();
+				}
+				else {
+					navigateUp();
+				}
+			}
+			// J, DOWN
+			else if( keyCode === 74 || keyCode === 40 ) {
+				if( !isOverview() && config.navigationMode === 'linear' ) {
+					navigateNext();
+				}
+				else {
+					navigateDown();
+				}
+			}
+			// HOME
+			else if( keyCode === 36 ) {
+				slide( 0 );
+			}
+			// END
+			else if( keyCode === 35 ) {
+				slide( Number.MAX_VALUE );
+			}
+			// SPACE
+			else if( keyCode === 32 ) {
+				if( isOverview() ) {
+					deactivateOverview();
+				}
+				if( event.shiftKey ) {
+					navigatePrev();
+				}
+				else {
+					navigateNext();
+				}
+			}
+			// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
+			else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
+				togglePause();
+			}
+			// F
+			else if( keyCode === 70 ) {
+				enterFullscreen();
+			}
+			// A
+			else if( keyCode === 65 ) {
+				if ( config.autoSlideStoppable ) {
+					toggleAutoSlide( autoSlideWasPaused );
+				}
+			}
+			else {
+				triggered = false;
 			}
 
 		}
@@ -4832,7 +5307,7 @@
 			event.preventDefault && event.preventDefault();
 		}
 		// ESC or O key
-		else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) {
+		else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) {
 			if( dom.overlay ) {
 				closeOverlay();
 			}
@@ -4863,18 +5338,6 @@
 		touch.startY = event.touches[0].clientY;
 		touch.startCount = event.touches.length;
 
-		// If there's two touches we need to memorize the distance
-		// between those two points to detect pinching
-		if( event.touches.length === 2 && config.overview ) {
-			touch.startSpan = distanceBetween( {
-				x: event.touches[1].clientX,
-				y: event.touches[1].clientY
-			}, {
-				x: touch.startX,
-				y: touch.startY
-			} );
-		}
-
 	}
 
 	/**
@@ -4893,37 +5356,8 @@
 			var currentX = event.touches[0].clientX;
 			var currentY = event.touches[0].clientY;
 
-			// If the touch started with two points and still has
-			// two active touches; test for the pinch gesture
-			if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) {
-
-				// The current distance in pixels between the two touch points
-				var currentSpan = distanceBetween( {
-					x: event.touches[1].clientX,
-					y: event.touches[1].clientY
-				}, {
-					x: touch.startX,
-					y: touch.startY
-				} );
-
-				// If the span is larger than the desire amount we've got
-				// ourselves a pinch
-				if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) {
-					touch.captured = true;
-
-					if( currentSpan < touch.startSpan ) {
-						activateOverview();
-					}
-					else {
-						deactivateOverview();
-					}
-				}
-
-				event.preventDefault();
-
-			}
 			// There was only one touch point, look for a swipe
-			else if( event.touches.length === 1 && touch.startCount !== 2 ) {
+			if( event.touches.length === 1 && touch.startCount !== 2 ) {
 
 				var deltaX = currentX - touch.startX,
 					deltaY = currentY - touch.startY;
@@ -5073,8 +5507,8 @@
 	/**
 	 * Event handler for navigation control buttons.
 	 */
-	function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); }
-	function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); }
+	function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
+	function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
 	function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); }
 	function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); }
 	function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); }
@@ -5464,6 +5898,10 @@
 		// Returns an Array of all slides
 		getSlides: getSlides,
 
+		// Returns an Array of objects representing the attributes on
+		// the slides
+		getSlidesAttributes: getSlidesAttributes,
+
 		// Returns the total number of slides
 		getTotalSlides: getTotalSlides,
 
@@ -5514,6 +5952,16 @@
 			return query;
 		},
 
+		// Returns the top-level DOM element
+		getRevealElement: function() {
+			return dom.wrapper || document.querySelector( '.reveal' );
+		},
+
+		// Returns a hash with all registered plugins
+		getPlugins: function() {
+			return plugins;
+		},
+
 		// Returns true if we're currently on the first slide
 		isFirstSlide: function() {
 			return ( indexh === 0 && indexv === 0 );
@@ -5555,22 +6003,25 @@
 		// Forward event binding to the reveal DOM element
 		addEventListener: function( type, listener, useCapture ) {
 			if( 'addEventListener' in window ) {
-				( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture );
+				Reveal.getRevealElement().addEventListener( type, listener, useCapture );
 			}
 		},
 		removeEventListener: function( type, listener, useCapture ) {
 			if( 'addEventListener' in window ) {
-				( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture );
+				Reveal.getRevealElement().removeEventListener( type, listener, useCapture );
 			}
 		},
 
-		// Adds a custom key binding
+		// Adds/removes a custom key binding
 		addKeyBinding: addKeyBinding,
-
-		// Removes a custom key binding
 		removeKeyBinding: removeKeyBinding,
 
-		// Programatically triggers a keyboard event
+		// API for registering and retrieving plugins
+		registerPlugin: registerPlugin,
+		hasPlugin: hasPlugin,
+		getPlugin: getPlugin,
+
+		// Programmatically triggers a keyboard event
 		triggerKey: function( keyCode ) {
 			onDocumentKeyDown( { keyCode: keyCode } );
 		},


Follow ups