blob: 88f57bfb99189440e171c5ab9acd3dbc12b0681b [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { loadScript } from '../utils/loader.js'
2
3/**
4 * Manages loading and registering of reveal.js plugins.
5 */
6export 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
Marc Kupietz09b75752023-10-07 09:32:19 +020015 // An id:instance map of currently registered plugins
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020016 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 *
Marc Kupietz09b75752023-10-07 09:32:19 +0200174 * reveal.js waits for all registered plugins to initialize
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200175 * 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
Marc Kupietz09b75752023-10-07 09:32:19 +0200241 destroy() {
242
243 Object.values( this.registeredPlugins ).forEach( plugin => {
244 if( typeof plugin.destroy === 'function' ) {
245 plugin.destroy();
246 }
247 } );
248
249 this.registeredPlugins = {};
250 this.asyncDependencies = [];
251
252 }
253
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200254}