addEvent() recoding contest entry, mark II

by Cris Perdue

Remove border effect.

Description

The functions addEvent and removeEvent provide a means for adding multiple handlers for the same event on a DOM node. They are patterned after the W3C standard facilities of the same name, but work across platforms and provide a couple of additional capabilities. This API does not provide access to the W3C event capture capabilities, which are not supported by some browsers.

This implementation is strictly platform-neutral to keep it simple and obtain most consistent behavior across browsers. It avoids calling any of the advanced event registration methods. Instead it relies only on the traditional, least-common-denominator style of event handling through node properties such as onclick.

Compared with my earlier implementation, this has a few cleanups, but mainly it deregisters at unload time all handlers originally registered via addEvent, and it only registers a function once as a handler for any pair of node and event type.

The automatic de-registration should help to avoid memory leaks due to circular references between the DOM and event handler functions.

addEvent(node, eventName, handler)

This adds a handler function for eventName to the given node. The event name should omit the "on" prefix, for example, "click" rather than "onclick". This function can set up multiple handlers for the same event on the same DOM node, and call them in order of registration when the event occurs. Furthermore, if there is a traditional handler already set up for the node and event, it will be registered as the first handler on the new list. It ignores addition of a handler function if already established for the given node and event.

All handlers will be called with "this" set to the DOM element, like traditional handlers set directly through properties.

removeEvent(node, eventName, handler)

Removes the given handler function for eventName from the set defined for the given DOM node. When removing the last registered handler, it also sets the the handler property to null so the browser can omit dispatching the event type to this node at all.

Note: Beyond the scope of this contest, I believe a valuable addition would be for addEvent to support objects with handleEvent properties as event handlers. This should provide a foundation for adding Aspect-Oriented Programming, as in the Dojo Toolkit framework. This sort of handler has been supported already by some browsers.


// Isolate internals from the global namespace except where
// made explicitly public.
(function() {

  // Hash that maps from event name (e.g. "click") to array of
  // DOM elements with handlers set up for that event.
  // This registry provides access to all handlers set up
  // through addEvent.
  var handlerNodes = new Object;


  // Cross-platform event handler adder.
  //
  function addEvent(node, evname, fn) {
    var handler = "on"+evname;
    var property = evname+"__handlers";
    if (node[handler] && node[handler]!=callHandlers) {
      node[property] = [fn];
    }
    node[handler] = callHandlers;
    node[property] = node[property] || new Array;
    add(node[property], fn);
    // Register that this node handles this event.
    var h = handlerNodes[evname] = handlerNodes[evname] || new Array;
    add(handlerNodes, evname);
    add(h, node);
  }


  // Add element to array if not already present
  // 
  function add(a, x) {
    for (var i in a) {
      if (a[i]==x) return;
    }
    a[a.length] = fn;
  }

  // Remove element from an array if present.
  //
  function remove(a, x) {
    var i = 0;
    for (var j=0; j<a.length; j++) {
      if (a[j]!=x) {
        a[i++] = a[j];
      }
    }
    a.length = i;
  }


  function removeEvent(node, evname, fn) {
    var handler = "on"+evname;
    var property = evname+"__handlers";
    var a = node[property];
    if (!a) return;
    remove(a, fn);
    if (a.length==0) {
      node[handler] = null;
    }
  }


  // Registered as an event handler, calls all handlers 
  // registered through addEvent for its node.
  //
  function callHandlers(event) {
    if (!event)
      event = window.event;
    var a = this[event.type+"__handlers"];
    try {
      for (var i=0; i<a.length; i++) {
        this.__handler = a[i];
        var v = this.__handler(event);
      }
    } finally {
      // Clean out the object reference.
      this.__handler = null;
    }
  }


  // To be set up as a window onunload handler, this walks the handler
  // registry removing all handlers set up through addEvent.
  // 
  function deRegister() {
    for (var evname in handlerNodes) {
      var a = handlerNodes[evname];
      var property = evname+"__handlers";
      for (var i=0; i<a.length; i++) {
        a[i][property] = null;
      }
    }
  }

  // Make public objects public.
  window.addEvent = addEvent;
  window.removeEvent = removeEvent;

  addEvent(window, "unload", deRegister);

})()