The Ultimate addEvent(..) function

Two articles in particular that I've written on this blog have garnered a lot of positive feedback.

  1. Fixing IE's attachEvent Failures
  2. mouseenter and mouseleave Events for Firefox (and other Non-IE Browsers)

As time has gone on, I've seen mention of these articles pop up in programming forums here and there, and they still receive comments from time to time, which lets me know that people are still getting something out of them.

Unfortunately, the code in them is a tad dated. I've learned more and improved my code since I originally wrote them, and while these improvements have made their way into my Javascript Library, I know that the majority of people are just grabbing the code directly from the blog entries.

So I've decided to post an update: a sort of fusion between these two blog entries which offers functionality for addressing all of the issues discussed in the aforementioned entries (and then some).

I consider this new function the "Ultimate addEvent Function" because I really believe it addresses the large majority of cross-browser issues that people face when dealing with events in Javascript. And furthermore, this function goes beyond anything that any other Javascript library provides (at least as far as I know).

So without further ado, I offer to you:

sstchur's Ultimate addEvent(..) function!

var xb = {};
(function()
{
   var GUIDCounter = 0;
   var evtHash = {};
   var pseudoEvents =
   {
      'mouseenter': function(_fn, _useCapture, _listening)
      {
         var f = mouseEnter(_fn);
         _listening ?
            xb.addEvent(this, 'mouseover', f, _useCapture, false) :
            xb.removeEvent(this, 'mouseover', f, _useCapture, false);
         f = null;      
      },
      
      'mouseleave': function(_fn, _useCapture, _listening)
      {
         var f = mouseEnter(_fn);
         _listening ?
            xb.addEvent(this, 'mouseout', f, _useCapture, false) :
            xb.removeEvent(this, 'mouseout', f, _useCapture, false);
         f = null;      
      }
   };
   
   xb.Helper =
   {
      getObjectGUID: getObjectGUID,
      storeHandler: storeHandler,
      retrieveHandler: retrieveHandler,
      isAnAncestorOf: isAnAncestorOf,
      mouseEnter: mouseEnter
   }
   
   xb.addEvent = function()
   {
      if (typeof document.addEventListener !== 'undefined')
      {
         return w3c_addEvent;
      }
      else if (typeof document.attachEvent !== 'undefined')
      {
         return ie_addEvent;
      }
      else
      {
         // no modern event support I guess 🙁
         // (you could use DOM 0 here if you really wanted to)
         return function() {};
      }
      
      function w3c_addEvent(_elem, _evtName, _fn, _useCapture, _directCall)
      {
         var eventFn = pseudoEvents[_evtName];
                  
         if (typeof eventFn === 'function' && _directCall !== false)
         {
            eventFn.call(_elem, _fn, _useCapture, true);
         }
         else
         {
            _elem.addEventListener(_evtName, _fn, _useCapture);
         }
      }
      
      function ie_addEvent(_elem, _evtName, _fn, _useCapture, _directCall)
      {
         var eventFn = pseudoEvents[_evtName];
               
         if (typeof eventFn === 'function' && _directCall !== false)
         {
            eventFn.call(_elem, _fn, _useCapture, true);
         }
         else
         {
            // create a key to identify this element/event/function combination
            var key = generateHandlerKey(_elem, _evtName, _fn);
            
            // if this element/event/combo has already been wired up, just return
            var f = evtHash[key];
            if (typeof f !== 'undefined')
            {
               return;
            }
            
            // create a helper function to fix IE's lack of standards support
            f = function(e)
            {
               // map .target to .srcElement
               e.target = e.srcElement;
               
               // map .relatedTarget to either .toElement or .fromElement
               if (_evtName == 'mouseover') { e.relatedTarget = e.fromElement; }
               else if (_evtName == 'mouseout') { e.relatedTarget = e.toElement; }
                  
               e.preventDefault = function() { e.returnValue = false; };
               e.stopPropagation = function() { e.cancelBubble = true; };

               // call the actual function, using entity (the element) as the 'this' object
               _fn.call(_elem, e);
               
               // null out these properties to prevent memory leaks
               e.target = null;
               e.relatedTarget = null;
               e.preventDefault = null;
               e.stopPropagation = null;
               e = null;
            };
            
            // add the helper function to the event hash
            evtHash[key] = f;

            // hook up the event (IE style)
            _elem.attachEvent('on' + _evtName, f);
            
            key = null;
            f = null;
         }
      }
   }();

   xb.defineEvent = function(_evtName, _logicFn)
   {
      pseudoEvents[_evtName] = _logicFn;
   };

   
   // Helper Functions
   function storeHandler(_key, _handler)
   {
      evtHash[_key] = _handler;
   }
   
   function retrieveHandler(_key)
   {
      return evtHash[_key];
   }
   
   function generateHandlerKey(_elem, _evtName, _handler)
   {
      return '{' + getObjectGUID(_elem) + '/' + _evtName + '/' + getObjectGUID(_handler) + '}'
   }
   
   function isAnAncestorOf(_ancestor, _descendant, _generation)
   {
      if (_ancestor === _descendant) { return false; }
      
      var gen = 0;
      while (_descendant && _descendant != _ancestor)
      {
         gen++;
         _descendant = _descendant.parentNode;
      }
      
      _generation = _generation || gen;
      return _descendant === _ancestor && _generation === gen;    
   }
   
   function mouseEnter(_fn)
   { 
      var key = xb.Helper.getObjectGUID(_fn);
      var f = evtHash[key];
      if (typeof f === 'undefined')
      {
         f = evtHash[key] = function(_evt)
         {
            var relTarget = _evt.relatedTarget;
            if (this === relTarget || isAnAncestorOf(this, relTarget)) { return; }
      
            _fn.call(this, _evt);
         };
      }
      return f;   
   }
   
   function getObjectGUID(_elem)
   {
      if (_elem === window)
      {
         return 'theWindow';
      }
      else if (_elem === document)
      {
         return 'theDocument';
      }
      else if (typeof _elem.uniqueID !== 'undefined')
      {
         return _elem.uniqueID;
      }

      var ex = '__$$GUID$$__';
      if (typeof _elem[ex] === 'undefined')
      {
         _elem[ex] = ex + GUIDCounter++;
      }
      return _elem[ex];
   }

})();
 
The code above, as is, won't work "out of the box" because (for the sake of brevity) I have not included the xb.removeEvent(..) function. Fear not however. There will be a link to a download of the full source code, complete with both xb.addEvent(..) and xb.removeEvent(..) at the end of this post.

Commentary

So some of you may be wondering: "Why another addEvent function. Haven't we been through this?" We have, but I think you'll find that this version does more than any other addEvent function you've seen. For example:

  • Works in all browsers that matter
  • Ensures one event wire up for any given element/event/handler combination (this is mostly for IE)
  • Forces IE to honor the this keyword from within event handler functions
  • Normalizes the wire up mechanism in all browsers (no need to include the "on" prefix for IE)
  • Forces IE to recognize the following properties/methods on event objects: .stopPropagation(), .preventDefault(), .target, .relatedTarget
  • Enhances Non-IE browsers to support mouseenter and mouseleave events
  • Provides an extension mechanism so developers can write their own custom events that plug right in ('mousewheel' or 'DOMContentReady' for example)

Given all that the Ultimate addEvent function does, it's not surprising that it has a bit of length to it. But it isn't super long, and I think it's well worth it.

Usage

Using the function(s) is just as easy as you'd expect:

var myDiv = document.getElementById('myDiv');
xb.addEvent(myDiv, 'click', clickHandler);

function clickHandler(e)




{
   alert('The this keywords works (even in IE!): ' + this.id);
}

Extending Functionality

Since I mentioned that it was possible to extend the Ultimate addEvent function with custom events that plug right in, I figure I'd better offer an example. Actually, there already is an example built right into it. Both the mouseenter and mouseleave events utilize the extension mechanism internally, but for demonstration purposes, let's go ahead and add a mousewheel event:

// Extend the Ultimate addEvent function to recognize a "mousewheel" event
xb.defineEvent('mousewheel', function(_fn, _useCapture, _listening)
{
   // event name for IE, Opera and Safari
   var evtName = 'mousewheel';

   // hander for IE, Opera, and Safari
   var f = _fn;
   
   // if we're dealing with a Gecko browser, the event name
   // and handler need some adjustment
   var ua = navigator.userAgent.toLowerCase();
   if (ua.indexOf('khtml') === -1 && ua.indexOf('gecko') !== -1)
   {
      evtName = 'DOMMouseScroll';
      f = mouseWheel(_fn);
   }

   // _listening represents whether this is an event attachment or detachment
   // that 5th parameter?  Don't worry about it; just always use false
   _listening ? xb.addEvent(this, evtName, f, _useCapture, false) : xb.removeEvent(this, evtName, f, _useCapture, false);
});

// Helper function for dealing with mousewheel in Gecko browsers
function mouseWheel(_fn)
{
   var key = xb.Helper.getObjectGUID(_fn);
   var f = xb.Helper.retrieveHandler(key);
   if (typeof f === 'undefined')
   {
      f = function(_evt)
      {
         _evt.wheelDelta = -(_evt.detail);
         _fn.call(this, _evt);
         _evt.wheelDelta = null;
      };
      xb.Helper.storeHandler(key, f);
   }
   return f;
}

I'm glossing over some details here because I don't want this post to get bogged down in something that it isn't really about, but I trust you get the idea.

Most of this code actually comes from the The Gimme Javascript Library. If you're using Gimme in any of your web pages, you already have all of this functionality for free. The syntax is a touch different, but all the capabilities are the same.

And if you happen to be the author of a Live Maps Mashup, you already have Gimme, as the latest version shipped with the most recent Virtual Earth MapControl.

Complete Source

As promised, here is the complete source, including both xb.addEvent(..) and xb.removeEvent(..)

Enjoy! And please don't hesitate to send your feedback, both good and bad (I'm very willing to address issues or try to make requested enhancements).