1 /**
  2  * @author <a href="http://www.creynders.be">Camille Reynders</a>
  3  */
  4 ( function ( scope ) {
  5 
  6     "use strict";
  7 
  8     /**
  9      * @namespace
 10      */
 11     var dijon = {
 12         /**
 13          * framework version number
 14          * @constant
 15          * @type String
 16          */
 17         VERSION:'0.5.3'
 18     };//dijon
 19 
 20 
 21     //======================================//
 22     // dijon.System
 23     //======================================//
 24 
 25     /**
 26      * @class dijon.System
 27      * @constructor
 28      */
 29     dijon.System = function () {
 30         /** @private */
 31         this._mappings = {};
 32 
 33         /** @private */
 34         this._outlets = {};
 35 
 36         /** @private */
 37         this._handlers = {};
 38 
 39         /**
 40          * When <code>true</code> injections are made only if an object has a property with the mapped outlet name.<br/>
 41          * <strong>Set to <code>false</code> at own risk</strong>, may have quite undesired side effects.
 42          * @example
 43          * system.strictInjections = true
 44          * var o = {};
 45          * system.mapSingleton( 'userModel', UserModel );
 46          * system.mapOutlet( 'userModel' );
 47          * system.injectInto( o );
 48          *
 49          * //o is unchanged
 50          *
 51          * system.strictInjections = false;
 52          * system.injectInto( o );
 53          *
 54          * //o now has a member 'userModel' which holds a reference to the singleton instance
 55          * //of UserModel
 56          * @type Boolean
 57          * @default true
 58          */
 59         this.strictInjections = true;
 60 
 61         /**
 62          * Enables the automatic mapping of outlets for mapped values, singletons and classes
 63          * When this is true any value, singleton or class that is mapped will automatically be mapped as a global outlet
 64          * using the value of <code>key</code> as outlet name
 65          *
 66          * @example
 67          * var o = {
 68          *     userModel : undefined; //inject
 69          * }
 70          * system.mapSingleton( 'userModel', UserModel );
 71          * system.injectInto( o ):
 72          * //o.userModel now holds a reference to the singleton instance of UserModel
 73          * @type Boolean
 74          * @default false
 75          */
 76         this.autoMapOutlets = false;
 77 
 78         /**
 79          * The name of the method that will be called for all instances, right after injection has occured.
 80          * @type String
 81          * @default 'setup'
 82          */
 83         this.postInjectionHook = 'setup';
 84 
 85     };//dijon.System
 86 
 87     dijon.System.prototype = {
 88 
 89         /**
 90          * @private
 91          * @param {Class} clazz
 92          */
 93         _createAndSetupInstance:function ( key, Clazz ) {
 94             var instance = new Clazz();
 95             this.injectInto( instance, key );
 96             return instance;
 97         },
 98 
 99         /**
100          * @private
101          * @param {String} key
102          * @param {Boolean} overrideRules
103          * @return {Object}
104          */
105         _retrieveFromCacheOrCreate:function ( key, overrideRules ) {
106             if ( typeof overrideRules === 'undefined' ) {
107                 overrideRules = false;
108             }
109             var output;
110             if ( this._mappings.hasOwnProperty( key ) ) {
111                 var config = this._mappings[ key ];
112                 if ( !overrideRules && config.isSingleton ) {
113                     if ( config.object == null ) {
114                         config.object = this._createAndSetupInstance( key, config.clazz );
115                     }
116                     output = config.object;
117                 } else {
118                     if ( config.clazz ) {
119                         output = this._createAndSetupInstance( key, config.clazz );
120                     } else {
121                         //TODO shouldn't this be null
122                         output = config.object;
123                     }
124                 }
125             } else {
126                 throw new Error( 1000 );
127             }
128             return output;
129         },
130 
131 
132         /**
133          * defines <code>outletName</code> as an injection point in <code>targetKey</code>for the object mapped to <code>sourceKey</code>
134          * @example
135          system.mapSingleton( 'userModel', TestClassA );
136          var o = {
137          user : undefined //inject
138          }
139          system.mapOutlet( 'userModel', 'o', 'user' );
140          system.mapValue( 'o', o );
141 
142          var obj = system.getObject( 'o' );
143          * //obj.user holds a reference to the singleton instance of UserModel
144          *
145          * @example
146          system.mapSingleton( 'userModel', TestClassA );
147          var o = {
148          userModel : undefined //inject
149          }
150          system.mapOutlet( 'userModel', 'o' );
151          system.mapValue( 'o', o );
152 
153          var obj = system.getObject( 'o' );
154          * //obj.userModel holds a reference to the singleton instance of UserModel
155          *
156          * @example
157          system.mapSingleton( 'userModel', TestClassA );
158          system.mapOutlet( 'userModel' );
159          var o = {
160          userModel : undefined //inject
161          }
162          system.mapValue( 'o', o );
163 
164          var obj = system.getObject( 'o' );
165          * //o.userModel holds a reference to the singleton instance of userModel
166          *
167          * @param {String} sourceKey the key mapped to the object that will be injected
168          * @param {String} [targetKey='global'] the key the outlet is assigned to.
169          * @param {String} [outletName=sourceKey] the name of the property used as an outlet.<br/>
170          * @return {dijon.System}
171          * @see dijon.System#unmapOutlet
172          */
173         mapOutlet:function ( sourceKey, targetKey, outletName ) {
174             if ( typeof sourceKey === 'undefined' ) {
175                 throw new Error( 1010 );
176             }
177             targetKey = targetKey || "global";
178             outletName = outletName || sourceKey;
179 
180             if ( !this._outlets.hasOwnProperty( targetKey ) ) {
181                 this._outlets[ targetKey ] = {};
182             }
183             this._outlets[ targetKey ][ outletName ] = sourceKey;
184 
185             return this;
186         },
187 
188         /**
189          * Retrieve (or create) the object mapped to <code>key</code>
190          * @example
191          * system.mapValue( 'foo', 'bar' );
192          * var b = system.getObject( 'foo' ); //now contains 'bar'
193          * @param {Object} key
194          * @return {Object}
195          */
196         getObject:function ( key ) {
197             if ( typeof key === 'undefined' ) {
198                 throw new Error( 1020 );
199             }
200             return this._retrieveFromCacheOrCreate( key );
201         },
202 
203         /**
204          * Maps <code>useValue</code> to <code>key</code>
205          * @example
206          * system.mapValue( 'foo', 'bar' );
207          * var b = system.getObject( 'foo' ); //now contains 'bar'
208          * @param {String} key
209          * @param {Object} useValue
210          * @return {dijon.System}
211          */
212         mapValue:function ( key, useValue ) {
213             if ( typeof key === 'undefined' ) {
214                 throw new Error( 1030 );
215             }
216             this._mappings[ key ] = {
217                 clazz:null,
218                 object:useValue,
219                 isSingleton:true
220             };
221             if ( this.autoMapOutlets ) {
222                 this.mapOutlet( key );
223             }
224             if ( this.hasMapping( key )) {
225                 this.injectInto( useValue, key );
226             }
227             return this;
228         },
229 
230         /**
231          * Returns whether the key is mapped to an object
232          * @example
233          * system.mapValue( 'foo', 'bar' );
234          * var isMapped = system.hasMapping( 'foo' );
235          * @param {String} key
236          * @return {Boolean}
237          */
238         hasMapping:function ( key ) {
239             if ( typeof key === 'undefined' ) {
240                 throw new Error( 1040 );
241             }
242             return this._mappings.hasOwnProperty( key );
243         },
244 
245         /**
246          * Maps <code>clazz</code> as a factory to <code>key</code>
247          * @example
248          * var SomeClass = function(){
249          * }
250          * system.mapClass( 'o', SomeClass );
251          *
252          * var s1 = system.getObject( 'o' );
253          * var s2 = system.getObject( 'o' );
254          *
255          * //s1 and s2 reference two different instances of SomeClass
256          *
257          * @param {String} key
258          * @param {Function} clazz
259          * @return {dijon.System}
260          */
261         mapClass:function ( key, clazz ) {
262             if ( typeof key === 'undefined' ) {
263                 throw new Error( 1050 );
264             }
265             if ( typeof clazz === 'undefined' ) {
266                 throw new Error( 1051 );
267             }
268             this._mappings[ key ] = {
269                 clazz:clazz,
270                 object:null,
271                 isSingleton:false
272             };
273             if ( this.autoMapOutlets ) {
274                 this.mapOutlet( key );
275             }
276             return this;
277         },
278 
279         /**
280          * Maps <code>clazz</code> as a singleton factory to <code>key</code>
281          * @example
282          * var SomeClass = function(){
283          * }
284          * system.mapSingleton( 'o', SomeClass );
285          *
286          * var s1 = system.getObject( 'o' );
287          * var s2 = system.getObject( 'o' );
288          *
289          * //s1 and s2 reference the same instance of SomeClass
290          *
291          * @param {String} key
292          * @param {Function} clazz
293          * @return {dijon.System}
294          */
295         mapSingleton:function ( key, clazz ) {
296             if ( typeof key === 'undefined' ) {
297                 throw new Error( 1060 );
298             }
299             if ( typeof clazz === 'undefined' ) {
300                 throw new Error( 1061 );
301             }
302             this._mappings[ key ] = {
303                 clazz:clazz,
304                 object:null,
305                 isSingleton:true
306             };
307             if ( this.autoMapOutlets ) {
308                 this.mapOutlet( key );
309             }
310             return this;
311         },
312 
313         /**
314          * Force instantiation of the class mapped to <code>key</code>, whether it was mapped as a singleton or not.
315          * When a value was mapped, the value will be returned.
316          * TODO: should this last rule be changed?
317          * @example
318          var SomeClass = function(){
319          }
320          system.mapClass( 'o', SomeClass );
321 
322          var s1 = system.getObject( 'o' );
323          var s2 = system.getObject( 'o' );
324          * //s1 and s2 reference different instances of SomeClass
325          *
326          * @param {String} key
327          * @return {Object}
328          */
329         instantiate:function ( key ) {
330             if ( typeof key === 'undefined' ) {
331                 throw new Error( 1070 );
332             }
333             return this._retrieveFromCacheOrCreate( key, true );
334         },
335 
336         /**
337          * Perform an injection into an object's mapped outlets, satisfying all it's dependencies
338          * @example
339          * var UserModel = function(){
340          * }
341          * system.mapSingleton( 'userModel', UserModel );
342          * var SomeClass = function(){
343          *      user = undefined; //inject
344          * }
345          * system.mapSingleton( 'o', SomeClass );
346          * system.mapOutlet( 'userModel', 'o', 'user' );
347          *
348          * var foo = {
349          *      user : undefined //inject
350          * }
351          *
352          * system.injectInto( foo, 'o' );
353          *
354          * //foo.user now holds a reference to the singleton instance of UserModel
355          * @param {Object} instance
356          * @param {String} [key] use the outlet mappings as defined for <code>key</code>, otherwise only the globally
357          * mapped outlets will be used.
358          * @return {dijon.System}
359          */
360         injectInto:function ( instance, key ) {
361             if ( typeof instance === 'undefined' ) {
362                 throw new Error( 1080 );
363             }
364 			if( ( typeof instance === 'object' ) ){
365 				var o = [];
366 				if ( this._outlets.hasOwnProperty( 'global' ) ) {
367 					o.push( this._outlets[ 'global' ] );
368 				}
369 				if ( typeof key !== 'undefined' && this._outlets.hasOwnProperty( key ) ) {
370 					o.push( this._outlets[ key ] );
371 				}
372 				for ( var i in o ) {
373 					var l = o [ i ];
374 					for ( var outlet in l ) {
375 						var source = l[ outlet ];
376 						//must be "in" [!]
377 						if ( !this.strictInjections || outlet in instance ) {
378 							instance[ outlet ] = this.getObject( source );
379 						}
380 					}
381 				}
382 				if ( "setup" in instance ) {
383 					instance.setup.call( instance );
384 				}
385 			}
386             return this;
387         },
388 
389         /**
390          * Remove the mapping of <code>key</code> from the system
391          * @param {String} key
392          * @return {dijon.System}
393          */
394         unmap:function ( key ) {
395             if ( typeof key === 'undefined' ) {
396                 throw new Error( 1090 );
397             }
398             delete this._mappings[ key ];
399 
400             return this;
401         },
402 
403         /**
404          * removes an injection point mapping for a given object mapped to <code>key</code>
405          * @param {String} target
406          * @param {String} outlet
407          * @return {dijon.System}
408          * @see dijon.System#addOutlet
409          */
410         unmapOutlet:function ( target, outlet ) {
411             if ( typeof target === 'undefined' ) {
412                 throw new Error( 1100 );
413             }
414             if ( typeof outlet === 'undefined' ) {
415                 throw new Error( 1101 );
416             }
417             delete this._outlets[ target ][ outlet ];
418 
419             return this;
420         },
421 
422         /**
423          * maps a handler for an event/route.<br/>
424          * @example
425          var hasExecuted = false;
426          var userView = {
427          showUserProfile : function(){
428          hasExecuted = true;
429          }
430          }
431          system.mapValue( 'userView', userView );
432          system.mapHandler( 'user/profile', 'userView', 'showUserProfile' );
433          system.notify( 'user/profile' );
434          //hasExecuted is true
435          * @example
436          * var userView = {
437          *      showUserProfile : function(){
438          *          //do stuff
439          *      }
440          * }
441          * system.mapValue( 'userView', userView );
442          * <strong>system.mapHandler( 'showUserProfile', 'userView' );</strong>
443          * system.notify( 'showUserProfile' );
444          *
445          * //userView.showUserProfile is called
446          * @example
447          * var showUserProfile = function(){
448          *          //do stuff
449          * }
450          * <strong>system.mapHandler( 'user/profile', undefined, showUserProfile );</strong>
451          * system.notify( 'user/profile' );
452          *
453          * //showUserProfile is called
454          * @example
455          * var userView = {};
456          * var showUserProfile = function(){
457          *          //do stuff
458          * }
459          * system.mapValue( 'userView', userView );
460          * <strong>system.mapHandler( 'user/profile', 'userView', showUserProfile );</strong>
461          * system.notify( 'user/profile' );
462          *
463          * //showUserProfile is called within the scope of the userView object
464          * @example
465          * var userView = {
466          *      showUserProfile : function(){
467          *          //do stuff
468          *      }
469          * }
470          * system.mapValue( 'userView', userView );
471          * <strong>system.mapHandler( 'user/profile', 'userView', 'showUserProfile', true );</strong>
472          * system.notify( 'user/profile' );
473          * system.notify( 'user/profile' );
474          * system.notify( 'user/profile' );
475          *
476          * //userView.showUserProfile is called exactly once [!]
477          * @example
478          * var userView = {
479          *      showUserProfile : function( route ){
480          *          //do stuff
481          *      }
482          * }
483          * system.mapValue( 'userView', userView );
484          * <strong>system.mapHandler( 'user/profile', 'userView', 'showUserProfile', false, true );</strong>
485          * system.notify( 'user/profile' );
486          *
487          * //userView.showUserProfile is called and the route/eventName is passed to the handler
488          * @param {String} eventName/route
489          * @param {String} [key=undefined] If <code>key</code> is <code>undefined</code> the handler will be called without
490          * scope.
491          * @param {String|Function} [handler=eventName] If <code>handler</code> is <code>undefined</code> the value of
492          * <code>eventName</code> will be used as the name of the member holding the reference to the to-be-called function.
493          * <code>handler</code> accepts either a string, which will be used as the name of the member holding the reference
494          * to the to-be-called function, or a direct function reference.
495          * @param {Boolean} [oneShot=false] Defines whether the handler should be called exactly once and then automatically
496          * unmapped
497          * @param {Boolean} [passEvent=false] Defines whether the event object should be passed to the handler or not.
498          * @return {dijon.System}
499          * @see dijon.System#notify
500          * @see dijon.System#unmapHandler
501          */
502         mapHandler:function ( eventName, key, handler, oneShot, passEvent ) {
503             if ( typeof eventName === 'undefined' ) {
504                 throw new Error( 1110 );
505             }
506             key = key || 'global';
507             handler = handler || eventName;
508 
509             if ( typeof oneShot === 'undefined' ) {
510                 oneShot = false;
511             }
512             if ( typeof passEvent === 'undefined' ) {
513                 passEvent = false;
514             }
515             if ( !this._handlers.hasOwnProperty( eventName ) ) {
516                 this._handlers[ eventName ] = {};
517             }
518             if ( !this._handlers[eventName].hasOwnProperty( key ) ) {
519                 this._handlers[eventName][key] = [];
520             }
521             this._handlers[ eventName ][ key ].push( {
522                 handler:handler,
523                 oneShot:oneShot,
524                 passEvent:passEvent
525             } );
526 
527             return this;
528         },
529 
530         /**
531          * Unmaps the handler for a specific event/route.
532          * @param {String} eventName Name of the event/route
533          * @param {String} [key=undefined] If <code>key</code> is <code>undefined</code> the handler is removed from the
534          * global mapping space. (If the same event is mapped globally and specifically for an object, then
535          * only the globally mapped one will be removed)
536          * @param {String | Function} [handler=eventName]
537          * @return {dijon.System}
538          * @see dijon.System#mapHandler
539          */
540         unmapHandler:function ( eventName, key, handler ) {
541             if ( typeof eventName === 'undefined' ) {
542                 throw new Error( 1120 );
543             }
544             key = key || 'global';
545             handler = handler || eventName;
546 
547             if ( this._handlers.hasOwnProperty( eventName ) && this._handlers[ eventName ].hasOwnProperty( key ) ) {
548                 var handlers = this._handlers[ eventName ][ key ];
549                 for ( var i in handlers ) {
550                     var config = handlers[ i ];
551                     if ( config.handler === handler ) {
552                         handlers.splice( i, 1 );
553                         break;
554                     }
555                 }
556             }
557             return this;
558         },
559 
560         /**
561          * calls all handlers mapped to <code>eventName/route</code>
562          * @param {String} eventName/route
563          * @return {dijon.System}
564          * @see dijon.System#mapHandler
565          */
566         notify:function ( eventName ) {
567             if ( typeof eventName === 'undefined' ) {
568                 throw new Error( 1130 );
569             }
570             var argsWithEvent = Array.prototype.slice.call( arguments );
571             var argsClean = argsWithEvent.slice( 1 );
572             if ( this._handlers.hasOwnProperty( eventName ) ) {
573                 var handlers = this._handlers[ eventName ];
574                 for ( var key in handlers ) {
575                     var configs = handlers[ key ];
576                     var instance;
577                     if ( key !== 'global' ) {
578                         instance = this.getObject( key );
579                     }
580                     var toBeDeleted = [];
581                     var i, n;
582                     for ( i = 0, n = configs.length ; i < n ; i++ ) {
583                         var handler;
584                         var config = configs[ i ];
585                         if ( instance && typeof config.handler === "string" ) {
586                             handler = instance[ config.handler ];
587                         } else {
588                             handler = config.handler;
589                         }
590 
591                         //see deletion below
592                         if ( config.oneShot ) {
593                             toBeDeleted.unshift( i );
594                         }
595 
596                         if ( config.passEvent ) {
597                             handler.apply( instance, argsWithEvent );
598                         } else {
599                             handler.apply( instance, argsClean );
600                         }
601                     }
602 
603                     //items should be deleted in reverse order
604                     //either use push above and decrement here
605                     //or use unshift above and increment here
606                     for ( i = 0, n = toBeDeleted.length ; i < n ; i++ ) {
607                         configs.splice( toBeDeleted[ i ], 1 );
608                     }
609                 }
610             }
611 
612             return this;
613         }
614 
615     };//dijon.System.prototype
616 
617     scope.dijon = dijon;
618 }( this ));
619 
620 
621