if (typeof Gmenu == 'undefined') {

  Gmenu = function() {};
}

Gmenu.getPath = function() {
  // Get last script element
  var objContainer = document.body;
  if (!objContainer) {
    objContainer = document.getElementsByTagName('head')[0];
    if (!objContainer) {
      objContainer = document;
    }
  }
  var objScript = objContainer.lastChild;
  // Get path
  var strSrc = objScript.getAttribute('src');
  if (!strSrc) {
    return '';
  }
  var arrTokens = strSrc.split('/');
  // Remove last token
  arrTokens = arrTokens.slice(0, -1);
  if (!arrTokens.length) {
    return '';
  }
  return arrTokens.join('/') + '/';
};


Gmenu.include = function(strSrc) {
  document.write('<s' + 'cript type="text/javascript" src="' + strSrc +
   '"></s' + 'cript>');
};


Gmenu.GmenuPath = Gmenu.getPath();

// Include required scripts
// This can be defined in other modules
if (typeof Gmenu == 'undefined') {
  /// define the global Gmenu namespace
  Gmenu = {};
}

/// define the Utils namespace
Gmenu.Utils = {};

/// Retrieves the absolute position (relative to <body>) of a given element.
///
/// If it doesn't work in IE 6, try this:
/// \code
/// setTimeout(function() {
///   var objPos = Gmenu.Utils.getAbsolutePos(objElement);
///   do something with objPos
/// }, 0);
/// \endcode
///
/// @param el [HTMLElement] reference to the element.
/// @return [object] { x, y } containing the position.
Gmenu.Utils.getAbsolutePos = function(el, scrollOff) {
	var SL = 0, ST = 0;
	if (!scrollOff) {
		var is_div = /^div$/i.test(el.tagName);
		if (is_div && el.scrollLeft)
			SL = el.scrollLeft;
		if (is_div && el.scrollTop)
			ST = el.scrollTop;
	}
	var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
	if (el.offsetParent) {
		var tmp = this.getAbsolutePos(el.offsetParent);
		r.x += tmp.x;
		r.y += tmp.y;
	}
	return r;
};

/// Modify the position of a box to fit in browser's view.  This function will
/// modify the passed object itself, so it doesn't need to return a value.
///
/// @param [object] box { x, y, width, height } specifying the area.
Gmenu.Utils.fixBoxPosition = function(box) {
	if (box.x < 0)
		box.x = 0;
	if (box.y < 0)
		box.y = 0;
	var cp = Gmenu.Utils.createElement("div");
	var s = cp.style;
	s.position = "absolute";
	s.right = s.bottom = s.width = s.height = "0px";
	window.document.body.appendChild(cp);
	var br = Gmenu.Utils.getAbsolutePos(cp);
	window.document.body.removeChild(cp);
	if (Gmenu.is_ie) {
		br.y += window.document.body.scrollTop;
		br.x += window.document.body.scrollLeft;
	} else {
		br.y += window.scrollY;
		br.x += window.scrollX;
	}
	var tmp = box.x + box.width - br.x;
	if (tmp > 0) box.x -= tmp;
	tmp = box.y + box.height - br.y;
	if (tmp > 0) box.y -= tmp;
};

/// Determines if an event is related to a certain element.  This is a poor
/// substitute for some events that are missing from DOM since forever (like
/// onenter, onleave, which MSIE provides).  Basically onmouseover and
/// onmouseout are fired even if the mouse was already in the element but moved
/// from text to a blank area, so in order not to close a popup element when
/// onmouseout occurs in this situation, one would need to first check if the
/// event is not related to that popup element:
///
/// \code
///      function handler_onMouseOut(event) {
///         if (!Gmenu.Utils.isRelated(this, event)) {
///            /// can safely hide it now
///            this.style.display = "none";
///         }
///      }
/// \endcode
///
/// @param el [HTMLElement] reference to the element to check the event against
/// @param evt [Event] reference to the Event object
/// @return [boolean] true if the event is related to the element
Gmenu.Utils.isRelated = function (el, evt) {
	evt || (evt = window.event);
	var related = evt.relatedTarget;
	if (!related) {
		var type = evt.type;
		if (type == "mouseover") {
			related = evt.fromElement;
		} else if (type == "mouseout") {
			related = evt.toElement;
		}
	}
	try {
		while (related) {
			if (related == el) {
				return true;
			}
			related = related.parentNode;
		}
	} catch(e) {};
	return false;
};

/// Remove a certain [CSS] class from the given element.
/// @param el [HTMLElement] reference to the element.
/// @param className [string] the class to remove.
Gmenu.Utils.removeClass = function(el, className) {
	if (!(el && el.className)) {
		return;
	}
	var cls = el.className.split(" ");
	var ar = [];
	for (var i = cls.length; i > 0;) {
		if (cls[--i] != className) {
			ar[ar.length] = cls[i];
		}
	}
	el.className = ar.join(" ");
};

/// Appends a certain [CSS] class to the given element.
/// @param el [HTMLElement] reference to the element.
/// @param className [string] the class to append.
Gmenu.Utils.addClass = function(el, className) {
	Gmenu.Utils.removeClass(el, className);
	el.className += " " + className;
};

/// Retrieves the current target element for some event (useful when bubbling).
/// This function is not actually very useful, but it's legacy from the old calendar code.
/// @param ev [Event] the event object.
/// @return [HTMLElement] window.event.srcElement for MSIE, ev.currentTarget for other browsers.
Gmenu.Utils.getElement = function(ev) {
	if (Gmenu.is_ie) {
		return window.event.srcElement;
	} else {
		return ev.currentTarget;
	}
};

/// Retrieves the target element for some event (useful when bubbling).
/// This function is not actually very useful, but it's legacy from the old calendar code.
/// @param ev [Event] the event object.
/// @return [HTMLElement] window.event.srcElement for MSIE, ev.target for other browsers.
Gmenu.Utils.getTargetElement = function(ev) {
	if (Gmenu.is_ie) {
		return window.event.srcElement;
	} else {
		return ev.target;
	}
};

/**
 * Returns mouse position during the event.
 *
 * @param {object} objEvent Optional. Event object
 * @return Mouse position during the event:
 * <pre>
 * {
 *   pageX: [number] x coordinate relative to the document,
 *   pageY: [number] y coordinate relative to the document,
 *   clientX: [number] x coordinate relative to the window,
 *   clientY: [number] y coordinate relative to the window
 * }
 * </pre>
 * @type object
 */
Gmenu.Utils.getMousePos = function(objEvent) {
  var objPos = {
    pageX: 0,
    pageY: 0,
    clientX: 0,
    clientY: 0
  };
  var boolIsPageX = (typeof objEvent.pageX != 'undefined');
  var boolIsClientX = (typeof objEvent.clientX != 'undefined');
  objEvent || (objEvent = window.event);
  if (objEvent && (boolIsPageX || boolIsClientX)) {
    if (boolIsPageX) {
      objPos.pageX = objEvent.pageX;
      objPos.pageY = objEvent.pageY;
    } else {
      objPos.pageX = objEvent.clientX + Gmenu.Utils.getPageScrollX();
      objPos.pageY = objEvent.clientY + Gmenu.Utils.getPageScrollY();
    }
    if (boolIsClientX) {
      objPos.clientX = objEvent.clientX;
      objPos.clientY = objEvent.clientY;
    } else {
      objPos.clientX = objEvent.pageX - Gmenu.Utils.getPageScrollX();
      objPos.clientY = objEvent.pageY - Gmenu.Utils.getPageScrollY();
    }
  }
  return objPos;
};

/// Stops bubbling and propagation of some event.
/// @param ev [Event] the event object
/// @return false
Gmenu.Utils.stopEvent = function(ev) {
	ev || (ev = window.event);
	if (ev) {
		if (Gmenu.is_ie) {
			ev.cancelBubble = true;
			ev.returnValue = false;
		} else {
			ev.preventDefault();
			ev.stopPropagation();
		}
	}
	return false;
};

Gmenu.Utils.removeOnUnload = [];
/// Adds an event handler to a certain element.  This function adds a handler
/// using the DOM2 addEventListener (or attachEvent for MSIE).  Doing this
/// means that you can add multiple handlers for the same element and same
/// event name, and they will be called in order.
///
/// WARNING: for really old browsers that don't support attachEvent nor
/// addEventListener, it falls back to the default way: el.onclick = func.
/// This means that you CANNOT add multiple handlers in those browsers, as a
/// new one will override the old one.
///
/// @param el [HTMLElement] reference to the element.
/// @param evname [string] the event name, excluding the "on" prefix.
/// @param func event handler function.
/// @param useCapture [boolean, optional] default: false, see
/// http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget-addEventListener
/// for details
Gmenu.Utils.addEvent = function(el, evname, func, useCapture) {
	if (el.addEventListener) { // Gecko / W3C
	  if (!useCapture) {
	    useCapture = false;
	  }
		el.addEventListener(evname, func, useCapture);
	} else if (el.attachEvent) { // IE
		el.attachEvent("on" + evname, func);
		if (useCapture) {el.setCapture(false);}
	} else {
		el["on" + evname] = func;
	}
	Gmenu.Utils.removeOnUnload.push({"element" : el, "event" : evname, "listener" : func});
};

/// Removes an event handler added with Gmenu.Utils.addEvent().  The
/// prototype scheme is the same.
Gmenu.Utils.removeEvent = function(el, evname, func) {
	if (el.detachEvent) { // IE
		el.detachEvent("on" + evname, func);
	} else if (el.removeEventListener) { // Gecko / W3C
		el.removeEventListener(evname, func, false);
	} else {
		el["on" + evname] = null;
	}
};

/// Create an element of a certain type using document.createElement().  A
/// function was needed in order to add some common attributes to all created
/// elements, but also in order to be able to use it in XHTML too (Gecko and
/// other W3C-compliant browsers).
///
/// This function will create an element of the given type and set certain
/// properties to it: unselectable for IE, and the CSS "-moz-user-select" for
/// Gecko, in order to make the element unselectable in these browsers.
/// Optionally, if the second argument is passed, it will appendChild() the
/// newly created element to its parent.
///
/// @param type [string] the tag name of the new element.
/// @param parent [HTMLElement, optional] a parent for the new element.
/// @param selectable [boolean] the flag to indicate wether element is selectable(rather usefull).
/// @return [HTMLElement] reference to the new element.
Gmenu.Utils.createElement = function(type, parent, selectable) {
	var el = null;
	if (window.self.document.createElementNS)
		// use the XHTML namespace; IE won't normally get here unless
		// _they_ "fix" the DOM2 implementation.
		el = window.self.document.createElementNS("http://www.w3.org/1999/xhtml", type);
	else
		el = window.self.document.createElement(type);
	if (typeof parent != "undefined" &&parent != null)
		parent.appendChild(el);
	if (!selectable) {
		if (Gmenu.is_ie)
			el.setAttribute("unselectable", true);
		if (Gmenu.is_gecko)
			el.style.setProperty("-moz-user-select", "none", "");
	}
	return el;
};

// Cookie management

/// Sets a cooke given certain specifications.  It overrides any existing
/// cookie with the same name.
///
/// @param name [string] the cookie name.
/// @param value [string] the cookie value.
/// @param domain [string, optional] the cookie domain.
/// @param path [string, optional] the cookie path.
/// @param exp_days [number, optional] number of days of cookie validity.
Gmenu.Utils.writeCookie = function(name, value, domain, path, exp_days) {
	value = escape(value);
	var ck = name + "=" + value, exp;
	if (domain)
		ck += ";domain=" + domain;
	if (path)
		ck += ";path=" + path;
	if (exp_days) {
		exp = new Date();
		exp.setTime(exp_days * 86400000 + exp.getTime());
		ck += ";expires=" + exp.toGMTString();
	}
	document.cookie = ck;
};

/**
 * Retrieves the value of a cookie.
 *
 * @param name [string] the cookie name
 * @return [string or null] a string with the cookie value, or null if it can't be found.
 */

/* ? inside regular expression is not supported in IE 5.0
Gmenu.Utils.getCookie = function(name) {
	var re = new RegExp("(^|;\\s*)" + name + "\\s*=(.*?)(;|$)");
	if (re.test(document.cookie)) {
		var value = RegExp.$2;
		value = unescape(value);
		return (value);
	}
	return null;
};
*/

Gmenu.Utils.getCookie = function(name) {
	var pattern = name + "=";
	var tokenPos = 0;
	while (tokenPos < document.cookie.length) {
		var valuePos = tokenPos + pattern.length;
		if (document.cookie.substring(tokenPos, valuePos) == pattern) {
			var endValuePos = document.cookie.indexOf(";", valuePos);
			if (endValuePos == -1) { // Last cookie
				endValuePos = document.cookie.length;
			}
			return unescape(document.cookie.substring(valuePos, endValuePos));
		}
		tokenPos=document.cookie.indexOf(" ",tokenPos)+1;
		if (tokenPos == 0) { // No more tokens
			break;
		}
	}
	return null;
};

/**
 * Given an object, create a string suitable for saving the object in a cookie.
 * This is similar to serialization.  WARNING: it does not support nested
 * objects.
 *
 * @param obj [Object] reference to the object to serialize.
 * @return [string] the serialized object.
 */
Gmenu.Utils.makePref = function(obj) {
	function stringify(val) {
		if (typeof val == "object" && !val)
			return "null";
		else if (typeof val == "number" || typeof val == "boolean")
			return val;
		else if (typeof val == "string")
			return '"' + val.replace(/\22/, "\\22") + '"';
		else return null;
	};
	var txt = "", i;
	for (i in obj)
		txt += (txt ? ",'" : "'") + i + "':" + stringify(obj[i]);
	return txt;
};

/**
 * The reverse of Gmenu.Utils.makePref(), this function unserializes the
 * given string and creates an object from it.
 *
 * @param txt [string] the serialized value.
 * @return [Object] a new object if it was created successfully or null otherwise.
 */
Gmenu.Utils.loadPref = function(txt) {
	var obj = null;
	try {
		eval("obj={" + txt + "}");
	} catch(e) {}
	return obj;
};

/**
 * Merges the values of the source object into the destination object.
 *
 * @param dest [Object] the destination object.
 * @param src [Object] the source object.
 */
Gmenu.Utils.mergeObjects = function(dest, src) {
	for (var i in src)
		dest[i] = src[i];
};

// based on the WCH idea
// http://www.aplus.co.yu/WCH/code3/WCH.js

/// \defgroup WCH functions
//@{

Gmenu.Utils.__wch_id = 0;	/**< [number, static] used to create ID-s for the WCH objects */

/**
 * Create an WCH object.  This function does nothing if the browser is not
 * IE5.5 or IE6.0.  A WCH object is one of the most bizarre tricks to avoid a
 * notorious IE bug: IE normally shows "windowed controls" on top of any HTML
 * elements, regardless of any z-index that might be specified in CSS.  This
 * technique is described at: http://www.aplus.co.yu/WCH/
 *
 * A "WCH object" is actually an HTMLIFrame element having a certain "CSS
 * filter" (proprietary MSIE extension) that forces opacity zero.  This object,
 * displayed on top of a windowed control such as a select box, will completely
 * hide the select box, allowing us to place other HTMLElement objects above.
 *
 * WCH stands for "Windowed Controls Hider".
 *
 * @param element [HTMLElement, optional] -- Create the WCH IFRAME inside this.
 *
 *
 * @return [HTMLIFrame or null] a new WCH object if the browser is "supported", null otherwise.
 */
Gmenu.Utils.createWCH = function(element) {
	var f = null;
	element = element || window.self.document.body;
	if (Gmenu.is_ie && !Gmenu.is_ie5) {
		var filter = 'filter:progid:DXImageTransform.Microsoft.alpha(style=0,opacity=0);';
		var id = "WCH" + (++Gmenu.Utils.__wch_id);
		element.insertAdjacentHTML
			('beforeEnd', '<iframe id="' + id + '" scroll="no" frameborder="0" ' +
			 'style="z-index:0;position:absolute;visibility:hidden;' + filter +
			 'border:0;top:0;left:0;width:0;height:0;" ' +
			 'src="javascript:false;"></iframe>');
		f = window.self.document.getElementById(id);
	}
	return f;
};

/**
 * Configure a given WCH object to be displayed on top of the given element.
 * Optionally, a second element can be passed, and in this case it will setup
 * the WCH object to cover both elements.
 *
 * @param f [HTMLIFrame] the WCH object
 * @param el [HTMLElement] the element to cover.
 * @param el2 [HTMLElement, optional] another element to cover.
 */
Gmenu.Utils.setupWCH_el = function(f, el, el2) {
	if (f) {
		var pos = Gmenu.Utils.getAbsolutePos(el),
			X1 = pos.x,
			Y1 = pos.y,
			X2 = X1 + el.offsetWidth,
			Y2 = Y1 + el.offsetHeight;
		if (el2) {
			var p2 = Gmenu.Utils.getAbsolutePos(el2),
				XX1 = p2.x,
				YY1 = p2.y,
				XX2 = XX1 + el2.offsetWidth,
				YY2 = YY1 + el2.offsetHeight;
			if (X1 > XX1)
				X1 = XX1;
			if (Y1 > YY1)
				Y1 = YY1;
			if (X2 < XX2)
				X2 = XX2;
			if (Y2 < YY2)
				Y2 = YY2;
		}
		Gmenu.Utils.setupWCH(f, X1, Y1, X2-X1, Y2-Y1);
	}
};

/**
 * Configure a WCH object to cover a certain part of the screen.
 *
 * @param f [HTMLIFrame] the WCH object.
 * @param x [number] the X coordinate.
 * @param y [number] the Y coordinate.
 * @param w [number] the width of the area.
 * @param h [number] the height of the area.
 */
Gmenu.Utils.setupWCH = function(f, x, y, w, h) {
	if (f) {
		var s = f.style;
		(typeof x != "undefined") && (s.left = x + "px");
		(typeof y != "undefined") && (s.top = y + "px");
		(typeof w != "undefined") && (s.width = w + "px");
		(typeof h != "undefined") && (s.height = h + "px");
		s.visibility = "inherit";
	}
};

/**
 * Hide a WCH object.
 *
 * @param f [HTMLIFrame] object to hide.
 */
Gmenu.Utils.hideWCH = function(f) {
	if (f)
		f.style.visibility = "hidden";
};

//@}

/// \defgroup Scroll-with-window functions
//@{

/**
 * A generic Utils function that returns the current scroll position.
 *
 */
Gmenu.Utils.getPageScrollY = function() {
	return window.pageYOffset ||
			document.documentElement.scrollTop ||
			(document.body ? document.body.scrollTop : 0) ||
			0;
};

/**
 * A generic Utils function that returns the current scroll position.
 *
 */
Gmenu.Utils.getPageScrollX = function() {
	return window.pageXOffset ||
			document.documentElement.scrollLeft ||
			(document.body ? document.body.scrollLeft : 0) ||
			0;
};

// Object setup.
Gmenu.ScrollWithWindow = {};
Gmenu.ScrollWithWindow.list = [];
// Set to a number between 0 and 1, lower means longer scrolling.
Gmenu.ScrollWithWindow.stickiness = 0.25;

/**
 * Registers a given object to have its style.top set equal to the window
 * scroll position as the browser scrolls.
 *
 * @param node [HTMLElement] -- a reference to the node to scroll.
 */
Gmenu.ScrollWithWindow.register = function(node) {
	var top = node.offsetTop || 0;
	var left = node.offsetLeft || 0;
	node.parentNode.style.position = "relative";
	var scrollY = Gmenu.Utils.getPageScrollY();
	var scrollX = Gmenu.Utils.getPageScrollX();
	top -= scrollY;
	left -= scrollX;
	Gmenu.ScrollWithWindow.list[Gmenu.ScrollWithWindow.list.length] = {
		node: node,
		origTop: top,
		origleft: left
	};
};

/**
 * Unregisters a given object.
 *
 * @param node [HTMLElement] -- a reference to the node to scroll.
 */
Gmenu.ScrollWithWindow.unregister = function(node) {
	for (var count = 0; count < Gmenu.ScrollWithWindow.list.length; count++) {
		var elm = Gmenu.ScrollWithWindow.list[count];
		if (node == elm.node) {
			Gmenu.ScrollWithWindow.list.splice(count, 1);
			return;
		}
	}
};

/**
 * \internal Called each time the window is scrolled to set objects' positions.
 *
 * @param newScrollY [number] -- the new window scroll position.
 * @param direction [string] - "vertical"/"horizontal" controlls which scrolling we are handling
 */
Gmenu.ScrollWithWindow.handler = function(newScroll, direction) {
	// Move oldScrollY towards newScrollY, evening up if the difference is small.
	if (direction == "vertical") {
		var newScrollY = newScroll;
		oldScrollY += ((newScrollY - oldScrollY) * this.stickiness);
		if (Math.abs(oldScrollY - newScrollY) <= 1) oldScrollY = newScrollY;
	} else {
		var newScrollX = newScroll;
		oldScrollX += ((newScrollX - oldScrollX) * this.stickiness);
		if (Math.abs(oldScrollX - newScrollX) <= 1) oldScrollX = newScrollX;
	}
	for (var count = 0; count < Gmenu.ScrollWithWindow.list.length; count++) {
		var elm = Gmenu.ScrollWithWindow.list[count];
		var node = elm.node;
		node.style.position = 'absolute';
		if (!elm.origTop && elm.origTop !== 0 && (direction == "vertical")) {
			elm.origTop = parseInt(node.style.top) || 0;
		}
		if (!elm.origLeft && elm.origLeft !== 0 && (direction == "horizontal")) {
			elm.origLeft = parseInt(node.style.left) || 0;
		}
		if (direction == "vertical") {
			node.style.top = (elm.origTop + parseInt(oldScrollY)) + 'px';
		} else {
			node.style.left = (elm.origLeft + parseInt(oldScrollX)) + 'px';
		}
	}
};

// Processed scroll position & Event hook.
var oldScrollY = Gmenu.Utils.getPageScrollY();
var oldScrollX = Gmenu.Utils.getPageScrollX();
setInterval(
	'var newScrollY = Gmenu.Utils.getPageScrollY(); ' +
	'var newScrollX = Gmenu.Utils.getPageScrollX(); ' +
	'if (newScrollY != oldScrollY) { ' +
		'Gmenu.ScrollWithWindow.handler(newScrollY, "vertical"); ' +
	'}' +
	'if (newScrollX != oldScrollX) { ' +
		'Gmenu.ScrollWithWindow.handler(newScrollX, "horizontal"); ' +
	'}', 50);

//@}

/**
 * Destroys the given element (remove it from the DOM tree) if it's not null
 * and it's parent is not null.
 *
 * @param el [HTMLElement] the element to destroy.
 */
Gmenu.Utils.destroy = function(el) {
	if (el && el.parentNode)
		el.parentNode.removeChild(el);
};

/**
 * Opens a new window at a certain URL and having some properties.
 *
 * @param url [string] the URL to open a new window to.
 * @param windowName [string] the name of the new window (as for target attribute).
 * @param width [number] the width of the new window in pixels.
 * @param height [number] the height of the new window in pixels.
 * @param scrollbars [string] "yes" or "no" for scrollbars.
 *
 * @return [object] the new window
 */
Gmenu.Utils.newCenteredWindow = function(url, windowName, width, height, scrollbars){
	var leftPosition = 0;
	var topPosition = 0;
	if (screen.width)
		leftPosition = (screen.width -  width)/2;
	if (screen.height)
		topPosition = (screen.height -  height)/2;
	var winArgs =
		'height=' + height +
		',width=' + width +
		',top=' + topPosition +
		',left=' + leftPosition +
		',scrollbars=' + scrollbars +
		',resizable';
	var win = window.open(url,windowName,winArgs);
	return win;
};

/**
 * Finds the size of the current web page. This is the usable size
 * and does not include the browser's menu and buttons.
 *
 * @return [object] dimension with the height and width of the window
 */
Gmenu.Utils.getWindowSize = function() {
	var iWidth = 0;
	var iHeight = 0;

	if (document.compatMode && document.compatMode == 'CSS1Compat') {
	    // Standards-compliant mode
		if (window.opera) {
			iWidth = document.body.clientWidth || 0;
			iHeight = document.body.clientHeight || 0;
		} else {
			iWidth = document.documentElement.clientWidth || 0;
			iHeight = document.documentElement.clientHeight || 0;
		}
	} else {
	    // Non standards-compliant mode
		iWidth = window.innerWidth || document.body.clientWidth ||
			document.documentElement.clientWidth || 0;
		iHeight = window.innerHeight || document.body.clientHeight ||
			document.documentElement.clientHeight || 0;
	}

	return {
		width: iWidth,
		height: iHeight
	};
};


/**
 * Given a reference to a select element, this function will select the option
 * having the given value and optionally will call the default handler for
 * "onchange".
 *
 * @param sel [HTMLSelectElement] reference to the SELECT element.
 * @param val [string] the value that we should select.
 * @param call_default [boolean] true if the default onchange should be called.
 */
Gmenu.Utils.selectOption = function(sel, val, call_default) {
	var a = sel.options, i, o;
	for (i = a.length; --i >= 0;) {
		o = a[i];
		o.selected = (o.val == val);
	}
	sel.value = val;
	if (call_default) {
		if (typeof sel.onchange == "function")
			sel.onchange();
		else if (typeof sel.onchange == "string")
			eval(sel.onchange);
	}
};

/**
 * A more flexible way to get the "nextSibling" of a given element.  If the
 * "tag" argument is passed, then this function will return the next sibling
 * that has a certain tag.  Otherwise it will simply return el.nextSibling.
 *
 * @param el [HTMLElement] reference to the anchor element.
 * @param tag [string] the tag name of the returned node.
 * @param alternateTag [string] the alternate tag name of the returned node.
 *
 * @return [HTMLElement or null] el.nextSibling if tag is not passed, or the
 * first element after el having the specified tag.  Null is returned if no
 * element could be found.
 */
Gmenu.Utils.getNextSibling = function(el, tag, alternateTag) {
	el = el.nextSibling;
	if (!tag) {
		return el;
	}
	tag = tag.toLowerCase();
	if (alternateTag) alternateTag = alternateTag.toLowerCase();
	while (el) {
		if (el.nodeType == 1 && (el.tagName.toLowerCase() == tag ||
		 (alternateTag && el.tagName.toLowerCase() == alternateTag))) {
			return el;
		}
		el = el.nextSibling;
	}
	return el;
};

/**
 * Similar to Gmenu.Utils.getNextSibling(), this function will return the
 * first child of the given element that has a specified tag.
 *
 * @param el [HTMLElement] reference to the anchor element.
 * @param tag [string] the tag name of the returned node.
 * @param alternateTag [string] the alternate tag name of the returned node.
 *
 * @return [HTMLElement] reference to the found node, or null if none could be
 * found.
 */
Gmenu.Utils.getFirstChild = function(el, tag, alternateTag) {
  if (!el) {
    return null;
  }
	el = el.firstChild;
  if (!el) {
    return null;
  }
	if (!tag) {
		return el;
	}
	tag = tag.toLowerCase();
	if (el.nodeType == 1) {
		if (el.tagName.toLowerCase() == tag) {
			return el;
		} else if (alternateTag) {
			alternateTag = alternateTag.toLowerCase();
			if (el.tagName.toLowerCase() == alternateTag) {
				return el;
			}
		}
	}
	return Gmenu.Utils.getNextSibling(el, tag, alternateTag);
};

/**
 * Function that concatenates and returns all text child nodes of the
 * specified node.
 *
 * @param objNode [Node] -- reference to the node.
 * @return [string] -- concatenated text child nodes
 */
Gmenu.Utils.getChildText = function(objNode) {
	if (objNode == null) {
		return '';
	}
	var arrText = [];
	var objChild = objNode.firstChild;
	while (objChild != null) {
		if (objChild.nodeType == 3) { // Node.TEXT_NODE
			arrText.push(objChild.data);
		}
		objChild = objChild.nextSibling;
	}
	return arrText.join(' ');
};

/**
 * Similar to the DOM's built in insertBefore.
 * Insert a node after an existing node.
 *
 * @param el [oldNode] The existing element
 * @param el [newNode] the new element to insert after the old one.
 *
 */
Gmenu.Utils.insertAfter = function(oldNode, newNode) {
	if(oldNode.nextSibling) {
		oldNode.parentNode.insertBefore(newNode, oldNode.nextSibling);
	} else {
		oldNode.parentNode.appendChild(newNode);
	}
}

Gmenu.Utils._ids = {};	/**< [number, static] maintains a list of generated IDs */

/**
 * Generates an unique ID, for a certain code (let's say "class").  If the
 * optional "id" argument is passed, then it just returns the id for that code
 * (no generation).  This function is sometimes useful when we need to create
 * elements and be able to access them later by ID.
 *
 * @param code [string] the class of ids.  User defined, can be anything.
 * @param id [string, optional] specify if the ID is already known.
 *
 * @return [string] the unique ID
 */
Gmenu.Utils.generateID = function(code, id) {
	if (typeof id == "undefined") {
		if (typeof this._ids[code] == "undefined")
			this._ids[code] = 0;
		id = ++this._ids[code];
	}
	return "Gmenu-" + code + "-" + id;
};

/**
*  Add a tooltip to the specified element.
*
*  Function that adds a custom tooltip for an element.  The "target" is the
*  element to where the tooltip should be added to, and the "tooltip" is a DIV
*  that contains the tooltip text.  Optionally, the tooltip DIV can have the
*  "title" attribute set; if so, its value will be displayed highlighted as
*  the title of the tooltip.
*
*  @param target  reference to or ID of the target element
*  @param tooltip reference to or ID of the tooltip content element
*/

Gmenu.Utils.addTooltip = function(target, tooltip) {
return new Gmenu.Tooltip(target, tooltip);
};

Gmenu.isLite=true;

Gmenu.Utils.checkActivation = function() {
	if (!Gmenu.isLite)	return true;

	var arrProducts=[]

	add_product=function(script, webdir_in, name_in)
	{
	arrProducts[script]={webdir:webdir_in, name:name_in, bActive:false}
	}

	add_product('calendar.js', 'prod1',   'Calendar')
	add_product('zpmenu.js',   'prod2',   'Menu')
	add_product('tree.js',     'prod3',   'Tree')
	add_product('form.js',     'forms',   'Forms')
	add_product('effects.js',  'effects', 'Effects')
	add_product('hoverer.js',  'effects', 'Effects - Hoverer')
	add_product('slideshow.js','effects', 'Effects - Slidshow')
	add_product('zpgrid.js',   'grid',    'Grid')
	add_product('slider.js',   'slider',  'Slider')
	add_product('zptabs.js',   'tabs',    'Tabs')
	add_product('zptime.js',   'time',    'Time')
	add_product('window.js',   'windows', 'Window')


	var strName, arrName, i
	var bProduct=false // Flag yes if we have a Gmenu script
	var scripts = document.getElementsByTagName('script');
	for (i=0; i<scripts.length; i++)
	{
		// If wizard then do NOT do link back check, which makes wizard err out
		if (/wizard.js/i.test(scripts[i].src))
			return true

		arrName=scripts[i].src.split('/')
		if (arrName.length==0)
			strName=scripts[i]
		else
			strName=arrName[arrName.length-1]
		strName=strName.toLowerCase()
		// Get each active product
		if (typeof arrProducts[strName] != 'undefined')
			{
			bProduct=true
			arrProducts[strName].bActive=true
			}
	}

	// Is a LITE product even being used?
	/*if (!bProduct) return true;


	var anchors = document.getElementsByTagName('A');
	for(i = 0; i < anchors.length; i++)
		if (/(dev|www)\.Gmenu\.com/i.test(anchors[i].href))
			return true;

	var strMsg='You are using the Free version of the Gmenu Software.\n'+
	'While using the Free version, a link to www.Gmenu.com in this page is required.'

	for (i in arrProducts)
		// Get each active product
		if (arrProducts[i].bActive==true)
			strMsg+='\nTo purchase the Gmenu ' + arrProducts[i].name + ' visit www.Gmenu.com/website/main/products/' + arrProducts[i].webdir + '/'

	alert(strMsg)*/

	return false;
}

// Browser sniffing

/// detect Opera browser
Gmenu.is_opera = /opera/i.test(navigator.userAgent);

/// detect a special case of "web browser"
Gmenu.is_ie = ( /msie/i.test(navigator.userAgent) && !Gmenu.is_opera );

/// detect IE5.0/Win
Gmenu.is_ie5 = ( Gmenu.is_ie && /msie 5\.0/i.test(navigator.userAgent) );

/// detect IE for Macintosh
Gmenu.is_mac_ie = ( /msie.*mac/i.test(navigator.userAgent) && !Gmenu.is_opera );

/// detect KHTML-based browsers
Gmenu.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent);

/// detect Konqueror
Gmenu.is_konqueror = /Konqueror/i.test(navigator.userAgent);

/// detect Gecko
Gmenu.is_gecko = /Gecko/i.test(navigator.userAgent);

/**
 * Simulation of Object hasOwnProperty() method that is missing in IE 5.0.
 */
if (!Object.prototype.hasOwnProperty) {
  Object.prototype.hasOwnProperty = function(strProperty) {
    try {
      var objPrototype = this.constructor.prototype;
      while (objPrototype) {
        if (objPrototype[strProperty] == this[strProperty]) {
          return false;
        }
        objPrototype = objPrototype.prototype;
      }
    } catch (objException) {}
    return true;
  };
}

/**
 * Simulation of Function call() method that is missing in IE 5.0.
 */
if (!Function.prototype.call) {
	Function.prototype.call = function() {
		var objThis = arguments[0];
		objThis._this_func = this;
		var arrArgs = [];
		for (var iArg = 1; iArg < arguments.length; iArg++) {
			arrArgs[arrArgs.length] = 'arguments[' + iArg + ']';
		}
		var ret = eval('objThis._this_func(' + arrArgs.join(',') + ')');
		objThis._this_func = null;
		return ret;
	};
}

/**
 * Simulation of Function apply() method that is missing in IE 5.0.
 */
if (!Function.prototype.apply) {
	Function.prototype.apply = function() {
		var objThis = arguments[0];
		var objArgs = arguments[1];
		objThis._this_func = this;
		var arrArgs = [];
		if (objArgs) {
			for (var iArg = 0; iArg < objArgs.length; iArg++) {
				arrArgs[arrArgs.length] = 'objArgs[' + iArg + ']';
			}
		}
		var ret = eval('objThis._this_func(' + arrArgs.join(',') + ')');
		objThis._this_func = null;
		return ret;
	};
}

/**
 * Simulation of Array pop() method that is missing in IE 5.0.
 */
if (!Array.prototype.pop) {
	Array.prototype.pop = function() {
		var last;
		if (this.length) {
			last = this[this.length - 1];
			this.length -= 1;
		}
		return last;
	};
}

/**
 * Simulation of Array push() method that is missing in IE 5.0
 */
if (!Array.prototype.push) {
	Array.prototype.push = function() {
		for (var i = 0; i < arguments.length; i++) {
			this[this.length] = arguments[i];
		}
		return this.length;
	};
}

/**
 * Simulation of Array shift() method that is missing in IE 5.0.
 */
if (!Array.prototype.shift) {
	Array.prototype.shift = function() {
		var first;
		if (this.length) {
			first = this[0];
			for (var i = 0; i < this.length - 1; i++) {
				this[i] = this[i + 1];
			}
			this.length -= 1;
		}
		return first;
	};
}

/**
 * Simulation of Array unshift() method that is missing in IE 5.0.
 */
if (!Array.prototype.unshift) {
	Array.prototype.unshift = function() {
		if (arguments.length) {
			var i, len = arguments.length;
			for (i = this.length + len - 1; i >= len; i--) {
				this[i] = this[i - len];
			}
			for (i = 0; i < len; i++) {
				this[i] = arguments[i];
			}
		}
		return this.length;
	};
}

/**
 * Simulation of Array splice() method that is missing in IE 5.0.
 */
if (!Array.prototype.splice) {
	Array.prototype.splice = function(index, howMany) {
		var elements = [], removed = [], i;
		for (i = 2; i < arguments.length; i++) {
			elements.push(arguments[i]);
		}
		for (i = index; (i < index + howMany) && (i < this.length); i++) {
			removed.push(this[i]);
		}
		for (i = index + howMany; i < this.length; i++) {
			this[i - howMany] = this[i];
		}
		this.length -= removed.length;
		for (i = this.length + elements.length - 1; i >= index + elements.length;
		 i--) {
			this[i] = this[i - elements.length];
		}
		for (i = 0; i < elements.length; i++) {
			this[index + i] = elements[i];
		}
		return removed;
	};
}

/**
 * Crossbrowser replacement for Array indexOf() method. See:
 * http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
 *
 * @param {object} arr Array to search in
 * @param {any} searchElement Element to locate in the array
 * @param {any} fromIndex The index at which to begin the search
 * @return The first index at which a given element can be found in the array,
 * or -1 if it is not present
 * @type number
 */
Gmenu.Utils.arrIndexOf = function(arr, searchElement, fromIndex) {
  if (Array.prototype.indexOf) {
    return arr.indexOf(searchElement, fromIndex);
  }
  if (!fromIndex) {
    fromIndex = 0;
  }
  for (var iElement = fromIndex; iElement < arr.length; iElement++) {
    if (arr[iElement] == searchElement) {
      return iElement;
    }
  }
  return -1;
};

/**
 * Displays error message. Override this if needed.
 *
 * \param objArgs [number] error object:
 * {
 *   severity: [string, optional] error severity,
 *   description: [string] human readable error description
 * }
 */
Gmenu.Log = function(objArgs) {
  // Check arguments
  if (!objArgs) {
    return;
  }
  // Form error message
  var strMessage = objArgs.description;
  if (objArgs.severity) {
    strMessage = objArgs.severity + ':\n' + strMessage;
  }
  // Display error message
  alert(strMessage);
};

/// Gmenu.Utils.Array object which contains additional for arrays method
Gmenu.Utils.Array = {};

/**
 * Inserts the element into array.
 * It influences the order in which the elements will be iterated in the for...in cycle.
 *
 * @param arr [array] array to work with.
 * @param el [mixed] element to insert.
 * @param key [string] element to insert.
 * @param nextKey [string] element to be inserted before.
 * @return [string] new Array.
 */
Gmenu.Utils.Array.insertBefore = function (arr, el, key, nextKey) {
	var tmp = new Array();
	for(var i in arr) {
		if (i == nextKey) {
			if (key) {
				tmp[key] = el;
			} else {
				tmp.push(el);
			}
		}
		tmp[i] = arr[i];
	}
	return tmp;
}

/**
 * Extends one class with another. Gives ability to access properties and
 * methods of the superclass having the same names as in subclass.
 *
 * Gmenu.Widget specific feature: If superclass has static property "path",
 * subclass will get path to js file from which this function is called into
 * "path" property instead of value from superclass.
 *
 * Should be used as follows:
 *
 * // Define SuperClass and its methods
 * Gmenu.SuperClass = function(objArgs) {
 *   ...
 * };
 *
 * Gmenu.SuperClass.prototype.init = function(objArgs) {
 *   ...
 * };
 *
 * // Define SubClass and its methods
 * Gmenu.SubClass = function(objArgs) {
 *   ...
 *   // Call constructor of superclass
 *   Gmenu.SubClass.SUPERconstructor.call(this, objArgs);
 *   ...
 * };
 *
 * // Inherit SuperClass
 * Gmenu.inherit(Gmenu.SubClass, Gmenu.SuperClass);
 *
 * Gmenu.SubClass.prototype.init = function(objArgs) {
 *   ...
 *   // Call method of superclass
 *   Gmenu.SubClass.SUPERclass.init.call(this, objArgs);
 *   ...
 * };
 *
 * Note: This function should not be called from another function. It must be
 * invoked during page load to determine path to js file from which it is called
 * correctly.
 *
 * \param objSubClass [object] inheriting class.
 * \param objSuperClass [object] class from which to inherit.
 */
Gmenu.inherit = function(objSubClass, objSuperClass) {
  // Duplicate prototype of superclass
  var Inheritance = function() {};
  Inheritance.prototype = objSuperClass.prototype;
  objSubClass.prototype = new Inheritance();
  // Fix constructor property to point to the self constructor because it is
  // pointing to the nested Inheritance function
  objSubClass.prototype.constructor = objSubClass;
  // Reference to constructor of superclass to be able to invoke it
  objSubClass.SUPERconstructor = objSuperClass;
  // Reference to prototype of superclass to be able to invoke its methods
  objSubClass.SUPERclass = objSuperClass.prototype;
  // Path to js file from which this function is called
  if (typeof objSuperClass.path != 'undefined') {
    objSubClass.path = Gmenu.getPath();
  }
};

/**
 * Returns path from the last loaded script element. Splits src attribute value
 * and returns path without js file name.
 *
 * Note: This function should not be called from another function. It must be
 * invoked during page load to determine path to js file from which it is called
 * correctly.
 *
 * @return Path to the script, e.g. '../src/' or '' if path is not found
 * @type string
 */
Gmenu.getPath = function() {
  if(Gmenu.lastLoadedModule != null){
    return Gmenu.lastLoadedModule;	
  }

  // Get last script element
  var objContainer = document.body;
  if (!objContainer) {
    objContainer = document.getElementsByTagName('head')[0];
    if (!objContainer) {
      objContainer = document;
    }
  }

  var objScript = objContainer.lastChild;

  if(
    objScript.nodeType != 1 ||
    objScript.nodeName.toLowerCase() != 'script'
  ){
  	return "";
  }

  // Get path
  var strSrc = objScript.getAttribute('src');
  if (!strSrc) {
    return '';
  }
  var arrTokens = strSrc.split('/');
  // Remove last token
  arrTokens = arrTokens.slice(0, -1);
  if (!arrTokens.length) {
    return '';
  }
  return arrTokens.join('/') + '/';
};

/**
 * This is some kind of indicator that window has already loaded.
 */
Gmenu.windowLoaded = typeof(document.readyState) != 'undefined' ?
	(
		document.readyState == 'loaded' || // Konqueror
		document.readyState == 'complete' // IE/Opera
	) :
	// Mozilla
	document.getElementsByTagName != null && typeof(document.getElementsByTagName('body')[0]) != 'undefined'
;

Gmenu.Utils.addEvent(window, "load", function() {Gmenu.windowLoaded = true;});

/*
 * Use this method to display your custom message before unload event.
 * For example you could warn user that he has some unsaved changes
 * on page.
 *
 * \param msg [string] - message to display. Default value - "All your changes will be lost.":
 * \param win [object] - reference to window object. By default - current window
 */
Gmenu.Utils.warnUnload = function(msg, win){
    Gmenu.Utils.warnUnloadFlag = true;
	if(typeof(msg) != "string"){
		msg = "All your changes will be lost.";
	}

	if(typeof(win) == 'undefined'){
		win = window;
	}

	Gmenu.Utils.addEvent(win, 'beforeunload', function(ev){
		if(Gmenu.Utils.warnUnloadFlag != true){
			return true;
		}

		if(typeof(ev) == 'undefined'){
			ev = window.event;
	    }

		ev.returnValue = msg;
		
		return false;
	});
}

/*
 * Using this method you can remove displaying of your message on page unload.
 */
Gmenu.Utils.unwarnUnload = function(msg, win){
    Gmenu.Utils.warnUnloadFlag = false;
}

/*
 * \internal Variable that determines if unload handler should be used.
 */
Gmenu.Utils.warnUnloadFlag = false;

/**
 * @return Max z-index value
 * @type number
 */
Gmenu.Utils.getMaxZindex = function() {
  if (window.opera || Gmenu.is_khtml) {
    return 2147483583;
  } else if (Gmenu.is_ie){
    return 2147483647;
  } else {
    return 10737418239;
  }
};

/**
 * Corrects CSS length.
 *
 * @param {any} val Value to correct
 * @return Valid CSS length
 * @type string
 */
Gmenu.Utils.correctCssLength = function(val) {
  if (typeof val == 'undefined' || (typeof val == 'object' && !val)) {
    // Undefined or null
    return 'auto';
  }
  // Convert to string
  val += '';
  if (!val.length) {
    // Empty
    return 'auto';
  }
  if (/\d$/.test(val)) {
    // Number
    val += 'px';
  }
  return val;
};

/**
 * @ignore Holds properties of DOM objects that must be set to null on window
 * unload event to prevent memory leaks in IE.
 */
Gmenu.Utils.destroyOnUnload = [];

/**
 * Saves a property that must be set to null on window unload event. Should be
 * used for properties that can't be destroyed by garbage collector due to
 * circular references.
 *
 * @param {object} objElement DOM object
 * @param {string} strProperty Property name
 */
Gmenu.Utils.addDestroyOnUnload = function(objElement, strProperty) {
  Gmenu.Utils.destroyOnUnload.push([objElement, strProperty]);
};

/**
 * Assigns a value to a custom property of DOM object. This property will be
 * set to null on window unload event. Use this function to create properties
 * that can't be destroyed by garbage collector due to circular references.
 *
 * @param {object} objElement DOM object
 * @param {string} strProperty Property name
 * @param {any} val Property value
 */
Gmenu.Utils.createProperty = function(objElement, strProperty, val) {
  objElement[strProperty] = val;
  Gmenu.Utils.addDestroyOnUnload(objElement, strProperty);
};

// Remove circular references to prevent memory leaks in IE
Gmenu.Utils.addEvent(window, 'unload', function() {
  for (var iObj = Gmenu.Utils.destroyOnUnload.length - 1; iObj >= 0; iObj--) {
    var objDestroy = Gmenu.Utils.destroyOnUnload[iObj];
    objDestroy[0][objDestroy[1]] = null;
    objDestroy[0] = null;
  }
  for (var iLis = Gmenu.Utils.removeOnUnload.length - 1; iLis >= 0; iLis--) {
  	var listener = Gmenu.Utils.removeOnUnload[iLis];
	Gmenu.Utils.removeEvent(listener["element"], listener["event"], listener["listener"]);
  }
});
if (typeof Gmenu == 'undefined') {
  /**
   * Namespace definition.
   * @constructor
   */
  Gmenu = function() {};
}

/**
 * @constructor
 */
Gmenu.Transport = function() {};

// Determine most current versions of ActiveX objects available
if (typeof ActiveXObject != 'undefined') {

  /**
   * String variable with most current version of XMLDOM ActiveX object name
   * available.
   * @private
   */
  Gmenu.Transport.XMLDOM = null;

  /**
   * String variable with Most current version of XMLHTTP ActiveX object name
   * available.
   * @private
   */
  Gmenu.Transport.XMLHTTP = null;

  /**
   * @ignore
   * Returns first available ActiveX object name from the given list.
   *
   * @param {object} arrVersions List of ActiveX object names to test
   * @return First available ActiveX object name or null
   * @type string
   */
  Gmenu.Transport.pickActiveXVersion = function(arrVersions) {
    for (var iVn = 0; iVn < arrVersions.length; iVn++) {
      try {
        var objDocument = new ActiveXObject(arrVersions[iVn]);
        // If it gets to this point, the string worked
        return arrVersions[iVn];
      } catch (objException) {};
    }
    return null;
  };

  /**
   * Most current version of XMLDOM ActiveX object.
   * @private
   */
  Gmenu.Transport.XMLDOM = Gmenu.Transport.pickActiveXVersion([
    'Msxml2.DOMDocument.4.0',
    'Msxml2.DOMDocument.3.0',
    'MSXML2.DOMDocument',
    'MSXML.DOMDocument',
    'Microsoft.XMLDOM'
  ]);

  /**
   * Most current version of XMLHTTP ActiveX object.
   * @private
   */
  Gmenu.Transport.XMLHTTP = Gmenu.Transport.pickActiveXVersion([
    'Msxml2.XMLHTTP.4.0',
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
  ]);

  // We don't need this any more
  Gmenu.Transport.pickActiveXVersion = null;

}

/**
 * Creates cross browser XMLHttpRequest object.
 *
 * @return New XMLHttpRequest object.
 * @type object
 */
Gmenu.Transport.createXmlHttpRequest = function() {
  if (typeof XMLHttpRequest != 'undefined') {
    return new XMLHttpRequest();
  }
  if (typeof ActiveXObject != 'undefined') {
    try {
      return new ActiveXObject(Gmenu.Transport.XMLHTTP);
    } catch (objException) {};
  }
  return null;
};

/**
 * Checks if animated GIF is already displayed in the specified div.
 *
 * <pre>
 * Arguments object format:
 * {
 *   busyContainer: [object or string] element where to put animated GIF,
 *   busyImage: [string, optional] image name
 * }
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 * @return True if image is displayed
 * @type boolean
 */
Gmenu.Transport.isBusy = function(objArgs) {
  // Get container
  var objContainer = objArgs.busyContainer;
  if (typeof objContainer == 'string') {
    objContainer = document.getElementById(objContainer);
  }
  if (!objContainer) {
    return;
  }
  // Get image name
  var strImage = objArgs.busyImage;
  if (typeof strImage != 'string') {
    strImage = '';
  }
  strImage = strImage.split('/').pop();
  if (!strImage.length) {
    strImage = 'zpbusy.gif';
  }
  // Check if image is displayed
  var objFC = objContainer.firstChild;
  if (objFC) {
    objFC = objFC.firstChild;
    if (objFC) {
      objFC = objFC.firstChild;
      if (objFC && objFC.tagName && objFC.tagName == 'img') {
        var strSrc = objFC.getAttribute('src');
        if (typeof strSrc == 'string' && strSrc.length) {
          // Get last token
          strSrc = strSrc.split('/').pop();
          if (strSrc == strImage) {
            return true;
          }
        }
      }
    }
  }
  return false;
};

/**
 * Shows animated GIF in the specified div.
 *
 * <pre>
 * Arguments object format:
 * {
 *   busyContainer: [object or string] element where to put animated GIF,
 *   busyImage: [string, optional] image name,
 *   busyImageWidth: [number or string, optional] image width,
 *   busyImageHeight: [number or string, optional] image height
 * }
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.showBusy = function(objArgs) {
  // Make sure image is not displayed yet
  if (Gmenu.Transport.isBusy(objArgs)) {
    return;
  }
  // Get container
  var objContainer = objArgs.busyContainer;
  if (typeof objContainer == 'string') {
    objContainer = document.getElementById(objContainer);
  }
  if (!objContainer) {
    return;
  }
  // Get image name and dimensions
  var strImage = objArgs.busyImage;
  var strImageWidth = objArgs.busyImageWidth;
  var strImageHeight = objArgs.busyImageHeight;
  if (typeof strImage != 'string' || !strImage.length) {
    strImage = 'zpbusy.gif';
  } else {
    if (typeof strImageWidth == 'number' ||
     (typeof strImageWidth == 'string' && /\d$/.test(strImageWidth))) {
      strImageWidth += 'px';
    }
    if (typeof strImageHeight == 'number' ||
     (typeof strImageHeight == 'string' && /\d$/.test(strImageHeight))) {
      strImageHeight += 'px';
    }
  }
  if (!strImageWidth) {
    strImageWidth = '65px';
  }
  if (!strImageHeight) {
    strImageHeight = '35px';
  }
  // Get path
  var strPath = '';
  // Check if path is specified
  if (strImage.indexOf('/') < 0) {
    // Use default path
    strPath = Gmenu.Transport.getPath('transport.js');
  }
  // Form tag
  var arrImgTag = [];
  arrImgTag.push('<img src="');
  arrImgTag.push(strPath);
  arrImgTag.push(strImage);
  arrImgTag.push('"');
  if (strImageWidth || strImageHeight) {
    arrImgTag.push(' style="');
    if (strImageWidth) {
      arrImgTag.push('width:');
      arrImgTag.push(strImageWidth);
      arrImgTag.push(';');
    }
    if (strImageHeight) {
      arrImgTag.push('height:');
      arrImgTag.push(strImageHeight);
    }
    arrImgTag.push('"');
  }
  arrImgTag.push(' />');
  strImgTag = arrImgTag.join('');
  // Get container dimensions
  var iContainerWidth = objContainer.offsetWidth;
  var iContainerHeight = objContainer.offsetHeight;
  // Display image
  var objBusyContainer = Gmenu.Utils.createElement('div');
  objBusyContainer.style.position = 'relative';
  objBusyContainer.style.zIndex = 2147483583;
  var objBusy = Gmenu.Utils.createElement('div', objBusyContainer);
  objBusy.style.position = 'absolute';
  objBusy.innerHTML = strImgTag;
  if (objContainer.firstChild) {
    objContainer.insertBefore(objBusyContainer, objContainer.firstChild);
  } else {
    objContainer.appendChild(objBusyContainer);
  }
  // Move to the center of container
  var iBusyWidth = objBusy.offsetWidth;
  var iBusyHeight = objBusy.offsetHeight;
  if (iContainerWidth > iBusyWidth) {
    objBusy.style.left = objContainer.scrollLeft +
     (iContainerWidth - iBusyWidth) / 2 + 'px';
  }
  if (iContainerHeight > iBusyHeight) {
    objBusy.style.top = objContainer.scrollTop +
     (iContainerHeight - iBusyHeight) / 2 + 'px';
  }
};

/**
 * Removes animated GIF which was put by {@link Gmenu.Transport#showBusyGif}
 * from the specified div.
 *
 * <pre>
 * Arguments object format:
 * {
 *   busyContainer: [object or string] element where to put animated GIF,
 *   busyImage: [string, optional] image name
 * }
 * </pre>
 *
 * @private
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.removeBusy = function(objArgs) {
  // Get container
  var objContainer = objArgs.busyContainer;
  if (typeof objContainer == 'string') {
    objContainer = document.getElementById(objContainer);
  }
  if (!objContainer) {
    return;
  }
  // Make sure image is displayed
  if (Gmenu.Transport.isBusy(objArgs)) {
    // Remove image
    objContainer.removeChild(objContainer.firstChild);
  }
};

/**
 * Fetches specified URL using new XMLHttpRequest object.
 *
 * <pre>
 * Asynchronous mode is recommended because it is safer and there is no risk of
 * having your script hang in case of network problem. Synchronous mode means
 * that the code will hang until a response comes back.
 *
 * When request is completed, one of provided callback functions is called:
 * onLoad on success or onError on error. In synchronous mode onLoad callback
 * can be omitted. Instead use returned object.
 *
 * onLoad callback function receives XMLHttpRequest object as argument and may
 * use its various properties like responseText, responseXML, etc.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: server status number (404, etc.) [number],
 *   errorDescription: human readable error description [string]
 * }
 *
 * Note: Some browsers implement caching for GET requests. Caching can be
 * prevented by adding 'r=' + Math.random() parameter to URL.
 *
 * If you use POST method, content argument should be something like
 * 'var1=value1&var2=value2' with urlencoded values. If you wish to send other
 * content, set appropriate contentType. E.g. 'multipart/form-data', 'text/xml',
 * etc.
 *
 * If server response contains non-ASCII characters, server must send
 * corresponding content-type header. E.g.
 * "Content-type: text/plain; charset=utf-8" or
 * "Content-type: text/plain; charset=windows-1251".
 *
 * Arguments object format:
 * {
 *   url: [string] relative or absolute URL to fetch,
 *   method: [string, optional] method ('GET', 'POST', 'HEAD', 'PUT'),
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   contentType: [string, optional] content type when using POST,
 *   content: [string or object, optional] postable string or DOM object data
 *    when using POST,
 *   onLoad: [function, optional] function reference to call on success,
 *   onError: [function, optional] function reference to call on error,
 *   username: [string, optional] username,
 *   password: [string, optional] password,
 *   busyContainer: [object or string, optional] element or id of element where
 *    to put "Busy" animated GIF,
 *   busyImage: [string, optional] image name,
 *   busyImageWidth: [number or string, optional] image width,
 *   busyImageHeight: [number or string, optional] image height
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return In synchronous mode XMLHttpRequest object or null. In asynchronous
 * mode always null.
 * @type object
 */
Gmenu.Transport.fetch = function(objArgs) {
  // Check arguments
  if (objArgs == null || typeof objArgs != 'object') {
    return null;
  }
  if (!objArgs.url) {
    return null;
  }
  if (!objArgs.method) {
    objArgs.method = 'GET';
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  if (!objArgs.contentType && objArgs.method.toUpperCase() == 'POST') {
    objArgs.contentType = 'application/x-www-form-urlencoded';
  }
  if (!objArgs.content) {
    objArgs.content = null;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  // Request URL
  var objRequest = Gmenu.Transport.createXmlHttpRequest();
  if (objRequest == null) {
    return null;
  }
  // Show "Busy" animated GIF
  Gmenu.Transport.showBusy(objArgs);
  // IE 6 calls onreadystatechange and then raises an exception if local file is
  // not found. This flag is used to prevent duplicate onError calls.
  var boolErrorDisplayed = false;
  try {
    // Open request
    if (typeof objArgs.username != 'undefined' &&
     typeof objArgs.password != 'undefined') {
      objRequest.open(objArgs.method, objArgs.url, objArgs.async,
       objArgs.username, objArgs.password);
    } else {
      objRequest.open(objArgs.method, objArgs.url, objArgs.async);
    }
    // Onready handler
    var funcOnReady = function () {
      // Remove "Busy" animated GIF
      Gmenu.Transport.removeBusy(objArgs);
      // Process response
      if (objRequest.status == 200 || objRequest.status == 304 ||
       (location.protocol == 'file:' && !objRequest.status)) {
        // OK or found, but determined unchanged and loaded from cache
        if (typeof objArgs.onLoad == 'function') {
          objArgs.onLoad(objRequest);
        }
      } else if (!boolErrorDisplayed) {
        boolErrorDisplayed = true;
        // 404 Not found, etc.
        Gmenu.Transport.displayError(objRequest.status,
         'Error: Cannot fetch ' + objArgs.url + '.\n' +
         (objRequest.statusText || ''),
         objArgs.onError);
      }
    };
    // Prevent duplicate funcOnReady call in synchronous mode
    if (objArgs.async) {
      // Set onreadystatechange handler
      objRequest.onreadystatechange = function () {
        if (objRequest.readyState == 4) {
          // Request complete
          funcOnReady();
          // Prevent memory leak
 	  objRequest.onreadystatechange = {};
 	}
      };
    }
    // Set content type if needed
    if (objArgs.contentType) {
      objRequest.setRequestHeader('Content-Type', objArgs.contentType);
    }
    // Send request
    objRequest.send(objArgs.content);
    // In synchronous mode the result is ready on the next line
    if (!objArgs.async) {
      funcOnReady();
      return objRequest;
    }
  } catch (objException) {
    // Remove "Busy" animated GIF
    Gmenu.Transport.removeBusy(objArgs);
    // Process error
    if (!boolErrorDisplayed) {
      boolErrorDisplayed = true;
      if (objException.name &&
       objException.name == 'NS_ERROR_FILE_NOT_FOUND') {
        Gmenu.Transport.displayError(0,
         'Error: Cannot fetch ' + objArgs.url + '.\nFile not found.',
         objArgs.onError);
      } else {
        Gmenu.Transport.displayError(0,
         'Error: Cannot fetch ' + objArgs.url + '.\n' +
         (objException.message || ''),
         objArgs.onError);
      }
    }
  };
  return null;
};

/**
 * Parses HTML fragment into HTMLElement object.
 *
 * @param {string} strHtml HTML fragment
 * @return Div element which contains parsed HTML fragment
 * @type object
 */
Gmenu.Transport.parseHtml = function(strHtml) {
  // Convert to string
  strHtml += '';
  // Remove leading whitespace characters because Firefox and Opera don't parse
  // fragment that starts from whitespace character
  strHtml = strHtml.replace(/^\s+/g, '');
  // Create temporaty container
  var objTempContainer = null;
	if (document.createElementNS) {
		// use the XHTML namespace
		objTempContainer =
		 document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
	} else {
		objTempContainer = document.createElement('div');
	}
  // Parse HTML fragment
  objTempContainer.innerHTML = strHtml;
  // Return container element
  return objTempContainer;
};

/**
 * Evaluates javascript in global scope.
 *
 * <p><b>
 * Note: Global variables must be declared without "var" keyword. Otherwise
 * they will be ignored by Safari.
 * </b></p>
 *
 * @param {string} strScript Script to evaluate
 */
Gmenu.Transport.evalGlobalScope = function(strScript) {
  if (typeof strScript != 'string' || !strScript.match(/\S/)) {
    return;
  }
  if (window.execScript) {
    // IE
    window.execScript(strScript, 'javascript');
  } else if (window.eval) {
    // Others
    window.eval(strScript);
/*
 This should never be reached
  } else {
    var funcScript = new Function(strScript);
    funcScript.call(window);
*/
  }
};

/**
 * Assigns passed HTML fragment to the specified element's innerHTML property
 * and evaluates in global scope javascripts found in the fragment.
 *
 * <pre>
 * Arguments object format:
 * {
 *   html: [string] HTML fragment,
 *   container: [object or string, optional] element or id of element to put
 *    HTML fragment into
 * }
 * </pre>
 *
 * <p><b>
 * Note: Scripts are executed after HTML fragment is assigned to innerHTML.
 * If external scripts are used, they are loaded asynchronously and execution
 * sequence is not preserved.
 * </b></p>
 *
 * <p><b>
 * Note: Global variables must be declared without "var" keyword. Otherwise
 * they will be ignored by Safari.
 * </b></p>
 *
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.setInnerHtml = function(objArgs) {
  // Check arguments
  if (!objArgs || typeof objArgs.html != 'string') {
    return;
  }
  var strHtml = objArgs.html;
  // Get container
  var objContainer = null;
  if (typeof objArgs.container == 'string') {
    objContainer = document.getElementById(objArgs.container);
  } else if (typeof objArgs.container == 'object') {
    objContainer = objArgs.container;
  }
  // Extract javascripts
  var arrScripts = [];
  if (strHtml.match(/<\s*\/\s*script\s*>/i)) {
    // Split whole string by </script>
    var arrTokens = strHtml.split(/<\s*\/\s*script\s*>/i);
    var arrHtml = [];
    for (var iToken = arrTokens.length - 1; iToken >= 0; iToken--) {
      var strToken = arrTokens[iToken];
      if (strToken.match(/\S/)) {
        // Search <script ... > in the middle of each token
        var arrMatch = strToken.match(/<\s*script([^>]*)>/i);
        if (arrMatch) {
          // Separate HTML from javascript
          var arrCouple = strToken.split(/<\s*script[^>]*>/i);
          // IE doesn't put empty tokens into the array
          while (arrCouple.length < 2) {
            if (strToken.match(/^<\s*script[^>]*>/i)) {
              // HTML part is absent
              arrCouple.unshift('');
            } else {
              // javascript part is absent
              arrCouple.push('');
            }
          }
          // Save HTML fragment
          arrHtml.unshift(arrCouple[0]);
          // Get script attributes
          var strAttrs = arrMatch[1];
          // Get script text
          var srtScript = arrCouple[1];
          // Ignore script text if "src" attribute is present
          if (strAttrs.match(/\s+src\s*=/i)) {
            srtScript = '';
          } else {
            // Fix functions: function aaa() -> aaa = function()
            srtScript = srtScript.replace(/function\s+([^(]+)/g, '$1=function');
          }
          arrScripts.push([strAttrs, srtScript]);
        } else if (iToken < arrTokens.length - 1) {
          // On error assume this token is a part of previous token
          arrTokens[iToken - 1] += '</script>' + strToken;
        } else {
          // If this is last token, assume it is HTML fragment
          arrHtml.unshift(strToken);
        }
      } else {
        // Empty token
        arrHtml.unshift(strToken);
      }
    }
    // Get HTML part
    strHtml = arrHtml.join('');
  }
  // Set inner HTML
  if (objContainer) {
    // Opera hack
    objContainer.innerHTML = '<form></form>';
    objContainer.innerHTML = strHtml;
  }
  // Evaluate javascripts
  for (var iScript = 0; iScript < arrScripts.length; iScript++) {
    if (arrScripts[iScript][1].length) {
      // Evaluate in global scope
      Gmenu.Transport.evalGlobalScope(arrScripts[iScript][1]);
    }
    // Load external script
    var strAttrs = arrScripts[iScript][0];
    strAttrs = strAttrs.replace(/\s+/g, ' ').replace(/^\s/, '')
     .replace(/\s$/, '').replace(/ = /g, '=');
    if (strAttrs.indexOf('src=') >= 0) {
      // Get container
      var objContainer = document.body;
      if (!objContainer) {
        objContainer = document.getElementsByTagName('head')[0];
        if (!objContainer) {
          objContainer = document;
        }
      }
      // Get attributes
      var arrAttrs = strAttrs.split(' ');
      // Load script
      var objScript = Gmenu.Utils.createElement('script');
      for (var iAttr = 0; iAttr < arrAttrs.length; iAttr++) {
        var arrAttr = arrAttrs[iAttr].split('=');
        if (arrAttr.length > 1) {
          objScript.setAttribute(arrAttr[0],
           arrAttr[1].match(/^[\s|"|']*([\s|\S]*[^'|"])[\s|"|']*$/)[1]);
        } else {
          objScript.setAttribute(arrAttr[0], arrAttr[0]);
        }
      }
      // It's important for Safari to assign attributes before appending
      objContainer.appendChild(objScript);
    }
  }
};

/**
 * Fetches and parses XML document from the specified URL.
 *
 * <pre>
 * When XML document is fetched and parsed, one of provided callback functions
 * is called: onLoad on success or onError on error. In synchronous mode onLoad
 * callback can be omitted. Instead use returned object.
 *
 * onLoad callback function receives XMLDocument object as argument and may use
 * its documentElement and other properties.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: error code [number],
 *   errorDescription: human readable error description [string]
 * }
 * Error code will be 0 unless Gmenu.Transport.fetch was used to fetch URL
 * and there was a problem during fetching.
 *
 * If method argument is not defined, more efficient XMLDOM in IE and
 * document.implementation.createDocument in Mozilla will be used to fetch
 * and parse document. Otherwise Gmenu.Transport.fetch will be used to fetch
 * document and Gmenu.Transport.parseXml to parse.
 *
 * Note: Some browsers implement caching for GET requests. Caching can be
 * prevented by adding 'r=' + Math.random() parameter to URL.
 *
 * If you use POST method, content argument should be something like
 * 'var1=value1&var2=value'. If you wish to send other content, set appropriate
 * contentType. E.g. to send XML string, you should set contentType: 'text/xml'.
 *
 * If server response contains non-ASCII characters, encoding must be specified.
 * E.g. <?xml version="1.0" encoding="utf-8"?> or
 * <?xml version="1.0" encoding="windows-1251"?>.
 *
 * If server response contains non-ASCII characters, server must send
 * corresponding content-type header. E.g.
 * "Content-type: text/xml; charset=utf-8" or
 * "Content-type: text/xml; charset=windows-1251".
 *
 * Arguments object format:
 * {
 *   url: [string] relative or absolute URL to fetch,
 *   method: [string, optional] method ('GET', 'POST', 'HEAD', 'PUT'),
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   contentType: [string, optional] content type when using POST,
 *   content: [string or object, optional] postable string or DOM object data
 *    when using POST,
 *   onLoad: [function, optional] function reference to call on success,
 *   onError: [function, optional] function reference to call on error,
 *   username: [string, optional] username,
 *   password: [string, optional] password,
 *   busyContainer: [object or string, optional] element or id of element where
 *    to put "Busy" animated GIF,
 *   busyImage: [string, optional] image name,
 *   busyImageWidth: [number or string, optional] image width,
 *   busyImageHeight: [number or string, optional] image height
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return In synchronous mode XMLDocument object or null. In asynchronous mode
 * always null.
 * @type object
 */
Gmenu.Transport.fetchXmlDoc = function(objArgs) {
  // Check arguments
  if (objArgs == null || typeof objArgs != 'object') {
    return null;
  }
  if (!objArgs.url) {
    return null;
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  // Try more efficient methods first
  if (!objArgs.method && typeof objArgs.username == 'undefined' &&
   typeof objArgs.password == 'undefined') {
    if (document.implementation && document.implementation.createDocument) {
      // Mozilla
      var objDocument = document.implementation.createDocument('', '', null);
      // Opera 8.51 also has document.implementation, but hasn't implemented
      // XMLDOM load method yet
      if (objDocument.load) {
        objDocument.async = objArgs.async;
        // Prevent duplicate onXmlDocLoad call in synchronous mode
        if (objArgs.async) {
          objDocument.onload = function() {
            // Remove "Busy" animated GIF
            Gmenu.Transport.removeBusy(objArgs);
            // Process response
            Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
             objArgs.onError);
          };
        }
        // Show "Busy" animated GIF
        Gmenu.Transport.showBusy(objArgs);
        // Load document
        try {
          objDocument.load(objArgs.url);
          // In synchronous mode the result is ready on the next line
          if (!objArgs.async) {
            // Remove "Busy" animated GIF
            Gmenu.Transport.removeBusy(objArgs);
            // Process response
            Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
             objArgs.onError);
            return objDocument;
          }
          return null;
        } catch (objException) {
          // Remove "Busy" animated GIF
          Gmenu.Transport.removeBusy(objArgs);
          // Process error
          if (objException.name &&
           objException.name == 'NS_ERROR_FILE_NOT_FOUND') {
            Gmenu.Transport.displayError(0,
             'Error: Cannot fetch ' + objArgs.url + '.\nFile not found.',
             objArgs.onError);
          } else {
            Gmenu.Transport.displayError(0,
             'Error: Cannot fetch ' + objArgs.url + '.\n' +
             objException.toString(),
             objArgs.onError);
          }
        };
      }
    }
    if (typeof ActiveXObject != 'undefined') {
      // IE
      // Show "Busy" animated GIF
      Gmenu.Transport.showBusy(objArgs);
      // Load document
      try {
        var objDocument = new ActiveXObject(Gmenu.Transport.XMLDOM);
        objDocument.async = objArgs.async;
        // Prevent duplicate onXmlDocLoad call in synchronous mode
        if (objArgs.async) {
          objDocument.onreadystatechange = function () {
            if (objDocument.readyState == 4) {
              // Remove "Busy" animated GIF
              Gmenu.Transport.removeBusy(objArgs);
              // Process response
              Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
               objArgs.onError);
              // Prevent memory leak
              objDocument.onreadystatechange = {};
            }
          };
        }
        objDocument.load(objArgs.url);
        // In synchronous mode the result is ready on the next line
        if (!objArgs.async) {
          // Remove "Busy" animated GIF
          Gmenu.Transport.removeBusy(objArgs);
          // Process response
          Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
           objArgs.onError);
          return objDocument;
        }
        return null;
      } catch (objException) {
        // Remove "Busy" animated GIF
        Gmenu.Transport.removeBusy(objArgs);
      };
    }
  }
  // Try XMLHttpRequest
  // Form argument for fetch
  var objFetchArgs = {};
  for (var strKey in objArgs) {
    objFetchArgs[strKey] = objArgs[strKey];
  }
  // Prevent duplicate parseXml call in synchronous mode
  if (objArgs.async) {
    objFetchArgs.onLoad = function(objRequest) {
      Gmenu.Transport.parseXml({
        strXml: objRequest.responseText,
        onLoad: objArgs.onLoad,
        onError: objArgs.onError
      });
    };
  } else {
    objFetchArgs.onLoad = null;
  }
  // Fetch URL
  var objRequest = Gmenu.Transport.fetch(objFetchArgs);
  // In synchronous mode the result is ready on the next line
  if (!objArgs.async && objRequest) {
    return Gmenu.Transport.parseXml({
      strXml: objRequest.responseText,
      onLoad: objArgs.onLoad,
      onError: objArgs.onError
    });
  }
  return null;
};

/**
 * Parses XML string into XMLDocument object.
 *
 * <pre>
 * When XML string is parsed, one of provided callback functions is called:
 * onLoad on success or onError on error. In synchronous mode onLoad callback
 * can be omitted. Instead use returned object.
 *
 * onLoad callback function receives XMLDocument object as argument and may use
 * its documentElement and other properties.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: error code [number],
 *   errorDescription: human readable error description [string]
 * }
 * Error code will be always 0.
 *
 * Returns XMLDocument object, so onLoad callback function is optional.
 * Returned value and its documentElement property should be checked before
 * use because they can be null or undefined.
 *
 * If XML string contains non-ASCII characters, encoding must be specified.
 * E.g. <?xml version="1.0" encoding="utf-8"?> or
 * <?xml version="1.0" encoding="windows-1251"?>.
 *
 * Arguments object format:
 * {
 *   strXml: XML string to parse [string],
 *   onLoad: function reference to call on success [function] (optional),
 *   onError: function reference to call on error [function] (optional)
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return XMLDocument object or null
 * @type object
 */
Gmenu.Transport.parseXml = function(objArgs) {
  if (objArgs == null || typeof objArgs != 'object') {
    return null;
  }
  if (!objArgs.strXml) {
    return null;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  if (window.DOMParser) {
    // Mozilla
    try {
      var objDocument = (new DOMParser()).parseFromString(objArgs.strXml,
       'text/xml');
      Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
       objArgs.onError);
      return objDocument;
    } catch (objException) {
      Gmenu.Transport.displayError(0,
       'Error: Cannot parse.\n' +
       'String does not appear to be a valid XML fragment.',
       objArgs.onError);
    };
    return null;
  }
  if (typeof ActiveXObject != 'undefined') {
    // IE
    try {
      var objDocument = new ActiveXObject(Gmenu.Transport.XMLDOM);
      objDocument.loadXML(objArgs.strXml);
      Gmenu.Transport.onXmlDocLoad(objDocument, objArgs.onLoad,
       objArgs.onError);
      return objDocument;
    } catch (objException) {};
  }
  return null;
};

/**
 * Checks if there were errors during XML document fetching and parsing and
 * calls onLoad or onError callback function correspondingly.
 *
 * @private
 * @param {object} objDocument XMLDocument object
 * @param {function} onLoad Callback function provided by user
 * @param {function} onError Callback function provided by user
 */
Gmenu.Transport.onXmlDocLoad = function(objDocument, onLoad, onError) {
  var strError = null;
  if (objDocument.parseError) {
    // Parsing error in IE
    strError = objDocument.parseError.reason;
    if (objDocument.parseError.srcText) {
      strError += 'Location: ' + objDocument.parseError.url +
       '\nLine number ' + objDocument.parseError.line + ', column ' +
       objDocument.parseError.linepos + ':\n' +
       objDocument.parseError.srcText + '\n';
    }
  } else if (objDocument.documentElement &&
   objDocument.documentElement.tagName == 'parsererror') {
    // If an error is caused while parsing, Mozilla doesn't throw an exception.
    // Instead, it creates an XML string containing the details of the error:
    // <parsererror xmlns="http://www.w3.org/1999/xhtml">XML Parsing Error: ...
    // Check if strings has been generated.
    strError = objDocument.documentElement.firstChild.data + '\n' +
     objDocument.documentElement.firstChild.nextSibling.firstChild.data;
  } else if (!objDocument.documentElement) {
    strError = 'String does not appear to be a valid XML fragment.';
  }
  if (strError) {
    // Parsing error
    Gmenu.Transport.displayError(0,
     'Error: Cannot parse.\n' + strError,
     onError);
  } else {
    // Success
    if (typeof onLoad == 'function') {
      onLoad(objDocument);
    }
  }
};

/**
 * Serializes XMLDocument object into XML string.
 *
 * @param {object} objDocument XMLDocument object
 * @return XML string
 * @type string
 */
Gmenu.Transport.serializeXmlDoc = function(objDocument) {
  if (window.XMLSerializer) {
    // Mozilla
    return (new XMLSerializer).serializeToString(objDocument);
  }
  if (objDocument.xml) {
    // IE
    return objDocument.xml;
  }
};

/**
 * Fetches and parses JSON object from the specified URL.
 *
 * <pre>
 * When JSON object is fetched and parsed, one of provided callback functions
 * is called: onLoad on success or onError on error. In synchronous mode onLoad
 * callback can be omitted. Instead use returned object.
 *
 * onLoad callback function receives JSON object as argument.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: error code [number],
 *   errorDescription: human readable error description [string]
 * }
 * Error code will be 0 unless there was a problem during fetching.
 *
 * Note: Some browsers implement caching for GET requests. Caching can be
 * prevented by adding 'r=' + Math.random() parameter to URL.
 *
 * If you use POST method, content argument should be something like
 * 'var1=value1&var2=value'. If you wish to send other content, set appropriate
 * contentType. E.g. to send XML string, you should set contentType: 'text/xml'.
 *
 * If server response contains non-ASCII characters, server must send
 * corresponding content-type header. E.g.
 * "Content-type: text/plain; charset=utf-8" or
 * "Content-type: text/plain; charset=windows-1251".
 *
 * Arguments object format:
 * {
 *   url: [string] relative or absolute URL to fetch,
 *   reliable: [boolean, optional] false (string will be parsed) or true
 *   (evaluated) (default: false),
 *   method: [string, optional] method ('GET', 'POST', 'HEAD', 'PUT'),
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   contentType: [string, optional] content type when using POST,
 *   content: [string or object, optional] postable string or DOM object data
 *    when using POST,
 *   onLoad: [function, optional] function reference to call on success,
 *   onError: [function, optional] function reference to call on error,
 *   username: [string, optional] username,
 *   password: [string, optional] password,
 *   busyContainer: [object or string, optional] element or id of element where
 *    to put "Busy" animated GIF,
 *   busyImage: [string, optional] image name,
 *   busyImageWidth: [number or string, optional] image width,
 *   busyImageHeight: [number or string, optional] image height
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return In synchronous mode JSON object or null. In asynchronous mode always
 * null.
 * @type object
 */
Gmenu.Transport.fetchJsonObj = function(objArgs) {
  // Check arguments
  if (objArgs == null || typeof objArgs != 'object') {
    return null;
  }
  if (!objArgs.url) {
    return null;
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  if (!objArgs.reliable) {
    objArgs.reliable = false;
  }
  // Form argument for fetch
  var objFetchArgs = {};
  for (var strKey in objArgs) {
    objFetchArgs[strKey] = objArgs[strKey];
  }
  // Prevent duplicate parseXml call in synchronous mode
  if (objArgs.async) {
    objFetchArgs.onLoad = function(objRequest) {
      Gmenu.Transport.parseJson({
        strJson: objRequest.responseText,
        reliable: objArgs.reliable,
        onLoad: objArgs.onLoad,
        onError: objArgs.onError
      });
    };
  } else {
    objFetchArgs.onLoad = null;
  }
  // Fetch URL
  var objRequest = Gmenu.Transport.fetch(objFetchArgs);
  // In synchronous mode the result is ready on the next line
  if (!objArgs.async && objRequest) {
    return Gmenu.Transport.parseJson({
      strJson: objRequest.responseText,
      reliable: objArgs.reliable,
      onLoad: objArgs.onLoad,
      onError: objArgs.onError
    });
  }
  return null;
};

/**
 * Parses JSON string into object.
 *
 * <pre>
 * When JSON string is parsed, one of provided callback functions is called:
 * onLoad on success or onError on error.
 *
 * onLoad callback function receives JSON object as argument.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: error code [number],
 *   errorDescription: human readable error description [string]
 * }
 * Error code will be always 0.
 *
 * Returns JSON object, so onLoad callback function is optional.
 * Returned value should be checked before use because it can be null.
 *
 * Arguments object format:
 * {
 *   strJson: JSON string to parse [string],
 *   reliable: false (string will be parsed) or true (evaluated) [boolean]
 *   (optional, false by default),
 *   onLoad: function reference to call on success [function] (optional),
 *   onError: function reference to call on error [function] (optional)
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return JSON object or null
 * @type object
 */
Gmenu.Transport.parseJson = function(objArgs) {
  if (objArgs == null || typeof objArgs != 'object') {
    return null;
  }
  if (!objArgs.strJson) {
    return null;
  }
  if (!objArgs.reliable) {
    objArgs.reliable = false;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  var objJson = null;
  try {
    if (objArgs.reliable) {
      objJson = eval('(' + objArgs.strJson + ')');
    } else {
      objJson = Gmenu.Transport.parseJsonStr(objArgs.strJson);
    }
  } catch (objException) {
    Gmenu.Transport.displayError(0,
     'Error: Cannot parse.\n' +
     'String does not appear to be a valid JSON fragment: ' +
     objException.message + '\n' + objException.text,
     objArgs.onError);
  };
  if (typeof objArgs.onLoad == 'function') {
    objArgs.onLoad(objJson);
  }
  return objJson;
};

/**
 * Parses JSON string into object.
 *
 * <pre>
 * Was taken with changes from http://json.org/json.js.
 *
 * Throws exception if parsing error occurs.
 *
 * JSON format is described at http://json.org/js.html.
 * </pre>
 *
 * @private
 * @param {string} text JSON string to parse
 * @return JSON object
 * @type object
 */
Gmenu.Transport.parseJsonStr = function(text) {
  var p = /^\s*(([,:{}\[\]])|"(\\.|[^\x00-\x1f"\\])*"|-?\d+(\.\d*)?([eE][+-]?\d+)?|true|false|null)\s*/,
      token,
      operator;
  function error(m, t) {
      throw {
          name: 'JSONError',
          message: m,
          text: t || operator || token
      };
  }
  function next(b) {
      if (b && b != operator) {
          error("Expected '" + b + "'");
      }
      if (text) {
          var t = p.exec(text);
          if (t) {
              if (t[2]) {
                  token = null;
                  operator = t[2];
              } else {
                  operator = null;
                  try {
                      token = eval(t[1]);
                  } catch (e) {
                      error("Bad token", t[1]);
                  }
              }
              text = text.substring(t[0].length);
          } else {
              error("Unrecognized token", text);
          }
      } else {
          // undefined changed to null because it is not supported in IE 5.0
          token = operator = null;
      }
  }
  function val() {
      var k, o;
      switch (operator) {
      case '{':
          next('{');
          o = {};
          if (operator != '}') {
              for (;;) {
                  if (operator || typeof token != 'string') {
                      error("Missing key");
                  }
                  k = token;
                  next();
                  next(':');
                  o[k] = val();
                  if (operator != ',') {
                      break;
                  }
                  next(',');
              }
          }
          next('}');
          return o;
      case '[':
          next('[');
          o = [];
          if (operator != ']') {
              for (;;) {
                  o.push(val());
                  if (operator != ',') {
                      break;
                  }
                  next(',');
              }
          }
          next(']');
          return o;
      default:
          if (operator !== null) {
              error("Missing value");
          }
          k = token;
          next();
          return k;
      }
  }
  next();
  return val();
};

/**
 * Serializes JSON object into JSON string.
 *
 * Was taken with changes from http://json.org/json.js.
 *
 * @param {object} v JSON object
 * @return JSON string
 * @type string
 */
Gmenu.Transport.serializeJsonObj = function(v) {
  var a = [];
  /*
    Emit a string.
  */
  function e(s) {
      a[a.length] = s;
  }
  /*
    Convert a value.
  */
  function g(x) {
      var c, i, l, v;
      switch (typeof x) {
      case 'object':
          if (x) {
              if (x instanceof Array) {
                  e('[');
                  l = a.length;
                  for (i = 0; i < x.length; i += 1) {
                      v = x[i];
                      if (typeof v != 'undefined' &&
                              typeof v != 'function') {
                          if (l < a.length) {
                              e(',');
                          }
                          g(v);
                      }
                  }
                  e(']');
                  return;
              } else if (typeof x.toString != 'undefined') {
                  e('{');
                  l = a.length;
                  for (i in x) {
                      v = x[i];
                      if (x.hasOwnProperty(i) &&
                              typeof v != 'undefined' &&
                              typeof v != 'function') {
                          if (l < a.length) {
                              e(',');
                          }
                          g(i);
                          e(':');
                          g(v);
                      }
                  }
                  return e('}');
              }
          }
          e('null');
          return;
      case 'number':
          e(isFinite(x) ? +x : 'null');
          return;
      case 'string':
          l = x.length;
          e('"');
          for (i = 0; i < l; i += 1) {
              c = x.charAt(i);
              if (c >= ' ') {
                  if (c == '\\' || c == '"') {
                      e('\\');
                  }
                  e(c);
              } else {
                  switch (c) {
                      case '\b':
                          e('\\b');
                          break;
                      case '\f':
                          e('\\f');
                          break;
                      case '\n':
                          e('\\n');
                          break;
                      case '\r':
                          e('\\r');
                          break;
                      case '\t':
                          e('\\t');
                          break;
                      default:
                          c = c.charCodeAt();
                          e('\\u00' + Math.floor(c / 16).toString(16) +
                              (c % 16).toString(16));
                  }
              }
          }
          e('"');
          return;
      case 'boolean':
          e(String(x));
          return;
      default:
          e('null');
          return;
      }
  }
  g(v);
  return a.join('');
};

/**
 * Displays error message.
 *
 * <pre>
 * Calls onError callback function provided by user. If there is no onError
 * callback function, displays alert with human readable error description.
 * onError callback function receives following object:
 * {
 *   errorCode: error code [number],
 *   errorDescription: human readable error description [string]
 * }
 * </pre>
 *
 * @private
 * @param {number} iErrCode Error code
 * @param {string} strError Human readable error description
 * @param {function} onError Callback function provided by user
 */
Gmenu.Transport.displayError = function(iErrCode, strError, onError) {
  if (typeof onError == 'function') {
    onError({
      errorCode: iErrCode,
      errorDescription: strError
    });
  } else {
    alert(strError);
  }
};

/**
 * Translates a URL to the URL relative to the specified or to absolute URL.
 *
 * <pre>
 * Arguments object format:
 * {
 *   url: absolute or relative URL to translate [string] (if absolute, will be
 *    returned as is),
 *   relativeTo: "url" will be translated to the URL relative to this absolute
 *    or relative URL [string] (optional, current page URL by default)
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 * @return Translated URL
 * @type string
 */
Gmenu.Transport.translateUrl = function(objArgs) {
  if (!objArgs || !objArgs.url) {
    return null;
  }
  // Cut arguments part
  var arrFullUrl = objArgs.url.split('?', 2);
  var strUrl = arrFullUrl[0];
  // Check if it is absolute
  if (strUrl.charAt(0) == '/' || strUrl.indexOf(':') >= 0) {
    return objArgs.url;
  }
  // Make relative to current page URL by default
  if (!objArgs.relativeTo) {
    objArgs.relativeTo = document.location.toString();
  }
  // Remove arguments
  objArgs.relativeTo = objArgs.relativeTo.split("?", 2)[0];
  // Split URLs
  var arrUrl = strUrl.split('/');
  var arrRelativeTo = objArgs.relativeTo.split('/');
  // Remove file name
  arrRelativeTo.pop();
  // Form new URL
  for (var iToken = 0; iToken < arrUrl.length; iToken++) {
    var strToken = arrUrl[iToken];
    if (strToken == '..') {
      arrRelativeTo.pop();
    } else if (strToken != '.') {
      arrRelativeTo.push(strToken);
    }
  }
  arrFullUrl[0] = arrRelativeTo.join('/');
  // Restore arguments part
  return arrFullUrl.join('?');
};

/**
 * Associative array to keep list of loaded JS files to prevent duplicate loads.
 * @private
 */
Gmenu.Transport.loadedJS = {};

/**
 * Checks if specified JS file is already loaded.
 *
 * @private
 * @param {string} strUrl Absolute or relative URL of JS file
 * @param {string} strAbsoluteUrl Optional. Absolute URL of JS file
 * @return Loaded or not
 * @type boolean
 */
Gmenu.Transport.isLoadedJS = function(strUrl, strAbsoluteUrl) {
  // Get absolute URL of the JS file
  if (typeof strAbsoluteUrl == 'undefined') {
    strAbsoluteUrl = Gmenu.Transport.translateUrl({url: strUrl});
  }
  // Check in the list of loaded
  if (Gmenu.Transport.loadedJS[strAbsoluteUrl]) {
    return true;
  }
  // Try to find script tag
  var arrScripts = document.getElementsByTagName('script');
  for (var iScript = 0; iScript < arrScripts.length; iScript++) {
    var strSrc = arrScripts[iScript].getAttribute('src') || '';
    if (strSrc == strUrl) {
      // Add this URL to the list of loaded
      Gmenu.Transport.loadedJS[strAbsoluteUrl] = true;
      return true;
    }
  }
  // Not found
  return false;
};

/**
 * Returns path to the specified js file. Iterates over all loaded script
 * elements starting from the end. Finds specified js file in src attribute of
 * the script element. Splits src attribute value and returns path without js
 * file name.
 *
 * @param {string} strScriptFileName Script file name, e.g. 'zpmywidget.js'
 * @return Path to the script, e.g. '../src/' or '' if path is not found
 * @type string
 */
Gmenu.Transport.getPath = function(strScriptFileName) {
  // Get all script elements
  var arrScripts = document.getElementsByTagName('script');
  // Find the script in the list
  for (var iScript = arrScripts.length - 1; iScript >= 0; iScript--) {
    var strSrc = arrScripts[iScript].getAttribute('src') || '';
    var arrTokens = strSrc.split('/');
    // Remove last token
    var strLastToken = arrTokens.pop();
    if (strLastToken == strScriptFileName) {
      return arrTokens.join('/') + '/';
    }
  }
  // Search in loaded JS files
  for (var strSrc in Gmenu.Transport.loadedJS) {
    var arrTokens = strSrc.split('/');
    // Remove last token
    var strLastToken = arrTokens.pop();
    if (strLastToken == strScriptFileName) {
      return arrTokens.join('/') + '/';
    }
  }
  // Not found
  return '';
};

/**
 * Simply writes script tag to the document. Checks if specified JS file is
 * already loaded unless boolForce argument is true.
 *
 * @param {string} strSrc Src attribute value of the script element
 * @param {boolean} boolForce Optional. Force reload if it is already loaded
 */
Gmenu.Transport.include = function(strSrc, boolForce) {
  // Get absolute URL of the JS file
  var strAbsoluteUrl = Gmenu.Transport.translateUrl({url: strSrc});
  // Check if it is already loaded
  if (!boolForce && Gmenu.Transport.isLoadedJS(strSrc, strAbsoluteUrl)) {
    return;
  }
  // Include file
  document.write('<s' + 'cript type="text/javascript" src="' + strSrc +
   '"></s' + 'cript>');
  // Add this URL to the list of loaded
  Gmenu.Transport.loadedJS[strAbsoluteUrl] = true;
};

/**
 * Includes JS file into the page. Allows URLs from foreign domains.
 * Notice that script is loaded asynchronously.
 *
 * @param {string} strSrc Src attribute value of the script element
 * @param {boolean} boolForce Optional. Force reload if it is already loaded
 */
Gmenu.Transport.includeJS = function(strSrc) {
  var objContainer = document.body;
  if (!objContainer) {
    objContainer = document.getElementsByTagName('head')[0];
    if (!objContainer) {
      objContainer = document;
    }
  }
  var objScript = document.createElement('script');
  objScript.type = 'text/javascript';
  objScript.src = strSrc;
  // It's important for Safari to assign attributes before appending
  objContainer.appendChild(objScript);
};

/**
 * Fetches JS file using fetch and evaluates it in local scope.
 *
 * <pre>
 * When JS file is loaded successfully, onLoad callback function is called
 * without arguments. URL is added into Gmenu.Transport.loadedJS array
 * and will not be fetched again on next function call unless force argument is
 * set to true.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: [number] server status number (404, etc.),
 *   errorDescription: [string] human readable error description
 * }
 *
 * One of the arguments: module or url is required. When url is passed,
 * module argument is ignored.
 *
 * If module argument is used, function gets all "script" elements using
 * getElementsByTagName and searches for the first element having "src"
 * attribute value ending with (relativeModule + ".js") (default relativeModule
 * value is "transport"). Path to the module is taken from that src attribute
 * value and will be the same as path to relativeModule file.
 *
 * Arguments object format:
 * {
 *   url: [string, optional] absolute or relative URL of JS file,
 *   module: [string, optional] module name (file name without .js extension),
 *   relativeModule: [string, optional] search module in the same directory as
 *    relative module (default: 'transport') (file name without .js extension),
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   force: [boolean, optional] force reload if it is already loaded,
 *   onLoad: [function, optional] function reference to call on success,
 *   onError: [function, optional] function reference to call on error
 * }
 *
 * Note: If "force" is used, you should add 'r=' + Math.random() parameter to
 * URL to prevent loading from browser cache.
 * </pre>
 *
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.loadJS = function(objArgs) {
  // Check arguments
  if (!(objArgs instanceof Object)) {
    return;
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  // Get URL of JS file
  var strUrl = null;
  if (objArgs.url) {
    strUrl = objArgs.url;
  } else if (objArgs.module) {
    var strRelativeModule = 'transport.js';
    if (objArgs.relativeModule) {
      strRelativeModule = objArgs.relativeModule + '.js';
    }
    strUrl = Gmenu.Transport.getPath(strRelativeModule) + objArgs.module +
     '.js';
  } else {
    return;
  }
  // Get absolute URL of the JS file
  var strAbsoluteUrl = Gmenu.Transport.translateUrl({url: strUrl});
  // Check arguments
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  // Check if it is already loaded
  if (!objArgs.force && Gmenu.Transport.isLoadedJS(strUrl, strAbsoluteUrl)) {
    // onLoad callback
    if (typeof objArgs.onLoad == 'function') {
      objArgs.onLoad();
    }
    return;
  }
  // Load JS file
  Gmenu.Transport.fetch({
    url: strUrl,
    async: objArgs.async,
    onLoad: function(objRequest) {
      // Can be loaded in two processes simultaneously
      if (!Gmenu.Transport.loadedJS[strAbsoluteUrl]) {
        var arrTokens = strUrl.split('/');
        // Remove last token
        var strLastToken = arrTokens.pop();
        // Store path to current module
        Gmenu.lastLoadedModule = arrTokens.join('/') + '/';
        // Evaluate code in global scope
        var funcScripts = new Function(objRequest.responseText);
        funcScripts.call(window);

        // clear path to last loaded module
        Gmenu.lastLoadedModule = null;

        // Add this URL to the list of loaded
        Gmenu.Transport.loadedJS[strAbsoluteUrl] = true;
      }
      // onLoad callback
      if (typeof objArgs.onLoad == 'function') {
        objArgs.onLoad();
      }
    },
    onError: objArgs.onError
  });
};

/**
 * Includes CSS file into the page. Allows URLs from foreign domains.
 *
 * @param {string} strHref Href attribute value of the link element
 */
Gmenu.Transport.includeCSS = function(strHref) {
  // May appear only inside head
  var objContainer = document.getElementsByTagName('head')[0];
  if (!objContainer) {
    return;
  }
  var objLink = document.createElement('link');
  objLink.setAttribute('rel', 'stylesheet');
  objLink.setAttribute('type', 'text/css');
  objLink.setAttribute('href', strHref);
  objContainer.appendChild(objLink);
};

/**
 * Associative array to keep list of loaded CSS files to prevent duplicate
 * loads.
 * @private
 */
Gmenu.Transport.loadedCss = {};

/**
 * Fetches style sheet using fetch and loads it into the document.
 *
 * <pre>
 * When stylesheet is loaded successfully, onLoad callback function is called
 * without arguments. URL is added into Gmenu.Transport.loadedCss array
 * and will not be fetched again on next function call unless force argument is
 * set to true.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: server status number (404, etc.) [number],
 *   errorDescription: human readable error description [string]
 * }
 *
 * Arguments object format:
 * {
 *   url: absolute or relative URL of CSS file [string],
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   force: [boolean, optional] force reload if it is already loaded,
 *   onLoad: function reference to call on success [function] (optional),
 *   onError: function reference to call on error [function] (optional)
 * }
 *
 * Note: If "force" is used, you should add 'r=' + Math.random() parameter to
 * URL to prevent loading from browser cache.
 * </pre>
 *
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.loadCss = function(objArgs) {
  // Check arguments
  if (!(objArgs instanceof Object)) {
    return;
  }
  if (!objArgs.url) {
    return;
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  // Get absolute URL of the CSS file
  var strAbsoluteUrl = Gmenu.Transport.translateUrl({url: objArgs.url});
  // Check if it is already loaded
  if (!objArgs.force) {
    if (Gmenu.Transport.loadedCss[strAbsoluteUrl]) {
      // onLoad callback
      if (typeof objArgs.onLoad == 'function') {
        objArgs.onLoad();
      }
      return;
    }
    var arrLinks = document.getElementsByTagName('link');
    for (var iLnk = 0; iLnk < arrLinks.length; iLnk++) {
      var strHref = arrLinks[iLnk].getAttribute('href') || '';
      if (strHref == objArgs.url) {
        // Add this url to the list of loaded
        Gmenu.Transport.loadedCss[strAbsoluteUrl] = true;
        // onLoad callback
        if (typeof objArgs.onLoad == 'function') {
          objArgs.onLoad();
        }
        return;
      }
    }
  }
  // Load Gmenu.StyleSheet class definition
  Gmenu.Transport.loadJS({
    module: 'stylesheet',
    async: objArgs.async,
    onLoad: function() {
      // Load CSS file
      Gmenu.Transport.fetch({
        url: objArgs.url,
        async: objArgs.async,
        onLoad: function(objRequest) {
          // Parse CSS file.
          // Find URLs and translate them to absolute.
          // Find @import rules and load corresponding CSS files.
          var strCss = objRequest.responseText;
          var arrResultCss = [];
          // Will hold image URLs to preload
          var arrImgUrls = [];
          // Will hold CSS URLs to load
          var arrCssUrls = [];
          // Move first cursor to the beginning of the string
          var iPos = 0;
          // Move second cursor to the pattern
          var iNextPos = strCss.indexOf('url(', iPos);
          while (iNextPos >= 0) {
            // Move first cursor to the URL
            iNextPos += 4;
            // Check if this is @import rule
            var strToken = strCss.substring(iPos, iNextPos);
            var boolIsImport = /@import\s+url\($/.test(strToken);
            // Add part of the string before URL
            arrResultCss.push(strToken);
            // Move second cursor to the new location to start the search from
            iPos = iNextPos;
            // Search the end of URL
            iNextPos = strCss.indexOf(')', iPos);
            if (iNextPos >= 0) {
              // Remove quotes
              var strImgUrl = strCss.substring(iPos, iNextPos);
              strImgUrl = strImgUrl.replace(/['"]/g, '');
              // Translate image URL relative to CSS file URL
              strImgUrl = Gmenu.Transport.translateUrl({
                url: strImgUrl,
                relativeTo: objArgs.url
              });
              // Convert to absolute URL
              strImgUrl = Gmenu.Transport.translateUrl({
                url: strImgUrl
              });
              // Add translated URL
              arrResultCss.push(strImgUrl);
              // Add URL to the list
              if (boolIsImport) {
                // Add CSS URL to load list
                arrCssUrls.push(strImgUrl);
              } else {
                // Add image URL to preload list
                arrImgUrls.push(strImgUrl);
              }
              // Move second cursor to the new location to start the search from
              iPos = iNextPos;
              // Search next pattern
              iNextPos = strCss.indexOf('url(', iPos);
            }
          }
          // Add the rest of string
          arrResultCss.push(strCss.substr(iPos));
          // Get translated CSS text
          strCss = arrResultCss.join('');
          // Load CSS files
          Gmenu.Transport.loadCssList({
            urls: arrCssUrls,
            async: objArgs.async,
            onLoad: function() {
              // Add style sheet rules into the page
              var objStyleSheet = new Gmenu.StyleSheet();
              objStyleSheet.addParse(strCss);
              // onLoad callback
              if (typeof objArgs.onLoad == 'function') {
                objArgs.onLoad();
              }
            }
          });
          // Add this URL to the list of loaded
          Gmenu.Transport.loadedCss[strAbsoluteUrl] = true;
          // Preload images
          Gmenu.Transport.preloadImages({
            urls: arrImgUrls,
            timeout: 60000 // 1 minute
          });
        },
        onError: objArgs.onError
      });
    },
    onError: objArgs.onError
  });
};

/**
 * Loads several CSS files one by one it into the document.
 *
 * <pre>
 * This function behaves differently from other Gmenu.Transport functions.
 * onLoad callback function will be called in any case, even if errors occured
 * during loading. If there are multiple errors, onError callback function will
 * be called once for every passed URL that wasn't loaded successfully.
 *
 * onLoad callback function is called without arguments.
 *
 * onError callback function receives following object:
 * {
 *   errorCode: server status number (404, etc.) [number],
 *   errorDescription: human readable error description [string]
 * }
 *
 * Arguments object format:
 * {
 *   urls: array of absolute or relative URLs of CSS files to load [object]
 *    (files will be loaded in order they appear in the array),
 *   async: [boolean, optional] use asynchronous mode (default: true),
 *   force: [boolean, optional] force reload if it is already loaded,
 *   onLoad: function reference to call on completion [function] (optional),
 *   onError: function reference to call on error [function] (optional)
 * }
 *
 * Note: If "force" is used, you should add 'r=' + Math.random() parameter to
 * URL to prevent loading from browser cache.
 * </pre>
 *
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.loadCssList = function(objArgs) {
  // Check arguments
  if (!(objArgs instanceof Object)) {
    return;
  }
  if (typeof objArgs.async == 'undefined') {
    objArgs.async = true;
  }
  if (!objArgs.onLoad) {
    objArgs.onLoad = null;
  }
  if (!objArgs.onError) {
    objArgs.onError = null;
  }
  if (!objArgs.urls || !objArgs.urls.length) {
    // onLoad callback
    if (typeof objArgs.onLoad == 'function') {
      objArgs.onLoad();
    }
    return;
  }
  // Get first URL in the array
  var strUrl = objArgs.urls.shift();
  // CSS file onLoad handler
  var funcOnLoad = function() {
    // Load the rest of URLs
    Gmenu.Transport.loadCssList({
      urls: objArgs.urls,
      async: objArgs.async,
      force: objArgs.force,
      onLoad: objArgs.onLoad,
      onError: objArgs.onError
    });
  };
  // Load CSS file
  Gmenu.Transport.loadCss({
    url: strUrl,
    async: objArgs.async,
    force: objArgs.force,
    onLoad: funcOnLoad,
    onError: function(objError) {
      Gmenu.Transport.displayError(objError.errorCode,
       objError.errorDescription, objArgs.onError);
      funcOnLoad();
    }
  });
};

/**
 * Array to hold image preloads.
 * @private
 */
Gmenu.Transport.imagePreloads = [];

/**
 * Preloads one or several images at once. See Gmenu.PreloadImages class
 * (utils/preloadimages.js) for details.
 *
 * <pre>
 * Arguments object format:
 * {
 *   urls: [object] array of absolute or relative image URLs to preload,
 *   onLoad: [function, optional] onload event handler,
 *   timeout: [number, optional] number of milliseconds to wait for onload
 *    event before forcing it
 * }
 * </pre>
 *
 * @param {object} objArgs Arguments object
 */
Gmenu.Transport.preloadImages = function(objArgs) {
  Gmenu.Transport.loadJS({
    module: 'preloadimages',
    onLoad: function() {
      Gmenu.Transport.imagePreloads.push(new Gmenu.PreloadImages(objArgs));
    }
  });
};
if (typeof Gmenu == 'undefined') {
  /**
   * @ignore Namespace definition.
   */
  Gmenu = {};
}

/**
 * Base event-driven class. Contains basic methods for event-driven class.
 *
 * @constructor
 */
Gmenu.EventDriven = function() {};

/**
 * Initializes object.
 */
Gmenu.EventDriven.prototype.init = function() {
  this.events = {};
};

/**
 * Adds event listener.
 *
 * @param {string} strEvent Event name
 * @param {function} funcListener Event listener
 * @param {function} first If true funcListener will be put as the first one
 */
Gmenu.EventDriven.prototype.addEventListener = function(strEvent,
 funcListener, first) {
  if (typeof funcListener != "function") {
    return false;
  }
  if (!this.events[strEvent]) {
    this.events[strEvent] = {
      listeners: []
    };
  }
  if (!first) {
    this.events[strEvent].listeners.push(funcListener);
  } else {
    this.events[strEvent].listeners.unshift(funcListener);
  }
};

/**
 * Removes event listener.
 *
 * @param {string} strEvent Event name
 * @param {function} funcListener Event listener
 */
Gmenu.EventDriven.prototype.removeEventListener = function(strEvent,
 funcListener) {
  if (!this.events[strEvent]) {
    return;
  }
  var arrListeners = this.events[strEvent].listeners;
  for (var iListener = 0; iListener < arrListeners.length; iListener++) {
    if (arrListeners[iListener] == funcListener) {
      arrListeners.splice(iListener, 1);
      return;
    }
  }
};

/**
 * Fires event.
 *
 * @param {string} strEvent Event name
 */
Gmenu.EventDriven.prototype.fireEvent = function(strEvent) {
  if (!this.events[strEvent]) {
    return;
  }
  var arrListeners = this.events[strEvent].listeners;
  for (var iListener = 0; iListener < arrListeners.length; iListener++) {
    // Remove first argument
    var arrArgs = [].slice.call(arguments, 1);
    // Call in scope of this object
    arrListeners[iListener].apply(this, arrArgs);
  }
};

/**
 * Base widget class.
 *
 * <pre>
 * Defines following config options:
 *
 * <b>theme</b> [string] Theme name that will be used to display the widget.
 * Corresponding CSS file will be picked and added into the HTML document
 * automatically. Case insensitive. Default: "default".
 * May also contain relative or absolute URL of themes directory.
 * E.g. "../themes/default.css" or "http://my.web.host/themes/default.css".
 *
 * <b>themePath</b> [string] Relative or absolute URL to themes directory.
 * Trailing slash is required. Default: path to child widget's file +
 * "../themes/". You may also include path into "theme" option instead of using
 * "themePath" option.
 *
 * <b>asyncTheme</b> [boolean] Load theme asynchronously. This means that script
 * execution will not be suspended until theme is loaded. Theme will be applied
 * once it is loaded. Default: false.
 *
 * <b>source</b> Depends on "sourceType" option. Possible sources:
 * -----------------------------------------------------------------------------
 * sourceType     | source
 * ---------------|-------------------------------------------------------------
 * 1) "html"      | [object or string] HTMLElement or its id.
 * 2) "html/text" | [string] HTML fragment.
 * 3) "html/url"  | [string] URL of HTML fragment.
 * 4) "json"      | [object or string] JSON object or string (http://json.org).
 * 5) "json/url"  | [string] URL of JSON data source.
 * 6) "xml"       | [object or string] XMLDocument object or XML string.
 * 7) "xml/url"   | [string] URL of XML data source.
 * -----------------------------------------------------------------------------
 *
 * <b>sourceType</b> [string] Used together with "source" option to specify how
 * source should be processed. Possible source types:
 * "html", "html/text", "html/url", "json", "json/url", "xml", "xml/url".
 * JSON format is described at http://www.json.org.
 *
 * <b>callbackSource</b> [function] May be used instead of "source" and
 * "sourceType" config options to get source depending on passed arguments.
 * Receives object with passed arguments. Must return following object:
 * {
 *   source: [object or string] see table above for possible sources,
 *   sourceType: [string] see table above for possible source types
 * }
 *
 * <b>asyncSource</b> [boolean] Load source asynchronously. This means that
 * script execution will not be suspended until source is loaded. Source will be
 * processed once it is loaded. Default: true.
 *
 * <b>reliableSource</b> [boolean] Used together with "json" or "json/url"
 * sourceType to skip JSON format verification. It saves a lot of time for large
 * data sets. Default: true.
 *
 * <b>eventListeners</b> [object] Associative array with event listeners:
 * {
 *   [string] event name: [function] event listener,
 *   ...
 * }
 * </pre>
 *
 * @constructor
 * @extends Gmenu.EventDriven
 * @param {object} objArgs User configuration
 */
Gmenu.Widget = function(objArgs) {
  // User configuration
  this.config = {};
  // Call constructor of superclass
  Gmenu.Widget.SUPERconstructor.call(this);
  // Initialize object
  this.init(objArgs);
};

// Inherit EventDriven
Gmenu.inherit(Gmenu.Widget, Gmenu.EventDriven);

/**
 * @private Holds path to this file.
 */
Gmenu.Widget.path = Gmenu.getPath();

/**
 * Initializes object.
 *
 * <pre>
 * Important: Before calling this method, define config options for the widget.
 * Initially "this.config" object should contain all config options with their
 * default values. Then values of config options will be changed with user
 * configuration in this method. Config options provided by user that were not
 * found in "this.config" object will be ignored.
 * </pre>
 *
 * @param {object} objArgs User configuration
 */
Gmenu.Widget.prototype.init = function(objArgs) {
  // Call parent method
  Gmenu.Widget.SUPERclass.init.call(this);
  // Add this widget to the list
  if (typeof this.id == 'undefined') {
    this.id = Gmenu.Widget.all.length;
    Gmenu.Widget.all.push(this);
  }
  // Default configuration
  this.defineConfigOption('theme', 'default');
  if (typeof this.constructor.path != 'undefined') {
    this.defineConfigOption('themePath', this.constructor.path + '../themes/');
  } else {
    this.defineConfigOption('themePath', '../themes/');
  }
  this.defineConfigOption('asyncTheme', false);
  this.defineConfigOption('source');
  this.defineConfigOption('sourceType');
  this.defineConfigOption('callbackSource');
  this.defineConfigOption('asyncSource', true);
  this.defineConfigOption('reliableSource', true);
  this.defineConfigOption('eventListeners', {});
  // Get user configuration
  if (objArgs) {
    for (var strOption in objArgs) {
      if (typeof this.config[strOption] != 'undefined') {
        this.config[strOption] = objArgs[strOption];
      } else {
        Gmenu.Log({
          description: "Unknown config option: " + strOption
        });
      }
    }
  }
  // Add custom event listeners
  this.addUserEventListeners();
  // Add standard event listeners
  this.addStandardEventListeners();
  // Load theme
  this.loadTheme();
};

/**
 * @private Array to access any widget on the page by its id number.
 */
Gmenu.Widget.all = [];

/**
 * @private Defines config option if it is not defined yet. Sets default value
 * of new config option. If default value is not specified, it is set to null.
 *
 * @param {string} strOption Config option name
 * @param {any} val Optional. Config option default value
 */
Gmenu.Widget.prototype.defineConfigOption = function(strOption, val) {
  if (typeof this.config[strOption] == 'undefined') {
    if (typeof val == 'undefined') {
      this.config[strOption] = null;
    } else {
      this.config[strOption] = val;
    }
  }
};

/**
 * Adds custom event listeners.
 */
Gmenu.Widget.prototype.addUserEventListeners = function() {
  for (var strEvent in this.config.eventListeners) {
    if (this.config.eventListeners.hasOwnProperty(strEvent)) {
      this.addEventListener(strEvent, this.config.eventListeners[strEvent]);
    }
  }
};

/**
 * Adds standard event listeners.
 */
Gmenu.Widget.prototype.addStandardEventListeners = function() {
  this.addEventListener('loadThemeError', function(objError) {
    Gmenu.Log({
      description: 'Can not load theme. ' + objError.errorDescription
    });
  });
};

/**
 * Loads specified theme.
 */
Gmenu.Widget.prototype.loadTheme = function() {
	// Correct theme config option
	if (typeof this.config.theme == 'string' && this.config.theme.length) {
		// Remove path
		var iPos = this.config.theme.lastIndexOf('/');
		if (iPos >= 0) {
			iPos++; // Go to first char of theme name
			this.config.themePath = this.config.theme.substring(0, iPos);
			this.config.theme = this.config.theme.substring(iPos);
		}
		// Remove file extension
		iPos = this.config.theme.lastIndexOf('.');
		if (iPos >= 0) {
			this.config.theme = this.config.theme.substring(0, iPos);
		}
		// Make lower case
		this.config.theme = this.config.theme.toLowerCase();
	} else {
		this.config.theme = '';
	}
	// Load theme
	if(this.config.theme){
    this.fireEvent('loadThemeStart');
		this.themeLoaded = false;
		var objWidget = this;
		var strUrl = this.config.themePath + this.config.theme + '.css';
		Gmenu.Transport.loadCss({
			// URL of theme file
			url: strUrl,
			// Suspend script execution until theme is loaded or error received
			async: this.config.asyncTheme,
			// Onload event handler
			onLoad: function() {
        objWidget.fireEvent('loadThemeEnd');
				objWidget.themeLoaded = true;
				objWidget.hideLoader();
			},
			onError: function(objError) {
        objWidget.fireEvent('loadThemeEnd');
        objWidget.fireEvent('loadThemeError', objError);
				objWidget.themeLoaded = true;
				objWidget.hideLoader();
			}
		});
	}
}
/**
 * Forms class name from theme name and provided prefix and suffix.
 *
 * <pre>
 * Arguments object format:
 * {
 *   prefix: [string, optional] prefix,
 *   suffix: [string, optional] suffix
 * }
 * E.g. if this.config.theme == 'default' and following object provided
 * {
 *   prefix: 'zpWidget',
 *   suffix: 'Container'
 * },
 * class name will be 'zpWidgetDefaultContainer'.
 * </pre>
 *
 * @param objArgs [object] Arguments object
 * @return Class name
 * @type string
 */
Gmenu.Widget.prototype.getClassName = function(objArgs) {
  var arrClassName = [];
  if (objArgs && objArgs.prefix) {
    arrClassName.push(objArgs.prefix);
  }
  if (this.config.theme != '') {
    arrClassName.push(this.config.theme.charAt(0).toUpperCase());
    arrClassName.push(this.config.theme.substr(1));
  }
  if (objArgs && objArgs.suffix) {
    arrClassName.push(objArgs.suffix);
  }
  return arrClassName.join('');
};

/**
 * @private if theme for current widget is not loaded yet - this method will
 * hide widget container and show progress bar instead of it.
 */
Gmenu.Widget.prototype.showLoader = function(message){
	if(this.container != null && this.config.theme && !this.themeLoaded){
		// if window content is not fulle loaded - progress bar can resize
		// incorrectly
		if(!Gmenu.windowLoaded){
			var self = this;
			Gmenu.Utils.addEvent(window, "load", function(){self.showLoader(message)});
			return null;
		}

		if(typeof(Gmenu.Progress) == 'undefined'){
			var self = this;

			Gmenu.Transport.loadJS({
				module: 'progress',
				onLoad: function() {
					// if theme is already loaded - do not show progress bar
					if(self.themeLoaded){
						return null;
					}

					self.showLoader(message);
				}
			});

			return null;
		}

		this.loader = new Gmenu.Progress({
			container: this.container,
			themePath: Gmenu.Progress.path + "../../zpextra/themes/progress/"
		});

		this.loader.start(message || 'loading');
		this.container.style.visibility = 'hidden';
	}
}

/**
 * @private Hides progressbar created using #showLoader and shows widget.
 * This method will be called automatically on theme loading.
 */
Gmenu.Widget.prototype.hideLoader = function(){
	if(this.loader && this.loader.isActive()){
		this.container.style.visibility = '';
		this.loader.stop();
	}
}

/**
 * Shows widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 * @param {function} onFinish Function to call on effect end.
 */
Gmenu.Widget.prototype.showContainer = function(effects, animSpeed, onFinish){
	return this.showHideContainer(effects, animSpeed, onFinish, true);
}

/**
 * Hides widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 */
Gmenu.Widget.prototype.hideContainer = function(effects, animSpeed, onFinish){
	return this.showHideContainer(effects, animSpeed, onFinish, false);
}

/**
 * Show/hides widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 * @param {boolean} show if true - show widget. Otherwise - hide.
 */
Gmenu.Widget.prototype.showHideContainer = function(effects, animSpeed, onFinish, show){
	if(this.container == null){
		return null;
	}

	if(typeof(Gmenu.Effects) == 'undefined'){
		var self = this;

		Gmenu.Transport.loadJS({
			url: Gmenu.Widget.path + '../../zpeffects/src/effects.js',
			onLoad: function() {
				self.showHideContainer(effects, animSpeed, onFinish, show);
			}
		});

		return false;
	}

	if(animSpeed == null && isNaN(parseInt(animSpeed))){
		animSpeed = 5;
	}

	if(effects == null || effects.length == 0){
		if(show){
			this.container.style.display = this.originalContainerDisplay;
			this.originalContainerDisplay = null;
		} else {
			this.originalContainerDisplay = this.container.style.display;
			this.container.style.display = 'none';
		}

		if (onFinish) {
			onFinish();
		}
	} else {
		if(show){
			Gmenu.Effects.show(this.container, animSpeed, effects, onFinish);
		} else {
			Gmenu.Effects.hide(this.container, animSpeed, effects, onFinish);
		}
	}

	return true;
}

/**
 * Loads data from the specified source.
 *
 * <pre>
 * If source is URL, fires following events:
 * <ul>
 * <li><i>fetchSourceStart</i> when Gmenu.Transport#fetch is called</li>
 * <li><i>fetchSourceEnd</i> when Gmenu.Transport#fetch returns result or
 *  error</li>
 * <li><i>fetchSourceError</i> when Gmenu.Transport#fetch returns error.
 *  fetchSourceError event listener receives object returned by
 *  Gmenu.Transport#fetch</li>
 * </ul>
 *
 * Fires following events:
 * <ul>
 * <li><i>loadDataStart</i> when data parsing is started</li>
 * <li><i>loadDataEnd</i> when data parsing is ended or error occured during
 *  fetch</li>
 * </ul>
 * </pre>
 *
 * @param {object} objArgs Arguments object passed to callbackSource function
 */
Gmenu.Widget.prototype.loadData = function(objArgs) {
  // Get source using callback function
  if (typeof this.config.callbackSource == 'function') {
    var objSource = this.config.callbackSource(objArgs);
    if (objSource) {
      if (typeof objSource.source != 'undefined') {
        this.config.source = objSource.source;
      }
      if (typeof objSource.sourceType != 'undefined') {
        this.config.sourceType = objSource.sourceType;
      }
    }
  }
  // Process source
  if (this.config.source != null && this.config.sourceType != null) {
    var strSourceType = this.config.sourceType.toLowerCase();
    if (strSourceType == 'html') {
      this.fireEvent('loadDataStart');
      this.loadDataHtml(Gmenu.Widget.getElementById(this.config.source));
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'html/text') {
      this.fireEvent('loadDataStart');
      this.loadDataHtmlText(this.config.source);
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'html/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetch({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Onload event handler
        onLoad: function(objRequest) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataHtmlText(objRequest.responseText);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    } else if (strSourceType == 'json') {
      this.fireEvent('loadDataStart');
      if (typeof this.config.source == 'object') {
        this.loadDataJson(this.config.source);
      } else if (this.config.reliableSource) {
        this.loadDataJson(eval(this.config.source));
      } else {
        this.loadDataJson(Gmenu.Transport.parseJson({
          strJson: this.config.source
        }));
      }
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'json/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetchJsonObj({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Skip JSON format verification
        reliable: this.config.reliableSource,
        // Onload event handler
        onLoad: function(objResult) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataJson(objResult);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    } else if (strSourceType == 'xml') {
      this.fireEvent('loadDataStart');
      if (typeof this.config.source == 'object') {
        this.loadDataXml(this.config.source);
      } else {
        this.loadDataXml(Gmenu.Transport.parseXml({
          strXml: this.config.source
        }));
      }
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'xml/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetchXmlDoc({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Onload event handler
        onLoad: function(objResult) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataXml(objResult);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    }
  } else {
    this.fireEvent('loadDataStart');
    this.loadDataHtml(Gmenu.Widget.getElementById(this.config.source));
    this.fireEvent('loadDataEnd');
  }
};

/**
 * Loads data from the HTML source. Override this in child class.
 *
 * @param {object} objSource Source HTMLElement object
 */
Gmenu.Widget.prototype.loadDataHtml = function(objSource) {};

/**
 * Loads data from the HTML fragment source.
 *
 * @param {string} strSource Source HTML fragment
 */
Gmenu.Widget.prototype.loadDataHtmlText = function(strSource) {
  // Parse HTML fragment
  var objTempContainer = Gmenu.Transport.parseHtml(strSource);
  // Load data
  this.loadDataHtml(objTempContainer.firstChild);
};

/**
 * Loads data from the JSON source. Override this in child class.
 *
 * @param {object} objSource Source JSON object
 */
Gmenu.Widget.prototype.loadDataJson = function(objSource) {};

/**
 * Loads data from the XML source. Override this in child class.
 *
 * @param {object} objSource Source XMLDocument object
 */
Gmenu.Widget.prototype.loadDataXml = function(objSource) {
};

/**
 * Converts element id to reference.
 *
 * @param {string} element Element id
 * @return Reference to element
 * @type object
 */
Gmenu.Widget.getElementById = function(element) {
  if (typeof element == 'string') {
    return document.getElementById(element);
  }
  return element;
};

/**
 * Returns style attribute of the specified element.
 *
 * @param {object} element Element
 * @return Style attribute value
 * @type string
 */
Gmenu.Widget.getStyle = function(element) {
  var style = element.getAttribute('style') || '';
  if (typeof style == 'string') {
    return style;
  }
  return style.cssText;
};

/**
 * Emulates "window.event" for certain events in Mozilla. To be able to access
 * Event object when event handler is set using element attribute, e.g.
 * &lt;a onclick="eventHandler()"&gt;.
 *
 * @param {object} arrEventNames array of event names where "window.event" is
 * needed, e.g. ['click', 'dblclick'].
 */
Gmenu.Widget.emulateWindowEvent = function(arrEventNames) {
  if (document.addEventListener) {
    // Set up emulation for certain events
    for (var iEvent = 0; iEvent < arrEventNames.length; iEvent++) {
      document.addEventListener(arrEventNames[iEvent], function (objEvent) {
        if (objEvent) {
          window.event = objEvent;
        }
      }, true);
    }
  }
};

// Replace Gmenu.include with more complex function from transport library
if (Gmenu.Transport && Gmenu.Transport.include) {
  Gmenu.include = Gmenu.Transport.include;
}

if (typeof Gmenu == 'undefined') {
  /**
   * @ignore Namespace definition.
   */
  Gmenu = {};
}

/**
 * Base event-driven class. Contains basic methods for event-driven class.
 *
 * @constructor
 */
Gmenu.EventDriven = function() {};

/**
 * Initializes object.
 */
Gmenu.EventDriven.prototype.init = function() {
  this.events = {};
};

/**
 * Adds event listener.
 *
 * @param {string} strEvent Event name
 * @param {function} funcListener Event listener
 * @param {function} first If true funcListener will be put as the first one
 */
Gmenu.EventDriven.prototype.addEventListener = function(strEvent,
 funcListener, first) {
  if (typeof funcListener != "function") {
    return false;
  }
  if (!this.events[strEvent]) {
    this.events[strEvent] = {
      listeners: []
    };
  }
  if (!first) {
    this.events[strEvent].listeners.push(funcListener);
  } else {
    this.events[strEvent].listeners.unshift(funcListener);
  }
};

/**
 * Removes event listener.
 *
 * @param {string} strEvent Event name
 * @param {function} funcListener Event listener
 */
Gmenu.EventDriven.prototype.removeEventListener = function(strEvent,
 funcListener) {
  if (!this.events[strEvent]) {
    return;
  }
  var arrListeners = this.events[strEvent].listeners;
  for (var iListener = 0; iListener < arrListeners.length; iListener++) {
    if (arrListeners[iListener] == funcListener) {
      arrListeners.splice(iListener, 1);
      return;
    }
  }
};

/**
 * Fires event.
 *
 * @param {string} strEvent Event name
 */
Gmenu.EventDriven.prototype.fireEvent = function(strEvent) {
  if (!this.events[strEvent]) {
    return;
  }
  var arrListeners = this.events[strEvent].listeners;
  for (var iListener = 0; iListener < arrListeners.length; iListener++) {
    // Remove first argument
    var arrArgs = [].slice.call(arguments, 1);
    // Call in scope of this object
    arrListeners[iListener].apply(this, arrArgs);
  }
};

/**
 * Base widget class.
 *
 * <pre>
 * Defines following config options:
 *
 * <b>theme</b> [string] Theme name that will be used to display the widget.
 * Corresponding CSS file will be picked and added into the HTML document
 * automatically. Case insensitive. Default: "default".
 * May also contain relative or absolute URL of themes directory.
 * E.g. "../themes/default.css" or "http://my.web.host/themes/default.css".
 *
 * <b>themePath</b> [string] Relative or absolute URL to themes directory.
 * Trailing slash is required. Default: path to child widget's file +
 * "../themes/". You may also include path into "theme" option instead of using
 * "themePath" option.
 *
 * <b>asyncTheme</b> [boolean] Load theme asynchronously. This means that script
 * execution will not be suspended until theme is loaded. Theme will be applied
 * once it is loaded. Default: false.
 *
 * <b>source</b> Depends on "sourceType" option. Possible sources:
 * -----------------------------------------------------------------------------
 * sourceType     | source
 * ---------------|-------------------------------------------------------------
 * 1) "html"      | [object or string] HTMLElement or its id.
 * 2) "html/text" | [string] HTML fragment.
 * 3) "html/url"  | [string] URL of HTML fragment.
 * 4) "json"      | [object or string] JSON object or string (http://json.org).
 * 5) "json/url"  | [string] URL of JSON data source.
 * 6) "xml"       | [object or string] XMLDocument object or XML string.
 * 7) "xml/url"   | [string] URL of XML data source.
 * -----------------------------------------------------------------------------
 *
 * <b>sourceType</b> [string] Used together with "source" option to specify how
 * source should be processed. Possible source types:
 * "html", "html/text", "html/url", "json", "json/url", "xml", "xml/url".
 * JSON format is described at http://www.json.org.
 *
 * <b>callbackSource</b> [function] May be used instead of "source" and
 * "sourceType" config options to get source depending on passed arguments.
 * Receives object with passed arguments. Must return following object:
 * {
 *   source: [object or string] see table above for possible sources,
 *   sourceType: [string] see table above for possible source types
 * }
 *
 * <b>asyncSource</b> [boolean] Load source asynchronously. This means that
 * script execution will not be suspended until source is loaded. Source will be
 * processed once it is loaded. Default: true.
 *
 * <b>reliableSource</b> [boolean] Used together with "json" or "json/url"
 * sourceType to skip JSON format verification. It saves a lot of time for large
 * data sets. Default: true.
 *
 * <b>eventListeners</b> [object] Associative array with event listeners:
 * {
 *   [string] event name: [function] event listener,
 *   ...
 * }
 * </pre>
 *
 * @constructor
 * @extends Gmenu.EventDriven
 * @param {object} objArgs User configuration
 */
Gmenu.Widget = function(objArgs) {
  // User configuration
  this.config = {};
  // Call constructor of superclass
  Gmenu.Widget.SUPERconstructor.call(this);
  // Initialize object
  this.init(objArgs);
};

// Inherit EventDriven
Gmenu.inherit(Gmenu.Widget, Gmenu.EventDriven);

/**
 * @private Holds path to this file.
 */
Gmenu.Widget.path = Gmenu.getPath();

/**
 * Initializes object.
 *
 * <pre>
 * Important: Before calling this method, define config options for the widget.
 * Initially "this.config" object should contain all config options with their
 * default values. Then values of config options will be changed with user
 * configuration in this method. Config options provided by user that were not
 * found in "this.config" object will be ignored.
 * </pre>
 *
 * @param {object} objArgs User configuration
 */
Gmenu.Widget.prototype.init = function(objArgs) {
  // Call parent method
  Gmenu.Widget.SUPERclass.init.call(this);
  // Add this widget to the list
  if (typeof this.id == 'undefined') {
    this.id = Gmenu.Widget.all.length;
    Gmenu.Widget.all.push(this);
  }
  // Default configuration
  this.defineConfigOption('theme', 'default');
  if (typeof this.constructor.path != 'undefined') {
    this.defineConfigOption('themePath', this.constructor.path + '../themes/');
  } else {
    this.defineConfigOption('themePath', '../themes/');
  }
  this.defineConfigOption('asyncTheme', false);
  this.defineConfigOption('source');
  this.defineConfigOption('sourceType');
  this.defineConfigOption('callbackSource');
  this.defineConfigOption('asyncSource', true);
  this.defineConfigOption('reliableSource', true);
  this.defineConfigOption('eventListeners', {});
  // Get user configuration
  if (objArgs) {
    for (var strOption in objArgs) {
      if (typeof this.config[strOption] != 'undefined') {
        this.config[strOption] = objArgs[strOption];
      } else {
        Gmenu.Log({
          description: "Unknown config option: " + strOption
        });
      }
    }
  }
  // Add custom event listeners
  this.addUserEventListeners();
  // Add standard event listeners
  this.addStandardEventListeners();
  // Load theme
  this.loadTheme();
};

/**
 * @private Array to access any widget on the page by its id number.
 */
Gmenu.Widget.all = [];

/**
 * @private Defines config option if it is not defined yet. Sets default value
 * of new config option. If default value is not specified, it is set to null.
 *
 * @param {string} strOption Config option name
 * @param {any} val Optional. Config option default value
 */
Gmenu.Widget.prototype.defineConfigOption = function(strOption, val) {
  if (typeof this.config[strOption] == 'undefined') {
    if (typeof val == 'undefined') {
      this.config[strOption] = null;
    } else {
      this.config[strOption] = val;
    }
  }
};

/**
 * Adds custom event listeners.
 */
Gmenu.Widget.prototype.addUserEventListeners = function() {
  for (var strEvent in this.config.eventListeners) {
    if (this.config.eventListeners.hasOwnProperty(strEvent)) {
      this.addEventListener(strEvent, this.config.eventListeners[strEvent]);
    }
  }
};

/**
 * Adds standard event listeners.
 */
Gmenu.Widget.prototype.addStandardEventListeners = function() {
  this.addEventListener('loadThemeError', function(objError) {
    Gmenu.Log({
      description: 'Can not load theme. ' + objError.errorDescription
    });
  });
};

/**
 * Loads specified theme.
 */
Gmenu.Widget.prototype.loadTheme = function() {
	// Correct theme config option
	if (typeof this.config.theme == 'string' && this.config.theme.length) {
		// Remove path
		var iPos = this.config.theme.lastIndexOf('/');
		if (iPos >= 0) {
			iPos++; // Go to first char of theme name
			this.config.themePath = this.config.theme.substring(0, iPos);
			this.config.theme = this.config.theme.substring(iPos);
		}
		// Remove file extension
		iPos = this.config.theme.lastIndexOf('.');
		if (iPos >= 0) {
			this.config.theme = this.config.theme.substring(0, iPos);
		}
		// Make lower case
		this.config.theme = this.config.theme.toLowerCase();
	} else {
		this.config.theme = '';
	}
	// Load theme
	if(this.config.theme){
    this.fireEvent('loadThemeStart');
		this.themeLoaded = false;
		var objWidget = this;
		var strUrl = this.config.themePath + this.config.theme + '.css';
		Gmenu.Transport.loadCss({
			// URL of theme file
			url: strUrl,
			// Suspend script execution until theme is loaded or error received
			async: this.config.asyncTheme,
			// Onload event handler
			onLoad: function() {
        objWidget.fireEvent('loadThemeEnd');
				objWidget.themeLoaded = true;
				objWidget.hideLoader();
			},
			onError: function(objError) {
        objWidget.fireEvent('loadThemeEnd');
        objWidget.fireEvent('loadThemeError', objError);
				objWidget.themeLoaded = true;
				objWidget.hideLoader();
			}
		});
	}
}
/**
 * Forms class name from theme name and provided prefix and suffix.
 *
 * <pre>
 * Arguments object format:
 * {
 *   prefix: [string, optional] prefix,
 *   suffix: [string, optional] suffix
 * }
 * E.g. if this.config.theme == 'default' and following object provided
 * {
 *   prefix: 'zpWidget',
 *   suffix: 'Container'
 * },
 * class name will be 'zpWidgetDefaultContainer'.
 * </pre>
 *
 * @param objArgs [object] Arguments object
 * @return Class name
 * @type string
 */
Gmenu.Widget.prototype.getClassName = function(objArgs) {
  var arrClassName = [];
  if (objArgs && objArgs.prefix) {
    arrClassName.push(objArgs.prefix);
  }
  if (this.config.theme != '') {
    arrClassName.push(this.config.theme.charAt(0).toUpperCase());
    arrClassName.push(this.config.theme.substr(1));
  }
  if (objArgs && objArgs.suffix) {
    arrClassName.push(objArgs.suffix);
  }
  return arrClassName.join('');
};

/**
 * @private if theme for current widget is not loaded yet - this method will
 * hide widget container and show progress bar instead of it.
 */
Gmenu.Widget.prototype.showLoader = function(message){
	if(this.container != null && this.config.theme && !this.themeLoaded){
		// if window content is not fulle loaded - progress bar can resize
		// incorrectly
		if(!Gmenu.windowLoaded){
			var self = this;
			Gmenu.Utils.addEvent(window, "load", function(){self.showLoader(message)});
			return null;
		}

		if(typeof(Gmenu.Progress) == 'undefined'){
			var self = this;

			Gmenu.Transport.loadJS({
				module: 'progress',
				onLoad: function() {
					// if theme is already loaded - do not show progress bar
					if(self.themeLoaded){
						return null;
					}

					self.showLoader(message);
				}
			});

			return null;
		}

		this.loader = new Gmenu.Progress({
			container: this.container,
			themePath: Gmenu.Progress.path + "../../zpextra/themes/progress/"
		});

		this.loader.start(message || 'loading');
		this.container.style.visibility = 'hidden';
	}
}

/**
 * @private Hides progressbar created using #showLoader and shows widget.
 * This method will be called automatically on theme loading.
 */
Gmenu.Widget.prototype.hideLoader = function(){
	if(this.loader && this.loader.isActive()){
		this.container.style.visibility = '';
		this.loader.stop();
	}
}

/**
 * Shows widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 * @param {function} onFinish Function to call on effect end.
 */
Gmenu.Widget.prototype.showContainer = function(effects, animSpeed, onFinish){
	return this.showHideContainer(effects, animSpeed, onFinish, true);
}

/**
 * Hides widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 */
Gmenu.Widget.prototype.hideContainer = function(effects, animSpeed, onFinish){
	return this.showHideContainer(effects, animSpeed, onFinish, false);
}

/**
 * Show/hides widget using given effects and animation speed. You need to define
 * this.container to use this method.
 * @param {object} effects list of effects to apply
 * @param {number} animSpeed possible values - 1..100. Bigger value - more fast animation.
 * @param {boolean} show if true - show widget. Otherwise - hide.
 */
Gmenu.Widget.prototype.showHideContainer = function(effects, animSpeed, onFinish, show){
	if(this.container == null){
		return null;
	}

	if(typeof(Gmenu.Effects) == 'undefined'){
		var self = this;

		Gmenu.Transport.loadJS({
			url: Gmenu.Widget.path + '../../zpeffects/src/effects.js',
			onLoad: function() {
				self.showHideContainer(effects, animSpeed, onFinish, show);
			}
		});

		return false;
	}

	if(animSpeed == null && isNaN(parseInt(animSpeed))){
		animSpeed = 5;
	}

	if(effects == null || effects.length == 0){
		if(show){
			this.container.style.display = this.originalContainerDisplay;
			this.originalContainerDisplay = null;
		} else {
			this.originalContainerDisplay = this.container.style.display;
			this.container.style.display = 'none';
		}

		if (onFinish) {
			onFinish();
		}
	} else {
		if(show){
			Gmenu.Effects.show(this.container, animSpeed, effects, onFinish);
		} else {
			Gmenu.Effects.hide(this.container, animSpeed, effects, onFinish);
		}
	}

	return true;
}

/**
 * Loads data from the specified source.
 *
 * <pre>
 * If source is URL, fires following events:
 * <ul>
 * <li><i>fetchSourceStart</i> when Gmenu.Transport#fetch is called</li>
 * <li><i>fetchSourceEnd</i> when Gmenu.Transport#fetch returns result or
 *  error</li>
 * <li><i>fetchSourceError</i> when Gmenu.Transport#fetch returns error.
 *  fetchSourceError event listener receives object returned by
 *  Gmenu.Transport#fetch</li>
 * </ul>
 *
 * Fires following events:
 * <ul>
 * <li><i>loadDataStart</i> when data parsing is started</li>
 * <li><i>loadDataEnd</i> when data parsing is ended or error occured during
 *  fetch</li>
 * </ul>
 * </pre>
 *
 * @param {object} objArgs Arguments object passed to callbackSource function
 */
Gmenu.Widget.prototype.loadData = function(objArgs) {
  // Get source using callback function
  if (typeof this.config.callbackSource == 'function') {
    var objSource = this.config.callbackSource(objArgs);
    if (objSource) {
      if (typeof objSource.source != 'undefined') {
        this.config.source = objSource.source;
      }
      if (typeof objSource.sourceType != 'undefined') {
        this.config.sourceType = objSource.sourceType;
      }
    }
  }
  // Process source
  if (this.config.source != null && this.config.sourceType != null) {
    var strSourceType = this.config.sourceType.toLowerCase();
    if (strSourceType == 'html') {
      this.fireEvent('loadDataStart');
      this.loadDataHtml(Gmenu.Widget.getElementById(this.config.source));
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'html/text') {
      this.fireEvent('loadDataStart');
      this.loadDataHtmlText(this.config.source);
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'html/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetch({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Onload event handler
        onLoad: function(objRequest) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataHtmlText(objRequest.responseText);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    } else if (strSourceType == 'json') {
      this.fireEvent('loadDataStart');
      if (typeof this.config.source == 'object') {
        this.loadDataJson(this.config.source);
      } else if (this.config.reliableSource) {
        this.loadDataJson(eval(this.config.source));
      } else {
        this.loadDataJson(Gmenu.Transport.parseJson({
          strJson: this.config.source
        }));
      }
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'json/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetchJsonObj({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Skip JSON format verification
        reliable: this.config.reliableSource,
        // Onload event handler
        onLoad: function(objResult) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataJson(objResult);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    } else if (strSourceType == 'xml') {
      this.fireEvent('loadDataStart');
      if (typeof this.config.source == 'object') {
        this.loadDataXml(this.config.source);
      } else {
        this.loadDataXml(Gmenu.Transport.parseXml({
          strXml: this.config.source
        }));
      }
      this.fireEvent('loadDataEnd');
    } else if (strSourceType == 'xml/url') {
      this.fireEvent('fetchSourceStart');
      // Fetch source
      var objWidget = this;
      Gmenu.Transport.fetchXmlDoc({
        // URL of the source
        url: this.config.source,
        // Suspend script execution until source is loaded or error received
        async: this.config.asyncSource,
        // Onload event handler
        onLoad: function(objResult) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataStart');
          objWidget.loadDataXml(objResult);
          objWidget.fireEvent('loadDataEnd');
        },
        // Onerror event handler
        onError: function(objError) {
          objWidget.fireEvent('fetchSourceEnd');
          objWidget.fireEvent('loadDataEnd');
          objWidget.fireEvent('fetchSourceError', objError);
        }
      });
    }
  } else {
    this.fireEvent('loadDataStart');
    this.loadDataHtml(Gmenu.Widget.getElementById(this.config.source));
    this.fireEvent('loadDataEnd');
  }
};

/**
 * Loads data from the HTML source. Override this in child class.
 *
 * @param {object} objSource Source HTMLElement object
 */
Gmenu.Widget.prototype.loadDataHtml = function(objSource) {};

/**
 * Loads data from the HTML fragment source.
 *
 * @param {string} strSource Source HTML fragment
 */
Gmenu.Widget.prototype.loadDataHtmlText = function(strSource) {
  // Parse HTML fragment
  var objTempContainer = Gmenu.Transport.parseHtml(strSource);
  // Load data
  this.loadDataHtml(objTempContainer.firstChild);
};

/**
 * Loads data from the JSON source. Override this in child class.
 *
 * @param {object} objSource Source JSON object
 */
Gmenu.Widget.prototype.loadDataJson = function(objSource) {};

/**
 * Loads data from the XML source. Override this in child class.
 *
 * @param {object} objSource Source XMLDocument object
 */
Gmenu.Widget.prototype.loadDataXml = function(objSource) {
};

/**
 * Converts element id to reference.
 *
 * @param {string} element Element id
 * @return Reference to element
 * @type object
 */
Gmenu.Widget.getElementById = function(element) {
  if (typeof element == 'string') {
    return document.getElementById(element);
  }
  return element;
};

/**
 * Returns style attribute of the specified element.
 *
 * @param {object} element Element
 * @return Style attribute value
 * @type string
 */
Gmenu.Widget.getStyle = function(element) {
  var style = element.getAttribute('style') || '';
  if (typeof style == 'string') {
    return style;
  }
  return style.cssText;
};

/**
 * Emulates "window.event" for certain events in Mozilla. To be able to access
 * Event object when event handler is set using element attribute, e.g.
 * &lt;a onclick="eventHandler()"&gt;.
 *
 * @param {object} arrEventNames array of event names where "window.event" is
 * needed, e.g. ['click', 'dblclick'].
 */
Gmenu.Widget.emulateWindowEvent = function(arrEventNames) {
  if (document.addEventListener) {
    // Set up emulation for certain events
    for (var iEvent = 0; iEvent < arrEventNames.length; iEvent++) {
      document.addEventListener(arrEventNames[iEvent], function (objEvent) {
        if (objEvent) {
          window.event = objEvent;
        }
      }, true);
    }
  }
};


Gmenu.Tree = function() {
	var objArgs = {};

	switch(arguments.length){
		case 1:
			objArgs = arguments[0];
			break;
		case 2:
			objArgs = arguments[1];
			objArgs.tree = arguments[0];
			break;
	}
	
	Gmenu.Tree.SUPERconstructor.call(this, objArgs);
};

// Inherit SuperClass
Gmenu.inherit(Gmenu.Tree, Gmenu.Widget);

/**
 * Initializes object. Needed to be able to inherit this class.
 *
 * @param config [Object, optional] -- the configuration options
 */
Gmenu.Tree.prototype.init = function(config) {
	this.config.tree = null;
	this.config.d_profile = false;
	this.config.hiliteSelectedNode = true;
	this.config.defaultIcons = null;
	this.config.compact = false;
	this.config.dynamic = false;
	this.config.initLevel = false;

	//expand/collapse the tree when the text or image are clicked
	this.config.expandOnLabel = true;

	//Keep track, using cookies, of the last location the user opened
	this.config.saveState = false;
	this.config.saveId = null;
	// processing Widget functionality
	Gmenu.Tree.SUPERclass.init.call(this, config);

	if(this.config.dynamic){
		this.config.initLevel = 0;
	}

	if(
		this.config.dynamic ||
		this.config.saveState &&
		(
			typeof(this.config.saveId) != "string" ||
			(
				typeof(this.config.saveId) == "string" &&
				this.config.saveId.length == 0
			)
		)
	){
		this.config.saveState = false;
	}

	// <PROFILE>
	if (this.config.d_profile) {
		var T1 = new Date().getTime();
	
		profile = {
			items : 0,
			trees : 0,
			icons : 0
		};
	}
	// </PROFILE>

	if (typeof(this.config.tree) == "string"){
		this.config.tree = document.getElementById(this.config.tree);
	}

	if(this.config.tree == null){
		throw "no target UL element given!";
	}

	this.id = this.config.tree.id || Gmenu.Utils.generateID("tree");
	this.list = this.config.tree;
	this.items = {};
	this.trees = {};
	this.selectedItem = null;
	var top = this.top_parent = Gmenu.Utils.createElement("div");
	top.className = "tree tree-top";
	this.createTree(this.config.tree, top, 0);
	this.config.tree.parentNode.insertBefore(top, this.config.tree);
	this.config.tree.parentNode.removeChild(this.config.tree);
	Gmenu.Tree.all[this.id] = this;

	// check if we have an initially selected node and sync. the tree if so
	if (this.selectedItem){
		this.sync(this.selectedItem.__msh_item);
	}

	//if we're keeping track of state, and we have saved state information
	if(this.config.saveState) {
		//restore to previous node
		var txt = Gmenu.Utils.getCookie("Gmenu.Tree-" + config.saveId)

		if (txt) {
			this.sync(txt);
		}
	}

	// <PROFILE>
	if (this.config.d_profile) {
		alert("Generated in " + (new Date().getTime() - T1) + " milliseconds\n" +
		      profile.items + " total tree items\n" +
		      profile.trees + " total (sub)trees\n" +
		      profile.icons + " total icons");
	}
	// </PROFILE>
};

/**
 * This global variable keeps a "hash table" (that is, a plain JavaScript
 * object) mapping ID-s to references to Gmenu.Tree objects.  It's helpful if
 * you want to operate on a tree but you don't want to keep a reference to it.
 * Example:
 *
 * \code
 *   // the following makes a tree for the <ul id="tree-id"> element
 *   var tree = new Gmenu.Tree("tree-id");
 *   // ... later
 *   var existing_tree = Gmenu.Tree.all("tree-id");
 *   // and now we can use \b existing_tree the same as we can use \b tree
 *   // the following displays \b true
 *   alert(existing_tree == tree);
 * \endcode
 *
 * So in short, this variable remembers values returned by "new
 * Gmenu.Tree(...)" in case you didn't.
 */
Gmenu.Tree.all = {};

/**
 * \internal Function that creates a (sub)tree.  This function walks the UL
 * element, computes and assigns CSS class names and creates HTML elements for
 * a subtree.  Each time a LI element is encountered, createItem() is called
 * which effectively creates the item.  Beware that createItem() might call
 * back this function in order to create the item's subtree. (so createTree and
 * createItem form an indirect recursion).
 *
 * @param list [HTMLElement] -- reference to the UL element
 * @param parent [HTMLElement] -- reference to the parent element that should hold the (sub)tree
 * @param level [integer] -- the level of this (sub)tree in the main tree.
 *
 * @return id -- the (sub)tree ID; might be automatically generated.
 */
Gmenu.Tree.prototype.createTree = function(list, parent, level) {
	// PROFILE
	if (this.config.d_profile){
		++profile.trees;
	}

	var id = list.id || Gmenu.Utils.generateID("tree.sub");
	var self = this;

	function _makeIt() {
		self.creating_now = true;
		var last_li = null;
		var next_li = null;
		var i = list.firstChild;
        var items = parent.__msh_items = [];

		self.trees[id] = parent;
		parent.__msh_level = level;
		parent.__msh_treeid = id;

		while (i) {
			if (last_li){
				last_li.className += " tree-lines-c";
			}

			if (i.nodeType != 1){
				i = i.nextSibling;
			} else {
				next_li = Gmenu.Utils.getNextSibling(i, 'li');

				if (i.tagName.toLowerCase() == 'li') {
					last_li = self.createItem(i, parent, next_li, level);

					if (last_li) { //false when webmaster creates malformed tree
						items[items.length] = last_li.__msh_item;
					}
				}

				i = next_li;
			}
		}

		i = parent.firstChild;

		if (i && !level) {
			i.className = i.className.replace(/ tree-lines-./g, "");
			i.className += (i === last_li) ? " tree-lines-s" : " tree-lines-t";
		}

		if (last_li && (level || last_li !== i)) {
			last_li.className = last_li.className.replace(/ tree-lines-./g, "");
			last_li.className += " tree-lines-b";
		}

		self.creating_now = false;
	};

	if (this.config.dynamic && level > 0){
		parent.style.display = "none";
		this.trees[id] = _makeIt;
	} else {
		_makeIt();
	}

	return id;
};

/**
 * \internal This function walks through a LI element and creates the HTML
 * elements associated with that tree item.  When it encounters an UL element
 * it calls createTree() in order to create the item's subtree.  This function
 * may also call item_addIcon() in order to add the +/- buttons or icons
 * present in the item definition as IMG tags, or item_addDefaultIcon() if the
 * tree configuration specifies "defaultIcons" and no IMG tag was present.
 *
 * @param li [HTMLElement] -- reference to the LI element
 * @param parent [HTMLElement] -- reference to the parent element where the HTML elements should be created
 * @param next_li [HTMLLiElement] -- reference to the next LI element, if this is not the last one
 * @param level [integer] -- the level of this item in the main tree
 * @param atStart [HTMLElement optional] -- reference to the element DIV with a TABLE object that represents the child following the new child added or inserted
 *
 * @return [HTMLElement] -- a reference to a DIV element holding the HTML elements of the created item
 */
Gmenu.Tree.prototype.createItem = function(li, parent, next_li, level, atStart) {
    // PROFILE
	if (this.config.d_profile){
		++profile.items;
	}

	if (!li.firstChild){
		return;
	}

	var afterNode = null;
	if (atStart) { //Optional parameter after the fourth parameter to allow the new created node to be inserted before it, instead of appending to the parent
		afterNode = atStart;
	}

	var
		id = li.id || Gmenu.Utils.generateID("tree.item"),
		item = this.items[id] = ((afterNode == null) ? Gmenu.Utils.createElement("div", parent) : Gmenu.Utils.createElement("div")), //Do not append the new div element to the parent, the new node in the div will be inserted before the 'afterNode'.
		t = Gmenu.Utils.createElement("table", item),
		tb = Gmenu.Utils.createElement("tbody", t),
		tr = Gmenu.Utils.createElement("tr", tb),
		td = Gmenu.Utils.createElement("td", tr),
		is_list,
		tmp,
		i = li.firstChild,
		has_icon = false;

	t.className = "tree-table";
	t.cellSpacing = 0;
	t.cellPadding = 0;
	td.className = "label";
	item.className = li.className + " tree-item";
	item.__msh_item = id;
	item.__msh_tree = this.id;
	item.__msh_parent = parent.__msh_treeid;

	if (afterNode) { //A child node from the same parent is sent in to let the new child node inserted before it.
		parent.insertBefore(item, afterNode); //New item inserted before the 'afterNode'
	}

	while (i) {
		is_list = i.nodeType == 1 && /^[ou]l$/i.test(i.tagName.toLowerCase());

		if (i.nodeType != 1 || !is_list) {
			if (i.nodeType == 3) {
				// remove whitespace, it seems to cause layout trouble
				tmp = i.data.replace(/^\s+/, '');
				tmp = tmp.replace(/\s+$/, '');
				li.removeChild(i);

				if (tmp) {
					i = Gmenu.Utils.createElement("span");
					i.className = "label";
					i.innerHTML = tmp;
					i.onclick = Gmenu.Tree.onItemToggle;
					td.appendChild(i);
				}
			} else if (i.tagName.toLowerCase() == 'img') {
				this.item_addIcon(item, i);
				has_icon = true;
			} else {
				i.onclick = Gmenu.Tree.onItemToggle;
				td.appendChild(i);
			}

			i = li.firstChild;

			continue;
		}

		if (is_list) {
			this.item_addIcon(item, null);

			var np;
			if (afterNode != null) {
				np = Gmenu.Utils.createElement("div");
				parent.insertBefore(np, afterNode); //New item inserted before the 'afterNode'
			} else {
				np = Gmenu.Utils.createElement("div", item.parentNode);
			}

			np.__msh_item = id;
			np.className = "tree";

			if (next_li){
				np.className += " tree-lined";
			}

			item.__msh_subtree = this.createTree(i, np, level + 1);

			if ((this.config.initLevel !== false && this.config.initLevel <= level) ||
				(this.config.compact && !/(^|\s)expanded(\s|$)/i.test(li.className))
				|| /(^|\s)collapsed(\s|$)/i.test(li.className)
			){
				item.className += " tree-item-collapsed";
				item.__msh_state = false;
				this.toggleItem(id, false, true);
			} else {
				item.className += " tree-item-expanded";
				this.toggleItem(id, true, true);
				item.__msh_state = true;
			}
			
			if (/(^|\s)selected(\s|$)/i.test(li.className)){
				this.selectedItem = item;
			}

			break;
		}
	}

	if (!has_icon){
		this.item_addDefaultIcon(item, this.config.defaultIcons);
	}

	return item;
};

/**
 * Call this function to create a html element with the optional element type specified.
 * By default, it is a <LI> element.
 * @param html [string, optional] -- html of the node; may include <UL>, <LI> elements; user is responsible for the content of the html
 * @param type [string, optional] -- type of the node to be created
 */
Gmenu.Tree.prototype.makeNode = function(html, type) {
        if (!type) {
           type = "li"; //Make it a <LI> node if the type is not specified.
        }
        var node = Gmenu.Utils.createElement(type);
        if (html) {
                node.innerHTML = html; //Assign the inner html of the node if it is specified.
        }
        return node;
}

/**
 * Call to get the parent to append, insert or remove a child either at start or end position or in between two nodes of the (sub-)tree.
 *
 * For insert, if the user selects a tree-item that represents the root node of a subtree, then the new child will be inserted outside the subtree,
 * i.e. exactly before the root node of the subtree. However, if the user selects the node that represents one of the children in the subtree,
 * then the new child will be inserted in the subtree under the root.
 * For remove, if the user selects the root node of a subtree, the root node including its children will be removed. If the user selects a node that is not
 * a root node of a subtree, then only that node will be deleted.
 * In the implementation, the p will first retrieve the subtree from Tree.trees. But this is just the object that encapsulates the subtree, not the root of the
 * entire subtree. For insert and remove,
 * @param id     [String] -- id of the parent the new child will be added to or inserted before/at.
 * @param mode   [String optional] -- "I" is for inserting a child node. "R" is for removing a child node.
 */
Gmenu.Tree.prototype.getParent = function(id, mode) {
        var parent = null;
        for (var i in this.trees) {
            //id sent in may be name of the top tree or one of the tree item's or subtree parent's name
            if ( (this.trees[i].__msh_treeid == id) || (this.trees[i].__msh_item == id) ) {
               parent = this.trees[i]; //Get the body of a subtree
               break;
            }
        }
        //For inserting a new child before the referece child, the reference child should be the tree item,
        //not the subtree under it (in case it has tree nodes under it). This is because inserting a new child
        //should be 'before' the subtree (if the tree item has tree nodes under it), not inside or become part of the subtree.
        if ( (mode != null) && ((mode.toUpperCase() == "I") || (mode.toUpperCase() == "R")) ) {  //At this point, if p not null, then it must be body of the subtree. So, get the tree item (root node of the subtree) instead.
           //Otherwise id doesn't refer to a subtree. In this case, get the tree item instead.
           if (parent != null) { //A subtree (not include its root) has been retrieved. That means the insertion or removal operation got to be performed before or at the root of the subtree. This warrants the retrival of the root of the subtree.
              if (parent.className != this.top_parent.className) //As long as p is not the top parent, get the previous sibling or node of p. The previous sibling will be the root of the subtree.
                 parent = parent.previousSibling;
           } else parent = this.items[id.toLowerCase()]; //If no subtree is retrieved, then it must an item node, then get it from array this.items.
        }
        if (!parent) { //If the node matching the id still cannot be found, then look into each item under a subtree
           parent = this.items[id.toLowerCase()];
        }
        return parent;
}

/**
 * Append a child to the start or end of the given parent.
 *
 * @param parent   [HTMLElement] -- reference to a tree node (either as a node at top level or at a subtree level) the new child going to be appended to in the tree.
 * @param newChild [HTMLElement] -- reference to an HTML element created of type LI, to which futher HTML elements such UL and LI can be included to generate subtrees.
 * @param atStart  [boolean, optional] -- true if the child going to be added at the start of the parent.
 */
Gmenu.Tree.prototype.appendChild = function(parent, newChild, atStart) {
	atStart = (atStart == true);

    // Abort operation when either parent/child is empty or the child node is
    //   already added to the tree.
	if (
		parent == null ||
		newChild == null ||
		typeof(parent) == "undefined" ||
		typeof(newChild) == "undefined" ||
		this.items[newChild.id]
	){
		return;
	}

	var item = null;

	if(parent.firstChild == null){
		atStart = false;
	}
	
	if (atStart) { //Append new child before first child of the parent
		item = this.createItem(newChild, parent, parent.firstChild.nextSibling, parent.__msh_level, parent.firstChild);
	} else { //Append new child after last child of the parent
		item = this.createItem(newChild, parent, null, parent.__msh_level);
	}

	// After adding a child, re-draw tree lines that connect the new child to
	//   the tree. This is necessary because the tree structure has been
	//   changed due to addition or insertion of a new child node.
	if (item) { //Child added has no subtree
		var this_node = null;
		var next_node = null;
		var prev_node = null;
		var subtree   = false;

		if (atStart) { //Child appended at start of the tree
			this_node = parent.childNodes[0]; //Always the first node regardless of whether has a subtree under it
			
			if (item.__msh_subtree==null) { //New child appended has no subtree
				next_node = parent.childNodes[1]; //Next node will be the second node - the one after the new child node.
			} else { //New child appended has a subtree
				next_node = parent.childNodes[2]; //Next node will be the third node - two nodes after the new child node
			}
		} else { //Child appended at end of the tree
			//Get the appropriate tree node the new node going to attach to
			if (!item.__msh_subtree) { //Child appended has no subtree
				this_node = parent.childNodes[parent.childNodes.length-1]; //Last node

				if(parent.childNodes.length > 1){
					prev_node = parent.childNodes[parent.childNodes.length-2];
					subtree = (prev_node.className != null && prev_node.className == "tree");
					
					if (subtree) { //prev_node is a not a tree item, it has a subtree
						prev_node.className += " tree-lined"; //Add vertical tree line to the tree
						prev_node = parent.childNodes[parent.childNodes.length-3]; //Get its parent instead
					}
				}
			} else {
				this_node = parent.childNodes[parent.childNodes.length-2]; //Second last node

				prev_node = parent.childNodes[parent.childNodes.length-3];
				subtree = (prev_node.className != null && prev_node.className == "tree");
				
				if (subtree) { //prev_node is a not a tree item, it has a subtree
					prev_node.className += " tree-lined"; //Add vertical tree line to the tree
					prev_node = parent.childNodes[parent.childNodes.length-4]; //Get its parent instead
				}
			}
		}

		//Draw tree lines between the child and the parent
		if (this_node) { //Make sure the child is in the parent (sub-)tree
			this_node.className = this_node.className.replace(/ tree-lines-./g, "");
			
			if (atStart) {
				this_node.className += " tree-lines-t";
				
				if (next_node) {
					next_node.className = next_node.className.replace(/ tree-lines-./g, "");
					next_node.className += " tree-lines-c";
				}
			} else {
				this_node.className += " tree-lines-b";

				if (prev_node) {
					prev_node.className = prev_node.className.replace(/ tree-lines-./g, "");

					prev_node.className += " tree-lines-c";
					
					if (subtree) {
						prev_node.className += " tree-lines-c";
					}
				}
			}
		}

		return item;
	}
};

/**
 * A new child can be inserted between two nodes/children or before one node /children at the same tree level.
 * Inserting the new child before the first node of a tree is allowed but not allowed
 * after the last node of the tree, i.e. end of the tree, except before the last node.
 * @param newChild [HTMLElement] -- New child node to be inserted into the tree
 * @param refChild [HTMLElement] -- Reference to the child node which the new child node will be inserted before
 */
Gmenu.Tree.prototype.insertBefore = function(newChild, refChild) {
	//Abort operation when either newChild/refChild is empty or the child node is already added to the tree..
	if(
		newChild == null ||
		refChild == null ||
		typeof(newChild) == "undefined" ||
		typeof(refChild) == "undefined" ||
		this.items[newChild.id]
	){
		return;
	}

	var parent = refChild.parentNode;
	var item = this.createItem(newChild, parent, parent.firstChild.nextSibling, parent.__msh_level, refChild);
	var nodeBefore = false, nodeAfter = false;
	var next_node  = null;

	if(item.previousSibling){
		nodeBefore = true;
	}

	if(item.nextSibling){
		nodeAfter = true;
	}

	item.className = item.className.replace(/ tree-lines-./g, "");

	if (nodeBefore && nodeAfter){
		item.className += " tree-lines-c";
	} else if (nodeBefore){
		item.className += " tree-lines-b";
	} else if (nodeAfter) {
		//Insert new child at the start of the tree.
		item.className += " tree-lines-t";

		//Since the node now after the new child was the first node before, the tree line from it is not connected to the new child.
		//So it needs to redraw the tree line for the second node (formerly the first node in the tree).
		if (item.className.indexOf("tree-item-more tree-item")>-1) { //New child has a subtree under it.
			next_node = item.nextSibling.nextSibling; //Get the next node at the 'same' level as the new child, skip the new child's subtree.
		} else {
			next_node = item.nextSibling; //Get the next node at the 'same' level.
		}

		next_node.className = next_node.className.replace(/ tree-lines-./g, "");

		//To find out whether next node has next node after it.
		if (next_node.className.indexOf("tree-item-more tree-item")>-1) { //Next node has a subtree
			if (next_node.nextSibling.nextSibling != null){
				next_node.className += " tree-lines-c";
			} else {
				next_node.className += " tree-lines-b";
			}
		} else { //Next node is an tree item.
			if (next_node.nextSibling != null){
				next_node.className += " tree-lines-c";
			} else {
				next_node.className += " tree-lines-b";
			}
		}
	}
};

/**
 * An old child node in the tree can be removed at any level.
 * If the old child node happens to be the root of a subtree,
 * then the entire subtree including child node(s) under the root node will be removed.
 * If the old child node is just an item node without any child node under it as children,
 * then the only node removed is the old child node.
 * Once an item node or a subtree is removed, the node/subtree before and/or after the old
 * node will be joined together, sometimes with tree lines redrawn.
 * The only limitations are that the root node of the top tree is not permitted for removal;
 * the first child node found will be removed if there are more than one tree node with the same id.
 * @param oldChild [HTMLElement] -- Old child node to be removed from the tree where it can
 * be an item node without children or a subtree including the old child node as the root.
 */
Gmenu.Tree.prototype.removeChild = function(oldChild) {
 	if (
		oldChild == null ||
		typeof(oldChild) == "undefined"
	){
		//No child to remove
		return;
	} else if (oldChild.className == this.top_parent.className) {
		//Top root node is not allowed
		alert("Removing root node not allowed.")
		return;
	}

	//Get the child node's parent node
	var p = oldChild.parentNode;

	//Remove node(s) - check if the old child represents a root node of a subtree. If it does, remove the child nodes in the subtree as well as the root node.
	//If it does not represents a root node of a subtree, just remove it from the parent it attached to.
	//Clean up - remove the subtree's id from this.trees or the item node's id from this.items.
	if (oldChild.__msh_item && oldChild.__msh_tree && oldChild.__msh_parent) { //Make sure all common attributes of root node (of a subtree) or item node are there.
		var prev_node = oldChild.previousSibling;
		var next_node = oldChild.nextSibling;
		var hasPrevNode = false;
		var hasNextNode = false;

		//Find out if there is a node before the old node at the same tree level.
		if (prev_node) {
			//If the node before the old child is the entire subtree at next level, get the root node of that subtree.
			//The root node will be at the same level as the old child. Otherwise, the node before the old child is an item node.

			if (prev_node.__msh_treeid) { //The node before the old child is a tree consists of all tree nodes in a subtree.
			    //Get the root node of that subtree. It should have the same level as the old child.
				prev_node = prev_node.previousSibling;
			}

			hasPrevNode = true;
		} else { //No child before the old child at the same level.
			//Check if the old child has a parent node such as root of a subtree and the parent node is not the top of the tree
			if (oldChild.parentNode && oldChild.parentNode.className != this.top_parent.className) {
			    //This is needed in order to 'not' change any tree line for the child node(s) under a subtree.
				hasPrevNode = true;
			}
		}

		//Find out if there is a node after the old node at the same tree level.
		if (next_node) {
			if (oldChild.__msh_subtree) { //Old child is a subtree. So old node is the root of a subtree. Get the node after the subtree then.
			    //Get the node after the old node at the same level.
				next_node = next_node.nextSibling;
				
				if (next_node) {
					//There is a next node at same level as the old node.
					hasNextNode = true;
				}
		} else {
			//Old child is an item node. So old node is an item node, not a subtree. Old node and next node are at the same level.
			hasNextNode = true; //There is a next node at same level as the old node.
		}
	} //else no next node

	if (oldChild.__msh_subtree) { //Root node of a subtree; has child node(s) under it.
	    //Try to get the whole subtree of the old child node
		var subtreeNode = oldChild.nextSibling;

		if (subtreeNode && oldChild.__msh_subtree == subtreeNode.__msh_treeid) { //Subtree node exists
			for (var i = 0; i < subtreeNode.childNodes.length; i++) { //Loop through each children of the subtree
				if (subtreeNode.childNodes[i]){
					//and delete the corresponding object in this.items
					delete this.items[subtreeNode.childNodes[i].__msh_item];
				}
            }

			//Remove the corresponding item in this.items for the root of the subtree
			delete this.items[subtreeNode.__msh_item];

			//Remove the whole subtree
			p.removeChild(subtreeNode);
		}
		
		//Remove the corresponding subtree in this.trees
		delete(this.trees[oldChild.__msh_subtree]);

		//Remove the child node from its parent
		p.removeChild(oldChild);
	} else {
		//An item node, not a subtree
		delete this.items[oldChild.__msh_item]; //Remove the corresponding item in this.items for the item node
		p.removeChild(oldChild); //Remove the child node from its parent
	}

	//Re-draw tree lines nodes/subtrees between removed node if necessary
	//If there is a previous node and next node between the old node, there is no need to redraw thr tree lines
	//as the previous and next node will become joined together after the old node is removed.
	//If 'before' removal of the old node, there is a next node after the old node but no previous node before
	//the old node, then it needs to redraw the tree line of the next node. The next node thus become the first node of the tree.
	if (!hasPrevNode && hasNextNode) {
		if (next_node) {
			next_node.className = next_node.className.replace(/ tree-lines-./g, "");
			next_node.className += " tree-lines-t";
		}
	} else if (hasPrevNode && !hasNextNode) {
		if (prev_node) {
			if (prev_node.__msh_subtree) {
				prev_node.nextSibling.className = prev_node.nextSibling.className.replace(/ tree-lined/g, "");
			} //else not a subtree

			prev_node.className = prev_node.className.replace(/ tree-lines-./g, "");
			prev_node.className += " tree-lines-b";
		}
	}
}
};

/**
 * \internal This function adds a TD element having a certain class attribute
 * which helps having a tree containing icons without defining IMG tags for
 * each item.  The class name will be "tgb icon className" (where "className"
 * is the specified parameter).  Further, in order to customize the icons, one
 * should add some CSS lines like this:
 *
 * \code
 *  div.tree-item td.customIcon {
 *    background: url("themes/img/fs/document2.png") no-repeat 0 50%;
 *  }
 *  div.tree-item-expanded td.customIcon {
 *    background: url("themes/img/fs/folder-open.png") no-repeat 0 50%;
 *  }
 *  div.tree-item-collapsed td.customIcon {
 *    background: url("themes/img/fs/folder.png") no-repeat 0 50%;
 *  }
 * \endcode
 *
 * As you can see, it's very easy to customize the default icons for a normal
 * tree item (that has no subtrees) or for expanded or collapsed items.  For
 * the above example to work, one has to pass { defaultIcons: "customIcon" } in
 * the tree configuration object.
 *
 * This function does nothing if the \b className parameter has a false logical
 * value (i.e. is null).
 *
 * @param item [HTMLElement] -- reference to the DIV element holding the item
 * @param className -- a string containing the additional class name
 */
Gmenu.Tree.prototype.item_addDefaultIcon = function(item, className) {
	if (!className){
		return;
	}

	var last_td = item.firstChild.firstChild.firstChild.lastChild, td;
	var td = Gmenu.Utils.createElement("td");
	
	td.className = "tgb icon " + className;
	td.onclick = Gmenu.Tree.onItemToggle;
	last_td.parentNode.insertBefore(td, last_td);
};

/**
 * \internal This function does different things, depending on whether the \b
 * img parameter is passed or not.  If the \b img is passed, then this function
 * adds it as an icon for the given item.  If not passed, this function creates
 * a "+/-" button for the given item.
 *
 * @param item [HTMLElement] -- reference to the DIV holding the item elements
 * @param img [HTMLImgElement, optional] -- reference to an IMG element; normally one found in the <LI>
 */
Gmenu.Tree.prototype.item_addIcon = function(item, img) {
	// PROFILE
	if (this.config.d_profile){
		++profile.icons;
	}
	
	var last_td = item.firstChild.firstChild.firstChild, td;
	last_td = img ? last_td.lastChild : last_td.firstChild;
	
	if (!img || !item.__msh_icon) {
		td = Gmenu.Utils.createElement("td");
		td.className = "tgb " + (img ? "icon" : "minus");
		last_td.parentNode.insertBefore(td, last_td);
		td.onclick = Gmenu.Tree.onItemToggle;
	} else {
		td = item.__msh_icon;
		img.style.display = "none";
	}
	
	if (!img) {
		td.innerHTML = "&nbsp;";
		item.className += " tree-item-more";
		item.__msh_expand = td;
	} else {
		td.appendChild(img);
		item.__msh_icon = td;

		if(td.childNodes.length == 1){
			item.__msh_icon_expanded = img;
		} else {
			item.__msh_icon_collapsed = img;
		}
	}
};

/**
 * This function gets called from a global event handler when some item was
 * clicked.  It selects the item and toggles it if it has a subtree (expands or
 * collapses it).
 *
 * @param item_id [string] -- the item ID
 */
Gmenu.Tree.prototype.itemClicked = function(item_id, expand) {
	this.selectedItem = this.toggleItem(item_id, null, expand);
	Gmenu.Utils.writeCookie("Gmenu.Tree-" + this.config.saveId, this.selectedItem.__msh_item, null, '/', 7);

	if (this.config.hiliteSelectedNode && this.selectedItem){
		Gmenu.Utils.addClass(this.selectedItem, "tree-item-selected");
	}

	this.onItemSelect(item_id);
};

/**
 * This function toggles an item if the \b state parameter is not specified.
 * If \b state is \b true then it expands the item, and if \b state is \b false
 * then it collapses the item.
 *
 * @param item_id [string] -- the item ID
 * @param state [boolean, optional] -- the desired item state
 * @param expand [boolean, optional] -- should expand/collapse if it is a branch
 *
 * @return a reference to the item element if found, null otherwise
 */
Gmenu.Tree.prototype.toggleItem = function(item_id, state, expand) {
	if (item_id) {
		var stateDefined = false;

		if(this.config.saveState) {
			stateDefined = true;
		}

		if (this.config.hiliteSelectedNode && this.selectedItem){
			Gmenu.Utils.removeClass(this.selectedItem, "tree-item-selected");
		}
		var item = this.items[item_id];

		if (typeof(state) == "undefined" || state == null) {
			state = !item.__msh_state;
			stateDefined = false;
		}

		// Expand is true when the '+' or '-' are clicked.
		// If expandOnLabel is true we will expand even when the label is clicked
		// We will also expand when the state is explictally passed
		if (
			(expand || this.config.expandOnLabel || stateDefined)
		){
			var subtree = this._getTree(item.__msh_subtree, this.creating_now);

			if (subtree) {
				subtree.style.display = state ? "block" : "none";
				Gmenu.Utils.removeClass(item, "tree-item-expanded");
				Gmenu.Utils.removeClass(item, "tree-item-collapsed");
				Gmenu.Utils.addClass(item, state ?
					"tree-item-expanded" : "tree-item-collapsed");
			}

			var img = item.__msh_expand;

			if (img){
				img.className = "tgb " + (state ? "minus" : "plus");
			}

			item.__msh_state = state;
			img = item.__msh_icon;

			if (img) {
				item.__msh_icon_expanded.style.display = 'none'
				item.__msh_icon_collapsed.style.display = 'none'
				item[state ? "__msh_icon_expanded" : "__msh_icon_collapsed"].style.display = 'block';
			}

			if (this.config.compact && state) {
				var a = this._getTree(item.__msh_parent).__msh_items;
				for (var i = a.length; --i >= 0;)
					if (a[i] != item_id)
						this.toggleItem(a[i], false);
			}

			if(/zpLoad(JSON|HTML)=([^ $]*)/.test(item.className) && state){
				var dataType = RegExp.$1;
				var url = RegExp.$2;

				item.className = item.className.replace(/zpLoad(JSON|HTML)=([^ $]*)/g, "");
				var loadingLabel = this.appendChild(this.getParent(item.__msh_item), this.makeNode("...loading..."), true);
				var errorFunc = function (objError){
					alert(objError.errorDescription);
					item.className += "zpLoad" + dataType + "=" + url;

					self.removeChild(self.getParent(loadingLabel.__msh_item, "R"));
				}

				var self = this;

				if(dataType == "JSON"){
					Gmenu.Transport.fetchJsonObj({
						url: url,
						method: "GET",
						onLoad: function(objResponse){
							function json2html(json){
								if(json == null || json.length == 0){
									return "";
								}

								var res = "<ul>";

								for(var ii = 0; ii < json.length; ii++){
									var currVal = json[ii];

									if(typeof(currVal) == "string"){
										currVal = [currVal];
									}

									res += "<li>" + currVal[0] + json2html(currVal[1]) + "</li>"
								}

								res += "</ul>";

								return res
							}

							var arr = objResponse;

							for(var ii =0; ii < arr.length; ii++){
								var currVal = arr[ii];

								if(typeof(currVal) == "object" && currVal.length == 1){
									currVal = currVal[0];
								} else if (typeof(currVal) == "object" && currVal.length == 2){
									currVal = currVal[0] + json2html(currVal[1]);
								}
								self.appendChild(self.getParent(item.__msh_item), self.makeNode(currVal));
							}

							self.removeChild(self.getParent(loadingLabel.__msh_item, "R"));
						},
						onError : errorFunc
					});
				} else if(dataType == "HTML"){
					Gmenu.Transport.fetchXmlDoc({
						url: url,
						method: "GET",
						onLoad: function(xmlDoc){
							var cNodes = xmlDoc.documentElement.childNodes;

							for(var jj = 0; jj < cNodes.length; jj++){
								var currentNode = cNodes[jj];

								if(currentNode.nodeType != 1){
									continue;
								}

								var li = self.makeNode(Gmenu.Tree.serializeNode(currentNode, true));

								for (var ii = 0; ii < currentNode.attributes.length; ii++){
									var attr = currentNode.attributes[ii];
									if(attr.name == 'class'){
										li.className = currentNode.getAttribute(attr.name);
									} else {
										li.setAttribute(attr.name, currentNode.getAttribute(attr.name));
									}
								}

								self.appendChild(self.getParent(item.__msh_item), li);
							}

							self.removeChild(self.getParent(loadingLabel.__msh_item, "R"));
						},
						onError : errorFunc
					});
				}
			}
		}

		return item;
	}

	return null;
};

/**
 * Call this function to collapse all items in the tree.
 */
Gmenu.Tree.prototype.collapseAll = function() {
	for (var i in this.trees){
		this.toggleItem(this._getTree(i).__msh_item, false, true);
	}
};

/**
 * Call this function to expand all items in the tree.
 */
Gmenu.Tree.prototype.expandAll = function() {
	for (var i in this.trees) {
		this.toggleItem(this._getTree(i).__msh_item, true, true);
	}
};

/**
 * Call this function to toggle all items in the tree.
 */
Gmenu.Tree.prototype.toggleAll = function() {
	for (var i in this.trees) {
		this.toggleItem(this._getTree(i).__msh_item);
	}
};

/**
 * Call this function to synchronize the tree to a given item.  This means that
 * all items will be collapsed, except that item and the full path to it.
 *
 * @param item_id [string] -- the ID of the item to sync to.
 */
Gmenu.Tree.prototype.sync = function(item_id) {
	var item = this.items[item_id];
	
	if (item) {
		this.collapseAll();
		this.selectedItem = item;
		var a = [];
	
		while (item.__msh_parent) {
			a[a.length] = item;
			var pt = this._getTree(item.__msh_parent);
		
			if (pt.__msh_item){
				item = this.items[pt.__msh_item];
			} else {
				break;
			}
		}

		for (var i = a.length; --i >= 0;){
			this.toggleItem(a[i].__msh_item, true);
		}

		Gmenu.Utils.addClass(this.selectedItem, "tree-item-selected");
	}
};

/**
 * Destroys the tree. Removes all elements. Does not destroy the Gmenu.Tree
 * object itself (actually there's no proper way in JavaScript to do that).
 */
Gmenu.Tree.prototype.destroy = function() {
	var p = this.top_parent;
	p.parentNode.removeChild(p);
};

/**
 * \internal This function is used when "dynamic initialization" is on.  It
 * retrieves a reference to a subtree if already created, or creates it if it
 * wasn't yet and \b dont_call is \b false (returns null in that case).
 *
 * @param tree_id [string] the ID of the subtree
 * @param dont_call [boolean] pass true here if you don't want the subtree to be created
 *
 * @return reference to the tree if it was found or created, null otherwise.
 */
Gmenu.Tree.prototype._getTree = function(tree_id, dont_call) {
	var tree = this.trees[tree_id];

	if (typeof(tree) == "function") {
		if (dont_call){
			tree = null;
		} else {
			tree();
			tree = this.trees[tree_id];
			tree.__msh_state = false;
		}
	}

	return tree;
};

// CUSTOMIZABLE EVENT HANDLERS; default action is "do nothing"

/**
 * Third party code can override this member in order to add an event handler
 * that gets called each time a tree item is selected.  It receives a single
 * string parameter containing the item ID.
 */
Gmenu.Tree.prototype.onItemSelect = function() {};

// GLOBAL EVENT HANDLERS (to workaround the stupid Microsoft memory leak)

/**
 * \internal This is a global event handler that gets called when a tree item
 * is clicked.  Don't override! ;-)
 */
Gmenu.Tree.onItemToggle = function() {
	var item = this;
	var body = document.body;
	var expand = false;

	if(/tgb (minus|plus)/.test(this.className)) {
		expand = true;
	}
	
	while (item && item !== body && !/tree-item/.test(item.className)){
		item = item.parentNode;
	}

	Gmenu.Tree.all[item.__msh_tree].itemClicked(item.__msh_item, expand);
};

/** \internal serialize Node object to text
*	@param currentNode [Node] - Node to serialize
*/
Gmenu.Tree.serializeNode = function(currentNode, omitTopLevel){
	if(currentNode == null){
		return;
	}

	if(currentNode.nodeType == 3){ // text node
		return currentNode.nodeValue;
	}

	if(omitTopLevel){
		var res = "";

		if(currentNode.hasChildNodes()){
			for(var ii = 0; ii < currentNode.childNodes.length; ii++){
				res += Gmenu.Tree.serializeNode(currentNode.childNodes[ii]);
			}
		}
	} else {
		var res = "<" + currentNode.tagName;
		
		for (var ii = 0; ii < currentNode.attributes.length; ii++){
			var attr = currentNode.attributes[ii];
			res += " " + attr.name + "=\"" + currentNode.getAttribute(attr.name).replace(/"/g, '\\"') +  "\"";
		}
	
		if(currentNode.hasChildNodes()){
			res += ">";
	
			for(var ii = 0; ii < currentNode.childNodes.length; ii++){
				res += Gmenu.Tree.serializeNode(currentNode.childNodes[ii]);
			}
	
			res += "</" + currentNode.tagName + ">"
		} else {
			res += "/>"
		}
	}
	
	return res;
};
Gmenu.Utils.addEvent(window, 'load', Gmenu.Utils.checkActivation);
