text: /**
   * @fileoverview Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
   * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
   * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
   * robust as jquery's, so there's probably some differences.
   */
  
  import * as Dom from './dom.js';
  import * as Guid from './guid.js';
  import window from 'global/window';
  import document from 'global/document';
  
  /**
   * Add an event listener to element
   * It stores the handler function in a separate cache object
   * and adds a generic handler to the element's event,
   * along with a unique id (guid) to the element.
   * @param  {Element|Object}   elem Element or object to bind listeners to
   * @param  {String|Array}   type Type of event to bind to.
   * @param  {Function} fn   Event listener.
   */
  export function on(elem, type, fn){
    if (Array.isArray(type)) {
      return _handleMultipleEvents(on, elem, type, fn);
    }
  
    let data = Dom.getElData(elem);
  
    // We need a place to store all our handler data
    if (!data.handlers) data.handlers = {};
  
    if (!data.handlers[type]) data.handlers[type] = [];
  
    if (!fn.guid) fn.guid = Guid.newGUID();
  
    data.handlers[type].push(fn);
  
    if (!data.dispatcher) {
      data.disabled = false;
  
      data.dispatcher = function (event, hash){
  
        if (data.disabled) return;
        event = fixEvent(event);
  
        var handlers = data.handlers[event.type];
  
        if (handlers) {
          // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
          var handlersCopy = handlers.slice(0);
  
          for (var m = 0, n = handlersCopy.length; m < n; m++) {
            if (event.isImmediatePropagationStopped()) {
              break;
            } else {
              handlersCopy[m].call(elem, event, hash);
            }
          }
        }
      };
    }
  
    if (data.handlers[type].length === 1) {
      if (elem.addEventListener) {
        elem.addEventListener(type, data.dispatcher, false);
      } else if (elem.attachEvent) {
        elem.attachEvent('on' + type, data.dispatcher);
      }
    }
  }
  
  /**
   * Removes event listeners from an element
   * @param  {Element|Object}   elem Object to remove listeners from
   * @param  {String|Array=}   type Type of listener to remove. Don't include to remove all events from element.
   * @param  {Function} fn   Specific listener to remove. Don't include to remove listeners for an event type.
   */
  export function off(elem, type, fn) {
    // Don't want to add a cache object through getElData if not needed
    if (!Dom.hasElData(elem)) return;
  
    let data = Dom.getElData(elem);
  
    // If no events exist, nothing to unbind
    if (!data.handlers) { return; }
  
    if (Array.isArray(type)) {
      return _handleMultipleEvents(off, elem, type, fn);
    }
  
    // Utility function
    var removeType = function(t){
       data.handlers[t] = [];
       _cleanUpEvents(elem,t);
    };
  
    // Are we removing all bound events?
    if (!type) {
      for (let t in data.handlers) removeType(t);
      return;
    }
  
    var handlers = data.handlers[type];
  
    // If no handlers exist, nothing to unbind
    if (!handlers) return;
  
    // If no listener was provided, remove all listeners for type
    if (!fn) {
      removeType(type);
      return;
    }
  
    // We're only removing a single handler
    if (fn.guid) {
      for (let n = 0; n < handlers.length; n++) {
        if (handlers[n].guid === fn.guid) {
          handlers.splice(n--, 1);
        }
      }
    }
  
    _cleanUpEvents(elem, type);
  }
  
  /**
   * Trigger an event for an element
   * @param  {Element|Object}      elem  Element to trigger an event on
   * @param  {Event|Object|String} event A string (the type) or an event object with a type attribute
   * @param  {Object} [hash] data hash to pass along with the event
   */
  export function trigger(elem, event, hash) {
    // Fetches element data and a reference to the parent (for bubbling).
    // Don't want to add a data object to cache for every parent,
    // so checking hasElData first.
    var elemData = (Dom.hasElData(elem)) ? Dom.getElData(elem) : {};
    var parent = elem.parentNode || elem.ownerDocument;
        // type = event.type || event,
        // handler;
  
    // If an event name was passed as a string, creates an event out of it
    if (typeof event === 'string') {
      event = { type:event, target:elem };
    }
    // Normalizes the event properties.
    event = fixEvent(event);
  
    // If the passed element has a dispatcher, executes the established handlers.
    if (elemData.dispatcher) {
      elemData.dispatcher.call(elem, event, hash);
    }
  
    // Unless explicitly stopped or the event does not bubble (e.g. media events)
      // recursively calls this function to bubble the event up the DOM.
      if (parent && !event.isPropagationStopped() && event.bubbles !== false) {
        trigger.call(null, parent, event, hash);
  
    // If at the top of the DOM, triggers the default action unless disabled.
    } else if (!parent && !event.defaultPrevented) {
      var targetData = Dom.getElData(event.target);
  
      // Checks if the target has a default action for this event.
      if (event.target[event.type]) {
        // Temporarily disables event dispatching on the target as we have already executed the handler.
        targetData.disabled = true;
        // Executes the default action.
        if (typeof event.target[event.type] === 'function') {
          event.target[event.type]();
        }
        // Re-enables event dispatching.
        targetData.disabled = false;
      }
    }
  
    // Inform the triggerer if the default was prevented by returning false
    return !event.defaultPrevented;
  }
  
  /**
   * Trigger a listener only once for an event
   * @param  {Element|Object}   elem Element or object to
   * @param  {String|Array}   type
   * @param  {Function} fn
   */
  export function one(elem, type, fn) {
    if (Array.isArray(type)) {
      return _handleMultipleEvents(one, elem, type, fn);
    }
    var func = function(){
      off(elem, type, func);
      fn.apply(this, arguments);
    };
    // copy the guid to the new function so it can removed using the original function's ID
    func.guid = fn.guid = fn.guid || Guid.newGUID();
    on(elem, type, func);
  }
  
  /**
   * Fix a native event to have standard property values
   * @param  {Object} event Event object to fix
   * @return {Object}
   * @private
   */
  export function fixEvent(event) {
  
    function returnTrue() { return true; }
    function returnFalse() { return false; }
  
    // Test if fixing up is needed
    // Used to check if !event.stopPropagation instead of isPropagationStopped
    // But native events return true for stopPropagation, but don't have
    // other expected methods like isPropagationStopped. Seems to be a problem
    // with the Javascript Ninja code. So we're just overriding all events now.
    if (!event || !event.isPropagationStopped) {
      var old = event || window.event;
  
      event = {};
      // Clone the old object so that we can modify the values event = {};
      // IE8 Doesn't like when you mess with native event properties
      // Firefox returns false for event.hasOwnProperty('type') and other props
      //  which makes copying more difficult.
      // TODO: Probably best to create a whitelist of event props
      for (var key in old) {
        // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
        // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
        if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation') {
          // Chrome 32+ warns if you try to copy deprecated returnValue, but
          // we still want to if preventDefault isn't supported (IE8).
          if (!(key === 'returnValue' && old.preventDefault)) {
            event[key] = old[key];
          }
        }
      }
  
      // The event occurred on this element
      if (!event.target) {
        event.target = event.srcElement || document;
      }
  
      // Handle which other element the event is related to
      event.relatedTarget = event.fromElement === event.target ?
        event.toElement :
        event.fromElement;
  
      // Stop the default browser action
      event.preventDefault = function () {
        if (old.preventDefault) {
          old.preventDefault();
        }
        event.returnValue = false;
        event.defaultPrevented = true;
      };
  
      event.defaultPrevented = false;
  
      // Stop the event from bubbling
      event.stopPropagation = function () {
        if (old.stopPropagation) {
          old.stopPropagation();
        }
        event.cancelBubble = true;
        event.isPropagationStopped = returnTrue;
      };
  
      event.isPropagationStopped = returnFalse;
  
      // Stop the event from bubbling and executing other handlers
      event.stopImmediatePropagation = function () {
        if (old.stopImmediatePropagation) {
          old.stopImmediatePropagation();
        }
        event.isImmediatePropagationStopped = returnTrue;
        event.stopPropagation();
      };
  
      event.isImmediatePropagationStopped = returnFalse;
  
      // Handle mouse position
      if (event.clientX != null) {
        var doc = document.documentElement, body = document.body;
  
        event.pageX = event.clientX +
          (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
          (doc && doc.clientLeft || body && body.clientLeft || 0);
        event.pageY = event.clientY +
          (doc && doc.scrollTop || body && body.scrollTop || 0) -
          (doc && doc.clientTop || body && body.clientTop || 0);
      }
  
      // Handle key presses
      event.which = event.charCode || event.keyCode;
  
      // Fix button for mouse clicks:
      // 0 == left; 1 == middle; 2 == right
      if (event.button != null) {
        event.button = (event.button & 1 ? 0 :
          (event.button & 4 ? 1 :
            (event.button & 2 ? 2 : 0)));
      }
    }
  
    // Returns fixed-up instance
    return event;
  }
  
  /**
   * Clean up the listener cache and dispatchers
   * @param  {Element|Object} elem Element to clean up
   * @param  {String} type Type of event to clean up
   * @private
   */
  function _cleanUpEvents(elem, type) {
    var data = Dom.getElData(elem);
  
    // Remove the events of a particular type if there are none left
    if (data.handlers[type].length === 0) {
      delete data.handlers[type];
      // data.handlers[type] = null;
      // Setting to null was causing an error with data.handlers
  
      // Remove the meta-handler from the element
      if (elem.removeEventListener) {
        elem.removeEventListener(type, data.dispatcher, false);
      } else if (elem.detachEvent) {
        elem.detachEvent('on' + type, data.dispatcher);
      }
    }
  
    // Remove the events object if there are no types left
    if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
      delete data.handlers;
      delete data.dispatcher;
      delete data.disabled;
    }
  
    // Finally remove the element data if there is no data left
    if (Object.getOwnPropertyNames(data).length === 0) {
      Dom.removeElData(elem);
    }
  }
  
  /**
   * Loops through an array of event types and calls the requested method for each type.
   * @param  {Function} fn   The event method we want to use.
   * @param  {Element|Object} elem Element or object to bind listeners to
   * @param  {String}   type Type of event to bind to.
   * @param  {Function} callback   Event listener.
   * @private
   */
  function _handleMultipleEvents(fn, elem, types, callback) {
    types.forEach(function(type) {
      //Call the event method for each one of the types
      fn(elem, type, callback);
    });
  }
  