/* 
 * Wbd namespace / toplevel methods
 */
function Wbd() { }

Wbd.ev = { onload:[ ] };	// list of events

Wbd.addOnload = function(f) {
	window.onload = Wbd.onload;
	if (f.apply) {
		if (Wbd.Event.available()) {
			Wbd.Event.addListener(window,'load',f);
		} else {
			Wbd.ev.onload.push(f);	
		}
	}
}

Wbd.compatible = function() {
	if (!document.getElementById || !document.getElementsByTagName || !Array.prototype.pop) {
		return false;
	}
	return true;
}

Wbd.onload = function() { 
	if (!Wbd.Event.available()) {
		Wbd.triggerEvents('onload');	
	}
}

Wbd.triggerEvents = function(type) {
	if (Wbd.ev[type]) {
		for (var i=0; i<Wbd.ev[type].length; i++) {
			Wbd.ev[type][i].apply(window);
		}
	}
}

/**
 * Raise a fatal error.
 **/
Wbd.error = function(msg) {
	
	if (console && console.trace) {
		console.trace();
	}
	
	if (console && console.error) {
		console.error.apply(console,arguments);
		throw 'caught error';
	}
	
	throw msg;
	
}

/* UrlGenerator object; note this can be changed, just needs to include
 * a "generate" function
 */
Wbd.UrlGenerator =
	{
		generate : function(part) {
			if (!part) {
				return window.location.href;
			}
			return getBaseUrl() + '/apps/' + part;
		}
	};
	
/**
 * WBD event handling; currently, this relies on the YAHOO event library
 **/
Wbd.Event = function() { }

/**
 * The character code for the return key.
 * @type {Integer}
 **/
Wbd.Event.CHAR_CODE_RETURN = 13;

/**
 * Check if the wbd event utility is available.  Depends on the YAHOO
 * event library.
 * @type {Boolean}
 **/
Wbd.Event.available = function() {
	return (typeof(window.YAHOO) != 'undefined') && (typeof(window.YAHOO.util) != 'undefined') && (typeof(window.YAHOO.util.Event) != 'undefined');
}

/**
 * Add a listener to a specified DOM element.  Handlers are by default
 * passed an object that represents the event.  This object should be
 * treated as opaque - i.e., event handlers should use the methods 
 * provided by this class to get more information about the event.
 * If oScope is passed to this method, then it will be passed as a
 * second argument to the handler.  However, if bOverride is passed
 * and is true, then oScope will be used as the handler's scope instead.
 * @param {Element|String|Array} el The element itself, the element's
 * ID, or an array containing a mixture of those types.
 * @param {String} sType The type of event to listen for
 * @param {Function} fn The function to call when the event is fired
 * @param {Object} oScope An associated object to either pass to
 * the handler, or to use as the handler's scope (see next arg).
 * @param {bOverride} bOverride Whether or not to use oScope
 * as the handler's scope.  If false or not provided, oScope is
 * passed as the second argument to the handler.
 **/ 
Wbd.Event.addListener = function(el,sType,fn,oScope,bOverride) {
	return YAHOO.util.Event.addListener(el,sType,fn,oScope,bOverride);		
}

/**
 * Create a "YAHOO style" custom event and return it. See YAHOO's
 * API documentation to understand how it works.  I don't like it,
 * in the future I will only use (new Wbd.Event.CustomEvent()).
 * @deprecated
 **/
Wbd.Event.customEvent = function(sName) {
	return new YAHOO.util.CustomEvent(sName);
}

/** 
 * Get the char code property for a keypress event.
 * @param {Event} ev
 * @type {Unicode}
 **/
Wbd.Event.getCharCode = function(ev) {
	return YAHOO.util.Event.getCharCode(ev);
}

/**
 * Get the target of a given event.
 * @param {Event} ev
 * @type {Element}
 **/
Wbd.Event.getTarget = function(ev) {
	return YAHOO.util.Event.getTarget(ev);
}

/**
 * Register an event handler to fire when a given element becomes
 * available, i.e. when it is rendered into the DOM tree.  Note that
 * at time of writing, if you pass the actual element, the event will
 * fire immediately.  If instead you pass a unique ID for the element,
 * the event will fire once the element is rendered.
 * @see #addListener
 **/
Wbd.Event.onAvailable = function(el,fn,oScope,bOverride) {
	YAHOO.util.Event.onAvailable(el,fn,oScope,bOverride);
}

/**
 * Remove a listener from a given target for a given event type.
 * @param {Element} el
 * @param {String} sType
 * @param {Function} fn
 **/
Wbd.Event.removeListener = function(el,sType,fn,index) {
	YAHOO.util.Event.removeListener(el,sType,fn,index);
}

/**
 * Stop an event's normal functionality, and cancel the bubble.
 * @param {Event} ev
 **/
Wbd.Event.stopEvent = function(ev) {
	YAHOO.util.Event.stopEvent(ev);
}

/**
 * @class An adapter for YAHOO's custom event class that passes arguments
 * in a more sane way.  Rather than using an array, which is stupid, this
 * class passes each argument individually to the subscribers.  Much
 * cleaner and easier to understand.  It's up to event providers to define
 * what arguments will be passed to subscribers.  The only limitation of
 * this object is that it does not currently support passing the scope
 * and override paramters to subscribe().  But IMO this is a bad construct
 * (places the responsibility of scoping objects all over the place), just
 * use Wbd.Lang.hitch instead.
 * @param {String} name The event name
 **/
Wbd.Event.CustomEvent = function(name) {
	
	var self = this;
	
	/**
	 * The YAHOO custom event used to implement this.  Just because
	 * we don't like YAHOO's custom event API doesn't mean we can't
	 * leverage it!
	 * @type {YAHOO.util.CustomEvent}
	 **/
	var yahoo_evt = new YAHOO.util.CustomEvent(name);
	
	/**
	 * The actual subscribers (created by this object), used for unsubscribe.
	 * @type {Array}
	 **/
	var subscribers = [ ];
	
	/**
	 * Fire this event, calling all subscribers.  Any parameters are passed
	 * to the subscribers after the event type.
	 **/
	this.fire = function() {
		yahoo_evt.fire.apply(yahoo_evt,arguments);
	}
	
	/**
	 * Subscribe to this event.  Subscribers are passed the event type
	 * and any additional arguments passed to the fire() method.  If the
	 * scope is not overridden, subscribers will be called with the window
	 * object as scope.
	 * @param {Function} fn
	 * @param {Object} scope (optional)
	 * @param {Boolean} override (optional)
	 **/
	this.subscribe = function(fn) {
		
 		var subscriber = function(type,args) {
			fn.apply(window,[type].append(args));
		}
		
		yahoo_evt.subscribe(subscriber);
		
	}
	
	/**
	 * Unsubscribe a particular function.
	 * @param {Function} fn
	 **/
	this.unsubscribe = function(fn) {
		
		// not implemented yet
		
	}
	
	/**
	 * Unsubscribe all functions.
	 **/
	this.unsubscribeAll = function() {
		yahoo_evt.unsubscribeAll();
	}
	
}
	
/**
 * Core class: anything in the "core" package on the server
 **/
Wbd.Core = function() { }

Wbd.Core.member_details = { };

/**
 * Fetch membership details for a specified membership object.
 * @param string membspec Membership object identifier.  Examples:
 * - user:tbarstow
 * - set:207
 * @param function callback Callback function to call with the result
 * @param boolean ret_full_obj Whether to pass the full membership object
 * to the callback, or to pass a DOM element representing a div with the
 * information already included.  TRUE means pass full object, FALSE
 * (or not passed at all) means pass DOM element.
 **/
Wbd.Core.getMembershipDetails = function(membspec,callback,ret_full_obj) {
	
	// return full object ?
	
	var return_object = ret_full_obj ? ret_full_obj : false;
	
	// create a function to call with the ajax XML response
	
	var ajax_callback = function(xml_response) {
		
		// cache the response
		
		Wbd.Core.member_details[membspec] = xml_response;
		
		// convert to a JS object (discarding root node) and store in the cache
		
		var obj = Wbd.Dom.toSimpleObject(xml_response).core_membership_object;
		
		// if returning full object, just call the callback now, passing the
		// inner object
		
		if (return_object) {
			callback.call(callback,obj.inner_obj);
			return;
		}
		
		// otherwise, build a DOM node to describe the object
			
		switch (obj.type) {

			case 'user':

				var user = obj.inner_obj;
				var items = 
					{ 
						'Username'	: user.username, 
						'Company'	: user.company + ( user.department != null ? ' / ' + user.department : '' ),
						'E-mail'	: user.email
					};

			break;

			case 'set':

				var set = obj.inner_obj;
				var items = { 'Description'	: set.description };

				// build the members

				var max_members = 5;		// max # of members to show
				var members = [ ];

				for (var i=0; i<max_members && i<set.members.member.length; i++) {
					members.push(set.members.member[i].name);
				}

				items['Members'] = members.join(', ');

			break;

		}

		// render the items using DOM

		var container = document.createElement('dl');
		container.className = 'membership_details';

		for (var v in items) {

			var label = document.createElement('dt');
			label.appendChild(document.createTextNode(v + ":"));
			container.appendChild(label);

			var value = document.createElement('dd');
			value.appendChild(document.createTextNode(items[v]));
			container.appendChild(value);

		}
		
		// call the actual callback
		
		callback.call(callback,container);
			
	}
	
	// cache hit ?
	
	if (Wbd.Core.member_details[membspec]) {
		ajax_callback.call(callback,Wbd.Core.member_details[membspec]);
		return;
	}
	
	// check the membspec format
	
	var membinfo = Wbd.Core.parseMembSpec(membspec);
	if (!membinfo) {
		alert("Invalid membspec for getSetMemberDetails: '" + membspec + "'");
		return;
	}
	
	var type = membinfo.type;
	var ref = membinfo.ref;
	
	// create the ajax object
	
	var a = new Ajax();
	if (!a.valid) {
		return;
	}
	
	// set the URL and the callback and execute
	
	a.url = '/core.php/get-membership-details/' + type + '/' + ref;
	a.callback = function(ajax) { ajax_callback.call(ajax_callback,ajax.getXmlResponse()); }
	a.doRequest();

}

/**
 * Parse a core membership identifier and return an object describing
 * its contents.
 * @param string membspec
 * @return mixed On a successful parse, an object with properties "type" and
 * "ref".  On failure, boolean FALSE.
 **/
Wbd.Core.parseMembSpec = function(membspec) {
	var matches = membspec.match(/^(.+):(.+)$/);
	if (matches) {
		return { type : matches[1], ref : matches[2] };
	} else {
		return false;
	}
}

/*
 * Dom class: miscellaneous dom util functions
 */
Wbd.Dom = function() { }

// Written by Jonathan Snook, http://www.snook.ca/jonathan
// Add-ons by Robert Nyman, http://www.robertnyman.com
Wbd.Dom.getElementsByClassName = function(oElm, strTagName, strClassName){
    var arrElements = (strTagName == "*" && document.all)? document.all : 
    oElm.getElementsByTagName(strTagName);
    var arrReturnElements = new Array();
    strClassName = strClassName.replace(/\-/g, "\\-");
    var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
    var oElement;
    for(var i=0; i<arrElements.length; i++){
        oElement = arrElements[i];      
        if(oRegExp.test(oElement.className)){
            arrReturnElements.push(oElement);
        }   
    }
    return (arrReturnElements)
}

Wbd.Dom.getTextValue = function(node) {
	if (Wbd.Dom.isTextNode(node)) {
		return node.nodeValue;
	} else {
		val = "";
		for (var i=0; i<node.childNodes.length; i++) {
			val += Wbd.Dom.getTextValue(node.childNodes[i]);
		}
		return val;
	}
}

Wbd.Dom.setTextValue = function(node,text) {
	if (Wbd.Dom.isTextNode(node)) {
		node.nodeValue = text;
	} else {
		for (var i=(node.childNodes.length-1); i>=0; i--) {
			node.removeChild(node.childNodes[i]);
		}
		node.appendChild(document.createTextNode(text));
	}
}

Wbd.Dom.addClassName = function(node,classname) {
	if (!node) { return; }
	var classes = (node.className ? node.className.split(' ') : [ ]);
	for (var i=0; i<classes.length; i++) {
		if (classes[i] == classname) {
			return;
		}
	}
	classes.push(classname);
	node.className = classes.join(' ');
}

Wbd.Dom.hasClassName = function(node,classname) {
	var regexp = new RegExp('(^|\\s)' + classname + '($|\\s)', 'g');
	return (node.className && node.className.search(regexp) != -1);
}

Wbd.Dom.removeClassName = function(node,classname) {
	if (!node.className) { return; }
	var cOld = node.className.split(' ');
	var cNew = [ ];
	for (var i=0; i<cOld.length; i++) {
		if (cOld[i] != classname) {
			cNew.push(cOld[i]);
		}
	}
	node.className = cNew.join(' ');
}

Wbd.Dom.isTextNode = function(n) {
	return n.nodeType == Node.TEXT_NODE;
}

/**
 * Convert a simple DOM tree to a JS object.  The DOM tree is "simple" in
 * the sense that it has no attributes (or at least, attributes are not handled).
 * Just child nodes and text children.
 **/
Wbd.Dom.toSimpleObject = function(node,obj) {
	
	var value;	// JS value of the node
	
	// merge adjacent text children; not supported by all browsers
	
	try { node.normalize(); } catch (e) { }
		
	// discard the document node
	
	if (node.nodeType == Node.DOCUMENT_NODE) {
		return Wbd.Dom.toSimpleObject(node.lastChild);
	}
	
	// if this node only has a single text child, use the child's value
	
	if (node.childNodes.length == 1 && node.firstChild.nodeType == Node.TEXT_NODE) {
		
		value = node.firstChild.nodeValue;
		
	// otherwise, if this node has children, then the node's JS value is an
	// object build via recursive calls
	
	} else if (node.childNodes.length > 0) {
		
		value = { };
		for (var i=0; i<node.childNodes.length; i++) {
			Wbd.Dom.toSimpleObject(node.childNodes[i],value);
		}
		
	// otherwise, it has no value
		
	} else {
		
		value = null;
		
	}
	
	// create the object or use one from a recursive call
	
	obj = obj ? obj : { };
	
	// if an element with this name already exists, make it an array
	
	if (obj[node.nodeName]) {
		
		if (!obj[node.nodeName].push) {
			obj[node.nodeName] = [ obj[node.nodeName] ];
		}
		
		obj[node.nodeName].push(value);
		
	// otherwise just set the value directly
		
	} else {
		
		obj[node.nodeName] = value;
		
	}
	
	return obj;
	
}

/** 
 * Set the sole child of a given parent node.  This removes all previous
 * children and sets the target as the sole child.
 * @param DOMNode parent
 * @param DOMNode child
 **/
Wbd.Dom.setSoleChild = function(parent,child) {
	Wbd.Dom.clearChildren(parent);
	parent.appendChild(child);	
}

/**
 * Remove all children from a given parent node.
 * @param DOMNode parent
 **/
Wbd.Dom.clearChildren = function(parent) {
	for (var i=(parent.childNodes.length - 1); i>=0; i--) {
		parent.removeChild(parent.childNodes[i]);
	}
}

/**
 * Move all children from one node to another
 * @param DOMNode from
 * @param DOMNode to
 **/
Wbd.Dom.moveChildren = function(from,to) {
	for (var i=(from.childNodes.length - 1); i>=0; i--) {
		var n = from.childNodes[i];
		from.removeChild(n);
		to.appendChild(n);
	}
}

/**
 * Get the region represented by a given element.  Requires YAHOO UI dom.
 **/
Wbd.Dom.getRegion = function(obj) {
	return YAHOO.util.Dom.getRegion(obj);
}

/**
 * Get the viewport height.  Requires YAHOO UI dom.
 **/
Wbd.Dom.getViewportHeight = function() {
	return YAHOO.util.Dom.getViewportHeight();
}

Wbd.Dom.getViewportWidth = function() {
	return YAHOO.util.Dom.getViewportWidth();
}

Wbd.Dom.get = function(el) {
	return YAHOO.util.Dom.get(el);
}

Wbd.Dom.getXY = function(el) {
	return YAHOO.util.Dom.getXY(el);
}

Wbd.Dom.getY = function(el) {
	return YAHOO.util.Dom.getY(el);
}


Wbd.Dom.getX = function(el) {
	return YAHOO.util.Dom.getX(el);
}

Wbd.Dom.createTitle = function(title) {
	var box = document.createElement('div');
	box.className = 'title';
	box.appendChild(document.createTextNode(title));
	return box;
}

Wbd.Dom.getStyle = function(el,property) {
	return YAHOO.util.Dom.getStyle(el,property);
}

Wbd.Dom.setStyle = function(el,property,val) {
	YAHOO.util.Dom.setStyle(el,property,val);
}

/*
 * Util class: miscellaneous utility functions
 */

Wbd.Util = function() { }

Wbd.Util.UNICODE_NBSP = '\u00A0';

// base64 code was written by Tyler Akins and has been placed in the
// public domain.  It would be nice if you left this header intact.
// Base64 code from Tyler Akins -- http://rumkin.com

// Modified by TB for "modified Base64 for URL" (see http://en.wikipedia.org/wiki/Base64#URL_Applications)
// "+" becomes "*", "/" becomes "-"
// there are matching methods on the server side

Wbd.Util.base64KeyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-=";

Wbd.Util.base64UrlEncode = function(input) {
	var output = "";
	var chr1, chr2, chr3;
	var enc1, enc2, enc3, enc4;
	var i = 0;
	var keyStr = Wbd.Util.base64KeyStr;

	do {
		chr1 = input.charCodeAt(i++);
		chr2 = input.charCodeAt(i++);
		chr3 = input.charCodeAt(i++);

		enc1 = chr1 >> 2;
		enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
		enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
		enc4 = chr3 & 63;

		if (isNaN(chr2)) {
			enc3 = enc4 = 64;
		} else if (isNaN(chr3)) {
			enc4 = 64;
		}

		output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + 
			keyStr.charAt(enc3) + keyStr.charAt(enc4);
	} while (i < input.length);
	
	// TB - remove trailing "=" - "modified Base64 for URL" (see http://en.wikipedia.org/wiki/Base64#URL_Applications)
	
	output = output.replace(/=$/,'');

	return output;
}

Wbd.Util.base64UrlDecode = function(input) {
	var output = "";
	var chr1, chr2, chr3;
	var enc1, enc2, enc3, enc4;
	var i = 0;
	var keyStr = Wbd.Util.base64KeyStr;

	// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
	// TB - + becomes *, / becomes - for "modified Base64 for URL" (see http://en.wikipedia.org/wiki/Base64#URL_Applications)
	input = input.replace(/[^A-Za-z0-9\*\-\=]/g, "");

	do {
		enc1 = keyStr.indexOf(input.charAt(i++));
		enc2 = keyStr.indexOf(input.charAt(i++));
		enc3 = keyStr.indexOf(input.charAt(i++));
		enc4 = keyStr.indexOf(input.charAt(i++));

		chr1 = (enc1 << 2) | (enc2 >> 4);
		chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
		chr3 = ((enc3 & 3) << 6) | enc4;

		output = output + String.fromCharCode(chr1);

		if (enc3 != 64) {
			output = output + String.fromCharCode(chr2);
		}
		if (enc4 != 64) {
			output = output + String.fromCharCode(chr3);
		}
	} while (i < input.length);

	return output;
}

Wbd.Util.emptyString = function(s) {
	return s.length == 0 || s == "\0";
}

Wbd.Util.getHost = function() {
	return window.location.hostname;
}

Wbd.Util.getBaseUrl = function() {
	loc = new String(window.location.href);
	index = loc.indexOf('/apps/');
	if (index < 0) {
		// This won't work if you URL is not off the root of the host name, like laptop web sites
		index = loc.indexOf(window.location.pathname);
		return loc.substring(0,index);
	}
	return loc.substring(0,index);
}

Wbd.Util.getApp = function() {
	loc = new String(window.location.href);
	app_start = loc.indexOf('/apps/') + 6;
	subpath = loc.substring(app_start);
	app_end = subpath.indexOf('/');
	return loc.substring(app_start,app_end);
}

Wbd.Util.getScrollTop = function() {
	return (document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop);
}

Wbd.Util.isNumber = function(it) {
	n = Number(it);
	if ( isNaN(n) ) { return false; }
	str = n.toString();
	if ( str != it ) { return false; }
	return true;
}

Wbd.Util.isSafari = function() {
	return navigator && (navigator.userAgent.indexOf('Safari') > -1);
}

Wbd.Util.refresh = function() {
	var href = window.location.pathname;
	window.location.replace(href);
}

/**
 * Run a given application by matching the current location to a function
 * in the application's controller.
 * @param {Object} controller The controller to call
 * @param {String} loc The location path to use to (try to) find a method
 * to call.
 **/
Wbd.Util.run = function(controller,loc) {
		
	// split that path into parts by each slash
	
	var p = null;
	var parts = loc.replace(/^\//,'').split(/\//);
	var base = [ ];
	
	// use the parts as a queue and try to match each part to a function
	// inside this object
	
	while (p = parts.shift()) {
		
		// add to base - this will be used to remember the base URL
		
		base[base.length] = p;
		
		// turn dashes into innerCaseNames
		
		var func = p.replace(/(.)-(.)/g,function(match,a,b){return a + b.toUpperCase()});

		// try to find a function with this name
		
		if (controller[func]) {
			
			// save the current base URL
			
			base_url = base.join('/');
			
			// call it with the remaining path parts as arguments and
			// get out of here
			
			controller[func].apply(self,parts);
			return;
			
		}
		
	}
	
	alert('Could not match path "' + loc + '" to a controller function');
	
}

/*
* It seems that in Firefox, Safari and PC IE 6.0 setting the value of the select
* will select the correct option with that value.  Needs more testing: Browsers 
* that this works for, what happens if an option with that value doesn't exist,
* what if two options have that value?
*/
Wbd.Util.setSelected = function(elem,val) {
	Wbd.Forms.setSelected(elem,val);
}

/**
 * Given a container box, truncate the text inside the container to a word
 * boundary within a given upper bound on the length.  If truncation
 * occurs, add a link to "expand" to full text.  One day, it would be nice
 * to make this smarter so that it handles nested elements within the
 * element.  Right now we have to assume that the element just contains
 * plain text.
 * @param elem The element to truncate text in
 * @param len The maximum length the text can be
 * @param expand_text The text to use to say, "Expand to full text".  Defaults
 * to "... (more)"
 * @param replace_pattern The pattern to use to replace unwanted characters
 * at the end of the string before truncation.  Defaults to /\S+$/
 **/
Wbd.Util.truncate = function(elem,len,expand_text,replace_pattern) {
	
	var expand_text = expand_text ? expand_text : '... (more)';
	var replace_pattern = (replace_pattern ? replace_pattern : new RegExp(/\S+$/));

	if (elem && len > 0) {
		
		// grab the node's full value (may contain html)
		
		var innerhtml = elem.innerHTML;
		
		// grab the text of this node and all of its children
		
		var text = Wbd.Dom.getTextValue(elem);
		
		if (text.length > len) {
		
			// truncate & set the truncated text value
		
			Wbd.Dom.setTextValue(elem,text.substring(0,len).replace(replace_pattern,''));
		
			// create the expander
		
			var expander = document.createElement('a');
			expander.appendChild(document.createTextNode(expand_text));
			expander.href = '#';
			expander.onclick = function() {
				this.innerHTML = innerhtml;
				return false;
			}.closure(elem);
		
			// add the expander to the element
		
			elem.appendChild(expander);
		
		}
		
		
	}
	
}

Wbd.Util.Error = function(scope,msg) {
	
	this.getMsg = function() {
		return msg;
	}
	
	this.getScope = function() {
		return scope;
	}
	
}

Wbd.Util.isError = function(thing) {
	return typeof thing == 'object' && thing.constructor == Wbd.Util.Error;
}

/**
 * @class A class to encapsulate simple HTML generation.
 * @param {String} tag The root tag; defaults to 'HTML'.
 * @constructor
 **/
Wbd.Util.HTMLGenerator = function(tag) {
	
	var self = this;
	
	/**
	 * This generator's attributes.
	 * @type {Object}
	 **/
	var attributes = { };
	
	/**
	 * This generator's children.
	 * @type {Array}
	 **/
	var children = [ ];
	
	/**
	 * This generator's CSS classes.
	 * @type {Array}
	 **/
	var classes = [ ];
	
	/**
	 * The DOM element representing this generator.  This only becomes
	 * available after draw() has been called.  
	 * @type {Element}
	 **/
	this.el = null;
	
	/**
	 * An event that is fired when the element is drawn.  The event
	 * is passed this object.
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onDraw = new Wbd.Event.CustomEvent('draw');
	
	/**
	 * This generator's styles.
	 * @type {Object}
	 **/
	var styles = { };
	
	/**
	 * Add an arbitrary attibute to this element.
	 * @param {String} name
	 * @param {String} value
	 * @return this
	 * @type {Wbd.Util.HTMLGenerator}
	 **/
	this.addAttr = function(name,value) {
		
		attributes[name] = value;
		
		if (self.el) {
			try {
				self.el.setAttribute(name,value);
			} catch (e) {
				throw 'html generator could not set attribute; probably a browser bug';
			}
		}
		
		return self;
		
	}
	
	/**
	 * Add a class name to this element.
	 * @param {String} classname
	 * @return this
	 * @type {Wbd.Util.HTMLGenerator}
	 **/
	this.addClass = function(classname) {
		classes.push(classname);
		if (self.el) {
			Wbd.Dom.addClassName(self.el,classname);	
		}
		return self;
	}
	
	/**
	 * Add a style attribute to this element.
	 * @param {String} name
	 * @param {String} value
	 * @return this
	 * @type {Wbd.Util.HTMLGenerator}
	 **/
	this.addStyle = function(name,value) {
		styles[name] = value;
		if (self.el) {
			Wbd.Dom.setStyle(self.el,name,value);	
		}
		return self;
	}

	/**
	 * Create a text child of this element with the specified content.
	 * @param {String} text
	 * @return this
	 * @type {Wbd.Util.HTMLGenerator}
	 **/
	this.addText = function(text) {
		children.push(text);
		if (self.el) {
			self.el.appendChild(document.createTextNode(text));	
		}
		return self;
	}
	
	/**
	 * Create a new child of this element with the specified tag.
	 * @param {String} tag
	 * @return The new child
	 * @type {Wbd.Util.HTMLGenerator}
	 **/
	this.createChild = function(tag) {
		var c = new Wbd.Util.HTMLGenerator(tag);
		children.push(c);
		if (self.el) {
			self.el.appendChild(c.draw());	
		}
		return c;
	}
	
	/**
	 * Draw this structure to a DOM node.
	 * @type {Element}
	 **/
	this.draw = function() {
		
		// if already drawn, just return the drawn element
		
		if (self.el) {
			return self.el;
		}
		
		// if this is an input element, help <=IE6 out, and optimize
		
		if (tag == 'input') {
			try {
				var attrs = [ ];
				Wbd.Lang.walk(attributes,function(name,value){
					attrs[attrs.length] = [name,'="',value,'"'].join('');
				});
				self.el = document.createElement(['<',tag,' ',attrs.join(' '),'>'].join(''));
			} catch (e) {
				// probably failed because we are not on IE
			}
		}
		
		// create the element with all of its attributes
		
		if (!self.el) {
			self.el = document.createElement(tag);
			Wbd.Lang.walk(attributes,function(name,value){
				self.el.setAttribute(name,value);
			});
		}
		
		if (!self.el) {
			throw 'html generator could not draw the element';
		}
		
		// add all classes
		
		Wbd.Lang.walk(classes,function(c){
			Wbd.Dom.addClassName(self.el,c);
		});
		
		// set all styles
		
		Wbd.Lang.walk(styles,function(name,value){
			Wbd.Dom.setStyle(self.el,name,value);
		});
		
		// draw descendants
		
		Wbd.Lang.walk(children,function(c){
			if (Wbd.Lang.isString(c)) {
				self.el.appendChild(document.createTextNode(c));
			} else {
				self.el.appendChild(c.draw());
			}
		})
		
		// fire onDraw
		
		self.onDraw.fire(self);
		
		// return the element
		
		return self.el;
		
	}
	
}

/**
 * @class An object to encapsulate an asynchronous connection.
 * @constructor
 **/
Wbd.Util.AsyncConnection = function() {
	
	var self = this;
	
	/**
	 * The form to post, if any.
	 * @type {Element}
	 **/
	var form;
	
	/**
	 * The HTTP method ('GET' or 'POST').  Defaults to 'GET'.
	 * @type {String}
	 **/
	this.method = 'GET';
	
	/**
	 * A custom event that gets fired if this connection is aborted.  Only
	 * the event type ('abort') is passed to subscribers. This event does
	 * not get fired in the request bypass case (see {@link #bypassRequest})
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onAbort = new Wbd.Event.CustomEvent('abort');
	
	/**
	 * A custom event that gets fired no matter how this connection is
	 * completed.  No parameters.  Does not get fired in the request
	 * bypass case (see {@link #bypassRequest}).
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onComplete = new Wbd.Event.CustomEvent('complete');
	
	/**
	 * A custom event that gets fired if this connection fails.  The event
	 * type ('failure') and an object with keys 'status' (the HTTP status code)
	 * and 'statusText' (the associated text) are passed to subscribers.
	 * This event does not get fired in the request bypass case 
	 * (see {@link #bypassRequest})
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onFailure = new Wbd.Event.CustomEvent('failure');
	
	/**
	 * A custom event that gets fired when the request is initiated.  The
	 * event type ('start') is passed to subscribers.  This event does
	 * not get fired in the request bypass case (see {@link #bypassRequest})
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onStart = new Wbd.Event.CustomEvent('start');
	
	/**
	 * A custom event that gets fired if this connection succeeds.  The event
	 * type ('success') and an object with keys 'responseText' and 
	 * 'responseXML' are passed.  This event does not get fired in the 
	 * request bypass case (see {@link #bypassRequest})
	 * @type {Wbd.Event.CustomEvent}
	 **/
	this.onSuccess = new Wbd.Event.CustomEvent('success');

	/** 
	 * The URL to GET/POST to.
	 * @type {String}
	 **/
	this.url = '';
	
	/**
	 * If bypassRequest has been called, the callback to bypass the request
	 * with.
	 * @type {Function}
	 **/
	var request_bypass;
	
	/**
	 * The YAHOO connection that actually does the work.
	 * @type {Object}
	 **/
	var yahoo_conn;
	
	/**
	 * Abort the request.
	 **/
	this.abort = function() {
		if (yahoo_conn) {
			yahoo_conn.abort();
			self.onAbort.fire();
			self.onComplete.fire();
		}
	}
	
	/**
	 * Bypass the request, defining a custom function to be called on
	 * execute().  This completely bypasses the asynchronous request and
	 * any event handlers.
	 * @param {Function} callback Function to be called on execute(), with
	 * no parameters.
	 **/
	this.bypassRequest = function(callback) {
		request_bypass = callback;
	}
	
	/**
	 * Execute this request.
	 **/
	this.execute = function() {
		
		if (request_bypass) {
			request_bypass();
		} else {
			
			// if a form has been set, handle it
			
			if (form) {
				
				YAHOO.util.Connect.setForm(form);
				self.method = 'POST';
				
				if (!self.url) {
					self.url = form.action;
				}
				
			}
			
			// fire the start event
			
			self.onStart.fire();
			
			// create the callback object as YAHOO wants it
			
			var callback = 
				{
					success : function(response) {
						self.onSuccess.fire(response);
						self.onComplete.fire();
					},
					failure : function(response) {
						self.onFailure.fire(response)
						self.onComplete.fire();
					}
				};
			
			// begin the request
			
			yahoo_conn = YAHOO.util.Connect.asyncRequest(self.method,self.url,callback);
			
		}
		
	}
	
	/**
	 * Set a form to POST.  This populates this request object as follows:
	 * - pulls all data from the form
	 * - pulls URL from the form's action (as long as the URL hasn't been set yet)
	 * - makes this a POST connection
	 * @param {Element|String} frm
	 **/
	this.setForm = function(frm) {
		form = Wbd.Dom.get(frm);
	}
	
}



/**
 * Forms class: common form-related operations
 */
 
Wbd.Forms = function() { }

/**
 * Clear the selection of a single-item select box.
 * @param Select selem
 **/
Wbd.Forms.clearSelected = function(selem) {
	if (selem.selectedIndex >= 0 && selem.options.length > selem.selectedIndex) {
		selem.options[selem.selectedIndex].selected = false;
		selem.selectedIndex = -1;
	}
}

/**
 * Given an errors object, render each message into an unordered list.  
 * Add the result of this method to a div class="errors" to mimick what
 * wbd.forms:errors does.
 **/
Wbd.Forms.drawErrors = function(errors) {
	
	var ul = document.createElement('ul');
	
	for (var v in errors) {
		var li = document.createElement('li');
		li.appendChild(document.createTextNode(errors[v]));
		ul.appendChild(li);
	}
	
	return ul;
	
}

/**
 * Get an object containing all data represented in a form
 */
Wbd.Forms.getFormData = function(frm) {
	var data = { };
	for (var i=0; i<frm.elements.length; i++) {
		var e = frm.elements[i];
		switch (e.type) {
			case 'text':
			case 'hidden':
			case 'submit':
			case 'button':
			case 'select-one':
				data[e.name] = e.value;
				break;
				
			case 'radio':
				if (e.selected) {
					data[e.name] = e.value;
				}
				break;
				
			case 'checkbox':
				if (e.checked) {
					data[e.name] = e.value;
				}
				break;
			
			default:
				alert('Unexpected form input type ' + e.type + '; please add to Wbd.Forms.getFormData (found in wbd.js)');
		}
	}
	return data;
}

/*
 * It seems that in Firefox, Safari and PC IE 6.0 setting the value of the select
 * will select the correct option with that value.  Needs more testing: Browsers 
 * that this works for, what happens if an option with that value doesn't exist,
 * what if two options have that value?
 */
Wbd.Forms.setSelected = function(elem,val) {
	opt = elem.options[ elem.selectedIndex ];
	opt.selected = false;
	for (i = 0; i < elem.options.length; i++) {
		if ( elem.options[i].value == val ) {
			elem.options[i].selected = true;
			return;
		}
	}
}

/**
 * Create a "status" area, which can display progress data.   Status
 * areas are meant to contain the elements that can trigger them to 
 * display messages.
 * @return Wbd.Forms.StatusArea
 */
Wbd.Forms.createStatusArea = function(parent) {
	return new Wbd.Forms.StatusArea(parent);
}

Wbd.Forms.StatusArea = function(parent,className) { 
	if (!parent) { return; }
	
	this.msg = "";
	this.area = document.createElement('span');
	Wbd.Dom.addClassName(this.area,'status');
	if (className) { Wbd.Dom.addClassName(this.area,className); }
	parent.appendChild(this.area);
	
	this.intvl_id = null;
	
	var self = this;
	this.createMsgArea = function() {
		if (!self.msg_area) {
			self.msg_area = document.createElement('span');
			Wbd.Dom.addClassName(self.msg_area,'msg_area');
			self.area.appendChild(self.msg_area);
		}
	}
}
new Wbd.Forms.StatusArea();
Wbd.Forms.StatusArea.prototype = 
	{
	
		setMessage : function(msg,timeout) {
			this.createMsgArea();
			this.msg_area.innerHTML = msg;
			if (timeout) {
				var self = this;
				var fn = function() {
					window.clearInterval(self.intvl_id);
					self.msg_area.innerHTML = '';
				}
				self.intvl_id = window.setInterval(fn,timeout * 1000);
			}
		},
	
		appendChild : function(child) {
			this.area.appendChild(child);
		},
		
		removeChild : function(child) {
			this.area.removeChild(child);
		},
		
		replaceChild : function(old_child,new_child) {
			this.area.replaceChild(old_child,new_child);
		}
		
	};
/**
 * replaceFields
 * takes every form field on the page and replaces it with plain text 
 **/
Wbd.Forms.replaceFields = function() {
	
	/**
	 * replaceWithText
	 * used as a standard textreplacement
	 **/
	
	var replaceWithText = function (formNum, elemNum, specStr){
		
		//make a new span element
		var sp = document.createElement("span");
		
		//set the span's class to value, so it formats properly
		sp.setAttribute("class", "value");
		
		//set the innerHTML of the span to the string
		sp.innerHTML = specStr;
		
		//replace carriage returns with line break tags
		sp.innerHTML = sp.innerHTML.replace(/\n/g, '<br />');
		
		//replace the text input element in the form with the new span
		var n = document.forms[formNum].elements[elemNum];
		document.forms[formNum].elements[elemNum].parentNode.replaceChild(sp, n);
	}
	for (i=0; i<document.forms.length; i++) {
		// for each form on the page
		for (j=document.forms[i].elements.length-1; j>=0; j--) {
			/* for each element in that form
			// go backwards to prevent live array errors
			// Separate by type of element
			*/
			var val = document.forms[i].elements[j].value;
			switch (document.forms[i].elements[j].type){
				case "button" :
					//buttons will only need to have their 'value' att printed...
					replaceWithText(i,j,val);
					break;
					
				case "checkbox" :
					//remove the checkbox itself and leave the text
					//put an 'X' in place of any checked boxes
					if (document.forms[i].elements[j].checked){
						replaceWithText(i,j,"X - ");
					} else{
						var n = document.forms[i].elements[j];
						document.forms[i].elements[j].parentNode.removeChild(n);
					}
					break;
					
				case "file" :
					//print out file path text, or 'no file' if nothing was specified
					if(document.forms[i].elements[j].value){
						replaceWithText(i,j,val);
					} else {
						replaceWithText(i,j,"No File - ");
					}
					break;
					
				case "password" :
					//just replace with stars...
					replaceWithText(i,j,"****** - ");
					break;
					
				case "radio" :
					//highlight the value of the radio button that was selected...
					if (document.forms[i].elements[j].checked){
						replaceWithText(i,j,"X - ");
					} else{
						var n = document.forms[i].elements[j];
						document.forms[i].elements[j].parentNode.removeChild(n);
					}
					break;
					
				case "reset" :
					//buttons will only need to have their 'value' att printed...
					replaceWithText(i,j,val);
					break;
					
				case "select-one" :
					//take the one selected and print it out
					//document.forms[i].elements[j].disabled=true;
					var sel = document.forms[i].elements[j].options.selectedIndex;
					if (sel >= 0){
						var txt = document.forms[i].elements[j].options[sel].innerHTML;
						replaceWithText(i,j,txt);
					}
					break;
					
				case "select-multiple" :
					//print all selected values?
					document.forms[i].elements[j].disabled=true;
					break;
					
				case "submit" :
					//buttons will only need to have their 'value' att printed...
					replaceWithText(i,j,val);
					break;
					
				case "text" :
					//pull text out of the box and print it.
					replaceWithText(i,j,val);
					break;
					
				case "textarea" :
					//print out the text area... same as 'text'
					replaceWithText(i,j,val);
					break;
					
				default :
				//anything else, don't touch.
				;
			}
		}
	}
}


/*
 * Messages class: user messages (notices, warnings, and errors)
 */

Wbd.Messages = function() { }

Wbd.Messages.notice		= function(msg) { Wbd.Messages.add('notice',msg); }
Wbd.Messages.error		= function(msg) { Wbd.Messages.add('error',msg); }
Wbd.Messages.warning	= function(msg) { Wbd.Messages.add('warning',msg); }

Wbd.Messages.add = function(type,msg) {
	box = document.getElementById('messages');
	if ( !box ) { alert(msg); return; }
	
	// try to find a previously-existing box of this type
	d = null;
	divs = box.getElementsByTagName("div");
	for (i = 0; i < divs.length; i++) {
		if ( divs[i].className == type ) {
			d = divs[i];
			break;
		}
	}
	
	if ( d ) {
	
		// found pre-existing div
		lists = d.getElementsByTagName("ol");
		ol = lists[0];
		
	} else {
	
		// create a div
		d = document.createElement('div');
		d.className = type;
	
		// add a title
		t = document.createElement('span');
		t.className = "title";
		t.innerHTML = ucfirst(type) + "s:";
		d.appendChild(t);
		
		// add an ordered list
		ol = document.createElement("ol");
		d.appendChild(ol);
		
		box.appendChild(d);
	}
	
	// create a new list item and append it
	li = document.createElement("li");
	li.innerHTML = msg;
	ol.appendChild(li);
}


/*
 * ADD-ONS TO EXISTING PROTOTYPES (classes)
 * This section also fixes compatibility issues in some browsers
 */

if (!window.Node) {
	var Node = {
		ELEMENT_NODE : 1,
		ATTRIBUTE_NODE : 2,
		TEXT_NODE : 3,
		COMMENT_NODE : 8,
		DOCUMENT_NODE : 9,
		DOCUMENT_FRAGMENT_NODE : 10
	}
}

String.prototype.ucfirst = function() {
	return this.substr(0,1).toUpperCase() + this.substr(1);
}

if (!Array.prototype.push) {
	Array.prototype.push = function(item) {
		this[this.length] = item;
	}
}

if (!Array.prototype.pop) {
	Array.prototype.pop = function() {
		var item = this[this.length - 1];
		this.length--;
		return item;
	}
}

Array.prototype.append = function(items) {
	for (var i=0; i<items.length; i++) {
		this.push(items[i]);
	}
	return this;
}

if (!Function.prototype.apply) {
	Function.prototype.apply = function(thisobj,args) {
		// build list of arguments to pass by reference
		var arglist = [ ];
		if (args) {
			for (var i=0; i<args.length; i++) { arglist.push("args["+i+"]"); }
		}
		
		// create an eval string an eval it
		var evalstr = "this("+arglist.join(",")+")";
		return eval(evalstr);
	}
}

if (!Function.prototype.call) {
	Function.prototype.call = function(thisobj) {
		var args = [ ];
		for (var i=1; i<arguments.length; i++) { args.push(arguments[i]); }
		this.apply(thisobj,args);
	}
}

// use this to avoid IE memory leaks when using closures that produce
// circular references; see http://laurens.vd.oever.nl/weblog/items2005/closures/
// TB: I've modified this function to make passing of obj optional; if obj
// is not passed, it is assumed to be the function being called; if obj
// is passed, it becomes "this" inside the function being called
Function.prototype.closure = function(obj) {
	obj = obj ? obj : this;
	
	// Init object storage.
	if (!window.__objs) {
		window.__objs = [];
		window.__funs = [];
	}
	
	// For symmetry and clarity.
	var fun = this;
	
	// Make sure the object has an id and is stored in the object store.
	var objId = obj.__objId;
	if (!objId) { __objs[objId = obj.__objId = __objs.length] = obj; }
	
	// Make sure the function has an id and is stored in the function store.
	var funId = fun.__funId;
	if (!funId) { __funs[funId = fun.__funId = __funs.length] = fun; }
	
	// Init closure storage.
	if (!obj.__closures) { obj.__closures = []; }
	
	// See if we previously created a closure for this object/function pair.
	var closure = obj.__closures[funId];
	if (closure) { return closure; }
	
	// Clear references to keep them out of the closure scope.
	obj = null; fun = null;
	
	// Create the closure, store in cache and return result.
	return __objs[objId].__closures[funId] = function () {
		return __funs[funId].apply(__objs[objId], arguments);
	}
}

Wbd.strftime_funks = {
	zeropad: function(n,len,side) {
		len = len ? len : 2;
		side = side ? side : 'left';
		var nstr = new String(n);
		while (nstr.length < len) { 
			if (side == 'left') { nstr = '0' + nstr; }
			else { nstr = nstr + '0'; }
		}
		return nstr; 
	},
	a: function(t) { return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][t.getDay()] },
	A: function(t) { return ['Sunday','Monday','Tuedsay','Wednesday','Thursday','Friday','Saturday'][t.getDay()] },
	b: function(t) { return ['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'][t.getMonth()] },
	B: function(t) { return ['January','February','March','April','May','June', 'July','August',
	    'September','October','November','December'][t.getMonth()] },
	c: function(t) { return t.toString() },
	d: function(t) { return this.zeropad(t.getDate()) },
	H: function(t) { return this.zeropad(t.getHours()) },
	I: function(t) { return this.zeropad((t.getHours() + 12) % 12) },
	m: function(t) { return this.zeropad(t.getMonth()+1) }, // month-1
	M: function(t) { return this.zeropad(t.getMinutes()) },
	p: function(t) { return this.H(t) < 12 ? 'AM' : 'PM'; },
	S: function(t) { return this.zeropad(t.getSeconds()) },
	w: function(t) { return t.getDay() }, // 0..6 == sun..sat
	y: function(t) { return this.zeropad(this.Y(t) % 100); },
	Y: function(t) { return t.getFullYear() },
	z: function(t) {
		var offset = t.getTimezoneOffset();
		var sign = offset > 0 ? '-' : '+';
		return sign + '0' + this.zeropad(offset/60,3,'right');
	},
	Z: function(t) { return this.z(t); },
	'%': function(t) { return '%' }
};

Date.prototype.strftime = function (fmt) {
    var t = this;
    for (var s in Wbd.strftime_funks) {
        if (s.length == 1 )
            fmt = fmt.replace('%' + s, Wbd.strftime_funks[s](t));
    }
    return fmt;
};

// JS Cookie handling
Wbd.Cookie = function() { }
Wbd.Cookie.setCookie = function(name, value, expires, path, domain, secure) {
	var curCookie = name + "=" + value +
		((expires) ? "; expires=" + expires.toGMTString() : "") +
		((path) ? "; path=" + path : "") +
		((domain) ? "; domain=" + domain : "") +
		((secure) ? "; secure" : "");
	document.cookie = curCookie;
}
Wbd.Cookie.getCookie = function(name) {
	var dc = document.cookie;
	var prefix = name + "=";
	var begin = dc.indexOf("; " + prefix);
	if (begin == -1) {
		begin = dc.indexOf(prefix);
		if (begin != 0) return null;
	} else
		begin += 2;
	var end = document.cookie.indexOf(";", begin);
	if (end == -1)
		end = dc.length;
	return dc.substring(begin + prefix.length, end);
}
Wbd.Cookie.deleteCookie = function(name, path, domain) {
	if (Wbd.Cookie.getCookie(name)) {
		document.cookie = name + "=" + 
		((path) ? "; path=" + path : "") +
		((domain) ? "; domain=" + domain : "") +
		"; expires=Thu, 01-Jan-70 00:00:01 GMT";
	}
}

// backwards compatibility

function getHost() { return Wbd.Util.getHost(); }
function getBaseUrl() { return Wbd.Util.getBaseUrl(); }
function getApp() { return Wbd.Util.getApp(); }
function setSelected(elem,val) { return Wbd.Util.setSelected(elem,val); }
function notice(msg) { Wbd.Messages.notice(msg); }
function error(msg) { Wbd.Messages.error(msg); }
function warning(msg) { Wbd.Messages.warning(msg); }
function ucfirst(str) { return str.ucfirst(); }
function refreshPage() { Wbd.Util.refresh(); }
function isNumber(it) { return Wbd.Util.isNumber(it); }
function isSafari() { return Wbd.Util.isSafari(); }

/*
 * wbd-ajax-lib.js
 * WBD AJAX library, to take the pain out of remote scripting
 * Copyright (C) 2005 Workflow by Design
 * Author: Taylor Barstow
 */


// Constructor
function Ajax(url,callback,params,method) {

	this.url = url ? url : '';
	this.params = params ? params : {};
	this.callback = callback ? callback : null;
	this.method = method ? method : "GET";
	
	if ( window.XMLHttpRequest ) {			// Mozilla, Safari, ...
		this.http_request = new XMLHttpRequest();
	} else if ( window.ActiveXObject ) {	// IE
		try {
			this.http_request = new ActiveXObject('Msxml2.XMLHTTP');
		} catch (e) {
			try {
				this.http_request = new ActiveXObject('Microsoft.XMLHTTP');
			} catch (e) { }
		}
	}
	
	this.valid = Boolean(this.http_request);

}

//
// DATA MEMBERS
//

Ajax.prototype.valid = false;
Ajax.prototype.url = '';						// the URL to request
Ajax.prototype.params = {};						// parameters, if any
Ajax.prototype.method = 'GET';					// the method (POST or GET)
Ajax.prototype.async = true;					// asynchronous? (typically true)
Ajax.prototype.callback = null;					// the function to call on response
Ajax.prototype.ignore_response_errors = false;	// whether or not to ignore response errors (like 404s)
Ajax.prototype.http_request = null;				// (private) the http request object


// 
// METHODS
//

// doRequest: send a request to the server
Ajax.prototype.doRequest = function() {

	if ( !this.url || this.url == '' ) {
		error('no url');
		return false;
	}
	if ( !this.http_request ) {
		error('no http_request');
		return false;
	}
	
	// build parameter list
	
	var ps = [];
	for ( var pname in this.params ) {
		ps.push( escape(pname) + "=" + escape(this.params[pname]) );
	}
	
	var params = "";
	var url = this.url;
	if ( ps.length > 0 ) {		
		params = ps.join("&");
		if ( this.method == 'GET' ) {
			url += this.url.indexOf('?') == -1 ? '?' : '&';
			url += params;
			params = null;
		}
	}
	
	// call the ajax functions
	
	var method = this.method.toUpperCase();
	if ( this.callback != null ) {
		this.http_request.onreadystatechange = this.stateChangeCallback();
	}
	this.http_request.open(method,url,this.async);
	if ( method == 'POST' ) {
		this.http_request.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
	}
	this.http_request.send(params);	
	
}

// getDataResponse: eval js code returned by the server and return the result
Ajax.prototype.getDataResponse = function() {
	return eval(this.getTxtResponse());
}

// getTxtResponse: get the txt formatted response
Ajax.prototype.getTxtResponse = function() {
	return this.http_request.responseText;
}

// getXmlResponse: get the xml formatted response (use with DOM functions)
Ajax.prototype.getXmlResponse = function() {
	return this.http_request.responseXML;
}

// stateChangeCallback: returns callback that gets called on ready state change
Ajax.prototype.stateChangeCallback = function() {
	return function() {		// this is the actual callback function, nicely curried
		if ( this.http_request.readyState == 4 ) {
			if ( this.http_request.status == 200 ) {
				// OK, call the real callback
				this.callback(this);
			} else {
				if ( !this.ignore_response_errors ) {
					error("Error retrieving data from the server: " + this.http_request.statusText);
				}
			}
		}
	}.closure(this);
}

// Determine if this browser is stupid.
var detect = navigator.userAgent.toLowerCase();
var macIE = false;
if ((detect.indexOf('msie') >= 0) 
 && (detect.indexOf('mac') >= 0)) {
	macIE = true;
}

/*****************************************************************************
 * function getObj(name): Select available methods for locating DOM objects.  
 * Then return a pointer to that object and create a put() function to write to
 * webpage document. 
 * PRECONDITION: A string containing the ID for the object is passed as name.
 * POSTCONDITION: return an object that points to the DOM object requested, 
 * create the put() function for document output.
 *****************************************************************************/
function getObj(name) {
  if (document.getElementById) {  // Test for DOM level 2 compliance. IE 5+ & Mozilla 6+
  	this.obj = document.getElementById(name);  // Point variable to object.
	/*************************************************************************
	 * Function put(obj, display): write display string to object.
	 * PRECONDITION: obj points to a DOM lvl2 object and display is a sting 
	 *				 containing the text to output.
	 * POSTCONDITION: obj has a new value equal to display.
	 *************************************************************************/
	put = new Function("obj", "display", "if (obj) { obj.firstChild.nodeValue = display }");
  } 
}

/**
 * Return an element based upon its id.
 * PRECONDITION:
 * POSTCONDITION:
 */
function getObject(objectId) {
	// checkW3C DOM, then MSIE 4, then NN 4.
	if (document.getElementById && document.getElementById(objectId)) {
		return document.getElementById(objectId);
	} else if (document.all && document.all(objectId)) {  
		return document.all(objectId);
	} else if (document.forms[0] && document.forms[0].elements[objectId]) {
		return document.forms[0].elements[objectId];
	}  else {
		return false;
	}
}

/**
 * Delete an option from a select list
 * PRECONDITION:
 * POSTCONDITION:
 */
function deleteListItem(list) {
	var sel = getObject(list);	// The current Select element
	
	if (sel && sel.type == 'select-one' && !sel.disabled) {
		var indx = sel.selectedIndex;
		
		if (indx < 0) {	// If nothing is selected
			return false;
		} else {
			sel.options[indx] = null;
			sel.selectedIndex = (indx < sel.options.length) ? indx : sel.options.length-1;
			return true;
		}
	}
	return false;
}

/**
 * Add an option to a select list with special considerations
 * PRECONDITIONS:
 * POSTCONDITIONS:
 */ 
function addListItem(list, text, value, defSel, selected) {
	var sel = getObject(list);	// The current Select element

	// Ensure that we have all the data we need to create a new option
	if (sel && sel.type == 'select-one'/*ROB:2005-01-10 && text != "" && value != ""*/) {
		// Add a new option, and set its class
		sel.options[sel.length] = new Option(text, value, Boolean(defSel), Boolean(selected));

		if (sel.selectedIndex < 0) // If nothing was selected then select the new option
			sel.selectedIndex = sel.length-1;
		return true;
	}
	return false;
}

/**
 * Reorder the options in a select list either up or down
 * PRECONDITION:
 * POSTCONDITION:
 */
function moveListItem(list, direction) {
	var sel = getObject(list);	// The current Select element
	
	if (sel && sel.type == 'select-one' && !sel.disabled) {
		var indx = sel.selectedIndex;
		
		if ((indx < 0)										// if nothing is selected
			|| (indx == 0 && direction < 0)					// or if trying to move the first element up
			|| (indx == sel.length-1 && direction > 0)) {	// or if trying to move the last element down
			return true;
		} else {
			// Store a copy of the options being moved and replaced.
			var replaced = new Option(
				sel.options[indx+direction].text,
				sel.options[indx+direction].value,
				sel.options[indx+direction].defaultSelected,
				sel.options[indx+direction].selected);
			var selected = new Option(
				sel.options[indx].text,
				sel.options[indx].value,
				sel.options[indx].defaultSelected,
				sel.options[indx].selected);
			
			// Replace the option above/below the moving option with the copy of the moving option
			sel.options[indx+direction] = selected;
			// Replace the moving option with the replaced option's copy
			sel.options[indx] = replaced;
			return true;
		}
	}
	return false;
}

function addOnClick(fromL, toL) {
	var sel = getObject(fromL);	// The current Select element
	
	// Ensure that we have all the data we need to create a new option
	if (sel && sel.type == 'select-one') {
		var indx = sel.selectedIndex;
		
		if (indx >= 0)
			addListItem(toL, sel.options[indx].text, sel.options[indx].value, false, false);
	}
}

function saveList(theForm,from,to) {
	// TB 2005-10-14 allow passing the names of the from and to objects
	// (backwards compatible)
	from = from ? from : 'toList';		// Allow the user to pass from & to 
	to = to ? to : 'postString';		// or use old defaults
	var sel = theForm.elements[from];	// The current Select element
	var txt = theForm.elements[to];		// The string to submit from list

	txt.value = "";
	if (sel && sel.type == 'select-one') {
		for (i=0; i < sel.options.length; i++) {
			val = sel.options[i].value;
			indx = val.indexOf("\t");
			if (indx >= 0)
				txt.value += val.substr(0, indx);
			else
				txt.value += val;
			txt.value += "\n";
		}
		txt.value = txt.value.substr(0, txt.value.length-1);
		return true;
	}
//	return false;  // Removed for category dev
}

function addEmail(toL) {
	var me = getObject('email');	// The current Email element
	// regex copied from stdlib.inc.php:valid_email()
	var myRegEx = /^[a-zA-Z0-9_\.\-']+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$/;
	if (me && me.value)
		m = myRegEx.exec(me.value);

	if ((me.value != "") && (m != null)) {
		addListItem(toL, m[0], m[0]+"\t\t"+m[0]+"\t", false, false);
		me.value = "";
	} else {
		alert("'"+me.value+"' is not a valid email address.");
	}
}

function legacyChangeDetails(me) {
	var val=me.options[me.selectedIndex].value.split("\t");
	
	element = new getObj("userName");
	put(element.obj, "Username: " + val[0]);
	put(new getObj("userComp").obj,  "Company: " + val[1]);
	put(new getObj("userEmail").obj, "Email: " + val[2]);
}

function changeDetails(me) {
	id = me.options[me.selectedIndex].value;
	fetchMemberDetails(id);
}

// This function applies to deleting files
function sureDelete(location) {
	if(confirm("Are you sure you want to delete this item?"))
		window.location = location;
}

// These functions all apply to admin/users.php
function sureDisable(location) {
	if(confirm("Disabled user will be removed from all groups and sets, continue?"))
		window.location = location;
}

function submitForm() {
	// requires a form named entryForm
	document.entryform.status.value = 'enabled';
	document.entryform.submit();
}

// call an AJAX method to get the details for a particular member of a set
// (id could be role id or username)
// NOTE this routine requires ajax.js
function fetchMemberDetails(id) {
	ajax = new Ajax;
	if ( !ajax.valid || !document.getElementById ) {
		return;
	}
	
	if (isNumber(id)) {
		type = "set";
	} else if (id.indexOf(':') >= 0) {
		var parts = id.split(/:/);
		type = parts[0];
		id = parts[1];
	} else {
		type = "user";
	}
	ajax.url = Wbd.UrlGenerator.generate("sets/get-details/" + type + "/" + id);
	ajax.callback = function(a) {
		html = a.getTxtResponse();
		details = document.getElementById("memberDetails");
		details.innerHTML = html;
	};
	ajax.ignore_response_errors = true;
	ajax.doRequest();
}

/*** BEGIN Special for Checkout.php ***/
function saveCheckout() {
	document.editSets.email.value = "";
	if(document.editSets != null) {
		l=document.editSets.toList.length;
		for(i=0; i<l; i++) {
			val=document.editSets.toList.options[i].value;
			indx=val.lastIndexOf("\t");
			if(indx < val.length) { 
				val=val.slice(indx+1);
			}
			document.editSets.email.value += val + "\t";
		}
	}	
	return(true);
}
/*** END Special for Checkout.php ***/
/**
 * Top-level namespace for UI related functionality.
 * @constructor
 **/
Wbd.UI = function() { }

/**
 * Add a confirmation dialog on click of a given set of elements in the
 * current document.
 * @param {Array} elems List of elements to add behavior to
 * @param {String} msg Confirmation message
 **/
Wbd.UI.addConfirmation = function(elems,msg) {
	
	Wbd.Lang.walk(elems,function(e){
		Wbd.Event.addListener(e,'click',function(evt){
			var confirm = window.confirm(msg);
			if (!confirm) {
				Wbd.Event.stopEvent(evt);
			}
		});
	});
	
}

/**
 * Add a delete confirmation message to any delete links in the current
 * document.  Delete links have class "action_delete".
 **/
Wbd.UI.addDeleteConfirmation = function() {
	
	var elems = Wbd.Dom.getElementsByClassName(document.body,'*','action_delete');
	Wbd.UI.addConfirmation(elems,'Are you SURE you want to delete the selected item?  This action CANNOT be undone.');
	
}

/**
 * Convenience function to create a new hierarchy selector using the append
 * to base URL method for generating URLs.
 * @param {Array} hier_types See Wbd.UI.HierarchySelector.setHierarchyTypes()
 * @param {String} base_url See Wbd.UI.HierarchySelector.generateURLAppendToBase()
 * @return The hierarchy select object.
 * @type {Wbd.UI.HierarchySelector}
 **/
Wbd.UI.addHierarchySelectBehaviors = function(hier_types,base_url) {
	var h = new Wbd.UI.HierarchySelector();
	h.urlGenerator = h.generateURLAppendToBase(base_url);
	h.setHierarchyTypes(hier_types);
	return h;
}

/**
 * Class to manage a set of descendant hierarchy selectors.
 * @constructor
 **/
Wbd.UI.HierarchySelector = function() {
	
	var self = this;
	
	/**
	 * The hierarchy points controlled by this hierarchy selector, in correct
	 * order.  Each array element is an object containing the following:
	 * - ref: The reference for this hierarchy level
	 * - elem: The select element for this hierarchy level
	 * @type {Array} 
	 **/
	var hiers;
	
	/**
	 * Custom event that is fired when any of the select elements
	 * changes.  Subscribers take the name of the event ('change'),
	 * the specific element that just changed, and the current value 
	 * (see getCurrentValue).
	 **/
	this.onChange = new Wbd.Event.CustomEvent('change');
	
	/**
	 * Custom event that is fired when the list of hierarchy types is set,
	 * that is, at load time.  Subscribers take the name of the event
	 * ('set_hierarchy_types') and the current value (see getCurrentValue()).
	 **/
	this.onSetHierarchyTypes = new Wbd.Event.CustomEvent('set_hierarchy_types');
	
	/**
	 * This hierarchy selector's URL generator.  A URL generator is simply
	 * a function that is passed the currently selected hierarchy ID
	 * and is expected to return a URL to retrieve the children of
	 * that hierarchy ID from the server.  This class provides common
	 * URL generators, see methods named generateURL*
	 **/
	this.urlGenerator;
	
	/**
	 * Clear this hierarchy selector.
	 **/
	this.clear = function() {
		Wbd.Lang.walk(hiers,function(h){
			h.elem.selectedIndex = 0;
		});
	}
	
	/**
	 * URL generator that simply appends the selected hierarchy ID
	 * as the last controller paramter, given a base URL to work with.
	 **/
	this.generateURLAppendToBase = function(base) {
		return function(id) {
			return base.replace(/\/$/,'') + '/' + id;
		}
	}
	
	/**
	 * Get the current value of this hierarchy selector, represented as
	 * an array of objects, each representing a hierarchy point.  Each object 
	 * contains:
	 * - ref: the reference for this hierarchy level
	 * - value: the currently selected hierarchy id (may be empty)
	 * - label: the label for the selected item (empty of value is empty)
	 * - elem: the form element for this hierarchy point
	 * @type {Array}
	 **/
	var getCurrentValue = function() {
						
		var cur_value = [ ];
		
		Wbd.Lang.walk(hiers,function(h){
			cur_value[cur_value.length] = 
				{
					'ref'		: h.ref,
					'label'		: Wbd.Dom.getTextValue(h.elem.options[h.elem.selectedIndex]),
					'value'		: h.elem.value,
					'elem'		: h.elem
				};
		});
		
		return cur_value;
		
	}
	
	/**
	 * Set the list of named hierarchy types.  Each type should also be the ID
	 * of the select element on this page representing that hierarhcy
	 * type.  For example: ['brand','sub_brand','category'].
	 * @param {Array} types
	 **/
	this.setHierarchyTypes = function(types) {
		
		// walk over the types and find the actual objects on the page
		
		var my_hiers = [ ];
		
		Wbd.Lang.walk(types,function(t){
			my_hiers[my_hiers.length] = Wbd.Dom.get(t);
		});
		
		// now use setHierarchySelectors to set them
		
		self.setHierarchySelectors(my_hiers);
		
	}
		
	/**
	 * Set the hierarchy selectors managed by this hierarchy selector.
	 * Each selector should be a select element; the name of each
	 * select element is understood as the name of the given hierarchy point.
	 * @param {Array} selectors
	 **/
	this.setHierarchySelectors = function(selectors) {
		
		// overwrite any current hierarchy select bindings
		
		hiers = [ ];
		
		// walk over the hierarchy types
		
		Wbd.Lang.walk(selectors,function(s){
			
			// add the entry to the "hiers" array
			
			var h = {'ref':s.name,'elem':s};
			hiers[hiers.length] = h;
			
			// add a listener to fire the change event
			
			Wbd.Event.addListener(h.elem,'change',function(evt){
				self.onChange.fire(h,getCurrentValue());
			});
			
		});
		
		// fire the set hierarchy types event
		
		self.onSetHierarchyTypes.fire(getCurrentValue());
		
	}
	
	/**
	 * Event listener that updates the state of the hierarchy
	 * selector when a user makes a selection, possibly pulling hierarchy
	 * children from the server.
	 * @param {Object} hier_changed
	 * @param {Array} cur_value
	 **/
	var showHierarchyChildren = function(evt,hier_changed,cur_value) {

		// empty & disable hierarchy selectors after this one

		var next 	= false;		// the next one
		var changed	= false;		// the one that changed

		Wbd.Lang.walk(cur_value,function(h){

			// if this is the one that changed, set the shared variable

			if (h.ref == hier_changed.ref) {
				changed = h;
				return;
			}
			
			// if the one that changed has already been found, empty and
			// disable this selector

			if (changed) {

				if (!next) {
					next = h;
				}

				var empty_opt = document.createElement('option');
				empty_opt.value = '';
				empty_opt.appendChild(document.createTextNode('--'));
				Wbd.Dom.setSoleChild(h.elem,empty_opt);

				h.elem.disabled = true;
				h.elem.value = '';
				
				h.value = '';
				h.label = '';

			}

		});

		// if a nonempty selection was made and there is a next selector to fill,
		// ask the server for the selected hierarchy's children and fill
		// the next one up

		if (changed && changed.value != '' && next) {

			// show a waiting indicator

			var waiting;
			waiting = new Wbd.UI.WaitingIndicator('Loading Hierarchy...');

			// create the connection object

			var conn = new Wbd.Util.AsyncConnection();
			conn.url = self.urlGenerator(changed.value);

			// on success, show the results in the next select box

			conn.onSuccess.subscribe(function(evt,resp){
				
				var children = eval(resp.responseText);
				Wbd.Lang.walk(children,function(id,label){
					var opt = document.createElement('option');
					opt.value = id;
					opt.appendChild(document.createTextNode(label));
					next.elem.appendChild(opt);
				});

				next.elem.disabled = false;

			});

			// on success, clear the waiting indicator

			conn.onSuccess.subscribe(function(){
				waiting.clear();
			});

			conn.execute();

		}
		
	}
	
	// add internal event listeners
	
	self.onChange.subscribe(showHierarchyChildren);
	self.onSetHierarchyTypes.subscribe(function(evt,cur_value){
		
		// find the first empty selector and call showHierarchyChildren
		// with that selector as the one that has "changed"
		
		Wbd.Lang.walk(cur_value,function(h){
			if (h.value == '') {
				showHierarchyChildren('',h,cur_value);
				Wbd.Lang.walk.stop();
			}
		});
		
	});
	
}

/**
 * A page object encapsulates the idea of holding multiple page-level
 * widgets inside a single object.  It provides convenience methods for
 * reporting the height of the page, and so on.
 * @constructor
 * @param {String} id This page's unique ID
 **/
Wbd.UI.Page = function(id) {
	
	var self = this;
	
	/**
	 * The box that this page is rendered into.
	 * @type {Element}
	 **/
	var box = null;
	
	/**
	 * This page's class name(s).
	 * @type {Array} 
	 **/
	var classes = ['page'];
	
	/**
	 * The page header.
	 * @type {Element|String}
	 **/
	var header = 'header';
	
	/**
	 * The list of widgets contained by this page.
	 * @type {Wbd.UI.Widget}
	 **/
	var widgets = [ ];
	
	/**
	 * Add a classname to this page.
	 * @param {String} className
	 **/
	this.addClassName = function(className) {
		
		// add to the list
		
		classes[classes.length] = className;
		
		// if this has been drawn, add the class name to the box
		
		if (box) {
			Wbd.Dom.addClassName(box,className);
		}
		
	}

	/**
	 * Add a widget to this page.
	 * @param {Wbd.UI.Widget} widg
	 **/
	this.addWidget = function(widg) {
		
		// set the widget's page, but only if it wants it - we are 
		// respectful around here, after all
		
		if (widg.setPage) {
			widg.setPage(self);
		}
		
		// add it to the list of widgets
		
		widgets[widgets.length] = widg;
		
		// if this page has already been drawn, append to our box
		
		if (box) {
			box.appendChild(widg.draw());
		}
		
	}
	
	/**
	 * Draw this page, and all of its widgets.
	 * @type {Element}
	 **/
	this.draw = function() {
		
		// create the containing box
		
		box = document.createElement('div');
		box.style.position = 'absolute';
		box.style.top = this.getUpperBound() + 'px';
		
		// set the ID if it was passed
		
		if (id) {
			box.id = id;
		}
		
		// add current class names
		
		box.className = classes.join(' ');
		
		// draw each widget
		
		Wbd.Lang.walk(widgets,function(widg){
			box.appendChild(widg.draw());
		});
		
		// return the box
		
		return box;
		
	}
	
	/**
	 * Draw this page in such a way that it wraps an existing box on 
	 * the page.
	 * @type {Element}
	 * @return The drawn box.
	 **/
	this.drawWrap = function(replace_box) {
		
		// do the normal draw
		
		self.draw();
		
		// replace the box
		
		replace_box.parentNode.replaceChild(box,replace_box);
		
		// create a content area widget containing the replaced box
		// and add it
		
		var c = new Wbd.UI.Widget.ContentArea();
		c.setContent(replace_box);
		self.addWidget(c);
		
		// return this page's box
		
		return {'box':box,'contentArea':c};
		
	}
	
	/**
	 * Get this page's "upper bound" - the topmost y coordinate within
	 * the page's scope.
	 * @type {Integer}
	 **/
	this.getUpperBound = function() {
		return Wbd.Dom.get(header).offsetHeight;
	}
	
	/**
	 * Get this page's "visible height".  The visible height is defined
	 * as the distance from the bottom-edge of the navigation to the
	 * top-edge of the window's status bar.
	 * @return The visible height in pixels.
	 * @type {Integer}
	 **/
	this.getVisibleHeight = function() {
		return Wbd.Dom.getViewportHeight() - Wbd.Dom.get(header).offsetHeight;
	}
	
	/**
	 * Get this page's "visible width".
	 * @return The visible width in pixels.
	 * @type {Integer}
	 **/
	this.getVisibleWidth = function() {
		Wbd.Dom.getRegion(box).right;
	}
	
	/**
	 * Set the header to use.  This is only necessary if not using the
	 * standard header (#header).
	 * @param {String|Element} head
	 **/
	this.setHeader = function(head) {
		header = head;
	}
	
}

/**
 * Default widget contsructor.  This is really just to provide an API for
 * widgets.  Think of it like an interface.
 * @constructor
 **/
Wbd.UI.Widget = function() {
	
	/**
	 * Draw this widget.  Required.
	 * @type {Element}
	 **/
	this.draw = function() { }
	
	/**
	 * Set the page that contains this widdget.  Optional.  Only called
	 * for page-level widgets which are directly added to pages.
	 * @param {Wbd.UI.Page} page
	 **/
	this.setPage = function(page) { }
	
}

/**
 * A widget to encapsulate an easily updateable content area.
 * @constructor
 **/
Wbd.UI.Widget.ContentArea = function() {
	
	var self = this;
	
	/**
	 * This content area's box.
	 * @type {Element}
	 **/
	var box = null;
	
	/**
	 * This content area's content.  This is only used before the box
	 * is drawn.
	 * @type {String|Element}
	 **/
	var content = null;
	
	/**
	 * This content area's class name.
	 * @type {String}
	 **/
	this.className = 'contentArea';
	
	/**
	 * This content area's ID.
	 * @type {String}
	 **/
	var id = '';
	
	/**
	 * Draw this content area.
	 * @type {Element}
	 **/
	this.draw = function() {
		
		// if the box exists, return that
		
		if (box) {
			return box;
		}
		
		// create the box
		
		box = document.createElement('div');
		box.className = self.className;
		if (id) {
			box.id = id;
		}
		
		// set the content
		
		if (content) {
			self.setContent(content);
		}
		
		// return the box
		
		return box;
		
	}
	
	/**
	 * If this content area has been drawn, get its box.
	 **/
	this.getBox = function() {
		return box;
	}
	
	/**
	 * Check if this content area has been drawn.
	 * @type {Boolean}
	 **/
	this.isDrawn = function() {
		return new Boolean(box);
	}
	
	/**
	 * Replace this content area's box with another box.  The new box's ID
	 * will be honored if it has one.  If not, this content area's ID will
	 * be used.
	 * @param {Element} new_box
	 **/
	this.replace = function(new_box) {
		
		if (box) {

			// unset the current box's ID so there is no conflict

			if (box.id) {
				box.id = '';
			}

			// do the replace

			box.parentNode.replaceChild(new_box,box);
			
		}
		
		// update the box reference
		
		box = new_box;
		
		// set the ID
		
		if (box.id) {
			id = box.id
		} else if (id) {
			box.id = id;
		}
		
		// set the classname
		
		Wbd.Dom.addClassName(box,self.className);
		
	}
	
	/**
	 * Set this box's content.
	 * @param {String|Element} content
	 **/
	this.setContent = function(new_content) {
		
		if (box) {
			
			if (typeof new_content == 'string') {
				box.innerHTML = new_content;
			} else {
				Wbd.Dom.setSoleChild(box,new_content);
			}			
			
		} else {
			content = new_content;
		}
		
	}
	
	/**
	 * Set the ID to use for this content area.
	 * @param {String} id
	 **/
	this.setID = function(new_id) {
		id = new_id;
		if (box) {
			box.id = new_id;
		}
	}
	
}

/**
 * A page-level widget to encapsulate a dock that has several panels.
 * @constructor
 **/
Wbd.UI.Widget.Dock = function() {
	
	var self = this;
	
	/**
	 * This dock's box.
	 * @type {Element}
	 **/
	var box = null;
	
	/**
	 * This dock's unique ID.
	 * @type {String}
	 **/
	this.id = "dock";
	
	/**
	 * This doc's page.
	 * @type {Wbd.UI.Page}
	 **/
	var page = null;
	
	/**
	 * This dock's panels.
	 * @type {Array}
	 **/
	var panels = [ ];
	
	/**
	 * Add a panel to this dock.  Panels are just widgets that get 
	 * displayed inside of a dock, along with a title.
	 * @param {String} title
	 * @param {Wbd.UI.Widget} widget
	 **/
	this.addPanel = function(title,widget) {
		
		// make sure this is really a widget
		
		if (!widget.draw) {
			Wbd.error("this widget is not a widget!  missing the draw() method: %o",widget);
		}
		
		// add it to the array
		
		var widg = {'title':title,'widget':widget};
		panels[panels.length] = widg;
		
		// if this thing has already been drawn, draw the widget and add it
		
		if (box) {
			drawWidget(widg);
		}
		
	}
	
	/**
	 * Draw this panel.
	 * @type {Element}
	 **/
	this.draw = function() {
		
		// create a container box
		
		box = document.createElement('div');
		box.className = 'dock';
		box.id = self.id;
		
		// in page context, we automatically stretch the dock to be
		// the full height of the page
		
		if (page) {
		
			var fnStretchHeight = function() {
				box.style.height = page.getVisibleHeight() + 'px';
			}

			fnStretchHeight();
			Wbd.Event.addListener(window,'resize',fnStretchHeight);
			
		}
		
		// draw each panel
		
		Wbd.Lang.walk(panels,function(p){
		
			// container
			
			p.box = document.createElement('div');
			p.box.className = 'panel';
			box.appendChild(p.box);
			
			// title
			
			p.title_box = document.createElement('div');
			p.title_box.className = 'titlebar';
			p.title_box.innerHTML = p.title;
			p.box.appendChild(p.title_box);
			
			// content
			
			p.content_box = document.createElement('div');
			p.content_box.className = 'content';
			p.content_box.appendChild(p.widget.draw());
			p.box.appendChild(p.content_box);
			
		});
		
		return box;
		
	}
	
	/**
	 * Get a region representing this dock.
	 **/
	this.getRegion = function() {
		return Wbd.Dom.getRegion(box);
	}
	
	/**
	 * Set this dock's page object.
	 * @param {Wbd.UI.Page} page
	 **/
	this.setPage = function(p) {
		page = p;
	}
	
}

/**
 * An object to encapsulate a pretty simple search panel, to be used
 * inside of a dock.
 * @constructor
 **/
Wbd.UI.Widget.SearchPanel = function(extra) {
	
	var self = this;
	
	/**
	 * This search panel's box.
	 * @type {Element}
	 **/
	var box = null;
	
	/**
	 * Extra content
	 * @type {Element}
	 **/
	var content = null;
	
	/**
	 * The query input element.
	 * @type {Element}
	 **/
	var qinput = null;
	
	/**
	 * The query to populate qinput with on draw.
	 * @type {String}
	 **/
	var query = { };
	
	/**
	 * The query box.
	 * @type {Element}
	 **/
	var query_box = null;
	
	/**
	 * The results box.
	 * @type {Element}
	 **/
	this.results_box = null;
	
	/**
	 * The results HTML.  This gets set if setResultsHTML is called
	 * before this object is drawn.
	 * @type {String}
	 **/
	var results_html = null;
	
	/**
	 * An event that will be fired when a user enters some search terms
	 * and clicks "go".  Subscribers will be passed the event type
	 * ("search") and the query that the user entered.
	 **/
	this.onSearch = new Wbd.Event.CustomEvent('search');
	
	/**
	 * Draw the search panel.
	 * @type {Element}
	 **/
	this.draw = function() {
		
		// containing box
		
		box = document.createElement('div');
		box.className = 'search_panel';
		
		// add extra info before search box
		
		if (extra) {
			box.appendChild(extra);
		}
		
		// query box
		
		query_box = document.createElement('div');
		query_box.className = 'query';
		box.appendChild(query_box);
		
		clear_search = document.createElement('a');
		clear_search.href = '#';
		clear_search.id = 'clear_search';
		s = document.createTextNode('Clear Search');
		clear_search.appendChild(s);
		query_box.appendChild(clear_search);
		
		br = document.createElement('br');
		query_box.appendChild(br);
		
		var frm = document.createElement('form');
		query_box.appendChild(frm);
		
		qinput = document.createElement('input');
		qinput.type = 'text';
		qinput.name = 'q';
		qinput.id = 'q';
		frm.appendChild(qinput);
		
		if (query && typeof(query) != 'object') {
			qinput.value = query;
		}
		
		var fnClearSearch = function(evt) {
			
			qinput.value='';
			
		}

		var go = document.createElement('input');
		go.type = 'submit';
		go.name = 'go';
		go.value = 'Go';
		frm.appendChild(document.createTextNode(Wbd.Util.UNICODE_NBSP));
		frm.appendChild(go);
		
		var fnTriggerSearch = function(evt) {
			
			// if it's a keypress but it's not enter, don't do anything
			
			if (evt.type == 'keypress' && Wbd.Event.getCharCode(evt) != 13) {
				return;
			}
			
			Wbd.Event.stopEvent(evt);
			self.onSearch.fire(qinput.value);
			
		}
		
		Wbd.Event.addListener(clear_search,'click',fnClearSearch);
		Wbd.Event.addListener(go,'click',fnTriggerSearch);
		Wbd.Event.addListener(qinput,'keypress',fnTriggerSearch);
		
		// results box
		
		self.results_box = document.createElement('div');
		self.results_box.className = 'sresults';
		box.appendChild(self.results_box);
		
		if (results_html) {
			self.results_box.innerHTML = results_html;
			results_html = null;
		}
		
		// extra content
		
		if (content) {
			for ( var i=0; i<content.length; i++ ) {
				box.appendChild(content[i].draw());
			}
		}
		
		return box;
			
	}
	
	this.addContent = function(cnt) {
		// make sure this is really a widget
		
		if (!cnt.draw) {
			Wbd.error("this is not a content area!  missing the draw() method: %o",cnt);
		}

		content = content || new Array();
		content[ content.length ] = cnt;
	}
	
	/**
	 * Get the current value of the query field.
	 * @param {String} q
	 **/
	this.getQuery = function() {
		if (qinput) {
			return qinput.value;
		} else if (query.value) {
			return query.value;
		} else {
			return '';
		}
	}
	
	/**
	 * Populate the query field with a given value.
	 * @param {String} q
	 **/
	this.setQuery = function(q) {
		if (qinput) {
			qinput.value = q;
		} else {
			query.value = q;
		}
	}
	
	/**
	 * Set the search results using HTML.
	 * @param {String} html
	 **/
	this.setResultsHTML = function(html) {
		if (self.results_box) {
			self.results_box.innerHTML = html;	
		} else {
			results_html = html;
		}
	}
	
}

/**
 * A waiting indicator which appears in the top-right portion of the
 * screen.  As soon as this is insantiated it will be displayed.  It can
 * be removed later by calling "clear()".
 * @param {String} message A textual message to display that indicates what
 * is going on.
 * @constructor
 **/
Wbd.UI.WaitingIndicator = function(message) {
	
	/**
	 * The DOM element for this indicator
	 * @type {Element}
	 **/
	var elem = document.createElement('div');
	elem.className = 'waiting';
	
	/**
	 * Activate this indicator with a given message.
	 * @param {String} message
	 **/
	this.activate = function(msg) {
		elem.appendChild(document.createTextNode(msg));
		document.body.appendChild(elem);
	}
	
	/**
	 * Clear this indicator.
	 **/
	this.clear = function() {
		document.body.removeChild(elem);
	}
	
	if (message) {
		this.activate(message);
	}
	
}
/**
 * @class Convenience methods to simplify some common problems related
 * to the way javascript works.
 * @constructor
 **/
Wbd.Lang = function() { }

/**
 * Normalize a pseudo-array to be real array.  Javascript is rife with
 * pseudo-arrays.  Examples: the Arguments object, and a DOM element's
 * nodeList. Note that this method explicitly copies the values out of
 * the pseudo-array and into a real array (instance of Array), which
 * is returned.
 * @param {Unknown} pseudo Anything with a length property and numbered
 * properties.
 * @type Array
 **/
Wbd.Lang.arrayNormalize = function(pseudo) {
	
	// check for the length property
	
	if (typeof pseudo.length == 'undefined') {
		throw 'cannot array-normalize an object without a length property';
	}
	
	// copy the values into an array
	
	var arr = [ ];
	for (var i=0; i<pseudo.length; i++) {
		arr[i] = pseudo[i];
	}
	
	return arr;
	
}

/**
 * "Fold" two or more objects together.  Creates a new object representing
 * the combination of all objects passed.
 * @type {Object}
 **/
Wbd.Lang.fold = function() {
	var obj = { };
	for (var i=0; i<arguments.length; i++) {
		for (var v in arguments[i]) {
			obj[v] = arguments[i][v];
		}
	}
	return obj;
}

/**
 * "Hitch" a scope onto a given method.  Returns a function that will execute
 * a given method in a given scope, passing all parameters.
 * @type {Object} scope
 * @type {Function} method
 * @type {Function}
 **/
Wbd.Lang.hitch = function(scope,method) {
	return function() {
		return method.apply(scope,arguments);
	}
}

/**
 * Check if a given object is an array.
 * @param {Unknown obj} The object to check
 **/
Wbd.Lang.isArray = function(obj) {
	return Wbd.Lang.isType(obj,Array);
}

/**
 * Check if a given object is of a particular type.
 * @param {Unknown} obj The object to check
 * @param {Function} c The constructor to check for
 * @type {Boolean}
 **/
Wbd.Lang.isType = function(obj,constructor) {
	return (typeof obj == 'object') && (obj.constructor == constructor);
}

/**
 * Check if a given object is a string.
 * @param {Unknown} obj
 * @type {Boolean}
 **/
Wbd.Lang.isString = function(obj) {
	return typeof obj == 'string' || Wbd.Lang.isType(obj,String);
}

/**
 * Listen for a change to a given property of a given object.
 * @param {Object} obj
 * @param {String} prop The name of the property to listen for a change on
 * @param {Function} callback The function to call with the updated value
 * when it changes
 * @param {Integer} intvl The polling interval in milliseconds.  Defaults to
 * 500.
 * @return An opaque value to pass to Wbd.Lang.stopListen 
 **/
Wbd.Lang.listen = function(obj,prop,callback,intvl) {
	
	var new_value = obj[prop];
	
	var poll = function() {
		
		var old_value = new_value;
		new_value = obj[prop];
		
		if (old_value != new_value) {
			callback.call(callback,new_value);
		}
		
	}
	
	return window.setInterval(poll,intvl ? intvl : 500);
	
}

/**
 * Walk a composite structure (object or array) and apply a callback to
 * each element.  If the structure is an array, the callback will be 
 * applied to each of the array's elements.  If the structure is an
 * object, the callback will be applied to each property:value pair, 
 * so, the callback will receive two arguments on each invocation.
 * @param {Array|Object} struct
 * @param {Function} callback
 * @throws string exception if struct is not walkable (non array or object)
 **/
Wbd.Lang.walk = function(struct,callback) {
	
	if (typeof struct != 'object') {
		Wbd.error('cannot walk struct without typeof "object"; type is: ' + typeof struct);
	}
	
	try {
		
		// is array? if not, treat with "object" case

		if (struct.constructor == Array) {

			for (var i=0; i<struct.length; i++) {
				callback.call(callback,struct[i]);
			}

		} else {

			for (var p in struct) {
				callback.call(callback,p,struct[p]);
			}

		}
				
	} catch (e) { 
		if (e != Wbd.Lang.walk.STOP_INSTRUCTION) {
			//throw e;
		}
	}
	
}

/**
 * Function to call when you want to stop traversal inside a 
 * Wbd.Lang.walk callback.  Similar to "break" in a procedural loop.
 **/
Wbd.Lang.walk.stop = function() {
	throw Wbd.Lang.walk.STOP_INSTRUCTION;
}
Wbd.Lang.walk.STOP_INSTRUCTION = 'stop';

/**
 * Stop listening on something setup via Wbd.Lang.Listen.
 * @param {Opaque} listen_id
 **/
Wbd.Lang.stopListen = function(listen_id) {
	window.clearInterval(listen_id);
}
