1 //
  2 // Copyright 2010, Tridium, Inc. All Rights Reserved.
  3 //
  4 
  5 /**
  6  * BajaScript Nav and Event Architecture.
  7  *
  8  * @author Gareth Johnson
  9  * @version 1.0.0.0
 10  */
 11 
 12 //JsLint options (see http://www.jslint.com )
 13 /*jslint rhino: true, onevar: false, plusplus: true, white: true, undef: false, nomen: false, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false, indent: 2, vars: true, continue: true */
 14 
 15 // Globals for JsLint to ignore 
 16 /*global baja, BaseBajaObj*/ 
 17 
 18 (function event(baja) {
 19   // Use ECMAScript 5 Strict Mode
 20   "use strict";
 21   
 22   var strictArg = baja.strictArg,
 23       bsguid = 0;      // A unique id for assigned to event handlers
 24       
 25   /**
 26    * @namespace Event Handling framework.
 27    */
 28   baja.event = new BaseBajaObj();
 29     
 30   // TODO: Could probably factor this into a framework method at some point
 31   function isObjEmpty(obj) {
 32     var q;
 33     for (q in obj) {
 34       if (obj.hasOwnProperty(q)) {
 35         return false;
 36       }
 37     }
 38     return true;
 39   }
 40   
 41   /**
 42    * Attach an event handler to listen for events.
 43    *
 44    * @private
 45    *
 46    * @param {String} event handler name.
 47    * @param {Function} the event handler function.
 48    */
 49   var attach = function (hName, func) {
 50     // If an Object is passed in then scan it for handlers (hName can be an Object map)
 51     if (hName && typeof hName === "object") {
 52       var p;
 53       for (p in hName) {
 54         if (hName.hasOwnProperty(p)) {
 55           if (typeof hName[p] === "function") {
 56             this.attach(p, hName[p]);
 57           }
 58         }
 59       }
 60     }
 61     else {
 62     
 63       // Validate and then add a handler
 64       strictArg(hName, String);
 65       strictArg(func, Function);
 66       
 67       // Assign a unique id to this function      
 68       if (!func.$bsguid) {
 69         func.$bsguid = ++bsguid; 
 70       }
 71             
 72       // Lazily create handlers map
 73       if (this.$handlers === undefined) {
 74         this.$handlers = {};
 75       }
 76       
 77       // If separated by a space then assign the function to multiple events
 78       var names = hName.split(" "), i;
 79       
 80       for (i = 0; i < names.length; ++i) {     
 81         // Assign handler into map
 82         if (!this.$handlers[names[i]]) {
 83           this.$handlers[names[i]] = {};
 84         }
 85         
 86         this.$handlers[names[i]][func.$bsguid] = func;
 87       }
 88     }
 89   };
 90     
 91   /**
 92    * Detach an Event Handler.
 93    * <p>
 94    * If no arguments are used with this method then all events are removed.
 95    *
 96    * @private
 97    *
 98    * @param {String} [hName] the name of the handler to detach.
 99    * @param {Function} [func] the function to remove. It's recommended to supply this just in case
100    *                          other scripts have added event handlers.
101    */
102   var detach = function (hName, func) {
103     // If there are no arguments then remove all handlers...
104     if (arguments.length === 0) {
105       this.$handlers = undefined;
106     }
107   
108     if (!this.$handlers) {
109       return;
110     }
111         
112     var p;
113        
114     // If an object is passed in then scan it for handlers 
115     if (hName && typeof hName === "object") {
116       if (hName.hasOwnProperty(p)) {
117         if (typeof hName[p] === "function") {
118           this.detach(p, hName[p]);
119         }
120       }
121     }
122     else {
123       strictArg(hName, String);
124       
125       if (func) {
126         strictArg(func, Function);
127       }
128       
129       // If separated by a space then remove from multiple event types...
130       var names = hName.split(" "),
131           i;
132       
133       for (i = 0; i < names.length; ++i) {    
134         if (!func) {
135           delete this.$handlers[names[i]];
136         }
137         else {
138           if (func.$bsguid && this.$handlers[names[i]] && this.$handlers[names[i]][func.$bsguid]) {
139             delete this.$handlers[names[i]][func.$bsguid];
140             
141             // If there aren't any more handlers then delete the entry
142             if (isObjEmpty(this.$handlers[names[i]])) {
143               delete this.$handlers[names[i]];
144             }
145           }
146         }
147       }
148       
149       // If there are no handlers then set this back to undefined      
150       if (isObjEmpty(this.$handlers)) {
151         this.$handlers = undefined;
152       }
153     }
154   };
155 
156   /**
157    * Fire events for the given handler name.
158    * <p>
159    * Any extra arguments will be used as parameters in any invoked event handler functions.
160    * <p>
161    * Unlike 'getHandlers' or 'hasHandlers' this method can only invoke one event handler name at a time.
162    * <p>
163    * This method should only be used internally by Tridium developers.
164    *
165    * @private
166    *
167    * @param {String} hName the name of the handler.
168    * @param {Function} called if any of the invoked handlers throw an error.
169    * @param context the object used as the 'this' parameter in any invoked event handler.
170    */
171   var fireHandlers = function (hName, error, context) {
172     // Bail if there are no handlers registered
173     if (!this.$handlers) {
174       return;
175     }
176     
177     var p,
178         handlers = this.$handlers,
179         args;
180     
181     // Iterate through and invoke the event handlers we're after    
182     if (handlers.hasOwnProperty(hName)) {    
183       // Get arguments used for the event
184       args = Array.prototype.slice.call(arguments);
185       args.splice(0, 3); // Delete the first three arguments and use 
186                          // the rest as arguments for the event handler
187     
188       for (p in handlers[hName]) {
189         if (handlers[hName].hasOwnProperty(p)) {
190           try {
191             handlers[hName][p].apply(context, args);
192           }
193           catch(err) {
194             error(err);
195           }
196         }
197       }
198     }
199   };
200   
201   /**
202    * Return an array of event handlers.
203    * <p>
204    * To access multiple handlers, insert a space between the handler names.
205    *
206    * @private
207    *
208    * @param {String} hName the name of the handler
209    * @returns {Array}
210    */
211   var getHandlers = function (hName) {
212     if (!this.$handlers) {
213       return [];
214     }
215     
216     var names = hName.split(" "),
217         i,
218         p,
219         a = [],
220         handlers = this.$handlers;
221         
222     for (i = 0; i < names.length; ++i) {
223       if (handlers.hasOwnProperty(names[i])) {
224         for (p in handlers[names[i]]) {
225           if (handlers[names[i]].hasOwnProperty(p)) {
226             a.push(handlers[names[i]][p]);
227           }
228         }
229       }
230     }
231         
232     return a;
233   };
234   
235   /**
236    * Return true if there any handlers registered for the given handler name.
237    * <p>
238    * If no handler name is specified then test to see if there are any handlers registered at all.
239    * <p>
240    * Multiple handlers can be tested for by using a space character between the names.
241    *
242    * @private
243    *
244    * @param {String} [hName] the name of the handler. If undefined, then see if there are any 
245    *                         handlers registered at all.
246    * @returns {Boolean}
247    */
248   var hasHandlers = function (hName) {
249     // If there are no handlers then bail
250     if (!this.$handlers) {
251       return false;
252     }
253     
254     // If there isn't a handler name defined then at this point we must have some handler
255     if (hName === undefined) {
256       return true;
257     }
258     
259     var names = hName.split(" "),
260         i;
261         
262     for (i = 0; i < names.length; ++i) {
263       if (!this.$handlers.hasOwnProperty(names[i])) {
264         return false;
265       }
266     }
267  
268     return true;
269   };
270  
271   /**
272    * Mix-in the event handlers onto the given Object.
273    *
274    * @private
275    *
276    * @param obj
277    */
278   baja.event.mixin = function (obj) {
279     obj.attach = attach;
280     obj.detach = detach;
281     obj.getHandlers = getHandlers;
282     obj.hasHandlers = hasHandlers;
283     obj.fireHandlers = fireHandlers;
284   };
285     
286 }(baja));
287  
288 (function nav(baja) {
289 
290   // Use ECMAScript 5 Strict Mode
291   "use strict";
292 
293   var objectify = baja.objectify,
294       navFileRoot,     // If undefined then we need to make a request for the Nav File Root.
295       navFileMap = {};
296     
297   /**
298    * @class NavContainer is a generic NavNode.
299    *
300    * @name baja.NavContainer
301    * @extends baja.Object
302    */
303   baja.NavContainer = function (obj) {
304     baja.NavContainer.$super.apply(this, arguments); 
305     if (obj) {      
306       this.$navName = obj.navName;
307       this.$navDisplayName = obj.displayName || obj.navName;
308       this.$navOrdStr = obj.ord;
309       this.$navIconStr = obj.icon;
310     }
311   }.$extend(baja.Object).registerType("baja:NavContainer");
312   
313   /**
314    * Return the Nav Name.
315    *
316    * @returns {String}
317    */
318   baja.NavContainer.prototype.getNavName = function () {
319     return this.$navName;
320   };
321   
322   /**
323    * Return the Nav Display Name.
324    * 
325    * @returns {String}
326    */
327   baja.NavContainer.prototype.getNavDisplayName = function () {
328     return this.$navDisplayName;
329   };
330   
331   /**
332    * Return the Nav Description.
333    *
334    * @returns {String}
335    */
336   baja.NavContainer.prototype.getNavDescription = function () {
337     return this.$navDisplayName;
338   };
339   
340   /**
341    * Return the Nav ORD.
342    *
343    * @returns {baja.Ord}
344    */
345   baja.NavContainer.prototype.getNavOrd = function () {
346     if (!this.$navOrd) {
347       this.$navOrd = baja.Ord.make(this.$navOrdStr);
348     }
349     return this.$navOrd;
350   };
351   
352   /**
353    * Return the Nav Parent (or null if there's no parent).
354    *
355    * @returns nav parent
356    */
357   baja.NavContainer.prototype.getNavParent = function () {
358     return this.$navParent || null;
359   };
360       
361   /**
362    * Access the Nav Children.
363    * <p>
364    * This method takes an Object Literal the method arguments or an ok function.
365    * <pre>
366    *   container.getNavChildren(function (kids) {
367    *     // Process children
368    *   });
369    *   // or...
370    *   container.getNavChildren({
371    *     ok: function (kids) {
372    *       // Process children
373    *     },
374    *     fail: function (err) {
375    *       baja.error(err);
376    *     }
377    *   });
378    * </pre>
379    *
380    * @param {Object} obj the Object Literal for the method's arguments.
381    * @param {Function} obj.ok called when we have the Nav Children. An array of Nav Children is
382    *                          is passed as an argument into this function.
383    * @param {Function} [obj.fail] called if the function fails to complete.
384    *
385    * @returns {Array}
386    */
387   baja.NavContainer.prototype.getNavChildren = function (obj) {
388     obj = objectify(obj, "ok");
389     obj.ok(this.$navKids || []);
390   };
391   
392   /**
393    * Return the Nav Icon for this node.
394    *
395    * @returns {baja.Icon}
396    */
397   baja.NavContainer.prototype.getNavIcon = function () {
398     if (!this.$navIcon) {
399       this.$navIcon = baja.Icon.make(this.$navIconStr);
400     }
401     return this.$navIcon;
402   };
403   
404   /**
405    * Add a child node to this container.
406    * <p>
407    * Please note, this is a private method and should only be used by Tridium developers.
408    *
409    * @private
410    *
411    * @param node
412    * @returns node
413    */
414   baja.NavContainer.prototype.$addChildNode = function (node) {
415     if (!this.$navKids) {
416       this.$navKids = [];
417     }
418     node.$navParent = this;
419     this.$navKids.push(node);
420     return node;
421   };
422 
423   /**
424    * @class NavRoot
425    *
426    * @inner
427    * @public
428    * @name NavRoot
429    * @extends baja.NavContainer
430    */  
431   var NavRoot = function () {
432     NavRoot.$super.apply(this, arguments);
433   }.$extend(baja.NavContainer).registerType("baja:NavRoot");
434   
435   /**
436    * @class The decoded NavFile Space.
437    *
438    * @inner
439    * @public
440    * @name NavFileSpace
441    * @extends baja.NavContainer
442    */
443   var NavFileSpace = function () {
444     NavFileSpace.$super.call(this, {
445       navName: "navfile",
446       ord: "dummy:",
447       icon: "module://icons/x16/object.png"
448     }); 
449   }.$extend(baja.NavContainer).registerType("baja:NavFileSpace");
450   
451   function decodeNavJson(json) {
452     if (json === null) {
453       return null;
454     }
455   
456     var node = baja.$("baja:NavFileNode", {
457       navName: json.n,
458       displayName: json.d,
459       description: json.e,
460       ord: json.o,
461       icon: json.i
462     });
463     
464     // Register in the map
465     navFileMap[json.o] = node;
466               
467     if (json.k) {
468       var i;
469       for (i = 0; i < json.k.length; ++i) {
470         node.$addChildNode(decodeNavJson(json.k[i]));
471       }
472     }
473   
474     return node;
475   }
476   
477   /**
478    * If the NavFile isn't already loaded, make a network call to load 
479    * the NavFile across the network.
480    * <p>
481    * An Object Literal is used for the method's arguments.
482    *
483    * @param {Object} obj the Object Literal for the method's arguments.
484    * @param {Function} obj.ok called once the NavFile has been loaded.
485    *                          The Nav Root Node will be passed to this function when invoked.
486    * @param {Function} [obj.fail] called if any errors occur.
487    * @param {baja.comm.Batch} [obj.batch] if specified, this will batch any network calls.
488    */
489   NavFileSpace.prototype.load = function (obj) {
490     obj = objectify(obj, "ok");
491     
492     var cb = new baja.comm.Callback(obj.ok, obj.fail, obj.batch);
493     
494     // If already loaded then don't bother making a network call
495     if (navFileRoot !== undefined) {
496       cb.ok(navFileRoot);
497     }
498     else {
499       // Add an intermediate callback to decode the NavFile
500       cb.addOk(function (ok, fail, navFileJson) {        
501         // Parse NavFile JSON
502         navFileRoot = decodeNavJson(navFileJson);   
503         
504         ok(navFileRoot);
505       });
506     
507       baja.comm.navFile(cb);
508     }
509   };
510   
511   /**
512    * Return the NavFileRoot.
513    * <p>
514    * If there's no NavFile specified for the user, this will return null.
515    *
516    * @returns nav file root node (or null if there's no specified NavFile).
517    */
518   NavFileSpace.prototype.getRootNode = function () {
519     if (navFileRoot === undefined) {
520       var batch = new baja.comm.Batch();
521       this.load({batch: batch});
522       batch.commitSync();
523     }
524     return navFileRoot || null;
525   };
526   
527   /**
528    * Look up the NavNode for the specified Nav ORD.
529    *
530    * @param {String|baja.Ord} the Nav ORD used to look up the Nav ORD.
531    *
532    * @returns Nav Node
533    */
534   NavFileSpace.prototype.lookup = function (ord) {
535     if (!this.getRootNode()) {
536       return null;
537     }
538     return navFileMap[ord.toString()] || null;
539   };
540   
541   /**
542    * Access the Nav Children.
543    * 
544    * @see baja.NavContainer#getNavChildren
545    */
546   NavFileSpace.prototype.getNavChildren = function (obj) {
547     obj = objectify(obj, "ok");
548     var root = this.getRootNode();
549     if (root) {
550       root.getNavChildren(obj);
551     }
552     else {
553       obj.ok([]);
554     }
555   };
556   
557   /**
558    * Return the Nav Display Name.
559    * 
560    * @returns {String}
561    */
562   NavFileSpace.prototype.getNavDisplayName = function () {
563     var root = this.getRootNode();
564     return root ? root.getNavDisplayName() : this.$navDisplayName;
565   };
566   
567   /**
568    * Return the Nav ORD.
569    *
570    * @returns {baja.Ord}
571    */
572   NavFileSpace.prototype.getNavOrd = function () {
573     var root = this.getRootNode();
574     return root ? root.getNavOrd() : NavFileSpace.$super.prototype.getNavOrd.call(this);
575   };
576         
577   /**
578    * @namespace Nav Root
579    */
580   baja.nav = new NavRoot({
581     navName: "root",
582     ord: "root:", // TODO: Implement root scheme
583     icon: "module://icons/x16/planet.png"
584   });
585           
586   /**
587    * @namespace NavFileSpace
588    */
589   baja.nav.navfile = baja.nav.$addChildNode(new NavFileSpace());
590   
591   // Mix-in the event handlers for the Nav Root
592   baja.event.mixin(baja.nav);
593   
594   // These comments are left in for the benefit of JsDoc Toolkit...
595   
596   /**
597    * Attach an event handler to listen for navigation events.
598    * <p>
599    * Please note, navigation events only cover 'add', 'remove', 'renamed' and 'reordered'.
600    * <p>
601    * For a list of all the event handlers and some of this method's more advanced 
602    * features, please see {@link baja.Subscriber#attach}.
603    *
604    * @function
605    * @name baja.nav.attach
606    *
607    * @see baja.Subscriber
608    * @see baja.nav.detach
609    * @see baja.nav.getHandlers
610    * @see baja.nav.hasHandlers
611    *
612    * @param {String} event handler name.
613    * @param {Function} the event handler function.
614    */
615    
616   /**
617    * Detach an Event Handler.
618    * <p>
619    * If no arguments are used with this method then all events are removed.
620    * <p>
621    * For some of this method's more advanced features, please see {@link baja.Subscriber#detach}.
622    *
623    * @function
624    * @name baja.nav.detach
625    *
626    * @see baja.Subscriber
627    * @see baja.nav.attach
628    * @see baja.nav.getHandlers
629    * @see baja.nav.hasHandlers
630    *
631    * @param {String} [hName] the name of the handler to detach.
632    * @param {Function} [func] the function to remove. It's recommended to supply this just in case
633    *                          other scripts have added event handlers.
634    */
635         
636   /**
637    * Return an array of event handlers.
638    * <p>
639    * To access multiple handlers, insert a space between the handler names.
640    *
641    * @function
642    * @name baja.nav.getHandlers
643    *
644    * @see baja.Subscriber
645    * @see baja.nav.detach
646    * @see baja.nav.attach
647    * @see baja.nav.hasHandlers
648    *
649    * @param {String} hName the name of the handler
650    * @returns {Array}
651    */
652    
653   /**
654    * Return true if there any handlers registered for the given handler name.
655    * <p>
656    * If no handler name is specified then test to see if there are any handlers registered at all.
657    * <p>
658    * Multiple handlers can be tested for by using a space character between the names.
659    *
660    * @function
661    * @name baja.nav.hasHandlers
662    *
663    * @see baja.Subscriber
664    * @see baja.Component#detach
665    * @see baja.Component#attach
666    * @see baja.Component#getHandlers
667    *
668    * @param {String} [hName] the name of the handler. If undefined, then see if there are any 
669    *                         handlers registered at all.
670    * @returns {Boolean}
671    */
672 
673 }(baja)); //nav