/*
 JUG GUI library

 Copyright (c) 2010, Simon Wiseman
 
 This software is provided 'as-is', without any express or implied
 warranty. In no event will the authors be held liable for any damages
 arising from the use of this software.
 
 Permission is granted to anyone to use this software for any purpose,
 including commercial applications, and to alter it and redistribute it
 freely, subject to the following restrictions:
 
    1. The origin of this software must not be misrepresented; you must not
    claim that you wrote the original software. If you use this software
    in a product, an acknowledgment in the product documentation would be
    appreciated but is not required.
 
    2. Altered source versions must be plainly marked as such, and must not be
    misrepresented as being the original software.
 
    3. This notice may not be removed or altered from any source distribution.
*/

//************************************************************************************************************
// The JUG GUI support functions provide an object oriented interface for constructing user interfaces
//************************************************************************************************************

//=================================================================================================
// JugSendMessage - Send a message to an element and any child elements
//=================================================================================================

function JugSendMessage( element, eventName, args )
{
	// First check this element
	if( element.JugType !== undefined && element[ eventName ] !== undefined )
	{
		// this is a JUG element that defines a handler for the event
		var result = element[ eventName ].apply( this, args );
		if( result === false )
		{
			return false;	// do not pass message on
		}
	}
	
	// Now check children - assume no more nodes are added, but some may be deleted, so search backwards
	var n = element.childNodes.length;
	var i;
	for( i=0; i < n; i++ )
	{
		var index = n-1 - i;
		if( element.childNodes[index] !== undefined && JugSendMessage( element.childNodes[index], eventName, args ) === false)
		{
			return false;	// stop trying
		}
	}
	
	return true;	// keep trying
}

//=================================================================================================
// JugGetDefaultButton - Get the default button
//=================================================================================================

var JugDefaultButtonId = null;	// the current default button object id

function JugGetDefaultButton()
{
	return JugDefaultButtonId;
}

//=================================================================================================
// JugSetDefaultButton - Set the default button
//=================================================================================================

function JugSetDefaultButton( id )
{
	// id	ID of JUG button that is to become the default
	if( id !== null )
	{
		JugElem( id ).setDefault();
	}
	JugDefaultButtonId = id;
}

//=================================================================================================
// JugSetNoDefaultButton - Make all buttons be non-default
//=================================================================================================

function JugSetNoDefaultButton()
{
	if( JugDefaultButtonId != null )
	{
		JugElem( JugDefaultButtonId ).setNoDefault();
		JugDefaultButtonId = null;
	}
}

//=================================================================================================
// JugCreateRichText - create an HTML object representing some rich text
//=================================================================================================

function JugCreateRichText( richText )
{
	// Generates an HTML object representing some rich text
	// richText - the text with rtf control flags for markup
	//		\b \b0 = bold, \i \i0 = italic, \fsNN = font size, \par \CRLF = paragraph break
	var container = document.createElement( 'span' );
	var style = {'font-style':'normal','font-weight':'normal','font-size':'medium'};
	while( richText != '' )
	{
		// Grab the next piece of text and following escape token - if any
		var text;
		var escape;
		var pos = richText.indexOf( "\\" );
		if( pos == -1 )
		{
			// no more escapes
			text = richText;
			escape = '';
			richText = '';
		}
		else
		{
			text = richText.substr( 0, pos );
			if( richText.length > pos && richText.charCodeAt(pos+1) == 13 && richText.charCodeAt(pos+2) == 10 )
			{
				// CR LF
				escape = 'par';
				richText = richText.substr( pos + 2 );
			}
			else if( richText.length >= pos && richText.charCodeAt(pos+1) == 10 )
			{
				// LF
				escape = 'par';
				richText = richText.substr( pos + 1 );
			}
			else
			{
				var spacePos = richText.indexOf( ' ', pos );
				if( spacePos == -1 )
				{
					// no space after the escape
					escape = richText.substr( pos+1 );
					richText = '';
				}
				else
				{
					escape = richText.substr( pos+1, spacePos - pos - 1 );
					richText = richText.substr( spacePos + 1 );
				}
			}
		}

		// output the text
		if( text != '' )
		{
			var textSpan = document.createElement( 'span' );
			textSpan.style.fontWeight = style[ 'font-weight' ];
			textSpan.style.fontStyle = style[ 'font-style' ];
			textSpan.style.fontSize = style[ 'font-size' ];
			textSpan.appendChild( document.createTextNode( text ) );
			container.appendChild( textSpan );
		}
		
		if( escape == 'par' )
		{
			container.appendChild( document.createElement( 'p' ) );
		}
		else if( escape != '' )
		{
			// fix up the state on the basis of the escape
			
			var c1 = escape.charAt(0);
			if( c1 == 'b' )
			{
				// b or b0
				if( escape.length == 1 )
				{
					style['font-weight'] = 'bold';
				}
				else
				{
					escape = escape.substr( 1 );
					var val = parseInt( escape );
					if( val == 0 )
					{
						style['font-weight'] = 'normal';
					}
					else
					{
						style['font-weight'] = 'bold';
					}
				}
			}
			else if( c1 == 'i' )
			{
				// i or i0
				if( escape.length == 1 )
				{
					style['font-style'] = 'italic';
				}
				else
				{
					escape = escape.substr( 1 );
					var val = parseInt( escape );
					if( val == 0 )
					{
						style['font-style'] = 'normal';
					}
					else
					{
						style['font-style'] = 'italic';
					}
				}
			}
			else if( c1 == 'f' && escape.length > 1 && escape.charAt(1) == 's' )
			{
				// fsNN = font size
				escape = escape.substr( 2 );
				var val = parseInt( escape );
				style['font-size'] = val;
			}
			else
			{
				// unknown - so ignore
			}
		}
	}
	return container;
}

//====== Global Properties =========

// Mouse coordinates - maintained by all window event handlers
window.JugMouseCoords = {x:0,y:0};

// Object being dragged - set to a floating div that needs to be dragged
window.JugDragElem = null;

// Object receiving mouse move messages - set when an element wants to be notified that the mouse has moved
window.JugMoveElem = null;

//=================================================================================================
// JugPopUpConfirm - Simple Pop Up confirmation dialog
//=================================================================================================

function JugPopUpConfirm( legend, okText, okAction, cancelText, cancelAction, defaultButton, normalStyle, defaultStyle, depressedStyle )
{
	// Displays a simple two-button pop up modal dialog
	// legend -         The legend text for the dialog
	// okText -         The text for the OK button
	// action -         Function that is called if the OK button is clicked
	// cancelText -     The text for the cancel button
	// cancelAction -	null, or function that is called if the Cancel button is clicked
	// defaultButton -  Indicates which button is the default - cancelText, okText or "" for neither
	// normalStyle -    Style class name for the buttons in their normal state, default to JugNormalButton
	// defaultStyle -   Style class name for the default button, if any, default to JugDefaultButton
	// depressedStyle - Style class name for buttons when depressed, default to JugDepressedButton
	
	if( normalStyle === undefined )
	{
		normalStyle = "JugNormalButton";
	}
	if( defaultStyle === undefined )
	{
		defaultStyle = "JugDefaultButton";
	}
	if( depressedStyle === undefined )
	{
		depressedStyle = "JugDepressedButton";
	}
	
	var background = document.createElement("div");
	background.style.position = "absolute";
	background.style.filter = "alpha(opacity=50)";
	background.style.MozOpacity = ".50";
	background.style.opacity = ".50";
	background.style.background = "#000000";
	background.style.position ="absolute";
	background.style.top = "0px";
	background.style.left = "0px";
	background.style.width = "100%";
	background.style.height = "100%";

    // create a div that can be positioned
	var dialog = document.createElement("div");
	dialog.style.position = "absolute";
	dialog.style.top = "100px";
	dialog.style.left = "100px";

	// remember any existing default button
	var oldDefaultButton = JugDefaultButtonId;
	JugSetNoDefaultButton();	// disable any default button

	// create a table within the positioned div
	var content = document.createElement("table");
	content.style.background = "#ffffff";
	content.style.border = "4px solid #999999"
	var row1 = content.insertRow(-1);		// the legend row
	var row2 = content.insertRow(-1);		// the buttons row
	var row1col1 = row1.insertCell(-1);		// the legend text
	var row2col1 = row2.insertCell(-1);
	var row2col2 = row2.insertCell(-1);
	dialog.appendChild(content);

	// add the text legend to the first row
	var text = document.createTextNode( legend );
	row1col1.colspan = 2;
	row1col1.appendChild( text );

	// add the cancel button to the second row
	var cancelButton = document.createElement("input");
	cancelButton.type = "button";
	cancelButton.value = cancelText;
	cancelButton.JugType = "popupButton";

	// give the cancel button an id in case it is the default
	cancelButton.id = JugGetUniqueId( "JugPopUpConfirm" );			// generate a new unique ID for the cancel button
	cancelButton.setNoDefault = function(){};
	cancelButton.setDefault = function(){};

	// define the onclick action for the cancel button - it just closes the dialog
	function clickCancel()
	{
		// the cancel button has been clicked 
		if( cancelAction )
		{
			try{
				cancelAction();	// run the user action
			}
			catch( e )
			{
				JugShowException( e );
			}
		}

		// remove the dialog
		dialog.parentNode.removeChild( dialog );
		background.parentNode.removeChild( background );
		
		// restore the default button
		JugSetDefaultButton( oldDefaultButton );	
		return false;	// do not propagate <return> message if default button clicked
	}

	// setup the button's style	and actions
	if( defaultButton == cancelText )
	{
		cancelButton.className = defaultStyle;
		JugDefaultButtonId = cancelButton.id;
		cancelButton.onReturn = clickCancel;
	}
	else
	{
		cancelButton.className = normalStyle;
	}
	cancelButton.onclick = clickCancel;

	row2col1.appendChild( cancelButton );

	// add the ok button to the second row
	var okButton = document.createElement("input");
	okButton.type = "button";
	okButton.value = okText;
	okButton.JugType = "popupButton";

	// give the ok button an id in case it is the default
	okButton.id = JugGetUniqueId( "JugPopUpConfirm" );			// generate a new unique ID for the ok button
	okButton.setNoDefault = function(){};
	okButton.setDefault = function(){};

	// define the onclick action for the ok button - it runs the supplied OK Action and closes the dialog
	function clickOk()
	{
		// the ok button has been clicked 
		try{
			okAction();	// run the user action
		}
		catch( e )
		{
			JugShowException( e );
		}
		
		// remove the dialog
		dialog.parentNode.removeChild( dialog );
		background.parentNode.removeChild( background );

		// restore the default button
		JugSetDefaultButton( oldDefaultButton );	
		
		return false;	// do not propagate <return> message if default button clicked
	}

	// setup the button's style	and actions
	if( defaultButton == okText )
	{
		okButton.className = defaultStyle;
		JugDefaultButtonId = okButton.id;
		okButton.onReturn = clickOk;
	}
	else
	{
		okButton.className = normalStyle;
	}
	okButton.onclick = clickOk;
	
	row2col2.appendChild( okButton );

	// display the transparent grey background and the dialog on top of it
	document.body.appendChild(background);
	document.body.appendChild(dialog);
}

//==== General Pop Up dialog class ====

// JUG factory function
function JugSetupPopupDialog( id, props )
{
	// id		string id of element that is to be converted into a JUG popup dialog
	// props	string property set of the JUG dialog
	//			{  }
	
	// get the element that is to be converted
	var elem = JugElem( id );

	// remove the moveable frame from the DOM but keep it as a detached JUG object so it can be put back when needed
	JugDetach( elem );
	
    // create a div that can be positioned
	var dialog = document.createElement("div");
	dialog.style.position = "absolute";
	dialog.style.background = "#ffffff";
	dialog.style.top = "100px";
	dialog.style.left = "100px";

	dialog.appendChild( elem );

	// create a background to make the dialog modal
	var background = document.createElement("div");
	background.style.position = "absolute";
	background.style.filter = "alpha(opacity=50)";
	background.style.MozOpacity = ".50";
	background.style.opacity = ".50";
	background.style.background = "#000000";
	background.style.position ="absolute";
	background.style.top = "0px";
	background.style.left = "0px";
	background.style.width = "100%";
	background.style.height = "100%";

	var oldDefault = null;		// this is set to the old default button while the dialog is showing
	
	elem.show = function()
	{
		// display the transparent grey background and the dialog on top of it
		document.body.appendChild(background);
		document.body.appendChild(dialog);
		
		oldDefault = JugDefaultButtonId;
		JugSetNoDefaultButton();	// disable any default button
	}
	
	elem.hide = function()
	{
		background.parentNode.removeChild(background);
		dialog.parentNode.removeChild(dialog);
		
		// restore the old default button
		if( oldDefault )
		{
			JugSetDefaultButton( oldDefault );	
			oldDefault = null;
		}
	}
	
	elem.setPosition = function( x, y )
	{
		var area = { left : x, top : y, width : dialog.offsetWidth, height : dialog.offsetHeight };
		area = JugConstrainToPage( area );
		dialog.style.left = area.left + "px";
		dialog.style.top = area.top + "px";
	}
}

//====== Standard Button interface =========

// JUG factory function
function JugSetupButton( id, props )
{
	// id		string id of button element that is to be converted into a JUG button
	// props	string property set of the JUG button
	//			{ function action, optional string status }
	
	// get the button element that is to be converted
	var buttonElem = JugElem( id );

	// pick out the properties
	var clickAction = props.action;
	var statusMessage = props.status;
	var normalStyle = props.normal;
	var defaultStyle = props.def;
	var disabledStyle = props.disable;
	var depressedStyle = props.depressed;

	var name = ( buttonElem.name == "" ? "Button " + id : buttonElem.name );

	// fill in the defaults for the various styles
	if( normalStyle === undefined )
	{
		normalStyle = "JugNormalButton";
	}
	if( defaultStyle === undefined )
	{
		defaultStyle = "JugDefaultButton";
	}
	if( depressedStyle === undefined )
	{
		depressedStyle = "JugDepressedButton";
	}
	if( disabledStyle === undefined )
	{
		disabledStyle = "JugDisabledButton";
	}

	var enabled = true;
	
	if( statusMessage === undefined )
	{
		statusMessage = "";
	}

	// if no action property is defined, use the object's onclick handler
	if( clickAction === undefined )
	{
		if( buttonElem.onclick )
		{
			clickAction = buttonElem.onclick;
		}
		else
		{
			clickAction = function(){ alert( "No action defined for button " + id + "(" + name + ")" ); };
		}
	}

	// variable to hold timeout object used to run button script asynchronously
	var actionTimer = null;

	// called asynchronously when button is clicked
	function runClickAction()
	{
		clickAction( buttonElem );			// call the user defined click action, passing the button element as a parameter
		// restore the styling
		if( JugDefaultButtonId == id )
		{
			buttonElem.className = defaultStyle;
		}
		else if( enabled )
		{
			buttonElem.className = normalStyle;
		}
		else
		{
			// not enabled, so button action must've disabled it otherwise we couldn't be clicked in the first place
			buttonElem.className = disabledStyle;
		}
		window.status = "";					// clear the window status
		actionTimer = null;
	}

	// onclick event handler for button
	buttonElem.onclick = function( evt )
	{
		if( enabled && actionTimer == null )
		{
			buttonElem.className = depressedStyle;
			window.status = statusMessage;					// display the status message while the action runs
			actionTimer = setTimeout( runClickAction, 1 );	// schedule the action to run
		}
	}
	
	var amDefault = false;	// record whether this button is the default button

	buttonElem.className = normalStyle;		// initially use normal styling
	
	// render this as the default button
	buttonElem.setDefault = function()
	{
		if( JugDefaultButtonId != null && JugElemExists(JugDefaultButtonId) )	// check the button is not hidden
		{
			JugElem( JugDefaultButtonId ).setNoDefault();
		}
		buttonElem.className = defaultStyle;
		JugDefaultButtonId = id;
		amDefault = true;
	}

	// render this as normal, non-default, button
	buttonElem.setNoDefault = function()
	{
		// this button is not default
		if( enabled )
		{
			buttonElem.className = normalStyle;
		}
		else
		{
			buttonElem.className = disabledStyle;
		}
		amDefault = false;
	}

	// event handler called when RETURN is pressed
	buttonElem.onReturn = function()
	{
		if( amDefault )
		{
			if( enabled && actionTimer == null )
			{
				// RETURN pressed and this is the default button, so count it as a click
				buttonElem.className = depressedStyle;
				window.status = statusMessage;
				actionTimer = setTimeout( runClickAction, 1 );
			}
			return false;	// do not pass the message on
		}
		else
		{
			return true;	// pass the message on
		}
	}

	// Enable / disable the button
	buttonElem.enable = function( enb )
	{
		enabled = enb;
		if( enabled )
		{
			buttonElem.className = normalStyle;
		}
		else
		{
			buttonElem.className = disabledStyle;
		}
	}
	
	// Get whether the button is enabled
	buttonElem.isEnabled = function()
	{
		return enabled;
	}
	
	buttonElem.JugType = "button";
}

//========= Sticky Note ========

// JUG factory function
function JugShowSticky( text, id, styleName )
{
	// text		text of the sticky
	// id		string id of element where the note is to be placed, or null if it is to be centred

	var container = document.createElement('div');	// div holding the sticky note
	
	if( styleName )
	{
		container.className = styleName;
	}
	else
	{
		container.style.background = "#F0F0F0";
		container.style.borderStyle = "outset";
	}

	container.style.position = "absolute";
	container.style.width = 120 + "px";

	container.appendChild( document.createTextNode( text ) );

	document.body.appendChild( container );
	
	// check if the text overflows the width and if so widen the box
	while( container.offsetWidth < container.scrollWidth )
	{
		container.style.width = ( parseInt( container.style.width ) + 10 ) + "px";
	}

	if( id )
	{
		var area = JugGetElementArea( JugElem( id ) );
		container.style.left = (area.left + area.width) + "px";
		container.style.top = (area.top + area.height) + "px";
	}
	else
	{
		var area = JugGetWindowArea();
		container.style.left = (area.left + area.width / 2) + "px";
		container.style.top = (area.top + area.height / 2) + "px";
	}
	
	container.onmousedown = function()
	{
		container.parentNode.removeChild( container );
	}
	
	container.deleteSticky = function()
	{
		container.parentNode.removeChild( container );
		return true;
	}
	
	container.JugType = "sticky";
}

function JugDeleteStickies()
{
	JugSendMessage( document.body, "deleteSticky", [] );
}

//============ Tabbed display ====================

// JUG factory function
function JugCreateTabbedDisplay( id, props )
{
	// id		string id of div element that is to become a tabbed display
	// props	string property set of the JUG tabbed display
	//			{ string tabList, function change, function notify }  - tablist is comma separated list of tab-name:div-name

	var tabs = props.tabList.split( "," );	// array of "tab names : div names" for the tabs in the set
	var change = eval( props.change );
	var notify = eval( props.notify );
	
	// find the element that is to be the tab set
	var tabsElem = JugElem( id );
	tabsElem.style.display = "inline";

	// create a div to push the tab bodies below the tab header
	var space = document.createElement( 'div' );
	space.style.width = 1 + "px";
	space.style.height = 1 + "px";
	tabsElem.appendChild( space );		// currently don't know the height, but it gets set properly once it is known

	var gap = 10;			// gap between tabs in the tab header

	var x = 0;				// becomes the width of the tab header
	var h = 0;				// becomes the height of the tab header
	var tabSet = {};		// map from div name to tab header div object
	var bodySet = {};		// map from div name to tab body div object
	var enabledSet = {};	// map from div name to enabled bool

	var currentTab = "";

	function clickTab( divName )
	{
		if( notify )
		{
			notify( divName );
		}
		else
		{
			selectTab( divName );
		}
	}

	function selectTab( divName )
	{
		if( enabledSet[ divName ] )
		{
			for( name in tabSet )
			{
				var tab = tabSet[ name ];
				var tabBody = bodySet[ name ];
				if( name == divName )
				{
					// this is the tab that is to be selected
					tabBody.style.display = "";
					tab.selected = true;
					tab.style.backgroundColor = "#c0c0c0";
					tab.style.borderBottomWidth = 0;
					tab.style.height = h - 4;
					
					JugSendMessage( tabBody, "onDisplay", [] );
					if( change )
					{
						change( currentTab, divName );
					}
					currentTab = divName;
				}
				else
				{
					// this tab is not selected
					tabBody.style.display = "none";
					tab.selected = false;
					tab.style.backgroundColor = "#f0f0f0";
					tab.style.borderBottomWidth = "1px";
					tab.style.height = h - 5;
				}
			}
		}
	}

	function enableTab( divName, enabled )
	{
		enabledSet[ divName ] = enabled == true;
	}

	var i;
	for( i in tabs )
	{
		var tabsSplit = tabs[ i ].split( ":" );
		var tabName = tabsSplit[ 0 ];
		var divName = tabsSplit[ 1 ];
		
		var text = document.createTextNode( tabName );
		var div = JugElem( divName );

		var tab = document.createElement( 'a' );
		
		tab.style.backgroundColor = "#f0f0f0";
		tab.style.border = "1px solid #000000";
		tab.style.borderBottomWidth = 0;
		tab.style.padding = "2px 1em 2px 1em";
		tab.style.textDecoration = "none";
		
		tab.style.position = "absolute";
		tab.style.left = (tabsElem.offsetLeft + x) + "px";
		tab.style.top = tabsElem.offsetTop + "px";
		
		tab.selected = false;

		// set up the event handlers for the tab 
		tab.onmouseover = function( tab )
		{
			return function()
			{
				if( tab.selected )
				{
					tab.style.backgroundColor = "#c0c0c0";
				}
				else
				{
					tab.style.backgroundColor = "#d0d0d0";
				}
				tab.oldCursor = tab.style.cursor;
				tab.style.cursor = "pointer";
			}
		}( tab );
		
		tab.onmouseout = function( tab )
		{
			return function()
			{
				if( tab.selected )
				{
					tab.style.backgroundColor = "#c0c0c0";
				}
				else
				{
					tab.style.backgroundColor = "#f0f0f0";
				}
				tab.style.cursor = tab.oldCursor;
			}
		}( tab );
		
		tab.onclick = function( divName )
		{
			return function()
			{
				if( currentTab != divName )
				{
					clickTab( divName );
				}
			}
		}( divName );

		tab.JugType = "tab";
		tab.onDisplay = function( tab )		// called when element gets displayed
		{
			return function()
			{
				tab.style.top = tabsElem.offsetTop + "px";
			}
		}( tab );

		tab.onResize = function( tab )		// called when window gets resized
		{
			return function()
			{
				tab.style.top = tabsElem.offsetTop + "px";
			}
		}( tab );

		// remember the tab in the tab set
		tabSet[ divName ] = tab;
		
		// add the tab to the tab set header
		tab.appendChild( text );
		tabsElem.appendChild( tab );
		tabsElem.appendChild( div );

		// calculate the size of the tab set header
		x = x + tab.offsetWidth + gap;
		h = Math.max( h, tab.offsetHeight );

		// show the tab body
		div.style.display = "inline";
		div.style.backgroundColor = "#c0c0c0";
		div.style.border = "1px solid #000000";
		div.style.padding = "2px 1em 2px 1em";

		// remember the tab body in the set of tab bodies
		bodySet[ divName ] = div;
		
		// enabled by default
		enabledSet[ divName ] = true;
		
	} // end loop over tab names
	
	// set the spacers height to the correct value
	space.style.height = h;

	tabsElem.select = selectTab;
	tabsElem.enable = enableTab;
	tabsElem.getCurrent = function(){ return currentTab; };
	tabsElem.getTabHeight = function(){ return h; };
	tabsElem.JugType = "TabSet";

	var firstDivName = tabs[ 0 ].split( ":" )[ 1 ];
	
	tabsElem.onJugLoaded = function()					// called when the page has been loaded
	{
		// All loaded, now select the first tab
		selectTab( firstDivName );
	};
}

//============ Menu and MenuItem =========

// JUG factory function for Menu
function JugCreateMenu( id, props )
{
	// id		string id of table element that is to become a menu
	// props	string property set of the JUG menu - use {}
	
	// find the div element that is to be the menu
	var menuElem = JugElem( id );
	JugClearId( id );		// clear element's ID as it is going to be given to the floating div

	// create a div to float the menu
	var container = document.createElement('div');	// div holding the entire modal frame
	container.id = id;

	container.style.position = "absolute";
	container.style.left = 0 + "px";
	container.style.top = 0 + "px";
	container.style.background = "#FFFFFF";
	container.appendChild( menuElem );	

	// prevent the div's children's text being selected
	container.onselectstart = function () { return false; } // ie
  	container.onmousedown = function () { return false; } // mozilla

	container.JugType = "menu";
	JugKeepDetached( container );	// remember container as a detached object that is not in the DOM

	var oldDefaultButtonId = null;	// the ID of the button that was default when the frame is shown

	// Create methods for hiding/showing the frame
	
	var area = null;
	
	var parent = null;

	container.show = function( foreground, background )
	{
		document.body.appendChild( container );
		
		// now position the dialog at the mouse coordinates
		container.style.left = (window.JugMouseCoords.x - 8) + "px";
		container.style.top = (window.JugMouseCoords.y - 8) + "px";

		// adjust to fit page
		area = JugConstrainToPage( JugGetElementArea( container ) );
		container.style.left = area.left + "px";
		container.style.top = area.top + "px";
		
		JugSendMessage( this, "ready", [this,foreground,background] );
		parent = window.JugMoveElem;
		window.JugMoveElem = container;		// capture mouse movements
		
		oldDefaultButtonId = JugDefaultButtonId;
		JugSetNoDefaultButton();
	}

	container.hide = function()
	{
		container.parentNode.removeChild( container );
		
		window.JugMoveElem = parent;
		JugSetDefaultButton( oldDefaultButtonId );
		oldDefaultButtonId = null;
	}
	
	container.selected = function()
	{
		container.hide();
		if( parent != null )
		{
			// ripple up the tree......
			parent.hide();
		}
	}
	
	container.ready = function()
	{
		// we do nothing - this message is intended for our children
	}
	
	// function that is called when the mouse moves and the container is visible
	container.mouseMove = function()
	{
		if( !JugWithin( window.JugMouseCoords, area ) )
		{
			container.hide();
		}
	}
}

// JUG factory function for MenuItem
function JugCreateMenuItem( id, props )
{
	// id		string id of text input that is to become a menu item
	// props	string property set of the JUG menu item
	//			{ function action, value result }
	
	var action = props.action;
	
	// take the element and use it as the menu legend
	var elem = JugElem( id );
	var oldBackground = getStyleElement( elem, "background-color" );
	var oldForeground = getStyleElement( elem, "color" );
	elem.newForeground = props.foreground;
	elem.newBackground = props.background;
	
	// prevent the anchor's text being selected
	elem.onselectstart = function () { return false; } // ie
  	elem.onmousedown = function () { return false; } // mozilla

	var parent = null;
	elem.enabled = true;		// whether this menu item is enabled
	
	elem.onmouseout = function()
	{
		this.style.cursor = originalCursor;

		if( elem.enabled )
		{
			elem.style.color = oldForeground;
			elem.style.backgroundColor = oldBackground;
		}
	}

	elem.onmouseover = function()
	{
		originalCursor = this.style.cursor;				// remember the cursor style so it can be restored
		this.style.cursor = "pointer";					// set the cursor to "pointer" while the mouse is over the button

		if( elem.enabled )
		{
			if( elem.newBackground )
			{
				elem.style.color = elem.newForeground;
			}
			if( elem.newForeground )
			{
				elem.style.backgroundColor = elem.newBackground;
			}
		}
	}

	elem.ready = function( container, foreground, background )
	{
		parent = container;
		// remember the foreground/background colours as properties of the element so any derivative can access them
		elem.newForeground = foreground;
		elem.newBackground = background;
	}
	
	elem.onmouseup = function()
	{
		if( elem.enabled )
		{
			parent.selected();
			if( action != null )
			{
				action( elem );
			}
		}
	}
	
	elem.id = id;
	elem.JugType = "menuItem";
}

// JUG factory function for SubMenu
function JugCreateSubMenu( id, props )
{
	// id		string id of menu item
	// props	string property set of the JUG menu value
	//			{ child name of sub menu }
	
	var menuItem = JugElem( id );	// the menu item element	
	var child = props.child;		// name of child menu to display if this item is selected

	menuItem.onmouseup = function()
	{
		if( menuItem.enabled )
		{
			var foreground = ( props.foreground ? props.foreground : menuItem.newForeground );
			var background = ( props.background ? props.background : menuItem.newBackground );
	
			var subMenu = JugElem( child );
			subMenu.show( foreground, background );
		}
	}
}

// JUG factory function for OptionalItem menu items
function JugCreateOptionalItem( id, props )
{
	// id		string id of optional item
	// props	string property set of the JUG menu value
	//			{ optional = function to check whether element is enabled, optionalForeground = colour to use if disabled }
	var elem = JugElem( id );
	var optionalTest = props.optional;
	
	var oldForeground = getStyleElement( elem, "color" );

	var oldReady = elem.ready;
	elem.ready = function()
	{
		if( optionalTest && optionalTest( elem ) )
		{
			elem.enabled = true;
			elem.style.color = oldForeground;
		}
		else
		{
			elem.enabled = false;
			elem.style.color = props.optionalForeground;
		}
		
		if( oldReady )
		{
			oldReady.apply( this, arguments );
		}
	}
}

// JUG factory function for dynamic menus
function JugCreateDynamicMenu( id, props )
{
	// id		string id of dynamic menu item
	// props	string property set of the JUG dynamic menu
	//			{ optional = function to check whether element is enabled, optionalForeground = colour to use if disabled }
	var menuElem = JugElem( id );
	var n = menuElem.childNodes.Length;
	
	var action = props.action;
	var maxHeight = (props.maxHeight ? props.maxHeight : 200 );

	var oldForeground;
	var oldBackground;
	
	var oldReady = menuElem.ready;
	menuElem.ready = function( container, foreground, background )
	{
		var parent = container;
		
		// Remove any existing nodes from the menu
		var menu = menuElem.firstChild;		// the menu is a div that contains a div with the menu items in it
 		while( menu.hasChildNodes() )
		{
	        menu.removeChild( menu.firstChild );
		}

		var contents = props.content( menuElem );
		var recents = contents.recents;
		var values = contents.values;
		
		var itemHeight;	// this is set to the height of an item, allowing us to scroll to a particular item when a key is struck - all items are the same height
		var items = new Array();	// builds into an array of the items, allowing them to be selected by key-down
		var currentItem = null;		// refers to currently selected item
		
		function addItem( menu, e, v )
		{
			var d = document.createElement( 'div' );
			var text = document.createTextNode( e );
			d.appendChild( text );	
			menu.appendChild( d );
			items.push( d );
			
			// remember height of item
			itemHeight = JugGetElementArea( d ).height;
			
			d.onmouseover = function()
			{
				originalCursor = this.style.cursor;				// remember the cursor style so it can be restored
				this.style.cursor = "pointer";					// set the cursor to "pointer" while the mouse is over the item
				
				if( currentItem != null )
				{
					// some other item is selected (by key clicks) and so must be de-selected
					currentItem.onmouseout();
				}

				currentItem = d;		// the highlighted item
		
				oldForeground = d.style.color;
				if( foreground )
				{
					d.style.color = foreground;
				}
				oldBackground = d.style.backgroundColor;
				if( background )
				{
					d.style.backgroundColor = background;
				}
			}
			
			d.onmouseout = function()
			{
				this.style.cursor = originalCursor;

				if( currentItem != null && currentItem != d )
				{
					// some other item is selected (by key clicks) and so must be de-selected
					currentItem.onmouseout();
				}
				
				currentItem = null;
				
				d.style.color = oldForeground;
				d.style.backgroundColor = oldBackground;
			}

			d.onmouseup = function()
			{
				parent.selected();
				if( action != null )
				{
					action( v );
				}
			}
		}
		
		// add the recents 
		for( e in recents )
		{
			addItem( menu, e, recents[ e ] );
		}
		
		if( recents.length != 0 && values.length != 0 )
		{
			// add a dividing bar to separate the recents from the main list
			var bar = document.createElement( 'hr' );
			menu.appendChild( bar );
		}
		
		// add the top scroll bar, zero sized until proved to be needed
		var topBar = document.createElement( 'div' );
		topBar.style.height = "0px";
		topBar.style.backgroundColor = "#202020";
		menu.appendChild( topBar );
		
		// add div that clips the area for the main list of values, with a div inside it to hold the actual list
		var clipArea = document.createElement( 'div' );
		menu.appendChild( clipArea );
		var scrollArea = document.createElement( 'div' );
		clipArea.appendChild( scrollArea );

		// add the bottom scroll bar, zero sized until proved to be needed
		var bottomBar = document.createElement( 'div' );
		bottomBar.style.height = "0px";
		bottomBar.style.backgroundColor = "#202020";
		menu.appendChild( bottomBar );

		// add the main list of values
		items = new Array();		// reset the array of items so we do not record the recents list
		for( e in values )
		{
			addItem( scrollArea, e, values[ e ] );
		}

		var actualHeight = JugGetElementArea( clipArea ).height;
		var minOffset = -(actualHeight - maxHeight);	// most negative offset leaves last list element at the bottom of the scroll area
		if( actualHeight > maxHeight )
		{
			// reveal the scroll bars
			topBar.style.height = "10px";
			bottomBar.style.height = "10px";
			// add the mouse-over handlers
			var interval;										// interval object
			// when the mouse moves over one of the scroll bars, start an interval timer that scrolls the area in the appropriate direction
			function makeMouseOver( delta )
			{
				function doScroll()
				{
					var top = parseInt( scrollArea.style.top );
					scrollArea.style.top = Math.min( 0, Math.max( minOffset, top + delta ) ) + "px";
				}
				return function()
				{
					interval = setInterval( doScroll, 5 );
				}
			}
			topBar.onmouseover = makeMouseOver( +1 );
			bottomBar.onmouseover = makeMouseOver( -1 );
			// when the mouse moves out of the scroll bar, cancel the timer
			topBar.onmouseout = function()
			{
				clearInterval( interval );
			}
			bottomBar.onmouseout = topBar.onmouseout;
	
			var w = parseInt( JugGetElementArea( clipArea ).width );

			// position the clip area relative so the scroll area within it can be positioned absolute to effect the scrolling
			clipArea.style.position = "relative";
			clipArea.style.width = w + "px";
			clipArea.style.height = maxHeight + "px";
			// make the clip area hide the scroll area where it overflows the clip area
			clipArea.style.overflow = "hidden";
			clipArea.style.overflowX = "hidden";

			// place the scroll area in an absolute position within the clip area
			scrollArea.style.position = "absolute";
			scrollArea.style.width = w + "px";
			scrollArea.style.height = maxHeight + "px";
			scrollArea.style.top = "0px";
			scrollArea.style.left = "0px";
		}
		
		// handle key-down events
		
		var searchString = "";	// the search string typed so far
		var searchTimer;		// timer that clears the search string
		
		menuElem.onKeydown = function( key )
		{
			function clearSearch()
			{
				searchString = "";
			}
			clearTimeout( searchTimer );
			searchTimer = setTimeout( clearSearch, 500 );
			
			searchString = searchString + key.toUpperCase();
			
			var index = 0;
			for( e in values )
			{
				var name = e.toUpperCase();
				if( name.length >= searchString.length && name.substring( 0, searchString.length ) == searchString )
				{
					scrollArea.style.top = Math.max( minOffset, - index * itemHeight ) + "px";
					if( currentItem != null )
					{
						currentItem.onmouseout();
					}
					currentItem = items[ index ];
					currentItem.onmouseover();
					break;
				}
				index += 1;
			}
			return false;
		}
		
		menuElem.onReturn = function()
		{
			if( currentItem != null )
			{
				currentItem.onmouseup();
			}
			return false;
		}
		
		if( oldReady )
		{
			oldReady.apply( this, arguments );
		}
	}
}

//============ Grid =========

// JUG factory function for Menu
function JugCreateGrid( id, props )
{
	// id		string id of grid element
	// props	string property set of the JUG grid
	var container = JugElem( id );
	
	var legends = props.legends.split(",");
	var highliteStyle = props.highlite;
	var normalStyle = props.normal;
	var action = props.action;
	
	// work out which columns are smart
	var smart = new Array();
	var i;
	for( i in legends )
	{
		if( legends[ i ].substring(0,1) == '*' )
		{
			smart[ i ] = true;
			legends[ i ] = legends[ i ].substring(1);
		}
		else
		{
			smart[ i ] = false;
		}
	}

	var data = new Array();								// the data placed in the grid

	var table;											// the table that is placed inside the container
	var current = -1;
	var rows = new Array();		// array of the table elements representing the rows
		
	container.clear = function()
	{
		// Delete any existing data
		data = new Array();
		current = -1;
		rows = new Array();
		
		// Empty the container
		while( container.hasChildNodes() )
		{
		  container.removeChild( container.firstChild );
		}
		
		// Create the table
		table = document.createElement('table');
		container.appendChild( table );
		table.cellSpacing = "0px";

		// add the header row
		var row = table.insertRow(-1);

		var i;
		for( i in legends )
		{
			var col = row.insertCell(-1);
			var text = document.createTextNode( legends[ i ] );
			col.appendChild( text );
			if( i != legends.length-1 )
			{
				var gapCol = row.insertCell(-1);
				var gapText = document.createTextNode( "\u00a0\u00a0" );
				gapCol.appendChild( gapText );
			}
		}
		
		// add the rule
		var ruleRow = table.insertRow(-1);
		var ruleCol = ruleRow.insertCell(-1);
		ruleCol.colSpan = legends.length * 2 - 1;		// span col+gap
		ruleCol.appendChild( document.createElement( 'hr' ) );
	}

	function highlight( n )
	{
		if( n != -1 )
		{
			rows[ n ].className = highliteStyle;
			
			var cells = rows[ n ].cells;
			var i;
			for( i=0; i<cells.length; i+=2 )		// skip over gap columns
			{
				if( cells[ i ].firstChild.duplicate )
				{
					cells[ i ].firstChild.style.visibility = "";
				}
			}
		}
	}
	
	function unhighlight( n )
	{
		if( n != -1 )
		{
			rows[ n ].className = normalStyle;		// highlight the row

			var cells = rows[ n ].cells;
			var i;
			for( i=0; i<cells.length; i+=2 )		// skip over gap columns
			{
				if( cells[ i ].firstChild.duplicate )
				{
					cells[ i ].firstChild.style.visibility = "hidden";
				}
			}
		}
	}
	
	function makeMouseOver( n )
	{
		return function()
		{
			unhighlight( current );
			highlight( n );
			current = n;
		}
	}
	
	function makeMouseOut()
	{
		return function()
		{
			unhighlight( current );
			current = -1;
		}
	}
	
	function makeMouseUp( n )
	{
		return function()
		{
			if( action )
			{
				action( n, container );
			}
		}
	}
	
	container.append = function( elements )
	{
		var count = Math.min( elements.length, legends.length );
		if( count > 0 )
		{
			var row = table.insertRow(-1);
			row.className = normalStyle;

			row.onmouseover = makeMouseOver( data.length );
			row.onmouseout = makeMouseOut();
			row.onmouseup = makeMouseUp( data.length );
			
			data.push( elements );			// add this row to the table of data
			rows.push( row );
			
			var i;
			for( i=0; i < count; i++ )
			{
				var col = row.insertCell(-1);
				var div = document.createElement('div');	// create a div inside the cell so we can make it invisible while the cell remains visible
				col.appendChild( div );
				var text = document.createTextNode( elements[ i ] );
				div.appendChild( text );
				if( data.length != 1 && data[ data.length - 2 ][ i ] == elements[ i ] && smart[i] )
				{
					div.style.visibility = "hidden";
					div.duplicate = true;			// marks as a duplicate
				}
				else
				{
					div.style.visibility = "";
					div.duplicate = false;			// mark as not a duplicate
				}
				if( i != count-1 )
				{
					var gapCol = row.insertCell(-1);
					var gapText = document.createTextNode( "\u00a0\u00a0" );
					gapCol.appendChild( gapText );
				}
			}
		}
	}

	container.get = function( n )
	{
		return data[ n ];
	}
	
	container.clear();
	container.JugType = "grid";	
}

//============ ToolTip =========

// JUG factory function for tool tip
function JugCreateToolTip( id, props )
{
	// id		string id of element
	// props	string property set of the JUG tool tip
	var object = JugElem( id );
	
	var toolTipText = props.tip;
	var interval = ( props.interval ? props.interval : 1000 );

	var container = document.createElement( 'a' );
	object.parentNode.replaceChild( container, object );
	container.appendChild( object );
	
	var tip = document.createElement( 'div' );
	tip.style.position = "absolute";
	tip.style.top = 0 + "px";
	tip.style.left = 0 + "px";
	tip.style.width = 100 + "px";

	tip.style.borderStyle = "outset";
	tip.style.padding = 5 + "px";
	tip.style.borderWidth = "2px";
	tip.style.backgroundColor = "#f0f0f0";
	
	var tipText = document.createTextNode( toolTipText );
	tip.appendChild( tipText );

	var timer = null;			// timer used to fire tooltip
	var visible = false;		// whether the tool tip is displayed
	var hide = false;			// whether to hide the tool tip
	var position = null;		// the position where the mouse stopped

	tip.onmousemove = function()
	{
		container.removeChild( tip );	// stop displaying the tool tip
	}

	function tick()
	{
		timer = null;
		
		if( hide )
		{
			return;
		}
		
		// Decide where to place the tool tip
		var cursorHeight = 15;
		var area = { left : window.JugMouseCoords.x, top : window.JugMouseCoords.y + cursorHeight, width : tip.offsetWidth, height : tip.offsetHeight };
		tip.style.left = area.left + "px";
		tip.style.top = area.top + "px";
		
		// Make it visible
		container.appendChild( tip );
		visible = true;
		
		// Remember the position of the mouse
		position = window.JugMouseCoords;
	}
	
	container.onmouseover = function()
	{
		if( timer != null )
		{
			clearTimeout( timer );
		}
		timer = setTimeout( tick, interval );
	}
	
	container.onmouseout = function()
	{
		if( visible )
		{
			container.removeChild( tip );	// stop displaying the tool tip
			visible = false;
		}
		if( timer != null )
		{
			clearTimeout( timer );
			timer = null;
		}
	}
	
	container.onmousemove = function()
	{
		if( visible )
		{
			if( window.JugMouseCoords.x == position.x && window.JugMouseCoords.y == position.y )
			{
				// mouse hasn't actually moved - must be a screen resize gitter
			}
			else
			{
				container.removeChild( tip );	// stop displaying the tool tip
				visible = false;
			}
		}
		if( timer != null )
		{
			clearTimeout( timer );
		}
		timer = setTimeout( tick, interval );
	}

	var originalOnFocus = object.onfocus;
	object.onfocus = function()
	{
		if( visible )
		{
			container.removeChild( tip );	// stop displaying the tool tip
			visible = false;
		}
		if( timer != null )
		{
			clearTimeout( timer );
			timer = null;
		}
		hide = true;
		if( originalOnFocus )
		{
			originalOnFocus();
		}
	}
	
	var originalOnBlur = object.onblur;
	object.onblur = function()
	{
		hide = false;
		if( originalOnBlur )
		{
			originalOnBlur();
		}
	}
	
	if( object.JugType )
	{
		// object is already a Jug Object so preserve its type
	}
	else
	{
		object.JugType = "toolTip";
	}
}

//============ TextField =========

// JUG factory function for a text field
function JugCreateTextField( id, props )
{
	// id		string id of input text element or textarea
	// props	string property set of the JUG text field
	var object = JugElem( id );

	var change = eval( props.change );
	var update = eval( props.update );

	var originalValue = object.value;	// remember the original value of the text field
	var previousValue = object.value;	// remember the previous value reported
	
	var editable = true;

	object.onkeydown = function( e )
	{
		if( editable )
		{
			return true;
		}
		else
		{
			if( e !== undefined && e.stopPropagation )
			{
				e.stopPropagation();
			}
			else
			{
				event.cancelBubble = true;
			}
			return false;
		}
	}
	
	object.onkeyup = function()
	{
		if( object.value == originalValue )
		{
			if( previousValue != originalValue && change )
			{
				change( true );		// changed back to original value
			}
		}
		else
		{
			if( previousValue == originalValue && change )
			{
				change( false );	// changed to a different value
			}
		}
		
		if( update )
		{
			update();
		}

		previousValue = object.value;
	}

	object.SetValue = function( v )
	{
		if( previousValue != originalValue && change )
		{
			change( true );	// back to (new) original value
		}
		object.value = v;
		originalValue = v;
		previousValue = v;
	}

	object.EditValue = function( v )
	{
		object.value = v;
		if( object.value == originalValue )
		{
			if( previousValue != originalValue && change )
			{
				change( true );		// changed back to original value
			}
		}
		else
		{
			if( previousValue == originalValue && change )
			{
				change( false );	// changed to a different value
			}
		}
		previousValue = object.value;
	}

	object.GetValue = function()
	{
		return object.value;
	}
	
	object.HasChanged = function()
	{
		return object.value != originalValue;
	}

	object.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	object.JugType = "textField";	
}

//============ CountWords =========

// JUG factory function for a Count Words field
function JugCreateCountWordsField( id, props )
{
	// id		string id of input text field
	// props	string property set of the JUG Count Words field
	//				changeCount - function called when field's word count changes
	
	var object = JugElem( id );

	var change = eval( props.changeCount );

	function countWords( val )
	{
		var count = 0;
		var i=0;
		// skip leading spaces
		while( i < val.length && val.charAt( i ) <= " " )
		{
			i += 1;
		}
		if( i == val.length )
		{
			// entirely blank, so count is left at zero
		}
		else
		{
			// found the start of the first word
			count = 1;
			// look for beginning of each subsequent word
			while( i < val.length )
			{
				if( val.charAt( i ) <= " " && i+1 < val.length && val.charAt( i+1 ) > " " )
				{
					// found a space character followed by a non-space
					count += 1;
				}
				i += 1;
			}
		}
		return count;
	}
	
	// count existing words
	var currentCount = countWords( object.value );
	change( currentCount );	// callback so app can display initial count

	// chain new onChange handler onto the object
	var originalOnkeyup = object.onkeyup;
	
	object.onkeyup = function()
	{
		// update the word count
		var newCount = countWords( object.value );
		if( newCount != currentCount )
		{
			// word count has changed
			currentCount = newCount;
			change( currentCount );
		}

		// call original key-up handler if one is defined
		if( originalOnkeyup )
		{
			return originalOnkeyup();
		}
		else
		{
			return true;
		}
	}
	
	// chain new SetValue function
	var originalSetValue = object.SetValue;
	
	object.SetValue = function( value )
	{
		// update the word count
		var newCount = countWords( value );
		if( newCount != currentCount )
		{
			// word count has changed
			currentCount = newCount;
			change( currentCount );
		}
		
		// call original SetValue if one is defined
		if( originalSetValue )
		{
			originalSetValue( value );
		}
	}

	if( object.JugType )
	{
		// object is already a Jug Object so preserve its type
	}
	else
	{
		object.JugType = "countWords";
	}
}

//============ CheckBox =========

// JUG factory function for a check box
function JugCreateCheckBox( id, props )
{
	// id		string id of checkbox
	var object = JugElem( id );
	
	var change = eval( props.change );
	
	var originalValue = object.checked;		// remember the original value of the text field
	var previousValue = object.checked;		// remember the previous value reported

	var editable = true;		// whether the check box can be changed by the user
	var notify = null;			// function to call when value changes to notify anyone who has registered interest

	var originalOnclick = object.onclick;

	object.onclick = function()
	{
		if( !editable )
		{
			return false;
		}
		
		if( object.checked == originalValue )
		{
			if( previousValue != originalValue && change )
			{
				change( true );		// changed back to original value
			}
		}
		else
		{
			if( previousValue == originalValue && change )
			{
				change( false );	// changed to a different value
			}
		}
		previousValue = object.checked;
		
		// tell anyone who's interested that the check box has changed
		if( notify )
		{
			notify( object.checked );
		}
		
		// call original onclick function, if any
		if( originalOnclick )
		{
			originalOnclick();
		}
	}
	
	object.SetValue = function( value )
	{
		if( previousValue != originalValue && change )
		{
			change( true );
		}
		object.checked = value;
		originalValue = value;
		previousValue = value;
	}

	object.GetValue = function()
	{
		return object.checked;
	}

	object.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	object.JugRegister = function( callback )
	{
		var oldNotify = notify;
		notify = function( v )
		{
			if( oldNotify )
			{
				oldNotify( v );
			}
			callback( v );
		}
	}
	
	object.JugType = "checkBox";	
}

//============ DropDown =========

// JUG factory function for a drop down
function JugCreateDropDown( id, props )
{
	// id		string id of select drop down
	// props	{ change } - string property set of the JUG drop down
	//			
	// props.change		a function(asOriginal) that is called when the drop down's value changes from or to the drop down's original value
	//					asOriginal gives whether the drop down now has its original value
	
	var object = JugElem( id );
	
	var displayedStrings = [];				// array of displayed strings - one for each element in the drop down
	var selectableValues = [];				// array of selectable value objects - one for each element in the drop down
	
	if( object.options )
	{
		var i;
		for( i=0; i < object.options.length; i++ )
		{
			displayedStrings.push( object.options[ i ].text );
			selectableValues.push( object.options[ i ].value );
		}
	}

	var change = eval( props.change );		// on-change handler (diffs from original state)
	var update = eval( props.update );		// on-update handler (changes at all)
	var clicking = false;					// whether the user is clicking to change the drop down (rather than a programmatic call)
	
	var originalValue = object.value;		// remember the original value of the select drop down
	var previousValue = object.value;		// remember the previous value reported

	var originalIndex = 0;					// index of selected element before a change is made
	
	var editable = true;					// whether the drop down can be changed by the user

	// Set the list of displayed options and their associated values
	object.SetOptions = function( items )
	{
		// items - map from displayed string to selected value object
		
		// remove existing elements
		while( object.length != 0 )
		{
			object.remove(0);
		}

		// create an option element for each of the items
		
		var index = 0;	// index position of next item in drop down
		
		displayedStrings = [];
		selectableValues = [];

		for( itemText in items )
		{
			var option = document.createElement( 'option' );
			
			// set the option to display the displayed string and have a value of the index for the item
			option.text = itemText;
			option.value = index;
			
			// note the displayed string and selected value object for the item
			displayedStrings[ index ] = itemText;
			selectableValues[ index ] = items[ itemText ];
			
			// add the option to the drop down
			try{
				object.add( option, null ); // standards compliant
			}
			catch( ex )
			{
				object.add( option );	// IE only
			}
			
			index += 1;	// count the index position of the item in the drop down
		}

		originalValue = object.value;		// remember the original value of the select drop down
		previousValue = object.value;		// remember the previous value reported
	}
	
	// Get the options
	object.GetOptions = function()
	{
		var options = [];
		for( i in displayedStrings )
		{
			options[ displayedStrings[ i ] ] = selectableValues[ i ];
		}
		return options;
	}
	
	// mouse down event handler for drop down - called before the drop down changes value
	object.onmousedown = function()
	{
		clicking = true;
		return editable;		// kill the mouse down event if not editable
	}

	// mouse up event handler for drop down
	object.onmouseup = function()
	{
		clicking = false;
		originalIndex = object.selectedIndex;	// assume the value has changed, so remember it for next time
		return editable;		// kill the mouse down event if not editable
	}

	// on click event handler for drop down
	object.onclick = function()
	{
		if( object.value == originalValue )
		{
			if( previousValue != originalValue && change )
			{
				change( true );		// call the on-change handler, true => changed back to original value
			}
		}
		else
		{
			if( previousValue == originalValue && change )
			{
				change( false );	// call the on-change handler, false => changed to a different value
			}
		}
		
		if( update )
		{
			update();
		}
		
		previousValue = object.value;
	}
	
	// Select the item with the given return value
	object.SetValue = function( value )
	{
		// value - result object value of item to select
		for( v in selectableValues )
		{
			if( selectableValues[ v ] == value )
			{
				// element v has the value we seek
				object.SetIndex( v );
				originalValue = value;
				previousValue= value;
				return;
			}
		}
		// not found - ignore
	}
	
	// Get the return value of the currently selected item
	object.GetValue = function()
	{
		return selectableValues[ object.selectedIndex ];
	}

	// Select the item with the given displayed text
	object.SetSelected = function( text )
	{
		// text - display name of item to make current		
		object.value = text;	// try changing it in case the value is not legal
		if( object.value == text )
		{
			// the value has changed, so must be legal
			if( clicking && change )
			{
				var oldClicking = clicking;
				clicking = false;
				
				// not nested and change handler defined
				if( previousValue != originalValue && value == originalValue )
				{
					// previous value reported was not the original value but we have now changed back to the original
					change( true );		// true => changed backed to original value
				}
				else if( previousValue == originalValue && value != originalValue )
				{
					// previous value reported was the original value but we have now changed to something different
					change( false );		// false => value different from original
				}
				// note what we've just reported
				previousValue = text;
				
				clicking = oldClicking
			}
		}
	}
	
	// Get the displayed text of the current item
	object.GetSelected = function()
	{
		return displayedStrings[ object.selectedIndex ];
	}
	
	// Selected the item with the given index
	object.SetIndex = function( i )
	{
		// i - index of item to make current
		if( selectableValues[ i ] !== undefined )		// test if the index is legal - ignore if not
		{
			object.selectedIndex = i;			// change the drop down
			var value = selectableValues[ i ];
			if( clicking && change )
			{
				// not nested and change handler defined
				var oldClicking = clicking;
				clicking = false;
				
				if( previousValue != originalValue && value == originalValue )
				{
					// previous value reported was not the original value but we have now changed back to the original
					change( true );		// true => changed backed to original value
				}
				else if( previousValue == originalValue && value != originalValue )
				{
					// previous value reported was the original value but we have now changed to something different
					change( false );		// false => value different from original
				}
				previousValue = value;
				
				clicking = oldClicking;
			}
		}
	}
	
	// Get the index of the currently selected item
	object.GetIndex = function()
	{
		return object.selectedIndex;
	}

	// Get the index of the item selected before the drop down was clicked
	object.GetOriginalIndex = function()
	{
		return originalIndex;
	}
	
	// Get the index of the item selected before the drop down was clicked
	object.GetOriginalValue = function()
	{
		return selectableValues[ originalIndex ];
	}
	
	// Get the text of an element
	object.GetElementText = function( index )
	{
		return displayedStrings[ index ];
	}
	
	// Get the value of an element
	object.GetElementValue = function( index )
	{
		return selectableValues[ index ];
	}
	
	// Get the number of options
	object.GetCount = function()
	{
		return displayedStrings.length;
	}
	
	// Set whether the drop down is editable
	object.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	object.JugType = "dropDown";	
}

//====== RadioButtons interface =========

// JUG factory function
function JugSetupRadioButtons( id, props )
{
	// id		string id of text input element that is to become a JUG radio button bank
	// props	string property set of the JUG radio button bank
	//			{ function action }

	var action = eval( props.modify );
	var change = eval( props.change );

	var editable = true;

	// take the default text value from the input element and treat it as a list of radio button values
	var textElem = JugElem( id );
	var legends = textElem.value.split(",");

	// Create an anchor that is to represent the radio button bank
	var a = document.createElement( 'a' );
	a.name = "JugRadioButtons";	// give anchor a name otherwise it isn't counted in document.anchors

	var currentValue = null;	// remember current value as an integer offset
	var originalValue = null;	// 
	var previousValue = null;	// remember previously reported value

	// function
	function selectButton( n, act )
	{
		var i;
		for( i=0; i < a.childNodes.length; i++ )
		{
			var child = a.childNodes[ i ];
			if( i == n )
			{
				// this is the button that is now depressed
				child.style.borderStyle = "inset";
				if( currentValue == n )
				{
					// no change
				}
				else
				{
					currentValue = n;
					if( act !== undefined )
					{
						actionTimer = setTimeout( act, 1 );	// schedule the radio button set's click handler to fire
					}
					if( currentValue == originalValue )
					{
						if( previousValue != originalValue && change )
						{
							change( true );
							previousValue = currentValue;
						}
					}
					else
					{
						if( previousValue == originalValue && change )
						{
							change( false );
							previousValue = currentValue;
						}
					}
				}
			}
			else
			{
				// this button is not depressed
				child.style.borderStyle = "outset";
			}
		}
	}
	
	// function to get current value of radio buttons, ie. index of the one that is set
	a.GetValue = function()
	{
		return currentValue;	// returns null if none set, 0 if first set, 1 if second set etc
	}
	
	// function to set current value of radio buttons
	a.SetValue = function( n )
	{
		if( previousValue != originalValue && change )
		{
			change( true );	// back to (new) original value
		}
		currentValue = n;
		previousValue = n;
		originalValue = n;
		selectButton( n, undefined );
	}
	
	a.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	// function to make onclick handlers for the radio buttons
	function makeClick( n )
	{	
		// n		integer index of the radio button
		// RETURNS	function that is the onclick handler for the radio button
		
		return function()
		{
			// variable to hold timeout object used to run button script asynchronously
			var actionTimer = null;
			
			if( !editable )
			{
				// ignore the click
			}
			else if( action == undefined )
			{
				selectButton( n, undefined );
			}
			else
			{
				// called asynchronously when button is clicked
				function runClickAction()
				{
					action( n );		// call the user defined handler for the radio button set, passing the index of the radio button that was clicked
					actionTimer = null;
				}
			
				// restyle the radio buttons to depress the selected one
				selectButton( n, runClickAction );
			}
		}
	}
	
	// for each radio button in the set, create an anchor that displays it
	var i;
	for( i=0; i < legends.length; i++ )
	{
		var b = document.createElement( 'a' );					// create an anchor for the radio button
		var text = document.createTextNode( legends[i] );		// create the text for the radio button
		b.appendChild( text );									// add the text to the radio button anchor
		b.style.borderStyle = "outset";
		b.style.padding = 0 + "px";
		b.onclick = makeClick( i );
		a.appendChild( b );										// add the radio button anchor to the set anchor
	}

	// replace the text input element with the radio button set anchor
	textElem.parentNode.replaceChild( a, textElem );
	
	a.id = id;
	a.JugType = "radioButtons";
}

//====== RadioSet interface =========

// JUG factory function
function JugSetupRadioSet( id, props )
{
	// id		string id of text input element that is to become a JUG radio set
	// props	string property set of the JUG radio button set
	//			{ function action }

	var action = eval( props.modify );
	var change = eval( props.change );

	var editable = true;

	// take the default text value from the input element and treat it as a list of radio button values
	var textElem = JugElem( id );
	var legends = textElem.value.split(",");
	
	// Create an anchor that is to represent the radio button set
	var a = document.createElement( 'a' );
	a.name = "JugRadioButtons";	// give anchor a name otherwise it isn't counted in document.anchors

	var currentValue = 0;	// remember current value as a mask
	var originalValue = 0;	// 
	var previousValue = 0;	// remember previously reported value

	// function
	function clickButton( n, act )
	{
		if( editable )
		{
			var power = Math.pow(2,n);
			var child = a.childNodes[ n ];
			if( (currentValue & power) == 0 )
			{
				// button not currently selected, so depress it
				child.style.borderStyle = "inset";
				currentValue = currentValue | power;
			}
			else
			{
				// button is currently selected, so unpress it
				child.style.borderStyle = "outset";
				currentValue = currentValue - power;
			}
	
			if( act !== undefined )
			{
				actionTimer = setTimeout( act, 1 );	// schedule the radio button set's click handler to fire
			}
	
			if( currentValue == originalValue )
			{
				if( previousValue != originalValue && change )
				{
					change( true );
					previousValue = currentValue;
				}
			}
			else
			{
				if( previousValue == originalValue && change )
				{
					change( false );
					previousValue = currentValue;
				}
			}
		}		
	}

	// function to get current value of radio buttons
	a.GetValue = function()
	{
		return currentValue;
	}

	// function to set current value of radio buttons
	a.SetValue = function( mask )
	{
		if( previousValue != originalValue && change )
		{
			change( true );	// back to (new) original value
		}
		currentValue = mask;
		previousValue = mask;
		originalValue = mask;
		var i;
		for( i=0; i < legends.length; i++ )
		{
			var power = Math.pow(2,i);
			var child = a.childNodes[ i ];
			if( (mask & power) != 0 )
			{
				child.style.borderStyle = "inset";		// depress the button
			}
			else
			{
				child.style.borderStyle = "outset";		// unpress the button
			}
		}
	}

	a.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	// function to make onclick handlers for the radio buttons
	function makeClick( n )
	{	
		// n		integer index of the radio button
		// RETURNS	function that is the onclick handler for the radio button
		
		return function()
		{
			// variable to hold timeout object used to run button script asynchronously
			var actionTimer = null;
			
			if( action == undefined )
			{
				clickButton( n, undefined );
			}
			else
			{
				// called asynchronously when button is clicked
				function runClickAction()
				{
					action( n );		// call the user defined handler for the radio button set, passing the index of the radio button that was clicked
					actionTimer = null;
				}
			
				// restyle the radio buttons to toggle the selected one
				clickButton( n, runClickAction );
			}
		}
	}
	
	// for each radio button in the set, create an anchor that displays it
	var i;
	for( i=0; i < legends.length; i++ )
	{
		var b = document.createElement( 'a' );					// create an anchor for the radio button
		var text = document.createTextNode( legends[i] );		// create the text for the radio button
		b.appendChild( text );									// add the text to the radio button anchor
		b.style.borderStyle = "outset";
		b.style.padding = 0 + "px";
		b.onclick = makeClick( i );
		a.appendChild( b );										// add the radio button anchor to the set anchor
	}

	// replace the text input element with the radio button set anchor
	textElem.parentNode.replaceChild( a, textElem );
	
	a.id = id;
	a.JugType = "radioSet";
}

//============ Drop Down / Edit box combo Selector =========

// JUG factory function for Selector
function JugCreateSelector( id, props )
{
	// id		string id of drop down selector element
	// props	string property set of the JUG drop down selector menu
	var elem = JugElem( id );

	var maxItems = props.maxItems;
	var change = eval( props.change );			// change called when change-status alters (whether value is same as original)
	var update = eval( props.update );			// update called when a new value is chosen
	var validate = props.validate;

	var fg = getStyleElement( elem, "color" );
	var bg = getStyleElement( elem, "background-color" );

	var originalValue = elem.value;	// remember the original value of the text field
	var previousValue = elem.value;	// remember the previous value reported

	var selecting = false;	// whether we are processing a mouse-down over a list item

	var editable = true;

	// create a div that is to be the moveable frame
	var container = document.createElement('div');	// div holding the entire moveable frame
	container.style.position = "absolute";
	container.style.top = 0 + "px";
	container.style.left = 0 + "px";
	container.style.background = "#FFFFFF";
	container.style.opacity = 1.0;
	container.style.filter = "alpha(opacity=100)";	// IE
	container.style.borderStyle = "outset";
	container.style.overflow = "hidden";
	container.style.overflowX = "hidden";
	var list = document.createElement('div');	// div holding the list of options
	container.appendChild( list );
	
	// remember the moveable frame as a JUG object that is not in the DOM until needed
// why?????
//	JugKeepDetached( container );

	var fullList;					// array of strings which is the list of all chooseable items
	var currentList;				// array of strings currently displayed
		
	var anchors = new Array();		// array of anchor objects that are the items in the drop down list
	var current = -1;				// -1 => none selected
	
	function highlight( i )
	{
		if( i != -1 )
		{
			if( anchors[i] )
			{
				anchors[i].style.backgroundColor = fg;
				anchors[i].style.color = bg;
			}
			else
			{
				alert( "undefined highlight " + i );
			}
		}
	}
	
	function unhighlight( i )
	{
		if( i != -1 )
		{
			if( anchors[i] )
			{
				anchors[i].style.backgroundColor = bg;
				anchors[i].style.color = fg;
			}
			else
			{
				alert( "unhighlight undefined " + i );
			}
		}
	}
	
	function createDropDown()
	{
		// remove all children from list div
		while( list.hasChildNodes() )
		{
		  list.removeChild( list.firstChild );
		}

		// factory for mouse over event handler that fires when mouse moves over a list item - it moves the highlight
		function makeMouseOver( i )
		{
			return function()
			{
				unhighlight( current );
				highlight( i );
				current = i;
			}
		}

		// factory for mouse out event handler that fires when mouse moves out of a list item - it removes the highlight
		function makeMouseOut( i )
		{
			return function()
			{
				unhighlight( current );
				current = -1;
			}
		}

		// factory for mouse up event handler that fires on mouse up over a list item - it selects the item
		function makeMouseUp( i )
		{
			return function()
			{
				unhighlight( current );
				elem.value = currentList[ current ];
				current = -1;
				createDropDown();
				showDropDown();
				selecting = false;
			}
		}

		anchors = new Array();
		
		// remove items not matching current text
		var items = new Array();
		var text = elem.value.toUpperCase();
		if( elem.value != '' )		// add no items if no text has been entered
		{
			var i;
			for( i in fullList )
			{
				var listElementText =  fullList[ i ].toUpperCase();
				if( listElementText.indexOf( text ) == 0 && listElementText != text )
				{
					// this list item starts with the current text value, so include it in the drop down
					items.push( fullList[ i ] );
				}
			}
		}
		
		// add new children to list div
		currentList = new Array();
		for( i in items )
		{
			var a = document.createElement( 'a' );
			anchors[ i ] = a;
			a.onmouseover = makeMouseOver( i );
			a.onmouseout = makeMouseOut( i );
			a.onmouseup = makeMouseUp( i );
			a.onmousedown = function()
			{
				selecting = true;
			};
			list.appendChild( a );
			var text = document.createTextNode( items[ i ] );
			currentList[ i ] = items[ i ];
			a.appendChild( text );
			var br = document.createElement( 'br' );
			list.appendChild( br );
			
			if( maxItems != undefined && currentList.length >= maxItems )
			{
				break;
			}
		}
		
		if( elem.value == originalValue )
		{
			if( previousValue != originalValue && change != null )
			{
				change( true );
				previousValue = elem.value;
			}
		}
		else
		{
			if( previousValue == originalValue && change != null )
			{
				change( false );
				previousValue = elem.value;
			}
		}

		current = -1;	// nothing selected
	}
	
	elem.SetChoices = function( newList )
	{
		fullList = newList;
		createDropDown();
	}
	
	// make the drop down list visible
	function showDropDown()
	{
		if( !editable )
		{
			return;
		}

		if( currentList.length == 0 )
		{
			// no items, so hide it
			if( container.parentNode )
			{
				container.parentNode.removeChild( container );
			}

			if( update )
			{
				update();
			}	
		}
		else
		{
			// adjust backdrop to fit the window
			var e = JugGetElementArea( elem );					// get the edit box
			container.style.left = e.left + "px";				// position the drop down list below the edit box
			container.style.top = (e.top + e.height) + "px";
	
			document.body.appendChild( container );				// display the drop down list
			
			var containerArea = JugGetElementArea( container );
			container.style.width = Math.max( containerArea.width, e.width ) + "px";		// adjust the drop down's width
																						// this seems to make the width grow over time !!!!!!!
		}
	}

	// hide the drop down list
	function hideDropDown()
	{
		if( !editable )
		{
			return;
		}

		if( container.parentNode )
		{
			container.parentNode.removeChild( container );
		}
		
		if( update )
		{
			update();
		}

		selecting = false;
	}

	var timer = null;		// timer used to run createDropDown after update has happened
	
	// event fires on key down in edit box - fields special keys, moving the highlight item or making a selection
	elem.onkeydown = function( e )
	{
		if( !editable )
		{
			return false;
		}
		
		var key;
		if( window.event && window.event.keyCode )
		{
			key = window.event.keyCode;
		}
		else if( e.keyCode )
		{
			key = e.keyCode;
		}
		else if( e.which )
		{
			key = e.which;
		}
		else
		{
			key = 0;
		}
		if( key == 40 )
		{
			// down
			if( current + 1 == anchors.length )
			{
				return false;
			}
			unhighlight( current );
			current += 1;
			highlight( current );
			return false;
		}
		else if( key == 38 )
		{
			// up
			if( current == -1 )
			{
				return false;
			}
			unhighlight( current );
			current -= 1;
			highlight( current );
			return false;
		}
		else if( key == 9 )
		{
			// tab
			if( current != -1 )
			{
				unhighlight( current );
				elem.value = currentList[ current ];
				current = -1;
				createDropDown();

				hideDropDown();
			}
			return true;		// allow tab to propagate
		}
		
		function refreshDropDown()
		{
			createDropDown();
			showDropDown();
		}

		timer = setTimeout( refreshDropDown, 1 );	// schedule the createDropDown function to run once the edit box has been updated with this character
		
		return true;
	}
	
	elem.onReturn = function()
	{
		if( current == -1 )
		{
			// nothing highlighted so not for us
			return true;
		}
		else
		{
			unhighlight( current );
			elem.value = currentList[ current ];
			current = -1;
			createDropDown();
			hideDropDown();
			return false;
		}
	}

	elem.onSetEditable = function( e )
	{
		editable = e;
		return true;		// pass the message on
	}
	
	elem.GetValue = function()
	{
		return elem.value;
	}

	elem.SetValue = function( value )
	{
		if( previousValue != originalValue && change )
		{
			change( true );	// back to (new) original value
		}
		elem.value = value;
		originalValue = value;
		previousValue = value;
		createDropDown();
	}

	elem.HasChanged = function()
	{
		return elem.value != originalValue;
	}

	fullList = new Array();
	createDropDown();

	// event fires when the user clicks into the text box - it pops up the list
	elem.onfocus = function()
	{
		showDropDown();
		elem.select();
		return false;
	}

	// event fires when user clicks away from the text box - it hides the list
	elem.onblur = function()
	{
		if( selecting )
		{
			// lost focus because the mouse is being clicked on a list item, so leave the list in place
		}
		else
		{
			hideDropDown();
		}
	}

	elem.JugType = "selector";	
}

//============ Plain Text ====================

// JUG factory function for an uneditable plain text field - appears as normal page text not an edit box
function JugCreatePlainTextField( id, props )
{
	// id		string id of input text element or textarea
	// props	string property set of the JUG text field
	var object = JugElem( id );	// span object 

	var value = "";
	
	object.SetValue = function( v )
	{
		// remove the existing children of the span
		while( object.hasChildNodes() )
		{
			object.removeChild(object.childNodes[0]);
		}
		
		// create a new text node as the child of the span
		var text = document.createTextNode( v );
		object.appendChild( text );
		
		// remember the text value
		value = v;
	}

	object.GetValue = function()
	{
		return value;
	}

	object.JugType = "plainTextField";	
}


//============ Rich Text ====================

// JUG factory function for an uneditable rich text field - appears as normal page text not an edit box
function JugCreateRichTextField( id, props )
{
	// id		string id of input text element or textarea
	// props	string property set of the JUG text field
	var object = JugElem( id );	// span object 

	var value = "";
	
	object.SetValue = function( v )
	{
		// remove the existing children of the span
		while( object.hasChildNodes() )
		{
			object.removeChild(object.childNodes[0]);
		}
		
		// create a new text node as the child of the span
		object.appendChild( JugCreateRichText( v ) );
		
		// remember the text value
		value = v;
	}

	object.GetValue = function()
	{
		return value;
	}

	object.JugType = "richTextField";	
}

//============ Element Enabler =========

// JUG factory function for Element Enabler
function JugCreateEnabler( id, props )
{
	// id		string id of element
	// props	string property set of the JUG enabler
	//			{ source, condition } - source is optional checkbox name, condition is optional function that evaluates whether element is enabled in a special way
	var elem = JugElem( id );
	
	var source = props.source;	
	var condition = eval( props.condition );
	
	function notify( value )
	{
		// The source check box has changed to <value> so re-evaluate enable condition
		if( condition )
		{
			elem.disabled = !condition( value );	// evaluate custom condition
		}
		else
		{
			elem.disabled = !value;					// evaluate default condition, which is enable if checkbox is set
		}
	}
	
	if( source )
	{
		// Hook a function onto the on-loaded message handler that registers a notifier callback with the source element
		var oldLoaded = elem.onJugLoaded;	// remember any previous callback
		elem.onJugLoaded = function()
		{
			if( oldLoaded )
			{
				oldLoaded();	// fire previous callback
			}
			// Register notify as a callback so it fires if the source checkbox is changed
			JugElem( source ).JugRegister( notify );
		}
	}
}

//============ DynamicDropDown =========
//============ Pop Up Pane ====================
//============ Field Set ====================
//============ Char Buttons ====================
//============ Pop Up Dialog ====================

//============ Overlay ====================

// JUG factory function
function JugCreateOverlay( id, props )
{
	// id		string id of div element that is to become the overlay display
	// props	string property set of the JUG overlay display
	//			{ string ovrList } - ovrlist is comma separated list of div-name
	
	// find the element that is the overlay container
	var overlayContainer = JugElem( id );

	var overlayNames = props.overlayList.split( "," );	// array of "div names" for the overlays in the display
	if( overlayNames.count == 0 )
	{
		throw new Error( "no overlays" );
	}
	
	var currentOverlay = overlayNames[0];
	
	overlayContainer.show = function( name )
	{
		if( currentOverlay == name )
		{
			// already shown, so do nothing
		}
		else
		{
			JugElem( currentOverlay ).style.display = "none";
			currentOverlay = name ;
			JugElem( currentOverlay ).style.display = "inline";
		}
	}
	
	var i;
	for( i in overlayNames )
	{
		var divName = overlayNames[ i ];
		var div = JugElem( divName );
		
		// add the overlay element's div to the overlay container
		overlayContainer.appendChild( div );

		// hide the overlay element
		div.style.display = "none";
	} // end loop over overlay names
	
	overlayContainer.JugType = "OverlaySet";
	overlayContainer.onJugLoaded = function()					// called when the page has been loaded
	{
		// All loaded, now select the first tab
		JugElem( currentOverlay ).style.display = "inline";
	};
}

//============ Date Picker ====================

var JugDays = ["--","1st","2nd","3rd","4th","5th","6th","7th","8th","9th","10th",
		       "11th","12th","13th","14th","15th","16th","17th","18th","19th","20th",
		       "21st","22nd","23rd","24th","25th","26th","27th","28th","29th","30th","31st" ];

var JugMonths = ["---","January","February","March","April","May","June","July","August","September","October","November","December"];

function JugCreateDatePicker( id, props )
{
	// id		string id of span element that is to become the date picker
	// props	string property set of the JUG overlay display
	//			{  } - 

	// find the element that is the date picker container
	var pickerContainer = JugElem( id );
	
	function makeClickFunction( func, value )
	{
		return function()
		{
			func( value );
		}
	}

	function createKeypadTable( clickFunction )
	{
		var keypadTable = document.createElement('table');
		
		function addRow( values )
		{
			var row = keypadTable.insertRow(-1);
			for( v in values )
			{
				var cell = row.insertCell(-1);
				
				var button = document.createElement('a');
				button.onclick = makeClickFunction( clickFunction, values[v] );
				button.style.borderStyle = "outset";
				cell.align = "center";

				var buttonText = document.createTextNode( v );
				button.appendChild(buttonText);
				cell.appendChild( button );
			}
		}
		
		addRow( {"1":1, "2":2, "3":3 } );
		addRow( {"4":4, "5":5, "6":6 } );
		addRow( {"7":7, "8":8, "9":9 } );
		addRow( {"<":-1, "0":0, ">":10 } );
	
		return keypadTable;
	}
	
	function createMonthTable( clickFunction )
	{
		var monthTable = document.createElement('table');
		
		function addRow( values )
		{
			var row = monthTable.insertRow(-1);
			for( v in values )
			{
				var cell = row.insertCell(-1);

				var button = document.createElement('a');
				button.onclick = makeClickFunction( clickFunction, values[v] );
				button.style.borderStyle = "outset";
				cell.align = "center";

				var buttonText = document.createTextNode( v );
				button.appendChild(buttonText);
				cell.appendChild( button );
			}
		}
		
		addRow( {"Jan":1, "Feb":2, "Mar":3 } );
		addRow( {"Apr":4, "May":5, "Jun":6 } );
		addRow( {"Jul":7, "Aug":8, "Sep":9 } );
		addRow( {"Oct":10, "Nov":11, "Dec":12 } );
	
		return monthTable;
	}
	
	var currentDate = {year:0, month:0, day:0};
	
	var selectedDay = document.createElement( 'span' );
	var selectedMonth = document.createElement( 'span' );
	var selectedYear = document.createElement( 'span' );
	selectedDay.appendChild( document.createTextNode('--') );
	selectedMonth.appendChild( document.createTextNode('---') );
	selectedYear.appendChild( document.createTextNode('----') );

	function setDay( v )
	{
		selectedDay.removeChild( selectedDay.firstChild );
		selectedDay.appendChild( document.createTextNode( JugDays[v] ) );
		currentDate.day = v;
	}

	function setMonth( v )
	{
		selectedMonth.removeChild( selectedMonth.firstChild );
		selectedMonth.appendChild( document.createTextNode( JugMonths[v] ) );
		currentDate.month = v;
	}

	function setYear( v )
	{
		selectedYear.removeChild( selectedYear.firstChild );
		if( v == 0 )
		{
			selectedYear.appendChild( document.createTextNode( "----" ) );
		}
		else
		{
			selectedYear.appendChild( document.createTextNode( v ) );
		}
		currentDate.year = v;
	}

	function clickDay( value )
	{
		if( value == -1 )
		{
			currentDate.day -= 1;
		}
		else if( value == 10 )
		{
			currentDate.day += 1;
		}
		else
		{
			currentDate.day = currentDate.day % 10 * 10 + value;
			if( currentDate.day > 31 )
			{
				currentDate.day = value;
			}
		}
		currentDate.day = Math.max( currentDate.day, 1 );
		currentDate.day = Math.min( currentDate.day, 31 );
		setDay( currentDate.day );
	}
	
	function clickMonth( value )
	{
		if( value == -1 )
		{
			currentDate.month -= 1;
		}
		else if( value == 10 )
		{
			currentDate.month += 1;
		}
		else
		{
			currentDate.month = value;
		}
		currentDate.month = Math.max( currentDate.month, 1 );
		currentDate.month = Math.min( currentDate.month, 12 );
		setMonth( currentDate.month );
	}
	
	function clickYear( value )
	{
		if( value == -1 )
		{
			currentDate.year -= 1;
		}
		else if( value == 10 )
		{
			currentDate.year += 1;
		}
		else
		{
			currentDate.year = currentDate.year % 1000 * 10 + value;
		}
		currentDate.year = Math.max( currentDate.year, 1 );
		currentDate.year = Math.min( currentDate.year, 9999 );
		setYear( currentDate.year );
	}
	
	function setYesterday()
	{
		var d = new Date();
		d.setDate( d.getDate()-1 );
		
		setDay( d.getDate() );
		setMonth( d.getMonth() + 1 );
		setYear( d.getFullYear() );
	}
	
	function setToday()
	{
		var d = new Date();
		
		setDay( d.getDate() );
		setMonth( d.getMonth() + 1 );
		setYear( d.getFullYear() );
	}
	
	function setNone()
	{
		currentDate.day = 0;
		currentDate.month = 0;
		currentDate.year = 0;
		setDay( currentDate.day );
		setMonth( currentDate.month );
		setYear( currentDate.year );
	}
	
	var pickerTable = document.createElement('table');

	var dateRow = pickerTable.insertRow(-1);
	dateRow.insertCell(-1).appendChild( selectedDay );
	dateRow.insertCell(-1).appendChild( selectedMonth );
	dateRow.insertCell(-1).appendChild( selectedYear );

	var hr1 = pickerTable.insertRow(-1).insertCell(-1);
	hr1.colSpan = "3";
	hr1.appendChild( document.createElement('hr') );
	
	var pickerRow = pickerTable.insertRow(-1);
	pickerRow.insertCell(-1).appendChild( createKeypadTable( clickDay ) );
	pickerRow.insertCell(-1).appendChild( createMonthTable( clickMonth ) );
	pickerRow.insertCell(-1).appendChild( createKeypadTable( clickYear ) );

	var hr2 = pickerTable.insertRow(-1).insertCell(-1);
	hr2.colSpan = "3";
	hr2.appendChild( document.createElement('hr') );
	
	function makeButton( row, legend, click )
	{
		var button = document.createElement( 'a' );
		button.appendChild( document.createTextNode( legend ) );
		button.onclick = click;
		button.style.borderStyle = "outset";

		var cell = row.insertCell(-1);
		cell.align = "center";
		cell.appendChild( button );
	}
	
	var buttonRow = pickerTable.insertRow(-1);
	
	makeButton( buttonRow, 'Yesterday', setYesterday );
	makeButton( buttonRow, 'Today', setToday );
	makeButton( buttonRow, 'Clear', setNone );
	
	pickerContainer.show = function( dateValue )
	{
		// Convert from date value ("YYYY-MM-DD") to date structure {day:d,month:m,year:y}
		if( dateValue == "" )
		{
			setDay( 0 );
			setMonth( 0 );
			setYear( 0 );
		}
		else
		{
			var year = parseInt( dateValue.substr(0,4) ); 
			var month = parseInt( dateValue.substr(5,2) );
			var day = parseInt( dateValue.substr( 8,2) );
			setDay( day );
			setMonth( month );
			setYear( year );
		}
		
		pickerContainer.appendChild( pickerTable );
	}

	pickerContainer.hide = function( d )
	{
		pickerContainer.removeChild( pickerTable );
		// Convert date structure to date value
		return 		    JugNumberToStringPadZero( currentDate.year, 4 ) 
				+ "-" + JugNumberToStringPadZero( currentDate.month, 2 ) 
				+ "-" + JugNumberToStringPadZero( currentDate.day, 2 );
	}
	
	pickerContainer.JugType = "datePicker";	
}

//============ Upload Form ====================

function JugCreateUploadForm( id, props )
{
	// id		string id of form element that chooses the file to upload
	// props	string property set of the JUG upload form
	//			{ string action, function upload, function complete }
	//	props.action - name of action
	//	props.submit - optional function called when upload starts, must return request object
	//	props.complete - function called when upload completes, passing result object

	var actionName = props.action;
	var submitFunction = props.submit;
	var completefunction = props.complete;
	
	// find the form
	var uploadForm = JugElem( id );
	
	// create a hidden text box that is to hold the upload's request data object
	var requestInput;
	try{
		requestInput = document.createElement( '<input name="request"/>' );
	}
	catch( ex )
	{
		// IE hack failed, so do it the DOM way
		requestInput = document.createElement('input');
		requestInput.name = "request";
	}
	requestInput.type = "hidden";
	uploadForm.appendChild( requestInput );

	// create an iFrame that is to be the target of the form's result
	var name = JugGetUniqueId( "IFRAME" );
	var iframe;
	try{
		// try the IE way of giving the iframe a name attribute
		iframe = document.createElement('<iframe name="' + name + '"/>');
		JugLog( "iframe " + name + " created using IE hack" );
	}
	catch( ex )
	{
		// IE hack failed, so do it the DOM way
		iframe = document.createElement('iframe');
		iframe.setAttribute( 'name', name );
	}

	iframe.style.display = "none";					// hide the iframe

	// function that is called when the iframe is loaded - happens initially and after an upload
	function uploadDone()
	{
		try{
			// get the text from the iframe (the iframe treats this as HTML so it is escaped)
			var scriptText;
			if( iframe.contentDocument )
			{
			    scriptText = iframe.contentDocument.documentElement.lastChild.innerHTML;
			}
			else if( iframe.contentWindow ) 
			{
				// IE
			    scriptText = iframe.contentWindow.document.lastChild.document.body.innerHTML;
			}
			else
			{
				throw new Error( "Cannot get result from iframe" );
			}
			if( scriptText == "" )
			{
				// either returned empty or the web page has just loaded for the first time
				return;
			}
			
			// unescape the returned text
			scriptText= scriptText.replace(/\&amp;/g,'&');
			scriptText= scriptText.replace(/\&lt;/g,'<');
			scriptText= scriptText.replace(/\&gt;/g,'>');
			
			// display the results on the console for debugging purposes
			JugLog( "Upload result=" + scriptText );

			// parse the resulting XML text and convert to an object	
			var responseObject;
			try{
			    var doc = JugMakeXMLDoc( scriptText );
			    responseObject = JugXMLAsObject( doc );
			}
			catch( ex )
			{
				throw new Error( "Upload result bad: " + scriptText + " (" + ex + ")" );
			}	    
			
			// check and report the outcome
			if( responseObject.outcome == "success" )
			{
				// call succeeded
				if( completefunction !== undefined )
				{
					if( responseObject.result === undefined )
					{
						throw new Error( "Upload result missing: " + scriptText );
					}
					completefunction( responseObject.result );
				}
			}
			else if( responseObject.outcome == "exception" )
			{
				// call failed
				if( responseObject.error === undefined )
				{
					throw new Error( "Upload exception error missing: " + scriptText );
				}
				throw new Error( "Error returned uploading form with " + actionName + ": " + responseObject.error );
			}
			else if( responseObject.outcome == "expired" )
			{
				// session has expired
				throw new Error( "Session expired while uploading" );
			}
			else
			{
				// unexpected response
				throw new Error( "Unexpected response uploading form with " + actionName + ": " + scriptText );
			}
		}
		catch( ex )
		{
			JugShowException( ex );
		}	    
	}

	// function that is called when the form is submitted
	function formSubmitted()
	{
		var params;
		if( submitFunction === undefined )
		{
			// application has no submit function, so use dummy meta-data
			params = [];
		}
		else
		{
			// call the application's submit function to obtain the meta-data to accompany the file
			params = submitFunction();
		}
		
		// convert the metaData object to XML text and put it into hidden form field so it is posted to server
		var request = { action:actionName, param:params };
		var requestText = JugXMLToText( JugObjectAsXML( request ) );
		requestInput.value = requestText;	
		return true;	// ok to submit form
	}

	uploadForm.onsubmit = function(){formSubmitted();};
	
	JugAddEvent( iframe, 'load', uploadDone );

	uploadForm.appendChild( iframe );
	
	// setup the form properties
	uploadForm.enctype = "multipart/form-data";
	uploadForm.method = "POST";
	uploadForm.setAttribute('target', name);

	uploadForm.JugType = "uploadForm";	
}

//============ Download button =========

// JUG factory function for a download button
function JugCreateDownloadButton( id, props )
{
	// id		string id of button element
	// props	action = function to return download request {action:xxx, param:xxxx}
	//			url = address to download

	var button = JugElem( id );
	button.setAttribute( "type", "submit" );

	// the button's action is treated as the download setup action
	var action = props.action;
	button.setAttribute( "onclick", "" );	// disable the action setup by the button

	var url = props.url;

	// now wrap the button in a form
	var form = document.createElement( "form" );
	button.parentNode.replaceChild( form, button );
	form.appendChild( button );
	form.setAttribute( "target", "_blank" );
	form.setAttribute( "method", "get" );
	form.setAttribute( "action", url );

	// add a hidden field to the form to carry the request
	var hidden = document.createElement( "input" );
	hidden.setAttribute( "type", "hidden" );
	hidden.setAttribute( "name", "download" );
	form.appendChild( hidden );

	// called when submit button is clicked to "validate" the form
	form.onsubmit = function()
	{
		// fetch the request, encode it and add it to form's hidden "download" field
		var request = action();
		var requestText = JugXMLToText( JugObjectAsXML( request ) );
		hidden.setAttribute( "value", requestText );
		return true;
	}	

	if( button.JugType )
	{
		// button is already a Jug Object so preserve its type
	}
	else
	{
		button.JugType = "downloadButton";
	}
}

//============ Table Maker ====================
//============ Mapped Text ====================
//============ Text Set ====================
//============ Button Set ====================

//======= Component Factory =========

// An array, indexed by jug object style, of jug element conversion functions. Each function takes an element id and property set as parameters
var JugFactory = { 
	"button" : JugSetupButton, 					// standard button
	"popupDialog" : JugSetupPopupDialog,		// popup dialog
	"tabset" : JugCreateTabbedDisplay,			// a tabbed display

	"menu" : JugCreateMenu,						// a pop up menu
	"menuItem" : JugCreateMenuItem,				// a simple text menu item 
	"subMenu" : JugCreateSubMenu,				// a menu item that pops up a sub-menu
	"optionalItem" : JugCreateOptionalItem,		// an optional menu item (one that may be disabled)
	"dynamicMenu" : JugCreateDynamicMenu,		// a dynamically populated menu

	"grid" : JugCreateGrid,						// grid
	"toolTip" : JugCreateToolTip,				// tool tip

	"textField" : JugCreateTextField,			// text field
	"countWords" : JugCreateCountWordsField,	// count words in text field
	"checkBox" : JugCreateCheckBox,				// check box
	"dropDown" : JugCreateDropDown,				// drop down
	"radioButtons" : JugSetupRadioButtons,		// set of radio buttons with text legends (at most one can be depressed)
	"radioSet" : JugSetupRadioSet,				// a radio button set with text legends (any number can be depressed)
	"selector" : JugCreateSelector,				// edit box / drop down combo selector
	"plainText" : JugCreatePlainTextField,		// plain text element
	"richText" : JugCreateRichTextField,		// rich text element
	"enable" : JugCreateEnabler,				// element enabler
	"overlay" : JugCreateOverlay,				// overlay
	"datePicker" : JugCreateDatePicker,			// date picker
	"uploadForm" : JugCreateUploadForm,			// upload form
	"download"   : JugCreateDownloadButton		// download button
	};

//======= Start the Jug Framework ========
function JugStart()
{
	try{
		//=== Convert buttons etc ===

		var jugObjects = [];	// formed into array of elements that need converting
		
		// function to assemble list of jug objects, with deepest first
		function findObjects( elem )
		{
			// depth first search
			var i;
			for( i=0; i < elem.childNodes.length; i++ )
			{
				findObjects( elem.childNodes[ i ] );
			}
			
			if( elem.hasAttribute && elem.hasAttribute( "jug" ) )
			{
				// Safari, Firefox
				jugObjects.push( elem );
			}
			else if( elem.jug )
			{
				// IE
				jugObjects.push( elem );	
			}
		}
		
		findObjects( document.body );
		
		// Convert each object
		var i;
		for( i=0; i < jugObjects.length; i++ )
		{
			var obj = jugObjects[ i ];
			// convert the jug attribute to a property set
			var props;
			try{
				props = eval("({" + obj.getAttribute( "jug" ) + "})" );	// add round brackets to ensure braces are not treated as block begin/end
			}
			catch( e )
			{
				throw new Error( "eval error - " + obj.getAttribute( "jug" ) );
			}
			// take the kind property and apply the appropriate jug factory for each style in the list
			var kinds = props.kind.split(" ");
			var id = obj.id;
			if( id == "" )
			{
				throw new Error( "Jug object has no id property: " + obj );
			}
			var k;
			for( k=0; k < kinds.length; k++ )
			{
				var kind = kinds[k];
				if( !JugFactory[ kind ] )
				{
					throw new Error( "no kind " + kind + ", element " + id );
				}
				JugFactory[ kind ]( id, props );
			}
		}
	
		// Send the Loaded message
		JugSendMessage( document.body, "onJugLoaded", [] );
		
		//=== set up the global event handlers so we can check key/mouse events ===
		
		// Update the mouse coords, as (result.x, result.y), from a window event
		function setMouseCoords( e )
		{
			var posx = 0;
			var posy = 0;
			if( e === undefined )
			{
				e = window.event;
			}
			if( e.type == "keydown" )
			{
				// ignore this (can have junk as coords)
			}
			else if( e.pageX )
			{
if( e.pageX > 100000 )
{
	// mystery FF bug - wild values for pageX/Y
	alert( "page " + e.pageX + " " + typeof( e.pageX ) + " clientX " + e.clientX + " tpe " + e.type );
}
				window.JugMouseCoords = { x: e.pageX, y: e.pageY };
			}
			else if ( e.clientX )
			{
				window.JugMouseCoords = { x: e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft, y: e.clientY + document.body.scrollTop + document.documentElement.scrollTop };
			}
			else
			{
				// no coord information, so don't update coords
			}
		}
	
		function keyDown( e )
		{
			setMouseCoords( e );	// update mouse coordinates
	
			// find out which key has been pressed
			var key;
			if( window.event && window.event.keyCode )
			{
				key = window.event.keyCode;
			}
			else if( e.which )
			{
				key = e.which;
			}
			else if( e.keyCode )
			{
				key = e.keyCode;
			}
			else
			{
				key = 0;
			}
		
			// determine what kind of element the click is aimed at, because we want to allow "enter" in multi-line edit boxes
			var element;
			if( e.target )
			{
				element = e.target;
			}
			else
			{
				// Explorer
				element = e.srcElement;
			}

			if( key == 13 )
			{
				// "enter" key pressed
				
				if( element.nodeName == "TEXTAREA" )
				{
					return true;	// RETURN allowed in multiline text boxes
				}
				else
				{
					// pass onReturn message to any Jug elements wanting it
					JugSendMessage( document.body, "onReturn", [] );
					
					// stop Firefox handling the key anyway
					if( e.stopPropagation )
					{
						e.stopPropagation();
						e.preventDefault();
					}			
					return false;
				}
			}
			else if( element.tagName == "BODY" )
			{
				// key aimed at BODY means no input box has focus
				JugSendMessage( document.body, "onKeydown", [ String.fromCharCode(key) ] );
				return false;
			}
			else
			{
				// key aimed at some input box
				return true;
			}
		}
		
		var dragOffset = null;	// offset from top left corner of object being dragged to focus point
		
		function mouseDown( e )
		{
			setMouseCoords( e );
			return true;
		}
		
		function mouseMove( e )
		{
			var start = window.JugMouseCoords;
			setMouseCoords( e );				
			
			if( window.JugMoveElem != null )
			{
				window.JugMoveElem.mouseMove();
			}
			
			if( window.JugDragElem == null )
			{
				return true;
			}
			else
			{
				if( dragOffset == null )
				{
					// first drag, so remember focus point
					var corner = JugGetElementArea( window.JugDragElem );
					dragOffset = { left: start.x - corner.left, top: start.y - corner.top };
				}
	
				// drag element
				var area = { left : window.JugMouseCoords.x - dragOffset.left, top : window.JugMouseCoords.y - dragOffset.top, 
							 width : window.JugDragElem.offsetWidth, height : window.JugDragElem.offsetHeight };
				area = JugConstrainToPage( area );
				window.JugDragElem.style.left = area.left + "px";
				window.JugDragElem.style.top = area.top + "px";
				return false;
			}
		}
		
		function mouseUp( e )
		{
			setMouseCoords( e );
			
			if( window.JugDragElem == null )
			{
				return true;
			}
			else
			{
				// finish dragging
				window.JugDragElem = null;
				dragOffset = null;
				return false;
			}
		}
		
		JugAddEvent( document, 'keydown', keyDown );
		JugAddEvent( document, 'mousedown', mouseDown );
		JugAddEvent( document, 'mousemove', mouseMove );
		JugAddEvent( document, 'mouseup', mouseUp );

		window.onresize = function()
		{
			JugSendMessage( document.body, "onResize", new Array() );
		}
		window.onscroll = function()
		{
			JugSendMessage( document.body, "onResize", new Array() );
		}
	}
	catch( e )
	{
		JugShowException( e );
	}
}

//============ Save and Restore field values =========

function JugSaveFields( fieldNames )
{
	var state = {};
	var i;
	for( i in fieldNames )
	{
		var name = fieldNames[ i ];
		var elem = JugElem( name );
		if( elem.JugType )
		{
			if( elem.GetValue )
			{
				state[ name ] = elem.GetValue();
			}
			else
			{
				state[ name ] = name + " is " + elem.JugType;
			}
		}
		else
		{
			if( elem.tagName == "INPUT" )
			{
				if( elem.type == "text" )
				{
					state[ name ] = elem.value;
				}
				else if( elem.type == "checkbox" )
				{
					state[ name ] = elem.checked;
				}
				else
				{
					state[ name ] = name + " is INPUT " + elem.type;
				}
			}
			else if( elem.tagName == "SELECT" )
			{
				state[ name ] = elem.value;
			}
			else
			{
				state[ name ] = name + " is " + elem.tagName;
			}
		}
	}
	return state;
}

function JugRestoreFields( state )
{
	var i;
	for( name in state )
	{
		var elem = JugElem( name );
		if( elem.JugType )
		{
			if( elem.SetValue )
			{
				elem.SetValue( state[ name ] );
			}
			else
			{
				throw new Error( "Cannot set " + name );
			}
		}
		else
		{
			if( elem.tagName == "INPUT" )
			{
				if( elem.type == "text" )
				{
					elem.value = state[ name ];
				}
				else if( elem.type == "checkbox" )
				{
					elem.checked = state[ name ];
				}
				else
				{
					throw new Error( "Cannot set " + name );
				}
			}
			else if( elem.tagName == "SELECT" )
			{
				elem.value = state[ name ];
			}
			else
			{
				throw new Error( "Cannot set " + name );
			}
		}
	}
}

function JugSetDropDownOptions( name, list )
{
	// remove existing elements from the drop down
	// list is an object mapping display name to value
	var elem = JugElem( name );
	
	var n = elem.options.length;
	var i;
	for( i=0; i < n; i++ )
	{
		elem.remove(0);
	}

	// add the new elements
	var name;
	for( name in list )
	{
		var opt = document.createElement('option');
		opt.text = name;
		opt.value = list[ name ];
		try
		{
			elem.add( opt, null );
		}
		catch(ex) 
		{
			// IE only
			elem.add( opt );
		}
	}
}

