/*

================================================================================
MENU V0.1.0. An open-source javascript menu library for web browsers.
Copyright (C) 2006 Doug Kidd.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

IF YOU DO NOT AGREE, YOU MAY NOT USE THIS SOFTWARE FOR ANY PURPOSE.

Contact: oss AT chubborg DOT com DOT au

This copyright message must appear, in its entirety, with all copies and/or
derivatives of this software.
================================================================================

16-MAR-2006	DNK V0.1.1 Don't call MENU_HideAllMenus() in MENU_MenuItemEvent() on mouseup event.
				This stops the onclick event from happening in Gecko. MENU_MenuItemEvent() is no longer
				set as the mouseup handler for menu items in MENU_BuildMenus().
27-FEB-2006	DNK V0.1.1 Don't set innerHTML of document.body. It interferes with onload handlers in IE.
21-FEB-2006	DNK V0.1.1 Removed MENU_DocumentEvent().
				Added "object" argument to MENU_AddEventListener().
20-FEB-2006	DNK V0.1.1 Don't show empty menus. Add dynamic positioning.
				Deactivate all parent items on hide.
				Add MENU_OnShowRootMenu() and MENU_OnShowMenu() callbacks.
				Keep menubar item highlighted when selected.
27-JAN-2006	DNK V0.1.0 Initial release

*/

// Constants - Change at your peril
var MENU_MENUBAR_LAYOUT_HORIZONTAL	= 0;
var MENU_MENUBAR_LAYOUT_VERTICAL	= 1;

// Config - Defaults. Change the value of these in your load handler.
var MENU_MENUBAR_LAYOUT = MENU_MENUBAR_LAYOUT_HORIZONTAL;	// Default
var MENU_CLASS_NAME = "menu";		// The substring "<<id>>" in this will be replaced by the menu_id.
var MENU_ROOT_DX = -1;				// Fine-tune root menu X position.
var MENU_ROOT_DY = 5;				// Fine-tune root menu Y position.
var MENU_ITEM_DX = 3;				// Fine-tune submenu X position.
var MENU_ITEM_DY = -3;				// Fine-tune submenu Y position.
var MENU_RENDER_DELAY = 333;		// 333ms = (1/3)s for delaying submenu rendering.
var MENU_POSITION_DYNAMIC = false;	// Set to true if layout can move (ie. is centered)
var MENU_CONTAINER_ID = "menu_container";

// Global vars
var MENU_Menus = [];				// Client configures this. See loadHandler().
var MENU_MenuBarIndex = [];			// Index array of all menus in menubar
var MENU_MenuIndex = [];			// Index array of all menus
var MENU_MenuItemIndex = [];		// Index array of all menu items
var MENU_ActiveRootMenu = null;		// Reference to active root menu (if any)
var MENU_ActiveMenuItem = null;		// Reference to active menu item (if any)
var MENU_Timer = null;				// Menu timer handle for delaying submenu rendering

function MENU_BuildMenus()
{
	// Create a temporary wrapper to contain all the menus.
	var wrapper = document.createElement("div");
	// Pass 1: Build and index each menu.
	for (var menu_id in MENU_Menus)
		wrapper.appendChild(MENU_BuildMenu(menu_id));
	// Pass 2: Link the menu tree hierarchy.
	for (var menu_id in MENU_MenuIndex)
	{
		// Link each child menu and its menu items item to its parent menu item (not the parent menu itself).
		var items = MENU_MenuIndex[menu_id].getElementsByTagName("A");
		for (var i = 0; i < items.length; i++)
		{
			var item = items[i];
			var child_id = item.getAttribute("menu_id");
			if (child_id == "")
				continue;
			var menu = MENU_MenuIndex[child_id];
			menu.setAttribute("parent_id", item.id);
			var aa = menu.getElementsByTagName("A");
			for (var j = 0; j < aa.length; j++)
				aa[j].setAttribute("parent_id", item.id);
		}
	}
	// IE6 does not apply styles if we appendChild to document.body.
	// TODO: setting innerHTML _may_cause memory leaks in ie. Deref all objects onunload maybe?
	
	// V0.0.1 Writing directly to document.body kills other onload handlers in IE so write to a div element
	//document.body.innerHTML += wrapper.innerHTML;
	document.getElementById(MENU_CONTAINER_ID).innerHTML = wrapper.innerHTML;

	// Pass 3: Index all menu items and assign event handlers.
	// We have to set event handlers here. It appears that the nodes must be attached to the document
	// before a handler can be set. Makes sense really.
	// Also all the references in the menu index point to the DIVs in the wrapper rather than the ones
	// just inserted into the document via innerHTML. So we reapply the references in the menu index.
	// For each menu we also index each menu item.
	for (var menu_id in MENU_MenuIndex)
	{
		var menu = document.getElementById(menu_id);
		MENU_MenuIndex[menu_id] = menu;	// reference the actual element in the document
		var aa = menu.getElementsByTagName("A");
		for (var j = 0; j < aa.length; j++)
		{
			var a = aa[j];
			MENU_MenuItemIndex[a.id] = a;	// index all menu items
			a.onmouseover = a.onmouseout = a.onmousedown = a.onclick = MENU_MenuItemEvent;
		}
	}
	MENU_OnBuildMenus();
}

function MENU_OnBuildMenus()
{
	// Override this to perform site-specific stuff after the menus are built. For example:
	// - Make every third menu pink (could also be done in css).
	// - Override specific onclick handlers for a few menu items (don't forget to return false).
	// - Insert a span stack for rounded corners.
	// - Whatever.
}

function MENU_BuildMenu(menu_id)
{
	// Build a menu of the form:
	//
	// <div menu_id="menu_id" parent_id="" pos="">
	// <div>
	// <table>
	//   <tr><td>{MENU_ITEM}</td></tr>
	//     .
	//     .
	//   <tr><td>{MENU_ITEM}</td></tr>
	// </table>
	// </div>
	// <div>
	//
	// The nested divs are intentional and help with cross-browser positioning. The outer div should have
	// position:absolute;display:none; only. Any other style (ie padding) should be applied to the inner div.
	// Table layouts are still the most reliable. Sorry.
	// If you are an anti-table zealot, just override this function or override MENU_OnBuildMenu().
	
	var menu = document.createElement("div");
	menu.id = menu_id;
	MENU_MenuIndex[menu_id] = menu;		// index each menus
	menu.setAttribute("parent_id", "");	// define parent_id as a known attribute
	menu.setAttribute("pos", "false");	// define pos as a known attribute ( = "true" after menu is positioned)
	// It is possible to insert the menu_id in the menu's class name by including the string "<<id>>" in MENU_CLASS_NAME.
	// There is really no good reason to do this as you can set style by id.
	if (MENU_CLASS_NAME != "")
		menu.setAttribute("class", MENU_CLASS_NAME.replace(/<<id>>/, menu_id));
	var table = menu.appendChild(document.createElement("div"))
		.appendChild(document.createElement("table"));
	var item_count = MENU_Menus[menu_id].length;
	for (var j = 0; j < item_count; j++)
	{
		// Append <tr><td>{MENU_ITEM}</td></tr> for each menu item.
		table.appendChild(document.createElement("tr"))
			.appendChild(document.createElement("td"))
			.appendChild(MENU_BuildMenuItem(menu_id, j));
	}
	return MENU_OnBuildMenu(menu);
}

function MENU_OnBuildMenu(menu)
{
	// Override this if you want to do something whacky to the menu before it ends up in the document tree.
	return menu;
}

function MENU_BuildMenuItem(menu_id, ordinal)
{
	// Build a {MENU_ITEM} _WITH_ A SUBMENU of the form:
	//
	// <a id="menu_id_ordinal" class="menu" menu_id="submenu_id" parent_id="" href="href"><span>caption</span></a>
	// 
	// or a {MENU_ITEM} _WITHOUT_ A SUBMENU of the form:
	//
	// <a id="menu_id_ordinal" menu_id="" parent_id="" href="href"><span>caption</span></a>
	// 
	// Once again, override this if you don't approve.
	
	var params = MENU_Menus[menu_id][ordinal];	// [submenu_id, caption, href]
	var submenu_id = params[0];
	var caption = params[1];
	var href = params[2];
	var fSeparator = (caption.toUpperCase() == "#SEPARATOR");
	var item = document.createElement("a");
	item.id = menu_id + "_" + ordinal;			// id of this item: menu_id + underscore + ordinal
	item.setAttribute("parent_id", "");			// item_id of parent item (not parent menu)
	item.setAttribute("menu_id", submenu_id);	// child menu_id
	if (fSeparator)
		item.setAttribute("class", "menu-separator");
	else if (submenu_id != "")
		item.setAttribute("class", "menu");
	var span = item.appendChild(document.createElement("span"));
	if (!fSeparator)
		span.innerHTML = caption;
	item.href = href;
	// If href attribute is missing or is a hash or is explicitly set to the current page then
	// ensure it is explicitly set to the current page. MENU_MenuBarEvent() will then return false
	// to ensure the link is not navigated. Note that href="" WILL navigate to the default document
	// in the current directory. Different platforms handle a missing href attribute differently.
	// This should cover most if not all requirements. You can set event handlers per item in
	// MENU_OnBuildMenus() if you like.
	href = item.href	// retrieve canonical href via the magic of the href property of the a element.
	// Add option link target
	if (params.length > 3)
		item.target = params[3];
	var re = new RegExp("^" + location.href + "#?$", "i");
	if (href == null || href == "" || href.search(re) == 0)
		item.href = location.href;
	return item;
}

function MENU_OnBuildMenuItem(item)
{
	// Override this if you want to do something whacky to the menu item before it ends up in its menu.
	return item;
}

function MENU_InitMenuBar(menubar_id)
{
	var aa = document.getElementById(menubar_id).getElementsByTagName("A");
	var re = new RegExp("^" + location.href + "#?$", "i");
	for (var i = 0; i < aa.length; i++)
	{
		var a = aa[i];
		// a.getAttribute("href") behaves differently on different platforms.
		// a.href always returns the canonical URL.
		var href = a.href
		// If href attribute is missing or is a hash or is explicitly set to the current page then
		// ensure it is explicitly set to the current page. MENU_MenuBarEvent() will then return false
		// to ensure the link is not navigated. Note that href="" WILL navigate to the defoult document
		// in the current directory. Different platforms handle a missing href attribute differently.
		if (href == null || href == "" || href.search(re) == 0)
			a.href = location.href;
		a.onmouseover = a.onmouseout = a.onmousedown = a.onclick = MENU_MenuBarEvent;
		// Locate menu div if one is specified.
		var menu_id = a.getAttribute("menu_id")
		if (menu_id != null && menu_id != "")
		{
			// Index and position all root menus.
			MENU_MenuBarIndex[menu_id] = a;
			MENU_PositionRootMenu(menu_id)
		}
	}
}

function MENU_PositionRootMenu(menu_id)
{
	var a = MENU_MenuBarIndex[menu_id];
	var x = 0, y = 0, n= 100;
	for (var c = a; c != null && --n > 0; c = c.offsetParent)
	{
		x += c.offsetLeft;
		y += c.offsetTop;
	}
	if (MENU_MENUBAR_LAYOUT == MENU_MENUBAR_LAYOUT_VERTICAL)
	{
		x += a.offsetWidth + MENU_ROOT_DX;
		y += MENU_ROOT_DY;
	}
	else
	{
		x += MENU_ROOT_DX;
		y += a.offsetHeight + MENU_ROOT_DY;
	}
	// Check all root menus exist.
	var menu = MENU_CheckMenuExists(menu_id);
	if (menu == null)
		return;	// Error!
	menu.style.left = x + "px";
	menu.style.top = y + "px";
	menu.setAttribute("pos", "true");	// Flag as positioned.
}

function MENU_CheckMenuExists(menu_id)
{
	// Recursively check that all menus under menu_id exist.
	var menu = document.getElementById(menu_id);
	if (menu == null || menu == "")
	{
		MENU_ErrorNoMenu(menu_id);
		return null;	// Failure!
	}
	var aa = menu.getElementsByTagName("A");
	for (var i = 0; i < aa.length; i++)
	{
		var a = aa[i];
		var menu_id = a.getAttribute("menu_id");
		if (menu_id != null && menu_id != "")
			MENU_CheckMenuExists(menu_id);
	}
	return menu;	// Success!
}

function MENU_MenuBarEvent(e)
{
	// Cancel outstanding render request if any.
	if (MENU_Timer != null)
	{
		clearTimeout(MENU_Timer);
		MENU_Timer = null;
	}
	MENU_ActiveMenuItem = null;
	e = e || window.event;
	var a = e.target || e.srcElement;
	while (a.tagName != "A")
		a = a.parentNode;
	a.blur();	// Lose marquee in IE6.
	var menu_id = a.getAttribute("menu_id");
	if (menu_id != null && menu_id != "")
	{
		var menu = MENU_MenuIndex[menu_id];
		if (e.type == "mousedown")
		{
			// Activate/deactivate menu.
			if (MENU_ActiveRootMenu == null)
				MENU_ShowRootMenu(menu);
			else
				MENU_HideMenu(menu);
			MENU_StopPropagation(e);	// Don't propagate this event to MENU_DocumentEvent()
		}
		else if (e.type == "mouseover" && MENU_ActiveRootMenu != null)
			MENU_ShowRootMenu(menu);			// Show new menu if old was active.
		else if (e.type == "click")
			return (a.href != location.href);	// Don't follow link if we're there.
	}
}

function MENU_ShowRootMenu(menu)
{
	// Cancel outstanding render request if any.
	if (MENU_Timer != null)
	{
		clearTimeout(MENU_Timer);
		MENU_Timer = null;
	}
	for (var menu_id in MENU_MenuIndex)
	{
		var m = MENU_MenuIndex[menu_id];
		if (m != menu)
			MENU_HideMenu(m);
	}
	// V0.1.1 Don't show empty menus. Add dynamic positioning. Add callback onshow
	if (menu.getElementsByTagName("TR").length > 0)
	{
		menu.style.display = "block";
		MENU_MenuBarIndex[menu.id].className = "active"; // V0.1.1 leave menu bar item highlighted
		if (MENU_POSITION_DYNAMIC)
			MENU_PositionRootMenu(menu.id);
		menu = MENU_OnShowRootMenu(menu);
	}
	MENU_ActiveRootMenu = menu;
}

function MENU_OnShowRootMenu(menu)
{
	// Overide if you want to
	return menu;
}

function MENU_HideAllMenus()
{
	// Cancel outstanding render request if any.
	if (MENU_Timer != null)
	{
		clearTimeout(MENU_Timer);
		MENU_Timer = null;
	}
	for (var menu_id in MENU_MenuBarIndex)
		MENU_MenuBarIndex[menu_id].className = "";
	for (var menu_id in MENU_MenuIndex)
		MENU_MenuIndex[menu_id].style.display = "none";
	MENU_ActiveMenuItem = null;
	MENU_ActiveRootMenu = null;
}

// TODO: Orthogonalize menu showing hiding. There are probably too may ways to show/hide menus.
// This is probably due to working around bugs early in development.

function MENU_HideMenu(menu)
{
	// Cancel outstanding render request if any.
	if (MENU_Timer != null)
	{
		clearTimeout(MENU_Timer);
		MENU_Timer = null;
	}
	// Hide children recursively.
	var aa = menu.getElementsByTagName("A")
	for (var i = 0; i < aa.length; i++)
	{
		var a = aa[i];
		if (a.className == "menu-active")
			a.className = "menu";	// V0.1.1 deactivate
		if (a == MENU_ActiveMenuItem)
			MENU_ActiveMenuItem == null;
		menu_id = a.getAttribute("menu_id")
		if (menu_id != "")
			MENU_HideMenu(MENU_MenuIndex[menu_id]);
	}
	// Now hide menu.
	menu.style.display = "none";
	if (menu == MENU_ActiveRootMenu)
	{
		MENU_MenuBarIndex[menu.id].className = "";	// V0.1.1 remove menu bar item highlight
		MENU_ActiveRootMenu = null;
	}
}

function MENU_MenuItemEvent(e)
{
	// Cancel outstanding render request if any.
	if (MENU_Timer != null)
	{
		clearTimeout(MENU_Timer);
		MENU_Timer = null;
	}
	e = e || window.event;
	var a = e.target || e.srcElement;
	while (a.tagName != "A")
		a = a.parentNode;
	if (e.type == "mouseover")
	{
		MENU_ActiveMenuItem = a;
		MENU_HighlightAncestors(a)
	}
	else if (e.type == "mousedown")
		MENU_StopPropagation(e);
	else if (e.type == "mouseout")
	{
		var parent_id = a.getAttribute("parent_id");
		if (parent_id != "")
			MENU_ActiveMenuItem = document.getElementById(parent_id);
		else
			MENU_ActiveMenuItem = null;
	}
	else if (e.type == "click")
	{
		MENU_HideAllMenus();
		return (a.href != location.href);	// Don't follow link if we're there.
	}
	else
		MENU_ActiveMenuItem = null;
	// Now we play the waiting game...
	MENU_Timer = setTimeout("MENU_RenderMenus();", MENU_RENDER_DELAY);
	// The waiting game sux. let's play hungry hungry hippos!	
}

function MENU_HighlightAncestors(item)
{
	var highlight = [];
	var a = item;
	// Don't persist highlight if an end node.
	if (a.getAttribute("menu_id") != "")
		highlight[a.id] = "true";
	while (true)
	{
		var parent_id = a.getAttribute("parent_id");
		if (parent_id == "")
			break;
		a = MENU_MenuItemIndex[parent_id];
		highlight[a.id] = "true";
	}
	for (var item_id in MENU_MenuItemIndex)
	{
		a = MENU_MenuItemIndex[item_id];
		if (a.getAttribute("menu_id") != "")
			a.className = (highlight[a.id]) ? "menu-active" : "menu";
	}
}

function MENU_RenderMenus()
{
	// We can't be cancelled now.
	MENU_Timer = null;
	// If the active menu item went away due to a menubar event, just bail.
	if (MENU_ActiveMenuItem == null)
		return;
	var show = [];
	// Show child if any.
	var menu_id = MENU_ActiveMenuItem.getAttribute("menu_id");
	if (menu_id != "")
		show[menu_id] = true;
	// Show self.
	menu_id = MENU_ActiveMenuItem.id.replace(/_\d+$/, "");
	show[menu_id] = true;
	// Show ancestors.
	for (var a = MENU_ActiveMenuItem; (menu_id = a.getAttribute("parent_id")) != ""; a = document.getElementById(menu_id))
		show[menu_id.replace(/_\d+$/, "")] = true;
	// Show or hide menus.
	for (menu_id in MENU_MenuIndex)
	{
		var menu = MENU_MenuIndex[menu_id];
		if (show[menu_id])
		{
			menu.style.display = "block";
			var parent_id = menu.getAttribute("parent_id");
			var pos = menu.getAttribute("pos");
			if (parent_id != "" && pos == "false")
			{
				// Position menu.
				var a = document.getElementById(parent_id);
				var x = 0, y = 0;
				for (var c = a; c != null; c = c.offsetParent)
				{
					x += c.offsetLeft;
					y += c.offsetTop;
				}
				x += a.offsetWidth + MENU_ITEM_DX;	// Just to the left.
				y += MENU_ITEM_DY;
				menu.style.left = x + "px";
				menu.style.top = y + "px";
				// V0.1.1 Add dynamic positioning
				if (!MENU_POSITION_DYNAMIC)
					menu.setAttribute("pos", "true");
				menu = MENU_OnShowMenu(menu);	// V0.1.1
			}
		}
		else
			menu.style.display = "none";
	}
}

function MENU_OnShowMenu(menu)
{
	// Override if you want
	return menu;
}

function MENU_ErrorNoMenu(menu_id)
{
	var fn = MENU_ErrorNoMenu.caller.toString().replace(/function\s+(\w+)[\s\S]*/, "$1");
	alert(fn + "()\n\nCan't find menu. menu_id = " + menu_id);
	return false;
}

function MENU_StopPropagation(e)
{
	// Stop an event in FF or IE6
	if (e.stopPropagation)
		e.stopPropagation();	// FF
	else
		e.cancelBubble = true;	// IE6
}

function MENU_AddEventListener(object, eventType, handler)
{
	// Ad an event handler to an object in FF or IE6
	if (object.attachEvent)
		object.attachEvent("on" + eventType, handler);		// IE
	else if (object.addEventListener)
		object.addEventListener(eventType, handler, false);	// FF
}
