/******************************************************************************
*	navMenu.js
*  DHTML/AJAX component for navMenu.cfc component
* 
*  Richard.Davies@portlandoregon.gov
******************************************************************************/

var navMenu = new Object();

/******************************************************************************
*	Creates a menu object for each table with class='menu'
******************************************************************************/
navMenu.init = function () {
	// Get an array of the <table>s with class = menu
	var navMenuTables = Element.getElementsByClass(document, "navMenu");

	navMenu.menus = new Array();
	for (var i = 0; i < navMenuTables.length; i++) {
		navMenu.menus[i] = new navMenu.menu(navMenuTables[i]);
	}
	
	// Create an associative array to cache menus returned from ajax calls
	navMenu.menuCache = new Array();
};


/******************************************************************************
*	Call a function after the page has loaded
******************************************************************************/
navMenu.addLoadEvent = function (func) {
	navMenu.menu.prototype.addEvent(window, "load", func);
};





/******************************************************************************
*  Navigation Menu object.
*
*		@param	obj		table DOM object of menu
* 		@return				a navMenu object
******************************************************************************/
navMenu.menu = function (obj) {
	// Store a pointer to the table DOM element
	this.table = obj;
	
	// This is a "counter" that stores the timestamp of the most recently returned ajax call
	this.ajaxTimestamp = new Date().getTime()
	
	// Flag to record if the menu is in its original state
	this.isOrig = true;
	this.origInnerHTML = Element.getParent(obj).innerHTML;
	
	this.setupListeners(obj);
};


/******************************************************************************
*  Setup event listeners on menu options to make ajax calls to update the menu
*  when a user hovers over them.
*
*		@param	obj		table DOM object of menu
******************************************************************************/
navMenu.menu.prototype.setupListeners = function (obj) {
	// Store a pointer to the table DOM element
	this.table = obj;
	
	// Get an array containing the menu option <td> from each level
	// And loop through each level of the menu
	var menuTDs = Element.getElementsByClass(obj, "menu", "td");
	for (var i = 0; i < menuTDs.length; i++) {
		// Create an array of menu options for the current level that will receive listeners for ajax calls
		// Include every menu option that is a link
		var opts = new Array();
		var links = menuTDs[i].getElementsByTagName("a");
		for (var j = 0; j < links.length; j++) {		// Copy all links to the options array
			opts.push(links[j]);
		}
		// Also include text-only menu options if we're not on the original menu
		if (navMenuOpts.selectedID != navMenuOpts.currentID) { 
			var activeOpts = Element.getElementsByClass(menuTDs[i], "active", "span");
			for (var j = 0; j < activeOpts.length; j++) {
				opts.push(activeOpts[j]);
			}
		}

		// Loop through each menu option
		for (var j = 0; j < opts.length; j++) {
			// Add a reference back to the main object
			opts[j].menu = obj;
			
			// Add a 'level' property to each menu option and set it to the option's menu level 
			opts[j].level = i+1;		// Level needs to be 1-based, not 0-based
			
			// Attach onmouseover event listener to retrieve new menu via ajax 
			this.addEvent(opts[j], "mouseover", this.linkHover.bind(this));
		}
	}
	
	// Add listener to restore original menu when mouse leaves menu
	this.addEvent(obj, "mouseout", this.restoreMenu.bind(this));
	// If mouse returns to menu before restoreMenu() timer executes, cancel the timer
	this.addEvent(obj, "mouseover", this.cancelRestoreMenu.bind(this));
};


/******************************************************************************
*  Event listener to change the menu. It initiates ajax requests to retrieve
*  the updated menu options if the requested menu is not already cached (due to
*  a prior ajax call).
*
*		@param	e			the event object
******************************************************************************/
navMenu.menu.prototype.linkHover = function (e) {
	var target = this.getEventTarget(e);

	// Clear any existing timer
	clearTimeout(navMenu.menus[0].hoverTID);
	
	// Use the hasChildren class to detect a couple of situations where there is
	// no need to change the menu and abort
	var rtarget = document.getElementById(navMenuOpts.selectedID);
	if (target.level == rtarget.level+1 && !Element.hasClass(target, "hasChildren")) {
		return;
	} else if (target.level == rtarget.level && 
				 (!Element.hasClass(target, "hasChildren") && !Element.hasClass(rtarget, "hasChildren")) ) {
		return;
	}
	
	// Set a timer to update the menu after a brief delay
	navMenu.menus[0].hoverTID = setTimeout(function () {	
		// Check cache to see if we've already retrieved this menu. If not retrieve via ajax call
		if (navMenu.menuCache[target.id] == null) {		// Not in cache
			// Change mouse cursor to wait indicator during ajax call
			Element.addClass(target.menu, "wait");
		
			// navMenuOpts is created by generate() in menu.cfc 
			http("POST", "/shared/cfm/navMenuProxy.cfm?method=generate&selectedID=" + target.id, this.updateMenu, navMenuOpts);
		} else {		// already cached
			var result = {
				content: navMenu.menuCache[target.id],
				timestamp: new Date().getTime()
			};
			this.updateMenu(result);
		}
	}.bind(this), 500);
};


/******************************************************************************
*  Updates the menu with new menu options. It can be called directly or used
*  as an ajax callback function. It caches results returned from ajax calls.
*
*		@param	result	JSON object with:
* 									content = HTML string containing new menu options
* 									timestamp = value of "new Date().getTime()" when 
* 										ajax request was initiated
******************************************************************************/
navMenu.menu.prototype.updateMenu = function (result) {
	/* TODO: This won't work with more than one menu on a page.	In order for it to work 
	 * in this situation, we would need to pass some identifier with the ajax calls that
	 * would be returned that would enable the functions to know which navMenu.menu object
	 * initiated the ajax call. Any function using "navMenu.menus[0]" would need to be
	 * updated to reflect this change.
	*/
	var menuDiv = Element.getElementsByClass(document, "navMenuWrapper");

	// If the menu object is not defined on the server (i.e. the application/session expired)
	// then result will be undefined 
	if (result == null) {
		Element.removeClass(navMenu.menus[0].table, "wait");
		return;		
	}

	// Because ajax requests are asyncronous, they won't necessarily return in the same order
	// that they were executed. So use a counter to ensure we're not overwritting a newer result
	// with an older result
	if (result.timestamp > navMenu.menus[0].ajaxTimestamp) {
		// Update ajax counter
		navMenu.menus[0].ajaxTimestamp = result.timestamp;
	
		// Replace existing menu options with new content
		menuDiv[0].innerHTML = result.content;
		
		// Use setTimeout to reinitialize menu object to allow script in innerHTML to run before the object's creation
		setTimeout(function () {
				navMenu.menus[0].setupListeners( Element.getElementsByClass(menuDiv[0], "navMenu", "table")[0] );
				
				// Update cache
				navMenu.menuCache[navMenuOpts.selectedID] = result.content;
			}, 1);
			
		// Set flag
		navMenu.menus[0].isOrig = false;
	}
};


/******************************************************************************
*  Event listener to restore the menu to its original configuration. When the
*  mouse leaves the menu, it checks to see if the menu was changed from its
*  original state and if so restores the original state.
*
*		@param	e			the event object
******************************************************************************/
navMenu.menu.prototype.restoreMenu = function (e) {
	// Check if the menu has been changed by an ajax update 
	if (!navMenu.menus[0].isOrig) {
		// This listener is fired whenever the mouse leaves not only the table,
		// but also any element (i.e. link or td) inside the table because those
		// events bubble up to the table event. So we must filter out those extra
		// calls and only continue when the mouse is leaving the table element.
		var rtarget = this.getRelatedTarget(e);
		if ( !Element.contains(navMenu.menus[0].table, rtarget) ) {
			// Put the menu restoration stuf inside a timer so that it can be
			// cancelled if they immediately return to the menu (i.e. they didn't
			// really mean to exit).
			navMenu.menus[0].restoreTID = setTimeout(function () {
				var result = {
					content: navMenu.menus[0].origInnerHTML,
					timestamp: new Date().getTime()
				};
				this.updateMenu(result);
				
				// Reset flag
				navMenu.menus[0].isOrig = true;
	
				// Restore the selectedID to original value
				navMenuOpts.selectedID = navMenuOpts.currentID;
			}.bind(this), 500);
		}
	}
};


/******************************************************************************
*  Event listener to cancel restoreMenu() timer if the mouse returns to the
*  menu before the timer executes.
*
*		@param	e			the event object
******************************************************************************/
navMenu.menu.prototype.cancelRestoreMenu = function (e) {
	clearTimeout(navMenu.menus[0].restoreTID);
};


/******************************************************************************
*  Add event listener.
*
*		@param	obj		the DOM object to which the event listener will be attached
*		@param	type		the event type
*		@param	fn			the listener function 
******************************************************************************/
navMenu.menu.prototype.addEvent = function ( obj, type, fn ) {
	if (obj.addEventListener)
		obj.addEventListener( type, fn, false );
	else if (obj.attachEvent) {
		obj["e"+type+fn] = fn;
		obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
		obj.attachEvent( "on"+type, obj[type+fn] );
	}
};


/******************************************************************************
*  Remove event listener.
*
*		@param	obj		the DOM object from which the event listener will be removed
*		@param	type		the event type
*		@param	fn			the listener function 
******************************************************************************/
navMenu.menu.prototype.removeEvent = function ( obj, type, fn ) {
	if (obj.removeEventListener)
		obj.removeEventListener( type, fn, false );
	else if (obj.detachEvent) {
		obj.detachEvent( "on"+type, obj[type+fn] );
		obj[type+fn] = null;
		obj["e"+type+fn] = null;
	}
};


/******************************************************************************
*  Returns the target object of an event
*
*		@param	e			the event object
*		@return				the target object of an event
******************************************************************************/
navMenu.menu.prototype.getEventTarget = function (e) {
	// W3C DOM standard is event.target.parentNode, yet IE uses event.srcElement.parentElement
	if(e.target) {
		if (e.target.nodeType == 3) 
			// Safari: If an event occurs on an element that contains text, the text node, instead of the element, 
			// is considered the target of the event. So move up an additional node in the tree to the actual element.
			return e.target.parentNode;
		else
			// Mozilla
			return e.target;
	}
	if (e.srcElement)
		// IE
		return e.srcElement;
	
	// Default value
	return e;
};


/******************************************************************************
*  Cancel the default event. (i.e. navigating to a clicked link)
*
*		@param	e			the event object
******************************************************************************/
navMenu.menu.prototype.cancelDefault = function (e) {
	if (e && e.preventDefault) {						// DOM
		e.preventDefault();
		// Safari doesn't implement preventDefault so you should also set the target's
		// onclick attribute to cancelDefaultSafari (i.e. [target].onclick = cancelDefaultSafari;)
	} else if (window.event) {	// IE
		window.event.returnValue = false;
	}
};
navMenu.menu.prototype.cancelDefaultSafari = function () {
	return false;
};


/******************************************************************************
*  Stop an event from propogating up to parent elements
*
*		@param	e			the event object
******************************************************************************/
navMenu.menu.prototype.stopPropagation = function (e) {
	if (e && e.stopPropagation) {						// DOM
		e.stopPropagation();
		// Apparently, Safari incorrectly supports stopPropagation and doesn't actually do anything when it's called
	} else if (window.event) {							// Good ol' IE
		window.event.cancelBubble = true;
	}
}


/******************************************************************************
*  Get the related target of an event
*
*		@param	e			the event object
*		@return				the related target of an event
******************************************************************************/
navMenu.menu.prototype.getRelatedTarget = function (e) {
	if (e && e.relatedTarget) {		// DOM
		return e.relatedTarget;
	} else if (window.event) {			// IE			
		return window.event.toElement;
	}
	
	return null;
};


/******************************************************************************
*  Allows for easy prototype inheritance of objects
*  http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/
*
*		@param	descendant	the child object
*	 	@param	parent		the parent object that child will inherit from
******************************************************************************/
function copyPrototype(descendant, parent) {
	var sConstructor = parent.toString();
	var aMatch = sConstructor.match( /\s*function (.*)\(/ );
	if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
	for (var m in parent.prototype) {
		descendant.prototype[m] = parent.prototype[m];
	}
};


/******************************************************************************
*  Binds the 'this' keyword to a specified object when calling a function
*	http://www.brockman.se/writing/method-references.html.utf8
*
*		@param	object	the 'this' object to bind to the function
* 		@return				a function whose 'this' keyword is bound to the parameter
******************************************************************************/
Function.prototype.bind = function (object) {
    var method = this;
    var oldArguments = entries(arguments).slice(1);
    return function () {
        var newArguments = entries(arguments);
        return method.apply(object, oldArguments.concat(newArguments));
    };

};


/******************************************************************************
*  Binds the 'this' keyword to a specified object when calling a function as
*  an event handler. The function has access to the event through the 'event'
*  variable.
*
*		@param	object	the 'this' object to bind to the function
* 		@return				a function whose 'this' keyword is bound to the parameter
******************************************************************************/
Function.prototype.bindEventListener = function (object) {
    var method = this;
    var oldArguments = entries(arguments).slice(1);
    return function (event) {
        return method.apply(object, event || window.event, oldArguments);
    };
};


/******************************************************************************
*  Helper function for bind() and bindEventListener()
*  (Private function)
*
*  Some browsers don't like expressions such as foo.concat(arguments)
*  or arguments.slice(1), due to a kind of reverse duck typing:
*  an argument object looks like a duck and walks like a duck,
*  but it isn't really a duck and it won't quack like one.
******************************************************************************/
function entries(collection) {
    var result = [];  // This is our real duck.

    for (var i = 0; i < collection.length; i++)
        result.push(collection[i]);

    return result;
};


// Element object doesn't exist in IE6 so create it
if (!window.Element) {
	// IE
	var Element = new Object();
};


/******************************************************************************
*  Get parent element of DOM node
*
*		@param	obj				DOM node
*		@return						obj's parent node
******************************************************************************/
Element.getParent = function(obj) {
	// W3C DOM standard is object.parentNode, yet IE uses object.parentElement
	if(obj.parentNode)
		// Mozilla
		return obj.parentNode;
	if (obj.parentElement)
		// IE
		return obj.parentElement;

	// Default value
	return null;
}


/******************************************************************************
*  Get elements by class
*	Written by Dustin Diaz
*
*		@param	obj				DOM node
*		@param	searchClass		CSS class
*		@param	tag				limit matching elements to a specific tag [optional]
*		@return						array containing matching elements
******************************************************************************/
Element.getElementsByClass = function(obj, searchClass, tag) {
	var classElements = new Array();
	if ( obj == null )
		obj = document;
	if ( tag == null )
		tag = '*';
	var els = obj.getElementsByTagName(tag);
	var elsLen = els.length;
	var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
	for (var i = 0, j = 0; i < elsLen; i++) {
		if ( pattern.test(els[i].className) ) {
			classElements[j] = els[i];
			j++;
		}
	}
	return classElements;
};


/******************************************************************************
*  Checks if 'className' has been applied to the object
*
*		@param	obj				DOM node
*		@param	className		CSS class
*		@return						boolean
******************************************************************************/
Element.hasClass = function (obj, className) {
	return new RegExp('\\b' + className + '\\b').test(obj.className);
}


/******************************************************************************
*  Applies 'className' to the object
*
*		@param	obj				DOM node
*		@param	className		CSS class
******************************************************************************/
Element.addClass = function (obj, className) {
	if ( !Element.hasClass(obj, className) )
		obj.className += obj.className ? ' '+className : className;
}


/******************************************************************************
*  Removes 'className' from the object
*
*		@param	obj				DOM node
*		@param	className		CSS class
******************************************************************************/
Element.removeClass = function (obj, className) {
	if ( Element.hasClass(obj, className) ) {
		var classString = obj.className.match(' '+className) ? ' '+className : className;
		obj.className = obj.className.replace(classString, '');
	}
}


/******************************************************************************
*  Checks if an object contains another object
*
*		@param	obja				Ancestor DOM node
*		@param	objd				Descendent DOM node
*		@return						boolean
******************************************************************************/
Element.contains = function(obja, objd) {
	var thisNode = objd;
	while (thisNode) {
		if (thisNode == obja){
			return true;
		}
		thisNode = Element.getParent(thisNode);
	}
	return false;
}; 


// The Prototype dollar function : Basically a shorthand method for getElementById
// Use: $('elementID') or $('elementID1', 'elementID2', ...)
function $() {
	var elements = new Array();
	for (var i = 0; i < arguments.length; i++) {
		var element = arguments[i];
		if (typeof element == 'string')
			element = document.getElementById(element);
		if (arguments.length == 1)
			return element;
		elements.push(element);
	}
	return elements;
}


// Initialize the menu @ window.onload()
navMenu.addLoadEvent(navMenu.init);

