/************************************************* (c)2008 by Jennifer Simonds
Some basic JavaScript utility helper functions (not related to DOM processing).

NAMESPACE: gdu

   Date	  Who Changes
--------- --- ----------------------------------------------------------------
28sep2008 jls Created.
28sep2008 jls 
*****************************************************************************/


/*****************************************************************************
We use this as a singleton object.
*****************************************************************************/
if (typeof gdu == "undefined" || !gdu)
	{gdu = new CGDU;
	}

/*****************************************************************************
CONSTRUCTOR.
*****************************************************************************/
function CGDU () {};

/*****************************************************************************
Finds an optional named parameter from an anonymous object.

PARAMETERS:
	aaXP		The associative array with the extra properties.
	sPropname	The name of the property to look for.
	default		The default value to assign.
	
RETURNS:
	various		The property found, else the default.
	
Ex.: this.myprop = getExtraParam (p_oxp, "myprop", false);
*****************************************************************************/
CGDU.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

PARAMETERS:
	object			The function object.
	aaXP			An associative array of extra parameters for the method.

RETURNS:
	The function, wrapped up in a single function object that JavaScript can recognize
	as a callback.
	
Ex.: setTimeout (2000, bindToCallback(onMyTimeout[, {param1:"a", param2:"b"}]));
*****************************************************************************/
CGDU.prototype.bindFuncToCallback = function (p_fcn, p_aaxp)
	{
	// Parameter checks.
	if (!p_fcn)
		{return null;
		}
	
	return (function ()
		{return p_fcn (p_aaxp);
		});
	}


/*****************************************************************************
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

PARAMETERS:
	thisobj			The intended this pointer.
	string			The method function, or the function's name.
	aaXP			An associative array of extra parameters for the method.

RETURNS:
	The method, wrapped up in a single function object that the DOM can recognize
	as an event handler.
	
Event handler: CMyClass.prototype.onMyEvent (p_event [, {param1:"a", param2:"b"}]);
*****************************************************************************/
CGDU.prototype.bindMethodToCallback = function (p_oThis, p_sMethod, p_aaxp)
	{
	// Parameter checks.
	if (!p_oThis || !p_oThis[p_sMethod])
		{return null;
		}
	
	return (function ()
		{return p_oThis[p_sMethod] (p_aaxp);
		});
	}

/****************************************************************************/
CGDU.prototype.bindMethodToEvent = function (p_oThis, p_sMethod, p_aaxp)
	{
	var oMethod;

	// Parameter checks.
	if (!p_oThis || !p_oThis[p_sMethod])
		{return null;
		}
	
	return (function (p_e)
		{
		if (!p_e)
			{p_e = window.event;
			}
		if (window.event)
			{// Try to consistencize some properties.
			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;
				}
			}
		
		return p_oThis[p_sMethod] (p_e, p_aaxp);
		});
	}


/*****************************************************************************
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.)

PARAMETERS:
	object		The object to query.
	string		The name of the object member to look for.

RETURNS:
	true/false	Whether the object contains a member with that name.
*****************************************************************************/
CGDU.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
CGDU.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
CGDU.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).

RETURNS: Array of DOM elements that contain the classname. (Includes elements that
have more than one class declared.)
*****************************************************************************/
CGDU.prototype.getElementsByClass = function (p_sClass)
	{
	var oElems, aReturn, ixE, ixC, elem, 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;
	}


/*****************************************************************************
This gets an element, given either an id string or a reference to an object.

PARAMETERS:
	string | object			The id or the object itself.
	
RETURNS:
	object					The object being referenced.
*****************************************************************************/
CGDU.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;
	}
	

/*****************************************************************************
This gets the currentStyle/getComputedStyle, depending on the browser.

PARAMETERS:
	element					The element whose current style to get.
	
RETURNS:
	object					The readonly style object for the current state
							of the element.
*****************************************************************************/
CGDU.prototype.getComputedStyle = function (p_Elem)
	{
	var oElem;

	oElem = this.getElement (p_Elem);
	
	if (oElem.currentStyle)
		{return oElem.currentStyle;
		}
	else
		{return window.getComputedStyle(oElem, "");
		}
	}
	

/*****************************************************************************
bOK = addClassToElement (id,     class)
bOK = addClassToElement (object, class)

Adds a classname to an element's list of classes, if it's not already there.

RETURNS:
	true if we found the element to alter, else false if it doesn't exist.
*****************************************************************************/
CGDU.prototype.addClassToElement = function (p_obj, p_sClass)
	{
	var elem, ix, aClasses, bHasClass;

	// Get the element being specified.
	if (typeof p_obj == "String")
		{elem = document.getElementById(p_obj);
		}
	else
		{elem = p_obj;
		}
	if (!elem)
		{return false;
		}

	if (!elem.className)
		{elem.className = "";
		}
	aClasses = elem.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)
		{
		elem.className = elem.className + " " + p_sClass;
		return true;
		}
	else
		{return false;
		}
	}


/*****************************************************************************
bOK = removeClassFromElement (id,     class)
bOK = removeClassFromElement (object, class)

Removes a classname from an element's list of classes, if it exists.

RETURNS:
	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.
*****************************************************************************/
CGDU.prototype.removeClassFromElement = function (p_obj, p_sClass)
	{
	var elem, ix, aClasses, bHasClass;

	// Get the element being specified.
	if (typeof p_obj == "String")
		{elem = document.getElementById(p_obj);
		}
	else
		{elem = p_obj;
		}
	if (!elem)
		{return false;
		}

	if (!elem.className)
		{elem.className = "";
		}
	aClasses = elem.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);
			elem.className = aClasses.join(" ");
			return true;
			}
		}

	return false;
	}


/****************************************************************************
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.

PARAMETERS:
	elm				The element to size.
	sFilter			Which child elements to examine: ""=all, or "tagname", or "#id".
	nPadding		How many px padding to add, else omit to use whatever
					padding is being used on the right or top.
****************************************************************************/
CGDU.prototype.sizeWToContent = function (p_elm, p_sFilter, p_nPaddingR)
	{
	var oKid, nKidL, nKidR, nMinL, nMaxR, nParentW;
	
	nMinL = Number.MAX_VALUE;
	nMaxR = 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()))
			{
			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;
		}
		
	p_elm.style.width = nMaxR + p_nPaddingR + "px";
	}

/***************************************************************************/
CGDU.prototype.sizeHToContent = function (p_elm, p_sFilter, p_nPaddingB)
	{
	var oKid, nKidT, nKidB, nMinT, nMaxB, nParentH;
	
	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;
		}
		
	p_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.

PARAMETERS:
	element			The element to size.
****************************************************************************/
CGDU.prototype.sizeElementToContent = function (p_elm)
	{
	var oKid, nKidR, nKidB, nParentW, nParentH;
	
	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;
		}
	
	p_elm.style.width  = nParentW+"px";
	p_elm.style.height = nParentH+"px";
	}
	

/****************************************************************************
Determines if an element is equal to or a descendant of another.

PARAMETERS:
	elm				The element to check.
	elmParent		The possible containing element.
	bEqual=true		Also check if elm==elmParent?
****************************************************************************/
CGDU.prototype.isDescendantOf = function (p_elm, p_elmContainer, p_bEqual)
	{
	var o;
	
	// Optional params.
	if (arguments.length<3)
		{p_bEqual = true;
		}
	// Parameter checks.
	if (!p_elm || !p_elmContainer)
		{return false;
		}
		
	if (p_bEqual && p_elm==p_elmContainer)
		{return true;
		}
	
	o = p_elm.parentNode;
	while (o)
		{
		if (o == p_elmContainer)
			{return true;
			}
		
		o = o.parentNode;
		}
	
	return false;
	}
	

/****************************************************************************
****************************************************************************/
CGDU.prototype.onEvent = function (p_elm, p_sName, p_oFunc, p_bCapture)
	{
	var sName;
	
	// Parameter checks.
	if (arguments.length < 4)
		{p_bCapture = true;
		}
		
	if (window.addEventListener)
		{
		sName = p_sName.replace (/^on(.*)$/i, "$1");
		sName = sName.toLowerCase();
		p_elm.addEventListener (sName, p_oFunc, true);
		}
	else
		{p_elm[p_sName] = p_oFunc;
		}
	}
		

/****************************************************************************
Calculate px/em or em/px.

An em is relative to the ambient font height.
Inspired by http://dtott.com/thoughts/2008/01/19/convert-pixels-to-ems-a-bookmarklet/
****************************************************************************/
CGDU.prototype.calcPxPerEm = function (p_element)
	{
	var div, px;

	div = document.createElement("DIV");
	div.style.height = "1em";
	div.style.position = "absolute";
//	div.style.visibility = "hidden";
	p_element.appendChild (div);
	px = div.offsetHeight;

	p_element.removeChild(div);
	return px;
	}

/****************************************************************************/
CGDU.prototype.calcEmPerPx = function (p_element)
	{
	var nPxPerEm;

	nPxPerEm = calcPxPerEm (p_element);
	return (1/nPxPerEm);
	}
/****************************************************************************/
CGDU.prototype.getOffsetLeftEms = function (p_element)
	{
	return p_element.offsetLeft * calcEmPerPx(p_element);
	}
/****************************************************************************/
CGDU.prototype.getOffsetTopEms = function (p_element)
	{
	return p_element.offsetTop * calcEmPerPx(p_element);
	}
/****************************************************************************/
CGDU.prototype.getOffsetWidthEms = function (p_element)
	{
	return p_element.offsetWidth * calcEmPerPx(p_element);
	}
/****************************************************************************/
CGDU.prototype.getOffsetHeightEms = function (p_element)
	{
	return p_element.offsetHeight * calcEmPerPx(p_element);
	}



/*****************************************************************************
For a given element on the page, walk up its tree of parents that contribute
to its offsetTop or offsetLeft values & find the "true" offset values from
the document origin. (The true offset from the origin of BODY is the sum of 
offsetXXX's of our node and its offsetParents, not including the first ancestor
DIV. Basically this means any enclosing TABLEs do contribute, but enclosing
DIVs do not.)
*****************************************************************************/
CGDU.prototype.getTrueOffsetTop = function (p_oElement)
	{
	var oNode, nOffset;

	if (!p_oElement)
		{return 0;
		}
		
	oNode = p_oElement;
	nOffset = p_oElement.offsetTop;
	oNode = oNode.offsetParent;
	while (oNode && oNode.tagName.toUpperCase()!="DIV")
		{
		nOffset += oNode.offsetTop;
		oNode = oNode.offsetParent;
		}

	return nOffset;
	}
/****************************************************************************/
CGDU.prototype.getTrueOffsetLeft = function (p_oElement)
	{
	var oNode, nOffset;

	if (!p_oElement)
		{return 0;
		}
		
	oNode = p_oElement;
	nOffset = p_oElement.offsetLeft;
	oNode = oNode.offsetParent;
	while (oNode && oNode.tagName.toUpperCase()!="DIV")
		{
		nOffset += oNode.offsetLeft;
		oNode = oNode.offsetParent;
		}

	return nOffset;
	}


/*****************************************************************************
Determines if the specified execCommand command name is available.

PARAMETERS:
	object		The document object to query.
	string		The name of the execCommand cmdname to look for.

RETURNS:
	true/false	Whether you can call execCommand with this command name.
*****************************************************************************/
CGDU.prototype.queryCommandSupported = function (p_oDoc, p_sCmd)
	{
	try
		{
		if (p_oDoc.queryCommandSupported)
			{if (p_oDoc.queryCommandSupported(p_sCmd))
				{return true;
				}
			}
		}
	catch (e)
		{alert ("Exception:\n"+e);
		}
	return false;
	}
/****************************************************************************/
CGDU.prototype.queryCommandEnabled = function (p_oDoc, p_sCmd)
	{
	try
		{
		if (p_oDoc.queryCommandEnabled && p_oDoc.queryCommandEnabled(p_sCmd))
			{return true;
			}
		}
	catch (e)
		{//alert ("Exception:\n"+e);
		}
	return false;
	}


/*****************************************************************************
Builds a string representation of a DOM tree.

PARAMETERS:
	object		The object to query.

RETURNS:
	string		Description of the tree of nodes, starting with p_oTop.
*****************************************************************************/
CGDU.prototype.buildDOMTreeDesc = function (p_o)
	{
	var sMsg, oNode;

	sMsg = "";
	nIndent = 0;

	sMsg = _buildDOMTreeDesc (p_o, nIndent, sMsg);

	return sMsg;
	}


/****************************************************************************/
CGDU.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 += "    ";
		}

	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 = _buildDOMTreeDesc (oKid, p_nIndent+1, sMsg);

		oKid = oKid.nextSibling;
		}

	return sMsg;
	}

