addEvent() new style

Introduction

This is a modification of my entry for the addEvent() recoding contest on quirksmode.org.

Basically this library consists of 4 functions: addEvent, removeEvent, a generic array_search function and a helper function for browsers that do not support the DOM level 2 methods addEventListener and removeEventListener (that would generally only be Internet Explorer so I named this function IEEventHandler).

In my previous version I used for-in constructs to iterate through the function-'stack', but in this version I replaced those constructs because a) such iteration will also return prototyped methods, and b) according to the spec the iteration order is not defined (although most browsers will return properties in the order they were assigned). I added the array_search function to make checks easier.

To use this code, simply copy/paste the functions from below into your javascript and call these functions as follows:

addEvent(object, eventType, function);

The 'object' argument should be a reference to an HTML-element on which you want to attach a handler for the event.
The 'eventType' argument should be the name of the event type (without the 'on' prefix), e.g. 'click', 'mouseover', 'load' etc.
The 'function' argument should be a reference to an existing function, or an anonymous function. The event object will be passed as an argument to this function.

To remove a listener simply call the removeEvent function as follows:

removeEvent(object, eventType, function);

The arguments to this function should exactly match the arguments you used for addEvent to attach the eventlistener.

These functions work around a number of issues that simular functions, usually wrappers around attachEvent, have. I don't use anonymous functions to prevent closures. I use my own function stack to match the execution order of addEventListener. I also check if a function is already registered as a handler for the event to prevent duplicate calls (also matching addEventListener's behavior). Also I added a workaround for IE5.0 that doesn't support the call or apply method on function objects. Last but not least I take into account the fact that some elements may already have a handler attached using inline javascript.

The code seems somewhat chunky, but that's mainly because of the comments I added. Feel free to remove these, or check out the short version of this script.

Also check out the version based on Dean Edward's code.

The JavaScript code

/**
  * Crossbrowser event handling functions.
  *
  * A set of functions to easily attach and detach event handlers to HTML elements.
  * These functions work around the shortcomings of the traditional method ( element.onevent = function; )
  * where only 1 handler could be attached for a certain event on the object, and mimic the DOM level 2
  * event methods addEventListener and removeEventListener for browsers that do not support these
  * methods (e.g. Internet Explorer) without resorting to propriety methods such as attachEvent and detachEvent
  * that have a whole set of their own shortcomings.
  * Created as an entry for the 'contest' at quirksmode.org: http://www.quirksmode.org/blog/archives/2005/09/addevent_recodi.html
  *
  * @author Tino Zijdel ( crisp@xs4all.nl )
  * @version 1.2
  * @date 2005-10-21
  */


/**
  * addEvent
  *
  * Generic function to attach event listeners to HTML elements.
  * This function does NOT use attachEvent but creates an own stack of function references
  * in the DOM space of the element. This prevents closures and therefor possible memory leaks.
  * Also because of the way the function references are stored they will get executed in the
  * same order as they where attached - matching the behavior of addEventListener.
  *
  * @param obj The object to which the event should be attached.
  * @param evType The eventtype, eg. 'click', 'mousemove' etcetera.
  * @param fn The function to be executed when the event fires.
  * @param useCapture (optional) Whether to use event capturing, or event bubbling (default).
  */
function addEvent(obj, evType, fn, useCapture)
{
	//-- Default to event bubbling
	if (!useCapture) useCapture = false;

	//-- DOM level 2 method
	if (obj.addEventListener)
	{
		obj.addEventListener(evType, fn, useCapture);
	}
	else
	{
		//-- event capturing not supported
		if (useCapture)
		{
			alert('This browser does not support event capturing!');
		}
		else
		{
			var evTypeRef = '__' + evType;

			//-- create function stack in the DOM space of the element; seperate stacks for each event type
			if (obj[evTypeRef])
			{
				//-- check if handler is not already attached, don't attach the same function twice to match behavior of addEventListener
				if (array_search(fn, obj[evTypeRef]) > -1) return;
			}
			else
			{
				//-- create the stack if it doesn't exist yet
				obj[evTypeRef] = [];

				//-- if there is an inline event defined store it in the stack
				if (obj['on'+evType]) obj[evTypeRef][0] = obj['on'+evType];

				//-- attach helper function using the DOM level 0 method
				obj['on'+evType] = IEEventHandler;
			}

			//-- add reference to the function to the stack
			obj[evTypeRef][obj[evTypeRef].length] = fn;
		}
	}
}

/**
  * removeEvent
  *
  * Generic function to remove previously attached event listeners.
  *
  * @param obj The object to which the event listener was attached.
  * @param evType The eventtype, eg. 'click', 'mousemove' etcetera.
  * @param fn The listener function.
  * @param useCapture (optional) Whether event capturing, or event bubbling (default) was used.
  */
function removeEvent(obj, evType, fn, useCapture)
{
	//-- Default to event bubbling
	if (!useCapture) useCapture = false;

	//-- DOM level 2 method
	if (obj.removeEventListener)
	{
		obj.removeEventListener(evType, fn, useCapture);
	}
	else
	{
		var evTypeRef = '__' + evType;

		//-- Check if there is a stack of function references for this event type on the object
		if (obj[evTypeRef])
		{
			//-- check if function is present in the stack
			var i = array_search(fn, obj[evTypeRef]);
			if (i > -1)
			{
				try
				{
					delete obj[evTypeRef][i];
				}
				catch(e)
				{
					obj[evTypeRef][i] = null;
				}
			}
		}
	}
}

/**
  * IEEventHandler
  * 
  * IE helper function to execute the attached handlers for events.
  * Because of the way this helperfunction is attached to the object (using the DOM level 0 method)
  * the 'this' keyword will correctely point to the element that the handler was defined on.
  *
  * @param e (optional) Event object, defaults to window.event object when not passed as argument (IE).
  */
function IEEventHandler(e)
{
	e = e || window.event;
	var evTypeRef = '__' + e.type, retValue = true;

	//-- iterate through the stack and execute each function in the scope of the object by using function.call
	for (var i = 0, j = this[evTypeRef].length; i < j; i++)
	{
		if (this[evTypeRef][i])
		{
			if (Function.call)
			{
				retValue = this[evTypeRef][i].call(this, e) && retValue;
			}
			else
			{
				//-- IE 5.0 doesn't support call or apply, so use this
				this.__fn = this[evTypeRef][i];
				retValue = this.__fn(e) && retValue;
			}
		}
	}

	if (this.__fn) try { delete this.__fn; } catch(e) { this.__fn = null; }

	return retValue;
}

/**
  * array_search
  * 
  * Searches the array for a given value and returns the (highest) corresponding key if successful, -1 if not found.
  *
  * @param val The value to search for.
  * @param arr The array to search in.
  */
function array_search(val, arr)
{
	var i = arr.length;

	while (i--)
		if (arr[i] && arr[i] === val) break;

	return i;
}

Example

This is the original markup and menuhandling javascript of the contest page written bij PPK; I only changed the CSS to make it a more pretty example that behaves and looks pretty much the same in most browsers. This menu doesn't work perfectly and is only intended to demonstrate that the addEvent and removeEvent functions are working properly (here is an example of a better working menu, although this only uses the DOM level 0 event model).

Remove border/background effect.

Contact

If you have any questions regarding this page, please feel free to sent me an email.

September 9, 2005 - Tino Zijdel ( crisp@xs4all.nl )

Changes:
October 19, 2005 - replaced for-in constructs, added indexOf method to search in arrays
October 21, 2005 - replaced indexOf by general indexOfFunction for anonymous functions
October 21, 2005 - created a short version