/*	=========================   DOM stuff   =============================

02-Feb-2006:jblack	Moved Dom stuff out of Utilities.js and applied recent improvements 
					from the Security prototype project.
18-Sep-2006:jblack	Better assertion helpers for valid element, valid dimensions, etc.

*/

NODE_TYPE_ELEMENT = 1;
NODE_TYPE_ATTRIBUTE = 2;
NODE_TYPE_TEXT = 3;
NODE_TYPE_CDATA_SECTION = 4;
NODE_TYPE_ENTITY_REFERENCE = 5;
NODE_TYPE_ENTITY = 6;
NODE_TYPE_PROCESSING_INSTRUCTION = 7;
NODE_TYPE_COMMENT = 8;
NODE_TYPE_DOCUMENT = 9;
NODE_TYPE_DOCUMENT_TYPE = 10;
NODE_TYPE_DOCUMENT_FRAGMENT = 11;
NODE_TYPE_NOTATION = 12;



function fl_getComputedStyle(node, ieStylePropertyName, domStylePropertyName)
//	Returns the currently-calculated style value, including those inherited from stylesheets.
//	This of course represents only a snapshot of the current style calculation.
//	This wraps IE's "node.currentStyle" property and the DOM standard's "window.getComputedStyle(node, styleProperty)" method.
//	fl_getComputedStyle(element, 'borderWidth', 'border-width');
{
	if (null === node)  return '';
	FL_ASSERT(typeof(domStylePropertyName) != 'undefined');  // check for misuse

	if (typeof(node.currentStyle) != 'undefined')  // IE
	{
		return node.currentStyle[ieStylePropertyName];
	}
	else if (typeof(window.getComputedStyle) != 'undefined')  // DOM-standard
	{
		var compStyleObj = window.getComputedStyle(node, '');  // (node, pseudoElement)
		var styleValue = compStyleObj.getPropertyValue(domStylePropertyName);
		return styleValue;
	}
	else  // ??
	{
		return '';
	}
}


function fl_stylePropertyIeFormat(stylePropertyName)
{
	//	Converts a hyphen-format style property name ("border-style") to IE-format ("borderStyle").
	var spAtoms = stylePropertyName.split('-');
	if (spAtoms.length <= 1)
	{
		return stylePropertyName; // don't bother
	}

	for (var i=1; i<spAtoms.length; i++)
	{
		if (typeof(spAtoms[i] == 'string')  &&  spAtoms[i].length > 0)
		{
			spAtoms[i] = spAtoms[i].charAt(0).toUpperCase() + spAtoms[i].substr(1);
		}
	}
	return spAtoms.join('');
}

function fl_stylePropertyHyphenFormat(stylePropertyName)
{
	//	Converts an IE-format style property name ("borderStyle") to hyphen-format ("border-style").
	//	We don't have the luxury of fast array operations here; we have to hunt for capital letters. :-(
	//	(anyone have a better idea?)

	if (typeof(stylePropertyName) != 'string')  // who knows
		return '';

	var stylePropertyName = stylePropertyName.replace( /[A-Z]/, function(match){return '-'+match.toLowerCase();} );
	return stylePropertyName;
}




function FL_ASSERT_VALID_DOCUMENT(doc)
{
	//	do this in 2 steps so we only run the getDocError() code when isDocValid is false.
	//	(combining into 1 statement makes JS eval the getDocError() portion first.)
	if (isDocValid(doc))
	{
		return true;
	}
	else
	{
		FL_ASSERT(false, 'FL_ASSERT_VALID_DOCUMENT ... Parser error was:\n\n'+getDocError(doc));
		return	false;
	}
}

function fl_isValidElement(node)
{
	return(typeof(node) != 'undefined'  &&  null !== node  &&  typeof(node.nodeType) != 'undefined'  &&  NODE_TYPE_ELEMENT == node.nodeType);
}

function FL_ASSERT_VALID_ELEMENT(node)
{
	//	For success, node must be found in the document (not null) and be an "element" node type.
	//	In addition, if the node is a table, it must have proper HTML structure (table > tbody > [at least 1]tr > [at least 1]td)
	if (!FL_ASSERT((typeof(node) != 'undefined'), 'FL_ASSERT_VALID_ELEMENT - node is undefined (invalid reference passed in)'))
		return false;
	if (!FL_ASSERT((null !== node), 'FL_ASSERT_VALID_ELEMENT - node is null (not found in the document)'))
		return false;
	if (!FL_ASSERT((NODE_TYPE_ELEMENT == node.nodeType), 'FL_ASSERT_VALID_ELEMENT - node is valid, but not an HTML or XML Element type'))
		return false;

	if ('TABLE' == node.tagName)
	{
		var bValidTable = (	node.firstChild  &&  'TBODY' == node.firstChild.tagName  &&
						node.firstChild.firstChild  &&  'TR' == node.firstChild.firstChild.tagName  &&
						node.firstChild.firstChild.firstChild  &&  'TD' == node.firstChild.firstChild.firstChild.tagName);
		if (!FL_ASSERT(bValidTable, 'FL_ASSERT_VALID_TABLE\n\n'+reportNodeBasicInfo(node)))
			return false;
	}

	return true;
}

function FL_ASSERT_VALID_DIMENSIONS(node)
{
	//	For success, node must have display != 'none' and have any rendered width & height > 0.
	var bValid = ('none' != fl_getComputedStyle(node, 'display', 'display')  &&  0 != node.offsetWidth  &&  0 != node.offsetHeight);
	return FL_ASSERT(bValid, 'FL_ASSERT_VALID_DIMENSIONS\n\n'+reportNodeBasicInfo(node));
}

function FL_ASSERT_VALID_TABLE(node)
{
	//	For success, node must be a table with proper HTML structure (table > tbody > [at least 1]tr > [at least 1]td)
	var bValidTable = (	node.firstChild  &&  'TBODY' == node.firstChild.tagName  &&
					node.firstChild.firstChild  &&  'TR' == node.firstChild.firstChild.tagName  &&
					node.firstChild.firstChild.firstChild  &&  'TD' == node.firstChild.firstChild.firstChild.tagName);
	return FL_ASSERT(bValidTable, 'FL_ASSERT_VALID_TABLE\n\n'+reportNodeBasicInfo(node));
}


function fl_isHtmlElement(node)
{
	if (node === null  ||  typeof(node.tagName) == 'undefined')
		return false;

	if (NODE_TYPE_ELEMENT != node.nodeType)
		return false;

	if ('!' == node.tagName  ||  'SCRIPT' == node.tagName  ||  'HEAD' == node.tagName  ||  'LINK' == node.tagName  ||  'META' == node.tagName  ||  'TITLE' == node.tagName)
		return false;

	return true;
}

function reportNodeBasicInfo(node)
{
	return('tagName='+node.tagName+'\nid='+node.id+'\nclassName='+node.className+'\ndisplay='+fl_getComputedStyle(node, 'display', 'display')+'\nvisibility='+fl_getComputedStyle(node, 'visibility', 'visibility')+'\noffsetLeft='+node.offsetLeft+'\noffsetTop='+node.offsetTop+'\noffsetWidth='+node.offsetWidth+'\noffsetHeight='+node.offsetHeight);
}





function fl_removeDOMElement(elementId, parentId)
{
	var parentElement = document.getElementById(parentId);
	var element = document.getElementById(elementId);
	if(parentElement != null && element != null)
		parentElement.removeChild(element);
}

function fl_removeAllChildNodes(element)
{
	//	allow either object or ID as argument 
	if (typeof(element) == 'string')
		element = document.getElementById(element);

	if (null !== element)
	{
		while (element.childNodes.length > 0)
		{
			element.removeChild(element.childNodes[0], true);  // true = deep remove
		}
	}
}


//	fl_addClassToElement(elementObj, className)
//	fl_removeClassFromElement(elementObj, className)
//
//  HTML allows an element to have multiple classes.  The HTML syntax would be:
//      <element class="class1 class2 class3"></element>   using a space as delimiter.
//  Therefore we shouldn't add or remove classes by simple assignment, since we might overwrite
//  an existing one.

/* bool */ function fl_addClassToElement(elementObj, strClassToAdd, bPrepend/*=false*/)
{
	var bSuccess = false;
	//	allow either object or ID as argument 
	if (typeof(elementObj) == 'string')
		elementObj = document.getElementById(elementObj);

	if (null !== elementObj)
	{
		if (typeof(bPrepend) == 'undefined')   bPrepend = false;
		var oldClassName = elementObj.className;
		elementObj.className = fl_addToStringList(oldClassName, strClassToAdd, ' ', bPrepend);
		bSuccess = (oldClassName != elementObj.className);  // successful if different
	}
	return bSuccess;
}

/* bool */ function fl_removeClassFromElement(elementObj, strClassToRemove)
//  Removes a class (or classes) from an element.  If className ends in a wildcard (asterisk,) will remove all matching classes.
{
	var bSuccess = false;
	//	allow either object or ID as argument 
	if (typeof(elementObj) == 'string')
		elementObj = document.getElementById(elementObj);

	if (null !== elementObj)
	{
		var oldClassName = elementObj.className;
		elementObj.className = fl_removeFromStringList(oldClassName, strClassToRemove, ' ');
		bSuccess = (oldClassName != elementObj.className);  // successful if different
	}
	return bSuccess;
}


function fl_setStyleRecursively(elementObj, styleProperty, value)
//  Sets a style property to a value on the element, and also recursively on all child elements.
{
	if (null === elementObj)   return;  // getElementById() can yield null if element not found
	if (typeof(elementObj.nodeType) == 'undefined'  ||  NODE_TYPE_ELEMENT != elementObj.nodeType)   return;  // valid HTML element
	if (typeof(elementObj.tagName) == 'undefined'  ||  elementObj.tagName == 'OPTION')   return;   // visibility on individual options gets weird... parent "select" is sufficient
	if (typeof(elementObj.style) == 'undefined'  ||  typeof(elementObj.style[styleProperty]) == 'undefined')   return;  // add'l sanity check

	elementObj.style[styleProperty] = value;

	for (var i=0; i<elementObj.childNodes.length; i++)
	{
		fl_setStyleRecursively(elementObj.childNodes[i], styleProperty, value);
	}
}



function fl_getIntFromCssPx(elementObj, cssPxMeasurementIe, cssPxMeasurementDom)
{
	var intMeasurement = parseInt(fl_getComputedStyle(elementObj, cssPxMeasurementIe, cssPxMeasurementDom), 10);
	if (!isNaN(intMeasurement))
		return 	intMeasurement;
	else
		return null;
}



function fl_calculateCssOffsetWidthDifference(node)
{
	return _fl_calculateBorderPaddingDiff(node, 'width');
}

function fl_calculateCssOffsetHeightDifference(node)
{
	return _fl_calculateBorderPaddingDiff(node, 'height');
}

function _fl_calculateBorderPaddingDiff(node, measurementType)
//	If an element has a complex layout with non-pixel settings for margins/padding/borders,
//	we can't use the "getBorderMarginPaddingPx" method for figuring out explicit dimensions.
//	Instead, we use a rendering test to see what the difference is between the rendered 
//	measurement and the CSS "pixel" measurement required to create that rendered measurement.
//	NOTE:  In IE, this does NOT include margins!
{
	if (!FL_ASSERT_VALID_ELEMENT(node))
		return 0;

	var offsetPropertyName = (measurementType == 'width') ? 'offsetWidth' : 'offsetHeight';
	var origStyleValue = fl_getComputedStyle(node, measurementType, measurementType); // assumed to be either "width" or "height" which is OK for both IE and DOM

	var origOffsetPx = node[offsetPropertyName];
	node.style[measurementType] = origOffsetPx + 'px';  // start with the offset.  If there is extra border/margin space, this will temporarily expand the element.
	var newOffsetPx = node[offsetPropertyName];
	node.style[measurementType] = origStyleValue;  // we have our numbers, let it go back
	var borderPaddingDiffPx = newOffsetPx - origOffsetPx;
	if (borderPaddingDiffPx < 0)
		borderPaddingDiffPx = 0;
/*
	//	Now we have to find the margin.
	var marginPropertyName1 = (measurementType == 'width') ? 'marginLeft' : 'marginTop';
	var marginPropertyName2 = (measurementType == 'width') ? 'marginRight' : 'marginBottom';

	//	first check for non-px, non-zero margins.  If found, we must do the offset test on the parent.
	if (
		(node.currentStyle[marginPropertyName1] != 'auto'  &&  node.currentStyle[marginPropertyName1].slice(-2) != 'px')  ||
		(node.currentStyle[marginPropertyName2] != 'auto'  &&  node.currentStyle[marginPropertyName2].slice(-2) != 'px')
		)
	{
		if (node.parentNode  &&  NODE_TYPE_ELEMENT != node.parentNode.nodeType)
		{
			var parentOrigStyleValue = node.parentNode.currentStyle.width;
			var childOrigOffsetPx = node.offsetWidth;
			node.parentNode.style.width = childOrigOffsetPx + 'px';  // if there is a margin on the child, this will shrink it
			var childNewOffsetPx = node.offsetWidth;
			node.parentNode.style.width = parentOrigStyleValue;  // we have our numbers, let it go back
			var marginDiff = childOrigOffsetPx - childNewOffsetPx;
			borderPaddingDiffPx += marginDiff;
		}
	}
	else  // either Auto or a PX measurement -- easy, just get the value for PX and assume 0 for Auto
	{
		var marginDiff1 = 0;
		var marginDiff2 = 0;

		if (node.currentStyle[marginPropertyName1].slice(-2) == 'px')
			marginDiff1 = parseInt(node.currentStyle[marginPropertyName1]);
		if (node.currentStyle[marginPropertyName2].slice(-2) == 'px')
			marginDiff2 = parseInt(node.currentStyle[marginPropertyName2]);

		borderPaddingDiffPx += (marginDiff1 + marginDiff2);
	}
*/
	return borderPaddingDiffPx;
}




function fl_spaceChildElements(parentElem, measurementType, bProportion)
//	Resizes a number of child elements to fit evenly inside the parent.  Just like using 
//	percentage dimensions but automatically figures it on the # of child elements, and
//	without the danger of affecting the parent element's size, which can happen if there 
//	are any margins, borders, or padding on either element.
//	This assumes the "strict" (CSS-compliant) rendering model, where the padding, border, 
//	and margin of an element does NOT count towards its rendered measurements.
{
	//	allow either node object or string ID
	if (typeof(parentElem) == 'string')
		parentElem = document.getElementById(parentElem);

	if (!FL_ASSERT_VALID_ELEMENT(parentElem))
		return;
	//	none of this will work if display is suppressed in any way, or if one of the elements is a table that is not properly assembled
	if (!FL_ASSERT_VALID_DIMENSIONS(parentElem))
		return;

	var offsetPropertyName = (measurementType == 'width') ? 'offsetWidth' : 'offsetHeight';
	if (typeof(bProportion) == 'undefined')   bProportion = false;


	//	All looping has to consider that it's possible for some children to be non-element nodes (comments, text, etc.)
	//	Before we can loop through and calculate each proportion, we have to get the total rendered px of the children.
	//	This *may not* be the same as the total avail px of the parent, so we have to loop.
	var childMeasurements = [];
	var totalChildPx = 0;
	var childElementCount = 0;
	for (var i=0; i<parentElem.childNodes.length; i++)
	{
		var childNode = parentElem.childNodes[i];
		if (NODE_TYPE_ELEMENT == childNode.nodeType)  // xml/html element
		{
			//	This function is ineffective if any of the children are display=none.
			FL_ASSERT_VALID_DIMENSIONS(childNode);
			totalChildPx += childNode[offsetPropertyName];
			childElementCount++;
		}
	}
	FL_ASSERT((childElementCount>0), 'fl_spaceChildElements() - no valid child elements!');
	var totalAvailPx = parentElem[offsetPropertyName];
	var eachEqualPx = Math.round(totalAvailPx / childElementCount);

	//	Fill up our childMeasurements array with the desired values.  If proportional, use their current dimensions
	//	compared to their total space all together.  If not, assign the same value to all.
	var pxUsed = 0;
	if (bProportion)
	{
		for (var i=0; i<parentElem.childNodes.length; i++)
		{
			var childNode = parentElem.childNodes[i];
			if (NODE_TYPE_ELEMENT != childNode.nodeType)  // NOT a valid xml/html element
			{
				childMeasurements.push(null);
				continue;
			}
			var renderedPx = childNode[offsetPropertyName];
			var valToUse = Math.round((renderedPx / totalChildPx) * totalAvailPx);
			childMeasurements.push(valToUse);
			pxUsed += valToUse; // keep track of how many pixels we've used
			//	Don't figure in the diff b/w CSS and rendered measurements here, since that will throw off the "pxUsed" count.
		}
	}
	else
	{
		for (var i=0; i<parentElem.childNodes.length; i++)
			childMeasurements.push(eachEqualPx);  // don't care if we're filling in for non-element types
	}

	//	last element -- use the exact leftover space, not the usual calc
	//	since we already checked that there are 1 or more valid elements, this won't ever be out of bounds
	var lastElemIndex;
	for (lastElemIndex=parentElem.childNodes.length-1; lastElemIndex>=0; lastElemIndex--)
		if (NODE_TYPE_ELEMENT == childNode.nodeType)
			break;
	pxUsed -= childMeasurements[lastElemIndex]; // take this previous calc back out of the total
	childMeasurements[lastElemIndex] = totalAvailPx - pxUsed;

	for (var i=0; i<parentElem.childNodes.length; i++)
	{
		var childNode = parentElem.childNodes[i];
		if (NODE_TYPE_ELEMENT != childNode.nodeType)  // NOT a valid xml/html element
			continue;

		//  Subtract the difference between CSS width and Rendered width;
		var extraChildPx = 0;
		if (measurementType == 'width')
			extraChildPx = fl_calculateCssOffsetWidthDifference(childNode);
		else
			extraChildPx = fl_calculateCssOffsetHeightDifference(childNode);
		var valToUse = childMeasurements[i] - extraChildPx;
		valToUse = Math.max(valToUse, 0);  // make sure it's not less than 0
		childNode.style[measurementType] = valToUse + 'px';
	}

}



function fl_fitElementToParent(childElem, parentElem, bWidth, bHeight)
//	Resizes a child element to fit exactly inside the parent.  Just like using "100%" but 
//	without the danger of affecting the parent element's size, which can happen if there are
//	any margins, borders, or padding on either element.
//	This assumes the "strict" (CSS-compliant) rendering model, where the padding, border, and 
//	margin of an element does NOT count towards its rendered measurements.
{
	if (typeof(childElem) == 'undefined'  ||  null === childElem  ||  typeof(parentElem) == 'undefined'  ||  null === parentElem)
		return;

	if (typeof(childElem) == 'string')
		childElem = document.getElementById(childElem);
	if (typeof(parentElem) == 'string')
		parentElem = document.getElementById(parentElem);

	if (!FL_ASSERT_VALID_ELEMENT(parentElem)  ||  !FL_ASSERT_VALID_ELEMENT(childElem))
		return;
	//	none of this will work if display is suppressed in any way, or if one of the elements is a table that is not properly assembled
	if (!FL_ASSERT_VALID_DIMENSIONS(parentElem)  ||  !FL_ASSERT_VALID_DIMENSIONS(childElem))
		return;

	//	If neither param is present, it means do both width & height.
	if (typeof(bWidth) == 'undefined'  &&  typeof(bHeight) == 'undefined')
		bWidth = bHeight = true;

	if (bWidth)
	{
		var parentWidth = parentElem.offsetWidth;
		var parentExtraWidth = fl_calculateCssOffsetWidthDifference(parentElem);
		var childExtraWidth = fl_calculateCssOffsetWidthDifference(childElem);
		var targetWidth = parentWidth - parentExtraWidth - childExtraWidth;
		childElem.style.width = Math.max(targetWidth, 0) + 'px';
	}

	if (bHeight)
	{
		var parentHeight = parentElem.offsetHeight;
		var parentExtraHeight = fl_calculateCssOffsetHeightDifference(parentElem);
		var childExtraHeight = fl_calculateCssOffsetHeightDifference(childElem);
		var targetHeight = parentHeight - parentExtraHeight - childExtraHeight;
		childElem.style.height = Math.max(targetHeight, 0) + 'px';
	}
}




function fl_expandDocumentToDialogSize()
/*
 *  For use in showModalDialog() documents near the beginning of an onLoad script.
 *  Sets the width & height of the documentElement (<HTML>) to fixed dimensions, taking 
 *  into account the border width if there is one.  This means we don't have to 
 *  specify exact widths in the stylesheet that match the dimensions in the SKN file.
 */
{
    var targetWidth = window.document.documentElement.offsetWidth;
    var targetHeight = window.document.documentElement.offsetHeight;
    var docBorderStyle = fl_getComputedStyle(window.document.documentElement, 'borderStyle', 'border-style');
    if ('none' != docBorderStyle  &&  '' !== docBorderStyle)
    {
        var docBorderWidth = parseInt(fl_getComputedStyle(window.document.documentElement, 'borderWidth', 'border-width'), 10);
        if (!isNaN(docBorderWidth))
        {
            targetWidth -= (docBorderWidth*2);
            targetHeight -= (docBorderWidth*2);
        }
    }
    window.document.documentElement.style.width = targetWidth + 'px';
    window.document.documentElement.style.height = targetHeight + 'px';
}


//	Scales a style property on an element, assuming its current value is a PX measurement. (if not, does nothing.)
//		ex.  If "myDiv" has style.width of 100px, then
//				fl_scaleStylePx(myDiv, 'width', 1.5);
//		will make its width 150px.
function fl_scaleStylePx(elementObj, stylePropertyNameIe, stylePropertyNameDom, fScaleFactorFromPrevious)
{
	if (typeof(elementObj) == 'string')
		elementObj = document.getElementById(elementObj);

	if (FL_ASSERT_VALID_ELEMENT(elementObj)  &&  typeof(fScaleFactorFromPrevious) != 'undefined'  &&  !isNaN(fScaleFactorFromPrevious))
	{
		var styleOldVal = fl_getComputedStyle(elementObj, stylePropertyNameIe, stylePropertyNameDom);
		if (styleOldVal.indexOf('px') >= 0)
		{
			var iOldValue = parseFloat(styleOldVal);
			var iNewValue = Math.round(iOldValue * fScaleFactorFromPrevious);
			elementObj.style[stylePropertyName] = iNewValue + 'px';
		}
	}
}










/*	TEXT DOM NODES

		fl_getNodeText(node)		returns text inside any DOM node or HTML tag (only directly inside; won't get text inside further nested tags)
		fl_writeTextToElement(myElement, textToWrite)		inserts text inside any DOM node or HTML tag

	Text is only one of many "nodes" in a tag....and the order of the nodes is unpredictable.
	These functions go through all the child nodes of an HTML element until it finds the 
	text node, and gets or writes the text. 
*/

function fl_getNodeText(node)
{
	if (typeof(node) == 'string')
	{
		node = window.document.getElementById(node);
	}

	if (null === node)
	{
		return '';
	}
	else if (typeof(node.innerText) != 'undefined')  // IE (HTML docs)
	{
		return node.innerText;
	}
	else if (typeof(node.text) != 'undefined')  // IE (XML docs)
	{
		return node.text;
	}
	else if (typeof(node.textContent) != 'undefined')  // MOZ (both HTML and XML docs)
	{
		return node.textContent;
	}
	else  // no known accelerators, do it the hard way
	{
		var text = [];
		if (node !== null)
		{
    		for (var N = 0; N < node.childNodes.length; ++N)
    		{
    			var theChild = node.childNodes[N];
    			if (NODE_TYPE_TEXT == theChild.nodeType)
    				text.push(theChild.nodeValue);
    		}
		}
		return(text.join(''));
	}
}

/*
fl_getChildElementValue
Grabs a single element's text value, given the tag name and its parent node
*/
function fl_getChildElementValue(node, name)
{
	var nodeList = node.getElementsByTagName(name);
	if(nodeList.length > 0)
		return fl_getNodeText(nodeList[0]);
	else
		return '';
}



/*
	Given a reference to any document object, an owner tag, and an attribute name, returns the attribute value (as string).
	"Safe" get -- returns empty string if tag or attrib doesn't exist.
	For use with unique tag names only.
*/
function fl_getAttributeValue(docObj, tagName, attrName)
{
	var value = '';
	var nl = docObj.getElementsByTagName(tagName);
	if (nl.length > 0)
		value = nl[0].getAttribute(attrName);
	return(value);
}

// deprecated; exists only to pass to "fl_setNodeText"
function fl_writeTextToElement(myElement, textToWrite, myDocument)
{
	fl_setNodeText(myElement, textToWrite, myDocument);
}

function fl_setNodeText(node, textToWrite, myDocument)
/*
	Given an element (either as an DOM object OR a simple ID string,) this
	will write text inside it, replacing any existing text.
	JFR 11/03/06 - modified so a new text node can be created in a local
	XML document, passed in via myDocument param...
*/
{
	if (typeof(node) == 'undefined')
		return;

	// if passed item is a string of an ID, turn it into a real element object
	var id = '';
	if (typeof(node) == 'string')
	{
		id = node;
		node = document.getElementById(id);
	}

	if (!FL_ASSERT_VALID_ELEMENT(node))
		return;  // can't do anything

	if ('' === textToWrite)
	{
		fl_removeNodeText(node);
	}
	else
	{
		if (typeof(myDocument) == 'undefined'  &&  typeof(node.innerText) != 'undefined')  // IE
		{
			node.innerText = textToWrite;
		}
//		else if (typeof(myDocument) == 'undefined'  &&  typeof(node.textContent) != 'undefined')  // MOZ
//		{
//			node.textContent = textToWrite;
//		}
		else  // no known accelerators, do it the hard way... also do it this way if we passed in myDocument.
		{

			//	find any/all text node children 
			//	there may be more than one text node child...so find the first one, then continue
			//	iterating and nuke any remaining 
			var foundNode = null;
			for (var i=0; i<node.childNodes.length; i++)
			{
				if (NODE_TYPE_TEXT == node.childNodes[i].nodeType)	// text node 
				{
					if (null === foundNode)  // first one found (use)
						foundNode = node.childNodes[i];
					else  // additional text nodes in this level (remove)
						node.removeChild(node.childNodes[i], true);
				}
			}

			if (null === foundNode)  // no text node found (create new)
			{
				if (typeof(myDocument) != 'undefined'  &&  null !== myDocument)
				{
					// create new node in myDocument
					node.appendChild(myDocument.createTextNode(textToWrite));
				}
				else
				{
					// create new node in HTML document
					node.appendChild(document.createTextNode(textToWrite));
				}		
			} else {  // text node exists (replace value)
				foundNode.nodeValue = textToWrite;
			}
		}
	}
}

function fl_removeNodeText(node)
{
	if (typeof(node) == 'string')
	{
		node = document.getElementById(node);
	}

	if (null !== node)
	{
		if (typeof(node.innerText) != 'undefined')  // IE
		{
			node.innerText = '';
		}
		else if (typeof(node.textContent) != 'undefined')  // MOZ
		{
			node.textContent = '';
		}
		else  // no known accelerators, do it the hard way
		{
			for (var i=0; i<node.childNodes.length; i++)
			{
				if (NODE_TYPE_TEXT == node.childNodes[i].nodeType)  // text node 
				{
					node.removeChild(node.childNodes[i]);
				}
			}
		}
	}
}





/*bool*/function fl_appendTableRows(targetTableElement, newTableOrRowElement)
{
	var bSuccess = false;
	if (typeof(targetTableElement) == 'string')
		targetTableElement = document.getElementById(targetTableElement);
	if (typeof(newTableOrRowElement) == 'string')
		targetTableElement = document.getElementById(newTableOrRowElement);

	if (null === targetTableElement  ||  null === newTableOrRowElement)
	{
		FL_ASSERT(false, 'appendTableRow() ... one or more elements are invalid.');
		return false;
	}

	var parentElement = null;
	var rowsToAttach = [];

	if ('TABLE' == targetTableElement.tagName)
	{
		parentElement = targetTableElement.getElementsByTagName('TBODY')[0];
	}
	else if ('DIV' == targetTableElement.tagName)  // DIV structure with table-display CSS
	{
		parentElement = targetTableElement;
	}

	if (null !== parentElement)
	{
		if ('TABLE' == newTableOrRowElement.tagName)
		{
			var tbody = newTableOrRowElement.getElementsByTagName('TBODY')[0];
			rowsToAttach = tbody.getElementsByTagName('TR');
		}
		else if ('TR' == newTableOrRowElement.tagName  ||  'DIV' == newTableOrRowElement.tagName)
		{
			rowsToAttach = [newTableOrRowElement];  // spoof a node list with the passed row as the only member
		}

		if (rowsToAttach.length > 0)
		{
			for (var i=0; i<rowsToAttach.length; i++)
			{
				parentElement.appendChild(rowsToAttach[i]);
			}
			bSuccess = true;
		}
	}

	FL_ASSERT(bSuccess, 'appendTableRow() ... one or more elements are invalid.');
	return bSuccess;
}



//	=========================   Older DOM stuff (possibly obsolete)   =============================


/*
	Given a reference to any document object and a tag that we only expect to appear ONCE in the document,
	returns that node's text contents (as string).
	Returns empty string if tag doesn't exist or if document is empty.
	Safer than document.getElementsByTagName('TAG')[0]...
*/
function fl_getUniqueNodeText(docObj, tagName)
{
	var value = '';
	var nl = docObj.getElementsByTagName(tagName);
	if (nl.length > 0)
		value = fl_getNodeText(nl[0]);
	return(value);
}




/*
fl_arrayOfAttributeValuesFromNodes
Given an array of DOM nodes fl_arrayOfAttributeValuesFromNodes extracts all the 
values of attributes of type 'attributeName' and return them
as a one-dimensional array.

An optional filter parameter will be called as a function with each node as a parameter:

var theFilter = function(node) { return node.getAttribute('someAttribute') == 'foo'; } 

*/
function fl_arrayOfAttributeValuesFromNodes(nodes, attributeName, filter)
{
	var theArray = [];

	for (var N = 0; N < nodes.length; ++N)
	{
		var theNode = nodes[N];
		if (typeof(filter) != 'undefined'  &&  filter !== null  &&  filter(theNode) === false)
			continue;
		var theAttributeValue = theNode.getAttribute(attributeName);
		theArray.push(theAttributeValue);
	}
	return(theArray);
}

/*
fl_arrayOfMultipleAttributeValuesFromNodes
Like fl_arrayOfAttributeValuesFromNodes except it will store multiple attributes in the output array
*/
function fl_arrayOfMultipleAttributeValuesFromNodes(nodes, attributeNames, filter)
{
	var theArray = [];

	for (var N = 0; N < nodes.length; ++N)
	{
		var theNode = nodes[N];
		if (typeof(filter) != 'undefined'  &&  filter !== null  &&  filter(theNode) === false)
			continue;
		var theAttributeValues = [];
		for (var Y in attributeNames)
		{
			var theAttributeName = attributeNames[Y];
			var theAttributeValue = theNode.getAttribute(theAttributeName);
			theAttributeValues.push(theAttributeValue);
		}
		theArray.push(theAttributeValues);
	}
	return(theArray);
}

function fl_arrayOfValuesFromNodes(nodes, filter)
{
	var theArray = [];

	for (var N = 0; N < nodes.length; ++N)
	{
		var theNode = nodes[N];
		if (typeof(filter) != 'undefined'  &&  filter !== null  &&  filter(theNode) === false)
			continue;
		var theNodeValue = fl_getNodeText(theNode);
		theArray.push(theNodeValue);
	}
	return(theArray);
}



function fl_writeHTMLToElement(myElement, html)
/*
	Given an element (either as an DOM object OR a simple ID string,) this
	will write HTML inside it, replacing any existing HTML.
	Safer than "document.getElementById('id').innerHTML = ..." since null returns in the first lookup will fail the [index] part.
*/
{
	// if passed item is a string of an ID, turn it into a real element object
	if (typeof(myElement) != 'object')
		myElement = document.getElementById(myElement);

	if (typeof(myElement) != 'undefined'  ||  null === myElement)  // allow to fail silently if element not found 
		myElement.innerHTML = html;
	else
		FL_ASSERT(false, 'fl_writeHTMLToElement() -- element not found.');
}


/*  ####################  deprecated, doesn't account for non-PX values, don't use anymore  ####################  */

function fl_fitWindowToBody()
{
	var oldWidth = document.documentElement.offsetWidth;
	var oldHeight = document.documentElement.offsetHeight;

	var bodyWidth = document.body.offsetWidth;
	var bodyExtraWidth = fl_getHoriztontalBorderMarginPaddingPx(document.body);
	var docExtraWidth = fl_getHoriztontalBorderMarginPaddingPx(document.documentElement);
	var targetWidth = bodyWidth + bodyExtraWidth + docExtraWidth;

	var bodyHeight = document.body.offsetHeight;
	var bodyExtraHeight = fl_getVerticalBorderMarginPaddingPx(document.body);
	var docExtraHeight = fl_getVerticalBorderMarginPaddingPx(document.documentElement);
	var targetHeight = bodyHeight + bodyExtraHeight + docExtraHeight;

	//	Reposition the window based on new size?
	// get the difference.  If we got bigger, then these would be negative.
//	var diffWidth = oldWidth - targetWidth;
//	var diffHeight = oldHeight - targetHeight;

	resizeWindow(targetWidth, targetHeight);
//	window.external.moveWindow(targetWidth, targetHeight);
}




function fl_getVerticalBorderMarginPaddingPx(node)
{
	if (!FL_ASSERT_VALID_ELEMENT(node))
		return 0;

	var iTotalSpace = 0;

	if (fl_getComputedStyle(node, 'borderTopStyle', 'border-top-style') != 'none')
		iTotalSpace += fl_getStylePixelValue(node, 'borderTopWidth', 'border-top-width');

	if (fl_getComputedStyle(node, 'borderBottomStyle', 'border-bottom-style') != 'none')
		iTotalSpace += fl_getStylePixelValue(node, 'borderBottomWidth', 'border-bottom-width');

	iTotalSpace += fl_getStylePixelValue(node, 'marginTop', 'margin-top');
	iTotalSpace += fl_getStylePixelValue(node, 'marginBottom', 'margin-bottom');
	iTotalSpace += fl_getStylePixelValue(node, 'paddingTop', 'padding-top');
	iTotalSpace += fl_getStylePixelValue(node, 'paddingBottom', 'padding-bottom');

	return iTotalSpace;
}

function fl_getHoriztontalBorderMarginPaddingPx(node)
{
	if (!FL_ASSERT_VALID_ELEMENT(node))
		return 0;

	var iTotalSpace = 0;

	if (fl_getComputedStyle(node, 'borderLeftStyle', 'border-left-style') != 'none')
		iTotalSpace += fl_getStylePixelValue(node, 'borderLeftWidth', 'border-left-width');

	if (fl_getComputedStyle(node, 'borderRightStyle', 'border-right-style') != 'none')
		iTotalSpace += fl_getStylePixelValue(node, 'borderRightWidth', 'border-right-width');

	iTotalSpace += fl_getStylePixelValue(node, 'marginLeft', 'margin-left');
	iTotalSpace += fl_getStylePixelValue(node, 'marginRight', 'margin-right');
	iTotalSpace += fl_getStylePixelValue(node, 'paddingLeft', 'padding-left');
	iTotalSpace += fl_getStylePixelValue(node, 'paddingRight', 'padding-right');

	return iTotalSpace;
}



function fl_getStylePixelValue(node, strStylePropertyIe, strStylePropertyDom)
{
	if (!FL_ASSERT_VALID_ELEMENT(node))
		return 0;

	var iValue = 0;
	var textValue = fl_getComputedStyle(node, strStylePropertyIe, strStylePropertyDom);
	if (textValue.indexOf('px') == (textValue.length-2))
	{
		textValue = textValue.substr(0,(textValue.length-2));
		var iTmp = parseInt(textValue);
		if (!isNaN(iTmp))
			iValue = iTmp;
	}
	return iValue;
}


/*
function getScrollbarGlobalWidth()
{
	if (typeof(window.s__scrollbarGlobalWidth) == 'undefined') // only do this once
	{
		var outerScrollingDiv = document.createElement('div');
		outerScrollingDiv.style.display = 'block';
		outerScrollingDiv.style.position = 'absolute';
		outerScrollingDiv.style.visibility = 'hidden';
		outerScrollingDiv.style.width = '100px';
		outerScrollingDiv.style.height = '100px';
		outerScrollingDiv.style.border = 'none';
		outerScrollingDiv.style.margin = '0px';
		outerScrollingDiv.style.padding = '0px';
		document.body.appendChild(outerScrollingDiv);

		// Start with no scrollbar
		outerScrollingDiv.style.overflow = 'hidden';

		// Inner content div
		var innerContentDiv = document.createElement('div');
		innerContentDiv.style.width = 'auto';
		innerContentDiv.style.height = '200px';
		innerContentDiv.style.border = 'none';
		innerContentDiv.style.margin = '0px';
		innerContentDiv.style.padding = '0px';
		outerScrollingDiv.appendChild(innerContentDiv);

		// Width of the inner div w/o scrollbar
		var wNoScroll = innerContentDiv.offsetWidth;
		// Add the scrollbar
		outerScrollingDiv.style.overflow = 'auto';
		// Force CSS to recalculate the width=auto.  One way to do this is to turn off/on the element's display.
		innerContentDiv.style.display = 'none';
		innerContentDiv.style.display = 'block';
		// Width of the inner div width scrollbar
		var wScroll = innerContentDiv.offsetWidth;

		// Remove the divs from the doc
		outerScrollingDiv.removeChild(innerContentDiv);
		document.body.removeChild(outerScrollingDiv);

		// Pixel width of the scroller
		window.__scrollbarGlobalWidth = wNoScroll - wScroll;
    }
    return window.__scrollbarGlobalWidth;
}
*/

function fl_getTextLayoutWidth(text, elementToCloneFontStylesFrom)
{
	//	Given "text" and "elementObj" (reference or string ID,) returns how wide in pixels the text would
	//	lay out within that element object, **if there were no auto-wrapping or truncation.**  ("pre"formatted styles OK)

	if (typeof(elementToCloneFontStylesFrom) == 'string')
	{
		elementToCloneFontStylesFrom = document.getElementById(elementObj);
	}
	var width = null;  // return null if not a valid element, so caller doesn't confuse it with a real element of -0-
	if (null !== elementToCloneFontStylesFrom  &&  NODE_TYPE_ELEMENT == elementToCloneFontStylesFrom.nodeType)
	{
		var tempSpan = document.createElement('SPAN');
		tempSpan.style.display = 'inline';
		tempSpan.style.visibility = 'hidden';
		tempSpan.style.position = 'absolute';
		tempSpan.style.overflow = 'visible';
		tempSpan.style.width = 'auto';
		tempSpan.style.height = 'auto';
		tempSpan.style.whiteSpace = 'pre';
		tempSpan.style.fontFamily = fl_getComputedStyle(elementToCloneFontStylesFrom, 'fontFamily', 'font-family');
		tempSpan.style.fontSize = fl_getComputedStyle(elementToCloneFontStylesFrom, 'fontSize', 'font-size');
		tempSpan.style.fontWeight = fl_getComputedStyle(elementToCloneFontStylesFrom, 'fontWeight', 'font-weight');
		tempSpan.style.fontStyle = fl_getComputedStyle(elementToCloneFontStylesFrom, 'fontStyle', 'font-style');
		fl_setNodeText(tempSpan, text);
		document.body.appendChild(tempSpan);
		width = (typeof(tempSpan.offsetWidth) != 'undefined')  ?  tempSpan.offsetWidth  :  0;
		document.body.removeChild(tempSpan);
	}
	return width;
}


function fl_toggleElementDisplay(elementId, bDisplay)
{
	var elementObj = document.getElementById(elementId);
	if (null !== elementObj)
	{
		if (typeof(bDisplay) == 'undefined')
		{
			bDisplay = ('none' == fl_getComputedStyle(elementObj, 'display', 'display'));  // opposite of current state
		}
		if (bDisplay)
			elementObj.style.display = '';
		else
			elementObj.style.display = 'none';
	}
}


function fl_getEventSrcElement(domEvent)
{
	if (typeof(domEvent) == 'undefined'  ||  null === domEvent)
	{
		if (typeof(window.event) != 'undefined')
			domEvent = window.event;
		else
			return null;  //can't do anything.  in non-IE, the caller should have passed the event.
	}
	var srcElement;
	if (typeof(domEvent.target) != 'undefined')			srcElement = domEvent.target;
	else if (typeof(domEvent.srcElement) != 'undefined')	srcElement = domEvent.srcElement;
	if (NODE_TYPE_TEXT == srcElement.nodeType) // for text nodes, Safari erroneously reports the text as the source, instead of the element that owns the text.
		srcElement = srcElement.parentNode;
	return srcElement;
}

/*
function fl_addEventToElement(element, eventName_objSyntax, newFunctionReference)
{
	if (typeof(element[eventName_objSyntax]) != 'undefined'  &&  null !== element[eventName_objSyntax])
	{
		fl_previousResizeFn = element[eventName_objSyntax];
		element[eventName_objSyntax] = function() { fl_previousResizeFn(); newFunctionReference(); };
	}
	else
	{
		element[eventName_objSyntax] = newFunctionReference;
	}
}
*/

function fl_addEventToElement(elementObj, eventType, functionReference, bUseCaptureMozOnly)
{
	if (typeof(bUseCaptureMozOnly) == 'undefined')  bUseCaptureMozOnly = false;

	if (elementObj.addEventListener)  // DOM & Moz
	{
		elementObj.addEventListener(eventType, functionReference, bUseCaptureMozOnly);
	}
	else if (elementObj.attachEvent)  // IE
	{
		return elementObj.attachEvent('on'+eventType, functionReference);
	}
	else  // legacy
	{
		elementObj['on' + eventType] = functionReference;
	}
}



function onDomReady(fnRef)
//	by Stuart Colville -- not sure how reliable
{
	if (/(?!.*?compatible|.*?webkit)^mozilla|opera/i.test(navigator.userAgent)) // Feeling dirty yet?
	{
		document.addEventListener("DOMContentLoaded", fnRef, false);
	}
	else
	{
		window.setTimeout(fnRef,0);
	}
}


function fl_toggleSingleElement(elemId, extraClassNameOn, triggerElemId, triggerExtraClassNameOn)
{
	if (typeof(extraClassNameOn) == 'undefined'  ||  '' === extraClassNameOn)
		return false;

	var elem = document.getElementById(elemId);
	if (null !== elem)
	{
		var triggerElem = null;
		if (typeof(triggerElemId) != 'undefined'  &&  typeof(triggerExtraClassNameOn) != 'undefined')
		{
			var triggerElem = document.getElementById(triggerElemId);
			if (null !== triggerElem)
				triggerElem.blur();
		}

		var bIsCurrentlyOn = false;
		var currentClassNames = elem.className.split(' ');
		for (var i=0; i<currentClassNames.length; i++)
		{
			currentClassNames[i] = Trim(currentClassNames[i]);
			if ('' !== currentClassNames[i]  &&  currentClassNames[i] == extraClassNameOn)
			{
				bIsCurrentlyOn = true;
				currentClassNames[i] = '';
				delete currentClassNames[i];
				break;
			}
		}
		if (bIsCurrentlyOn)
		{
			fl_removeClassFromElement(elem, extraClassNameOn);
			if (null !== triggerElem)
				fl_removeClassFromElement(triggerElem, triggerExtraClassNameOn);
		}
		else
		{
			fl_addClassToElement(elem, extraClassNameOn);
			if (null !== triggerElem)
				fl_addClassToElement(triggerElem, triggerExtraClassNameOn);
		}
	}
}

