Event Package Continued (Part IV)

This article completes the discussion of my corejs event page. In case you have not been following the discussion so far, here are links to the other articles:

Event.js

Event Package Continued (Part III) on 11/07/07
Event Package Continued (Part II) on 10/24//07
Event Package (Part I) on 10/18/07
X-Browser Event Handling on 09/28/07

Today, I will be walking through the Event.js, discussing the parts not previously covered. Lets start by looking at the caching function:

Example 1: Cache Event function

var cacheEvent = function(el, eType, fn, opt) {
	// ensure that opt in an object
	var o = opt || {};
	o.capture = (o.capture || false);
	
	// wrap the function so we can control the scope and return the optional second object
	var wrapFn = function(e) {
		return fn.call(o.scope || window, e, o.data);
	};

	// cache the listener so we can try to automatically unload
	var evt = [el, eType, fn, wrapFn, o];
	ecache[ecache.length] = evt;
	return evt;
};

This function will be called any time an event is added using the add() function and accepts the same parameters as the add() function. The first part of the function, ensures that the options exist and that the capture value is set. Capture is a required for the actual attachment of the event, but since it is false 99% of the time, we are making it optional. The wrapFn internal function is used to set the scope of, and pass through the data option into, the callback function. We then create an array of the important values for the cache and push it onto the stack.

We return the cache object so that the add() function can use wrapFn as its callback and apply the value of capture. This creates an interesting problem when removing events using the old remove function, as we have attached the the wrapFn as the callback, but code outside of this package, does not have access to the wrapping function. That is why we use a private remove function, so that we can locate wrapFn from the cache. The public remove function does just that:

Example 2: Public Remove function

remove: function(el, eType, fn, fl) {
	var rs = that.getListeners(el, eType);

	Core.batch(rs, function(r) {
		if ((r.fn) == (fn)) {
			update(r);
		}
	});
},

The public remove function first gets all the cached objects for a given element that match the event type, using the getListeners() function (discussed later). We then iterate on the results and compare the paremeter fn with the cached non-wrapped function. When this is true, we actually remove the event, otherwise. The update function handles this removal:

Example 3: Removing Events and Cached Elements

var update = function(r) {
	remove(r.el, r.type, r.wfn, r.opt.capture);
	var arr = [];

	if (0 < r.index) {
		arr = ecache.slice(0, r.index);
	}
	arr.concat(ecache.slice(r.index + 1));
	ecache = arr;
};

First we actually remove the event with the internal remove() function (notice that the wrapped function is used). The rest of the function slices the cached event object that we are removing out of the stack.

In order to quickly and effectively find the events attached on an Element, both internally and externally, we created the public getListeners() function. I model the data architecture after a similar function in YUI, as I could not think of a more elegant way to return matching cache objects.

Example 4: getListeners function

getListeners: function(el, eType) {
	var results = [];

	Core.batch(ecache, function(l, i) {
		if (l  && l[EL] === el && (! eType || eType === l[TYPE]) ) {
			results.push({
				el:   	l[EL],
				type:   l[TYPE],
				fn:     l[FN],
				wfn:	l[WFN],
				opt:    l[OPT],
				index:  i
			});
		}
	});

	return results;
},

We simply iterate through the cache and create special result objects each time the element, and event type when provided, match that of a cached event object. Because the results objects can be passed externally, where the position name variables (EL, FN, TYPE, WFN, and OPT) do not exist, we instead create objects where each member is the lowercase of the constant and is set to the comparable value. The index is added, so the event object can later be removed from the cache.

I added two other functions that I found to be extremely useful. One is the removeEvents() function that removes all events from a given element by leveraging the existing caching infrastructure from event removal. And a removeAll() function that removes all events ever assigned using the add() function. This function is attached to the window.unload event and helps prevent memory leaks (especially in IE6) where the memory reference to the dom element that triggers the event is not cleared, because the event is still pointing to the dom element.

Lastly, I removed as spaces from the Event name constants:

Example 5: Event Name Constants

BLUR: blur,
CLICK: click,
DOUBLECLICK: dblclick,
KEYDOWN: keydown,
KEYPRESS: keypress,
KEYUP: keyup,
LOAD: load,
MOUSEDOWN: mousedown,
MOUSEMOVE: mousemove,
MOUSEOVER: mouseover,
MOUSEOUT: mouseout,
MOUSEUP: mouseup,
SUBMIT: submit,
UNLOAD: unload,

I did this because there was a conflict between several events, such as the KEYUP event and the KEY_UP that actual keyboard key up arrow.

Many of the function names have become shorter as well. I simply found that I knew when using the event package that add() meant that I was adding a listener, so I did not need the names to be longer.

Please let me know any feedback you have, especially if you have suggestions for improvement.

Today, I will be walking through the Event.js, discussing the parts not previously covered. Lets start by looking at the caching function:

Example 1: Cache Event Function

var cacheEvent = function(el, eType, fn, opt) {
	// ensure that opt in an object
	var o = opt || {};
	o.capture = (o.capture || false);
	
	// wrap the function so we can control the scope and return the optional second object
	var wrapFn = function(e) {
		return fn.call(o.scope || window, e, o.data);
	};

	// cache the listener so we can try to automatically unload
	var evt = [el, eType, fn, wrapFn, o];
	ecache[ecache.length] = evt;
	return evt;
};

This Function will be called any time an event is added using the add method and accepts the same parameters as the add method. The first part of the Function, ensures that the options exist and that the capture value is set. Capture is a required for the actual attachment of the event, but since it is false 99% of the time, we are making it optional. The wrapFn internal Function is used to set the scope of, and pass through the data option into, the callback Function. We then create an array of the important values for the cache and push it onto the stack.

We return the cache object so that the add Function can use wrapFn as its callback and apply the value of capture. This creates an interesting problem when removing events using the old remove Function, as we have attached the the wrapFn as the callback, but code outside of this package, does not have access to the wrapping Function. That is why we use a private remove Function, so that we can locate wrapFn from the cache. The public remove Function does just that:

Example 2: Public Remove Function

remove: function(el, eType, fn, fl) {
	var rs = that.getListeners(el, eType);

	Core.batch(rs, function(r) {
		if ((r.fn) == (fn)) {
			update(r);
		}
	});
},

The public remove Function first gets all the cached objects for a given element that match the event type, using the getListeners method (discussed later). We then iterate on the results and compare the paremeter fn with the cached non-wrapped Function. When this is true, we actually remove the event, otherwise. The update Function handles this removal:

Example 3: Removing Events and Cached Elements

var update = function(r) {
	remove(r.el, r.type, r.wfn, r.opt.capture);
	var arr = [];

	if (0 < r.index) {
		arr = ecache.slice(0, r.index);
	}
	arr.concat(ecache.slice(r.index + 1));
	ecache = arr;
};

First we actually remove the event with the internal remove Fucntion (notice that the wrapped Function is used). The rest of the method slices the cached event object that we are removing out of the stack.

In order to quickly and effectively find the events attached on an Element, both internally and externally, we created the public getListeners method. I model the data architecture after a similar Function in YUI, as I could not think of a more elegant way to return matching cache objects.

Example 4: getListeners Function

getListeners: function(el, eType) {
	var results = [];

	Core.batch(ecache, function(l, i) {
		if (l  && l[EL] === el && (! eType || eType === l[TYPE]) ) {
			results.push({
				el:   	l[EL],
				type:   l[TYPE],
				fn:     l[FN],
				wfn:	l[WFN],
				opt:    l[OPT],
				index:  i
			});
		}
	});

	return results;
},

We simply iterate through the cache and create special result objects each time the element, and event type when provided, match that of a cached event object. Because the results objects can be passed externally, where the position name variables (EL, FN, TYPE, WFN, and OPT) do not exist, we instead create objects where each member is the lowercase of the constant and is set to the comparable value. The index is added, so the event object can later be removed from the cache.

I added two other Functions that I found to be extremely useful. One is the removeEvents Function that removes all events from a given element by leveraging the existing caching infrastructure from event removal. And a removeAll Function that removes all events ever assigned using the add method. This method is attached to the window unload event and helps prevent memory leaks (especially in IE6) where the memory reference to the dom element that triggers the event is not cleared, because the event is still pointing to the dom element.

Lastly, I removed as spaces from the Event name constants:

Example 5: Event Name Constants

BLUR: blur,
CLICK: click,
DOUBLECLICK: dblclick,
KEYDOWN: keydown,
KEYPRESS: keypress,
KEYUP: keyup,
LOAD: load,
MOUSEDOWN: mousedown,
MOUSEMOVE: mousemove,
MOUSEOVER: mouseover,
MOUSEOUT: mouseout,
MOUSEUP: mouseup,
SUBMIT: submit,
UNLOAD: unload,

I did this because there was a conflict between several events, such as the KEYUP event and the KEY_UP that actual keyboard key up arrow.

Many of the Function names have become shorter as well. I simply found that I knew when using the event package that add meant that I was adding a listener, so I did not need the names to be longer.

Please let me know any feedback you have, especially if you have suggestions for improvement.