baja/nav/event.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.event}.
 * @module baja/nav/event
 */
define([ "bajaScript/sys" ], function mixin(baja) {

  "use strict";

  var strictArg = baja.strictArg,
      bsguid = 0,      // A unique id for assigned to event handlers
      event;


  //TODO: Could probably factor this into a framework method at some point
  function isObjEmpty(obj) {
    var q;
    for (q in obj) {
      if (obj.hasOwnProperty(q)) {
        return false;
      }
    }
    return true;
  }

  event = /** @lends baja.event */ {
    /**
     * Attach an event handler to listen for events.
     *
     * @private
     *
     * @param {String} hName event handler name.
     * @param {Function} func the event handler function.
     */
    attach: function attach(hName, func) {
      var that = this,
          p,
          i,
          names,
          name,
          handlers,
          map;
      
      //  If an Object is passed in then scan it for handlers 
      // (hName can be an Object map)
      if (hName && typeof hName === "object") {
        for (p in hName) {
          if (hName.hasOwnProperty(p)) {
            if (typeof hName[p] === "function") {
              that.attach(p, hName[p]);
            }
          }
        }
      } else {
  
        // Validate and then add a handler
        strictArg(hName, String);
        strictArg(func, Function);
  
        // Assign a unique id to this function      
        if (!func.$bsguid) {
          func.$bsguid = ++bsguid; 
        }
  
        // Lazily create handlers map
        handlers = that.$handlers;
        if (!handlers) {
          handlers = that.$handlers = {};
        }
  
        // If separated by a space then assign the function to multiple events
        names = hName.split(" ");
  
        for (i = 0; i < names.length; ++i) {
          name = names[i];
          map = handlers[name];
          
          if (!map) {
            map = handlers[name] = {};
          }
          
          // Assign handler into map
          map[func.$bsguid] = func;
        }
      }
    },
  
    /**
     * Detach an Event Handler.
     * 
     * If no arguments are used with this method then all events are removed.
     *
     * @private
     *
     * @param {String} [hName] the name of the handler to detach.
     * @param {Function} [func] the function to remove. It's recommended to supply this just in case
     *                          other scripts have added event handlers.
     */
    detach: function detach(hName, func) {
  
      var p,
          that = this,
          handlers,
          names,
          name,
          map,
          guid,
          i;
      
      //  If there are no arguments then remove all handlers...
      if (arguments.length === 0) {
        that.$handlers = undefined;
      }
  
      if (!that.$handlers) {
        return;
      }

  
      //  If an object is passed in then scan it for handlers 
      if (hName && typeof hName === "object") {
        if (hName.hasOwnProperty(p)) {
          if (typeof hName[p] === "function") {
            this.detach(p, hName[p]);
          }
        }
      } else {
        strictArg(hName, String);
  
        if (func) {
          strictArg(func, Function);
        }
        
        handlers = that.$handlers;
  
        // If separated by a space then remove from multiple event types...
        names = hName.split(" ");
  
        for (i = 0; i < names.length; ++i) {
          name = names[i];
          
          if (!func) {
            delete handlers[name];
          } else {
            map = handlers[name];
            guid = func.$bsguid;
            
            if (map && guid && map[guid]) {
              delete map[guid];
  
              // If there aren't any more handlers then delete the entry
              if (isObjEmpty(map)) {
                delete handlers[name];
              }
            }
          }
        }
  
        // If there are no handlers then set this back to undefined      
        if (isObjEmpty(handlers)) {
          that.$handlers = undefined;
        }
      }
    },
  
    /**
     * Fire events for the given handler name.
     * 
     * Unlike `getHandlers` or `hasHandlers` this method can only invoke one 
     * event handler name at a time.
     * 
     * This method should only be used internally by Tridium developers.
     *
     * @private
     *
     * @param {String} hName the name of the handler.
     * @param {Function} error called if any of the invoked handlers throw an error.
     * @param {*} context the object used as the 'this' parameter in any invoked event handler.
     * @param {...*} args Any extra arguments will be used as parameters in any invoked event
     * handler functions.
     */
    fireHandlers: function fireHandlers(hName, error, context, ...args) {
      const handlers = this.$handlers;
      
      //  Bail if there are no handlers registered
      if (!handlers) {
        return;
      }

  
      //  Iterate through and invoke the event handlers we're after    
      if (handlers.hasOwnProperty(hName)) {
        const handler = handlers[hName];
        for (const p in handler) {
          if (handler.hasOwnProperty(p)) {
            try {
              handler[p].apply(context, args);
            } catch (err) {
              error(err);
            }
          }
        }
      }
    },
  
    /**
     * Return an array of event handlers.
     * 
     * To access multiple handlers, insert a space between the handler names.
     *
     * @private
     *
     * @param {String} hName the name of the handler
     * @returns {Array}
     */
    getHandlers: function getHandlers(hName) {
      if (!this.$handlers) {
        return [];
      }
  
      var names = hName.split(" "),
          i,
          p,
          a = [],
          handlers = this.$handlers;
  
      for (i = 0; i < names.length; ++i) {
        if (handlers.hasOwnProperty(names[i])) {
          for (p in handlers[names[i]]) {
            if (handlers[names[i]].hasOwnProperty(p)) {
              a.push(handlers[names[i]][p]);
            }
          }
        }
      }
  
      return a;
    },
  
    /**
     * Return true if there any handlers registered for the given handler name.
     * 
     * If no handler name is specified then test to see if there are any handlers registered at all.
     * 
     * Multiple handlers can be tested for by using a space character between the names.
     *
     * @private
     *
     * @param {String} [hName] the name of the handler. If undefined, then see if there are any 
     *                         handlers registered at all.
     * @returns {Boolean}
     */
    hasHandlers: function hasHandlers(hName) {
      var handlers = this.$handlers,
          names,
          i;
      
      //  If there are no handlers then bail
      if (!handlers) {
        return false;
      }
  
      //  If there isn't a handler name defined then at this point we must have some handler
      if (hName === undefined) {
        return true;
      }
  
      names = hName.split(" ");
  
      for (i = 0; i < names.length; ++i) {
        if (!handlers.hasOwnProperty(names[i])) {
          return false;
        }
      }
  
      return true;
    },
    
    /**
     * Mix-in the event handlers onto the given Object.
     *
     * @private
     *
     * @param obj
     */
    mixin: function (obj) {
      obj.attach = event.attach;
      obj.detach = event.detach;
      obj.fireHandlers = event.fireHandlers;
      obj.getHandlers = event.getHandlers;
      obj.hasHandlers = event.hasHandlers;
    }
  };
  
  return event;
});