Cross-Browser Event Handling Using Plain ole JavaScript

Juriy Zaytsev | June 7, 2010

 

Cross browser event handling is not a trivial task. Fortunately, most of the popular Javascript libraries abstract this process away. But what's hidden behind the scenes is a wonderful bouquet of cross-browser inconsistencies, together with workarounds needed to get event handling to work. Let's take a look at what it takes to create a robust event handling abstraction. You might find this overview useful when building your own event-handling utilities, when assessing/optimizing existing ones, or just for educational purposes.

Quite naturally, the cornerstones of event handling are functions for adding and removing event listeners to/from elements. These functions are exactly what we'll be covering today. We will not touch upon abstraction for firing events, as that subject is a bit too involved to cover properly in this article.

DOM Level 2 defines addEventListener and removeEventListener methods as part of EventTarget interface.

element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);

EventTarget interface is usually implemented on objects implementing Node or Window interfaces. What this means is that DOM elements, as well as window objects, receive addEventListener/removeEventListener methods through which event listeners can be added or removed from the corresponding objects:

window.addEventListener('load', function (event) {  
  console.log('window\'s load event fired');
}, false);


// or


function bodyHandler() {
  console.log('click event was fired on body element');
}
document.body.addEventListener('click', bodyHandler, false);

removeEventListener is pretty straight-forward as well (note that the event handler should reference the same function as the one used when adding listener to an element — bodyHandler in this example):

document.body.removeEventListener('click', bodyHandler, false);

If we lived in a perfect world, these two functions would be all that's needed to get event handling to work. Alas, that's not the case, and a bit more work is needed to get to a finish line. The main obstacle on our way is a non-standard event-handling model used in Internet Explorer. Instead of addEventListener/removeEventListener, MSHTML DOM (Document Object Model used in Internet Explorer) defines attachEvent and detachEvent methods, accordingly. Both of these methods accept an eventName string as a first argument, and an event handler function object as a second argument.

window.attachEvent('onload', function () {
  console.log('window\'s load event fired');
});


function bodyHandler() {
  console.log('click event was fired on body element');
}
document.body.attachEvent('onclick', bodyHandler);
document.body.detachEvent('onclick', bodyHandler);

As you can see, attachEvent and detachEvent slightly differ from their standard counterparts. The name of event is always prepended with "on" while the third argument, useCapture, is missing. There are other deviations, which we can't see immediately, but which we'll be taking a closer look at later on.

Armed with this knowledge, it should now be trivial to create an abstraction of these two different models. Let's create the following wrapping methods, addListener and removeListener:

function addListener(element, eventName, handler) {
  if (element.addEventListener) {
    element.addEventListener(eventName, handler, false);
  }
  else if (element.attachEvent) {
    element.attachEvent('on' + eventName, handler);
  }
  else {
    element['on' + eventName] = handler;
  }
}


function removeListener(element, eventName, handler) {
  if (element.addEventListener) {
    element.removeEventListener(eventName, handler, false);
  }
  else if (element.detachEvent) {
    element.detachEvent('on' + eventName, handler);
  }
  else {
    element['on' + eventName] = null;
  }
}

To ensure future compatibility and interoperability, it's important to always first test for existence of standard method (e.g. addEventListener), and only then try proprietary ones (e.g. attachEvent).

Note that besides addEventListener and attachEvent, there's also a third branch, for when an element is missing both addEventListener and attachEvent methods. In such case, we fall back on non-standard event-handler property assignment.

Optimizing Performance

Notice how every time addListener/removeListener are evaluated, there's a code branching to be done. Methods are accessed on elements, and further actions are determined based on their existence. We can eliminate all this unnecessary work by defining different functions at "load time", not at "run time". For example, when declaring addListener, we can check for the existence of addEventListener or attachEvent on some DOM element that we have an access to at that time. One such elements is the root element in a document, which is represented as <html>...</html> in markup and is accessible via document.documentElement in the DOM. Contrary to the body element (accessible via document.body in DOM), document.documentElement exists even when document is not "ready". This obviously makes it perfect for tests that are performed before the page has finished loading. Another option for such a "test element" is any element created dynamically, such as document.createElement('div').

Let's take a look at how load-time branching can be done:

/* first, declare variables to assign functions to */
var addListener, removeListener,


    /* test element */
    docEl = document.documentElement;


if (docEl.addEventListener) {


  /* if `addEventListener` exists on test element, define function to use `addEventListener` */
  addListener = function (element, eventName, handler) {
    element.addEventListener(eventName, handler, false);
  };
}
else if (docEl.attachEvent) {


  /* if `attachEvent` exists on test element, define function to use `attachEvent` */
  addListener = function (element, eventName, handler) {
    element.attachEvent('on' + eventName, handler);
  };
}
else {


  /* if neither methods exists on test element, define function to fallback strategy */
  addListener = function (element, eventName, handler) {
    element['on' + eventName] = handler;
  };
}

The same goes for removeListener; the function is defined at load time, based on presence of methods in question.

if (docEl.removeEventListener) {
  removeListener = function (element, eventName, handler) {
    element.removeEventListener(eventName, handler, false);
  };
}
else if (docEl.detachEvent) {
  removeListener = function (element, eventName, handler) {
    element.detachEvent('on' + eventName, handler);
  };
}
else {
  removeListener = function (element, eventName, handler) {
    element['on' + eventName] = null;
  };
}

It's worth mentioning that such optimization has certain downsides. Besides increased code size, there's now a weaker, unrelated inference being used. What this means is that instead of checking for the existence of a method directly on a target element, we test for the existence of a method on a test element and assume that the method exists on the target element as well. As with any assumption, there's a risk of failure. Theoretically, document.documentElement can implement addEventListener, but another element, the one passed to addListener, will not have it. Practice shows that when it comes to addEventListener/removeEventListener, such inference is generally safe, as long as we test for the method on same type of object, for example, element object and not window or document.

Hardening inference

Speaking of unrelated inference, there's one little thing we can do to lower the chance of failure. We can test for the existence of a method on the window object as well. After all, addListener will often be passed window object (to listen to events like "load", "resize", "scroll", etc.):

var addListener, docEl = document.documentElement;

if (docEl.addEventListener && window.addEventListener) {
  addListener = function (element, eventName, handler) {
    /* ... */
  };
}
else if (docEl.attachEvent && window.attachEvent) {
  addListener = function (element, eventName, handler) {
    /* ... */
  };
}
else {
  /* ... */
}

Implementations that lack addEventListener or attachEvent on window objects will not jeopardize addListener anymore. From the existence of the method on document.documentElement we assume that the method is present on all objects implementing Node interface (such as DOM elements that implement Element interface, or documents that implement Document interface). And from the existence of the method on window, we assume that all window objects have this method.

Fail-safe feature testing

The way we've been testing methods on DOM elements (and other host objects, such as window) is by performing boolean type-conversion on them. Boolean type-conversion is what happens to a value when it's evaluated as expression in an if statement (e.g.: if (docEl.addEventListener) { ... }). The problem with such conversion is that it's known to blow up in some implementations:

// In Internet Explorer


var xhr = new ActiveXObject('Microsoft.XMLHTTP');
if (xhr.open) { } // Error


var element = document.createElement('p');
if (element.offsetParent) { } // Error

In Internet Explorer, certain host objects are implemented as ActiveX objects, and their type-conversion results in errors. What's interesting is that the type of such objects (as returned by typeof operator) is usually "unknown". If you think this is a weird type, that's because it is. But as per specification (ECMA-262, 3rd ed.), the typeof operator can return any kind of value when given a host object (e.g: "foo", "bar", "number", "undefined" or even an empty string).

So how do we deal with these quirky objects? Since we don't know exactly which ones they are, the safest strategy is to avoid type-conversion of any host object at all. Instead, we can infer method existence or callability by inspecting its type (as returned by typeof operator). If type is "object" or "function", we assume that object is callable. If the type is "unknown", we assume that the object is also callable, it's just represented as an ActiveX object but could still be called.

To abstract all this type checking, some libraries use so-called isHostMethod (popularized by David Mark, now used in FuseJS and My Library):

var isHostMethod = function (object, methodName) {
  var t = typeof object[methodName];
  return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};

We can now replace all host objects' type-conversion -based tests with safer, isHostMethod -based ones:

var addListener, docEl = document.documentElement;


if (isHostMethod(docEl, 'addEventListener')) {
  /* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
  /* ... */
}
else {
  /* ... */
}

Implementations in which addEventListener or attachEvent methods of document.documentElement are "quirky" are now handled without errors.

Normalizing event handler in IE

I mentioned before that the MSHTML event model deviates from the standard one in few other areas. One such difference is in what this references during event handler invocation. The handler initialized via addEventListener is always invoked in the context of the element that the listener was attached to. In the case of document.body.addEventListener(...), the event handler is called in the context of document.body. In case of window.addEventListener('...'), the event handler is called in the context of window object, etc.

attachEvent, however, behaves differently and always invokes the event handler in context of windowobject:

document.body.attachEvent('onclick', function () {
  console.log(this === window); // true
  console.log(this === document.body); // false
});

As a result, our addListener, as it stands now, is inconsistent with regards to the context of the event handler. If you're planning to use this in it, it's a good idea to eliminate this dangerous inconsistency. Let's see how this can be done:

if (isHostMethod(docEl, 'addEventListener')) {
  /* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
  addListener = function (element, eventName, handler) {
    element.attachEvent('on' + eventName, function () {
      handler.call(element, window.event);
    });
  };
}
else {
  /* ... */
}

Instead of passing the event handler directly to attachEvent, we pass it a wrapping function which in turn invokes the event handler in context of an element. Now this always references an element that the listener was attached to.

addListener(document.body, function () {
  console.log(this === window); // false
  console.log(this === document.body); // true
});

You might have noticed that not only is the event handler invoked in the context of an element, but it's also being passed a window.event object as a first argument — handler.call(element, window.event). This is done to work around another difference in MSHTML event model; lack of an event object as first argument of the event handler. Whereas addEventListener ensures the event handler is being given an event object, Internet Explorer makes the event object accessible via the global window.event property. Nothing is passed to the event handler.

By explicitly passing window.event to the event handler via handler.call(element, window.event), we make sure it always has the proper event object accessible as the first argument.

addListener(document.body, function (event) {
  console.log(typeof event != 'undefined'); // true
});

Cleaning up memory leaks in IE

While the previous solution solves the problem of thisand the eventobject, it introduces another, rather sneaky issue of memory leaks. The annoyance with memory leaks in Internet Explorer has been described at great length in other places, so we won't repeat it here. However, let's take a quick look at what exactly causes the leak in this particular case:

...
addListener = function (element, eventName, handler) {
  element.attachEvent('on' + eventName, function () {
    handler.call(element, window.event);
  });
};
...

When addListener function is executed, a circular reference is formed. An element has a reference to an event handler, and the event handler has a reference to an element through its scope chain. The following pattern is a classical memory leak trigger and is something we need to take care of.

So how do we fix the leak? Well, one of the easiest ways is to break these circular references on page unload. The idea is simple: when the window's "unload" event fires, iterate over all existing event listeners and clean them up. By clean-up, we mean detaching event and nullifying its reference. This effectively breaks a circular reference.

Let's take a look at one possible implementation of such system:

function wrapHandler(element, handler) {
  return function (e) {
    return handler.call(element, e || window.event);
  };
}


function createListener(element, eventName, handler) {
  return {
    element: element,
    eventName: eventName,
    handler: wrapHandler(element, handler)
  };
}


function cleanupListeners() {
  for (var i = listenersToCleanup.length; i--; ) {
    var listener = listenersToCleanup[i];
    litener.element.detachEvent(listener.eventName, listener.handler);
    listenersToCleanup[i] = null;
  }
  window.detachEvent('onunload', cleanupListeners);
}


var listenersToCleanup = [ ];


if (isHostMethod(docEl, 'addEventListener')) {
  /* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
  addListener = function (element, eventName, handler) {


    var listener = createListener(element, eventName, handler);
    element.attachEvent('on' + eventName, listener.handler);
    listenersToCleanup.push(listener);
  };


  window.attachEvent('onunload', cleanupListeners);
}
else {
  /* ... */
}

During declaration of addListener, we also attach an "unload" listener to window. That method goes over all listeners stored in the listenersToCleanup array, and invokes the detachEvent on each one of them, followed by nullifying of a reference.

During execution of addListener, we create a listener abstraction, an object which encapsulates element, eventName, and normalized handler in one place, and push that listener into a listenersToCleanup array for later cleanup.

Avoiding memory leaks in IE

The cleanup procedure we wrote in previous section is great at keeping memory leaks at bay. However, another sneaky problem is rearing its ugly head — presence of “unload” listener kills page cache — an amazing feature implemented in majority of modern browsers. Also known as bfcache, page cache is a feature that provides immediate page display when navigating via back/forward buttons, all without sending request to a server. Unfortunately, page cache is being disabled as soon as an "unload" listener is added to a window. The reason page cache is disabled in cases like that is due to potential document modifications performed from within an unload handler. It is no longer safe to display a cached version. Instead, the document has to be re-requested from a server.

So we can't use the unload listener since it disables page cache. And we don't want to leave circular references either since they leak memory. Then how to take care of this tricky problem?

The solution is surprisingly trivial; do not create circular references in the first place!

The reason we needed to clean-up in the previous snippet is due to a somewhat hard-to-find circular reference. Let's try to find that sneaky code:

addListener = function (element, eventName, handler) {
  ...
  element.attachEvent('on' + eventName, listener.handler);
  ...
};
function wrapHandler(element, handler) {
  return function (e) {
    return handler.call(element, e || window.event);
  };
}
function createListener(element, eventName, handler) {
  return {
    ...
    handler: wrapHandler(element, handler)
  };
}

Note how addListener attaches listener.handler to an element via attachEvent. listener.handler in turn, is created via wrapHandler(element, handler). If we look at wrapHandler, it becomes clear that returned function (the one that becomes an event handler) closes over an element in question. Once again, an element has a reference to an event handler and the event handler has a reference to an element. There is a circular reference in all its glory.

So how is it possible to avoid creation of circular references? Let's take a look at one possible implementation.

The goal of the following snippet is to avoid referencing an element from within an event handler. This is the key to eliminating those nasty circular references. Instead of passing an element to a handler maker, we pass it a unique id by which we can then retrieve an element. And how does an element get associated with id in the first place? By being assigned a unique id in the beginning of addListener.

So to sum it up, the main steps of this implementation of addListener are:

  1. Get unique id of an element
  2. Create a listener that holds a wrapped (normalized) handler. The wrapped handler never receives an actual element, to avoid creation of circular reference.
  3. The newly created listener is associated with the element's unique id and event type.
  4. A wrapped (normalized) handler is attached to an element (via attachEvent).

Circular reference is successfully avoided.

var getUniqueId = (function () {
  if (typeof document.documentElement.uniqueID != 'undefined') {
    return function (element) {
      return element.uniqueID;
    };
  }
  var uid = 0;
  return function (element) {
    return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
  };
})();


var getElement, setElement, listeners = { };


(function () {
  var elements = { };
  getElement = function (uid) {
    return elements[uid];
  };
  setElement = function (uid, element) {
    elements[uid] = element;
  };
})();


function createListener(uid, handler) {
  return {
    handler: handler,
    wrappedHandler: createWrappedHandler(uid, handler)
  };
}


function createWrappedHandler(uid, handler) {
  return function (e) {
    handler.call(getElement(uid), e || window.event);
  };
}


var addListener = function (element, eventName, handler) { 
  var uid = getUniqueId(element);
  setElement(uid, element);


  if (!listeners[uid]) {
    listeners[uid] = { };
  }
  if (!listeners[uid][eventName]) {
    listeners[uid][eventName] = [ ];
  }
  var listener = createListener(uid, handler);
  listeners[uid][eventName].push(listener);
  element.attachEvent('on' + eventName, listener.wrappedHandler);
};


var removeListener = function (element, eventName, handler) {
  var uid = getUniqueId(element), listener;
  if (listeners[uid] && listeners[uid][eventName]) {
    for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
      listener = listeners[uid][eventName][i];
      if (listener && listener.handler === handler) {
        element.detachEvent('on' + eventName, listener.wrappedHandler);
        listeners[uid][eventName][i] = null;
      }
    }
  }        
};

At the moment of this writing, only very few Javascript libraries employ event handling abstractions that do not assign unload listener. FuseJS is one of them. Others work around this problem by assigning an unload listener only "in IE", where "in IE" is determined either by sniffing userAgent string or based on some kind of inference test (jQuery 1.4.2, for example, assigns unload only when window.attachEvent is present, and window.addEventListener is not). Object inference (such as the one used in jQuery) is a safer choice compared to the fragile and unpredictable userAgent string. But as any other inference, it has a chance to produce false positives. Implementations that avoid attaching unload listener in the first place are safe from any potential "failures".

Fixing DOM L0 branch

We have normalized this and event in IE branch and did it in such way as to avoid creating memory leaks. Not only did we avoid memory leaks, but also left the page cache mechanism intact. We used robust feature testing and strong inference. We optimized performance by forking methods at load time. What else is left?

Unfortunately, the third branch of our implementation, the one that serves as a fallback for when neither addEventListener nor attachEvent are available, is somewhat defunct as of now.

If you look at it closely, the problem should become obvious.

var addListener;
if (isHostMethod(docEl, 'addEventListener')) {
  /* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
  /* ... */
}
else {
  addListener = function (element, eventName, handler) {
    element['on' + eventName] = handler;
  };
}

Every time addListener is executed, a new handler overwrites an existing one! Moreover, removeListener, as it stands now, completely removes the only existing handler of an element! This is, of course, absolutely inconsistent with the rest of the implementation. We need to either remove third branch, or fix it.

Let's first take a look at how we could go about removing third branch. Some of the older browsers (e.g. Netscape Navigator 4, Opera 6) lack both addEventListener and attachEvent, so for those browsers the third branch is the only option. However, since those browsers are most likely extinct by now, more relevant are mobile browsers, which are known to lack even basic DOM methods. If we don't want to make the third branch functional, we should at least prevent failures and notify user of this deficiency in the implementation.

var addListener;
...
else {
  addListener = function () {
    /* noop */
  };
  addListener.defunct = true;
}

Note how we still defineaddListenerfunction but make it essentially a no-op. We mark it as "defunct" by assigning "defunct" property with the value of true. Client code can now find if addListener is functional or not. If method is defunct, the application can degrade gracefully.

if (!addListener.defunct) {
  /* 
    Tab panel is only initialized when `addListener` is functional, 
    so that "click" event can be handled properly
    Otherwise, original, "unscripted" representation 
    (e.g. list of sections) is displayed.


    This prevents us from initializing a tab panel 
    that can't handle click events, and so appears to be "broken"
  */
  tabPanel.initialize();
  addListener(tabPanel, 'click', handleTabClick);
}

If, on the other hand, the goal is to support browsers without addEventListener / attachEvent, it's a good idea to make DOM L0 -based implementation consistent.

Before we look at one such implementation, here's a summary of the course of events. When addListener is called, a unique id is queried for an element. We use same helper as the one from the attachEvent branch. Next, this id is associated with an object to hold all handlers for this element by type. Next, an event handler is added to a queue of all event handlers for this particular element and this particular type. Finally, the existing event handler is replaced with a dispatcher, whose job is to iterate over all event handlers for this particular element/type combination and execute each one of them in proper context (element) and with proper arguments (event).

...
else {


  var createDispatcher = function (uid, eventName) {
    return function (e) {
      if (handlers[uid] && handlers[uid][eventName]) {
        var handlersForEvent = handlers[uid][eventName];
        for (var i = 0, len = handlersForEvent.length; i < len; i++) {
          handlersForEvent[i].call(this, e || window.event);
        }
      }
    };
  };

  var handlers = { };


  addListener = function (element, eventName, handler) {
    var uid = getUniqueId(element);
    if (!handlers[uid]) {
      handlers[uid] = { };
    }
    if (!handlers[uid][eventName]) {
      handlers[uid][eventName] = [ ];
      var existingHandler = element['on' + eventName];
      if (existingHandler) {
        handlers[uid][eventName].push(existingHandler);
      }
      element['on' + eventName] = createDispatcher(uid, eventName);
    }
    handlers[uid][eventName].push(handler);
  };


  removeListener = function (element, eventName, handler) {
    var uid = getUniqueId(element);
    if (handlers[uid] && handlers[uid][eventName]) {
      var handlersForEvent = handlers[uid][eventName];
      for (var i = 0, len = handlersForEvent.length; i < len; i++) {
        if (handlersForEvent[i] === handler) {
          handlersForEvent.splice(i, 1);
        }
      }
    }
  };
}

DOM L0 branch is now consistent with the rest of an implementation.

What wasn't taken care of.

Even though the final implementation turned out to be quite robust and complete, there are few things we haven't taken care of. One of them is support for the capturing phase in Internet Explorer. Lack of support for the capturing phase in the MSHTML event model is the reason addListener doesn't allow us to specify the useCapture argument (even though the standard addEventListener can handle it).

Another inconsistency is the order of event handler execution in Internet Explorer. While DOM L2 Events module doesn't specify in which order event handlers are to be fired, most of the browsers follow FIFO (First In, First Out) order, not LIFO (Last In, First Out) as it is in MSHTML event model. The DOM Level 3 Events module (currently draft) actually specifies the order as FIFO, saying that "all event listeners that have been registered on the current target in their order of registration"

We also haven't made the dispatcher in DOM L0 branch foolproof against errors in standalone handlers. Generally, it's a good idea to ensure none of the event handlers is affected by any failures in other handlers.

When retrieving a unique ID for an element, we didn't take care of cases when element is actually not an element, but something like a window object.

We can take a look at solving all of these "issues" next time. For now, they are left as an exercise to a reader. Finally, here's a complete implementation of addListener / removeListener at only about ~150 lines of code.

Final implementation

(function(global){


  function areHostMethods(object) {
    var methodNames = Array.prototype.slice.call(arguments, 1), 
        t, i, len = methodNames.length;


    for (i = 0; i < len; i++) {
      t = typeof object[methodNames[i]];
      if (!(/^(?:function|object|unknown)$/).test(t)) return false;
    }
    return true;
  }


  var getUniqueId = (function () {
    if (typeof document.documentElement.uniqueID !== 'undefined') {
      return function (element) {
        return element.uniqueID;
      };
    }
    var uid = 0;
    return function (element) {
      return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
    };
  })();


  var getElement, setElement;
  (function () {
    var elements = { };
    getElement = function (uid) {
      return elements[uid];
    };
    setElement = function (uid, element) {
      elements[uid] = element;
    };
  })();


  function createListener(uid, handler) {
    return {
      handler: handler,
      wrappedHandler: createWrappedHandler(uid, handler)
    };
  }


  function createWrappedHandler(uid, handler) {
    return function (e) {
      handler.call(getElement(uid), e || window.event);
    };
  }

  function createDispatcher(uid, eventName) {
    return function (e) {
      if (handlers[uid] && handlers[uid][eventName]) {
        var handlersForEvent = handlers[uid][eventName];
        for (var i = 0, len = handlersForEvent.length; i < len; i++) {
          handlersForEvent[i].call(this, e || window.event);
        }
      }
    };
  }


  var addListener, removeListener,

      shouldUseAddListenerRemoveListener = (
        areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&``
        areHostMethods(window, 'addEventListener', 'removeEventListener')),


      shouldUseAttachEventDetachEvent = (
        areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
        areHostMethods(window, 'attachEvent', 'detachEvent')),


      // IE branch
      listeners = { },


      // DOM L0 branch
      handlers = { };


  if (shouldUseAddListenerRemoveListener) {


    addListener = function (element, eventName, handler) {
      element.addEventListener(eventName, handler, false);
    };


    removeListener = function (element, eventName, handler) {
      element.removeEventListener(eventName, handler, false);
    };


  }
  else if (shouldUseAttachEventDetachEvent) {


    addListener = function (element, eventName, handler) { 
      var uid = getUniqueId(element);
      setElement(uid, element);


      if (!listeners[uid]) {
        listeners[uid] = { };
      }
      if (!listeners[uid][eventName]) {
        listeners[uid][eventName] = [ ];
      }
      var listener = createListener(uid, handler);
      listeners[uid][eventName].push(listener);
      element.attachEvent('on' + eventName, listener.wrappedHandler);
    };


    removeListener = function (element, eventName, handler) {
      var uid = getUniqueId(element), listener;
      if (listeners[uid] && listeners[uid][eventName]) {
        for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
          listener = listeners[uid][eventName][i];
          if (listener && listener.handler === handler) {
            element.detachEvent('on' + eventName, listener.wrappedHandler);
            listeners[uid][eventName][i] = null;
          }
        }
      }        
    };
  }
  else {


    addListener = function (element, eventName, handler) {
      var uid = getUniqueId(element);
      if (!handlers[uid]) {
        handlers[uid] = { };
      }
      if (!handlers[uid][eventName]) {
        handlers[uid][eventName] = [ ];
        var existingHandler = element['on' + eventName];
        if (existingHandler) {
          handlers[uid][eventName].push(existingHandler);
        }
        element['on' + eventName] = createDispatcher(uid, eventName);
      }
      handlers[uid][eventName].push(handler);
    };


    removeListener = function (element, eventName, handler) {
      var uid = getUniqueId(element);
      if (handlers[uid] && handlers[uid][eventName]) {
        var handlersForEvent = handlers[uid][eventName];
        for (var i = 0, len = handlersForEvent.length; i < len; i++) {
          if (handlersForEvent[i] === handler) {
            handlersForEvent.splice(i, 1);
          }
        }
      }
    };
  }


  /* export as global properties */
  global.addListener = addListener;
  global.removeListener = removeListener;


})(this);

About the Author

Juriy Zaytsev, otherwise known as "kangax", is a front end web developer based in New York. Most of his work involves exploring and taming various aspects of Javascript. He blogs about some of his findings at https://perfectionkills.com/. Juriy has been contributing to various projects ranging from libraries and frameworks, to articles and books.

Find Juriy on: