de

designoir

addEvent & removeEvent

addEvent() and removeEvent() provide one unified, cross-browser safe way to handle event listeners.

Events unleashed!

Unlike many other implementations, this addEvent not only adds event listeners. Usually, working with events is pain because of a number of browser inconsistencies (read: Internet Explorer incapabilities). Hence addEvent does:

  1. implement parts of the W3C DOM Level 2 Event interface:
    • methods: event.stopPropagation(), event.preventDefault()
    • attributes: event.currentTarget, event.target, event.relatedTarget (MouseEvent), event.eventPhase
    • constants: Event.AT_TARGET, Event.BUBBLING_PHASE
  2. support the W3C DOM Level 2 EventListener interface (i.e. handleEvent)
  3. reference this with the target which the listener was registered on (instead of window)
  4. call listeners in FIFO order (instead of chaotic)
  5. remove all listeners on page unload to prevent memory leaking.

Notes

  1. Contrary to addEventListener, addEvent has no useCapture argument. For what it’s worth, it’s impossible to implement capture for IE.
  2. Gecko 1.8 and prior also leaks memory, but it’s fixed as of Gecko 1.8.1 (Firefox 2). And since unload handlers are said to break bfcache, I’ve implemented the cleanup for IE only.

addEvent.js

/**
 * addEvent & removeEvent -- cross-browser event handling
 * Copyright (C) 2006-2007  Dao Gottwald
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Contact information:
 *   Dao Gottwald  <dao at design-noir.de>
 *
 * @version  1.2.1
 */

function addEvent(o, type, fn) {
  o.addEventListener(type, fn, false);
}
function removeEvent(o, type, fn) {
  o.removeEventListener(type, fn, false);
}
/*@cc_on if (!window.addEventListener) {
  var addEvent = function (o, type, fn) {
    if (!o._events) o._events = {};
    var queue = o._events[type];
    if (!queue) {
      o._events[type] = [fn];
      if (!o._events._callback)
        o._events._callback = function (e) { Event._callListeners(e, o) };
      o.attachEvent("on" + type, o._events._callback);
    } else if (Event._fnIndex(o, type, fn) == -1)
      queue.push(fn);
    else return;
    Event._mem.push([o, type, fn]);
  };
  var removeEvent = function (o, type, fn) {
    var i = Event._fnIndex(o, type, fn);
    if (i < 0) return;
    var queue = o._events[type];
    if (queue.calling) {
      delete queue[i];
      if (queue.removeListeners)
        queue.removeListeners.push(i);
      else
        queue.removeListeners = [i];
    } else
      if (queue.length == 1)
        Event._detach(o, type);
      else
        queue.splice(i, 1);
  };
  var Event = {
    AT_TARGET: 2,
    BUBBLING_PHASE: 3,
    stopPropagation: function () { this.cancelBubble = true },
    preventDefault: function () { this.returnValue = false },
    _mem: [],
    _callListeners: function (e, o) {
      e.stopPropagation = this.stopPropagation;
      e.preventDefault = this.preventDefault;
      e.currentTarget = o;
      e.target = e.srcElement;
      e.eventPhase = e.currentTarget == e.target ? this.AT_TARGET : this.BUBBLING_PHASE;
      switch (e.type) {
        case "mouseover":
          e.relatedTarget = e.fromElement;
          break;
        case "mouseout":
          e.relatedTarget = e.toElement;
      }
      var queue = o._events[e.type];
      queue.calling = true;
      for (var i = 0, l = queue.length; i < l; i++)
        if (queue[i])
          if ("handleEvent" in queue[i])
            queue[i].handleEvent(e);
          else
            queue[i].call(o,e);
      queue.calling = null;
      if (!queue.removeListeners)
        return;
      if (queue.length == queue.removeListeners.length) {
        this._detach(o, e.type);
        return;
      }
      queue.removeListeners = queue.removeListeners.sort(function(a,b){return a-b});
      var i = queue.removeListeners.length;
      while (i--)
        queue.splice(queue.removeListeners[i], 1);
      if (queue.length == 0)
        this._detach(o, e.type);
      else
        queue.removeListeners = null;
    },
    _detach: function (o, type) {
      o.detachEvent("on" + type, o._events._callback);
      delete o._events[type];
    },
    _fnIndex: function (o, type, fn) {
      var queue = o._events[type];
      if (queue)
        for (var i = 0, l = queue.length; i < l; i++)
          if (queue[i] == fn)
            return i;
      return -1;
    },
    _cleanup: function () {
      for (var m, i = 0; m = Event._mem[i]; i++)
        if (m[1] != "unload" || m[2] == Event._cleanup)
          removeEvent(m[0], m[1], m[2]);
    }
  };
  addEvent(window, "unload", Event._cleanup);
} @*/

Example #1

addEvent.html

This example is the same site, that had to be used at the addEvent() recoding contest. I’ve modified it slightly, because the original noBubble function isn’t necessary anymore thanks to stopPropagation().

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>addEvent() recoding contest entry</title>
<script type="text/javascript" src="addEvent.js"></script>
<script type="text/javascript">//<!--

/*
  Original idea by John Resig
  Tweaked by Scott Andrew LePera, Dean Edwards and Peter-Paul Koch
*/

var menu = {
  init: function () {
    var menu = document.getElementById("navigation");
    addEvent(menu, "mouseover", this);
    addEvent(menu, "mouseover", showBorder);
    var items = menu.getElementsByTagName("li");
    for (var i = 0; i < items.length; i++) {
      addEvent(items[i], "mouseout", this);
      addEvent(items[i], "mouseout", hideBorder);
    }
  },
  handleEvent: function (event) {
    switch (event.type) {
      case "load":
        this.init();
        break;
      case "mouseover":
        if (event.target.nodeName.toLowerCase() == "li")
          event.target.className += " over";
        break;
      case "mouseout":
        if (event.relatedTarget.parentNode != event.target &&
            event.relatedTarget.parentNode.parentNode != event.target &&
            event.relatedTarget.parentNode.parentNode.parentNode != event.target)
          event.target.className = event.target.className.replace(/over/g, "");
        event.stopPropagation();
        break;
    }
  }
};

addEvent(window, "load", menu);

function showBorder(event) {
  if (event.target.nodeName.toLowerCase() == "li")
    event.target.className += " current";
}

function hideBorder(event) {
  if (event.relatedTarget.parentNode != event.target &&
      event.relatedTarget.parentNode.parentNode != event.target &&
      event.relatedTarget.parentNode.parentNode.parentNode != event.target)
    this.className = this.className.replace(/current/g,'');
  event.stopPropagation();
}

function removeBorders() {
  var menu = document.getElementById("navigation");
  removeEvent(menu, "mouseover", showBorder);
  removeEvent(menu, "mouseout", hideBorder);
}

//--></script>
<style type="text/css">
<!--

ul#navigation {
  width: 150px;
}

li {
  border: 1px solid #ffffff;
}

li ul {
  display: none;
}

.over ul {
  display: block;
}

.current {
  border-color: #cc0000;
}

-->
</style>

</head>

<body>

<h1><code>addEvent()</code> recoding contest entry</h1>

<ul id="navigation">
  <li><a href="#">Item 1</a>
    <ul>
      <li><a href="#">Item 1.1</a></li>
      <li><a href="#">Item 1.2</a></li>
      <li><a href="#">Item 1.3</a></li>
    </ul>

  </li>
  <li><a href="#">Item 2</a>
    <ul>
      <li><a href="#">Item 2.1</a></li>
      <li><a href="#">Item 2.2</a></li>
      <li><a href="#">Item 2.3</a></li>
    </ul>

  </li>
  <li><a href="#">Item 3</a>
    <ul>
      <li><a href="#">Item 3.1</a></li>
      <li><a href="#">Item 3.2</a></li>
      <li><a href="#">Item 3.3</a></li>
    </ul>

  </li>
</ul>

<p><a href="#" onclick="removeBorders()">Remove border effect</a>.</p>

</body>
</html>

Comments

  1. joe wrote on November 17, 2006 10:42 PM GMT ():
    am i reading this right? If you are on a browser that supports addEventListener, the only code that is compiled is:

    function addEvent (o, type, fn) {
    o.addEventListener (type, fn, false);
    }
    function removeEvent (o, type, fn) {
    o.removeEventListener (type, fn, false);
    }

    ?
  2. Dao wrote on November 17, 2006 10:53 PM GMT ():
    joe, quite right. Only IE will compile the commented code and then test for addEventListener (which of course doesn’t exist for any released version of IE, but it could in future).
  3. Valter Borges wrote on November 20, 2006 03:40 PM GMT ():
    In order to debug how would you recommend intercepting all events.
    Would we add a generic pre event before the other events? or is there a better way?
  4. Valter Borges wrote on November 20, 2006 04:05 PM GMT ():
    Hi DAO is there a way to be notified of new versions and to see a version log?
  5. Dao wrote on November 20, 2006 04:10 PM GMT ():
    In order to debug how would you recommend intercepting all events.
    Would we add a generic pre event before the other events? or is there a better way?
    I’m not sure what you want to do, but I guess _callListeners would be the point to intercept events.
    is there a way to be notified of new versions
    I’ll see if I can add a microsummary
    and to see a version log?
    Currently not, but maybe I’ll introduce one later on.
  6. Valter Borges wrote on November 21, 2006 02:43 PM GMT ():
    Well as you probably already know when dealing with event debugging and things like onblur, onfocus, if you use a traditional debugger it will cause events to fire as it stops to do a watch. Therefore I like to create a div where I write out the events as they are firing so I can troubleshoot. I wrote my own my I believe Yahoo has aone with more features can be here http://developer.yahoo.com/yui/logger/.
    Therefore instead of putting the statement that writes out what event was fired inside each event I would like to put it once somewhere before the event get’s fired so once I’m done I can just comment it out in one place.

    Is _callListeners still the best place to do this?
  7. Dao wrote on November 21, 2006 03:35 PM GMT ():
    Yes, I still think it’s the appropriate place. There’s |e|, the event object, and |o|, the object the listeners were registered on. That’s all you need, right? You can just do something like |log += 'fired '+e.type+' for #'+o.id+'\n';| (presuming that all the objects have an id). |log| could be |document.getElementById('debug-output').firstChild.nodeValue| or so.
  8. Diego Perini wrote on April 4, 2007 01:04 AM BST ():
    Hi Dao how are you…

    A couple of things I noticed while browsing your addEvent code.
    Are you handling the IE returnValue as returned by each listeners ?
    The first listener returning “false” should also break the chain execution.

    Cheers,
    Diego
  9. Dao wrote on June 10, 2007 11:57 AM BST ():
    Are you handling the IE returnValue as returned by each listeners ?
    I’m using returnValue internally for emulating event.preventDefault(). returnValue isn’t part of the DOM spec and won’t work across browsers when set inside of a listener.
    The first listener returning „false“ should also break the chain execution.
    Should it? I don’t think the DOM spec says so.
  10. Diego Perini wrote on June 18, 2007 02:45 PM BST ():
    Dao,
    I mistyped my question, it should have been:

    Are you handling the IE return value as returned by each listeners ?

    I meant the handler return value and not “returnValue” which as you said is a non standard IE property…
    Yes ! The first event returning false should stop subsequent event in the same chain run (same event type).

    This is needed where “Event Managers” build chains of events to maintain cross-browser compatibility and to be able to implement event ordering (missing in IE). The standard “addEventListener” DOM method in the remaining browsers already handle these requirements correctly.
  11. Dao wrote on June 18, 2007 04:29 PM BST ():
    This is needed where „Event Managers“ build chains of events to maintain cross-browser compatibility and to be able to implement event ordering (missing in IE).
    Not sure what you mean. addEvent takes care of the order of events. Therefore, multiple event listeners attached to the same target can just share custom code if they want to communicate with each other.
    The standard „addEventListener“ DOM method in the remaining browsers already handle these requirements correctly.
    It’s a bit odd to say “correctly” if it’s not spec’ed anywhere. If this remains the case, I tend to not implement it.
  12. Diego Perini wrote on July 14, 2007 06:13 PM BST ():
    Dao,
    you are probably correct, no specs saying how an event return value should affect the firing of next events.
    However I believe it may be useful in some situations and in event capturing also.
    I believe the specs have purposely skipped that since there was a browser firing event unordered… :-)

    Now that events are run ordered in everyone’s library (like your) I thought it could be used to block the other events of the same type, on the same chain bound to the same object. There have been talks about this on Dean site.

    In the old DOM0 registration world and especially with the inline registration model a return of false was a mean of blocking the default action bound to that element (to make links not follow the URL for example).

    In the new days nearly all browsers accept a string as a return value from the “onbeforeonload” event, and that string will be showed in the standard dialog box brought up by that event, if you override that return value you are practically skipping the “hint” to the user. I know it is not standard, but is everywhere in web apps…

    However keep up with the nice work.

    Cheers,
    Diego
  13. Diego Perini wrote on July 17, 2007 01:22 PM BST ():
    I mistyped the event name in the last paragraph of the above message, it should be “onbeforeunload”…

    Diego
  14. Ari Kivimäki wrote on August 1, 2007 12:00 PM BST ():
    Excellent code to tackle IE’s event calling randomness!
  15. mack pexton wrote on February 14, 2008 10:22 PM GMT ():
    I had troubles with Event._cleanup() because _mem was undefined when executed. The reason was because the last line addEvent(window, “unload”, Event._cleanup); changes the meaning of “this” to be the window object, not the Event object. To cure, I changed the last line to addEvent(window, “unload”, function(){Event._cleanup()});
  16. Delan Ahmad wrote on April 7, 2009 12:12 PM BST ():
    Thank you very much for this script, I have used it (with credit) in my InterModule javascript library (intermodule.sf.net). This has finally fixed up the problems with cross-browser event handling. :D
  17. Michel wrote on February 16, 2010 09:04 PM GMT ():
    Hi,
    On FF I cant get rid of my events with :
    function removeEvent(o, type, fn) {
    o.removeEventListener(type, fn, false);
    }
    fn is passed the same way it is passed in addEvent but the mouseover function is still working !
    type is ’mouseover’ in add and in remove.
    Everything works in the addEvent but not in the remove…

    Did I missed something about returning something false ?
    Thank you
  18. Michel wrote on February 17, 2010 11:02 PM GMT ():
    I reply to myself :
    I made a small example test case to minimize the code but now it’s working !
    I will have to analyse my big code to see what happen :-(
  19. paslanmaz çelik wrote on August 3, 2011 04:42 PM BST ():
    I reply to myself

    Ty for post
  20. wes wrote on September 26, 2011 06:33 PM BST ():
    hi, nice code, what about integration of basic createEvent / initEvent / dispatchEvent for older ie? domcontentloaded event can be useful too! thank you!

Comments are closed.

Last changed on October 15, 2007 Kontakt
aggressiv akt andromeda bar beine blue efeu frontal fugaetu industriell komet land noir rost rot sonnenblume splash split winter wolke zeit