From 4862de26eb75d44b36849ef574db986203d3c879 Mon Sep 17 00:00:00 2001 From: Hakim El Hattab Date: Fri, 1 Mar 2019 21:28:52 +0100 Subject: [PATCH] async loading of external markdown, add Reveal.registerPlugin() --- js/reveal.js | 104 ++++++++++++++------ plugin/markdown/markdown.js | 191 +++++++++++++++++++++--------------- 2 files changed, 188 insertions(+), 107 deletions(-) diff --git a/js/reveal.js b/js/reveal.js index a17a33a..9b29f78 100644 --- a/js/reveal.js +++ b/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,21 +518,66 @@ 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. * @@ -1512,6 +1548,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 } ); diff --git a/plugin/markdown/markdown.js b/plugin/markdown/markdown.js index 31029ae..181116d 100755 --- a/plugin/markdown/markdown.js +++ b/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,58 +201,11 @@ */ function processSlides() { - var sections = document.querySelectorAll( '[data-markdown]'), - section; - - for( var i = 0, len = sections.length; i < len; i++ ) { - - section = sections[i]; + [].slice.call( document.querySelectorAll( '[data-markdown]') ).forEach( function( section, i ) { if( section.getAttribute( 'data-markdown' ).length ) { - 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() { - 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 ) - }); - - } - else { - - section.outerHTML = '
' + - 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + - 'Check your browser\'s JavaScript console for more details.' + - '

Remember that you need to serve the presentation HTML from a HTTP server.

' + - '
'; - - } - } - }; - - xhr.open( 'GET', url, false ); - - 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 ); - } + loadExternalMarkdown( section ); } else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) { @@ -266,6 +221,65 @@ else { section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) ); } + + }); + + checkIfLoaded(); + + } + + function loadExternalMarkdown( section ) { + + markdownFilesToLoad += 1; + + 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 ) { + + 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 ) + }); + + } + else { + + section.outerHTML = '
' + + 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + + 'Check your browser\'s JavaScript console for more details.' + + '

Remember that you need to serve the presentation HTML from a HTTP server.

' + + '
'; + + } + + convertSlides(); + + markdownFilesToLoad -= 1; + + checkIfLoaded(); + } + }.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 ) - - var notes = section.querySelector( 'aside.notes' ); - var markdown = getMarkdownFromSlide( section ); - - 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 ); - } + 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; + }));