Browse Source

async loading of external markdown, add Reveal.registerPlugin()

Hakim El Hattab 5 years ago
parent
commit
4862de26eb
2 changed files with 185 additions and 104 deletions
  1. 75 29
      js/reveal.js
  2. 110 75
      plugin/markdown/markdown.js

+ 75 - 29
js/reveal.js

@@ -319,6 +319,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 = {},
 
@@ -434,7 +440,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();
 
 	}
@@ -489,37 +495,22 @@
 	function load() {
 
 		var scripts = [],
-			scriptsAsync = [],
-			scriptsToPreload = 0;
-
-		// Called once synchronous scripts finish loading
-		function afterSynchronousScriptsLoaded() {
-			// Load asynchronous scripts
-			if( scriptsAsync.length ) {
-				scriptsAsync.forEach( function( s ) {
-					loadScript( s.src, s.callback );
-				} );
-			}
-
-			start();
-		}
-
-		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 );
+					asyncDependencies.push( s );
 				}
 				else {
 					scripts.push( s );
 				}
 			}
-		}
+		} );
 
 		if( scripts.length ) {
-			scriptsToPreload = scripts.length;
+			scriptsToLoad = scripts.length;
 
 			// Load synchronous scripts
 			scripts.forEach( function( s ) {
@@ -527,22 +518,67 @@
 
 					if( typeof s.callback === 'function' ) s.callback();
 
-					if( --scriptsToPreload === 0 ) {
-
-						afterSynchronousScriptsLoaded();
-
+					if( --scriptsToLoad === 0 ) {
+						loadPlugins();
 					}
 
 				} );
 			} );
 		}
 		else {
-			afterSynchronousScriptsLoaded();
+			loadPlugins();
+		}
+
+	}
+
+	/**
+	 * Loads all plugins that require preloading.
+	 */
+	function loadPlugins() {
+
+		var pluginsToLoad = Object.keys( plugins ).length;
+
+		for( var i in plugins ) {
+
+			var plugin = plugins[i];
+
+			// If the plugin has an 'init' method, initialize and
+			// wait for the callback
+			if( typeof plugin.init === 'function' ) {
+				plugin.init( function() {
+					if( --pluginsToLoad === 0 ) {
+						loadAsyncDependencies();
+					}
+				} );
+			}
+			else {
+				pluginsToLoad -= 1;
+			}
+
+		}
+
+		if( pluginsToLoad === 0 ) {
+			loadAsyncDependencies();
 		}
 
 	}
 
 	/**
+	 * 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
@@ -1513,6 +1549,15 @@
 	}
 
 	/**
+	 * Registers a new plugin with this reveal.js instance.
+	 */
+	function registerPlugin( id, plugin ) {
+
+		plugins[id] = plugin;
+
+	}
+
+	/**
 	 * Add a custom key binding with optional description to
 	 * be added to the help screen.
 	 */
@@ -5845,12 +5890,13 @@
 			}
 		},
 
-		// Adds a custom key binding
+		// Adds/remvoes a custom key binding
 		addKeyBinding: addKeyBinding,
-
-		// Removes a custom key binding
 		removeKeyBinding: removeKeyBinding,
 
+		// Called by plugins to register/unregister themselves
+		registerPlugin: registerPlugin,
+
 		// Programatically triggers a keyboard event
 		triggerKey: function( keyCode ) {
 			onDocumentKeyDown( { keyCode: keyCode } );

+ 110 - 75
plugin/markdown/markdown.js

@@ -7,13 +7,11 @@
 	if (typeof define === 'function' && define.amd) {
 		root.marked = require( './marked' );
 		root.RevealMarkdown = factory( root.marked );
-		root.RevealMarkdown.initialize();
 	} else if( typeof exports === 'object' ) {
 		module.exports = factory( require( './marked' ) );
 	} else {
 		// Browser globals (root is window)
 		root.RevealMarkdown = factory( root.marked );
-		root.RevealMarkdown.initialize();
 	}
 }( this, function( marked ) {
 
@@ -24,6 +22,10 @@
 
 	var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
 
+	var markdownFilesToLoad = 0;
+
+	var loadCallback;
+
 
 	/**
 	 * Retrieves the markdown contents of a slide section
@@ -199,73 +201,85 @@
 	 */
 	function processSlides() {
 
-		var sections = document.querySelectorAll( '[data-markdown]'),
-			section;
+		[].slice.call( document.querySelectorAll( '[data-markdown]') ).forEach( function( section, i ) {
 
-		for( var i = 0, len = sections.length; i < len; i++ ) {
+			if( section.getAttribute( 'data-markdown' ).length ) {
 
-			section = sections[i];
+				loadExternalMarkdown( section );
 
-			if( section.getAttribute( 'data-markdown' ).length ) {
+			}
+			else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) {
+
+				section.outerHTML = slidify( getMarkdownFromSlide( section ), {
+					separator: section.getAttribute( 'data-separator' ),
+					verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
+					notesSeparator: section.getAttribute( 'data-separator-notes' ),
+					attributes: getForwardedAttributes( section )
+				});
 
-				var xhr = new XMLHttpRequest(),
-					url = section.getAttribute( 'data-markdown' );
+			}
+			else {
+				section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) );
+			}
 
-				datacharset = section.getAttribute( 'data-charset' );
+		});
 
-				// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
-				if( datacharset != null && datacharset != '' ) {
-					xhr.overrideMimeType( 'text/html; charset=' + datacharset );
-				}
+		checkIfLoaded();
 
-				xhr.onreadystatechange = function() {
-					if( xhr.readyState === 4 ) {
-						// file protocol yields status code 0 (useful for local debug, mobile applications etc.)
-						if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
+	}
 
-							section.outerHTML = slidify( xhr.responseText, {
-								separator: section.getAttribute( 'data-separator' ),
-								verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-								notesSeparator: section.getAttribute( 'data-separator-notes' ),
-								attributes: getForwardedAttributes( section )
-							});
+	function loadExternalMarkdown( section ) {
 
-						}
-						else {
+		markdownFilesToLoad += 1;
 
-							section.outerHTML = '<section data-state="alert">' +
-								'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
-								'Check your browser\'s JavaScript console for more details.' +
-								'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
-								'</section>';
+		var xhr = new XMLHttpRequest(),
+			url = section.getAttribute( 'data-markdown' );
 
-						}
-					}
-				};
+		datacharset = section.getAttribute( 'data-charset' );
+
+		// see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
+		if( datacharset != null && datacharset != '' ) {
+			xhr.overrideMimeType( 'text/html; charset=' + datacharset );
+		}
+
+		xhr.onreadystatechange = function( section, xhr ) {
+			if( xhr.readyState === 4 ) {
+				// file protocol yields status code 0 (useful for local debug, mobile applications etc.)
+				if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
 
-				xhr.open( 'GET', url, false );
+					section.outerHTML = slidify( xhr.responseText, {
+						separator: section.getAttribute( 'data-separator' ),
+						verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
+						notesSeparator: section.getAttribute( 'data-separator-notes' ),
+						attributes: getForwardedAttributes( section )
+					});
 
-				try {
-					xhr.send();
 				}
-				catch ( e ) {
-					alert( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
+				else {
+
+					section.outerHTML = '<section data-state="alert">' +
+						'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
+						'Check your browser\'s JavaScript console for more details.' +
+						'<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
+						'</section>';
+
 				}
 
-			}
-			else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) {
+				convertSlides();
 
-				section.outerHTML = slidify( getMarkdownFromSlide( section ), {
-					separator: section.getAttribute( 'data-separator' ),
-					verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-					notesSeparator: section.getAttribute( 'data-separator-notes' ),
-					attributes: getForwardedAttributes( section )
-				});
+				markdownFilesToLoad -= 1;
 
+				checkIfLoaded();
 			}
-			else {
-				section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) );
-			}
+		}.bind( this, section, xhr );
+
+		xhr.open( 'GET', url, true );
+
+		try {
+			xhr.send();
+		}
+		catch ( e ) {
+			alert( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
 		}
 
 	}
@@ -342,44 +356,56 @@
 	 */
 	function convertSlides() {
 
-		var sections = document.querySelectorAll( '[data-markdown]');
+		var sections = document.querySelectorAll( '[data-markdown]:not([data-markdown-parsed])');
 
-		for( var i = 0, len = sections.length; i < len; i++ ) {
+		[].slice.call( sections ).forEach( function( section ) {
 
-			var section = sections[i];
+			section.setAttribute( 'data-markdown-parsed', true )
 
-			// Only parse the same slide once
-			if( !section.getAttribute( 'data-markdown-parsed' ) ) {
+			var notes = section.querySelector( 'aside.notes' );
+			var markdown = getMarkdownFromSlide( section );
 
-				section.setAttribute( 'data-markdown-parsed', true )
+			section.innerHTML = marked( markdown );
+			addAttributes( 	section, section, null, section.getAttribute( 'data-element-attributes' ) ||
+							section.parentNode.getAttribute( 'data-element-attributes' ) ||
+							DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
+							section.getAttribute( 'data-attributes' ) ||
+							section.parentNode.getAttribute( 'data-attributes' ) ||
+							DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
 
-				var notes = section.querySelector( 'aside.notes' );
-				var markdown = getMarkdownFromSlide( section );
+			// If there were notes, we need to re-add them after
+			// having overwritten the section's HTML
+			if( notes ) {
+				section.appendChild( notes );
+			}
 
-				section.innerHTML = marked( markdown );
-				addAttributes( 	section, section, null, section.getAttribute( 'data-element-attributes' ) ||
-								section.parentNode.getAttribute( 'data-element-attributes' ) ||
-								DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-								section.getAttribute( 'data-attributes' ) ||
-								section.parentNode.getAttribute( 'data-attributes' ) ||
-								DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
+		} );
 
-				// If there were notes, we need to re-add them after
-				// having overwritten the section's HTML
-				if( notes ) {
-					section.appendChild( notes );
-				}
+	}
 
-			}
+	function checkIfLoaded() {
 
+		if( markdownFilesToLoad === 0 ) {
+			if( loadCallback ) {
+				loadCallback();
+				loadCallback = null;
+			}
 		}
 
 	}
 
 	// API
-	return {
+	var RevealMarkdown = {
+
+		/**
+		 * Starts processing and converting Markdown within the
+		 * current reveal.js deck.
+		 *
+		 * @param {function} callback function to invoke once
+		 * we've finished loading and parsing Markdown
+		 */
+		init: function( callback ) {
 
-		initialize: function() {
 			if( typeof marked === 'undefined' ) {
 				throw 'The reveal.js Markdown plugin requires marked to be loaded';
 			}
@@ -392,14 +418,17 @@
 				});
 			}
 
+			// marked can be configured via reveal.js config options
 			var options = Reveal.getConfig().markdown;
-
-			if ( options ) {
+			if( options ) {
 				marked.setOptions( options );
 			}
 
+			loadCallback = callback;
+
 			processSlides();
 			convertSlides();
+
 		},
 
 		// TODO: Do these belong in the API?
@@ -409,4 +438,10 @@
 
 	};
 
+	// Register our plugin so that reveal.js will call our
+	// plugin 'init' method as part of the initialization
+	Reveal.registerPlugin( 'markdown', RevealMarkdown );
+
+	return RevealMarkdown;
+
 }));