/************************************************************************ (c)2009 by BookRags, Inc.
Basic JS utility functions suitable for all BookRags projects.

NAMESPACE: bru

This creates a singleton object called "bru". After including this file, you refer to these
functions as members of bru.

Ex.:
<script type='text/JavaScript' src='/qa/brutils.1.js'></script>
<script type='text/JavaScript'>
myelement.style.left = bru.getTrueOffsetLeft(myelement) + "px";
</script>

   Date	  Who Major changes
--------- --- -------------------------------------------------------------------------------------
20oct2008 jls Created.
02nov2008 jls Renamed to brutils.1.js. Not bothering with major/minor ver. #s
07jan2009 jls Added buildDOMTreeDesc.
24feb2009 jls Added disableElement from eic project.
06apr2009 jls
24may2009 jls get/setQueryVar.
01nov2009 jls Added form field hints.
01nov2009
**************************************************************************************************/


/**********************************************************************************************//**
We use this as a singleton object.
**************************************************************************************************/
if (typeof bru == "undefined" || !bru)
	{bru = new BookRagsUtils;
	}


/**********************************************************************************************//**
CONSTRUCTOR.
**************************************************************************************************/
function BookRagsUtils ()
	{
	this.spnHintSimple	  = null;
	this.spnHintCharsLeft = null;
	};


/**********************************************************************************************//**
Finds an optional named parameter from an anonymous object.

@param	aaXP		The associative array with the extra properties.
@param	sPropname	The name of the property to look for.
@param	default		The default value to assign.

@return	various		The property found, else the default.

Ex.:
ret = myFunc (1, {myprop=true});

function myFunc (a, p_oxp);
	{
	this.myprop = getExtraParam (p_oxp, "myprop", false);
	...
	}
**************************************************************************************************/
BookRagsUtils.prototype.getExtraParam = function (p_aaXP, p_sPropname, p_default)
	{
	// Parameter checks.
	if (!p_aaXP || !p_sPropname)
		{return p_default;
		}

	if (p_aaXP[p_sPropname]!==undefined)
		{return p_aaXP[p_sPropname];
		}
	else
		{return p_default;
		}
	}


/**********************************************************************************************//**
Uses closures to link a function with params to a generic callback reference.

See: http://www.jibbering.com/faq/faq_notes/closures.html#clObjI

@param	object			The function object.
@param	xp				One extra parameter for the method. For multiple params, use an object.

@return	The function, wrapped up in a single function object that JavaScript can recognize as a callback.

Ex.: setTimeout (2000, bindToCallback(onMyTimeout, "a");
Ex.: setTimeout (2000, bindToCallback(onMyTimeout, {param1:"a", param2:"b"}));
**************************************************************************************************/
BookRagsUtils.prototype.bindFuncToCallback = function (p_fcn, p_xp)
	{
	// Parameter checks.
	if (!p_fcn)
		{return null;
		}

	return (function ()
		{return p_fcn (p_xp);
		});
	}


/**********************************************************************************************//**
Uses closures to link a DOM event to an object's method.

This lets you specify an object method as a callback function. When the callback is invoked, the
method's this pointer will point to its containing object.

See: http://www.jibbering.com/faq/faq_notes/closures.html#clObjI

@param	thisobj		The intended this pointer.
@param	string		The method's name.
@param	xp			One extra parameter for the method. For multiple params, use an object.
					****** BUG: bindMethodToEvent doesn't pass on the xp param. ******

@return		The method, wrapped up in a single function object that the DOM can
			recognize as an event handler.

Ex.: setTimeout (2000, bindMethodToCallback(oMyObject, "timedOut", "a");
Ex.: oMyObject.onblur = bru.bindMethodToEvent(oMyObject, "onLostFocus", "a");
**************************************************************************************************/
BookRagsUtils.prototype.bindMethodToCallback = function (p_oThis, p_sMethod, p_xp)
	{
	// Parameter checks.
	if (!p_oThis || !p_oThis[p_sMethod])
		{return null;
		}

	return (function ()
		{return p_oThis[p_sMethod] (p_xp);
		});
	}

/*************************************************************************************************/
BookRagsUtils.prototype.bindMethodToEvent = function (p_oThis, p_sMethod, p_xp)
	{
	var oMethod;

	// Parameter checks.
	if (!p_oThis || !p_oThis[p_sMethod])
		{return null;
		}

	return (function (p_e, p_xp)
		{
		if (!p_e)
			{p_e = window.event;
			}
		if (window.event)
			{// Try to make some properties more consistent.
			p_e.pageX  = p_e.clientX;
			p_e.pageY  = p_e.clientY;
			p_e.target = p_e.srcElement;
			}
		if (p_e)
			{
			if (p_e.relatedTarget!=undefined)
				{p_e.related = p_e.relatedTarget;
				}
			else if (p_e.toElement!=undefined)
				{p_e.related = p_e.toElement;
				}
			else
				{p_e.related = null;
				}
			}

// BUG: This does not seem to pass on the p_xp.
		return p_oThis[p_sMethod] (p_e, p_xp);
		});
	}


/**********************************************************************************************//**
Determines if an object has a specified member declared. (In JavaScript, if a member isn't declared,
it's not enough to test for "undefined" or "null". It'll just give you a runtime error.)

@param	object		The object to query.
@param	string		The name of the object member to look for.

@return	true/false	Whether the object contains a member with that name.
**************************************************************************************************/
BookRagsUtils.prototype.hasMember = function (p_o, p_sName)
	{
	for (var sName in p_o)
		{if (sName.toUpperCase()==p_sName.toUpperCase())
			{return true;
			}
		}
	return false;
	}


/**********************************************************************************************//**
getClassStyle/setClassStyle

This changes a property of a CSS rule for a className directly in the stylesheet.

See http://www.experts-exchange.com/Web/Web_Languages/JavaScript/Q_22005907.html#a17618590
**************************************************************************************************/
// get the value of a property of a CSS Rule
BookRagsUtils.prototype.getClassStyle = function (sClassName,sProperty) {
  var i, sheets, rules, styleObj;

  sClassName="."+sClassName;
  sheets = document.styleSheets;
  for (i=0;i< sheets.length; i++) {
	rules=sheets[i].cssRules || sheets[i].rules;
	for (var j=0; j<rules.length; j++) {
	  if (rules[j].selectorText && rules[j].selectorText.toUpperCase()==sClassName.toUpperCase()) {
		styleObj=rules[j].style;
		return styleObj[sProperty];
	  }
	}
  }
}

// set the value of a property of a CSS Rule
BookRagsUtils.prototype.setClassStyle = function (sClassName,sProperty,sValue) {
  var i, sheets, rules, styleObj;

  sClassName="."+sClassName;
  sheets = document.styleSheets;
  for (i=0;i< sheets.length; i++) {
	rules=sheets[i].cssRules || sheets[i].rules;
	for (var j=0; j<rules.length; j++) {
	  if (rules[j].selectorText && rules[j].selectorText.toUpperCase()==sClassName.toUpperCase()) {
		styleObj=rules[j].style;
		styleObj[sProperty]=sValue;
		break;
	  }
	}
  }
}


/**********************************************************************************************//**
aElements = getElementsByClass (classname)

This gets all elements in the document that have a specified class (like the DOM should've let you
do all along).

@param		string		The classname whose elements to find.

@return		Array of DOM elements that contain the classname. (Includes elements that
			have more than one class declared.)
**************************************************************************************************/
BookRagsUtils.prototype.getElementsByClass = function (p_sClass)
	{
	var oElems, aReturn, ixE, ixC, el, aClasses, bHasClass;

	oElems = document.getElementsByTagName("*");
	aReturn = [];

	for (ixE=oElems.length-1; ixE>=0; ixE--)
		{
		aClasses = oElems[ixE].className.split(" ");
		bHasClass = false;
		for (ixC=0; ixC<aClasses.length; ixC++)
			{if (aClasses[ixC].toUpperCase()==p_sClass.toUpperCase())
				{// This element contains the class we're looking for.
				bHasClass = true;
				break;
				}
			}

		if (bHasClass)
			{aReturn.push (oElems[ixE]);
			}
		}

	return aReturn;
	}


/**********************************************************************************************//**
Gets an element, given either an id string or a reference to an object itself.

@param	string | object		The id or the object itself.

@return	object				The object being referenced.
**************************************************************************************************/
BookRagsUtils.prototype.getElement = function (p_Elem)
	{
	var oElem;

	oElem = null;
	if (typeof p_Elem=="object")
		{oElem = p_Elem;
		}
	else if (typeof p_Elem=="string")
		{oElem = document.getElementById(p_Elem);
		}
	return oElem;
	}


/**********************************************************************************************//**
Gets the currentStyle/getComputedStyle, depending on the browser.

@param	element			The element (or its ID) whose current style to get.

@return	object			The readonly style object for the current state of the element.
**************************************************************************************************/
BookRagsUtils.prototype.getComputedStyle = function (p_Elem)
	{
	var oElem;

	oElem = this.getElement (p_Elem);

	if (oElem.currentStyle)
		{return oElem.currentStyle;
		}
	else
		{return window.getComputedStyle(oElem, "");
		}
	}


/**********************************************************************************************//**
Adds a CSS class to an element's list of classes if it's not already there.

This does not check that the classname has been defined.

@param	string|object	The element (or its ID) whose style to modify.
@param	string			The CSS classname to add.

@return			true if we found the element to alter, else false if it doesn't exist.
**************************************************************************************************/
BookRagsUtils.prototype.addClassToElement = function (p_obj, p_sClass)
	{
	var el, ix, aClasses, bHasClass;

	// Get the element being specified.
	el = this.getElement (p_obj);
	if (!el)
		{return false;
		}

	if (!el.className)
		{el.className = "";
		}
	aClasses = el.className.split(" ");
	bHasClass = false;
	for (ix=0; ix<aClasses.length; ix++)
		{if (aClasses[ix].toUpperCase()==p_sClass.toUpperCase())
			{// This element contains the class we're looking for.
			bHasClass = true;
			break;
			}
		}

	if (!bHasClass)
		{
		el.className = el.className + " " + p_sClass;
		return true;
		}
	else
		{return false;
		}
	}


/**********************************************************************************************//**
Removes a CSS class from an element's list of classes.

@param	string|object	The element (or its ID) whose style to modify.
@param	string			The CSS classname to remove.

@return			true if we found the element and it contained the class we wanted to remove,
				else false if it doesn't exist or it didn't contain the class.
**************************************************************************************************/
BookRagsUtils.prototype.removeClassFromElement = function (p_obj, p_sClass)
	{
	var el, ix, aClasses, bHasClass;

	// Get the element being specified.
	el = this.getElement (p_obj);
	if (!el)
		{return false;
		}

	if (!el.className)
		{el.className = "";
		}
	aClasses = el.className.split(" ");
	bHasClass = false;
	for (ix=aClasses.length-1; ix>=0; ix--)
		{if (aClasses[ix].toUpperCase()==p_sClass.toUpperCase())
			{// This element contains the class we're looking for.
			aClasses.splice (ix, 1);
			el.className = aClasses.join(" ");
			return true;
			}
		}

	return false;
	}


/**********************************************************************************************//**
This gets the value of a query variable in a URL, or null if it's not found.

@param	string			The uri whose query variable to get.
@param	string			The name of the query variable to look for.
@param	string=null		The default value to return if not found.

@return string			The value of the query variable, or p_sDefault if not found.
**************************************************************************************************/
BookRagsUtils.prototype.getQueryVar = function (p_uri, p_sVar, p_sDefault)
	{
	var aParts1, sAnchor, aParts2, aPairs, aPair, ix;

	if (arguments.length < 3)
		{p_sDefault = null;
		}

	aParts1 = p_uri.split ("#");
	sAnchor = aParts1[1];

	aParts2 = aParts1[0].split ("?");
	if (!aParts2[1])
		{return p_sDefault;
		}

	aPairs = aParts2[1].split("&");
	for (ix=0; ix<aPairs.length; ix++)
		{
		aPair = aPairs[ix].split("=");
		if (aPair[0]==p_sVar)
			{return aPair[1];
			}
		}
	return p_sDefault;
	}


/**********************************************************************************************//**
This sets the value of a query variable in a URL. If not found then it creates one.

@param	string			The uri whose query variable to set.
@param	string			The name of the query variable to look for.
@param	string			The value to set it to.
**************************************************************************************************/
BookRagsUtils.prototype.setQueryVar = function (p_uri, p_sVar, p_sValue)
	{
	var aParts1, sAnchor, aParts2, aPairs, ix, urlRet;

	aParts1 = p_uri.split ("#");
	sAnchor = aParts1[1];

	aParts2 = aParts1[0].split ("?");
	if (!aParts2[1])
		{// No query vars. Just append ours.
		urlRet = aParts2[0] + "?" + p_sVar + "=" + p_sValue;
		urlRet += sAnchor ? ("#"+sAnchor) : "";
		return urlRet;
		}

	aPairs = aParts2[1].split("&");
	bFound = false;
	for (ix=0; ix<aPairs.length; ix++)
		{
		if (aPairs[ix].search("^"+p_sVar+"=")!=-1)
			{
			aPairs[ix] = p_sVar + "=" + encodeURIComponent(p_sValue);
			bFound = true;
			break;
			}
		}

	if (!bFound)
		{aPairs.push (p_sVar + "=" + encodeURIComponent(p_sValue));
		}

	urlRet = aParts2[0];
	if (aPairs.length)
		{urlRet += "?" + aPairs.join("&");
		}
	if (sAnchor)
		{urlRet += "#" + sAnchor;
		}

	return urlRet;
	}



/**********************************************************************************************//**
Calculate the width & height of the window's viewport (client area).

See Javascript the Definitive Guide, 14.3.1

@return	integer			The window's width/height, or -1 if it couldn't be calculated.
**************************************************************************************************/
BookRagsUtils.prototype.getWindowW = function ()
	{
	if (window.innerWidth) // All browsers but IE
		{return window.innerWidth;
		}
	else if (document.documentElement && document.documentElement.clientWidth) // IE6 w/DOCTYPE
		{return document.documentElement.clientWidth;
		}
	else if (document.body.clientWidth) // IE4-6 without a DOCTYPE
		{return document.body.clientWidth;
		}
	else
		{return -1;
		}
	}
/*************************************************************************************************/
BookRagsUtils.prototype.getWindowH = function ()
	{
	if (window.innerWidth) // All browsers but IE
		{return window.innerHeight;
		}
	else if (document.documentElement && document.documentElement.clientWidth) // IE6 w/DOCTYPE
		{return document.documentElement.clientHeight;
		}
	else if (document.body.clientWidth) // IE4-6 without a DOCTYPE
		{return document.body.clientHeight;
		}
	else
		{return -1;
		}
	}


/**********************************************************************************************//**
Calculate the window's H & V scroll positions. This is the pixel position in the document that is
at the top/left of the viewport.

See Javascript the Definitive Guide, 14.3.1

@return	integer			The window's horizontal/vertical scroll value, or -1 if it couldn't be calculated.
**************************************************************************************************/
BookRagsUtils.prototype.getWindowScrollX = function ()
	{
	if (window.innerWidth) // All browsers but IE
		{return window.pageXOffset;
		}
	else if (document.documentElement && document.documentElement.clientWidth) // IE6 w/DOCTYPE
		{return document.documentElement.scrollLeft;
		}
	else if (document.body.clientWidth) // IE4-6 without a DOCTYPE
		{return document.body.scrollLeft;
		}
	else
		{return -1;
		}
	}
/*************************************************************************************************/
BookRagsUtils.prototype.getWindowScrollY = function ()
	{
	if (window.innerWidth) // All browsers but IE
		{return window.pageYOffset;
		}
	else if (document.documentElement && document.documentElement.clientWidth) // IE6 w/DOCTYPE
		{return document.documentElement.scrollTop;
		}
	else if (document.body.clientWidth) // IE4-6 without a DOCTYPE
		{return document.body.scrollTop;
		}
	else
		{return -1;
		}
	}



/**********************************************************************************************//**
For a given element on the page, find its true offsetTop/offsetLeft. IOW, get the value to use to
place an absolutely positioned element in order to put it at the same spot that the specified
element is at.

offsetTop & offsetLeft don't give you the correct values to use to position a static element. To
find the true offsets for a static element, we must walk up its tree of offsetParents until we come
to a positioned element & tally up the offsetLeft's/offsetTop's ourselves, and also take into
account the offsetParents' scroll positions.

See Javascript the Definitive Guide, 16.2.3

@param	string|object	The element (or its ID) whose true position to get.
@return	integer			The offset value.
**************************************************************************************************/
BookRagsUtils.prototype.getTrueOffsetTop = function (p_el)
	{
	var nOffset, el;

	// Get the element being specified.
	p_el = this.getElement (p_el);
	if (!p_el)
		{return 0;
		}

//	if (this.getComputedStyle(p_el).position.toLowerCase() == 'static')
		{// A static element's true offsetParent is reported as its closest TD or positioned anc.,
		//  but for determining where to place an absolutely positioned el. we need the offset from
		//  the nearest positioned anc.
		nOffset = p_el.offsetTop;
		el = p_el.offsetParent;
		while (el != null
		&&	  this.getComputedStyle(el).position.toLowerCase()!='relative'
		&&	  this.getComputedStyle(el).position.toLowerCase()!='absolute'
		)
			{
			nOffset += el.offsetTop;
			el = el.offsetParent;
			}
		}
//	else
//		{nOffset = this.offsetTop;
//		}

	// Compensate for an ancestor being scrolled.
	for (el=p_el.parentNode; el!=null && el!=document.body; el=el.parentNode)
		{if (el.scrollTop)
			{nOffset -= el.scrollTop;
			}
		}

	return nOffset;
	}
/*************************************************************************************************/
BookRagsUtils.prototype.getTrueOffsetLeft = function (p_el)
	{
	var nOffset, el;

	// Get the element being specified.
	p_el = this.getElement (p_el);
	if (!p_el)
		{return 0;
		}

//	if (this.getComputedStyle(p_el).position.toLowerCase() == 'static')
		{// A static element's true offsetParent is reported as its closest TD or positioned anc.,
		//  but for determining where to place an absolutely positioned el. we need the offset from
		//  the nearest positioned anc.
		nOffset = p_el.offsetLeft;
		el = p_el.offsetParent;
		while (el != null
		&&	  this.getComputedStyle(el).position.toLowerCase()!='relative'
		&&	  this.getComputedStyle(el).position.toLowerCase()!='absolute'
		)
			{
			nOffset += el.offsetLeft;
			el = el.offsetParent;
			}
		}
//	else
//		{nOffset = this.offsetLeft;
//		}

	// Compensate for an ancestor being scrolled.
	for (el=p_el.parentNode; el!=null && el!=document.body; el=el.parentNode)
		{if (el.scrollLeft)
			{nOffset -= el.scrollLeft;
			}
		}

	return nOffset;
	}


/**********************************************************************************************//**
Sets an element's width or height to fully enclose its contents as currently
laid out.

This does a shallow search of the immediate child elements. This should normally be sufficient,
since elements usually physically enclose their children. (Except of course for absolutely
positioned items.)

@param	string|object	The element (or its ID) to size.
@param	sFilter			Which child elements to examine: ""=all, or "tagname", or "#id".
@param	nPadding		How many px padding to add to the width/height, else omit to use whatever
						padding is being used on the left or top.
**************************************************************************************************/
BookRagsUtils.prototype.sizeWToContent = function (p_elm, p_sFilter, p_nPaddingR)
	{
	var elm, oKid, nKidL, nKidR, nMinL, nMaxR, nParentW;

	// Get the element being specified.
	elm = this.getElement (p_elm);
	if (!elm)
		{return false;
		}

	nMinL = Number.MAX_VALUE;
	nMaxR = 0;
	oKid = elm.firstChild;
	while (oKid)
		{
		if (p_sFilter==""
		|| (p_sFilter.slice(0,1)=="#" && oKid.id==p_sFilter.slice(1))
		|| (oKid.tagName.toUpperCase()==p_sFilter.toUpperCase()))
			{
			nKidL = getTrueOffsetLeft(oKid);
			nKidR = nKidL + oKid.offsetWidth;
			if (nKidR > nMaxR)
				{nMaxR = nKidR;
				}
			if (nKidL < nMinL)
				{nMinL = nKidL;
				}
			}

		oKid = oKid.nextSibling;
		}

	// Optional params.
	if (arguments.length < 3)
		{p_nPaddingR = nMinL;
		}

	elm.style.width = nMaxR + p_nPaddingR + "px";
	}

/*************************************************************************************************/
BookRagsUtils.prototype.sizeHToContent = function (p_elm, p_sFilter, p_nPaddingB)
	{
	var elm, oKid, nKidT, nKidB, nMinT, nMaxB, nParentH;

	// Get the element being specified.
	elm = this.getElement (p_elm);
	if (!elm)
		{return false;
		}

	nMinT = Number.MAX_VALUE;
	nMaxB = 0;
	oKid = p_elm.firstChild;
	while (oKid)
		{
		if (p_sFilter==""
		|| (p_sFilter.slice(0,1)=="#" && oKid.id==p_sFilter.slice(1))
		|| (oKid.tagName.toUpperCase()==p_sFilter.toUpperCase()))
			{
			nKidT = getTrueOffsetTop(oKid);
			nKidB = nKidT + oKid.offsetHeight;
			if (nKidB > nMaxB)
				{nMaxB = nKidB;
				}
			if (nKidT < nMinT)
				{nMinT = nKidT;
				}
			}

		oKid = oKid.nextSibling;
		}

	// Optional params.
	if (arguments.length < 3)
		{p_nPaddingB = nMinT;
		}

	elm.style.height = nMaxB + p_nPaddingB + "px";
	}


/**********************************************************************************************//**
Sets an element's width & height to fully enclose its contents as currently laid out.

This does a shallow search of the immediate child elements.

@param	string|object	The element (or its ID) to size.
**************************************************************************************************/
BookRagsUtils.prototype.sizeElementToContent = function (p_elm)
	{
	var elm, oKid, nKidR, nKidB, nParentW, nParentH;

	// Get the element being specified.
	elm = this.getElement (p_obj);
	if (!elm)
		{return false;
		}

	nParentW = 0;
	nParentH = 0;
	oKid = p_elm.firstChild;
	while (oKid)
		{
		nKidR = getTrueOffsetLeft(oKid) + oKid.offsetWidth;
		nKidB = getTrueOffsetTop(oKid) + oKid.offsetHeight;

		if (nKidR > nParentW)
			{nParentW = nKidR;
			}
		if (nKidB > nParentH)
			{nParentH = nKidB;
			}

		oKid = oKid.nextSibling;
		}

	elm.style.width  = nParentW+"px";
	elm.style.height = nParentH+"px";
	}


/**********************************************************************************************//**
Determines if an email address is valid.

Uses the regex from EIC's main_script.js.

@param	string		The email address to validate.
**************************************************************************************************/
BookRagsUtils.prototype.isEmailValid = function (p_sAddr)
	{
	var rxEmail;

	rxEmail = /^[0-9a-zA-Z_\.-]+\@[0-9a-zA-Z_\.-]+\.[0-9a-zA-Z_\.-]+$/;

	return rxEmail.test(p_sAddr);
	}


/**********************************************************************************************//**
Determines if an web address is valid.

See Regular Expression Pocket Reference, pg. 16.

@param	string		The email address to validate.
**************************************************************************************************/
BookRagsUtils.prototype.isURLValid = function (p_url)
	{
	var rxScheme, rxDomain, rxPort, rxPath, rxURL;

	rxScheme = '(?:https?:\/\/)?';
	rxDomain = '(?:[0-9a-zA-Z][-\w]*[0-9a-zA-Z]*\.)+[a-zA-Z]{2,9}';
	rxPort	 = '(?:\:\d{1,4})?';
	rxPath	 = '(?:\/[-\w\/#~:.?+=&%@~]*)*';
	rxURL	 = new RegExp("^" + rxScheme + rxDomain + rxPort + rxPath + "$");

	return rxURL.test(p_url);
	}

/**********************************************************************************************//**
Builds a string representation of a DOM tree.

@param	object		The object to query.

@return	string		Description of the tree of nodes, starting with p_oTop.
**************************************************************************************************/
BookRagsUtils.prototype.buildDOMTreeDesc = function (p_o)
	{
	var sMsg, oNode;

	sMsg = "";
	nIndent = 0;

	sMsg = this._buildDOMTreeDesc (p_o, nIndent, sMsg);

	return sMsg;
	}


/*************************************************************************************************/
BookRagsUtils.prototype._buildDOMTreeDesc = function (p_o, p_nIndent, p_sMsg)
	{
	var aNodeTypes, sIndent, sNodeDesc, oKid, sMsg;

	sMsg = p_sMsg;

	// Describe this node.
	sIndent = "";
	for (ix=0; ix<p_nIndent; ix++)
		{sIndent += "    ";
		}

	if (p_o===undefined)
		{sNodeDesc = "node is undefined";
		}
	else if (p_o===null)
		{sNodeDesc = "node is null";
		}
	else
		{
		switch (p_o.nodeType)
			{
		case 1:  /* Element */		sNodeDesc = "<"+p_o.tagName+">"; break;
		case 2:  /* Attribute */	sNodeDesc = p_o.nodeName+"="+p_o.nodeValue; break;
		case 3:  /* Text */			sNodeDesc = "#text: ("+p_o.nodeValue+")"; break //p_o.nodeValue.length+" chars)"; break;
		case 4:  /* CDATA */		sNodeDesc = "#cdata-section: ("+p_o.nodeValue.length+" chars)"; break;
		case 5:  /* Entity ref. */	sNodeDesc = "Entity ref: "+p_o.nodeName; break;
		case 6:  /* Entity */		sNodeDesc = "Entity: "+p_o.nodeName; break;
		case 7:  /* PI */			sNodeDesc = "PI: "+p_o.nodeName+" "+p_o.nodeValue; break;
		case 8:  /* Comment */		sNodeDesc = "#comment: "+p_o.nodeValue; break;
		case 9:  /* Document */		sNodeDesc = "#document"; break;
		case 10: /* Doc type */		sNodeDesc = "document type: "+p_o.nodeName; break;
		case 11: /* Doc fragment */	sNodeDesc = "#document-fragment"; break;
		case 12: /* Notation */		sNodeDesc = "notation: "+p_oNodeName; break;
		default: /* Unk. type */	sNodeDesc = "unknown type "+p_o.nodeType; break;
			}
		}

	sMsg += sIndent + sNodeDesc + "\n";

	// Describe each of our children.
	oKid = p_o.firstChild;
	while (oKid)
		{
		sMsg = this._buildDOMTreeDesc (oKid, p_nIndent+1, sMsg);

		oKid = oKid.nextSibling;
		}

	return sMsg;
	}


/**************************************************************************************************
Form element disabler.

Disables a form element & puts a light gray mask over it. Earlier version used in EIC to disable the
[Save] button until the user_form is finished loading.

@param	el				The DOM element (or its ID) to disable/enable.
@param	bDisable		True to mask & gray out the element, else false to re-enable it.
**************************************************************************************************/
BookRagsUtils.prototype.disableElement = function (p_el, p_bDisable) {
	var el, elMask;

	// Sanity checks.
	el = bru.getElement (p_el);
	if (!el) {
		return;
	}

	// Optional params.
	if (p_bDisable===undefined) {p_bDisable = true;}

	if (bru.hasMember(el,"disabled")) {
		el.disabled = p_bDisable;
	}

	// Create the element's mask if it doesn't already exist.
	if (!el.hasAttribute("disablemask")) {
		elMask = document.createElement("div");
		elMask.id = 'disablemask-'+Math.floor(Math.random()*1000000);
		el.setAttribute('disablemask', elMask.id);
		elMask.onmousedown = disableOnClick;
		elMask.onmouseup   = disableOnClick;
		elMask.onclick	   = disableOnClick;
		elMask.style.visibility		 = "hidden";
		elMask.style.position		 = "absolute";
		elMask.style.backgroundColor = "white";
		elMask.style.opacity		 = .7;
		elMask.style.mozOpacity		 = .7;
		elMask.style.filter			 = "alpha(opacity=70)";
		el.parentNode.insertBefore (elMask, el.nextSibling);
	}

	elMask = document.getElementById(el.getAttribute("disablemask"));
	if (p_bDisable) {
		elMask.style.left	= bru.getTrueOffsetLeft(el) + "px";
		elMask.style.top	= bru.getTrueOffsetTop(el) + "px";
		elMask.style.width  = el.offsetWidth + "px";
		elMask.style.height = el.offsetHeight + "px";

		elMask.style.visibility = "visible";
	} else {
		elMask.style.visibility = "hidden";
	}
}

function disableOnClick (e) {
	var e = window.event ? window.event : e;
	e.stopPropagation ();
}


/**************************************************************************************************
Form field hints.

These functions create a piece of text that displays next to a form field when it gets the focus, then
disappears when focus leaves.

SIMPLE HINT
-----------
This displays a static text next to the form field.

Insert this html code somewhere suitable. (It will be moved up to just below the BODY.)
In $(document).ready, call bru.hintSimple for each field needing a static hint.

<span id='bru_spnFormHintSimple' class='form_hint' style='position:absolute;display:none'></span>

CHARACTERS LEFT
---------------
This displays "Characters left: nnn" next to a text input field.

Insert this html code somewhere suitable. (It will be moved up to just below the BODY.)
In $(document).ready, call bru.hintCharsLeft for each field with a max input length.

<span id='bru_spnFormHintCharsLeft' class='form_hint' style='position:absolute;display:none'>Characters Left: <span id='bru_spnFormHintCharsLeftValue'></span></span>

Ex.
---
$(document).ready(function()
	{
	bru.hintCharsLeft (myfrm.edName,'right',myfrm.edMyField.maxLength);
	bru.hintCharsLeft (myfrm.edPW,'right');
	bru.hintSimple	  (myfrm.edEmail,'right','You\'ll have to validate this email address');
	}

***************************************************************************************************/
BookRagsUtils.prototype.initFormHintSimple = function ()
	{// Make sure popup is outside any inner positioned DIVs. (else they'll get clipped in IE)
	this.spnHintSimple = document.getElementById('bru_spnFormHintSimple');
	if ($(this.spnHintSimple).parent()[0].tagName.toUpperCase()!='BODY')
		{$(this.spnHintSimple).insertBefore ($('body > *:first'));
		}
	$(this.spnHintSimple).css ('display',   'none');
	$(this.spnHintSimple).css ('position',  'absolute');
	$(this.spnHintSimple).css ('font-size', '11px');
	}

BookRagsUtils.prototype.hintSimple = function (p_el, p_eWhere, p_sText)
	{
	if (!this.spnHintSimple)
		{this.initFormHintSimple ();
		}
	$(p_el).focus(function() {bru.showFormHintSimple(p_el,p_eWhere,p_sText); });
	$(p_el).keyup(function() {bru.showFormHintSimple(p_el,p_eWhere,p_sText); });
	$(p_el).blur (function() {bru.hideFormHintSimple(p_el); });
	}

BookRagsUtils.prototype.showFormHintSimple = function (p_el, p_eWhere, p_sText)
	{
	$(this.spnHintSimple).html (p_sText);
	$(this.spnHintSimple).css ('left', (($(p_el).offset().left + p_el.offsetWidth + 6) + 'px'));
	$(this.spnHintSimple).css ('top',  $(p_el).offset().top + 'px');
	$(this.spnHintSimple).css ('display', 'block');
	}

BookRagsUtils.prototype.hideFormHintSimple = function ()
	{$(this.spnHintSimple).css('display', 'none');
	}

BookRagsUtils.prototype.initFormHintCharsLeft = function ()
	{// Make sure popup is outside any inner positioned DIVs. (else they'll get clipped in IE)
	this.spnHintCharsLeft	   = document.getElementById('bru_spnFormHintCharsLeft');
	this.spnHintCharsLeftValue = document.getElementById('bru_spnFormHintCharsLeftValue');
	if ($(this.spnHintCharsLeft).parent()[0].tagName.toUpperCase()!='BODY')
		{$(this.spnHintCharsLeft).insertBefore ($('body > *:first'));
		}
	$(this.spnHintCharsLeft).css ('display',   'none');
	$(this.spnHintCharsLeft).css ('position',  'absolute');
	$(this.spnHintCharsLeft).css ('font-size', '11px');
	}

BookRagsUtils.prototype.hintCharsLeft = function (p_el, p_eWhere, p_nMaxLen)
	{
	if (!this.spnHintCharsLeft)
		{this.initFormHintCharsLeft ();
		}
	$(p_el).focus(function() {bru.showFormHintCharsLeft(p_el,p_eWhere,p_nMaxLen); });
	$(p_el).keyup(function() {bru.showFormHintCharsLeft(p_el,p_eWhere,p_nMaxLen); });
	$(p_el).blur (function() {bru.hideFormHintCharsLeft(p_el); });
	}

BookRagsUtils.prototype.showFormHintCharsLeft = function (p_el, p_eWhere, p_nMaxLen)
	{
	var nMaxLen = (arguments.length > 2) ? p_nMaxLen : p_el.maxLength;

	$(this.spnHintCharsLeft).css ('left', (($(p_el).offset().left + p_el.offsetWidth + 6) + 'px'));
	$(this.spnHintCharsLeft).css ('top',  $(p_el).offset().top + 'px');
	$(this.spnHintCharsLeftValue).html (nMaxLen-p_el.value.length);
	$(this.spnHintCharsLeft).css('display', 'block');
	}

BookRagsUtils.prototype.hideFormHintCharsLeft = function ()
	{$(this.spnHintCharsLeft).css('display', 'none');
	}

