Event Package Continued (Part II)

In my last article (Event Package Part 1), I covered some of the necessary parts of an event package and mentioned some "optional" advanced features. I originally planned to discuss them all in this article, but there is too much material to cover, so I will be breaking it up into two articles. Some of the advanced features (such as, removing all listeners and scope control) require an internal caching mechanism and I will be covering this system in Fridays article. Todays article will cover the other features, including two more additions to the Core package.

These new core methods are "getBody" and "getScrollOffset":

/**
 * X-browser method to find the BODY tag; lazy definition to improve repeat performance
 *
 * @method getBody
 * @return {HTMLElement} the BODY tag
 * @static
 */
var getBody = function() {
	var bd = window.document.body || window.document.childNodes[0].childNodes[1] || window.document.getElementsByTagName("body")[0] || window.document;
	Mint.Client.getBody = function() {return bd;}
	return Mint.Client.getBody();
};

This method tries 3 ways to get the BODY tag, then defaults to the document. The "bd" variable will then hold the reference to the BODY tag. We then use the lazy initialization technique to create a closure that will improve the performance of any subsequent requests to this method.

/**
 * X-browser method to find the scroll offset; lazy definition to improve repeat performance
 *
 * @method getScrollOffset
 * @return {Object} the scroll offset object {x, y}
 * @static
 */
var getScrollOffset = function() {
	var de = document.documentElement, db = getBody();
	
	if (de && (de.scrollTop || de.scrollLeft)) {
		Mint.Client.getScrollOffset = function() {return {x: dd.scrollLeft, y: dd.scrollTop};}
	} 
	else if (db) {
		Mint.Client.getScrollOffset = function() {return {x: db.scrollLeft, y: db.scrollTop};}
	} 
	else {
		Mint.Client.getScrollOffset = function() {return {x: 0, y: 0};}
	}
	
	return Mint.Client.getScrollOffset();
};

This method returns the scrolled offset of the viewport, in both the x and y directions. We first create the "de" and "db" variables, then evaluate which of 3 methods to lazily define "getScrollOffset" as. The first if statement is true for most compliant browser, the second is true for IE, and the 3rd is a failsafe for older browsers (this way, calling this method wont throw an exception). Again, by using lazy function definition pattern, we will improve the runtime of subsequent requests.

I also added the following methods as private Functions of the Event package:

/**
 * Some browsers (Safari) will sometimes return a text node inside the targeted element.  This normalizes the return value for getEl and getRelatedEl.
 *
 * @method resolveTextNode
 * @param node {HTMLElement} the node to resolve
 * @return {HTMLElement} the normized node
 * @private
 */
var resolveTextNode = function(node) {
	return (node && document.TEXT_NODE == node.nodeType)? node.parentNode: node
};

/**
 * Some browsers (IE) do not actually return the event, but store it in a global object. We will try to normalize this in our event callback, but just in case, we resolve this each time we expect an event.
 *
 * @method resolveEvent
 * @param e {Event} The trigger event
 * @return {Event} the IE event
 * @private
 */
var resolveEvent = function(e) {
	return e || window.event;
}

"reseolveTextNode" was modeled after YUIs method, but is more compact and leverages the TEXT_NODE constant. Sometime Safari or Safari-like browsers return the textnode inside of the target element, instead of the target element. This method normalizes these situation and returns the appropriate target element. The "resolveEvent" is used to normalize an Event anytime it is expected as a paremeter. IE attaches the event to a global constant instead of returning it to the Event handler. And although, I plan on passing an already normalized event back to the callback Function, it is possible that another event system was used to get the Event without normalizing it.

Now armed with these methods, we can see how to implement the following Event package methods:

  • getRelatedEl
  • getTime
  • getX
  • getY
  • getXY

/**
 * X-browser method to return the event related target; tries to find relatedTarget, then toElement (mouseover), then fromElement (mouseout). 
 *	Some browsers (Safari) sometimes provide text nodes, so we automatically resolve them.
 *
 * @method getRelatedEl
 * @param e {Event} the trigger event
 * @return {HTMLElement} the related target element
 * @static
 */
getRelatedEl: function(e) {
	e = resolveEvent(e);
	return resolveTextNode(e.relatedTarget || e.toElement || e.fromElement);
},

/**
 * Returns the time of the event or set the event to the current time.
 * 
 * @method getTime
 * @param e {Event} the trigger event
 * @return {Date} the time of the event
 * @static
 */
getTime: function(e) {
	e = resolveEvent(e);
	
	if (! e.time) {
		var t = new Date().getTime();
		
		try {
			e.time = t;
		} 
		catch(ex) { 
			return t;
		}
	}

	return e.time;
},

/**
 * X-browser method to return the x position of the event
 *
 * @method getX
 * @param e {Event} The trigger event
 * @return {int} the x coordinate of the event
 * @static
 */
getX: function(e) {
	e = resolveEvent(e);
	var x = e.pageX;
	
	if (! x && 0 !== x) {
		x = e.clientX || 0;

		if ( Core.Client.isIE() ) {
			x += Core.Client.getScrollOffset().x;
		}
	}

	return x;
},

/**
 * X-browser method to return the y position of the event
 *
 * @method getY
 * @param e {Event} The trigger event
 * @return {int} the y coordinate of the event
 * @static
 */
getY: function(e) {
	e = resolveEvent(e);
	var y = e.pageY;
	
	if (! y && 0 !== y) {
		y = e.clientY || 0;

		if ( Core.Client.isIE() ) {
			y += Core.Client.getScrollOffset().y;
		}
	}

	return y;
},

/**
 * Returns the pageX and pageY properties as an indexed array.
 *
 * @method getXY
 * @param e {Event} The trigger event
 * @return {Object} the coordinate object {x: xpos, y: ypos}
 * @static
 */
getXY: function(e) {
	e = resolveEvent(e);
	return {x: self.getX(e), y: this.getY(e)};
},

The JAVADOC comments describing the Functions do a pretty good job of explaining their use. Let me know if you have any questions. On Friday I will be uploading the entire Event package JavaScript, which will probably help clear up confusion.