/*SurgeMenu, version 1.2
a multi-platform PHP/Javascript cascading menu system
Copyright (c) 2009, Paul Postuma and Ars Informatica

This Javascript/PHP is - as is all my code - relatively easy to read and modify. And yet, it allows
for some very deeply nested, multi-tiered menus, without any of the speed decreases that many other
menuing scripts experience as the menu complexity rises. It also uses and requires PHP, to automate
and simplify the creation of the Javascript, CSS, and HTML menu code.

Take my 'medical subjects' menu tree - see the deep_menu.php sample page in this package, and
demonstated on my site. The top link launches a top-tier eleven-item menu, each with variable-
length (6 to 10 items) second-tier submenus, twenty-five third-tier menus, and five fourth-tier
menus. Grand total: 289 selectable menu items.

I tried this using XMenu, by Erik Arvidsson & Emil A Eklund - a very nice menu system, and in some
ways more customizable/tweakable than mine. Unfortunately, it requires that far more files be
loaded, uses a larger amount of run-time code, and - most problematic - adds large numbers of
prototyped methods and properties to the WebFX objects. As menus grow, menuing rapidly slows down,
and on large menus, it chokes. It hangs.

SurgeMenu was inspired by their code, but developed from the ground up as a leaner, faster, easier
alternative. Heavily annotated! run-time Javascript weighs in at under 6K. I've built a 471-item
menu that loaded and ran as fast as a 4-item menu ...

One other main difference: for ease of configuration and convenience, PHP is used to create the
calling code, actual menu structure, etc. Not that this can't be done manually, but it would take a
lot more explaining, typing, testing. One mistyped character breaks the code.

The HTML can be created or customized by hand: a fair bit more work, but if you don't want to use
PHP, or this isn't available on your web server, try the SurgeMenu-J (Javascript-only) package,
which I hope to be releasing soon.

SurgeMenu has been developed for use with multiple browsers: as of this release, it has been tested
and works with:

	Internet Explorer 6
	Internet Explorer 7
	Internet Explorer 8
	Firefox 1.x
	Firefox 2.0
	Firefox 3.0
	Google Chrome 1.0
	Netscape Navigator 9
	Opera 9
	Safari 3
	Safari 4 public beta
As of this writing, these account for over 98% of all browsers used.

My thanks to Tomaž Toman for getting me to update the SurgeMenu code.

See my site at www.ars-informatica.ca for more details, and other PHP/Javascript code


This program is free software; you can redistribute it and/or modify it under the terms of the GNU
General Public License (GPL) as published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version. 

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
General Public License for more details. 

To read the license please visit http://www.gnu.org/copyleft/gpl.html.

Finally, I would ask that if you like this code, or use this code, you link back to my page.
Drop me a note. A small request. It's not like I'm asking for money ...



1. From the page you wish to use SurgeMenu in, between the <HEAD> and </HEAD> HTML tags, include
the following PHP reference, and customize to suit:


$menuitem_height = 20;								//height of each menu item, in pixels
$menuitem_style = array('font' => '12pt times', 'color' => 'black', 'background' => 'white', 'indent' => '7px');
$menuitem_hover = array('font' => '12pt times', 'color' => 'white', 'background' => '#316ac5', 'indent' => '7px');
$menu_opacity = 100;									//menu opacity
$menu_offset = array('x' => '0', 'y' => '22');					//default offset of menu from calling object

echo '<script type="text/JavaScript">';

//available parameters:
//	divID		- supplied by showMenu; references the element that originally launched the menu
//	selection 	- references the selection clicked on. One of the its most useful properties is innerHTML (the selection text)

function menuAction (selection) {
	selElement = document.getElementById(selection)				//set handler for selected menu element
	document.submitForm.elements[divID].value = selElement.innerHTML	//divID - from showMenu - is the name of the calling
}											//element. Reset its value to the desired selection


<?php include 'SurgeMenu.php';


Please be careful not to overwrite any of the quotation marks, or to overwrite a single-quote with
a double-quote.

All of the CSS style definitions are mandatory; font, color, background, and indent. Opacity is
given as a number between 0 to 100, though menus with less than 50% opacity become very hard to
see. Finally, $menu_offset specifies the desired offset of the menu from the top left corner of the
calling element. For examples, see the various sample PHP pages included with this package:


MenuAction is the Javascript function that is executed when a menu item is clicked on, either a
normal menu item or a submenu item. Available to the function are both the ID of the page item that
launched the menu (referenced by divID) or that of the menu item that was clicked on (referenced by
selection). Various ways of implementing different menu actions are demonstrated in the sample PHP
pages included with this package.

2. In the BODY of the HTML document, within the HTML of the element that launches a specific menu,
a call must be made to the PHP callMenu function, i.e.

<input type="input" <?php callMenu('Menu1') ?>>

A Menu name must be supplied, and this menu must then be created, as in step 3. For further, more
complex options, see the NOTE below.

3. Also within the document BODY, create the main menu and submenus using the custom PHP function
createMenu (defined on this page, below). Syntax is

	createMenu(int menu tier, int width, "MenuName","[+]item 1","[+]item2","etc.");

	int menu tier = integer to indicate menu tier being defined, 0 for top
	int width = width of this menu/submenu in pixels
	"MenuName" = name for menu
	"[+]item" = menu item. Plus sign references a submenu; omit for regular menu items

Note that all elements are mandatory; there is no limit on the number of menu items that may be
specified. For every submenu referenced in a menu, a submenu must be created. As an example:

	createMenu (0, 100, "Drnk", "+juice", "milk", "coffee", "tea");
		createMenu (1, 120, "juice", "apple juice", "orange juice");

That is, submenu item +juice requires a submenu juice.

For multiple submenus that all have the same name, but different content: not a problem. Append an
underscore and number to uniquely identify each unique submenu, i.e. +miscellaneous_0,
+miscellaneous_1, as shown in deep_menu.php. Underscores and numbers are stripped from the menu
items when the names are displayed.


Sometimes SurgeMenu does not display where intended - too far down the page, or up too high.

Unfortunately, browsers don't consistently determine the position of a web page element on any
given page. It depends on how the element is positioned, where it fits in the overall site layout,
how other page elements are defined and nested, and on the browser and browser version used.

I've created four additional code variations to accommodate for this weirdness. You don't need to
rewrite the code; you simply specify that a certain variation be used, i.e. 

$code_variant = 1;

and SurgeMenu does the rest.

Insert this code after the style definitions and before the PHP call to SurgeMenu.php. And test it,
in Internet Explorer, in Firefox, in Opera, in Safari, in Netscape Navigator. Test it in all
browsers: what works in one often doesn't in another.

More on this at www.ars-informatica.ca/article.php?article=16.


Alternately to the method shown in step 2, a menu may be called in non-standard fashion, or the
same menu may be called by multiple page elements, using a more complex calling structure, i.e.

	<input type="input" ID="1" value="" onMouseOver="collapseSubs(1);showMenu(event,'Menu',0)" 

At minimum, this requires:

	a unique ID for each calling element
	collapseSubs(1) and showMenu calls by the event handler that launches the menu
	an onMouseout call to hide the menu if it loses focus
The event that triggers collapseSubs and showMenu is usually onMouseOver, but onClick typically if
launched from a button. Function showMenu(event,'MenuName',0) causes the menu to be displayed. The
first function parameter is event, and is required for proper functioning; 'MenuName' is the name
of the desired menu, and the number indicates the menu tier to be displayed. For a top-level menu,
this is 0.

For an example of how this is implemented: menu 'bool' is used by multiple menu elements in the
deep_menu.php example.


Finally, showMenu() will take two more optional parameters: a corrective x, y offset if the menu
requires a different placement from that specified by the default parameters, i.e.


Good luck. Have fun.

//PHP function definitions

function callMenu ($menu) {								//creates standard menu calling code;
	echo 'ID="'.$menu.'" onMouseOver="collapseSubs(1);showMenu(event,\''.$menu.'\',0)" 	//used within an HTML tag

function createMenu () {								//creates the menu
	$menu_name = func_get_arg(2).'Menu';					//get base Menu name as provided, and append "Menu"
	$menu_name = preg_replace("/[_\s]+/","",$menu_name);			//and parse out some of the garbage
	echo '<div ID="'.$menu_name.'" class="cascading_menu" onmouseout="setTimeout(\'hideMenu()\',100)"
											//create display code for menu item
	$i = 3;									//menu items start with fourth argument
	$next_tier = func_get_arg(0)+1;						//calculate depth of next tier
	while (@func_get_arg($i)) {							//process names for menu items or submenus
		$item_text = func_get_arg($i);
		$item_ID = preg_replace("/[_\s]+/","",$menu_name).'Item'.($i-2);	//remove illegal characters to create item ID

		if (preg_match("/^\+/",$item_text)) {				//if + used at start of the string, process item as
			$item_text = substr($item_text,1);				//submenu. Strip +, which should not be displayed
			$submenu_name = preg_replace("/[_\s]+/","",$item_text);	//derive submenu name and parse as $menu_name above
			$item_name = preg_replace("/_\d+/",'',$item_text);	//and numerics used to create unique IDs

			echo "\t".'<div ID="'.$item_ID.'" class="cascading_submenu" onClick="menuAction(\''.$item_ID.'\')"
onmouseout="unHover(\''.$item_ID.'\')">'.$item_name."</div>\n";				//HTML for submenu

		else echo "\t".'<div ID="'.$item_ID.'" class="cascading_item" onmouseover="Hover(\''.$item_ID.'\','.$next_tier.')"
onmouseout="unHover(\''.$item_ID.'\')" onClick="menuAction(\''.$item_ID.'\')">'.str_replace("\\","",$item_text)."</div>\n";
		$i++;								//HTML for menu item
	echo "</div>\n\n";

//CSS definitions

echo '<style>
.cascading_menu { position:absolute;z-index:100;visibility:hidden;width:120px;padding:2px;background-color:white;font:11px arial;
margin:0;border-top:1px solid #bebebe;border-right:2px solid #3f3f3f;border-bottom:2px solid #3f3f3f;border-left:1px solid #bebebe;
if ($menu_opacity != "100") echo ';opacity:'.($menu_opacity/100).';filter:"alpha(opacity='.$menu_opacity.')"';
echo ' }
.cascading_item { height:'.$menuitem_height.'px;font:'.$menuitem_style["font"].';color:'.$menuitem_style["color"].';
text-indent:'.$menuitem_style["indent"].';cursor:default }
.cascading_submenu { height:'.$menuitem_height.'px;font:'.$menuitem_style["font"].';color:'.$menuitem_style["color"].';
if (@$submenu_flag) echo ' url(\''.$submenu_flag.'\') no-repeat right center';
echo ';vertical-align:middle;border:0;line-height:'.($menuitem_height-1).'px;text-indent:'.$menuitem_style["indent"].';cursor:default }

//Create Javascript

echo '<script type="text/JavaScript">
menu_nesting = new Array(5)
activeMenu = ""
menuFocus = false
opacity = '.$menu_opacity.'

iFont = "'.$menuitem_style["font"].'"						//define item font
iColor = "'.$menuitem_style["color"].'"						//define item color
iBackground = "'.$menuitem_style["background"].'"				//etc.
smiBackground = "'.$menuitem_style["background"];					//background style for submenus
if (@$submenu_flag) echo ' url(\''.$submenu_flag.'\') no-repeat right center';
echo "\"\n".'iIndent = "'.$menuitem_style["indent"].'"
iHeight = "'.($menuitem_height-1).'px"

hFont = "'.$menuitem_hover["font"].'"						//define hover font, etc.
hColor = "'.$menuitem_hover["color"].'"
hBackground = "'.$menuitem_hover["background"].'"
smhBackground = "'.$menuitem_hover["background"];
if (@$submenu_flag) echo ' url(\''.$submenu_flag.'\') no-repeat right center';
echo "\"\n".'hIndent = "'.$menuitem_hover["indent"].'"

//function Hover() highlights a menu item using a custom style if the mouse hovers over the same; function unHover restores the original

function Hover(div,tier) {								//highlights the hovered-over menu item
	divElement = document.getElementById(div)					//get active element
	divElement.style.font = hFont						//define new styles, beginning with font style,
	divElement.style.color = hColor						//color, etc.
	if (divElement.className == "cascading_item") divElement.style.background = hBackground	//write background for menu items
	else divElement.style.background = smhBackground							//and submenus
	divElement.style.lineHeight = iHeight					//fix the lineHeight reset
	divElement.style.textIndent = hIndent					//set CSS text-indent
	menuFocus = true								//informs hideMenu not to close an active element
	collapseSubs(tier)								//collapse any other menus at this level, as well
}											//as all sublevels

function unHover(div) {								//ditto reverting to the original style
	divElement = document.getElementById(div)					//get active element
	divElement.style.font =	iFont						//define new styles, beginning with font style,
	divElement.style.color = iColor						//color, etc.
	if (divElement.className == "cascading_item") divElement.style.background = iBackground
	else divElement.style.background = smiBackground
	divElement.style.lineHeight = iHeight
	divElement.style.textIndent = iIndent
	menuFocus = false								//hideMenu may hide this element

//showMenu note: to work properly, the calling element - usually an input-button - must have an ID

function showMenu(e,menu,tier,position,adjustX,adjustY) {
	menuFocus = true								//new menu has focus, and should be kept open
											//(hideMenu will close if false)
	menu = menu + "Menu"								//write full menu ID name to var. menu
	newElement = document.getElementById(menu)				//and get element ID for menu creation

	if (tier == 0) {								//display base-level menu
		if (navigator.appName == "Netscape"				//Netscape/Mozilla browsers
			&& navigator.vendor != "Apple Computer, Inc."		//other than Safari and Google Chrome,
			&& navigator.vendor != "Google Inc.")			//which conform to the W3C DOM
			divID = e.originalTarget.id					//use originalTarget to represent the source object;
		else divID = e.srcElement.id.toString()				//IE and others use srcElement

		if (divID != activeMenu && activeMenu != "") {			//if the menu to be shown differs from that active,
			menuFocus = false						//close the old, still active menu
			menuFocus = hideMenu(activeMenu + "Menu")			//and reset menuFocus to true, to keep hideMenu from
		}								//closing the new menu after its creation
		menu_nesting = new Array(menu)					//for a top menu, set new Array[0] this value
		divElement = document.getElementById(divID)				//create handle for the calling object
		divLeft = divElement.offsetLeft					//get left and top positions for element
		divTop = divElement.offsetTop'."\r\n\r\n";

if (!@$code_variant) {								//original positioning code: 
	echo '		divParent = divElement.offsetParent				//walk up parent elements
		while (divParent != null) {						//so long as these exist, and
			divLeft += divParent.offsetLeft				//add their offsets
			divTop += divParent.offsetTop
			divParent = divParent.offsetParent

else if ($code_variant == 1) {							//first variation on positioning code
	echo '		if (navigator.appName == "Netscape"			//for Netscape/Mozilla browsers
		&& navigator.vendor != "Apple Computer, Inc."			//other than Safari and Google Chrome,
		&& navigator.vendor != "Google Inc.") {				//offsetLeft and offsetTop only
			obj = divElement.offsetParent				//define the element location relative to its
			while (obj = obj.offsetParent) {				//parent. So, for as long as parent elements exist,
				divLeft += obj.offsetLeft				//add their offsets to derive the element location
				divTop += obj.offsetTop				//on the page

else if ($code_variant == 2) {							//second variation on positioning code
		echo '	divParent = divElement.offsetParent
		while (divParent != null) {
			if (divParent.style.position == "absolute") break
			divLeft += divParent.offsetLeft
			divTop += divParent.offsetTop
			divParent = divParent.offsetParent

else if ($code_variant == 3) {							//etc.
		echo '		divParent = divElement.offsetParent
		while (divParent != null) {
			if (divParent.style.position == "absolute"
			&& (navigator.appName != "Netscape" || navigator.vendor == "Apple Computer, Inc." || navigator.vendor == "Google Inc.")) break
			divLeft += divParent.offsetLeft
			divTop += divParent.offsetTop
			divParent = divParent.offsetParent;

//if $code_variant == 4, no additional positioning code is required

echo "\r\n\r\n".'		if (adjustX) divLeft += adjustX			//because of placement within DIVs, some adjustment
		if (adjustY) divTop += adjustY					//of menu position may be needed relative to calling
		newElement.style.left = divLeft + '.$menu_offset["x"].' + "px"		//object, in addition to the default defined offset
		newElement.style.top = divTop + '.$menu_offset["y"].' + "px"
	else {										//else: display submenu
		menu_nesting[tier] = menu;						//log the new submenu name to the appropriate
											//array position
		tier = tier-1								//get array position for current menu
		divElement = document.getElementById(menu_nesting[tier])	//set as element for style operations:
		divLeft = parseInt(divElement.style.left)				//get left and top positions
		divTop = parseInt(divElement.style.top)
		divLeft = divLeft + parseInt(divElement.offsetWidth)-2			//new left: left of parent DIV + its width - 2
		if (divLeft + parseInt(newElement.style.width) > document.body.clientWidth -2)
			divLeft = divLeft - parseInt(divElement.offsetWidth) - parseInt(newElement.offsetWidth) + 4
											//but if new menu slips past the right edge of the
											//window - set a new left, west of the parent window
		divTop = divTop + (position-1)*'.$menuitem_height.'		//new top: top of parent DIV + no. of menu items above
											//calling item x item height in pixels
		newElement.style.left = divLeft + "px"
		newElement.style.top = divTop + "px"
	newElement.style.visibility = "visible"					//display the specified new menu
	activeMenu = divID

function hideMenu() {
	if (menuFocus) return							//if menu has focus, exit function
	collapseSubs(0)								//collapse menu and all submenus
	menu_nesting = new Array(5)							//and reset menu_nesting array to zero
	return true

function collapseSubs(tier) {
	i = 0
	while (menu_nesting[tier+i]) {						//while any menu or submenus are active
		divElement = document.getElementById(menu_nesting[tier+i])	//select the old menu(s)
		divElement.style.visibility = "hidden"				//and hide them


This source code displayed in HTML format using the freeware source.php by Paul Postuma and Ars Informatica