gartz/ObjectEventTarget

View on GitHub
src/ObjectEventTarget.js

Summary

Maintainability
D
2 days
Test Coverage
// Add a ObjectEventTarget with a prototype that can be used
// by any object in the JavaScript context, to add, remove and trigger
// events.
// Example:
// ```
// function Foo(){}
// Foo.prototype = ObjectEventTarget.prototype;
// foo = new Foo();
// foo.addEventListener('dosomething', function(){
//   console.log(this, 'do something');
// });
// foo.dispatchEvent({type: 'dosomething'});
// // [Foo object], do something
// ```
//
// In old browsers that doesn't support enumerable property definition, the prototype
// methods will be iterated when you do for in, so don't forget to use the hasOwnProperty
(function(root) {
  'use strict';

  // Load options that can be setup before loading the library
  var options = root.ObjectEventTarget ? root.ObjectEventTarget.options : {};
  var DEBUG = root.DEBUG || options.DEBUG;
  var VERSION = options.VERSION || 'development';

  // Named object what contains the events queue for an object
  var EventsMap = options.EventsMap;
  if (!EventsMap){
    EventsMap = function EventsMap(type, callback){
      this[type] = [callback];
    };
    EventsMap.prototype = Object.prototype;
  }

  function typeErrors(type, callback){
    if (typeof type !== 'string' || type.length === 0){
      throw new TypeError('Type must be a string and can\'t be empty');
    }
    if (typeof callback !== 'function'){
      throw new TypeError('Callback must be a function');
    }
  }

  var WeakMap = options.WeakMap || root.WeakMap;
  if (!WeakMap) {
    WeakMap = function WeakMapArrayShim(){
      // A simple shim for WeakMap, only for usage in this lib, so it doesn't
      // support all features from a WeakMap, and it uses Array.indexOf for
      // search that isn't very performatic but allow us to have support in
      // old browsers

      this.map = [];
    };
    var find = function (arr, key){
      // Search for a meta element for the WeakMap shim, and if found
      // move this element for the begin of the array, to optimize usual
      // repetitive usage of the same object, and fast find the most used

      for (var m = arr.length - 1, i = m; i >= 0; i--){
        if (arr[i].key === key) {
          if (i !== m){
            var r = arr.splice(i, 1);
            arr.push(r[0]);
          }
          return arr[m];
        }
      }
      return;
    };
    WeakMap.prototype = {
      get: function (obj){
        var meta = find(this.map, obj);
        return meta && meta.value;
      },
      set: function (obj, value){
        var meta = find(this.map, obj);
        if (meta){
          meta.value = value;
        } else {
          this.map.push({key: obj, value: value});
        }
      },
      has: function (obj){
        return !!find(this.map, obj);
      },
      delete: function (obj){
        var meta = find(this.map, obj);
        if (meta) {
          this.map.pop();
        }
      }
    };
  }

  var map = new WeakMap();
  function add(obj, type, callback){
    // Add a object with a event type and callback to a weakmap

    typeErrors(type, callback);

    if (map.has(obj)) {
      if (map.get(obj)[type]) {
        map.get(obj)[type].push(callback);
        return;
      }
      map.get(obj)[type] = [callback];
      return;
    }
    map.set(obj, new EventsMap(type, callback));
  }

  function remove(obj, type, callback){
    // Removes the callback from the event queue and keep the memory clean
    // removing empty events queue and objects without events from the map

    typeErrors(type, callback);

    if (!map.has(obj)){
      return;
    }
    var eventsMap = map.get(obj);
    var eventQueue = eventsMap[type];
    if (!eventQueue){
      return;
    }
    var pos = eventQueue.indexOf(callback);
    if (pos === -1){
      return;
    }
    // Remove all matching callbacks
    while(pos !== -1){
      eventQueue.splice(pos, 1);
      pos = eventQueue.indexOf(callback);
    }

    // Remove the propertie with array when the array is empty
    if (eventQueue.length === 0){
      delete eventsMap[type];
    }

    // If the eventsMap isn't empty don't remove from the objects weakmap
    for (var prop in eventsMap)
    if (eventsMap.hasOwnProperty(prop)) {
      return;
    }
    map.delete(obj);
  }

  function dispatch(obj, event){
    // Dispatch a queue of events of the tye passed inside the event object

    // Check if the event is a valid object
    if (!event || typeof event.type !== 'string'){
      throw new TypeError('Illegal invocation');
    }

    // Init the event
    ObjectEvent.prototype.initEvent.call(event);

    var eventsMap = map.get(obj);
    if (!eventsMap) {
      return true;
    }
    var eventsQueue = eventsMap[event.type];
    if (!eventsQueue) {
      return true;
    }

    var eventPhase = ObjectEvent.prototype.AT_TARGET;

    // Bubbles support
    if (event.stack.length > 0) {
      if (!event.bubbles){
        return true;
      }
      if (event.cancelBubble){
        return true;
      }
      if (event.stack.indexOf(obj)!== -1){
        return true;
      }

      eventPhase = ObjectEvent.prototype.BUBBLING_PHASE;
    }

    // Add the obj instance to stack
    event.stack.push(obj);

    // Prevent forcing event new values after begin the callback queue calls
    var returnValue = true;
    var cancelable = !!event.cancelable;
    var type = event.type;
    var bubbles = event.bubbles;
    var cancelBubble = event.cancelBubble;

    var stack = event.stack;
    var target = stack[stack.length - 1];
    var currentTarget = stack[0];

    function resetEvent(event){
      // Ensure non-writetible event properties states
      event.eventPhase = eventPhase;
      event.cancelable = cancelable;
      event.returnValue = returnValue;
      event.defaultPrevented = !returnValue;
      event.type = type;
      event.stack = stack;
      event.target = target;
      event.currentTarget = currentTarget;
      event.bubbles = bubbles;
      event.cancelBubble = cancelBubble;
    }

    // Clone the array before iterate, avoid event changing the queue on fly
    eventsQueue = eventsQueue.slice();
    try{
      for (var i = 0, m = eventsQueue.length; i < m; i++) {
        resetEvent(event);

        // Call next event, using the object instance as context
        eventsQueue[i].call(obj, event);

        // Update the returnValue when default has been prevented
        returnValue = returnValue && !(cancelable && event.defaultPrevented);

        // Stop the immidiate propagation and return the returnValue
        if (event.immediatePropagationStopped) {
          return returnValue;
        }
      }
    }catch(e){
      setTimeout(function(){
        throw e;
      });
    }

    event.eventPhase = ObjectEvent.prototype.NONE;

    // true if default hasn't been prevented
    return returnValue;
  }

  function ObjectEventTarget(){
    // It's a singleton, once we have the instance for the prototype
    // the user should not be allowed to create new instances
    if (ObjectEventTarget.prototype.constructor === ObjectEventTarget){
      throw new TypeError('Illegal constructor');
    }

    function illegalIvovation(context){
      if (context === ObjectEventTarget.prototype) {
        throw new TypeError('Illegal invocation');
      }
    }

    this.constructor = ObjectEventTarget;

    this.addEventListener = function(type, callback){
      illegalIvovation(this);
      add(this, type, callback);
    };

    this.removeEventListener = function(type, callback){
      illegalIvovation(this);
      remove(this, type, callback);
    };

    this.dispatchEvent = function(event){
      illegalIvovation(this);
      return dispatch(this, event);
    };
  }

  ObjectEventTarget.prototype = Object.prototype;
  ObjectEventTarget.prototype = new ObjectEventTarget();

  function ObjectEvent(type, options){
    if (arguments.length === 0){
      throw new TypeError('Failed to construct \'ObjectEvent\': An event name must be provided.');
    }

    options = options || {};

    // Time that the event has created
    this.timeStamp = Date.now();

    // Allow preventDefault()
    this.cancelable = options.cancelable === true;

    // Allow bubbling()
    this.bubbles = options.bubbles === true;

    // Add custom details to the event
    if (typeof options.detail !== 'undefined') {
      this.detail = options.detail;
    }

    // Phase of the event
    this.eventPhase = ObjectEvent.prototype.NONE;

    this.stack = [];
    this.immediatePropagationStopped = false;
    this.cancelBubble = false;
    this.defaultPrevented = false;
    this.returnValue = true;
    this.type = String(type);
  }
  ObjectEvent.prototype.NONE = 0;
  ObjectEvent.prototype.AT_TARGET = 2;
  ObjectEvent.prototype.BUBBLING_PHASE = 3;
  ObjectEvent.prototype.initEvent = function() {
    // Init event if it has some propertie wrong, fix it

    // Time that the event has created
    this.timeStamp = this.timeStamp || Date.now();

    // Allow preventDefault()
    this.cancelable = this.cancelable === true;

    // Phase of the event
    this.eventPhase = ObjectEvent.prototype.NONE;

    // Cleanup the stack and add the instance to the event stack
    if (!this.stack || !this.stack.push) {
      // Opera and other browsers can throw exception if you are using native Event instance
      try{
        this.stack = [];
      }catch(e){}
    }

    this.immediatePropagationStopped = this.immediatePropagationStopped === true;
    this.cancelBubble = this.cancelBubble === true;
    this.defaultPrevented = this.cancelable && this.defaultPrevented === true;
    this.returnValue = true;
    this.type = String(this.type);

    // Add methods when they don't exist
    if (!(this instanceof ObjectEvent)){
      if (!this.hasOwnProperty('preventDefault')){
        var nativePreventDefault = this.preventDefault;
        if (typeof nativePreventDefault !== 'function'){
          nativePreventDefault = function(){};
        }
        this.preventDefault = function(){
          nativePreventDefault.apply(this, arguments);
          ObjectEvent.prototype.preventDefault.apply(this, arguments);
        };
      }
      if (!this.hasOwnProperty('stopPropagation')){
        var nativeStopPropagation = this.stopPropagation;
        if (typeof stopPropagation !== 'function'){
          nativeStopPropagation = function(){};
        }
        this.stopPropagation = function(){
          nativeStopPropagation.apply(this, arguments);
          ObjectEvent.prototype.stopPropagation.apply(this, arguments);
        };
      }
      if (!this.hasOwnProperty('stopImmediatePropagation')){
        var nativeStopImmediatePropagation = this.stopImmediatePropagation;
        if (typeof stopImmediatePropagation !== 'function'){
          nativeStopImmediatePropagation = function(){};
        }
        this.stopImmediatePropagation = function(){
          nativeStopImmediatePropagation.apply(this, arguments);
          ObjectEvent.prototype.stopImmediatePropagation.apply(this, arguments);
        };
      }
    }
  };
  ObjectEvent.prototype.preventDefault = function(){
    // Prevent the default result, makes the dispatchEvent returns false
    this.defaultPrevented = !!this.cancelable;
  };
  ObjectEvent.prototype.stopPropagation = function(){
    // Don't allow the next callback to be called
    this.cancelBubble = !!this.bubbles;
  };
  ObjectEvent.prototype.stopImmediatePropagation = function(){
    // Don't allow the next callback to be called
    this.immediatePropagationStopped = true;
  };

  // Prevent enumerable prototype when supported by the browser
  if (Object.defineProperties){
    var definePropertiesArgs = function (prototype){
      var props = {};
      for (var k in prototype) {
        if (prototype.hasOwnProperty(k)){
          props[k] = {
            value: prototype[k],
            enumerable: false
          };
        }
      }
      return [prototype, props];
    };

    // ObjectEvent remove enumerable prototype
    Object.defineProperties.apply(Object, definePropertiesArgs(ObjectEvent.prototype));
    // ObjectEventTarget remove enumerable prototype
    Object.defineProperties.apply(Object, definePropertiesArgs(ObjectEventTarget.prototype));
  }

  // Expose the ObjectEventTarget to global
  ObjectEventTarget.VERSION = VERSION;
  root.ObjectEventTarget = ObjectEventTarget;
  root.ObjectEvent = ObjectEvent;

  ObjectEventTarget.DEBUG = DEBUG;
  if (DEBUG){
    ObjectEventTarget.__debug = {
      EventsMap: EventsMap,
      typeErrors: typeErrors,
      WeakMap: WeakMap,
      map: map
    };
  }

  // Export as module to nodejs
  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined'){
    module.exports = {
      ObjectEventTarget: ObjectEventTarget,
      ObjectEvent: ObjectEvent
    };
  }
})(this);