1 | /** |
||
2 | * [Siteapp] - multi-purpose frontend application |
||
3 | * |
||
4 | * Siteapp (Data) Module Manager |
||
5 | * |
||
6 | * abstraction of plugin/module core, inspired by Zurb Foundation's core |
||
7 | * |
||
8 | * @package [Siteapp] |
||
9 | * @subpackage [Siteapp] module |
||
10 | * @author Björn Bartels <[email protected]> |
||
11 | * @link https://gitlab.bjoernbartels.earth/groups/themes |
||
12 | * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 |
||
13 | * @copyright copyright (c) 2016 Björn Bartels <[email protected]> |
||
14 | * |
||
15 | * @namespace Siteapp |
||
16 | * @module Siteapp.ModuleManager |
||
17 | */ |
||
18 | /** global: Siteapp */ |
||
19 | /** global: Siteapp_ModuleManager_DEFAULTS */ |
||
20 | /** global: Siteapp_ModuleManager_VERSION */ |
||
21 | |||
22 | "use strict"; |
||
23 | |||
24 | import {Exception} from '../sys/siteapp.exception'; |
||
25 | import Module from '../module/siteapp.module'; |
||
26 | |||
27 | const Siteapp_ModuleManager_VERSION = '0.0.1'; |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
28 | |||
29 | const Siteapp_ModuleManager_DEFAULTS = { |
||
0 ignored issues
–
show
|
|||
30 | |||
31 | /** |
||
32 | * use namespaced module trigger 'data-' attribute |
||
33 | * true => data-{namespace}-{modulename} (default) |
||
34 | * false => data-{modulename} |
||
35 | * |
||
36 | * @var {booloean} namespacedModuleTriggers |
||
37 | * @default true |
||
38 | */ |
||
39 | namespacedModuleTriggers: true, |
||
40 | |||
41 | /** |
||
42 | * Wether to override already registered modules ('true'). |
||
43 | * If set to 'false', throws a warning on occurance. |
||
44 | * The new module then will not be registered. |
||
45 | * |
||
46 | * @var {booloean} overrideRegistered |
||
47 | * @default false |
||
48 | */ |
||
49 | overrideRegistered: false, |
||
50 | |||
51 | /** |
||
52 | * Since registered plugins/modules are stored under |
||
53 | * [Siteapp].Modules.{PluginName}, this flag lets the |
||
54 | * module manager map them back to [Siteapp].{PluginName} |
||
55 | * for Foundation compatiblity when set to 'true'. |
||
56 | * ATTENTION: Only do that on one/the main module manager, |
||
57 | * because, in that scope, one manager would override |
||
58 | * the registered modules of another module manager if |
||
59 | * modules are assigned the same name! |
||
60 | * @var {booloean} mapModulesToApplication |
||
61 | * @default false |
||
62 | */ |
||
63 | mapModulesToApplication: false |
||
64 | |||
65 | }; |
||
66 | |||
67 | class ModuleManagerException extends Exception { |
||
68 | get name () { |
||
69 | return "SiteappModuleManagerException"; |
||
70 | } |
||
71 | }; |
||
72 | |||
73 | |||
74 | |||
75 | const ModuleManager = class ModuleManager { |
||
76 | |||
77 | /** |
||
78 | * Create a new instance of the module manager. |
||
79 | * @class |
||
80 | * @name ModuleManager |
||
81 | * @param {Object} options - Overrides to the default module settings. |
||
82 | */ |
||
83 | constructor (options) { |
||
84 | |||
85 | this._uuid = this.functionName(this) + '-' + this.genUUID( 6 ); |
||
86 | this.options = $.extend({}, Siteapp_ModuleManager_DEFAULTS, options); |
||
87 | |||
88 | this._init(); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Setup objects |
||
93 | */ |
||
94 | _init () { |
||
95 | this._version = Siteapp_ModuleManager_VERSION; |
||
96 | |||
97 | this._modules = {}; |
||
98 | this._uuids = []; |
||
99 | |||
100 | } |
||
101 | |||
102 | |||
103 | /** |
||
104 | * Initialize a module/plugin or it's factory. |
||
105 | * Creates a new module instance and attaches it to a corresponding element. |
||
106 | * |
||
107 | * @function |
||
108 | * @param {Module} _module - the module instance, usually 'this' when constructing/initiating the instance |
||
109 | * @param {string} name - optional: alternative extra name to set as 'data' attribute |
||
110 | * |
||
111 | */ |
||
112 | initialize ( _module, name ) { |
||
113 | //console.//log('initialize module...', _module); |
||
114 | |||
115 | if (_module instanceof Module) { |
||
0 ignored issues
–
show
There is no return statement if
_module instanceof Module is false . Are you sure this is correct? If so, consider adding return; explicitly.
This check looks for functions where a Consider this little piece of code function isBig(a) {
if (a > 5000) {
return "yes";
}
}
console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined
The function This behaviour may not be what you had intended. In any case, you can add a
![]() |
|||
116 | //console.//log('...is instance of Module...'); |
||
117 | return this.initializeModule(_module, name); |
||
118 | } |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * Register a module/plugin or it's factory to ModuleManager's namespace. |
||
123 | * Element triggering that module/plugin will be initialized on next 'reflow'. |
||
124 | * If no trigger name is given, the name of the module (constructor) is used. |
||
125 | * |
||
126 | * @function |
||
127 | * @param {Module} _module - the module to register |
||
128 | * @param {string} name - the trigger name, so it will reference the 'data' attribute [data-(namespace-)name] |
||
129 | * |
||
130 | */ |
||
131 | register ( _module, name ) { |
||
132 | //console.//log('register module...', typeof _module); |
||
133 | if (_module instanceof Object) { |
||
0 ignored issues
–
show
There is no return statement if
_module instanceof Object is false . Are you sure this is correct? If so, consider adding return; explicitly.
This check looks for functions where a Consider this little piece of code function isBig(a) {
if (a > 5000) {
return "yes";
}
}
console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined
The function This behaviour may not be what you had intended. In any case, you can add a
![]() |
|||
134 | //console.//log('...is instance of Object...'); |
||
135 | return this.registerModule(_module, name); |
||
136 | } |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Destroy a initialzed module/plugin. |
||
141 | * |
||
142 | * @function |
||
143 | * @param {Module} _module - the initialized module to destroy |
||
144 | * |
||
145 | */ |
||
146 | destroy ( _module ) { |
||
147 | //console.//log('destroy module...', _module); |
||
148 | var moduleName = _module; |
||
0 ignored issues
–
show
|
|||
149 | if (_module instanceof Module) { |
||
0 ignored issues
–
show
There is no return statement if
_module instanceof Module is false . Are you sure this is correct? If so, consider adding return; explicitly.
This check looks for functions where a Consider this little piece of code function isBig(a) {
if (a > 5000) {
return "yes";
}
}
console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined
The function This behaviour may not be what you had intended. In any case, you can add a
![]() |
|||
150 | //console.//log('...is instance of Module...'); |
||
151 | moduleName = this.hyphenate(this.functionName(_module.$element.data(this.application.appName+'Plugin').constructor)); |
||
152 | if (typeof this._modules[this.application.appName+'-'+moduleName] != 'undefined') { |
||
0 ignored issues
–
show
There is no return statement if
typeof this._modules.thi...uleName != "undefined" is false . Are you sure this is correct? If so, consider adding return; explicitly.
This check looks for functions where a Consider this little piece of code function isBig(a) {
if (a > 5000) {
return "yes";
}
}
console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined
The function This behaviour may not be what you had intended. In any case, you can add a
![]() |
|||
153 | return this.destroyModule(_module); |
||
154 | } |
||
155 | } |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * Defines a [Siteapp] module/plugin, adding it to the `Siteapp` namespace |
||
160 | * and the list of modules to initialize when reflowing. |
||
161 | * |
||
162 | * @function |
||
163 | * @param {Object} _module - The constructor of the module. |
||
164 | * @param {Object} name - The constructor of the module. |
||
165 | * |
||
166 | */ |
||
167 | registerModule (_module, name) { |
||
168 | // Object key to use when adding to registry |
||
169 | // Examples: Siteapp.Object1, Siteapp.Object2 |
||
170 | var className = (typeof name != 'undefined') ? name : this.functionName(_module); |
||
171 | // Object key to use when storing the module, also used to create the |
||
172 | // identifying data attribute for the module |
||
173 | // Examples: data-objecttriggername1, data-objecttriggername2 |
||
174 | var attrName = this.hyphenate(className); |
||
175 | |||
176 | _module._app = _module.prototype._app = this.application; |
||
177 | _module._manager = _module.prototype._manager = this; |
||
178 | |||
179 | var moduleName = attrName; |
||
180 | if (this.options.namespacedModuleTriggers) { |
||
181 | moduleName = this.application.appName+'-'+moduleName; |
||
182 | } |
||
183 | // Add to the modules list (for reflowing) |
||
184 | var mngrModule = this._modules[moduleName]; |
||
185 | if ( !this.options.overrideRegistered && (typeof mngrModule != 'undefined') ) { |
||
186 | console.warn(`Module registration: A Module with the name "${attrName}" has already been registered for this manager`); |
||
187 | return |
||
188 | } |
||
189 | this._modules[moduleName] = this[className] = _module; |
||
190 | |||
191 | // Add to the application object for Foundation compatiblity |
||
192 | if ( this.options.mapModulesToApplication ) { |
||
193 | var appModule = this.application._plugins[moduleName]; |
||
194 | if ( !this.options.overrideRegistered && (typeof appModule != 'undefined')) { |
||
195 | console.warn(`Module registration: A Module with the name "${attrName}" has already been mapped to the application object`); |
||
196 | } else { |
||
197 | this.application._plugins[moduleName] = this.application[className] = this._modules[moduleName]; |
||
198 | } |
||
199 | } |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Populates the _uuids array with pointers to each individual module instance. |
||
204 | * Adds the `siteappPlugin` data-attribute to programmatically created modules |
||
205 | * to allow use of $(selector).Siteapp(method) calls. |
||
206 | * Also fires the initialization event for each module, consolidating repeditive code. |
||
207 | * |
||
208 | * @function |
||
209 | * @param {Object} _module - an instance of a module, usually `this` in context. |
||
210 | * @param {String} name - the name of the module, passed as a camelCased string. |
||
211 | * @fires Module#init |
||
212 | */ |
||
213 | initializeModule (_module, name) { |
||
214 | var moduleName = (typeof name != 'undefined') ? this.hyphenate(name) : this.hyphenate(this.functionName(_module.constructor)); |
||
215 | var attrName = moduleName; |
||
216 | if (this.options.namespacedModuleTriggers) { |
||
217 | attrName = this.application.appName+'-'+attrName; |
||
218 | } |
||
219 | if (!_module.uuid) { |
||
220 | _module.uuid = moduleName + '-' + this.genUUID(6); |
||
221 | } |
||
222 | |||
223 | if(!_module.$element.attr('data-' + attrName)){ _module.$element.attr('data-' + attrName, _module.uuid); } |
||
224 | if(!_module.$element.data(this.application.appName+'Plugin')){ _module.$element.data(this.application.appName+'Plugin', _module); } |
||
225 | /** |
||
226 | * Fires when the module has initialized. |
||
227 | * @event Module#init |
||
228 | */ |
||
229 | _module.$element.trigger('init.'+this.application.appName+'.' + moduleName); |
||
230 | |||
231 | this._uuids.push(_module.uuid); |
||
232 | // Add UUID to the application registry for Foundation compatiblity |
||
233 | if ( this.options.mapModulesToApplication ) { |
||
234 | this.application._uuids.push(_module.uuid); |
||
235 | } |
||
236 | |||
237 | return; |
||
0 ignored issues
–
show
|
|||
238 | } |
||
239 | |||
240 | /** |
||
241 | * Removes the modules uuid from the _uuids array. |
||
242 | * Removes the siteappPlugin data attribute, as well as the data-module-name attribute. |
||
243 | * Also fires the destroyed event for the module, consolidating repeditive code. |
||
244 | * |
||
245 | * @function |
||
246 | * @param {Object} _module - an instance of a module, usually `this` in context. |
||
247 | * @fires Module#destroyed |
||
248 | */ |
||
249 | destroyModule (_module) { |
||
250 | var moduleName = this.hyphenate(this.functionName(_module.$element.data(this.application.appName+'Plugin').constructor)); |
||
251 | var attrName = moduleName; |
||
252 | if (this.options.namespacedModuleTriggers) { |
||
253 | attrName = this.application.appName+'-'+attrName; |
||
254 | } |
||
255 | |||
256 | this._uuids.splice(this._uuids.indexOf(_module.uuid), 1); |
||
257 | // remove from the application object |
||
258 | if ( this.options.mapModulesToApplication ) { |
||
259 | this.application._uuids.splice(this.application._uuids.indexOf(_module.uuid), 1); |
||
260 | } |
||
261 | |||
262 | _module.$element |
||
263 | .removeAttr('data-' + attrName) |
||
264 | .removeData(this.application.appName+'Plugin') |
||
265 | /** |
||
266 | * Fires when the module has been destroyed. |
||
267 | * @event Module#destroyed |
||
268 | */ |
||
269 | .trigger('destroyed.'+this.application.appName+'.' + moduleName); |
||
270 | |||
271 | for(var prop in _module){ |
||
0 ignored issues
–
show
A for in loop automatically includes the property of any prototype object, consider checking the key using
hasOwnProperty .
When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically: var someObject;
for (var key in someObject) {
if ( ! someObject.hasOwnProperty(key)) {
continue; // Skip keys from the prototype.
}
doSomethingWith(key);
}
![]() |
|||
272 | _module[prop] = null;//clean up script to prep for garbage collection. |
||
273 | } |
||
274 | |||
275 | return; |
||
0 ignored issues
–
show
|
|||
276 | } |
||
277 | |||
278 | /** |
||
279 | * Causes one or more active modules to re-initialize, resetting event listeners, |
||
280 | * recalculating positions, etc. |
||
281 | * |
||
282 | * @function |
||
283 | * @param {String} modules - optional string of an individual module key, |
||
284 | * attained by calling `$(element).data('moduleName')`, |
||
285 | * or string of a module class i.e. `'dropdown'` |
||
286 | * @default If no argument is passed, reflow all currently active modules. |
||
287 | */ |
||
288 | reInit (modules) { |
||
289 | //console.//log('module manager re-init:', this.functionName(this)); |
||
290 | var isJQ = modules instanceof $; |
||
291 | var $app = this, |
||
292 | _namespace = $app.application.appName |
||
293 | ; |
||
294 | |||
295 | try { |
||
296 | if (isJQ && (modules.length > 0)){ |
||
297 | modules.each(function(){ |
||
298 | $(this).data(_namespace+'Plugin')._init(); |
||
299 | }); |
||
300 | } else { |
||
301 | var type = typeof modules, |
||
302 | $this = this, |
||
303 | fns = { |
||
304 | 'object' : function (_modules) { |
||
305 | _modules.forEach(function (p) { |
||
306 | //console.//log('(re)init...:', _namespace, p, $('[data-'+ p +']'), ($('[data-'+ p +']'))[_namespace]); |
||
307 | $('[data-'+ p +']')[_namespace]('_init'); |
||
308 | }); |
||
309 | }, |
||
310 | 'string' : function () { |
||
311 | //console.//log('(re)init...:', _namespace, modules, $('[data-'+ modules +']'), ($('[data-'+ modules +']'))[_namespace]); |
||
312 | $('[data-'+ modules +']')[_namespace]('_init'); |
||
313 | }, |
||
314 | 'undefined' : function () { |
||
315 | //console.//log('(re)init...:', _namespace, '*all*', $('[data-'+ modules +']'), ($('[data-'+ modules +']'))[_namespace]); |
||
316 | this['object'](Object.keys($this._modules)); |
||
317 | } |
||
318 | } |
||
319 | ; |
||
320 | fns[type](modules); |
||
321 | } |
||
322 | } catch(err) { |
||
323 | console.error(err); |
||
324 | } finally { |
||
0 ignored issues
–
show
Comprehensibility
Documentation
Best Practice
introduced
by
|
|||
325 | } |
||
326 | return modules; |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * Initialize modules on any elements within `elem` (and `elem` itself) that |
||
331 | * aren't already initialized. |
||
332 | * @param {Object} elem - jQuery object containing the element to check inside. |
||
333 | * Also checks the element itself, unless it's the `document` |
||
334 | * object. |
||
335 | * @param {String|Array} modules - A list of modules to initialize. Leave this |
||
336 | * out to initialize everything. |
||
337 | */ |
||
338 | reflow (elem, modules) { |
||
339 | //console.//log('module manager reflow:', this.functionName(this), elem); |
||
340 | //console.//log('modules:', this._modules, modules); |
||
341 | |||
342 | var onlySpecificModules = false; |
||
0 ignored issues
–
show
|
|||
343 | // If modules is undefined, just grab everything |
||
344 | if (typeof modules === 'undefined') { |
||
345 | modules = Object.keys(this._modules); |
||
346 | onlySpecificModules = false; |
||
347 | } |
||
348 | // If modules is a string, convert it to an array with one item |
||
349 | else if (typeof modules === 'string') { |
||
350 | modules = [modules]; |
||
351 | onlySpecificModules = true; |
||
352 | } |
||
353 | if (typeof elem === 'undefined') { |
||
354 | elem = document; |
||
355 | } |
||
356 | var $moduleManager = this; |
||
357 | |||
358 | // |
||
359 | // Iterate through each module and re-flow... |
||
360 | // |
||
361 | $.each(modules, function(i, name) { |
||
362 | // Get the current module |
||
363 | var module = $moduleManager._modules[name]; |
||
364 | |||
365 | //console.//log('trying to reflow...: ', name); |
||
366 | |||
367 | // Localize the search to all elements inside elem, as well as elem |
||
368 | // itself, unless elem === document |
||
369 | var $elem = $(elem).find('[data-'+name+']').addBack('[data-'+name+']'); |
||
370 | |||
371 | //console.//log('elements...: ', $elem.length, '[data-'+name+']'); |
||
372 | |||
373 | // For each module found, initialize it |
||
374 | $elem.each(function() { |
||
375 | var $el = $(this), |
||
376 | opts = {} |
||
377 | ; |
||
378 | // Don't double-dip on modules, invoke 'reFlow' if available |
||
379 | if ($el.data($moduleManager.application.appName+'Plugin')) { |
||
380 | //console.//log('reflowing...: ', name, $el); |
||
381 | |||
382 | var plgIn = $el.data($moduleManager.application.appName+'Plugin'); |
||
383 | if ( plgIn.reflow ) { plgIn.reflow(); } |
||
384 | console.warn("Tried to initialize "+name+" on an element that "+ |
||
385 | "already has a ["+$moduleManager.functionName($moduleManager.application)+"] module."); |
||
386 | return; |
||
387 | } |
||
388 | |||
389 | // ... else try to (re)init the module/plugin |
||
390 | if($el.attr('data-options')){ |
||
391 | var thing = $el.attr('data-options').split(';').forEach(function(e, i){ |
||
0 ignored issues
–
show
|
|||
392 | var opt = e.split(':').map(function(el){ return el.trim(); }); |
||
393 | if (opt[0]) { |
||
394 | opts[opt[0]] = $moduleManager.parseValue(opt[1]); |
||
395 | } |
||
396 | }); |
||
397 | } |
||
398 | try { |
||
399 | //console.//log('new instance...: ', $moduleManager.functionName(module), $el); |
||
400 | $el.data($moduleManager.application.appName+'Plugin', new module($el, opts)); |
||
0 ignored issues
–
show
|
|||
401 | } catch(er) { |
||
402 | //console.//log('ERROR:', er); |
||
403 | throw new ModuleManagerException(er.message); |
||
404 | } finally { |
||
0 ignored issues
–
show
Comprehensibility
Documentation
Best Practice
introduced
by
|
|||
405 | } |
||
406 | return; |
||
407 | }); |
||
408 | }); |
||
409 | |||
410 | } // reflow |
||
411 | |||
412 | |||
413 | // |
||
414 | // some (internal) shortcuts |
||
415 | // |
||
416 | |||
417 | /** |
||
418 | * Polyfill to get the name of a function in IE9 |
||
419 | */ |
||
420 | functionName (fn) { |
||
421 | return this.application.utilities.functionName(fn); |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * Returns a random base-36 uid with namespacing |
||
426 | */ |
||
427 | genUUID (length, namespace) { |
||
428 | return this.application.utilities.genUUID (length, namespace); |
||
429 | } |
||
430 | |||
431 | /** |
||
432 | * Normalize value |
||
433 | */ |
||
434 | parseValue (str){ |
||
435 | return this.application.utilities.parseValue (str); |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * Convert PascalCase to kebab-case |
||
440 | */ |
||
441 | hyphenate (str) { |
||
442 | return this.application.utilities.hyphenate (str); |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * Retrieve attached application. |
||
447 | */ |
||
448 | get application () { |
||
449 | return this._app; |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * Attach application object. |
||
454 | */ |
||
455 | set application ( app ) { |
||
456 | if (app instanceof Siteapp) { |
||
457 | this._app = app; |
||
458 | } else { |
||
459 | throw new ModuleManagerException('Invalid application object to attach'); |
||
460 | } |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Retrieve current version. |
||
465 | */ |
||
466 | get version () { |
||
467 | return this._version; |
||
468 | } |
||
469 | |||
470 | |||
471 | } |
||
472 | |||
473 | |||
474 | export default ModuleManager; |