| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 1 | import { loadScript } from '../utils/loader.js' | 
|  | 2 |  | 
|  | 3 | /** | 
|  | 4 | * Manages loading and registering of reveal.js plugins. | 
|  | 5 | */ | 
|  | 6 | export default class Plugins { | 
|  | 7 |  | 
|  | 8 | constructor( reveal ) { | 
|  | 9 |  | 
|  | 10 | this.Reveal = reveal; | 
|  | 11 |  | 
|  | 12 | // Flags our current state (idle -> loading -> loaded) | 
|  | 13 | this.state = 'idle'; | 
|  | 14 |  | 
|  | 15 | // An id:instance map of currently registed plugins | 
|  | 16 | this.registeredPlugins = {}; | 
|  | 17 |  | 
|  | 18 | this.asyncDependencies = []; | 
|  | 19 |  | 
|  | 20 | } | 
|  | 21 |  | 
|  | 22 | /** | 
|  | 23 | * Loads reveal.js dependencies, registers and | 
|  | 24 | * initializes plugins. | 
|  | 25 | * | 
|  | 26 | * Plugins are direct references to a reveal.js plugin | 
|  | 27 | * object that we register and initialize after any | 
|  | 28 | * synchronous dependencies have loaded. | 
|  | 29 | * | 
|  | 30 | * Dependencies are defined via the 'dependencies' config | 
|  | 31 | * option and will be loaded prior to starting reveal.js. | 
|  | 32 | * Some dependencies may have an 'async' flag, if so they | 
|  | 33 | * will load after reveal.js has been started up. | 
|  | 34 | */ | 
|  | 35 | load( plugins, dependencies ) { | 
|  | 36 |  | 
|  | 37 | this.state = 'loading'; | 
|  | 38 |  | 
|  | 39 | plugins.forEach( this.registerPlugin.bind( this ) ); | 
|  | 40 |  | 
|  | 41 | return new Promise( resolve => { | 
|  | 42 |  | 
|  | 43 | let scripts = [], | 
|  | 44 | scriptsToLoad = 0; | 
|  | 45 |  | 
|  | 46 | dependencies.forEach( s => { | 
|  | 47 | // Load if there's no condition or the condition is truthy | 
|  | 48 | if( !s.condition || s.condition() ) { | 
|  | 49 | if( s.async ) { | 
|  | 50 | this.asyncDependencies.push( s ); | 
|  | 51 | } | 
|  | 52 | else { | 
|  | 53 | scripts.push( s ); | 
|  | 54 | } | 
|  | 55 | } | 
|  | 56 | } ); | 
|  | 57 |  | 
|  | 58 | if( scripts.length ) { | 
|  | 59 | scriptsToLoad = scripts.length; | 
|  | 60 |  | 
|  | 61 | const scriptLoadedCallback = (s) => { | 
|  | 62 | if( s && typeof s.callback === 'function' ) s.callback(); | 
|  | 63 |  | 
|  | 64 | if( --scriptsToLoad === 0 ) { | 
|  | 65 | this.initPlugins().then( resolve ); | 
|  | 66 | } | 
|  | 67 | }; | 
|  | 68 |  | 
|  | 69 | // Load synchronous scripts | 
|  | 70 | scripts.forEach( s => { | 
|  | 71 | if( typeof s.id === 'string' ) { | 
|  | 72 | this.registerPlugin( s ); | 
|  | 73 | scriptLoadedCallback( s ); | 
|  | 74 | } | 
|  | 75 | else if( typeof s.src === 'string' ) { | 
|  | 76 | loadScript( s.src, () => scriptLoadedCallback(s) ); | 
|  | 77 | } | 
|  | 78 | else { | 
|  | 79 | console.warn( 'Unrecognized plugin format', s ); | 
|  | 80 | scriptLoadedCallback(); | 
|  | 81 | } | 
|  | 82 | } ); | 
|  | 83 | } | 
|  | 84 | else { | 
|  | 85 | this.initPlugins().then( resolve ); | 
|  | 86 | } | 
|  | 87 |  | 
|  | 88 | } ); | 
|  | 89 |  | 
|  | 90 | } | 
|  | 91 |  | 
|  | 92 | /** | 
|  | 93 | * Initializes our plugins and waits for them to be ready | 
|  | 94 | * before proceeding. | 
|  | 95 | */ | 
|  | 96 | initPlugins() { | 
|  | 97 |  | 
|  | 98 | return new Promise( resolve => { | 
|  | 99 |  | 
|  | 100 | let pluginValues = Object.values( this.registeredPlugins ); | 
|  | 101 | let pluginsToInitialize = pluginValues.length; | 
|  | 102 |  | 
|  | 103 | // If there are no plugins, skip this step | 
|  | 104 | if( pluginsToInitialize === 0 ) { | 
|  | 105 | this.loadAsync().then( resolve ); | 
|  | 106 | } | 
|  | 107 | // ... otherwise initialize plugins | 
|  | 108 | else { | 
|  | 109 |  | 
|  | 110 | let initNextPlugin; | 
|  | 111 |  | 
|  | 112 | let afterPlugInitialized = () => { | 
|  | 113 | if( --pluginsToInitialize === 0 ) { | 
|  | 114 | this.loadAsync().then( resolve ); | 
|  | 115 | } | 
|  | 116 | else { | 
|  | 117 | initNextPlugin(); | 
|  | 118 | } | 
|  | 119 | }; | 
|  | 120 |  | 
|  | 121 | let i = 0; | 
|  | 122 |  | 
|  | 123 | // Initialize plugins serially | 
|  | 124 | initNextPlugin = () => { | 
|  | 125 |  | 
|  | 126 | let plugin = pluginValues[i++]; | 
|  | 127 |  | 
|  | 128 | // If the plugin has an 'init' method, invoke it | 
|  | 129 | if( typeof plugin.init === 'function' ) { | 
|  | 130 | let promise = plugin.init( this.Reveal ); | 
|  | 131 |  | 
|  | 132 | // If the plugin returned a Promise, wait for it | 
|  | 133 | if( promise && typeof promise.then === 'function' ) { | 
|  | 134 | promise.then( afterPlugInitialized ); | 
|  | 135 | } | 
|  | 136 | else { | 
|  | 137 | afterPlugInitialized(); | 
|  | 138 | } | 
|  | 139 | } | 
|  | 140 | else { | 
|  | 141 | afterPlugInitialized(); | 
|  | 142 | } | 
|  | 143 |  | 
|  | 144 | } | 
|  | 145 |  | 
|  | 146 | initNextPlugin(); | 
|  | 147 |  | 
|  | 148 | } | 
|  | 149 |  | 
|  | 150 | } ) | 
|  | 151 |  | 
|  | 152 | } | 
|  | 153 |  | 
|  | 154 | /** | 
|  | 155 | * Loads all async reveal.js dependencies. | 
|  | 156 | */ | 
|  | 157 | loadAsync() { | 
|  | 158 |  | 
|  | 159 | this.state = 'loaded'; | 
|  | 160 |  | 
|  | 161 | if( this.asyncDependencies.length ) { | 
|  | 162 | this.asyncDependencies.forEach( s => { | 
|  | 163 | loadScript( s.src, s.callback ); | 
|  | 164 | } ); | 
|  | 165 | } | 
|  | 166 |  | 
|  | 167 | return Promise.resolve(); | 
|  | 168 |  | 
|  | 169 | } | 
|  | 170 |  | 
|  | 171 | /** | 
|  | 172 | * Registers a new plugin with this reveal.js instance. | 
|  | 173 | * | 
|  | 174 | * reveal.js waits for all regisered plugins to initialize | 
|  | 175 | * before considering itself ready, as long as the plugin | 
|  | 176 | * is registered before calling `Reveal.initialize()`. | 
|  | 177 | */ | 
|  | 178 | registerPlugin( plugin ) { | 
|  | 179 |  | 
|  | 180 | // Backwards compatibility to make reveal.js ~3.9.0 | 
|  | 181 | // plugins work with reveal.js 4.0.0 | 
|  | 182 | if( arguments.length === 2 && typeof arguments[0] === 'string' ) { | 
|  | 183 | plugin = arguments[1]; | 
|  | 184 | plugin.id = arguments[0]; | 
|  | 185 | } | 
|  | 186 | // Plugin can optionally be a function which we call | 
|  | 187 | // to create an instance of the plugin | 
|  | 188 | else if( typeof plugin === 'function' ) { | 
|  | 189 | plugin = plugin(); | 
|  | 190 | } | 
|  | 191 |  | 
|  | 192 | let id = plugin.id; | 
|  | 193 |  | 
|  | 194 | if( typeof id !== 'string' ) { | 
|  | 195 | console.warn( 'Unrecognized plugin format; can\'t find plugin.id', plugin ); | 
|  | 196 | } | 
|  | 197 | else if( this.registeredPlugins[id] === undefined ) { | 
|  | 198 | this.registeredPlugins[id] = plugin; | 
|  | 199 |  | 
|  | 200 | // If a plugin is registered after reveal.js is loaded, | 
|  | 201 | // initialize it right away | 
|  | 202 | if( this.state === 'loaded' && typeof plugin.init === 'function' ) { | 
|  | 203 | plugin.init( this.Reveal ); | 
|  | 204 | } | 
|  | 205 | } | 
|  | 206 | else { | 
|  | 207 | console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' ); | 
|  | 208 | } | 
|  | 209 |  | 
|  | 210 | } | 
|  | 211 |  | 
|  | 212 | /** | 
|  | 213 | * Checks if a specific plugin has been registered. | 
|  | 214 | * | 
|  | 215 | * @param {String} id Unique plugin identifier | 
|  | 216 | */ | 
|  | 217 | hasPlugin( id ) { | 
|  | 218 |  | 
|  | 219 | return !!this.registeredPlugins[id]; | 
|  | 220 |  | 
|  | 221 | } | 
|  | 222 |  | 
|  | 223 | /** | 
|  | 224 | * Returns the specific plugin instance, if a plugin | 
|  | 225 | * with the given ID has been registered. | 
|  | 226 | * | 
|  | 227 | * @param {String} id Unique plugin identifier | 
|  | 228 | */ | 
|  | 229 | getPlugin( id ) { | 
|  | 230 |  | 
|  | 231 | return this.registeredPlugins[id]; | 
|  | 232 |  | 
|  | 233 | } | 
|  | 234 |  | 
|  | 235 | getRegisteredPlugins() { | 
|  | 236 |  | 
|  | 237 | return this.registeredPlugins; | 
|  | 238 |  | 
|  | 239 | } | 
|  | 240 |  | 
|  | 241 | } |