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).

13 Responses

  1. steve Says:

    nice looking background image on this site

  2. Jamie Says:

    Hi,

    I have been trying to use this event code without success in Firefox 3.0.3.

    The first thing is the funny:

    (function(){ … })();

    Mechanism doesn't work – I had to initiate the code without it.

    Secondly, Firefox 3.0.3 gives me this error:

    _elem.addEventListener is not a function
    w3c_addEvent()xb.js (line 76)

  3. sstchur Says:

    Jamie,

    Did you copy/paste or download the zip? I just downloaded the zip, included the .js file using a <script> tag, and tested the function in Firefox 3.0.3, and it worked just fine.

    I did not modify a single character in the .js file, and it worked perfectly for me — no errors at all. The (function(){…})() is perfectly valid and did not cause any errors. And the event I hooked up worked properly as well.

    I'd suggest maybe trying the version in the zip, if you haven't already. I did mention in the post, that in the interest of keeping the blog entry a little shorter, copying and pasting will not work as an "out of the box" solution.

  4. Mathias Says:

    Have you deleted the zip?
    When I click on the link it just gives me an error page saying the page wasn't found..

    Should I just get the gimme lib instead?

  5. sstchur Says:

    @Mathias:

    Shoot! I'm sorry. I know what happened. I upgraded WordPress and I accidentally deleted my blogcode directory.

    Let me see if I can dig up a backup and get the files back in place.

    In the meantime, certainly feel free to check out the Gimme Library. It has a lot more functionality than just the addEvent function.

  6. sstchur Says:

    @Mathias:

    Ok, this is back online now. Sorry for the inconvenience.

  7. Bob Carver Says:

    I discovered an interesting manifestation, that's only an issue in IE, when cloning nodes. It was necesssary to remove all event handlers before doing the clone, and then restoring those event handlers after the clone operation (and subsequent insertBefore elsewhere). If this was not done, for example, the mouseenter event was triggered by both the original node as well as the new node, even though the id of the new node was modified to be unique. There was no problem in FF or Chrome, just IE. It took quite a bit of time to figure this one out…

  8. HB Says:

    I still use this in some projects today, it's really straightforward and reliable, and the built-in mouseenter/mouseleave is great. Have you done any testing in IE9, and/or do you expect to need to make any changes? From the looks of things it should probably follow the w3c flow and have no problems, but figured I'd ask if you knew for sure.

  9. sstchur Says:

    Thanks! Glad you're finding it useful.

    We've been testing out IE9 around here lately and I haven't had any issues as of yet. On my team, we're using the version of addEvent that is in Gimme, but it is more or less identical to the UAE function. But I'm glad you brought it up — I might have a few tweaks or bug fixes I've made in the Gimme version that I should probably port over to UAE. I'll make it a mental TODO.

    If you run into any IE9 problems, let me know. If I can't fix it, I'll ping the IE team.

  10. Bettina Says:

    First of all, I want to thank you for this code, it's helped me a lot already. I'm having an issue however. I'm working with IE8 and I want to attach an event with a function to an element. Since the function requires two parameters, I'm doing the following: xb.addEvent(allFormElements[x], "blur", function () {updateField(this, this.value)};
    But then your script fails to recognize if the function has already been attached to the element and adds it again, thus causing my update function to be called more than once. I'm guessing since I'm using anonymous functions. Do you have a solution?

  11. Bettina Says:

    Sorry, forgot the last closing bracket. I'm using this:
    xb.addEvent(allFormElements[x], "blur", function () { updateField(this, this.value) });

  12. sstchur Says:

    Hi Bettina,

    Yes, the issue would be that you're using an anonymous function defined on the fly. It's important to realize that any function created this way is always unique, so every time you make a call, the Javascript interpreter sees it as a new function being wired up.

    The solution is simply to not use anonymous functions.

    function formElementBlur(e)
    {
    updateField(this, this.value);
    }

    xb.addEvent(allFormElements[x], 'blur', formElementBlur);

    Hope this helps!

  13. Maria Says:

    Thanks Andy for pointing out.I have fixed the prbolem in IE6 and IE7.The only prbolem remains is png transparency prbolem in IE6. Although I know there are few fixes available for this, but I leave it to the developer to use PNG Fix.

Got something to say?

Please note: Constructive criticism is welcome. Rude or vulgar comments however, are not and will be removed during moderation.