Issues (149)

js/module/siteapp.moduleManager.js (18 issues)

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
The constant Siteapp_ModuleManager_VERSION seems to be never used. Consider removing it.
Loading history...
28
29
const Siteapp_ModuleManager_DEFAULTS = {
0 ignored issues
show
The constant Siteapp_ModuleManager_DEFAULTS seems to be never used. Consider removing it.
Loading history...
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
Complexity Best Practice introduced by
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 return statement is found in some execution paths, but not in all.

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 isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
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
Complexity Best Practice introduced by
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 return statement is found in some execution paths, but not in all.

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 isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
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
The assignment to variable moduleName seems to be never used. Consider removing it.
Loading history...
149
    	if (_module instanceof Module) {
0 ignored issues
show
Complexity Best Practice introduced by
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 return statement is found in some execution paths, but not in all.

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 isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
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
Complexity Best Practice introduced by
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 return statement is found in some execution paths, but not in all.

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 isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
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
This return has no effect and can be removed.
Loading history...
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);
}
Loading history...
272
        	_module[prop] = null;//clean up script to prep for garbage collection.
273
        }
274
        
275
        return;
0 ignored issues
show
This return has no effect and can be removed.
Loading history...
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
This code block is empty. Consider removing it or adding a comment to explain.
Loading history...
Empty finally clause found. The clause can be removed.
Loading history...
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
The variable onlySpecificModules seems to be never used. Consider removing it.
Loading history...
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
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
The variable thing seems to be never used. Consider removing it.
Loading history...
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
Coding Style Best Practice introduced by
By convention, constructors like module should be capitalized.
Loading history...
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
This code block is empty. Consider removing it or adding a comment to explain.
Loading history...
Empty finally clause found. The clause can be removed.
Loading history...
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;