/*

01-Sep-2006:jblack	added Message & Registration behaviors to base ViewContainer class, more documentation to come

*/



/*

	The class ViewContainer is a generic container that can act as a page group, page,
	division of a page, or individual widget.  By attaching "child" containers, we
	can create a chain of inherited behaviors.

	Except for Show and Hide, the methods as defined in this class **don't have any
	defined behavior** aside from cascading the command to children.  The idea 	is to
	briefly define subclasses with the particular behavioral requirements of that page
	or widget, and then instantiate them.  This way, individual pages/groups/widgets
	can have their own requirements on how to behave when the command is received, but
	will not have to make the decision on _when_ to execute the behavior.

	Here are the suggested behaviors for the defined methods in this class.  For each
	of these methods, the container will attempt to pass the command down to its children.

		.SetElementId(id)			associates this object with an HTML element.  Not required, but
									pages & widgets need it to benefit from cascading behaviors.
									***NOTE -- Debugging is much easier if the elementID is set in the CONSTRUCTOR
									rather than executing this statement afterward, b/c the elementID then becomes
									part of the window object name.

		.AssignChild(childObj)		tells this ViewContainer that it's responsible for cascading
									bahviors to childObj (also sets Parent property of childObj)
		.SetCurrentChild(childObj)	Used when only one of many child views are shown instead of all
									children (ex. PageGroup A has Page X as its "current" child and
									will only show X when receiving a Show() command, instead of XYZ)

		.SetPolicyKey(key)			Associates a key from the UI Policy with this object, assuming that the policy
									translates to visible/invisible.  If the association is more complex than that,
									leave this alone and .SetPolicyVisible(bool) explicitly as needed.

		.SetPolicyVisible(bool)		True by default.  Policy actions can set this to False to prevent
									showing, even when receiving a Show command.

		.Show()  -	Displays itself and all children.
		.Hide()  -	Hides itself and all children.

		.LoadData()  -  *If defined locally*, gets new data from the service manager for the
						related Element and refreshes the element
		.SaveData()  -  *If defined locally*, gets new data from the service manager for the
						related Element and refreshes the element
		.onLoad() - 	no action given here, but defined for consistency
		.onUnload() - 	no action given here, but defined for consistency

	***To provide a specific behavior for a local object, define one of the above methods
	with "_local" appended to the method name.  Do not override the above method names as
	given (this will break the command chain.)  However, if one of these methods needs to
	be caled explicitly, call the superclass' method name instead (without "_local".)

ex.
	function MyPageClass(elementId)
	{
		this.baseConstructor = ViewContainer;
		this.baseConstructor(elementId);
		delete this.baseConstructor;

		this.LoadData_local = MyPageClass_LoadData_local;
	}

	function MyPageClass_LoadData_local()
	{
		do something specific to this object
	}

	myPageObj = new MyPageClass();
	myPageObj.LoadData();    <---  use original method name to execute, without "_local"

*/

function ViewContainer(elementID)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'ViewContainer';


	//	basic container functionality
	this.CreateWindowObjectReference = ViewContainer_CreateWindowObjectReference;
	this.AssignChild = ViewContainer_AssignChild;
	this.HasParent = function() { return(null !== this.Parent); };
	this.SetCurrentChild = ViewContainer_SetCurrentChild;
	this.GetParent = function() { return this.Parent };
	this.GetIndexInParent = function() { return this.indexInParent; };
	this.GetChildList = ViewContainer_GetChildList;
	this.GetNumberOfChildren = function() { return this.ChildList.length; };
	this.IsLoadComplete = function() { return this.bLoadComplete; };

	//	messaging
	this.RegisterMessage = ViewContainer_RegisterMessage;
	this._AddRegistration = _ViewContainer_AddRegistration; // private
	this.ProcessChildRegistrations = ViewContainer_ProcessChildRegistrations;
	this.SuppressDuplicateMessages = ViewContainer_SuppressDuplicateMessages;
	this.SendMessage = ViewContainer_SendMessage;
	this.OnMessage = null;  // override with custom method for each derived class

	//	deprecated
	this.OnAppMessage = ViewContainer_OnAppMessage;
	this.AppMessage = ViewContainer_AppMessage;


	//	DOM functionality
	this.IsValid = ViewContainer_IsValid;
	this.SetElementId = ViewContainer_SetElementId;
	this.GetElementId = ViewContainer_GetElementId;
	this.GetElementObj = ViewContainer_GetElementObj;
	this.GetWrapperElementObj = ViewContainer_GetWrapperElementObj;
	this.GetPositionOnScreen = ViewContainer_GetPositionOnScreen;

	this.CacheData = ViewContainer_CacheData;
	this.SetWrapperElement = ViewContainer_SetWrapperElement;
	this.ShowHideByLayout = ViewContainer_ShowHideByLayout;
	this.SetTooltip = ViewContainer_SetTooltip;
	this.SetOnClick = ViewContainer_SetOnClick;
	this.__OnClick = __ViewContainer_OnClick;

	//  policy stuff
	this.SetPolicyVisible = ViewContainer_SetPolicyVisible;
	this.PolicyVisible = true;  //  gets set to false if a policy restricts appearance of a widget
	this.SetPolicyKey = ViewContainer_SetPolicyKey;
	this.policyKey = null;

	// cascaded behaviors
	this.Show_local = ViewContainer_Show_local;
	this.Show = ViewContainer_Show;
	this.IsShowing = function() { return this.bIsShowing; };
	this.bIsShowing = false;  // assume hidden until explicitly shown by user

	this.Hide_local = ViewContainer_Hide_local;
	this.Hide = ViewContainer_Hide;

	this.Suppress = ViewContainer_Suppress;
	this.bSuppressed = false;

	this.LoadData_local = ViewContainer_LoadData_local;
	this.LoadData = ViewContainer_LoadData;

	this.SaveData_local = ViewContainer_SaveData_local;
	this.SaveData = ViewContainer_SaveData;

	this.onLoad_local = ViewContainer_onLoad_local;
	this.onLoad = ViewContainer_onLoad;

	this.onUnload_local = ViewContainer_onUnload_local;
	this.onUnload = ViewContainer_onUnload;
	this.IsUnloading = function() { return this.bIsUnloading; };

	this.Localize_local = ViewContainer_Localize_local;
	this.Localize = ViewContainer_Localize;

	this.Enable_local = ViewContainer_Enable_local;
	this.Enable = ViewContainer_Enable;

	//	private vars/etc.

	//	basic container
	this.windowObjectName = null;
	this.ChildList = [];
	this.CurrentChild = null;
	this.Parent = null;
	this.indexInParent = -1;
	this.bLoadComplete = false;
	this.bIsUnloading = false;

	this.messageRegistry = [];

	this.oldMessageRegistry = [];  // for deprecated "AppMessage" stuff

	//	DOM
	this.elementID = null;
	this.elementObj = null;
	this.bCacheData = false;
	this.loadedFirstTime = false;
	this.bShowHideByLayout = false;
	this.wrapperElementObj = null;

	this.__onClickFunction = null;
	this.__onClickObjectContext = null;
	this.__onClickReferredObjectMethodName = null;

	//	allow setting of elementID in constructor.
	if (typeof(elementID) != 'undefined')
	{
		this.SetElementId(elementID);
		this.SetWrapperElement(elementID + '_wrapper');
	}

	this.CreateWindowObjectReference();
}


function ViewContainer_CreateWindowObjectReference()
{
	//	#####  Save a reference for this object to the window, so that any scripts that run in global scope can easily find this instance
	this.windowObjectName = null;
	if (typeof(ViewContainer_WindowNameGenerator_Increment) == 'undefined')
		ViewContainer_WindowNameGenerator_Increment = 0;  // 1st time only

	//	first, try a version of the elementID, as long as it's not already in use.
	if (this.elementID !== null)
	{
		var tmp = this.elementID + '_windowObject';
		if (typeof(window[tmp]) == 'undefined')
			this.windowObjectName = tmp;
	}

	//	next, try a version of the constructor name, as long as it's not already in use.
	if (this.windowObjectName === null)
	{
		var constString = String(this.constructor);
		constString = constString.substr(9, (constString.indexOf('(') - 9));
		var tmp = constString + '_windowObject';
		if (typeof(window[tmp]) == 'undefined')
			this.windowObjectName = tmp;
	}

	//	either it was in use, or we don't have an elementID.  Choose an autogenerated name.
	if (this.windowObjectName === null)
	{
		//  Chances are we'll find a good name in the first iteration, since we increment the global static increment var with every test.
		//	But we'll use a loop & incrementor anyway, for safety (will skip any names in use & try next number.)
		while (this.windowObjectName === null  &&  ViewContainer_WindowNameGenerator_Increment<Number.MAX_VALUE)
		{
			var tmp = 'ViewContainer_WindowObject_AutoGen_' + ViewContainer_WindowNameGenerator_Increment;
			if (typeof(window[tmp]) == 'undefined')  // not already in use
				this.windowObjectName = tmp;
			ViewContainer_WindowNameGenerator_Increment++;  // increment regardless (so next time will match at 1st iteration)
		}
	}

	if (this.windowObjectName !== null)
	{
		window[this.windowObjectName] = this;
	} else {
		//	this should never happen....maybe we have more view containers than JS' max numeric value.
		throw(new Error(0, 'ViewContainer_CreateWindowObject()\nCould not generate a window object name.'));
	}
}


//	Messaging

function ViewContainer_RegisterMessage(messageKey)
{
	//	Only called by direct usage (not used in cascading registrations)
	this._AddRegistration(messageKey, true);  // true=direct set
}

//function _ViewContainer_AddRegistration(messageKey, messagePreferenceObj, bDirectSet)  // private
function _ViewContainer_AddRegistration(messageKey, bDirectSet)  // private
{
	//	When this is set directly from the object, we flag the entry that this is the "original"
	//	listener, as opposed to a "messenger" (a registration that exists only for cascading to
	//	children.)  This is so we can skip running the local OnMessage for messengers (performance.)
	//	True is dominant -- if multiple registrations of the same type conflict, true wins.

	if (typeof(this.messageRegistry[messageKey]) == 'undefined')  // new
	{
		this.messageRegistry[messageKey] = [];
		this.messageRegistry[messageKey].bOriginalListener = bDirectSet;
		this.messageRegistry[messageKey].bSuppressDuplicates = false;
		this.messageRegistry[messageKey].lastMessage = '';
	}
	else  // exists (update)
	{
		//	if this is set directly on an object and it was already registered as a messenger, turn on the original listener flag.
		if (bDirectSet  &&  !this.messageRegistry[messageKey].bOriginalListener)
			this.messageRegistry[messageKey].bOriginalListener = true;
		//	don't touch .bSuppressDuplicates or .lastMessage
	}
	if (this.HasParent())
		this.Parent._AddRegistration(messageKey, false);  // false=not the original
	}

function ViewContainer_ProcessChildRegistrations(childObj)
{
	for (var messageKey in childObj.messageRegistry)
	{
		this._AddRegistration(messageKey, false);  // false=not the original
	}
}

function ViewContainer_SuppressDuplicateMessages(messageType, b)
{
	//	Causes suppression of OnMessage() if the entire contents of a message (IOW the original xml)
	//	is a duplicate of the last message of the same type.
	//	Does not interfere with distribution of messages.
	if (typeof(b) == 'undefined')   b = true;

	if (typeof(this.messageRegistry[messageType]) != 'undefined')
	{
		this.messageRegistry[messageType].bSuppressDuplicates = b;
	}
	//	Future improvement:  Suppress not only the execution of OnMessage, but also the
	//	distribution of the message to children (for performance.)  Have to cascade this
	//	setting up the tree, but not sure what to do if other children need it.
}

function ViewContainer_SendMessage(msg)  // msg can be any data type, even complex object
{
	var matchValue = (typeof(msg) == 'object'  &&  typeof(msg.MessageType) != 'undefined')  ?  msg.MessageType  :  msg;
	if (typeof(this.messageRegistry[matchValue]) != 'undefined')
	{
		if (null !== this.OnMessage  &&  this.messageRegistry[matchValue].bOriginalListener)
		{
			if (this.messageRegistry[matchValue].bSuppressDuplicates)
			{
				if (this.messageRegistry[matchValue].lastMessage != msg.GetXml())
				{
					this.messageRegistry[matchValue].lastMessage = msg.GetXml();
					this.OnMessage(msg);
				}
			}
			else
				this.OnMessage(msg);
		}

		for (var i=0; i<this.ChildList.length; i++)
			this.ChildList[i].SendMessage(msg);
		}
	}



//	deprecated
function ViewContainer_OnAppMessage(message, codeString)
{
	this.oldMessageRegistry.push([message, codeString]);
}

function ViewContainer_AppMessage(message)
{
	for (var i=0; i<this.oldMessageRegistry.length; i++)
	{
		if (this.oldMessageRegistry[i][0] == message)
		    eval(this.oldMessageRegistry[i][1]);   // [1] is a code string
	}

	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].AppMessage(message);
	}
}







//	DOM
function ViewContainer_IsValid()
{
	return(typeof(this.elementObj) != 'undefined'  &&  this.elementObj !== null);
}

function ViewContainer_SetElementId(idOrElement)
{
	if (fl_isValidElement(idOrElement))
	{
		this.elementObj = idOrElement;
		this.elementID = idOrElement.id;
	}
	else if (typeof(idOrElement) == 'string')
	{
		if (fl_validateString(idOrElement, FL_VALIDATE_ID))
		{
			this.elementID = idOrElement;
			this.elementObj = document.getElementById(idOrElement);
		}
		else
		{
			ASSERT(false, 'ViewContainer_SetElementId(\''+idOrElement+'\') -- This string is not valid as an HTML element ID.');
		}
	}
	//if (this.elementObj === null  &&  (typeof(this.bSuppressElementWarnings) == 'undefined'  ||  !this.bSuppressElementWarnings)  )
	//	globalLogger.Error(LOGCOMPONENT_VIEWS_WIDGETS, 'ViewContainer_SetElementId()  >  ID \''+elementID+'\' was not found in the document.');
}

function ViewContainer_GetElementId()
{
	return this.elementID;
}

function ViewContainer_GetElementObj()
{
	return this.elementObj;
}

function ViewContainer_GetWrapperElementObj()
{
	return this.wrapperElementObj;
}

function ViewContainer_SetWrapperElement(elem)  // as used in base class, already has "_wrapper" appended
{
	if (typeof(elem) == 'string')
		this.wrapperElementObj = document.getElementById(elem);
	else if (typeof(elem) == 'object'  &&  typeof(elem.nodeType) != 'undefined')
		this.wrapperElementObj = elem;
	else
		this.wrapperElementObj = null;
}

function ViewContainer_AssignChild(childObj)
{
	this.ChildList.push(childObj);
	childObj.Parent = this;
	childObj.indexInParent = this.ChildList.length-1;

	//	process registrations that may have been set on the child, before this assignment
	this.ProcessChildRegistrations(childObj);
}

function ViewContainer_SetCurrentChild(childPageObj)
{
	this.CurrentChild = childPageObj;
}

function ViewContainer_GetChildList()
{
	return this.ChildList;
}

function ViewContainer_CacheData()
{
	this.bCacheData = true;
}

function ViewContainer_SetPolicyVisible(b)
{
	this.PolicyVisible = b;
}

function ViewContainer_SetPolicyKey(key)
{
	this.policyKey = key;
}

function ViewContainer_ShowHideByLayout()
{
	this.bShowHideByLayout = true;
}

function ViewContainer_SetTooltip(tooltipText)
{
	if (this.IsValid())
		this.elementObj.title = tooltipText;
}

//function ViewContainer_SetOnClick(objectContext, referenceToMethodOfThatObject)
//function ViewContainer_SetOnClick(referenceToGlobalFunction)
function ViewContainer_SetOnClick(/* ... */)
{
	if (this.IsValid())
	{
		//	One param -- (functionRef)
		if (1 == arguments.length  &&  (typeof(arguments[0]) == 'function'  ||  typeof(arguments[0]) == 'object'))  // older JS engines might report generic "object" instead of "function"
		{
			this.__onClickObjectContext = null;
			this.__onClickFunction = arguments[0];
			eval("this.elementObj.onclick = function() { window['" + this.windowObjectName + "'].__OnClick(); };");
		}
		else if (2 == arguments.length  &&  typeof(arguments[0]) == 'object'  &&  (typeof(arguments[1]) == 'function'  ||  typeof(arguments[1]) == 'object'))
		{
			this.__onClickObjectContext = arguments[0];
			this.__onClickReferredObjectMethodName = this.windowObjectName + '_onClickMethodReference';
			this.__onClickObjectContext[this.__onClickReferredObjectMethodName] = arguments[1];
			eval("this.elementObj.onclick = function() { window['" + this.windowObjectName + "'].__OnClick(); };");
		}
		else
		{
			FL_ASSERT(false, 'Invalid param format passed to ViewContainer_SetOnClick()');
		}
	}
	else
	{
		FL_ASSERT(false, 'ViewContainer_SetOnClick() -- Element must be initialized before setting events.');
	}
}

function __ViewContainer_OnClick()
{
	//	Using a private-style method allows these classes to add internal behaviors without conflicting with
	//	behaviors that developers want to add using the "OnClick" method name.
	if (null !== this.__onClickFunction)  // global function
	{
		this.__onClickFunction();
	}
	else if (null !== this.__onClickObjectContext)  // object method
	{
		this.__onClickObjectContext[this.__onClickReferredObjectMethodName]();
	}
}

function ViewContainer_Show(bShowHide)
{
	if (typeof(bShowHide) != 'undefined'  &&  !bShowHide)  // allow boolean param here to do either show or hide
	{
		this.Hide();
		return;
	}

	if (this.bSuppressed)
		return;

	//	First see if a standard policy key has been set.  If so, test it.
	//  If not, then use the member boolean, which defaults to True.  Sometimes a routine will use a complex policy algorithm
	//  and set the member boolean itself, instead of using the standard on/off key.
	if (this.policyKey !== null)
	{
		var policyVal = controllerWindow.UIPolicy.get(this.policyKey);
		var policyShow = (policyVal == '1'  ||  policyVal == true);
	} else {
		//  no key defined; test member boolean
		var policyShow = this.PolicyVisible;
	}

	if (policyShow)
	{
		this.bIsShowing = true;
		this.Show_local();
		if (null !== this.elementObj)
		{
			if (fl_getComputedStyle(this.elementObj, 'visibility', 'visibility') == 'hidden')
				this.elementObj.style.visibility = 'inherit';
			if (fl_getComputedStyle(this.elementObj, 'display', 'display') == 'none')
				this.elementObj.style.display = '';
		}
		if (null !== this.wrapperElementObj)
		{
			if (fl_getComputedStyle(this.wrapperElementObj, 'visibility', 'visibility') == 'hidden')
				this.wrapperElementObj.style.visibility = 'inherit';
			if (fl_getComputedStyle(this.wrapperElementObj, 'display', 'display') == 'none')
				this.wrapperElementObj.style.display = '';
		}
	} else {
		this.Hide();
	}

	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].Show();
	}
}
function ViewContainer_Show_local()   {   }

function ViewContainer_Hide()
{
	this.bIsShowing = false;
	this.Hide_local();

	//	iterate FIRST ... we need to hide from the inside-out to prevent IE painting problems.
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].Hide();
	}

	if (null !== this.elementObj)
	{
		if (this.bShowHideByLayout)
			this.elementObj.style.display = 'none';
		else
			this.elementObj.style.visibility = 'hidden';
	}

	if (null !== this.wrapperElementObj)
	{
		if (this.bShowHideByLayout)
			this.wrapperElementObj.style.display = 'none';
		else
			this.wrapperElementObj.style.visibility = 'hidden';
	}

}
function ViewContainer_Hide_local()   {   }

function ViewContainer_Suppress(bSuppress)
{
	this.bSuppressed = bSuppress;
	this.Hide();
}


function ViewContainer_LoadData()
{
	//  if caching is allowed, and this has been run before, abort.
	if (this.loadedFirstTime  &&  this.bCacheData)  return;
	this.loadedFirstTime = true;
	this.LoadData_local();
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].LoadData();
	}
}
function ViewContainer_LoadData_local()   {   }

function ViewContainer_SaveData()
{
	this.SaveData_local();
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].SaveData();
	}
}
function ViewContainer_SaveData_local()   {   }

function ViewContainer_onLoad()
{
	//	Since we're creating, run parent first
	this.onLoad_local();

	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].onLoad();
	}
	this.bLoadComplete = true;
}
function ViewContainer_onLoad_local()   {   }


function ViewContainer_onUnload()
//	Effectively deletes a ViewContainer object.  This is needed since IE does not release memory for a DOM node if there are any references
//	to the node from any object.  We really don't care about simple member data, since that's garbage-collected, but member
//	vars that refer to the DOM (or other objects) should be explicitly deleted.
//	Memory should be released, assuming there are no other references to the same object.
//	Uses safe_setTimeout so we are sure the current statement is finished (waits 1 sec) before doing this.
{
	// first call on children
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].onUnload();
	}

	this.bIsUnloading = true;

	//	Since we're destroying, run parent last
	this.onUnload_local();
}
function ViewContainer_onUnload_local()   {   }


function ViewContainer_Localize(localizeType)
{
	if (!this.IsUnloading())
	{
		this.Localize_local(localizeType);
		for (var i=0; i<this.ChildList.length; i++)
		{
			if (this.ChildList[i] !== null)
				this.ChildList[i].Localize(localizeType);
		}
	}
}

function ViewContainer_Localize_local(localizeType)
{
//	if ('' !== this.tooltipText  &&  this.IsValid())
//	{
//		this.elementObj.title = LocalizeString(this.tooltipText);
//	}
}


function ViewContainer_Enable(bEnable)
{
	this.Enable_local(bEnable);
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i] !== null)
			this.ChildList[i].Enable(bEnable);
	}
}

function ViewContainer_Enable_local()   {   }

function ViewContainer_GetPositionOnScreen()
{
	var rect = [];
	rect.left = 0;
	rect.right = 0;
	rect.top = 0;
	rect.bottom = 0;
	var selfElem = this.GetElementObj();
	if (null !== selfElem)
	{
		var topmostElem = selfElem;
		while (null !== topmostElem)
		{
			rect.left += topmostElem.offsetLeft;
			rect.top += topmostElem.offsetTop;
			topmostElem = topmostElem.offsetParent;
		}
		rect.right = rect.left + selfElem.offsetWidth;
		rect.bottom = rect.top + selfElem.offsetHeight;
	}
	return rect;
}







// #####################  Page Group Class  #####################
// ###############  inherited from ViewContainer  ###############
/*
	Differs from a standard ViewContainer in that it doesn't try to show all its children at the same
	time -- instead it remembers the last child shown, and when it gets a Show command, shows only
	that child.  Good for a page "group" or wizard containing multiple views in sequence.

		.ShowPage(childObj)  -	Shows one child page, and hides the rest.
		.Show()  -		overrides cascading behavior to instead show the child page referred to
						by pageGroupObj.CurrentChild.
		.LoadData()  -	has same effect as usual, except further cascading is limited to the
						currently-selected child.
*/
function PageGroup(elementID)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'PageGroup';
	this.bSuppressElementWarnings = true;  // set before constructor to ignore set-element warnings

	this.baseConstructor = ViewContainer;
	this.baseConstructor(elementID);
	delete this.baseConstructor;

	this._base_AssignChild = this.AssignChild;
	this.AssignChild = PageGroup_AssignChild;
	this.AlwaysShowFirstChild = PageGroup_AlwaysShowFirstChild;
	this.ShowPage = PageGroup_ShowPage;
	this.Show = PageGroup_Show;  //  cascade only to currently-selected child
	this.SetCurrentChild = PageGroup_SetCurrentChild;
	this.ShowDefault = PageGroup_ShowDefault;
	this.AddPage = PageGroup_AddPage;

	this.rememberCurrentChild = true;
	this.defaultChild = null;
	this.bAlwaysShowFirstChild = false;
}

function PageGroup_AssignChild(childObj)
{
	this._base_AssignChild(childObj);
	childObj.Hide();  // All page group members are initially hidden.  The page group, at some point, must make an initial selection.
}

function PageGroup_AlwaysShowFirstChild(b)
{
    if (typeof(b) == 'undefined')   b = true;
    this.bAlwaysShowFirstChild = b;
}

//	Shows ONLY the currently-selected child (if none yet selected, try 1st child)
function PageGroup_Show()
{
	//	first do what Show usually does, to this element itself (if there is one attached)
	//	have to dup code since we're overriding usual method, and no access to super class
	if (this.bSuppressed)
		return;

	if (this.PolicyVisible)
	{
		this.bIsShowing = true;  // have to include since we overrode VC class
		this.Show_local();
		if (!this.bAlwaysShowFirstChild  &&  this.CurrentChild !== null)
			this.ShowPage(this.CurrentChild);
		else if (typeof(this.ChildList[0]) != 'undefined')
			this.ShowPage(this.ChildList[0]);

		if (this.IsValid())
			this.elementObj.style.visibility='inherit';

	} else {
		this.Hide();
	}

}


function PageGroup_ShowPage(childPageObj)
{
	if (this.bIsShowing)
	{
		var iPageToShow = -1;

		if (typeof(childPageObj) == 'undefined')
		{
			globalLogger.Error(LOGCOMPONENT_VIEWS_WIDGETS, '.ShowPage() Error > pageObj undefined');
			return;
		}
		for (var i=0; i<this.ChildList.length; i++)
		{
			if (this.ChildList[i] === childPageObj)  // identity, not comparison
			{
				iPageToShow = i;
			} else if (this.ChildList[i].IsShowing()) {
				this.ChildList[i].Hide();
			}
		}

		if(iPageToShow >= 0)
			this.ChildList[iPageToShow].Show();
	}

	if(this.rememberCurrentChild === true)
		this.SetCurrentChild(childPageObj);  // remembers
}


function PageGroup_SetCurrentChild(childPageObj)
{
	//  If this is the first time we're doing this, flag the page as the "default" so ShowDefault will use it.
	if (this.CurrentChild === null)
		this.defaultChild = childPageObj;

	this.CurrentChild = childPageObj;
}


function PageGroup_ShowDefault()
{
	if (this.defaultChild !== null)
		this.ShowPage(this.defaultChild);
	else if (this.ChildList.length > 0)
		this.ShowPage(this.ChildList[0]);
}

function PageGroup_AddPage(elementId)
{
	//	for now, only works with simple VC pages.  Anything else, do it step by step in the calling code.
	var page = new ViewContainer(elementId);
	this.AssignChild(page);
	return page;
}





// #####################  ImageContainer Class  #####################
function ImageContainer(elementId)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'ImageContainer';

	this.baseConstructor = ViewContainer;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	this.IsValid = ImageContainer_IsValid;
	this.SetSourceByURL = ImageContainer_SetSourceByURL;
	this.SetSourceBySkinName = ImageContainer_SetSourceBySkinName;

	// override useless methods to save time
	this.LoadData_local = function(){};
	this.LoadData = function(){};
	this.SaveData_local = function(){};
	this.SaveData = function(){};
	this.onLoad_local = function(){};
	this.onLoad = function(){};
	this.onUnload_local = function(){};
	this.onUnload = function(){};
}

function ImageContainer_IsValid()
{
	return(
		    typeof(this.elementObj) != 'undefined'
		    &&  this.elementObj !== null
		    &&  this.elementObj.tagName == 'IMG'
		    );
}

function ImageContainer_SetSourceByURL(url)
{
	if (this.IsValid())
		this.elementObj.src = url;
}

function ImageContainer_SetSourceBySkinName(skinName)
{
	if (this.IsValid())
		this.elementObj.src = window.external.getImageSrc(skinName);
}






// #####################  Widget Class  #####################
// ###############  inherited from ViewContainer  ###############
/*
	A general class for simple widgets (input text boxes, input textareas, and plain-text displays.)
	For Listboxes, checkboxes, & radio buttons, see below.
	This class will help automate SaveData, Show/Hide, and PolicyShow concepts.

	Usage:
		myWidget = new Widget();
		myWidget.SetElementId('id');
		someParentObject.AssignChild(myWidget);  // make it receive load/save/hide/show commands from the container chain

	Stop here if you only need the widget's visibility controlled by parent containers.
	To have the widget load and save its own data, add EITHER:

	1)	myWidget.SetProfileKey('PROFILE_KEY');
			...this makes the widget Load & Save data to & from the ProfileStore under the given key
		(most common for text boxes and plain text displays)

	or

	2)	myWidget.SourceData = function() {  return(somethingReturningASingularValue)  };
		(rare for text boxes, but comes into play in Listbox subclasses...)
			This makes the widget Load data from the returned value of the evaluated code in SourceData.
			SaveData still saves to the ProfileStore.
			Note:  SourceData should be assigned either:
				- an inline function that will pass the return value of processed code
				- a reference to a declared function (w/o the parentheses).   ex  myFormElement.SourceData = someDefinedFunction;
			Caution:  Assigning a variable name (or function call w/ parens) will result in *static data*.

	FOR NOW, widgets on NON-MODAL pages (ie, those that need to commit data immediately instead
	of waiting for an OK) will need to set their own onChange handlers in HTML to run their own
	SaveData functions.  Eventually we could set onChange handlers programatically by a
	modal/nonmodal flag on the objects.

*/
function Widget(elementId)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'Widget';

	this.baseConstructor = ViewContainer;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	//	config methods
	this.SetProfileKey = Widget_SetProfileKey;
	this.GetProfileKey = Widget_GetProfileKey;
	this.SourceData = null;
	this.SetOnChange = Widget_SetOnChange;

	//	further usage
	this.Enable = Widget_Enable;
	this.IsEnabled = function() { return this.bEnabled; };
	this.SetValue = Widget_SetValue;
	this.GetValue = Widget_GetValue;
	this.LoadProfileValue = Widget_LoadProfileValue;
	this.SaveProfileValue = Widget_SaveProfileValue;
	this.Clear = Widget_Clear;
	this.Validate = Widget_Validate;
	this.Trim = Widget_Trim;
	this.Focus = Widget_Focus;

	//	internal stuff
	this.ProfileKey = null;
	this.bEnabled = true;  // assume enabled until explicitly disabled
	this.SaveData_local = Widget_SaveData;
	this.LoadData_local = Widget_LoadData;
	this.ConfirmParentNode = Widget_ConfirmParentNode;
	this.parentNodeId = '';
	this.parentNode = null;

	this.__Init = __Widget_Init;
	this.__base_SetElementId = this.SetElementId;
	this.SetElementId = Widget_SetElementId;
	this.__OnChange = __Widget_OnChange;
	this.__onChangeFunction = null;
	this.__onChangeObjectContext = null;
	this.__onChangeReferredObjectMethodName = null;
	this.__finishFocus = __Widget_FinishFocus;

	//	If the constructor already built the elementObj, run Init now.  
	//	If not, then Init will run when the user calls SetElementId().
	if (this.IsValid())
	{
		this.__Init();
	}
}

function Widget_SetElementId(id)
{
	this.__base_SetElementId(id);
	this.__Init();
}

function __Widget_Init()
{
}

function Widget_SetProfileKey(sKey)
{
	this.ProfileKey = sKey;
}
function Widget_GetProfileKey()
{
	return this.ProfileKey;
}

function Widget_LoadData()
{
	if (this.IsValid())
	{
		if (this.ProfileKey !== null  &&  this.ProfileKey !== '')
		{
			this.LoadProfileValue();
		}
		else if (this.SourceData !== null)
		{
			var sValue = this.SourceData();
			this.SetValue(sValue);
		}
	}
}
function Widget_SaveData()
{
	if (this.IsValid())
	{
		this.SaveProfileValue();
	}
}
function Widget_Clear()
{
	this.SetValue('');
}


// Enable method - true enables HTML control, false disables.  missing param defaults to true
function Widget_Enable(bEnable)
{
	if (typeof(bEnable) == 'undefined')
		bEnable = true;
	else
		bEnable = safeBool(bEnable);  // convert from string if needed ("true|false" or "1|0", etc.)

	this.bEnabled = bEnable;

	if(bEnable)
		EnableControl(this.elementObj);
	else
		DisableControl(this.elementObj);
}

function Widget_GetValue()
{
	return getValueOfFormElement(this.elementObj);
}

function Widget_SetValue(value)
{
	setValueOfFormElement(this.elementObj, value);
}

function Widget_LoadProfileValue()
// sets the value of the widget to the one in the ProfileStore, assuming a ProfileKey is defined
{
	if (this.ProfileKey !== null  &&  this.ProfileKey !== '')
	{
		this.SetValue(ProfileStore.safeget(this.ProfileKey));
	}
}

function Widget_SaveProfileValue(optionalValue)
// takes the current value in the widget and saves it to the ProfileStore, assuming a ProfileKey is defined
// or, if you pass in a string, uses that instead of the widget's current value.
{
	if (this.ProfileKey !== null  &&  this.ProfileKey !== '')
	{
		if (typeof(optionalValue) == 'undefined')
			optionalValue = this.GetValue();
		ProfileStore.put(this.ProfileKey, optionalValue);
	}
}

function Widget_Validate()
{
	if (null === this.elementObj)
		return true;

	//  assume valid if not showing
	if (fl_getComputedStyle(this.elementObj, 'display', 'display') == 'none'  ||  fl_getComputedStyle(this.elementObj, 'visibility', 'visibility') == 'hidden')
		return true;
	if (null !== this.wrapperElementObj)
	{
		if (fl_getComputedStyle(this.wrapperElementObj, 'display', 'display') == 'none'  ||  fl_getComputedStyle(this.wrapperElementObj, 'visibility', 'visibility') == 'hidden')
			return true;
	}

	//  We only care about text & select elements.. anything else assume valid
	if (this.elementObj.tagName != 'SELECT'  &&  !isTextElement(this.elementObj))
		return true;

	//  ready to validate
	var bValid = true;
	var val = this.GetValue();
	if (typeof(val) == 'string')
		bValid = fl_validateString(val, FL_VALIDATE_NONBLANK);

	if (bValid)
		fl_removeClassFromElement(this.elementObj, 'HighlightField');
	else
		fl_addClassToElement(this.elementObj, 'HighlightField');

	return bValid;
}

function Widget_Trim()
{
	if (isTextElement(this.elementObj))
	{
		this.SetValue(Trim(this.GetValue()));
	}
}

function Widget_ConfirmParentNode(parentNode)
{
	//	Does the work of initializing references to the parentNode, making sure it is valid along the way, and warning when it's not.
	//	Useful in Rendering methods of containers based on Widget.
	this.parentNodeId === '';
	if (typeof(parentNode) == 'string') // allow string ID too
	{
		this.parentNodeId = parentNode;
		this.parentNode = document.getElementById(this.parentNodeId);
	}
	else if (null !== parentNode  &&  typeof(parentNode.id) != 'undefined')
	{
		this.parentNodeId = parentNode.id;
		this.parentNode = parentNode;
	}

	if (null === parentNode)
	{
		FL_ASSERT(false, 'Cannot find the parentNode.\n\nthis container\'s ID = ['+this.elementID+']\nparentNode ID = ['+this.parentNodeId+']');
	}
}

//function Widget_SetOnChange(objectContext, referenceToMethodOfThatObject)
//function Widget_SetOnChange(referenceToGlobalFunction)
function Widget_SetOnChange(/* ... */)
{
	if (this.IsValid())
	{
		//	One param -- (functionRef)
		if (1 == arguments.length  &&  (typeof(arguments[0]) == 'function'  ||  typeof(arguments[0]) == 'object'))  // older JS engines might report generic "object" instead of "function"
		{
			this.__onChangeObjectContext = null;
			this.__onChangeFunction = arguments[0];
			eval("this.elementObj.onchange = function() { window['" + this.windowObjectName + "'].__OnChange(); };");
		}
		else if (2 == arguments.length  &&  typeof(arguments[0]) == 'object'  &&  (typeof(arguments[1]) == 'function'  ||  typeof(arguments[1]) == 'object'))
		{
			this.__onChangeObjectContext = arguments[0];
			this.__onChangeReferredObjectMethodName = this.windowObjectName + '_onChangeMethodReference';
			this.__onChangeObjectContext[this.__onChangeReferredObjectMethodName] = arguments[1];
			eval("this.elementObj.onchange = function() { window['" + this.windowObjectName + "'].__OnChange(); };");
		}
		else
		{
			FL_ASSERT(false, 'Invalid param format passed to ViewContainer_SetOnChange()');
		}
	}
	else
	{
		FL_ASSERT(false, 'ViewContainer_SetOnChange() -- Element must be initialized before setting events.');
	}
}

function __Widget_OnChange()
{
	//	Using a private-style method allows these classes to add internal behaviors without conflicting with
	//	behaviors that developers want to add using the "OnChange" method name.
	if (null !== this.__onChangeFunction)  // global function
	{
		this.__onChangeFunction();
	}
	else if (null !== this.__onChangeObjectContext)  // object method
	{
		this.__onChangeObjectContext[this.__onChangeReferredObjectMethodName]();
	}
}

function Widget_Focus()
{
	safe_setTimeout('window[\''+this.windowObjectName+'\'].__finishFocus();', 1, 'Widget-Focus-'+this.GetElementId());
}

function __Widget_FinishFocus()
{
	if (this.IsValid())
	{
		this.elementObj.focus();
		if (isTextElement(this.elementObj))
			this.elementObj.select();
	}
}






// #####################  TextContainer Class  #####################
/*
	myText = new TextContainer('htmlElementId', 'skinFileStringId');
	... will automatically load its text during LoadData events, assuming this is a child of some other container in the chain
*/
function TextContainer(elementId, stringId)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'TextContainer';

	this.baseConstructor = Widget;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	this._base_IsValid = this.IsValid;
	this.IsValid = TextContainer_IsValid;
	this.SetStringId = TextContainer_SetStringId;
	this.GetStringId = function() { return this.m_stringId; };
	this.SetTextElement = TextContainer_SetTextElement;
	this.AllowHtml = TextContainer_AllowHtml;
	this.SetValue = TextContainer_SetValue;
	this.GetValue = TextContainer_GetValue;
	this.TruncateValue = TextContainer_TruncateValue;
	this.SetTruncate = function(b){this.bTruncate = b};
	this.Base_LoadData_local = this.LoadData_local;
	this.LoadData_local = TextContainer_LoadData;
	this.Localize_local = function() { this.LoadData_local(); };
	this.OverrideText = TextContainer_OverrideText;

	//	Allow each derived class (or even each instance) to specify a custom "text element" that the
	//	TextContainer code will use for all of its text operations.  By default this is the usual "elementObj".
	//	This is useful for derived classes that have complex HTML and need the top-level element to be used
	//	for base container operations, but a (different) inner element for holding the text that changes.
	this.textElementId = this.GetElementId();
	this.textElementObj = this.GetElementObj();
	this.bUseSeparateTextElement = false;
	this.bTruncate = false;
	this.bAllowHtml = false;
	this.m_stringId = null;
	this.textOverride = '';

	if (typeof(stringId) != 'undefined')
		this.SetStringId(stringId);
}

function TextContainer_IsValid()
{
	//	If we are using a separate text element, then that also has to be valid.
	return(  this._base_IsValid()  &&  (!this.bUseSeparateTextElement  ||  null !== this.textElementObj)  );
}

function TextContainer_SetStringId(id)
{
	this.m_stringId = id;
}

function TextContainer_SetTextElement(elementObjectOrId)
{
	if (typeof(elementObjectOrId) == 'string')
	{
		this.textElementId = elementObjectOrId;
		this.textElementObj = document.getElementById(elementObjectOrId);
	}
	else if (typeof(elementObjectOrId) == 'object'  &&  typeof(elementObjectOrId.nodeName) != 'undefined')
	{
		this.textElementObj = elementObjectOrId;
		this.textElementId = elementObjectOrId.id;
	}
	else
	{
		FL_ASSERT(false, 'TextContainer_SetTextElement(...) -- can\'t interpret argument; expected element ID or DOM element object');
	}
	this.bUseSeparateTextElement = true;
}

function TextContainer_AllowHtml(b)
{
	if (typeof(b) == 'undefined')   b = true;
	this.bAllowHtml = b;
}

function TextContainer_LoadData()
{
	if ('' != this.textOverride)
	{
		this.SetValue(this.textOverride);
	}
	else if (null !== this.m_stringId)  // initialized
	{
		if ('' != this.m_stringId)
		{
			//	this can be used simply w/o the localization framework, if the string ID passed in was the desired text.
			var strTranslation = (typeof(LocalizeString) != 'undefined')  ?  LocalizeString(this.m_stringId)  :  this.m_stringId;
			if ('' !== strTranslation)
				this.SetValue(strTranslation);
		}
		else
		{
			this.SetValue('');
		}
	}
	else
	{
		this.Base_LoadData_local();
	}
}

function TextContainer_SetValue(value)
{
	if (this.IsValid())
	{
		if (null === this.textElementObj)
			this.textElementObj = this.GetElementObj();

		if(this.bTruncate)
		{
			this.TruncateValue(value);
		}
		else if (!this.bAllowHtml)
		{
			value = fl_escapeXml(value);
		}
		//	the skinning engine isn't consistent with translating \n's intended as line-breaks ... for now, do it here
		value = value.replace(/\n/g, '<br />');
		value = fl_safeFancyChars(value);
		this.textElementObj.innerHTML = value;  // renders html in content
	}
}

function TextContainer_GetValue()
{
	if (this.IsValid())
	{
		if (null === this.textElementObj)
			this.textElementObj = this.GetElementObj();
		return fl_getNodeText(this.textElementObj);
	}
	else
	{
		return '';
	}
}


// This method will attempt to truncate text that overflows the field length.
// The chars '...' will be appended to indicate the visible text is incomplete.
// A tooltip will be set to provide the complete string.  Currently this will
// only work properly for fields with a fixed style width in pixels and no padding
function TextContainer_TruncateValue(alreadyEscapedValue)
{
	if (!this.IsValid())
		return;

	if (null === this.textElementObj)
		this.textElementObj = this.GetElementObj();

	// clear tooltip
	this.SetTooltip('');

	// write full string to the field
	this.textElementObj.innerHTML = alreadyEscapedValue;

	// get available width from style
	var styleWidth = fl_getComputedStyle(this.textElementObj, 'width', 'width')

	// value should end in 'px', abort if it does not
	var index = styleWidth.lastIndexOf('px');
	if(index == -1)
		return;

	// drop the px and get int value
	var iMaxDesiredWidth = parseInt(styleWidth.substring(0, index));

	// abort if invalid pixel size
	if (isNaN(iMaxDesiredWidth)  ||  iMaxDesiredWidth <= 0)
		return;


	//	Since this relies heavily on comparing "rendered width" to "declared width", 
	//	we have to take into account the extra space created by borders, margins, & padding.
	var extraWidthPx = fl_calculateCssOffsetWidthDifference(this.textElementObj);
	iMaxDesiredWidth += extraWidthPx;  //!!! should this be "-=" instead?

	if(this.textElementObj.scrollWidth <= iMaxDesiredWidth)
	{
		// exit if the value fits within the available length
		// no tooltip will be set since the complete text is visible
		return;
	}

	var bDone = false;
	var truncText = alreadyEscapedValue;
	index = truncText.lastIndexOf(' '); // last space in string

	// if there are spaces in the string, drop words off the end
	// until the string fits in the field
	while (index != -1 && !bDone)
	{
		truncText = truncText.substring(0, index);

		finalText = truncText + '...';
		fl_writeTextToElement(this.textElementObj, finalText);

		if (this.textElementObj.scrollWidth <= iMaxDesiredWidth)
			bDone = true;
		else
			index = truncText.lastIndexOf(' ');
	}

	while(!bDone)
	{
		// truncText has no spaces and does not fit in the field,
		// start dropping letters (2 at a time) until it fits
		truncText = truncText.substring(0, truncText.length-2);

		if(truncText.length == 0)
		{
			// this field can't be evaluated properly, just write the full text
			this.textElementObj.innerHTML = alreadyEscapedValue;
			return;
		}

		finalText = truncText + '...';
		fl_writeTextToElement(this.textElementObj, finalText);

		if (this.textElementObj.scrollWidth <= iMaxDesiredWidth)
			bDone = true;
	}

	// apply full string to tooltip
	this.SetTooltip(fl_unescapeXml(alreadyEscapedValue));
}

function TextContainer_OverrideText(text)
{
	//	Overrides the element's text.  Future automatic Loads or Localizes will have no effect.
	//	However this can be un-overridden by calling obj.OverrideText('');  (empty string)
	this.textOverride = text;
	this.SetValue(text);  // if not yet valid, will get done next Load
}








// #####################  InputContainer Class  #####################
/*
	myInput = new InputContainer('htmlElementId', 'skinFileStringIdForTheLabel');
	... will automatically load its text during LoadData events, assuming this is a child of some other container in the chain
*/
function InputContainer(elementId, labelStringId/* optional */)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'TextContainer';

	this.baseConstructor = Widget;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	this.SetInputType = InputContainer_SetInputType;
	this.GetValue = InputContainer_GetValue;  // override these b/c they're much simpler here
	this.SetValue = InputContainer_SetValue;
	this.Localize = InputContainer_Localize;
	this.Render = InputContainer_Render;

	this.inputType = 'text';  // default, could also be 'password'
	this.labelStringId = (typeof(labelStringId) != 'undefined')  ?  labelStringId  :  null;
	this.labelElementId = elementId + '_label';
	this.labelElementObj = document.getElementById(labelStringId);
}

function InputContainer_SetInputType(type)
{
	this.inputType = type.toLowerCase();
}

function InputContainer_GetValue()
{
	if (this.IsValid())
	{
		return this.elementObj.value;
	}
	else
	{
		return '';
	}
}

function InputContainer_SetValue(value)
{
	if (this.IsValid())
	{
		this.elementObj.value = value;
	}
}

function InputContainer_Render(parentNode)
{
	//	06-Dec-2006:jblack -- Right now the only easy (and consistent) way to do this is to
	//	require a reference to the parentNode in the HTML document as a parameter.  We can't
	//	assume that the container's Parent object is already "initialized enough" that it has
	//	a valid element, because we don't know (and shouldn't care) what order the Containers
	//	were assembed together.  Open to suggestions.....

	this.ConfirmParentNode(parentNode);  // sets up this.parentNode & this.parentNodeId
	if (null !== this.parentNode)
	{
		var tbody = null;
		//	First try to append to an existing InputLayout.
		if (this.parentNode.childNodes.length > 0  &&
			this.parentNode.lastChild.tagName == 'TABLE'  &&
			this.parentNode.lastChild.className == 'InputLayout')
		{
			tbody = this.parentNode.lastChild.getElementsByTagName('TBODY')[0];  // pretty sure that tbody exists if table does
		}
		else
		{
			var tableElem = document.createElement('TABLE');
			//	The Table element is the Wrapper, in case we need to work with that for showing/hiding.
			var tableElemId = this.GetElementId() + '_wrapper';
			tableElem.id = tableElemId;
			tableElem.className = 'InputLayout';
			this.parentNode.appendChild(tableElem);

			var tbody = document.createElement('TBODY');
			tableElem.appendChild(tbody);
		}

		var tr = document.createElement('TR');
		tr.className = 'InputLayoutRow';
		tbody.appendChild(tr);

		this.labelElementObj = document.createElement('TD');
		this.labelElementObj.id = this.labelElementId;
		this.labelElementObj.className = 'InputLayoutCell InputLayoutCell_Label';
		tr.appendChild(this.labelElementObj);

		var elementCell = document.createElement('TD');
		elementCell.className = 'InputLayoutCell InputLayoutCell_Input';
		tr.appendChild(elementCell);

		this.elementObj = document.createElement('INPUT');
		this.elementObj.type = this.inputType;
		this.elementObj.id = this.GetElementId();
		this.elementObj.className = 'TEXTBOX';
		elementCell.appendChild(this.elementObj);

		elementCell = null;
		tr = null;
		tbody = null;
		tableElem = null;

		this.Localize();
	}
}

function InputContainer_Localize(localizeType)
{
	if (null !== this.labelStringId)
	{
		var labelText = LocalizeString(this.labelStringId);
		if ('' !== labelText)
			fl_writeTextToElement(this.labelElementObj, labelText);
	}
	this.Localize_local(localizeType);
}







// #####################  Listbox Class  #####################
// ###############  inherited from Widget  ###############
/*
		myFormElement.SetSourceDataType('type');   ['xml'|'complex
*/
function Listbox(elementId, labelStringId/* optional */)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'Listbox';

	this.baseConstructor = Widget;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	//  ### Automatic data generation (optional; can also easily build lists by hand w/ other methods below)
	this.bindXML = Listbox_bindXML;
	this.bindXMLArray = Listbox_bindXMLArray;
	this.bind2dimArray = Listbox_bind2dimArray;
	this.bindSimpleArray = Listbox_bindSimpleArray;
	this.SetXMLTagName = function(s)  { this.XMLTagName = s; };
	this.SetDataTextKey = function(s)  { this.SourceDataTextKey = s; };
	this.SetDataValueKey = function(s)  { this.SourceDataValueKey = s; };
	this.SetIncludeBlank = function(b)  { this.includeBlank = b; };
	this.SetDefaultValue = function(b)  { this.defaultValue = b; };
	this.Render = Listbox_Render;
	this.Localize = Listbox_Localize;

	//	### Inspecting State
	this.IsValid = Listbox_IsValid;  //  object is bound to a valid HTML Select element, with a valid Options collection
	this.GetCount = Listbox_GetCount;  //  returns count of options in list, including any blanks
	this.HasSelection = Listbox_HasSelection;  //   true/false - any item in list is selected (selectedIndex is valid)

	//	### Modifying List
	this.AddItem = Listbox_AddItem;  // (text, value, id)    id is optional (if no value, text is used for both text & value)
	this.AddItems = Listbox_AddItems;  // supply:  - a simple ordered array of strings, which will become text and value for each option
		                               //          - a 2-dim array where each row is a simple ordered array of [text, value, id]  (id is optional)
		                               //          - a 2-dim array where each row is an associative array containing "text", "value", and "id" members
		                               //            note: this last format is what is returned for GetCurrentList
	this.RemoveItemByIndex = Listbox_RemoveItemByIndex;  // (index)
	this.RemoveItemByValue = Listbox_RemoveItemByValue;  // (value)  Removes only the first one it finds.  Not a good idea to have duplicates anyway.
	this.Clear = Listbox_Clear;  // Clears the entire list, not the selection state (see "ClearSelection" for that)
	this.Sort = Listbox_Sort;  // Sorts the options alphabetically (by their text), retaining the current selection by value.

	//	### Retrieving items or their properites
	//  ### A "List Item" is a simple object with .text, .value, & .id properties.
	//  Currently-selected item
	this.GetValue = Listbox_GetValue;  //  returns value (string) of currently-selected item, or '' if no selection.
	this.GetText = Listbox_GetText;  //  returns display text (string) of currently-selected item, or '' if no selection.
	this.GetSelectedIndex = Listbox_GetSelectedIndex;  // returns index of currently-selected List Item.
	this.GetSelectedItem = Listbox_GetSelectedItem;   //  returns List Item
	//  Other items
	this.GetItem = Listbox_GetItem;   //  (index)  returns List Item at "index"

	this.GetIndexOfItem = Listbox_GetIndexOfItem;  // (value)  returns index of List Item that matches "value"
	this.GetIndexOfItemByText = Listbox_GetIndexOfItemByText;  // returns index of List Item that matches "text"

	this.GetValueOfItem = Listbox_GetValueOfItem;  // (index)  returns value of List Item at "index"
	this.GetTextOfItem = Listbox_GetTextOfItem;  // (index)  returns text of List Item at "index"

	this.GetCurrentList = Listbox_GetCurrentList;   //  returns ordered collection of List Items -- format matches AddItems( [...] ) for easy rebuilding
	this.GetPriorValue = function() { return this.priorValue };  // every Clear saves the current value internally

	//	### Modifying Selection
	//	All "Select" methods return true/false based on success of the selection.
	this.SelectItemByIndex	= Listbox_SelectItemByIndex;  // (index)
	this.SelectItemByValue	= Listbox_SelectItemByValue;  // (value)
	this.SetValue			= Listbox_SelectItemByValue;   // same as SelectItemByValue
	this.SelectItemByText	= Listbox_SelectItemByText;  // (value)

	this.ClearSelection		= Listbox_ClearSelection;
	this.SelectFirstNonblank = Listbox_SelectFirstNonblank;  // selects the first non-blank item in list
	this.SelectPriorValue	= Listbox_SelectPriorValue;  // tries to select the value that was selected before the previous Clear
	this.SelectProfileValue	= Listbox_SelectProfileValue;  // tries to select the value matching the ProfileStore, assuming a ProfileKey is defined
	this.SelectDefaultValue	= Listbox_SelectDefaultValue;  // tries to select the value previously set by SetDefaultValue()

	this.LoadData_local = Listbox_LoadData;
	

	//	Private
	this.__baseWidget_OnChange = this.__OnChange;
	this.__OnChange = __Listbox_OnChange;
	this.__CheckForTextOverrun = __Listbox_CheckForTextOverrun;

	this.includeBlank = true;
	this.defaultValue = null;
	this.SourceData = null;  // assign inline function or reference to function (must be "live" return)
	this.SourceDataTextKey = null;
	this.SourceDataValueKey = null;
	this.SourceDataMethod = null;
	this.XMLTagName = null;
	this.priorValue = null;

	this.labelStringId = (typeof(labelStringId) != 'undefined')  ?  labelStringId  :  null;
	this.labelElementId = elementId + '_label';
	this.labelElementObj = document.getElementById(this.labelElementId);
}


function Listbox_bindXML(functionReference)
{
	this.SourceData = functionReference;
	this.SourceDataMethod = 'XML';
}
function Listbox_bindXMLArray(functionReference)
{
	this.SourceData = functionReference;
	this.SourceDataMethod = 'XMLArray';
}
function Listbox_bind2dimArray(functionReference)
{
	this.SourceData = functionReference;
	this.SourceDataMethod = '2dimArray';
}
function Listbox_bindSimpleArray(functionReference)
{
	this.SourceData = functionReference;
	this.SourceDataMethod = 'SimpleArray';
}



//	### Usage
function Listbox_IsValid()
{
	return(
		    typeof(this.elementObj) != 'undefined'
		    &&  this.elementObj !== null
		    &&  typeof(this.elementObj.options) != 'undefined'
		    );
}

function Listbox_GetCount()
{
	if (this.IsValid())
		return(this.elementObj.options.length);
	else
		return(0);
}

function Listbox_GetItem(ind)
{
	//	returns a simple associative array with the properties text, value, & id.
	if (this.IsValid()  &&  ind >= 0  &&  ind < this.elementObj.options.length)
	{
		var op = [];
		op.text = this.elementObj.options[ind].text;
		op.value = this.elementObj.options[ind].value;
		if (this.elementObj.options[ind].id !== '')
			op.id = this.elementObj.options[ind].id;
		else
			op.id = '';
		return(op);
	} else {
		return(null);
	}
}

function Listbox_GetValueOfItem(ind)
{
	//	returns the value of the item matching the index "ind".  Returns null if it can't find it.
	if (this.IsValid()  &&  ind >= 0  &&  ind < this.elementObj.options.length)
	{
		return(this.elementObj.options[ind].value);
	} else {
		return(null);
	}
}

function Listbox_GetTextOfItem(ind)
{
	//	returns the value of the item matching the index "ind".  Returns null if it can't find it.
	if (this.IsValid()  &&  ind >= 0  &&  ind < this.elementObj.options.length)
	{
		return(this.elementObj.options[ind].text);
	} else {
		return(null);
	}
}

function Listbox_HasSelection()
{
	return (this.IsValid()  &&  this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length);
}

function Listbox_GetSelectedIndex()
{
	if (this.IsValid())
	{
		return(this.elementObj.selectedIndex);
	} else {
		return(-1);
	}
}

function Listbox_GetSelectedItem()
{
	//	returns a simple associative array with the properties text, value, & id.
	if (this.IsValid()  &&  this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length)
	{
		return(this.GetItem(this.elementObj.selectedIndex));
	} else {
		return(null);
	}
}

function Listbox_GetCurrentList()
{
	//	returns a 2-dim array where each row has the properties text, value, & id.
	var tmpList = [];
	if (this.IsValid())
	{
		for (var i=0; i<this.GetCount(); i++)
		{
			tmpList.push(this.GetItem(i));
		}
	}
	return(tmpList);
}

function Listbox_GetIndexOfItem(value)
//	returns -1 if not in list or list invalid
{
	var ind = -1;
	if (this.IsValid())
	{
		var tmp = this.GetCurrentList();  // will return empty array if listbox invalid
		for (var i=0; i<tmp.length; i++)
		{
			if (tmp[i].value == value)
			{
				ind = i;
				break;
			}
		}
	}  // end is valid
	return(ind);
}

function Listbox_GetIndexOfItemByText(text)
//	returns -1 if not in list or list invalid
{
	var ind = -1;
	if (this.IsValid())
	{
		var tmp = this.GetCurrentList();  // will return empty array if listbox invalid
		for (var i=0; i<tmp.length; i++)
		{
			if (tmp[i].text == text)
			{
				ind = i;
				break;
			}
		}
	}  // end is valid
	return(ind);
}

function Listbox_SelectItemByIndex(ind)
{
	if (this.IsValid()  &&  ind >= 0  &&  ind < this.elementObj.options.length)
	{
		this.elementObj.selectedIndex = ind;
		this.__CheckForTextOverrun();
		return true;
	}
	return false;
}

function Listbox_SelectItemByValue(value)
{
	if (this.IsValid())
	{
		var ind = this.GetIndexOfItem(value);
		if (ind >= 0)
		{
			this.elementObj.selectedIndex = ind;
			return true;
		}
	}
	return false;
}

function Listbox_SelectItemByText(text)
{
	if (this.IsValid())
	{
		var ind = this.GetIndexOfItemByText(text);
		if (ind >= 0)
		{
			this.elementObj.selectedIndex = ind;
			return true;
		}
	}
	return false;
}

function Listbox_AddItem(text, value, optionID)
{
	if (typeof(text) == 'undefined')
		text = '';
	if (typeof(value) == 'undefined')
		value = text;
	//	text / value = the displayed text and the Value attrib of the option
	//	optionID (optional) = will set the ID attrib of the individual option
	if (this.IsValid())
	{
		this.elementObj.options[this.elementObj.options.length] = new Option(text, value);
/*
		//	Each option (actually, the DOM text node that corresponds to the option) needs to be in a unicode charset regardless of
		//	the contents this time around.  The only way to preserve this is for the strings to contain high-order unicode when the
		//	option is first created with "new".  So we first create the option with bogus unicode text then immediately replace the
		//	text with what we really want.
		//	However -- Simply assigning to the option's text property has the effect of replacing the DOM text node, which destroys
		//	its notion of charset.  Instead, we have to get there via the parent SELECT object's child dom nodes, then into the correct
		//	node's text node, which is a child.
		//	\ufeff  is unicode (double-byte) NULL
		this.elementObj.options[this.elementObj.options.length] = new Option('\u329C', '\u329C');  // 329C is a Chinese glyph
		this.elementObj.childNodes[this.elementObj.options.length-1].firstChild.nodeValue = text;
		this.elementObj.options[this.elementObj.options.length-1].value = value;
*/

		if (typeof(optionID) != 'undefined')
		{
			if (optionID === null)  optionID = '';
			this.elementObj.options[this.elementObj.options.length-1].id = optionID;
		}
	}
	else
	{
		FL_ASSERT(false, 'Listbox_AddItem() called on an uninitialized Listbox container.');
	}
}

function Listbox_AddItems(optionArray)
{
	if (optionArray.length > 0)
	{
		if (typeof(optionArray[0]) == 'string')  // one-dimensional, use same for text/value
		{
			for (var i=0; i<optionArray.length; i++)
				this.AddItem(optionArray[i], optionArray[i]);

		}
		else if (typeof(optionArray[0]) == 'object')  // two-dimensional
		{
			if (typeof(optionArray[0].text) != 'undefined'  &&  typeof(optionArray[0].value) != 'undefined')  // 2nd dim is associative
			{
				for (var i=0; i<optionArray.length; i++)
				{
					if (typeof(optionArray[i].id) != 'undefined')
						this.AddItem(optionArray[i].text, optionArray[i].value, optionArray[i].id);
					else
						this.AddItem(optionArray[i].text, optionArray[i].value);
				}

			}
			else if (optionArray[0].length > 1)  // 2nd dim is enumerated
			{
				for (var i=0; i<optionArray.length; i++)
				{
					var id = (optionArray[i].length > 2)  ?  optionArray[i][2]  :  '';
					this.AddItem(optionArray[i][0], optionArray[i][1], id);
				}
			}

		}  // end 1-dim or 2-dim

	}  // optionArray has length
}

function Listbox_RemoveItemByIndex(ind)
{
	if (this.IsValid()  &&  ind >= 0  &&  ind < this.elementObj.options.length)
	{
		//	Some browsers, after removing the currently-selected option, will then arbitrarily select the first item
		//	in the list.  We think it makes more sense to have nothing selected after removing the currently-selected item.
		var bWasSelected = (ind === this.elementObj.selectedIndex);
		this.elementObj.options[ind] = null;
		if (bWasSelected)
		{
			this.ClearSelection();
		}
	}
}

function Listbox_RemoveItemByValue(value)
{
	var ind = this.GetIndexOfItem(value);
	this.RemoveItemByIndex(ind);
}

function Listbox_Clear()
{
	if (this.IsValid())
	{
		this.priorValue = null;
		if (this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length)
		{
			this.priorValue = this.elementObj.options[this.elementObj.selectedIndex].value;
		}
		for (var i=(this.GetCount() - 1); i>=0; i--)
		{
			this.elementObj.options[i] = null;
		}
	}
	this.SetTooltip('');
}

function Listbox_ClearSelection()
{
	if (this.elementObj.options.length > 0  &&  '' === this.elementObj.options[0].value)
		this.elementObj.selectedIndex = 0;
	else
		this.elementObj.selectedIndex = -1;

	this.SaveData();
	this.SetTooltip('');
}

function Listbox_Sort()
{
	var sortByTextProp =
		function(a, b) {
						return (
								(a.text == b.text) ? 0 : (a.text > b.text ? 1 : -1)
								);
						};
	if (this.IsValid())
	{
		var tmp = this.GetCurrentList();
		tmp.sort(sortByTextProp);
		this.Clear();
		this.AddItems(tmp);
	}
}

function Listbox_GetValue()
{
	//	returns the value of the currently-selected item.  Returns empty-string if there is no list or no selection.
	if (this.IsValid()  &&  this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length)
	{
		return(this.elementObj.options[this.elementObj.selectedIndex].value);
	} else {
		return('');    // null is problematic for now
	}
}

function Listbox_GetText()
{
	//	returns the text of the currently-selected item.  Returns empty-string if there is no list or no selection.
	if (this.IsValid()  &&  this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length)
	{
		return(this.elementObj.options[this.elementObj.selectedIndex].text);
	} else {
		return('');
	}
}

function Listbox_LoadData()
{
	var valueToSelect = null;
	if (this.defaultValue !== null)
	{
		//	first priority is the object's assigned "defaultValue"
		valueToSelect = this.defaultValue;
	}
	else if (typeof(ProfileStore) != 'undefined'  &&  this.ProfileKey !== null  &&  ProfileStore.containsKey(this.ProfileKey))
	{
		valueToSelect = ProfileStore.safeget(this.ProfileKey);
//		if (valueToSelect === '')   valueToSelect = null;  // empty string has meaning when testing for blank options -- must use null instead
	}

	//	Try to automatically get the data from the functions the user has "bound" to the object.
	//	The idea is to end up with a 2-dimensional array (each row 2 members, for text & value.)
	if (this.SourceData !== null)
	{

		var myData = this.SourceData();
		if (this.SourceDataMethod == 'XML')
		{
			myData = XMLToArray(myData, this.XMLTagName);
			myData = XMLArrayToListboxArray(myData, this.SourceDataTextKey, this.SourceDataValueKey);

		} else if (this.SourceDataMethod == 'XMLArray') {
			myData = XMLArrayToListboxArray(myData, this.SourceDataTextKey, this.SourceDataValueKey);

		} else if (this.SourceDataMethod == '2dimArray') {
			//	no action needed
		} else if (this.SourceDataMethod == 'SimpleArray') {
			//	no action needed, but AddItem needs to be different below
		}

		this.Clear();
		if (this.includeBlank)
			this.AddItem('', '');
		for (var i=0; i<myData.length; i++)
		{
			if (this.SourceDataMethod == 'SimpleArray')
				this.AddItem(myData[i], myData[i]);  // same for text & value
			else  // all others
				this.AddItem(myData[i][0], myData[i][1]);
		}
	}

	if (valueToSelect !== null)
		this.SelectItemByValue(valueToSelect);
}


function Listbox_SelectFirstNonblank()
//	Attempts to select the first non-blank option in the list (blank means the _value_ is '', not the text, although usually they would both be blank.)
{
	if (this.IsValid())
	{
		for (var i=0; i<this.elementObj.options.length; i++)
		{
			if (this.GetValueOfItem(i) !== '')
			{
				return(this.SelectItemByIndex(i));  // true only if successful
			}
		}
	}
	return false;
}

function Listbox_SelectPriorValue()
//	Attempts to restore selection to the last value selected before the most recent .Clear().  Every .Clear() sets an
//	internal flag to store the selected value before clearing the list.
{
	if (this.IsValid()  &&  this.priorValue !== null)
	{
		return(this.SelectItemByValue(this.priorValue));  // true only if successful
	} else {
		return false;
	}
}

function Listbox_SelectProfileValue()
//	Attempts to select an item whose value matches the value stored in ProfileStore
{
	if (this.IsValid()  &&  this.ProfileKey !== null  &&  ProfileStore.containsKey(this.ProfileKey))
	{
		var valueToSelect = ProfileStore.get(this.ProfileKey);  // not safe; we don't want to get confused with real empty string values
		if (valueToSelect !== null)
		{
			if (this.SelectItemByValue(valueToSelect))  // returns T/F for successfully found item in list & selected it
				return true;
		}
	}
	return false;
}

function Listbox_SelectDefaultValue()
//	Attempts to select an item whose value matches the value set by "SetDefaultValue"
{
	if (this.IsValid()  &&  this.defaultValue !== null)
	{
		return(this.SelectItemByValue(this.defaultValue));  // true only if successful
	} else {
		return false;
	}
}

function __Listbox_OnChange()
{
	this.__CheckForTextOverrun();
	this.__baseWidget_OnChange();
}

function __Listbox_CheckForTextOverrun()
{
	var bTextOverrun = false;
	if (this.IsValid()  &&  this.elementObj.selectedIndex >= 0  &&  this.elementObj.selectedIndex < this.elementObj.options.length)
	{
		// do this the long way b/c we actually need the option element ref, not just the text
		var optionElement = this.elementObj.options[this.elementObj.selectedIndex];
		var newText = optionElement.text;
		if ('' !== newText  &&  0 !== this.elementObj.offsetWidth)
		{
			//	We have to account for the width of the listbox button, and also an "unknown" amount of padding
			//	that IE adds to listbox options.  Neither of these are measurable in any way via JS/DHTML, nor
			//	are they included in the "offset" amounts reported.
			//	The Listbox Button is the same as Windows' Scrollbar Width.  Maybe we can get it from system metrics?
			//	The unknown padding seems to be +3 when 4pt, +6 when 8pt, and +9 when 12pt.  So it's fontSize*3/4.
			var widthOfListboxButton = 20;  // -20 = arbitrary guess for the width of the dropdown button.  Most people have 16-20.
			var unknownPadding = parseInt(fl_getComputedStyle(this.elementObj, 'fontSize', 'font-size'), 10);
			if (isNaN(unknownPadding))   unknownPadding = 0;
			unknownPadding = (Math.round(unknownPadding / 4)) * 3;

			var availableWidth = (this.elementObj.offsetWidth - widthOfListboxButton) - unknownPadding;
			var textWidth = fl_getTextLayoutWidth(newText, this.elementObj);
			if (textWidth > availableWidth)
			{
				bTextOverrun = true;
			}
		}
	}

	if (bTextOverrun)
	{
		this.SetTooltip(newText);
	}
	else
	{
		this.SetTooltip('');
	}
}

function Listbox_Render(parentNode)
{
	//	06-Dec-2006:jblack -- Right now the only easy (and consistent) way to do this is to
	//	require a reference to the parentNode in the HTML document as a parameter.  We can't
	//	assume that the container's Parent object is already "initialized enough" that it has
	//	a valid element, because we don't know (and shouldn't care) what order the Containers
	//	were assembed together.  Open to suggestions.....

	this.ConfirmParentNode(parentNode);  // sets up this.parentNode & this.parentNodeId
	if (null !== this.parentNode)
	{
		var tbody = null;
		//	First try to append to an existing InputLayout.
		if (this.parentNode.childNodes.length > 0  &&
			this.parentNode.lastChild.tagName == 'TABLE'  &&
			this.parentNode.lastChild.className == 'InputLayout')
		{
			tbody = this.parentNode.lastChild.getElementsByTagName('TBODY')[0];  // pretty sure that tbody exists if table does
		}
		else
		{
			var tableElem = document.createElement('TABLE');
			//	The Table element is the Wrapper, in case we need to work with that for showing/hiding.
			var tableElemId = this.GetElementId() + '_wrapper';
			tableElem.id = tableElemId;
			tableElem.className = 'InputLayout';
			this.parentNode.appendChild(tableElem);

			var tbody = document.createElement('TBODY');
			tableElem.appendChild(tbody);
		}

		var tr = document.createElement('TR');
		tr.className = 'InputLayoutRow';
		tbody.appendChild(tr);

		this.labelElementObj = document.createElement('TD');
		this.labelElementObj.id = this.labelElementId;
		this.labelElementObj.className = 'InputLayoutCell InputLayoutCell_Label';
		tr.appendChild(this.labelElementObj);

		var elementCell = document.createElement('TD');
		elementCell.className = 'InputLayoutCell InputLayoutCell_Input';
		tr.appendChild(elementCell);

		this.elementObj = document.createElement('SELECT');
		this.elementObj.id = this.GetElementId();
		elementCell.appendChild(this.elementObj);

		elementCell = null;
		tr = null;
		tbody = null;
		tableElem = null;

		this.Localize();
	}
}

function Listbox_Localize(localizeType)
{
	if (null !== this.labelStringId)
	{
		var labelText = LocalizeString(this.labelStringId);
		if ('' !== labelText)
			fl_writeTextToElement(this.labelElementObj, labelText);
	}
	this.Localize_local(localizeType);
}








// #####################  Combobox Class  #####################
// ###############  inherited from Listbox  ###############
/*
	Combobox is not just an element "manager" like the other Widget classes -- it actually creates the HTML needed to render
	the element.  To use, create an **empty DIV** in your page with a unique ID.  The resulting Combobox will take up the exact
	space of the DIV**, so position the DIV as if it were a real "select" tag to be seen in that spot.

	Due to IE limitations re: styling the exact look, height, & font of select boxes, the following arbitrary defaults are used:
		font size of combobox interior: 8pt
		height of combobox = 20px
	(The width is captured from the original DIV, and the font face is correctly inherited from the current CSS spec at that point.)
*/
function Combobox(elementID)
{
	//	elementID is important in this VC...complain if not valid
	//	must be done in constructor since lots of init stuff has to happen in the right order
	if (typeof(elementID) == 'undefined')
		throw(new Error(0, 'Combobox(elementID)\nArgument "elementID" expected in constructor.'));

	this.baseConstructor = Listbox;
	this.baseConstructor(elementID);  // we don't care what happens to elementID in superclass, we'll override its effect later anyway
	delete this.baseConstructor;

	//	usage
	this.onChange = null;  // define in inherited classes to use (will be triggered on any change)
	this.GetValue = Combobox_GetValue;
	this.SelectItemByValue = Combobox_SelectItemByValue;  // same effect as SetValue()

	//	private
	this.SetElementId = function() { return; };  // disable this
	this.ActionSelectboxChanged = Combobox_ActionSelectboxChanged;
	this.ActionTextboxChanged = Combobox_ActionTextboxChanged;
	this.AddItemToList = Combobox_AddItemToList;

	this.elementID = elementID;
	this.selectboxID = this.elementID + '_selectbox';
	this.divContainerID = this.elementID+'_divcontainer';
	this.selectboxMaskID = this.elementID+'_selectboxmask';
	this.textboxID = this.elementID + '_textbox';
	this.selectboxChanging = false;
	this.textboxChanging = false;

	//	"this.originalDivObj" will refer to the DIV in the user's HTML.
	//	"this.elementObj" will refer to the SELECTBOX component of the combo container (initialized later)...since that will always have the updated value.
	this.originalDivObj = document.getElementById(this.elementID);
	if (this.originalDivObj === null)
		throw(new Error(0, 'Combobox_SetElementId()\nAn element of ID \''+this.elementID+'\' was not found in the document.'));

	//	save a reference for this object to the window, so that onChange scripts (which run in global scope) can easily find this instance
	this.windowObjectName = this.elementID + '_windowObject';
	window[this.windowObjectName] = this;

	//	### Start generating internal widgets.
	//	get style info for the user's DIV (we have to reproduce things exactly)
	this.fontFamily = fl_getComputedStyle(this.originalDivObj, 'fontFamily', 'font-family');
	this.fontFamily = '\'Arial\', sans-serif';  // arbitrary default
	this.fontSize = 8;  // arbitrary default
	this.outerBorderWidth = 2;
	this.textBoxPadding = 2;
	this.backgroundColor = '#ffffff';

	//	the look/feel of a selectbox button seems to be driven off of font size... use arbitrary fixed-pixel dimensions for now.
	this.selectButtonOffsetRight = (this.outerBorderWidth * -1) - 1;
	this.selectButtonOffsetTop = (this.outerBorderWidth * -1) - 2;
	this.selectMaskOffsetRight = -1;
	this.selectMaskWidth = 18;
	this.selectMaskHeight = 16;

	this.divContainerWidth = this.originalDivObj.offsetWidth - (this.outerBorderWidth*2);
	this.divContainerHeight = 15;  // arbitrary, to match selectbox button size

	this.selectboxWidth = this.divContainerWidth;
	this.textboxWidth = this.divContainerWidth - (this.textBoxPadding*2 + this.selectMaskWidth + 1);
	this.textboxHeight = 13;

	var selectChangeScript = 'window[\''+this.windowObjectName+'\'].ActionSelectboxChanged();';
	var textChangeScript = 'window[\''+this.windowObjectName+'\'].ActionTextboxChanged();';

	this.selectboxHTML = '        <select onchange="'+selectChangeScript+'" id="'+this.selectboxID+'" name="'+this.selectboxID+'" style="position:absolute; right:'+this.selectButtonOffsetRight+'px; top:'+this.selectButtonOffsetTop+'px; width:'+this.selectboxWidth+'; border:none; font-size:'+this.fontSize+'pt; font-family:'+this.fontFamily+';"></select>\n';
	this.selectMaskHTML = '    <div id="'+this.selectboxMaskID+'" style="overflow:hidden; position:absolute; right:'+this.selectMaskOffsetRight+'px; top:0px; margin:0; padding:0; width:'+this.selectMaskWidth+'px; height:'+this.selectMaskHeight+'px; font:inherit; text-align:left;">\n'+this.selectboxHTML+'    </div>\n';

	this.textboxHTML = '    <input onchange="'+textChangeScript+'" type="text" id="'+this.textboxID+'" name="'+this.textboxID+'" style="position:absolute; left:0px; top:0px; width:'+this.textboxWidth+'; height:'+this.textboxHeight+'; border:0px inset; margin:0; padding:0px '+this.textBoxPadding+'px 0px '+this.textBoxPadding+'px; background-color:transparent; font-size:'+this.fontSize+'pt; font-family:'+this.fontFamily+';" />\n';
	this.divContainerHTML = '<div id="'+this.divContainerID+'" style="position:absolute; width:'+this.divContainerWidth+'px; height:'+this.divContainerHeight+'px; border-width:'+this.outerBorderWidth+'px; border-style:inset; background-color:'+this.backgroundColor+';">\n' + this.selectMaskHTML + this.textboxHTML + '</div>\n';

	this.originalDivObj.innerHTML = this.divContainerHTML;
	this.selectboxObj = document.getElementById(this.selectboxID);
	this.textboxObj = document.getElementById(this.textboxID);

	this.elementObj = this.selectboxObj;
}


function Combobox_ActionSelectboxChanged()
{
	if (this.selectboxChanging)  // prevent recursion
		return;
	this.selectboxChanging = true;
	//	can't use this.GetValue to get current selection since this.elementObj points to textbox.
	setValueOfFormElement(this.textboxObj, getValueOfFormElement(this.selectboxObj));
	if (this.onChange !== null)
		this.onChange();
	this.selectboxChanging = false;
}
function Combobox_ActionTextboxChanged()
{
	if (this.textboxChanging)  // prevent recursion
		return;
	this.textboxChanging = true;
	var val = getValueOfFormElement(this.textboxObj);
	this.AddItemToList(val);
	this.SelectItemByValue(val);
	if (this.onChange !== null)
		this.onChange();
	this.textboxChanging = false;
}

function Combobox_AddItemToList(val)
{
	if (this.GetIndexOfItem(val) == -1) // not in list
	{
		this.AddItem(val, val);  // same for text & value
		this.Sort();
		//	do something with Service Manager?
	}
}

function Combobox_GetValue()
{
	return getValueOfFormElement(this.textboxObj);
}

function Combobox_SelectItemByValue(value)
{
	if (this.IsValid())
	{
		var ind = this.GetIndexOfItem(value);
		if (ind >= 0)
		{
			this.selectboxObj.selectedIndex = ind;
			this.ActionSelectboxChanged();
		}
	}
}










// #####################  Checkbox Class  #####################
// ###############  inherited from Widget  ###############
function Checkbox(elementId, labelId)
{
	if (typeof(this.containerType) == 'undefined')  // careful not to re-set this if a derived class set it prior to running this constructor
		this.containerType = 'Checkbox';

	this.baseConstructor = Widget;
	this.baseConstructor(elementId);
	delete this.baseConstructor;

	this.ProfileKey = null;
	this.myLabelID = '';	// ID of label for this checkbox
	this.myLabel = null; // DOM Node object surrounding label for this checkbox

	this.SetStringId = Checkbox_SetStringId;
	this.Check = Checkbox_Check;
	this.IsChecked = Checkbox_IsChecked;
	this.Enable = Checkbox_Enable;
	this.Render = Checkbox_Render;
	this.Localize_local = Checkbox_Localize;

	// override base VC methods for faster processing
	this.SetValue = Checkbox_Check;  // pass bool
	this.GetValue = Checkbox_IsChecked;  // retuns bool

	this.SetOptionValue = Checkbox_SetOptionValue;
	this.GetOptionValue = Checkbox_GetOptionValue;

	this.bSelfRendered = false;
	this.groupName = '';  // radio groups
	this.elementType = 'checkbox';  // match HTML attrib syntax

	if (typeof(labelId) != 'undefined')
	{
		this.myLabelID = labelId;
		this.SetStringId(labelId);
	}

	if (this.elementObj  &&  this.elementObj.tagName == 'INPUT')
		this.checkboxObj = this.elementObj;
}

function Checkbox_SetStringId(sID)
{
	this.myLabel = document.getElementById(sID);
}
function Checkbox_Check(bChecked)
{
	this.checkboxObj.checked = safeBool(bChecked);
}

function Checkbox_IsChecked()
{
	return this.checkboxObj.checked;
}

/*
Enable/Disable the checkbox and its associated label
*/
function Checkbox_Enable(bEnable)
{
	if (typeof(bEnable) == 'undefined')
		bEnable = true;
	else
		bEnable = safeBool(bEnable);  // convert from string if needed ("true|false" or "1|0", etc.)

	this.bEnabled = bEnable;

	if(bEnable)
	{
		EnableControl(this.myLabel);
		EnableControl(this.checkboxObj);
	}
	else
	{
		DisableControl(this.myLabel);
		DisableControl(this.checkboxObj);
	}
}

function Checkbox_Render()
{
	if (this.checkboxObj)
		return;  // already in doc as a checkbox .. not handling this now

	this.bSelfRendered = true;

	var layoutElem = null;
	var layoutTbody = null;
	var groupName = '';

	//	If we are a child of a checkbox group, add to its table layout instead of creating a separate one.
	if (this.Parent  &&  'CheckboxGroup' == this.Parent.containerType  &&  this.Parent.elementObj)
	{
		layoutTbody = this.Parent.GetGroupElement();
		FL_ASSERT((null !== layoutTbody), 'Checkbox_Render -- cannot get parent group element.');
	}
	else
	{
		groupName = this.Parent.GetGroupName();
		layoutElem = document.createElement('TABLE');
		layoutElem.className = 'OptionLayout';

		if (this.elementObj)  // placeholder element exists
		{
			layoutElem.id = this.elementId + '_OptionLayout';
			this.elementObj.appendChild(layoutElem);
		}
		else if (this.Parent  &&  this.Parent.elementObj)  // add new element to parent
		{
			layoutElem.id = this.elementId;
			this.elementObj = layoutElem;
			this.Parent.elementObj.appendChild(layoutElem);
		}

		layoutTbody = document.createElement('TBODY');
		layoutElem.appendChild(layoutTbody);
	}

	var tr = document.createElement('TR');
	layoutTbody.appendChild(tr);

	var tdOption = document.createElement('TD');
	tdOption.className = 'option';
	tr.appendChild(tdOption);
	this.checkboxObj = document.createElement('INPUT');
	this.checkboxObj.type = this.elementType;
	this.checkboxObj.id = this.elementId + '_' + this.elementType;
	this.checkboxObj.name = this.checkboxObj.id;
	this.checkboxObj.className = 'CheckboxRadio';
	if ('' != groupName)
		this.checkboxObj.name = groupName;
	tdOption.appendChild(this.checkboxObj);

	var tdLabel = document.createElement('TD');
	tr.appendChild(tdLabel);
	this.myLabel = document.createElement('LABEL');
	this.myLabel.id = this.elementId + '_label';
	this.myLabel.setAttribute('for', this.checkboxObj.id);
	tdLabel.appendChild(this.myLabel);

	this.Localize();
}

function Checkbox_Localize()
{
	if (this.bSelfRendered  &&  this.myLabel)
	{
		var strTranslation = LocalizeString(this.myLabelID);
		if ('' !== strTranslation)
			this.myLabel.innerText = LocalizeString(this.myLabelID);
	}
}

function Checkbox_SetOptionValue(value)
{
	if (this.IsValid())
	{
		this.elementObj.value = value;
	}
	else
	{
		FL_ASSERT(false, 'Checkbox_SetOptionValue('+value+') -- Checkbox container object is not valid.');
	}
}

function Checkbox_GetOptionValue()
{
	if (this.IsValid())
	{
		return this.elementObj.value;
	}
	else
	{
		FL_ASSERT(false, 'Checkbox_GetOptionValue() -- Checkbox container object is not valid.');
		return '';
	}
}






function CheckboxGroup(elementIdArray)
{
	if (typeof(this.containerType) == 'undefined')
		this.containerType = 'CheckboxGroup';

	this.baseConstructor = Widget;
	this.baseConstructor();
	delete this.baseConstructor;

	this.IsValid = CheckboxGroup_IsValid;
	this.onLoad_local = CheckboxGroup_OnLoad;
	this.Localize_local = CheckboxGroup_Localize;
	this.__OnChildClick = __CheckboxGroup_OnChildClick;
	this.SetValue = function(){ FL_ASSERT(false, 'CheckboxGroup() class does not support Get/Set value.') };
	this.GetValue = function(){ FL_ASSERT(false, 'CheckboxGroup() class does not support Get/Set value.') };
//	this.Render = CheckboxGroup_Render;
//	this.GetGroupElement = function() { return this.layoutTbody; };
//	this.GetGroupName = function() { return this.groupingName; };

	for (var i=0; i<elementIdArray.length; i++)
	{
		var elementId = '';
		var stringId = '';
		var value = '';
		if (typeof(elementIdArray[i]) == 'object')
		{
			elementId = elementIdArray[i][0];
			value = elementIdArray[i][1];
			stringId = elementIdArray[i][2];
		}
		else
		{
			elementId = elementIdArray[i];
		}
		var chkbx = new Checkbox(elementId, stringId);
		if ('' !== value)
			chkbx.SetOptionValue(value);
		this.AssignChild(chkbx);
	}


	this.layoutTbody = null;
	this.groupingName = '';  // used for radio groups
}

function CheckboxGroup_IsValid()
{
	return(this.ChildList.length > 0  &&  this.ChildList[0].IsValid());
}

function CheckboxGroup_OnLoad()
{
	for (var i=0; i<this.ChildList.length; i++)
	{
		this.ChildList[i].SetOnClick(this, this.__OnChildClick);
	}
}

function CheckboxGroup_Localize()
{
	//TODO
}

function __CheckboxGroup_OnChildClick()
{
	var newValue = window.event.srcElement.value;
	this.OnSelection(newValue);
}

function CheckboxGroup_Render()
{
//	var layoutElem = document.createElement('TABLE');
//	layoutElem.className = 'OptionLayout';

//	this.layoutTbody = document.createElement('TBODY');
//	layoutElem.appendChild(this.layoutTbody);

//	if (this.elementObj)  // placeholder element exists
//	{
//		layoutElem.id = this.elementId + '_OptionLayout';
//		this.elementObj.appendChild(layoutElem);
//	}
//	else if (this.Parent  &&  this.Parent.elementObj)  // add new element to parent
//	{
//		layoutElem.id = this.elementId;
//		this.elementObj = layoutElem;
//		this.Parent.elementObj.appendChild(layoutElem);
//	}
}







// #####################  RadioButton Class  #####################
// ###############  inherited from Checkbox  ###############
function RadioButton(elementId, labelId)
{
	this.baseConstructor = Checkbox;
	this.baseConstructor(elementId, labelId);
	delete this.baseConstructor;

	this.elementType = 'radio';  // match HTML attrib syntax

}
//  for now just mimic the Checkbox interface ... future improvements = grouped radio collections?




function RadioGroup(elementIdArray)
{
	this.baseConstructor = CheckboxGroup;
	this.baseConstructor(elementIdArray);
	delete this.baseConstructor;

	this.SetValue = RadioGroup_SetValue;
	this.GetValue = RadioGroup_GetValue;
	this.OnSelection = RadioGroup_OnSelection;

	this.groupingName = createUniqueName('RadioGroup');
}

function RadioGroup_SetValue(value)  // Checks the radio in the group that matches the Value
{
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i].IsValid()  &&  value == this.ChildList[i].GetOptionValue())
		{
			var elementObj = this.ChildList[i].GetElementObj();
			elementObj.click();
			break;
		}
	}
}

function RadioGroup_GetValue()  // Returns the Value of the currently-checked radio in the group
{
	for (var i=0; i<this.ChildList.length; i++)
	{
		if (this.ChildList[i].IsChecked())
			return this.ChildList[i].GetOptionValue();
	}
	return '';
}

function RadioGroup_OnSelection(newValue)
{
}









function IPEntryClass(elementID)
{
	this.baseConstructor = Widget;
	this.baseConstructor(elementID);
	delete this.baseConstructor;

	this.sIPNodeIDs = null;
	this.oIPNodeBoxes = null;
	this.ipNodeInFocus = null;

	this.SetNodeIDs = IPEntryClass_SetNodeIDs;
	this.Keystroke = IPEntryClass_Keystroke;
	this.TestIPValid = IPEntryClass_TestIPValid;
	this.NewFocus = IPEntryClass_NewFocus;
	this.Advance = IPEntryClass_Advance;
	this.GetValue = IPEntryClass_GetValue;
	this.SetValue = IPEntryClass_SetValue;
	this.LoadData_local = IPEntryClass_LoadData;
	this.SaveData_local = IPEntryClass_SaveData;
	this.Enable = IPEntryClass_Enable;
	this.autoTab = IPEntryClass_autoTab;
}

function IPEntryClass_SetNodeIDs(id1, id2, id3, id4)
{
	this.sIPNodeIDs = [id1, id2, id3, id4];
	this.oIPNodeBoxes = [];
	for (var i=0; i<4; i++)
		this.oIPNodeBoxes[i] = document.getElementById(this.sIPNodeIDs[i]);
}
function IPEntryClass_Keystroke(oInputBox, oEvent)
{
	var kc = getKeyCode(oEvent);
	if (kc == 46)  // period
	{
		this.Advance(oInputBox);
		return(false);  // cancel entry of the char
	}

	return (isKeypressNumeric(oEvent))
	// return false to inline handler --> cancels keypress event
	//  no need to check for number of keystrokes -- box is limited to 3 by html attrib,
	//  and we require user to use period to advance

}
function IPEntryClass_autoTab(oInputBox,oEvent)
{

	if( (9 == event.keyCode) || (16 == event.keyCode) )// shift tab
	{
	    return(true);
	}
	else
    {
        if(oInputBox.value.length >= oInputBox.maxLength )
        {
            oInputBox.value = oInputBox.value.slice(0, oInputBox.maxLength);

            if (oInputBox.id == this.sIPNodeIDs[oInputBox.maxLength])  // if we're already in the 4th box
                oInputBox.blur();  // remove focus altogether
            else
                this.oIPNodeBoxes[(this.ipNodeInFocus+1) % this.sIPNodeIDs.length].focus();

        }
    }
}

function IPEntryClass_TestIPValid(oInputBox)
//  Checks for valid octal number, and resets the value of the input box if out of range.
//  This needs to be called on a "post-change" event (keyup, onblur, onchange) or else it won't function properly
{
	var numericValue = parseInt(oInputBox.value, 10);
	if (isNaN(numericValue)  ||  numericValue < 0)
		oInputBox.value = '0';
	else if (numericValue > 255)
		oInputBox.value = '255';
}

function IPEntryClass_NewFocus(oInputBox)
//	called when an IP node receives focus (doesn't matter how)
{
	//	set var in IPEntry object to know which node is in focus
	for (var i=0; i<this.sIPNodeIDs.length; i++)
	{
		if (this.sIPNodeIDs[i] == oInputBox.id)
		{
			this.ipNodeInFocus = i;
			break;
		}
	}
	oInputBox.select();  // select text in the box if already there
}
function IPEntryClass_Advance(oInputBox)
{
	if (oInputBox.id == this.sIPNodeIDs[3])  // if we're already in the 4th box
		oInputBox.blur();  // remove focus altogether
	else
		this.oIPNodeBoxes[(this.ipNodeInFocus+1)].focus();

}


function IPEntryClass_GetValue()
{	//	returns IP string by concatenating values of the node boxes
	var sWholeIP = '';
	for (var i=0; i<3; i++)
		sWholeIP += this.oIPNodeBoxes[i].value + '.';
	sWholeIP += this.oIPNodeBoxes[3].value;  // and the 4th w/o the dot
	return(sWholeIP=='...' ? '' : sWholeIP);   // replace empty IP with real empty string
}
function IPEntryClass_SetValue(sWholeIP)
{	//	given an IP-format string, sets the values of the 4 node boxes
	var aIPNodeVals;
	if (sWholeIP === '')  // allow passing in of plain empty string; will translate to empty string for the 4 nodes
		aIPNodeVals = ['','','',''];
	else
		aIPNodeVals = sWholeIP.split('.');
	for (var i=0; i<aIPNodeVals.length; i++)  // use aIPNodeVals.length instead of 4 in case we previously saved a malformed IP
		setValueOfFormElement(this.oIPNodeBoxes[i], aIPNodeVals[i]);
}

function IPEntryClass_LoadData()
{
	if (this.ProfileKey === null  ||
			this.ProfileKey === ''  ||
			typeof(this.oIPNodeBoxes) == 'undefined' ||
			typeof(this.oIPNodeBoxes[0]) == 'undefined')
		return(false);
	this.SetValue(ProfileStore.safeget(this.ProfileKey));
/*
	var aIPNodeVals = sWholeIP.split('.');
	for (var i=0; i<aIPNodeVals.length; i++)  // use aIPNodeVals.length instead of 4 in case we previously saved a malformed IP
		setValueOfFormElement(this.oIPNodeBoxes[i], aIPNodeVals[i]);
*/
	return(true);
}
function IPEntryClass_SaveData()
{
	if (this.ProfileKey === null  ||
			this.ProfileKey === ''  ||
			typeof(this.oIPNodeBoxes) == 'undefined' ||
			typeof(this.oIPNodeBoxes[0]) == 'undefined')
		return(false);
/*
	var sWholeIP = '';
	for (var i=0; i<3; i++)
		sWholeIP += this.oIPNodeBoxes[i].value + '.';
	sWholeIP += this.oIPNodeBoxes[3].value;  // and the 4th w/o the dot
*/
	ProfileStore.put(this.ProfileKey, this.GetValue());
	return(true);
}
// Enable method - true enables HTML control, false disables.  missing param defaults to true
function IPEntryClass_Enable(bEnable)
{
	if (typeof(bEnable) == 'undefined')
		bEnable = true;
	else
		bEnable = safeBool(bEnable);  // convert from string if needed ("true|false" or "1|0", etc.)

	if(bEnable)
	{
		EnableControl(this.elementObj);
		for (var i=0; i<4; i++)
			EnableControl(this.oIPNodeBoxes[i]);
//		this.oIPNodeBoxes[0].focus();  // very problematic :-(
	} else {
		DisableControl(this.elementObj);
		for (var i=0; i<4; i++)
			DisableControl(this.oIPNodeBoxes[i]);
	}
}


/*  a work in progress....maybe later....for now, HTML needs explicit DIVs for inner IP nodes
function IPEntryClass_Initialize()
//	given an IPEntry object linked to one outer DIV, creates the inner DIVs (nodes), spaces the visual styles, and sets the IDs of the nodes.
{
	if (this.elementID === ''  ||  this.elementObj === null)  // initial constructor state, never set
		return(false);
}
*/




