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