/**
 * WebDDM - The Web Drop Down Menu. It generates drop-down menus
 * on the fly from customizable menu data.
 * 
 * @version: 3.4b4
 * @started: 07/25/2004
 * @copyright: Copyright (c) 2004, 2005 JPortal, All Rights Reserved
 * @website: www.jportalhome.com/webddm
 * @license: GPL vs2.0
 * @subversion: $Id: core.WebDDM.js 126 2006-01-22 07:09:05Z josh $
 */

var WebDDM_version = '3.4b4';

// -- NOTE --
// All initializing code is below the Hash library

/**
 * Hash library function. Creates a new Hash object, and uses passed arguments
 * to populate the Hash.
 * Examples:
 * new Hash('key1', 'value1', 'key2', 'value2', 3, 'value3', 4, 'value4', 5, 6);
 * new Hash({'key1': 'value1', 'key2': 'value2'});
 * new Hash(new Array('val1', 'val2', 'val3'));
 * new Hash(existing_hash_object);
 */
function Hash()
{
	this.length = 0;
	this.numericLength = 0; 
	this.elementData = [];
	
	// If the first passed argument is a hash...
	if (arguments[0] && arguments[0].elementData)
	{
		this.length = arguments[0].length;
		this.numericLength = arguments[0].numericLength;
		this.elementData = arguments[0].elementData;
		return;
	}
	
	// Load hash data from an existing array, if it was passed
	if (typeof(arguments[0]) == 'object')
	{
		for (var i in arguments[0])
		{
			this.set(i, arguments[0][i]);
		}
		return;
	}
	
	// For regular Hash operations - load hash object from passed parameters
	for (var i = 0; i < arguments.length; i += 2)
	{
		if (typeof(arguments[i + 1]) != 'undefined')
		{
			this.set(arguments[i], arguments[i+1]);
		}
	}
};


/**
 * Hash prototype - get. Gets a Hash value that is associated with the passed in_key.
 */
Hash.prototype.get = function(in_key)
{
	return this.elementData[in_key];
};


/**
 * Hash prototype - set. Sets a Hash value that is paired with the passed in_key.
 */
Hash.prototype.set = function(in_key, in_value)
{
	// Allow "auto" to be a valid key, it will be converted to an integer here
	in_key = (in_key == 'auto' ? this.numericLength + 1 : in_key);
	
	if (typeof(in_value) != 'undefined')
	{
		if (typeof(this.elementData[in_key]) == 'undefined')
		{
			++this.length;
			if (parseInt(in_key) == in_key)
			{
				++this.numericLength;
			}
		}

		return this.elementData[in_key] = in_value;
	}

	return false;
};


/**
 * Hash prototype - remove. Removes an element from the Hash associated with the passed in_key.
 */
Hash.prototype.remove = function(in_key)
{
	var tmp_value;
	if (typeof(this.elementData[in_key]) != 'undefined')
	{
		this.length--;
		if (in_key == parseInt(in_key)) 
		{
			this.numericLength--;
		}

		tmp_value = this.elementData[in_key];
		delete this.elementData[in_key];
	}

	return tmp_value;
};


/**
 * Hash prototype - size. Returns the number of elements in the Hash.
 */
Hash.prototype.size = function()
{
	return this.length;
};


/**
 * Hash prototype - has. Checks to see if a key has a value in the Hash.
 */
Hash.prototype.has = function(in_key)
{
	return typeof(this.elementData[in_key]) != 'undefined';
};


/**
 * Hash prototype - find. Searches the Hash for a key that corresponds to the passed
 * value. 
 */
Hash.prototype.find = function(in_obj)
{
	for (var tmp_key in this.elementData) 
	{
		if (this.elementData[tmp_key] == in_obj) 
		{
			return tmp_key;
		}
	}
	return null;
};


/**
 * Hash prototype - merge. Merges the passed Hash into the current Hash.
 */
Hash.prototype.merge = function(in_hash)
{
	for (var tmp_key in in_hash.elementData) 
	{
		if (typeof(this.elementData[tmp_key]) == 'undefined') 
		{
			++this.length;
			if (tmp_key == parseInt(tmp_key)) 
			{
				++this.numericLength;
			}
		}

		this.elementData[tmp_key] = in_hash.elementData[tmp_key];
	}
};


/**
 * Hash prototype - compare. Compares the passed Hash with the current Hash.
 * Returns false if any elements are different, or true if all are the same.
 */
Hash.prototype.compare = function(in_hash)
{
	if (this.length != in_hash.length) 
	{
		return false;
	}

	for (var tmp_key in this.elementData) 
	{
		if (this.elementData[tmp_key] != in_hash.elementData[tmp_key]) 
		{
			return false;
		}
	}
	
	return true;
};


/**
 * "hashify" arrays. Converts an array to a Hash object.
 */
function hashify (in_array)
{
	// Loop through array and convert all array members to Hashes
	if (typeof(in_array) == 'object')
	{
		for (var i in in_array)
		{
			if (typeof(in_array[i]) == 'object')
			{
				in_array[i] = hashify(in_array[i]);
			}
		}
		
		// Make the array a hash
		in_array = new Hash(in_array);
	}
	
	return in_array;
};



// domLib code from Dan Allen's dom Library - www.mojavelinux.com
// domLib is released under the LGPL and can be re-released under the GPL.
// domLib version: 0.6
// -- Browsers --
var domLib_userAgent = navigator.userAgent.toLowerCase();
var domLib_isMac = navigator.appVersion.indexOf('Mac') != -1;
var domLib_isOpera = domLib_userAgent.indexOf('opera') != -1;
var domLib_isOpera7 = (domLib_userAgent.indexOf('opera/7') != -1 || domLib_userAgent.indexOf('opera 7') != -1);
var domLib_isSafari = domLib_userAgent.indexOf('safari') != -1;
var domLib_isKonq = domLib_userAgent.indexOf('konqueror') != -1;
// Both konqueror and safari use the khtml rendering engine
var domLib_isKHTML = (domLib_isKonq || domLib_isSafari);
var domLib_isIE = (!domLib_isKHTML && !domLib_isOpera && (domLib_userAgent.indexOf('msie 5') != -1 || domLib_userAgent.indexOf('msie 6') != -1));
var domLib_isIE5up = domLib_isIE;
var domLib_isIE50 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.0') != -1);
var domLib_isIE55 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.5') != -1);
var domLib_isIE5 = (domLib_isIE50 || domLib_isIE55);
// safari and konq may use string "khtml, like gecko", so check for destinctive /
var domLib_isGecko = domLib_userAgent.indexOf('gecko/') != -1;
var domLib_isMacIE = (domLib_isIE && domLib_isMac);
var domLib_isIE55up = domLib_isIE5up && !domLib_isIE50 && !domLib_isMacIE;
var domLib_isIE6up = domLib_isIE55up && !domLib_isIE55;

// -- Abilities --
var domLib_standardsMode = (document.compatMode && document.compatMode == 'CSS1Compat');
var domLib_useLibrary = (domLib_isOpera7 || domLib_isKonq || domLib_isIE5up || domLib_isGecko || domLib_isKHTML);
var domLib_hasBrokenTimeout = (domLib_isMacIE || (domLib_isKonq && domLib_userAgent.match(/konqueror\/3.([2-9])/) == null));
var domLib_canFade = (domLib_isGecko || domLib_isIE || domLib_isSafari);
var domLib_canDrawOverSelect = (domLib_isGecko || domLib_isOpera || domLib_isMac);

// -- Event Variables --
var domLib_eventTarget = domLib_isIE ? 'srcElement' : 'currentTarget';
var domLib_eventButton = domLib_isIE ? 'button' : 'which';
var domLib_eventTo = domLib_isIE ? 'toElement' : 'relatedTarget';
var domLib_stylePointer = domLib_isIE ? 'hand' : 'pointer';
// NOTE: a bug exists in Opera that prevents maxWidth from being set to 'none', so we make it huge
var domLib_styleNoMaxWidth = domLib_isOpera ? '1000000px' : 'none';
var domLib_hidePosition = '-1000px';
var domLib_scrollbarWidth = 14;
var domLib_autoId = 1;
var domLib_zIndex = 100;

// -- Detect mouse button codes --
var domLib_buttonLeft = (domLib_isGecko || domLib_isOpera || domLib_isIE || domLib_isSafari ? 1 : -1);
var domLib_buttonRight = (domLib_isGecko ? 3 : 2);

// -- Detection --
var domLib_obscuringElements;

// -- Timeouts --
var domLib_timeoutStateId = 0;
var domLib_timeoutStates = new Hash();

// -- WebDDM style constants --
var WebDDM_style_out = '';
var WebDDM_style_rollover = '_rollover';
var WebDDM_style_rolloverPressed = '_rollover_pressed';
var WebDDM_style_menuopenOut = '_menuopen';
var WebDDM_style_menuopenRollover = '_menuopen_rollover';
var WebDDM_style_menuopenPressed = '_menuopen_pressed';

// -- Plugins --
var WebDDM_actionPlugins = new Hash();
var WebDDM_transitionPlugins = new Hash();
var FloatAPI;

// -- Initialize other public variables --
// TRUE if global menu initilization has been done, FALSE otherwise
var WebDDM_globalInitDone = false;
// X,Y position of mouse on the screen.
var WebDDM_mousePosition = new Hash();
// Hash of WebDDM objects
var WebDDMObjects = new Hash();
// Initialize last mouseover element
var WebDDM_lastMouseoverElement = '';
var WebDDM_lastMouseoverElementOwner = false;
// Active event handlers that should be cleaned on unload
var WebDDM_activeEventHandlers = [];

// -- Remove event handlers on unload (fixes IE memory leaks) --
// This code was inspired by IE7
if (window.attachEvent && domLib_isIE)
{
	window.attachEvent("onunload", function()
	{
		// Loop through all set event handlers
		while (WebDDM_activeEventHandlers.length)
		{
			var obj = WebDDM_activeEventHandlers.pop();
			try
			{
				obj[0].detachEvent(obj[1], obj[2]);
			}
			catch (e)
			{
			}
		}
	});
}

// -- Create global items container stylesheet object --
document.write('<style>'+
	'div.WebDDM_items_container {'+
		'position: absolute; top: 0px; left: 0px; visibility: hidden;'+
		'overflow: visible; margin: 0px; padding: 0px;'+
		'border: 0px transparent none; background-color: transparent;'+
		'background-image: url(http://www.jportal.info/blank_img.gif);'+
	'}'+
	'</style>');

/**
 * Specially escapes a string so that it can be put inside of another string to
 * be evaluated. The quoteType should be either ' or ".
 * NOTE: This function is not used in this source file but it is used in some
 * plugins and is still very useful.
 */
function evalEscapeString (str, quoteType)
{
	// Set quote type, defaults to " if not already set
	var quoteType = quoteType ? quoteType : '"';
	// Escape and return string
	return 'unescape('+quoteType+escape(str)+quoteType+')';
};

/**
 * Builds one function object from the two functions passed. The function parameters of
 * the first function take precedence over the parameters of the second function.
 */
function WebDDM_buildFunction (function1, function2)
{
	// Initialize function body and parameters
	var function_body = '';
	var function_params = '';
	// Check that the first function is set; change the function
	// into a string; and parse the function.
	if (function1 && (data = WebDDM_parseFunction(function1 + '')))
	{
		function_body += data[1];
		function_params += data[0];	
	}
	if (function2 && (data = WebDDM_parseFunction(function2 + '')))
	{
		function_body += data[1];
		function_params += (function_params && data[0] ? ',' : '') + data[0];
	}
	// Create final function
	eval('var retFunc = function ('+function_params+') {'+function_body+'};');
	// Return composite
	return retFunc;
};

/**
 * Parses a function and returns an array of [0]=function parameters, [1]=body
 */
function WebDDM_parseFunction (parseFunction)
{
	var data = ['',''];
	if (parseFunction.match(/\s*function\s*\(([^\)]*)\)\s*\{([^$]*)\}\s*/))
	{
		data[0] = RegExp.$1;
		data[1] = RegExp.$2;
	}
	else
	{
		data[1] = parseFunction;
	}
	return data;
};

/**
 * Merge two arrays. Returns the resulting array. Elements in the second array will NOT
 * replace elements in the first array, unless the third argument is "false".
 */
function WebDDM_mergeArrays (baseArray, mergeArray, override, provideIncrementalKeys)
{
	for (var i in mergeArray)
	{
		// Try to make the key into an integer
		if (parseInt(i) == i)
		{
			i = parseInt(i);
		}
		// If we can, put key from the merge array into
		// the base array.
		if (typeof(baseArray[i]) == 'undefined' || override)
		{
			baseArray[i] = mergeArray[i];
		}
		else if (provideIncrementalKeys && typeof(i) == 'number')
		{
			// The key exists in the base array and we cannot override it
			// The key is numeric, and provideIncrementalKeys is TRUE
			// So, push this element to the end of the base array.
			baseArray[baseArray.length] = mergeArray[i];
		}
	}
	return baseArray;
};

/**
 * Clones an array
 */
function WebDDM_cloneArray (ary)
{
	// Clone this array
	ary = WebDDM_mergeArrays({}, ary);
	// Clone all sub-arrays
	for (var index in ary)
	{
		if (typeof(ary[index]) == 'object')
		{
			ary[index] = WebDDM_cloneArray(ary[index]);
		}
	}
	// Return cloned array
	return ary;
};

/**
 * Set a timeout, compatible with all browsers (sometimes timeout functionality 
 * broken).
 */
function domLib_setTimeout(in_function, in_timeout, in_args)
{
	if (typeof(in_args) == 'undefined')
	{
		in_args = [];
	}
	
	// Make sure in_function is a function
	if (typeof(in_function) == 'string')
	{
		eval('var in_function = function () { '+in_function+' }');
	}

	if (in_timeout <= 0)
	{
		in_function(in_args);
		return 0;
	}

	// must make a copy of the arguments so that we release the reference
	var args = WebDDM_mergeArrays({}, in_args);

	if (!domLib_hasBrokenTimeout)
	{
		return setTimeout(function() { in_function(args); }, in_timeout);
	}
	else
	{
		var id = domLib_timeoutStateId++;
		var data = new Hash();
		data.set('function', in_function);
		data.set('args', args);
		domLib_timeoutStates.set(id, data);

		data.set('timeoutId', setTimeout('domLib_timeoutStates.get(' + id + ').get(\'function\')(domLib_timeoutStates.get(' + id + ').get(\'args\')); domLib_timeoutStates.remove(' + id + ');', in_timeout));
		return id;
	}
};

/**
 * Clear a timeout that was set with domLib_setTimeout
 */
function domLib_clearTimeout(in_id)
{
	if (!domLib_hasBrokenTimeout)
	{
		clearTimeout(in_id);
	}
	else
	{
		if (domLib_timeoutStates.has(in_id))
		{
			clearTimeout(domLib_timeoutStates.get(in_id).get('timeoutId'));
			domLib_timeoutStates.remove(in_id);
		}
	}
};

/**
 * Inspect a DOM object.
 *
 * Returns a Hash with the following values:
 *   'left': Left offset of the object from the main document.
 *   'top': Top offset of the object from the main document.
 *   'right': Right offset of the object from the main document.
 *   'bottom': Bottom offset of the object from the main document.
 *   'width': Actual width (in pixels) of the object. Useful if you
 *	  haven't explicitly set the width/don't know what it is.
 *   'height': Actual height (in pixels) of the object. Useful if you
 *	  haven't explicitly set the height/don't know what it is.
 *   'centerLeft': Offset from the main document to the center of the object
 *	 on the X axis.
 *   'centerTop': Offset from the main document to the center of the object
 *	 on the Y axis.
 *   'radius': 'height' if 'height' is greater than 'width', and vice-versa.
 */
function domLib_getOffsets (in_object, stopAt)
{
	stopAt = typeof(stopAt) == 'undefined' ? false : stopAt;
	
	var originalObject = in_object;
	var originalWidth = in_object.offsetWidth;
	var originalHeight = in_object.offsetHeight;
	var offsetLeft = 0;
	var offsetTop = 0;

	while (in_object)
	{
		offsetLeft += in_object.offsetLeft;
		offsetTop += in_object.offsetTop;
		in_object = in_object.offsetParent;
		if (stopAt == in_object)
		{	
			break;
		}
	}

	// MacIE misreports the offsets (even with margin: 0 in body{}), still not perfect
	if (domLib_isMacIE)
	{
		offsetLeft += 10;
		offsetTop += 10;
	}

	return new Hash(
		'left',			offsetLeft,
		'top',			offsetTop,
		'right',		offsetLeft + originalWidth,
		'bottom',		offsetTop + originalHeight,
		'height',		originalHeight,
		'width',		originalWidth,
		'leftCenter',	offsetLeft + originalWidth/2,
		'topCenter',	offsetTop + originalHeight/2,
		'radius',		Math.max(originalWidth, originalHeight) 
	);
};

/**
 * Check if the first argument is the child of the second
 * argument in the DOM. The first argument doesn't have to
 * be a direct child; it can be grand-child, great-grand-child,
 * etc.
 */
function domLib_isDescendantOf(in_object, in_ancestor)
{
	if (in_object == in_ancestor)
	{
		return true;
	}

	while (in_object != document.documentElement)
	{
		try
		{
			if ((tmp_object = in_object.offsetParent) && tmp_object == in_ancestor)
			{
				return true;
			}
			else if ((tmp_object = in_object.parentNode) == in_ancestor)
			{
				return true;
			}
			else
			{
				in_object = tmp_object;
			}
		}
		// in case we get some wierd error, just assume we haven't gone out yet
		catch(e)
		{
			return true;
		}
	}

	return false;
};

/**
 * Gets the X, Y coordinates of the mouse for a particular event object.
 * Base code from PPK's article at evolt.
 */
function domLib_getEventPosition(in_eventObj)
{
	var eventPosition = new Hash('x', 0, 'y', 0);

	// Opera and Konq both offer event properties that give the
	// total offset from top of	document to current scroll offset
	if (domLib_isKonq)
	{
		eventPosition.set('x', in_eventObj.x);
		eventPosition.set('y', in_eventObj.y);
	}
	// IE varies depending on standard compliance mode
	else if (domLib_isIE)
	{
		if (domLib_standardsMode)
		{
			eventPosition.set('x', in_eventObj.clientX + document.documentElement.scrollLeft);
			eventPosition.set('y', in_eventObj.clientY + document.documentElement.scrollTop);
		}
		// NOTE: just in case we fire too early, go ahead and check for document.body
		else if (document.body)
		{
			eventPosition.set('x', in_eventObj.clientX + document.body.scrollLeft);
			eventPosition.set('y', in_eventObj.clientY + document.body.scrollTop);
		}
	}
	else
	{
		eventPosition.set('x', in_eventObj.pageX);
		eventPosition.set('y', in_eventObj.pageY);
	}

	return eventPosition;
};

/**
 * Hides all SELECT objects that are overlapping the passed object.
 * Optionally shows the hidden select objects if the second argument
 * is true (boolean).
 */
function domLib_detectCollisions(in_object, in_recover)
{
	if (typeof(domLib_obscuringElements) == 'undefined')
	{
		// If we can draw over select objects, like we can in at least Firefox
		// and Opera, don't hide selects
		domLib_obscuringElements = domLib_canDrawOverSelect
			? new Hash()
			: document.getElementsByTagName('select');
		// Get all <applet>s in document
		domLib_obscuringElements = WebDDM_mergeArrays(domLib_obscuringElements,
			document.getElementsByTagName('applet'), false, true);
		// Get all <object> tags in document. Flash.
		domLib_obscuringElements = WebDDM_mergeArrays(domLib_obscuringElements,
			document.getElementsByTagName('object'), false, true);
	}

	// If we're supposed to, unhide all hidden <applet>s, <select>s, and <object>s
	if (in_recover)
	{
		// Loop through all elements
		for (var cnt = 0; cnt < domLib_obscuringElements.length; ++cnt)
		{
			var thisObscuringElement = domLib_obscuringElements[cnt];

			// If this obscuring element does not have a Hash of objects
			// it's obscuring, create a new one
			if (!thisObscuringElement.hideList)
			{
				thisObscuringElement.hideList = new Hash();
			}

			// Remove the current object's ID from this obscuring element's hide list
			thisObscuringElement.hideList.remove(in_object.id);
			// If this element is not obscuring any objects, show it
			if (!thisObscuringElement.hideList.length)
			{
				domLib_obscuringElements[cnt].style.visibility = 'visible';
				if (domLib_isKonq)
				{
					domLib_obscuringElements[cnt].style.display = '';
				}
			}
		}

		return;
	}

	// okay, we have an object, so hunt and destroy
	var objectOffsets = domLib_getOffsets(in_object);

	// Loop through all obscuring elements
	for (var cnt = 0; cnt < domLib_obscuringElements.length; ++cnt)
	{
		var thisObscuringElement = domLib_obscuringElements[cnt];

		// If the obscuring element is in the object, then don't hide this
		// obscuring element.
		// :WARNING: is this too costly?
		if (domLib_isDescendantOf(thisObscuringElement, in_object))
		{
			continue;
		}

		// If this obscuring element does not have a Hash of objects
		// it's obscuring, create a new one
		if (!thisObscuringElement.hideList)
		{
			thisObscuringElement.hideList = new Hash();
		}

		var selectOffsets = domLib_getOffsets(thisObscuringElement); 
		var center2centerDistance = Math.sqrt(Math.pow(selectOffsets.get('leftCenter') - objectOffsets.get('leftCenter'), 2) + Math.pow(selectOffsets.get('topCenter') - objectOffsets.get('topCenter'), 2));
		var radiusSum = selectOffsets.get('radius') + objectOffsets.get('radius');
		// the encompassing circles are overlapping, get in for a closer look
		if (center2centerDistance < radiusSum)
		{
			if (
				// DOM object is left of selects/applets/objects
				objectOffsets.get('right') < selectOffsets.get('left') ||
				// DOM object is right of selects/applets/objects
				objectOffsets.get('left') > selectOffsets.get('right') ||
				// DOM object is above selects/applets/objects
				objectOffsets.get('bottom') < selectOffsets.get('top') ||
				// DOM object is below selects/applets/objects
				objectOffsets.get('top') > selectOffsets.get('bottom'))
			{
				// Select/applet/object is not obscuring DOM object
				thisObscuringElement.hideList.remove(in_object.id);
				if (!thisObscuringElement.hideList.length)
				{
					thisObscuringElement.style.visibility = 'visible';
					if (domLib_isKonq)
					{
						thisObscuringElement.style.display = '';
					}
				}
			}
			else
			{
				// Select/applet/object is obscuring DOM object, hide it now
				thisObscuringElement.hideList.set(in_object.id, true);
				thisObscuringElement.style.visibility = 'hidden';
				if (domLib_isKonq)
				{
					thisObscuringElement.style.display = 'none';
				}
			}
		}
	}
};

/**
 * This returns a DOM object with the passed ID.
 */
function WebDDM_getElement (elementId)
{
	return document.all ? document.all[elementId] : document.getElementById(elementId);
};

/**
 * InterCaps a string. Removes dashes, and uppercases the letter
 * directly after. This is useful for CSS. Examples:
 * -khtml-opacity = KhtmlOpacity
 * -moz-opacity = MozOpacity
 * background-color = backgroundColor
 * opacity = opacity
 */
function interCap (str)
{
	while (str.match(/\-(.)/))
	{
		// Character directly after the dash
		var c = RegExp.$1;
		// Intercap it
		str = str.replace('-'+c, c.toUpperCase());
	}
	return str;
};

/**
 * Takes a CSS string and returns an array of key:value pairs.
 */
function parseCssString (cssString)
{
	var css = {};

	// Split string into chunks
	var chunks = cssString.split(/\s*\;\s*/);
	// Loop through chunks
	for (var k in chunks)
	{
		// v will be something like "border-color: red"
		var v = chunks[k];
		if (v.match(/^\s*([a-zA-Z0-9\-]*)\s*\:\s*(.*?)$/))
		{
			css[RegExp.$1] = RegExp.$2;
		}
	}
	return css;
};

/**
 * Cancels event bubbling of the passed event object.
 */
function domLib_cancelBubble(in_event)
{
	if (in_event)
	{
		in_event.cancelBubble = true;
	}
};

/**
 * Attaches event listener to an object, and works across most (if not all) browsers.
 */
function WebDDM_attachEventListener (eventObject, eventName, eventHandler)
{
	// IE/Opera
	if (eventObject.attachEvent)
	{
		eventObject.attachEvent('on'+eventName, eventHandler);
		// IE has memory leaks when assigning event handlers so we
		// keep track of all assigned event handlers and destroy them later
		WebDDM_activeEventHandlers[WebDDM_activeEventHandlers.length] = [eventObject, 'on'+eventName, eventHandler];
	}
	// Gecko
	else if (eventObject.addEventListener)
	{
		eventObject.addEventListener(eventName, eventHandler, false);
	}
	// Other
	else
	{
		eventObject['on'+eventName] = WebDDM_buildFunction(eventHandler, eventObject['on'+eventName]);
	}
}

/**
 * The core WebDDM function/object/class/API.
 * The generated WebDDM object can be retrieved by a call to
 * getWebDDMObject(containerId);
 */
function WebDDM (containerId, menuData)
{
	// Make this WebDDM object easily accessible
	WebDDMObjects.set(containerId, this);
	
	// Make sure we have a valid container
	var container = WebDDM_getElement(containerId);
	if (!container)
	{
		container = document.createElement('div');
		container.id = containerId;
		document.body.appendChild(container);
	}
	
	// Save the container ID and container object
	this.container = container;
	this.containerId = containerId;
	// Intialize the item hash evaluation path cache
	this.hashEvalPathCache = new Hash();
	// Initialize menu
	this.initialize(menuData);
	
	// Return the new WebDDM object :)
	return this;
};

/**
 * Set attributes for a new menu.
 */
WebDDM.prototype.initialize = function (menuData)
{
	// Clone and save the menu data
	this.menuData = hashify(WebDDM_cloneArray(menuData));
	// Initialize the open menu data hash
	this.openMenuData = new Hash();
	// Initialize this menu's zIndex
	domLib_zIndex = (this.menuData.has('zIndex') ? this.menuData.get('zIndex') : domLib_zIndex);
	// Initialize the mouseover status hash
	this.mouseoverStatuses = new Hash();
	// Visibility of SUBITEM elements populated by the toggleVisibility
	// method.
	this.menuOpenStatuses = new Hash();
	// Timeout object for the menu cleaning loop
	this.cleanMenusLoopTimeout = false;
	// Timeout object for the timeout call while cleaning menus; this
	// checks if individual menus can be hidden.
	this.cleanMenuTimeout = false;
	// Used for the above timeout; the menu ID that is being checked.
	this.cleanMenuId = false;
	// First closeable key of menu
	this.firstCloseableKey = false;
	// Timeout ID Hash for showMenu timeouts.
	this.showTimeoutHash = new Hash();

	// Set 'defaultMeasurementUnit', and default to pixels
	this.menuData.set('defaultMeasurementUnit', this.menuData.has('defaultMeasurementUnit') ? this.menuData.get('defaultMeasurementUnit') : 'px');

	// Set "loading" message
	this.container.innerHTML = '<div style="visibility: visible;" id="WebDDM_loading_' + this.container.id + '">A WebDDM menu is loading; please wait!</div>';
	// Set container styles
	var styles = ['position', 'top', 'left', 'width', 'height'];
	for (var i = 0; styles[i]; ++i)
	{
		// Set CSS style
		// If the variable isn't set we may get an error?
		var style = styles[i];
		var value = this.menuData.get(style);
		if (i != 'position')
		{
			this.container.style[style] = this.formatUnit(value);
		}
		else
		{
			this.container.style[style] = value;
		}
	}
	// Set background color to transparent
	this.container.style.backgroundColor = 'transparent';
	// Set visibility and overflow to visible
	this.container.style.visibility = this.container.style.overflow = 'visible';
	// Set zIndex
	this.container.style.zIndex = domLib_zIndex++;
	
	// Make sure expand_menu and contract_menu are automatically set
	this.menuData.set('expand_menu', (this.menuData.has('expand_menu') ? this.menuData.get('expand_menu') : 'auto'));
	this.menuData.set('contract_menu', (this.menuData.has('contract_menu') ? this.menuData.get('contract_menu') : 'none'));
	
	// This variable will be used in global initialization (see block below)
	// It might be modified if there is a right or left-click context menu; if there
	// is, all menus should NOT be closed when the document is clicked.
	var click_closes_all_menus = true;
			
	// IS THIS A FLOATING MENU?
	// If it is, set it up to float
	// It's either floating menus or context menus; never both
	if (this.menuData.get('float') && FloatAPI)
	{
		this.floatHandler = new FloatAPI(container.id, this.menuData.get('top'),
			this.menuData.get('left'), this.menuData.get('floatSpeed'), true);
	}
	
	// IS THIS A CONTEXT MENU?
	// If it is, set code that will open the submenu...
	else if (this.menuData.get('expand_menu').match(/context\(([a-zA-Z\-]*)\,([a-zA-Z0-9\-\_]*)\)/))
	{
		// Get context menu options data
		// Get expand type
		var expandType = RegExp.$1;
		// Get context object
		var contextObjectId = RegExp.$2;
		
		// Now get the context object
		var contextObject;
		if (contextObjectId == 'document')
		{
			contextObject = document;
		}
		else
		{
			contextObject = WebDDM_getElement(contextObjectId);
			// Make sure contextObject is a valid object
			if (!contextObject)
			{
				contextObject = document;
			}
		}
		
		// Create the function code
		var code_end = // Cancel event bubble
				'domLib_cancelBubble(in_event);' +
				// NOTICE: we have to do this timeout or else
				// menus are immediately closed, why?
				'domLib_setTimeout(function()' +
				'{' +
					// Get WebDDM object
					// The next line needs to be an eval for Opera :/
					'eval("var obj = getWebDDMObject(\\"' + this.containerId + '\\");");' +
					// Set menu at cursor...
					'obj.setMenuAtCursor();' +
					// Show submenu
					'obj.showMenu(obj.menuData, "");' +
					// Return false so the event doesn't bubble
					'return false;' +
				'},10);' +
				'return false;' +
			'}';

		// Now get "event" handler name
		// MAC KEYS (boolean): altKey, ctrlKey, shiftKey, metaKey
		var contextEvent;
		var eventButton;
		var contextFunctionCode_oncontext = function (in_event) {};
		var modifierKeys = '';
		
		// We have to see how the menu is supposed to open.
		// On left-click?
		if (expandType == 'left-click')
		{
			contextEvent = 'mouseup';
			eventButton = domLib_buttonLeft;
 		}
		// Is this a right-click/control-click context menu?
		else if (expandType == 'context')
		{
			contextEvent = (domLib_isIE ? 'contextmenu' : 'mouseup');
			eventButton = (domLib_isIE ? 0 : domLib_buttonRight);

			// If browser is on a Macintosh, we have to adapt for the control-clicking scheme.
			// I still can't get Safari or Opera to work, but Firefox does fine.
			if (domLib_isMac)
			{
				eventButton = domLib_buttonLeft;
				modifierKeys = '|| !in_event.ctrlKey';
			}
			
			// We need special code for oncontextmenu on IE
			eval('var contextFunctionCode_oncontext = function (in_event) { domLib_cancelBubble(in_event); }');
		}
		// For anything else, use mouseover
		else
		{
			contextEvent = 'mouseover';
		}

		// Set final event code
		eval('var contextFunctionCode = function (in_event) {' +
			// Check for event button?
			(eventButton || modifierKeys
				? 'if (in_event[domLib_eventButton] != '+eventButton+modifierKeys+') { return; }'
				: ''
			) +
			code_end
		);
		
		// Now set the submenu opener code
		WebDDM_attachEventListener(contextObject, contextEvent, contextFunctionCode);
		if (!domLib_isIE)
		{
			WebDDM_attachEventListener(contextObject, 'contextmenu', contextFunctionCode_oncontext);
		}

		// Set styles of top-level submenu
		this.menuData.set('position', 'absolute');
		container.style.position = 'absolute';
	}
	
	// If the global menu initializations haven't been done yet,
	// do them now.
	if (!WebDDM_globalInitDone)
	{
		// Set code to capture the mouse position onscreen
		var mousemoveCode = function (in_event)
		{
			// Make sure we have a valid event (mainly for IE)
			// It may not pass us the event object; instead it
			// sets the global variable "event".
			var in_event = in_event ? in_event : window.event;
			WebDDM_mousePosition = domLib_getEventPosition(in_event);
			
			// Loop through any plugin callback functions
			if (WebDDM_actionPlugins.has('document_mousemove'))
			{
				for (var index in WebDDM_actionPlugins.get('document_mousemove').elementData)
				{
					WebDDM_actionPlugins.get('document_mousemove').get(index)(in_event);
				}
			}
		};
		
		// Set document.onmousedown and onblur code so all submenus in all menus are
		// hidden
		var code_mousedown = function (in_event)
		{
			// Sometimes errors occur if this function has not yet loaded (Mozilla)
			if (WebDDM_hideAllMenus)
			{
				WebDDM_hideAllMenus(true);
			}
			
			// Loop through any plugin callback functions
			if (WebDDM_actionPlugins.has('document_click'))
			{
				for (var index in WebDDM_actionPlugins.get('document_click').elementData)
				{
					WebDDM_actionPlugins.get('document_click').get(index)(in_event);
				}
			}
		};
		var code_blur = function (in_event)
		{
			// Sometimes errors occur if this function has not yet loaded (Mozilla)
			if (WebDDM_hideAllMenus)
			{
				WebDDM_hideAllMenus();
			}
			
			// Loop through any plugin callback functions
			if (WebDDM_actionPlugins.has('document_blur'))
			{
				for (var index in WebDDM_actionPlugins.get('document_blur').elementData)
				{
					WebDDM_actionPlugins.get('document_blur').get(index)(in_event);
				}
			}
		};
		
		// Now assign actions to objects
		WebDDM_attachEventListener(document, 'mousedown', code_mousedown);
		WebDDM_attachEventListener(document, 'blur', code_blur);
		WebDDM_attachEventListener(document, 'mousemove', mousemoveCode);
				
		// We've initialized WebDDM globally for all future menus and this
		// one; make sure it doesn't happen again
		WebDDM_globalInitDone = true;
	}
	
	// If we are supposed to, automatically show the base menu.
	if (this.menuData.get('expand_menu').match('auto'))
	{
		this.showMenu(this.menuData, "");
	}
	
	// Remove the loading message
	WebDDM_getElement('WebDDM_loading_' + this.container.id).style.visibility = 'hidden';
	WebDDM_getElement('WebDDM_loading_' + this.container.id).innerHTML = '';
};

/**
 * Set the menu on the cursor.
 */
WebDDM.prototype.setMenuAtCursor = function ()
{
	// Get positions
	var topPos = WebDDM_mousePosition.get('y') + parseInt(this.menuData.get('top'));
	var leftPos = WebDDM_mousePosition.get('x') + parseInt(this.menuData.get('left'));
	
	// Set positions for object
	WebDDM_getElement(this.containerId).style.top = topPos + 'px';
	WebDDM_getElement(this.containerId).style.left = leftPos + 'px';
};

/**
 * Build an item.
 */
WebDDM.prototype.buildMenu = function (parentItemHash)
{
	// Uncomment this to get build time
//	var start_time = (new Date()).getTime();

	// Get item ID
	var itemId = parentItemHash.get('itemId') || this.containerId;
		
	//
	// -- Build items container --
	//
	
	// Get the items hash
	var itemsHash = parentItemHash.get('items');
	
	// Get parent element ID where we will insert this node
	var parentItemsObj = WebDDM_getElement(
		parentItemHash.get('parentId')
			? parentItemHash.get('parentId') + '_subitems'
			: this.containerId);
	
	// Create the item container
	var itemsContainer = document.createElement('div');
	itemsContainer.id = itemId + '_subitems';
	itemsContainer.className = 'WebDDM_items_container';
	
	// Set initial positions
	itemsContainer.style.top = this.formatUnit(itemsHash.get('top'));
	itemsContainer.style.left = this.formatUnit(itemsHash.get('left'));

	// Now add positions of parent item as margins
	// I don't really like doing it this way, because the parent elements MAY have
	// positions set in units other than pixels - doing it this way makes the menu potentially
	// look weird if the window is resized... but, is there any other way? Since we don't
	// just have to jump to the position of the parent item, we also have to jump to the
	// position of the parent item's background item, I don't see any other way...
	if (!itemsHash.has('useOldPositioning') && !this.menuData.has('useOldPositioning'))
	{
		var parentBgItemObj;
		if (parentBgItemObj = WebDDM_getElement(parentItemHash.get('parentId')+'_subitems_background'))
		{
			var parentItemObj = WebDDM_getElement(itemId+'_item_container');
			itemsContainer.style.marginTop = this.formatUnit(parentBgItemObj.offsetTop + parentItemObj.offsetTop);  
			itemsContainer.style.marginLeft = this.formatUnit(parentBgItemObj.offsetLeft + parentItemObj.offsetLeft);  
		}
	}

	// Set events for item container
	var getWebDDMObjectCode = 'getWebDDMObject(\''+this.containerId+'\')';
	eval('var onmouseoverCode = function () {'+getWebDDMObjectCode+'.setMouseoverStatus(\''+itemId+'_subitems\', true, false, false);}');
	eval('var onmouseoutCode =  function () {'+getWebDDMObjectCode+'.setMouseoverStatus(\''+itemId+'_subitems\', false, false, false);}');
	WebDDM_attachEventListener(itemsContainer, 'mousemove', onmouseoverCode);
	WebDDM_attachEventListener(itemsContainer, 'mouseout', onmouseoutCode);

	// Add HTML of all items to the subitem container
	var bg_id = itemId + '_subitems_background';
	var scrollarea_id = itemId + '_subitems_scrollarea';
	var top = '', left = '', width = '', height = '', className = '', css ='';
	var bgItemSet = false;
	if (itemsHash.has('background-item'))
	{
		bgItemSet = true;
		var bg_item = itemsHash.get('background-item');
		top = bg_item.has('top') ? 'top: ' + this.formatUnit(bg_item.get('top')) + ';' : ''; 
		left = bg_item.has('left') ? 'left: ' + this.formatUnit(bg_item.get('left')) + ';' : ''; 
		className = bg_item.has('class') ? ' class="'+bg_item.get('class')+'"' : ''; 
		css = bg_item.has('css') ? bg_item.get('css') + ';' : '';

		// Width and height operations
		if (bg_item.has('width'))
		{
			width = 'width: ' + this.formatUnit(bg_item.get('width')) + ';';
			itemsContainer.style.width = this.formatUnit(bg_item.get('width'));
		}
		if (bg_item.has('height'))
		{
			height = 'height: ' + this.formatUnit(bg_item.get('height')) + ';';
			itemsContainer.style.height = this.formatUnit(bg_item.get('height'));
		}
	}

	//
	// -- Build scroll area --
	// If there is are scroll area attributes set...
	var scrollarea_css = 'top:0px;left:0px;width:0px;height:0px;overflow:visible;';
	var scrollarea_innerhtml = '&nbsp;';
	if (itemsHash.has('scroll-area'))
	{
		// Get scroll config
		var scrollerItemsHash = itemsHash.get('scroll-area');
		
		// Get scrolling type
		// (not needed/used currently)
//		var scrollType = scrollerItemsHash.get('scroll-type');
		
		// Switch to scroll type
//		switch (scrollType)
//		{
			// -- Native scrolling --
//			case 'native':
//			default:
				scrollarea_css = 'overflow:auto;' +
					'top:' + this.formatUnit(scrollerItemsHash.get('top')) + ';' +
					'left:' + this.formatUnit(scrollerItemsHash.get('left')) + ';' +
					'width:' + this.formatUnit(scrollerItemsHash.get('visible-width')) + ';' +
					'height:' + this.formatUnit(scrollerItemsHash.get('visible-height')) + ';';
//				break;
//		}
	}

	// Build item container, background item, and scroller	
	WebDDM_preloadImages('http://www.jportal.info/blank_img.gif');
	var itemsContainerHTML =
		'<div id="'+bg_id+'" style="background-color:transparent;background-image:url(http://www.jportal.info/blank_img.gif);border-style:none;margin:0px;padding:0px;visibility:inherit;position:absolute;'+top+left+width+height+';z-index:'+(++domLib_zIndex)+';">' +
			'<table style="background-color:transparent;background-image;url(http://www.jportal.info/blank_img.gif);border-style:none;margin:0px;padding:0px;width:100%;height:100%;background-color:transparent;visibility:inherit;" cellpadding="0" cellspacing="0"><tbody><tr>' +
				'<td '+className+' style="visibility:inherit;line-height:1px;'+css+'">' +
					'&nbsp;' +
					'<div id="'+scrollarea_id+'" style="margin:0px;padding:0px;visibility:inherit;position:absolute;'+scrollarea_css+'">' +
						scrollarea_innerhtml; // ... to be continued... at the end of this function			
	
	//
	// -- Build items --
	//
		
	// Last item positions
	var marginTop = 0, marginLeft = 0;
	var lastItemHash = new Hash('top', 0, 'left', 0);
	
	// Loop through all the items in the hash of items
	for (var itemIndex = 1; itemsHash.has(itemIndex); ++itemIndex)
	{
		// Get item ID
		var thisItemId = itemId + '_' + itemIndex;
		// Get item hash
		var itemHash = itemsHash.get(itemIndex);
		// Initialize item attributes
		this.initializeItemAttributes(itemHash, thisItemId);

		// Call any plugins needed...
		if (WebDDM_actionPlugins.has('item_init'))
		{
			for (var index in WebDDM_actionPlugins.get('item_init').elementData)
			{
				WebDDM_actionPlugins.get('item_init').get(index)(false, this, itemHash);
			}
		}
		
		// Make sure the item has top and left positions, calculate item offsets (as
		// margins) and set top and left positions to offsets if needed
		// Get top positions
		if (!itemHash.has('top'))
		{
			marginTop += parseFloat(lastItemHash.get('top'));
			itemHash.set('top', (itemHash.has('offsetTop') ? itemHash.get('offsetTop') : 0));
		}
		else
		{
			marginTop = 0;
		}

		// Get left positions
		if (!itemHash.has('left'))
		{
			marginLeft += parseFloat(lastItemHash.get('left'));
			itemHash.set('left', (itemHash.has('offsetLeft') ? itemHash.get('offsetLeft') : 0));
		}
		else
		{
			marginLeft = 0;
		}
		
		// Get cursor and correct it
		// If it is hand or pointer, make sure that in IE it's 'hand' and in
		// gecko browsers it's 'pointer'
		var cursor = (itemHash.has('cursor')
			? 'cursor:' + (itemHash.get('cursor').match(/hand|pointer/)
				? domLib_stylePointer
				: itemHash.get('cursor')) + ';'
			: '');

		// Get user-defined CSS
		var css = (itemHash.has('css') ? itemHash.get('css') + ';' : '') + cursor;
		
		// Get item class
		var itemClass = (itemHash.has('class') ? ' class="' + itemHash.get('class') + '"' : '');
			
		// Populate item CSS
		var div_css = 'visibility:inherit; position:absolute; margin:0px; padding:0px;'+
			'margin-top:'+this.formatUnit(marginTop)+'; margin-left:'+this.formatUnit(marginLeft)+'; '+
			'top:'+this.formatUnit(itemHash.get('top'))+';'+
			'left:'+this.formatUnit(itemHash.get('left'))+';'+
			'width:'+this.formatUnit(itemHash.get('width'))+';'+
			'height:'+this.formatUnit(itemHash.get('height'))+';';
			
		// Build event code
		var event_code = ' onmouseover="'+getWebDDMObjectCode+'.mouseAction(event,\'mouseover\',\''+thisItemId+'\');"' +
			' onmouseout="'+getWebDDMObjectCode+'.mouseAction(event,\'mouseout\',\''+thisItemId+'\');"' + 
			' onclick="'+getWebDDMObjectCode+'.mouseAction(event,\'click\',\''+thisItemId+'\');"' +
			' onmouseup="'+getWebDDMObjectCode+'.mouseAction(event,\'mouseup\',\''+thisItemId+'\');"' +
			' onmousedown="'+getWebDDMObjectCode+'.mouseAction(event,\'mousedown\',\''+thisItemId+'\');"';
			
		// Create item
		var item_container_id = thisItemId+'_item_container';
		itemsContainerHTML +=
			// Build table/div
			'<div id="'+item_container_id+'" style="'+div_css+'" '+event_code+'><table style="width:100%;height:100%;background-color:transparent;" cellpadding="0" cellspacing="0"><tbody><tr><td id="'+thisItemId+'_item"'+itemClass+' style="'+css+'">' +
				itemHash.get('content') +
			'</td></tr></tbody></table></div>';
		
		// -- Save last item hash --
		lastItemHash = itemHash;
	}
	
	// Set innerHTML of items container
	itemsContainer.innerHTML = itemsContainerHTML + '</div></td></tr></tbody></table></div>';

	// Append items container to DOM
	parentItemsObj.appendChild(itemsContainer);
	
	// If the items container doesn't have a height/width set, try to get it automatically..
	// this is to prevent weird bugs where the onmouseout/onmouseover code of the items
	// container doesn't ever execute
	if (!bgItemSet)
	{
		var itemsContainer = WebDDM_getElement(itemId+'_subitems');

		// Get extreme item positions: (L)eft, (R)ight, (T)op, (B)ottom.
		var extremeL = 99999999999999;
		var extremeR = 0;
		var extremeT = extremeL;
		var extremeB = 0;
		for (var i = 1; itemsHash.has(i); ++i)
		{
			var itemOffsets = domLib_getOffsets(WebDDM_getElement(itemId+'_'+i+'_item_container'));

			extremeL = (itemOffsets.get('left') < extremeL) ? itemOffsets.get('left') : extremeL;  
			extremeR = (itemOffsets.get('right') > extremeR) ? itemOffsets.get('right') : extremeR;
			extremeT = (itemOffsets.get('top') < extremeT) ? itemOffsets.get('top') : extremeT;  
			extremeB = (itemOffsets.get('bottom') > extremeB) ? itemOffsets.get('bottom') : extremeB;
		}
		
		// Now set dimensions of item container and background item
		WebDDM_getElement(bg_id).style.height = itemsContainer.style.height = (extremeB - extremeT) + 'px';
		WebDDM_getElement(bg_id).style.width = itemsContainer.style.width = (extremeR - extremeL) + 'px';
	}
		
	// Uncomment to get build time
//	var stop_time = (new Date()).getTime();
//	alert('Build time: ' + (stop_time - start_time) + 'ms.');
};

/**
 * Rebuild an existing menu, from scratch
 */
WebDDM.prototype.rebuildMenu = function (newMenuData)
{
	this.initialize(newMenuData);
};

/**
 * Sets the mouseover status of an element. The mouseover status can be
 * retrieved using the getMouseoverStatus method.
 */
WebDDM.prototype.setMouseoverStatus = function (elementId, status, setAsLast, executeLastElementMouseout)
{
	// Set last mouseover to "false" since if we're over a different item,
	// we're not over the last one now. Also execute it's onmouseout code.
	if (executeLastElementMouseout && WebDDM_lastMouseoverElement && WebDDM_lastMouseoverElement != elementId)
	{
		// Set mouseover status to false
		WebDDM_lastMouseoverElementOwner.mouseoverStatuses.set(WebDDM_lastMouseoverElement, false);

		// Execute onmouseout code
		WebDDM_lastMouseoverElementOwner.mouseAction(0, 'mouseout', WebDDM_lastMouseoverElement);

		// Kill all menu cleaning threads
		for (var i = this.openMenuData.length - 1; this.openMenuData.has(i); i--)
		{
			var itemHash = this.openMenuData.get(i);
			if (itemHash.cleanThread)
			{
				domLib_clearTimeout(itemHash.cleanThread);
			}
		}
	}

	// Set last mouseover ID and element
	if (setAsLast)
	{
		WebDDM_lastMouseoverElement = elementId;
		WebDDM_lastMouseoverElementOwner = this;
	}
		
	// Set current mouseover status
	this.mouseoverStatuses.set(elementId, status);
};

/**
 * Returns the current mouseover status of the element with the passed
 * ID.
 */
WebDDM.prototype.getMouseoverStatus = function (elementId)
{
	return this.mouseoverStatuses.has(elementId) ? this.mouseoverStatuses.get(elementId) : false;
};

/**
 * Checks whether a menu can be automatically hidden or not. If the menu does not
 * have "rollout" in it's contract_menu data hash parameter, or if the
 * mouse is over the menu, then it returns false.
 */
WebDDM.prototype.canHideMenu = function (itemHash)
{
	// Now get the item ID that we use
	var itemId = itemHash.get('itemId');
	
	// Can the menu be closed at all? If contract_menu
	// is not set or "rollout" is not in it, it can't be
	var contractMenu = itemHash.has('contract_menu') ? itemHash.get('contract_menu') : false;
	if (!contractMenu && itemId != this.containerId && itemId)
	{
		return true;
	}
	if (contractMenu && (!contractMenu.match(/rollout/) || contractMenu.match(/none/)))
	{
		return false;
	}
	
	// Now we know that we can close this menu, but we shouldn't if the mouse is over
	// the items or subitems
	if (this.getMouseoverStatus(itemId))
	{
		return false;
	}
	
	// Is the mouse over the background item, or parent item container?
	if (itemHash.has('parentId') && this.getMouseoverStatus(itemHash.get('parentId') + '_subitems'))
	{
		return false;
	}
	
	// Look to see if any subitems are moused over...
	// NOTE: I want to remove this code but it breaks context menu examples. Is the
	// onmouseover event not bubbling..?
	if (itemHash.has('items') && this.menuData.get('expand_menu').match('context'))
	{
		for (var i = 1; itemHash.get('items').has(i); ++i)
		{
			if (!this.canHideMenu(itemHash.get('items').get(i)))
			{
				return false;
			}
		}
	}
	
	// We can hide this menu
	return true;
};

/**
 * Initialize an item's attributes, like itemId, parentItemId, etc.
 */
WebDDM.prototype.initializeItemAttributes = function (itemHash, itemId)
{
	// Remove container ID from item ID
	var bareItemId = itemId.replace(this.containerId + '_', '');
	// Get menu level
	itemHash.set('menuLevel', (bareItemId ? bareItemId.split('_').length : 0));
	// Get parent ID
	itemHash.set('parentId', (itemId.match(/^(.*?)\_([0-9]+)$/) ? RegExp.$1 : ''));
	// Set item and container IDs in the item hash
	itemHash.set('itemId', itemId);

	// Set beginning styles
	itemHash.set('currentStyleContent', '');
	itemHash.set('currentStyleCSS', '');
	itemHash.set('currentStyleClass', '');
	
	// Set defaults for this item hash
	itemHash.set('expand_menu', (itemHash.has('expand_menu') ? itemHash.get('expand_menu') : 'rollover'));
	itemHash.set('contract_menu', (itemHash.has('contract_menu') ? itemHash.get('contract_menu') : 'rollout'));
	itemHash.set('closeDelay', (itemHash.has('closeDelay') ? itemHash.get('closeDelay') : 100));
};

/**
 * Get an item hash corresponding to the passed item ID.
 * Item ID is in this format: "1_1_2_3"
 */
WebDDM.prototype.getItemHash = function (itemId)
{
	// Get item ID
	var itemId = (this.containerId == itemId ? '' : itemId);

	// Remove container ID from item ID
	var bareItemId = itemId.replace(this.containerId + '_', '');

	// If this item hash evaluation path has not been cached,
	// create it.
	if (!this.hashEvalPathCache.has(bareItemId))
	{
		var evalCode = 'var itemHash = this.menuData';
		if (itemId != '')
		{
			var chunks = bareItemId.split('_');
			for (var i = 0; i < chunks.length; ++i)
			{
				evalCode += '.get("items")' + (parseInt(chunks[i]) == chunks[i] ? '.get('+chunks[i]+')' : '');
				if (parseInt(chunks[i]) != chunks[i])
				{
					break;
				}
			}
		}
		evalCode += ';';
		
		// Save (cache) eval code
		this.hashEvalPathCache.set(bareItemId, evalCode);
	}
	
	// Evaluate cached eval code
	eval(this.hashEvalPathCache.get(bareItemId));
	
	// Return item hash
	return itemHash;
};

/**
 * Checks whether a menu is open or not.
 */
WebDDM.prototype.menuIsOpen = function (itemId)
{
	// Basically, we'll just look at the hash of 
	// visible elements populated by the toggleVisibility
	// method. We can't directly look at the CSS visibility
	// property because an element may be transitioning out,
	// in which case the menu is closed but still visible.
	return (this.menuOpenStatuses.has(itemId) ? this.menuOpenStatuses.get(itemId) : false);
};

/**
 * Hides menus on or above the level of the passed item hash.
 */
WebDDM.prototype.hideMenusOnLevel = function (itemHash)
{
	var menuLevel = itemHash.get('menuLevel');
	var itemId = itemHash.get('itemId');

	// If the item hash level is above 0, continue
	if (itemHash.get('menuLevel') > 0)
	{
		// Loop through the open menus
		for (var tmp_key = (this.openMenuData.size()-1); tmp_key >= 0; tmp_key--)
		{
			// Get menu data
			var tmp_itemHash = this.openMenuData.get(tmp_key);
			var tmp_menuLevel = tmp_itemHash.get('menuLevel');
			var tmp_itemId = tmp_itemHash.get('itemId');

			// If the open menu meets certain criteria, hide it
			if (tmp_menuLevel >= menuLevel && !tmp_itemId.match(itemId))
			{
				this.hideMenu(tmp_itemHash);
			}
		}
	}
};
 
/**
 * Shows a menu's submenu. Builds the submenu if needed.
 */
WebDDM.prototype.showMenu = function (itemHash, itemId)
{
	// Uncomment this to get show time
//	var start_time = (new Date()).getTime();

	// Close all menus from other WebDDM objects
	WebDDM_hideAllMenus(false, this.containerId);
	
	// Close all menus on/above this level
	this.hideMenusOnLevel(itemHash);
	
	// If there are no subitems, stop now
	if (!itemHash.has('items'))
	{
		return;
	}
	
	//  Set menu open status
	this.menuOpenStatuses.set(itemId, true);
	
	// Get key for this Item in the hash of open menus
	var thisMenuOpen = false;
	var thisArrayId = this.openMenuData.find(itemHash) || -1;
	if (thisArrayId != -1)
	{
		thisMenuOpen = true;
	}
	
	// Get Item Or Container ID
	var itemOrContainerId = itemId || this.containerId;

	// Build menu
	if (!WebDDM_getElement(itemOrContainerId + '_subitems'))
	{
		this.buildMenu(itemHash);
	}

	// Increase the z-index
	WebDDM_getElement(this.containerId).style.zIndex = domLib_zIndex++;
	WebDDM_getElement(itemOrContainerId + '_subitems').style.zIndex = domLib_zIndex++;
	
	// The menu has not been opened yet.
	// Assign it a new hash key.
	if (thisMenuOpen == false)
	{
		// Clear menu cleaning timeouts
		if (this.cleanMenuTimeout)
		{
			domLib_clearTimeout(this.cleanMenuTimeout);
			this.cleanMenuTimeout = false;
		}

		// Get new array ID
		thisArrayId = this.openMenuData.size();

		// Change style to open
		if (itemId)
		{
			this.setItemStyle(itemHash, WebDDM_style_menuopenOut);
		}
		else if (itemHash.get('contract_menu').match('rollout'))
		{
			// There is no item_id, meaning that this is the base menu
			// Set the mouseover status to true
			// The menu closes on rollout, meaning that
			// (a) the menu is a context menu or (b) the user
			// is a crackhead. Assume A - the first item
			// will likely be placed directly under the cursor,
			// and the onmouseover code will not be executed
			// right away - so the mouseover status will be
			// FALSE, and the menu will be closed right away.
			this.setMouseoverStatus(this.containerId + '_subitems', true, false, false);
		}

		// Put this item hash in the open menu data hash
		this.openMenuData.set(thisArrayId, itemHash);
	}
	
	// Reposition as necessary (REALLY necessary with cliptrans)
	// This is quite time-consuming unfortunately.
	if (itemHash.get('itemId'))
	{
		var itemsHash = this.getItemHash(itemHash.get('itemId').replace(/[0-9]$/, ''));
		if (!itemsHash.has('useOldPositioning') && !this.menuData.has('useOldPositioning'))
		{
			var parentBgItemObj;
			if (parentBgItemObj = WebDDM_getElement(itemHash.get('parentId')+'_subitems_background'))
			{
				var parentItemObj = WebDDM_getElement(itemId+'_item_container');
				var itemsContainer = WebDDM_getElement(itemOrContainerId + '_subitems');
				itemsContainer.style.marginTop = parentBgItemObj.offsetTop + parentItemObj.offsetTop + 'px';  
				itemsContainer.style.marginLeft = parentBgItemObj.offsetLeft + parentItemObj.offsetLeft + 'px';  
			}
		}
	}

	// Show menu
	this.setVisibility(itemHash.get('items'), itemOrContainerId + '_subitems', true);
		
	// Loop through all items	
	for (var i = 1; itemHash.get('items').has(i); ++i)
	{
		// Get item hash
		var subItemHash = itemHash.get('items').get(i);
		
		// Get subitem ID
		var subItemId = subItemHash.get('itemId');

		// If there are any subitems that have "expand_menu" set to "auto",
		// open them now.
		// Check if the expand_menu of the subitem is set to auto
		var expandMenu = subItemHash.get('expand_menu');
		if (expandMenu && expandMenu.match('auto'))
		{
			// Show subitem's subitems
			this.showMenu(subItemHash, subItemId);

			// Set mouseover status of subitem to true if 
			// the subitem's subitems close with rollout.
			var contractMenu = subItemHash.get('contract_menu');
			if (contractMenu && contractMenu.match('rollout'))
			{
				this.setMouseoverStatus(subItemId, true, true, false);					
			}
		}
	}

	// Set loop for cleaning menus
	if (!this.cleanMenusLoopTimeout)
	{
		this.cleanMenusLoop();
	}
	
	// Try to get the first closeable openMenuData key
	if (this.firstCloseableKey === false)
	{
		this.getFirstCloseableKey();
	}

	// Uncomment this to get show time
//	var end_time = (new Date()).getTime();
//	alert("Time to show menu " + itemHash.get('itemId') + ": " + (end_time - start_time) + " milliseconds");
};

/**
 * "Checks" a click menu when opening it; basically looks to see if there
 * are any click menus open on the same level, if there are, close them
 * and open this  menu.
 */
WebDDM.prototype.checkClickMenu = function (itemId)
{
	// Get item hash
	var itemHash = this.getItemHash(itemId);
	
	// Get bare item ID
	var bareItemId = itemId.replace(this.containerId + '_', '');
	
	// Is there a menu open on this level?
	if (itemHash.get('menuLevel') > 0)
	{
		for (var i = (this.openMenuData.size() - 1); i > 0; i--)
		{
			var data = this.openMenuData.get(i);
			if (data && data.get('menuLevel') == itemHash.get('menuLevel') && data.get('itemId') != itemId)
			{
				// There is a menu open on this level. Does it
				// expand with onclick?
				if (data.get('expand_menu').match('click'))
				{
					if (data.get('contract_menu').match('click'))
					{
						// Show the menu and break the loop
						this.showMenu(itemHash, itemId);
						break;
					}
					else if (data.get('contract_menu').match('rollout'))
					{
						// The current menu on this level closes on rollout.
						// In this case, it should NOT be open now since we're
						// not over the menu! So just close it (and all
						// children) here
						this.hideMenusOnLevel(itemHash);
					}
				}
			}
		}
	}
};

/**
 * Start the menu cleaning loop; calls cleanMenus every X milliseconds.
 */
WebDDM.prototype.cleanMenusLoop = function ()
{
	// Set another timeout
	if ((this.openMenuData.size()-1) >= this.firstCloseableKey)
	{
		this.cleanMenusLoopTimeout = domLib_setTimeout(function (args)
		{
			args[0].cleanMenusLoopTimeout = false;
			args[0].cleanMenusLoop();
		}, 50, [this]);
	}
	this.cleanMenus();
};
	
/**
 * Clean menus - closes menus if possible.
 */
WebDDM.prototype.cleanMenus = function ()
{
	// Loop through all open menus
	// If any can be closed now, set a small timeout and try to close it later
	// if we try to close it now, we get the weird effect of menus never seeming to open
	// (try it - change itemHash.get('closeDelay') to 0)
	for (var i = this.openMenuData.length - 1; this.openMenuData.has(i); i--)
	{
		var itemHash = this.openMenuData.get(i);
		if (this.canHideMenu(itemHash))
		{
			itemHash.cleanThread = domLib_setTimeout(function (args)
			{
				var cItemHash = args[0].openMenuData.get(args[1]);
				if (cItemHash && args[0].canHideMenu(cItemHash))
				{
					args[0].hideMenu(cItemHash);
				}
			}, itemHash.get('closeDelay'), [this, i]);
		}
		else
		{
			break;
		}
	}	
};

/**
 * Hides a submenu with the specified parent item hash.
 * Note that the passed hash must point to the LAST OPEN SUBMENU in the
 * openMenuData hash; if it doesn't, then nothing will happen.
 */
WebDDM.prototype.hideMenu = function (itemHash, setMouseoverStatus)
{
	// Make sure the item hash is valid
	if (!itemHash)
	{
		return;
	}

	// Make sure setMouseoverStatus is valid 
	if (typeof(setMouseoverStatus) == 'undefined')
	{
		setMouseoverStatus = true;
	}
	
	// Make sure passed item_id matches the item ID in the item hash
	if (itemHash != this.openMenuData.get(this.openMenuData.size() - 1))
	{
		return;
	}

	// Get item ID
	var itemId = itemHash.get('itemId');
	
	if (setMouseoverStatus)
	{
		// Set menuover status
		this.setMouseoverStatus(itemId, false, false, false);
	}

	// Change styles
	if (itemHash.get('itemId'))
	{
		this.setItemStyle(itemHash,
			(this.getMouseoverStatus(itemHash.get('itemId'))
				? WebDDM_style_rollover
				: WebDDM_style_out));
	}
	
	// If this is the base menu and this is a context menu, move it offscreen
	if (!itemHash.get('itemId') && itemHash.get('expand_menu').match('context'))
	{
		WebDDM_getElement(this.containerId).style.top = '-10000px';
		WebDDM_getElement(this.containerId).style.left = '-10000px';
	}

	// Hide all the items
	this.setVisibility(itemHash.get('items'), (itemHash.get('itemId') || this.containerId) + '_subitems', false);

	this.openMenuData.remove(this.openMenuData.size() - 1);

	//  Set menu open status
	this.menuOpenStatuses.set(itemHash.get('itemId'), false);
};

/**
 * Hides all menus.
 */
WebDDM.prototype.hideAllSubmenus = function (calledOnClick)
{
	// Prevent errors from occuring...
	if (!this.openMenuData.has(this.firstCloseableKey))
	{
		return;
	}
	
	// Hide all item's submenus
	var childrenHidden = false;
	for (var i = (this.openMenuData.size() - 1); i >= this.firstCloseableKey; i--)
	{		
		if (!childrenHidden && calledOnClick && this.openMenuData.get(i).get('contract_menu').match('rollout'))
		{
			if (!this.menuData.get('expand_menu').match('context') || this.firstCloseableKey == 1)
			{
				return;
			}
		}
		
		// Make sure "none" is not found in the contract_menu string, if it is then stop here
		if (this.openMenuData.get(i).get('contract_menu').match('none'))
		{
			return;
		}
		
		// Set mouseover status of the item to false, since we know
		// we're over the document and not the item. Also, there are
		// some cases when WebDDM sets the mouseover status to true
		// automatically when the mouse is not over the item. This
		// fixes any bugs.
		this.setMouseoverStatus(this.openMenuData.get(i).get('itemId'), false, false, false);
		
		// Hide menu
		this.hideMenu(this.openMenuData.get(i));
		childrenHidden = true;
	}
};

/**
 * Get first closeable openMenuData key.
 */
WebDDM.prototype.getFirstCloseableKey = function ()
{
	// Get first closable ID of the menu
	var data = this.openMenuData.get(0);
	var firstId = 1;
	if (this.menuData.get('expand_menu').match('context'))
	{
		firstId = 0;
	}
	else if (data && data.has('contract_menu') && data.get('contract_menu').match(/rollout/))
	{
		firstId = 0;
	}
	this.firstCloseableKey = firstId;
};

/**
 * Sets the item's styles to '' (out), '_rollover', '_menuopen',
 * '_menuopen_rollover', '_rollover_pressed', or '_menuopen_pressed'
 */
WebDDM.prototype.setItemStyle = function (itemHash, new_style, forceStyle)
{
	// Get fallback style
	new_style.match(/^([\_a-zA-Z]*)\_([a-zA-Z]*)$/);
	var fallback_style = RegExp.$1;
	
	// Set current style...
	itemHash.set('currentStyle', new_style);

	// Get element
	var element = WebDDM_getElement(itemHash.get('itemId') + '_item');	

	// Change content
	var style = itemHash.has('content' + new_style) ? new_style : fallback_style; 
	if (itemHash.has('content' + style) && !(itemHash.get('currentStyleContent') == style && !forceStyle))
	{
		element.innerHTML = itemHash.get('content' + style);
		itemHash.set('currentStyleContent', style);
	}

	// Change class 
	var style = itemHash.has('class' + new_style) ? new_style : fallback_style; 
	if (itemHash.has('class' + style) && !(itemHash.get('currentStyleClass') == style && !forceStyle))
	{
		element.className = itemHash.get('class' + style);
		itemHash.set('currentStyleClass', style);
	}

	// Change CSS
	var style = itemHash.has('css' + new_style) ? new_style : fallback_style; 
	if (itemHash.has('css' + style) && !(itemHash.get('currentStyleCSS') == style && !forceStyle))
	{
		// Get array of CSS attribute:value pairs from CSS string	
		var css;
		if (!forceStyle && itemHash.has('css_cached_array' + style))
		{
			css = itemHash.get('css_cached_array' + style);
		}
		else
		{
			css = parseCssString(itemHash.get('css' + style));
			itemHash.set('css_cached_array' + style, css);
		}
		
		for (var k in css)
		{
			var intercapKey = interCap(k);
			try
			{
				// Set particular style
				if (element.style[intercapKey] != css[k])
				{
					element.style[intercapKey] = css[k];
				}
			}
			catch (e)
			{
				// Catch errors caused by invalid CSS
				// Don't alert the user of errors, because errors caught with Opera
				// might not be caught in Mozilla/IE, etc
			}
		}
		itemHash.set('currentStyleCSS', style);
	}
};

/**
 * Toggles the visibility of the passed element.
 * Uses the passed hash for data and passed element ID
 * as the element that we change the visibility status of.
 * The third argument is a boolean describing whether we
 * are showing or hiding the item.
 * This is another revolutionary WebDDM invention :)
 */
WebDDM.prototype.setVisibility = function (itemsData, itemsId, visibility)
{	
	var element = WebDDM_getElement(itemsId);

	// If we are making the items visible, detect
	// collisions with select lists, applets, objects, etc
	if (visibility)
	{
		domLib_detectCollisions(WebDDM_getElement(itemsId), false);
		element.style.visibility = 'visible';
	}
	
	// Initially set "transitionsUsed" to false
	var transitionsUsed = false;
	
	// Check if we are supposed to do some transition
	for (var index in WebDDM_transitionPlugins.elementData)
	{
		if (itemsData.has(WebDDM_transitionPlugins.get(index)))
		{
			transitionsUsed = true;
			this[WebDDM_transitionPlugins.get(index)](itemsData, itemsId + '_background', visibility);
		}
	}

	// Don't do anything special, just show or hide the element
	if (!transitionsUsed)
	{
		// Call the transitionCompleted method to clean up after everything.
		// Doesn't matter that we didn't transition at all; the transitionCompleted
		// method just needs to be called after the visibility is done changing.
		this.transitionCompleted(element, visibility, itemsId);
	}
};

/** 
 * Perform actions when a transition is done.
 */
WebDDM.prototype.transitionCompleted = function (domObj, visibilityStatus, itemsId)
{
	// Get a new DOM object if we have the background item here
	while (!domObj.id.match(/_subitems$/))
	{
		domObj = domObj.offsetParent;
	}

	// Did the transition make the element invisible?
	if (!visibilityStatus)
	{
		domObj.style.visibility = 'hidden';

		// Unhide selects, applets, etc
		domLib_detectCollisions(domObj, true);
	}
	else
	{
		// Hide selects, applets, etc
		domLib_detectCollisions(domObj, false);

		// Refresh item and any applicable plugins
		domLib_setTimeout('var obj = getWebDDMObject("'+this.containerId+'"); obj.reloadItems(obj.getItemHash("'+itemsId.replace(/subitems$/, '')+'"), "' + itemsId + '");', 10);
	}
};

/**
 * Reload a full items hash. Gets dimensional/locational properties
 * of the set of items and then
 */
WebDDM.prototype.reloadItems = function (itemsHash, itemsId)
{
	if (itemsHash.get(1))
	{
		// Make sure we have a correct itemsId
		itemsId = itemsHash.get(1).get('parentId') + '_subitems';

		// Get dimensional properties
		itemsHash.set('liveOffsets', new Hash());
		itemsHash.get('liveOffsets').set('first-item-offset-left', 0);
		itemsHash.get('liveOffsets').set('first-item-offset-top', 0);
		itemsHash.get('liveOffsets').set('items-total-height', 0);
		itemsHash.get('liveOffsets').set('items-total-width', 0);

		var firstItemObject = WebDDM_getElement(itemsHash.get(1).get('itemId') + '_item_container');

		if (firstItemObject)
		{
			var backgroundElement = WebDDM_getElement(itemsId + '_background');
			var initialOffsetLeft = firstItemObject.offsetLeft + backgroundElement.offsetLeft;
			var initialOffsetTop = firstItemObject.offsetTop + backgroundElement.offsetTop;
			itemsHash.get('liveOffsets').set('first-item-offset-left', initialOffsetLeft);
			itemsHash.get('liveOffsets').set('first-item-offset-top', initialOffsetTop);
			
			var itemsHeight = 0;
			var itemsWidth = 0;
			for (var i = 1; itemsHash.has(i); ++i)
			{
				var itemObject = WebDDM_getElement(itemsHash.get(i).get('itemId') + '_item_container');
				if ((itemObject.offsetLeft + itemObject.offsetWidth) > itemsWidth)
				{
					itemsWidth = (itemObject.offsetLeft + itemObject.offsetWidth);
				}
				if ((itemObject.offsetTop + itemObject.offsetHeight) > itemsHeight)
				{
					itemsHeight = (itemObject.offsetTop + itemObject.offsetHeight);
				}
			}

			itemsHash.get('liveOffsets').set('items-total-height', itemsHeight);
			itemsHash.get('liveOffsets').set('items-total-width', itemsWidth);
		}
		
		// Call any registered plugins...
		if (WebDDM_actionPlugins.has('item_show'))
		{
			for (var index in WebDDM_actionPlugins.get('item_show').elementData)
			{
				WebDDM_actionPlugins.get('item_show').get(index)(this, itemsHash, itemsId);
			}
		}
	}
};

/**
 * Reload an item. Should be used right after an item attribute is changed.
 */
WebDDM.prototype.reloadItem = function (itemHash)
{
	// If there is not a valid item ID, just return right away
	// If there is an item ID, it means that the item HAS been
	// built so it's possible that it's visible. However, if 
	// there is no item ID it's completely impossible that the
	// item is visible.
	if (!itemHash || !itemHash.has('itemId'))
	{
		return;
	}

	// Now update the current style of the item - remember,
	// the current style is held in the item Hash.
	this.setItemStyle(itemHash, itemHash.has('currentStyle') ? itemHash.get('currentStyle') : '', true);
};

/**
 * Modify attributes, and then reload the item.
 */
WebDDM.prototype.modifyMenuData = function (hashPath, newValue)
{
	// Evaluation path to hash
	var evalHashPath = 'this.menuData';
	var itemHashPath = 'this.menuData';
	// Attribute name to modify
	var attributeName = '';
	
	// Parse hashPath
	var chunks = hashPath.split('-');
	for (var i = 0; i < chunks.length; ++i)
	{
		var chunk = chunks[i];
		if (typeof(chunks[i+1]) == 'undefined')
		{
			attributeName = chunk;
		}
		else
		{
			evalHashPath += '.get("'+chunk+'")';
			if (chunk == 'items' || !isNaN(chunk))
			{
				itemHashPath += '.get("'+chunk+'")';
			}
		}
	}
	
	// Get hash to modify
	try
	{
		eval('var attributeHash = ' + evalHashPath);
	}
	catch (e)
	{
		alert('WebDDM error when modifying menu data ['+hashPath+'] of menu with ' +
			'container ID: ' + this.containerId +
			"\n\nError Message: " + e.message +
			"\n\nError Name: " + e.name +
			"\n\nError Line: " + (e.lineNumber ? e.lineNumber : e.number) +
			"\n\nError Stack: " + (e.stack ? e.stack : '[unknown]'));
	}

	// Get item hash
	eval('var itemHash = ' + itemHashPath);

	// Modify attribute
	attributeHash.set(attributeName, newValue);
	
	// If attribute is CSS, removed cached CSS array - see styling function
	if (attributeName.match(/^css(.*?)$/))
	{
		attributeHash.remove('css_cached_array' + RegExp.$1);
	}
	
	// Remove first closeable key if the modified attribute was expand_menu or
	// contract_menu
	if (attributeName.match(/(expand|contract)\_menu/))
	{
		this.firstCloseableKey = false;
	}
	
	// Reload item
	this.reloadItem(itemHash);
};

/**
 * Return value of requested attribute.
 */
WebDDM.prototype.getMenuData = function (hashPath)
{
	// Evaluation path to hash
	var evalAttributePath = 'this.menuData';
	// Attribute name to modify
	var attributeName = '';
	
	// Parse hashPath
	var chunks = hashPath.split('-');
	for (var i in chunks)
	{
		evalAttributePath += '.get("'+chunks[i]+'")';
	}
	
	// Get hash to modify
	eval('var attribute = ' + evalAttributePath);
	return attribute;
};

/**
 * Performs actions needed by a WebDDM element.
 */
WebDDM.prototype.mouseAction = function (in_event, action, itemId)
{
	// Make sure we have a valid in_event (for IE)
	// Sometimes we CAN'T have a valid event, in that event just set it to "false"
	var in_event = (in_event || (in_event == 0 ? false : window.event));
	
	// Get item hash
	var itemHash = this.getItemHash(itemId);
	
	// Make sure the item hash is valid
	if (!itemHash)
	{
		return;
	}
		
	// Switch to get the action
	switch (action) {
		default:
			break;

		//
		// Mouseover code
		//
		case 'mouseover':
			// Do not execute mouseover code if we are in the element already
			if (this.mouseoutTimeout == itemId && !this.getMouseoverStatus(itemId))
			{
				this.mouseoutTimeout = false;
				this.setMouseoverStatus(itemId, true, true, true);
				return;
			}

			// -- Submenu opening/closing --
			if (itemHash.has('items'))
			{
				// Are we opening the menu on onclick, and is this the click event?
				if (itemHash.get('expand_menu').match('click'))
				{
					// When the mouse rolls over the menu,
					// check if the currently opened menu opens with a click.
					// If it does, close that one and open this one.
					this.checkClickMenu(itemId);
				}
		
				// Are we opening the menu on rollover?
				if (itemHash.get('expand_menu').match('rollover'))
				{
					// Expand submenu
					this.setShowMenuTimeout(itemHash);
				}
			}
			else if (!itemHash.has('isContainerItem'))
			{
				// Expand submenu - although technically it doesn't exist. The effect that we
				// want is achieved, though - all the submenus on/above this level are closed.
				this.showMenu(itemHash, itemId);
			}

			// Basic mouseover code
			this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
				? WebDDM_style_menuopenRollover
				: WebDDM_style_rollover));
	
			// Open parent menu (basically re-showing all the items, INCLUDING this one -
			// useful if the menu is fading or sliding out and the mouse moves over.)
			// Nice effect, eh?		
			if (itemId != this.containerId && itemHash.get('parentId') && !itemHash.get('expand_menu').match('auto'))
			{
				this.showMenu(this.getItemHash(itemHash.get('parentId')), itemHash.get('parentId'));
			}
			
			// Set mouseover status to true
			this.setMouseoverStatus(itemId, true, true, true);
			break;
		// -- End onmouseover --

		//
		// Click/Mouseup code
		//
		case 'click':		
			// Prevent the event from bubbling
			domLib_cancelBubble(in_event);

			// -- Submenu opening/closing --
			if (itemHash.has('items'))
			{
				// Are we opening the menu on onclick?
				if (itemHash.get('expand_menu').match('click'))
				{
					// Is the menu opened or closed?
					if (this.menuIsOpen(itemId) && itemHash.get('contract_menu').match('click'))
					{
						// Contract submenu
						this.hideMenu(itemHash, false);
					}
					else
					{			
						// Expand submenu
						this.setShowMenuTimeout(itemHash);
					}
				}
				else if (itemHash.get('contract_menu').match('click'))
				{
					// The menu does not open on onclick, but it does
					// close on onclick
					this.hideMenu(itemHash, false);
				}
			}
		case 'mouseup':
			// Prevent the event from bubbling
			domLib_cancelBubble(in_event);

			// Reset style of the item on onclick
			this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
				? WebDDM_style_menuopenRollover
				: WebDDM_style_rollover));

			break;
		// -- End onclick/onmouseup --

		//
		// Mouseout code
		//
		case 'mouseout':
			// Set mouseover status to false
			this.setMouseoverStatus(itemId, false, false, false);
					
			// Stop executing the mouseout timeout
			if (this.mouseoutTimeoutId)
			{
				domLib_clearTimeout(this.mouseoutTimeoutId);
				this.mouseoutTimeoutId = false;
			}
			
			// Create mouseout code as a function
			var mouseout_code = function (args)
			{
				// Arguments:
				// 0 => WebDDM object
				// 1 => itemHash
				// 2 => itemId
				// 3 => in_event
				args[0].mouseoutTimeout = false;
				args[0].mouseoutTimeoutId = false;

				if (!args[0].getMouseoverStatus(args[2]))
				{
					// Set mouseover status
					args[0].setMouseoverStatus(args[2], false, false, false);
					
					// Set style of item
					args[0].setItemStyle(args[1], (args[0].menuIsOpen(args[2])
						? WebDDM_style_menuopenOut
						: WebDDM_style_out));

					// Execute custom actions?
					if (args[1].has('actions') && args[1].get('actions').has('onmouseout'))
					{
						eval(args[1].get('actions').get('onmouseout'));
					}
									
					// Call any plugins needed...
					if (WebDDM_actionPlugins.has('item_mouseout'))
					{
						for (var index in WebDDM_actionPlugins.get('item_mouseout').elementData)
						{
							WebDDM_actionPlugins.get('item_mouseout').get(index)(args[3], this, args[1]);
						}
					}
				}
			};
			
			// If we have a valid event, we are being called "genuinely"
			// To make sure we haven't moused over an object inside the item (that will
			// first the mouseout code) we first set a 10-ms timeout. If the mouse is
			// REALLY off the item, the code will execute normally. If not, the mouseover
			// code will execute, and set the mouseover status back to true - then when
			// the mouseout code fires, we know not to execute it!
			if (in_event)
			{
				// Set timeout
				this.mouseoutTimeout = itemId;
				this.mouseoutTimeoutId = domLib_setTimeout(mouseout_code, 10, [this, itemHash, itemId, in_event]);
			}
			else
			{
				// Execute normally
				mouseout_code([this, itemHash, itemId, in_event]);
			}
			break;
		// -- End mouseout --

		//
		// Mousedown code
		//
		case 'mousedown':
//			alert(in_event[domLib_eventButton]);
		
			// Prevent the event from bubbling
			domLib_cancelBubble(in_event);

			// Set item style
			this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
				? WebDDM_style_menuopenPressed
				: WebDDM_style_rolloverPressed));
			break;
		// -- End mousedown --
	}

	if (!action.match('mouseout'))
	{
		// Perform custom actions defined in the item hash
		if (itemHash.has('actions') && itemHash.get('actions').has('on'+action))
		{
			eval(itemHash.get('actions').get('on'+action));
		}
		
		// Call any plugins needed...
		if (WebDDM_actionPlugins.has('item_' + action))
		{
			for (var index in WebDDM_actionPlugins.get('item_' + action).elementData)
			{
				WebDDM_actionPlugins.get('item_' + action).get(index)(in_event, this, itemHash);
			}
		}
	}
};

/**
 * Set a timeout to open a submenu.
 */
WebDDM.prototype.setShowMenuTimeout = function (itemHash)
{
	// Set timeout...
	this.showTimeoutHash.set(this.showTimeoutHash.length, domLib_setTimeout(function(args)
	{
		// Stop any other showMenu timeouts
		for (var i = args[0].showTimeoutHash.length; i >= 0; i--)
		{
			domLib_clearTimeout(args[0].showTimeoutHash.get(i));
			args[0].showTimeoutHash.remove(i);
		}
		// Show menu
		args[0].showMenu(args[1], args[2]);
	}, (itemHash.has('openDelay') ? itemHash.get('openDelay') : 0), [this, itemHash, itemHash.get('itemId')]));
};

/**
 * Returns a formatted unit, which is unit+'px' if the variable doesn't have a
 * unit included with it.
 */
WebDDM.prototype.formatUnit = function (value)
{
	// Is value a number with no specified measurement unit?
	if (parseFloat(value) == value || parseInt(value) == value || typeof(value) == 'number')
	{
		return (value+'') + this.menuData.get('defaultMeasurementUnit');
	}
	// Measurement unit is specified
	else
	{
		return value;
	}
};

/**
 * Get a WebDDM object.
 */
function getWebDDMObject (containerId)
{
	return WebDDMObjects.has(containerId) ? WebDDMObjects.get(containerId) : false;	
}

/**
 * Hide all submenus in all menus.
 * First argument should be true if called from document.onclick, false otherwise.
 * Second argument is the containerId of a WebDDM menu to NOT hide.
 */
function WebDDM_hideAllMenus (calledFromClick, except)
{
	// Loop through all menu objects
	for (var i in WebDDMObjects.elementData)
	{
		if (i != except)
		{
			// Hide all submenus...
			getWebDDMObject(i).hideAllSubmenus(calledFromClick);
		}
	}
}

/**
 * Preloads images. Assumes ALL arguments are image path names.
 */
function WebDDM_preloadImages ()
{
    return;
	// DOM div object where img elements will be placed
	var domObj = document.createElement('div');
	domObj.style.top = '-999999px';
	for (var i = 0; i < arguments.length; ++i)
	{
		domObj.innerHTML += '<div style="background-image: url(\''+arguments[i]+'\');">'
			+ '<img src="'+arguments[i]+'" />'
			+ '</div>';
	}
	document.body.appendChild(domObj);
};

/**
 * Register a plugin relating to executing on-menu-action code.
 *
 * Possible action types (pass to first argument as a string):
 * -- item_mouseover - Called when the mouse moves over the item
 * -- item_mouseout - Called when the mouse moves off the item
 * -- item_click - Called when the item is clicked on
 * -- item_mousedown - Called when the mouse is pressed over the item
 * -- item_mouseup - Called when the mouse is released over the item
 * -- document_mousemove (global mousemove event)
 * -- document_click (global click event)
 * -- item_init - Called right before the item is built and displayed for the first time
 * -- item_show - Called right after the item is made completely visible (or "refreshed")
 */
function WebDDM_registerPlugin_action (action, pluginCallback)
{
	if (!WebDDM_actionPlugins.has(action))
	{
		WebDDM_actionPlugins.set(action, new Hash());
	}
	WebDDM_actionPlugins.get(action).set('auto', pluginCallback);
};


/**
 * Register a plugin relating to visibility transitions.
 * Pass the name of the plugin name as the only argument; it is
 * used for calling the plugin and getting information about it.
 * See alphaTrans and clipTrans for more information.
 */
function WebDDM_registerPlugin_transition (pluginName)
{
	WebDDM_transitionPlugins.set('auto', pluginName);
};
