/* $Revision: 529 $ | $CreationDate: 2007-09-18 14:51:15 +0100 (Tue, 18 Sep 2007) $ | $LastChangedDate: 2010-04-26 14:51:14 +0000 (Mon, 26 Apr 2010) $ | $LastChangedBy: andrewgillard $ */

/**
 * We use a "CBSGUI" "namespace" here to avoid our functions being overwritten by, or
 *  interfering with, other JS libraries that might be loaded.
 *
 * @todo IE6 totally fails to render the transparent PNG properly, and instead
 *        obscures the entire page except for the dialog.  Not a huge problem,
 *        but it looks a little ugly.
 */
var CBSGUI = {

	/*
	makeDraggable : function(dragarea, moveele, useMargins)
	positionDimmer : function(ev, sur)
	doShow : function()
	doHide : function()
    showDimmer : function()
    hideDimmer : function()
    showLoadingDialog : function()
    hideLoadingDialog : function()
	showDialog : function()
	addDialog : function(msg, callback, title, html, btns, extras, focusid, highpriority, width, loadfunc)
	showAlert : function(msg, callback, title, html, btntext, extras, highpriority, width, loadfunc)
	showPrompt : function(msg, callback, title, def, html, okbtntext, cnclbtntext, extras, highpriority, width, loadfunc)
	showConfirm : function(msg, callback, title, def, html, okbtntext, cnclbtntext, extras, highpriority, width, loadfunc)
	newTooltip : function(hoverel, message, title, msgishtml, xoffset, yoffset, width, height)
	newTooltipOnLoad : function(hoverel, message, title, msgishtml, xoffset, yoffset, width, height)
	setBoxStateCookie : function(box, openState)
	collapseBox : function(box, button)
	expandBox : function(box, button)
	toggleBoxState : function(button)
	addAutoComplete : function(fieldid, fieldname, acid, maxtodisplay, submitonclick, delay, searchallwords, quotebeforesubmitting, otherOptions)
	glossaryTerm : function(el, term)
	setupGlossaryTerms : function(elementIds, terms)
	ajaxHighlight : function(element)
	*/

	/**
	 * An Array of Objects indicating dialogs that still need to be displayed.
	 *
	 */
	DialogQueue : new Array(),

	/**
	 * The <div id="cbsalertsurround"> that dialogs are displayed in.
	 *
	 */
	Surround : null,

	/**
	 * The <div id="cbsalertbox"> of dialogs.
	 *
	 */
	AlertBox : null,

	/**
	 * Incremented every time we add a window.onkeypress event to keep the event names unique
	 *
	 */
	AddID : 1,

	/**
	 * Incremented every time we remove a window.onkeypress event so that we always remove the right event
	 *
	 */
	RemoveID : 1,

	/**
	 * Glossary terms that we have already loaded the content of.  The object's
	 *  member names indicate the terms, and the member value will either be a
	 *  string containing the term's explanation, or TRUE, indicating that the
	 *  content is still loading and shouldn't be re-requested.
	 *
	 */
	glossaryTerms : new Object(),

   /**
	*Whether pop-up is set for dragging.
	*
	*/
	popDrag : false,

    /**
     * Incremented every time a task requests the dimmer layer to be shown,
     * and decremented every time the task finishes.
     * The dimmer layer will be visible all the time this reference count is greater than zero.
     *
     */
    dimmerRefCount : 0,

    getDimmerLayer : function() {
        if(CBSGUI.multiplePopup){
            return CBS.getEl('dimmerlayeradmin');
        } else {
            return CBS.getEl('dimmerlayer');
        }
    },

	/**
	 * Makes the supplied 'moveele' movable by dragging on the 'dragarea'.
	 * The 'moveele's top/left styles will be changed, unless useMargins is true,
	 *  in which case marginTop and marginLeft will be used instead.
	 *
	 * @param element|string Area the user can drag the element with
	 * @param element|string Element that actually moves
	 * @param boolean Whether to use margin(Top|Left) instead of top|left
	 */
	makeDraggable : function(dragarea, moveele, useMargins) {
		if (typeof dragarea == 'string') dragarea = CBS.getEl(dragarea);
		if (typeof moveele == 'string') moveele = CBS.getEl(moveele);

		CBS.addEvent(dragarea, 'mousedown', function(ev) {
				CBSGUI.popDrag = true;
				var cd = CBS.getClientDimensions();

				initCoords = CBS.getMouseCoords(ev);
				if (useMargins) {
					var initLeft = moveele.style.marginLeft;
					var initTop = moveele.style.marginTop;
				} else {
					var initLeft = moveele.style.left;
					var initTop = moveele.style.top;
				}
				if (initLeft.substring(initLeft.length-2)=='px') initLeft = initLeft.substring(0, initLeft.length-2);
				if (initTop.substring(initTop.length-2)=='px') initTop = initTop.substring(0, initTop.length-2);
				initLeft *= 1; //Make it a number rather than a string
				initTop *= 1;

				function doPosition(ev) {
					var newCoords = CBS.getMouseCoords(ev);
					var newX = initLeft + (newCoords.x - initCoords.x);
					var newY = initTop + (newCoords.y - initCoords.y);

					//Ensure we can't/don't position it outside of the client area
					if (newX + moveele.offsetWidth > CBS.getBody().offsetWidth) newX = CBS.getBody().offsetWidth - moveele.offsetWidth;
					if (newX < 0) newX = 0;

					if (newY + moveele.offsetHeight > cd.height) newY = cd.height - moveele.offsetHeight;
					if (newY < 0) newY = 0;

					if (useMargins) {
						moveele.style.marginLeft = newX + 'px';
						moveele.style.marginTop = newY + 'px';
					} else {
						moveele.style.left = newX + 'px';
						moveele.style.top = newY + 'px';
					}
				}

				function endDrag(ev) {
					CBSGUI.popDrag = false;
					doPosition(ev);
					CBS.removeEvent(CBS.getBody(), 'mousemove', doPosition);
					CBS.removeEvent(CBS.getBody(), 'mouseup', endDrag);
				}

				CBS.addEvent(CBS.getBody(), 'mousemove', doPosition);
				CBS.addEvent(CBS.getBody(), 'mouseup', endDrag);

				try{ev.preventDefault();}catch(e){return false;}
			});
		CBS.addEvent(window, 'resize', function(ev) {
				//Window's resized so we might now be outside of the client area...
				var cd = CBS.getClientDimensions();

				if (moveele.offsetLeft + moveele.offsetWidth > CBS.getBody().offsetWidth) {
					var newLeft = CBS.getBody().offsetWidth - moveele.offsetWidth + 'px';
					if (useMargins)
						moveele.style.marginLeft = newLeft;
					else
						moveele.style.left = newLeft;
				}

				if (moveele.offsetTop + moveele.offsetHeight > cd.height) {
					var newTop = cd.height - moveele.offsetHeight + 'px';
					if (useMargins)
						moveele.style.marginTop = newTop;
					else
						moveele.style.top = newTop;
				}
			});
	},

	highestZIndex : 1,

    /**
	 * Repositions and resizes the dimmerlayer and alert box divs.
	 * Can be called when the window is resized or scrolled, and also when
	 *  the dialog is initially created.
	 *
	 */
	positionDimmer : function(ev, sur) {
		var d = CBSGUI.getDimmerLayer();
		if (!d) return;

		// Either an alert box, or the loading spinner may be visible
        var ab = CBSGUI.AlertBox;
        var spinner = CBS.getEl('dimmerlayerspinner');

		var cd = CBS.getClientDimensions();
		var cso = CBS.getClientScrollOffset();
		var surWidth = cd.width;
        var docSize = CBS.getDocumentDimensions();
		var bodyWidth = docSize.width;
		var bodyHeight = docSize.height;
        
        //The document width isn't always as wide as the actual page body
        if (document.body.offsetWidth > bodyWidth) bodyWidth = document.body.offsetWidth;
        if (document.body.offsetHeight > bodyHeight) bodyHeight = document.body.offsetHeight;

        if(!CBSGUI.multiplePopup){
            d.style.width = bodyWidth + 'px';
            d.style.height = bodyHeight + 'px';
        }

        //Jump out if our alert box is larger than the screen (in which case we don't want to move it when we scroll)
        if (ab && (ab.offsetWidth > cd.width || ab.offsetHeight > cd.height)) {
            //But first make sure that the dimmer is positioned sensibly
            d.style.top = '0px';
            d.style.left = '0px';
            return;
        }

		d.style.top = cso.y + 'px';
		d.style.left = cso.x + 'px';

		// Position the dialog, if there is one
		if (ab) {
			ab.style.position = 'absolute';
			ab.style.zIndex = CBSGUI.highestZIndex;

			//To bring the pop-up to the front on clicking.
			var makeZIndexHigh = function(){
				ab.style.zIndex = CBSGUI.highestZIndex++;
			}
			CBS.addEvent(ab, 'mousedown', makeZIndexHigh);

			if (ev === undefined && sur == undefined) {
				//Initial positioning
				var af = CBS.getEl('cbsalertform'+CBSGUI.multipleNum);

				af.style.height = (((af.firstChild.offsetHeight - CBS.getEl('cbsalerttitle'+CBSGUI.multipleNum).offsetHeight - 3) + 40) + 'px');
				var mT = Math.round((cd.height - ab.offsetHeight) / 2);
				if (mT < 0) mT = 0;
				ab.style.marginTop = mT + 'px';

				var mL = Math.round((cd.width - ab.offsetWidth) / 2);
				if (mL < 0) mL = 0;
				ab.style.marginLeft = mL + 'px';
			} else {
				if (ab.offsetTop < 0) ab.style.marginTop = 0 + 'px';
			}
		}

		// Position the loading spinner
        spinner.style.marginTop = ((cd.height - spinner.offsetHeight) / 2) + 'px';
	},

	/**
	 * Whether we have attached the positionDimmer() method to the window's
	 *  onresize and onscroll events.
	 *
	 */
	attachedPositionEvent : false,

	/**
	 * Whether to allow multiple pop-ups.
	 *
	 */
	multiplePopup : false,

	/**
	 * Number of multiple pop-ups.
	 *
	 */
	multipleNum : 0,

	/**
	 * Ajax request failed or not.
	 *
	 */
	isAjaxFailed : false,

    getZIndexOfHighestPopup : function(visibleOnly) {
        visibleOnly = !!visibleOnly; //default false

        var max = 0;
        var d = CBS.getEl('dimmerlayeradmin');
        for (var i=0; i<d.childNodes.length; i++) {
            var s = d.childNodes[i];
            var ab = s.childNodes[0];
            if (visibleOnly && s.style.display == 'none') continue;
            if (ab.style.zIndex > max) max = ab.style.zIndex;
        }
        return max;
    },

	/**
	 * Creates, then displays, a new dialog from the first Object found in the
	 *  DialogQueue if the call is from the admin section.
	 *
	 */
	doShowAdmin : function() {

		var classSuffix = 'admin';

		CBSGUI.multipleNum++;

		/**If there's a dialog in the queue, get its details**/
		if (CBSGUI.DialogQueue.length > 0) {
			var details = CBSGUI.DialogQueue.shift();
		} else return; /*Otherwise exit this function*/

		try {
				// Display the dimmer layer
		        CBSGUI.showDimmer();

		        var currentId;

				/*<div id="cbsalertsurround style="width:...px;height:...px;">*/
				var s = CBS.cE('div');
				s.id = 'cbsalertsurround'+CBSGUI.multipleNum;
				if(CBSGUI.multiplePopup){
					s.className = 'cbsdialogbtns'+classSuffix;
					currentId = ACPMisc.footerPopupId;
				}else{
					s.className = 'cbsalertsurroundadmin';
					currentId = false;
				}

				/*<div id="cbsalertbox" style="width:...px;height:...px;margin-top:...px;margin-left:...px;">*/
				var ab = CBS.cE('div');
					ab.id = 'cbsalertbox'+CBSGUI.multipleNum;
					ab.className = 'cbsalertbox'+classSuffix;
					if (details.width) {
						ab.style.width = details.width + 'px';
					} else {
						ab.style.width = '350px';
					}
                    ab.style.zIndex = CBSGUI.highestZIndex++;

					/**div carrying the title and minimize/close buttons.**/
					var titleDiv = CBS.cE('div');
					titleDiv.id = 'cbsalerttitle' + CBSGUI.multipleNum;
					titleDiv.className = 'cbsalertboxtitle'+classSuffix;

					/**div carrying the minimize/close buttons.**/
					if(CBSGUI.multiplePopup){
						var titleDivRight = CBS.cE('div');
						titleDivRight.id = 'cbsalertboxtitleright'+CBSGUI.multipleNum;
						titleDivRight.className = 'titlePopupRight';
						titleDiv.appendChild(titleDivRight);
					}

					/**div carrying the title.**/
					var titleDivLeft = CBS.cE('div');
					titleDivLeft.className = 'titlePopupLeft';
					if (!details.title) details.title = '[JavaScript Dialog]';
					titleDivLeft.appendChild(CBS.cTN(details.title));
					titleDiv.appendChild(titleDivLeft);

					var currentNum = CBSGUI.multipleNum;

					/**Define the button callback function**/
					var cb = function(ev) {
						/*Determine the target element of this event.*/
						var target = (ev.target)?ev.currentTarget:ev.srcElement;
						var targetbtn = target;

						/*Then get its parent form*/
						while (target.tagName.toLowerCase() != 'form' && target.parentNode) {
							target = target.parentNode;
						}

						/*Get any extras it might have attached to it.*/
						var extras = target.CBSGUIExtras;

						/**This is an unused feature, but it allows the callback to return
						false to keep the dialog displayed.
						Could be used to validate input from "prompt()" calls.**/

						if (details.callback(target, targetbtn, extras) !== false) {
							CBSGUI.doHide(CBS.getEl('cbsalertsurround'+currentNum), true);
						}

						/*Prevent the form submitting, since that would be Bad.*/
						try {ev.preventDefault();} catch (ex) {return false;}
					};


					/*If multiple pop-up is allowed, then show the minimize and close buttons.*/
					if (CBSGUI.multiplePopup) {
						var closebtnForm = CBS.cE('form');

						var minTitle = ACPMisc.minTitleToDisplay;
						var currentImg = ACPMisc.imgForTitle;

                        var restoreFunc = function(ev) {
                            var evButton = CBS.getEventTarget(ev);
                            if (evButton.CBSGUIfooterBtn) evButton = evButton.CBSGUIfooterBtn;
                            var evBox = evButton.CBSGUIpopupBox;
                            evBox.style.display = 'block';
                            evButton.className = 'footerPopupWidgets';
                        }
                        var minimiseFunc = function(ev) {
                            var evButton = CBS.getEventTarget(ev);
                            if (evButton.CBSGUIfooterBtn) evButton = evButton.CBSGUIfooterBtn;
                            var evBox = evButton.CBSGUIpopupBox;
                            evBox.style.display = 'none';
                            evButton.className = 'footerPopupWidgetsMin';
                        }
                        var footerClickFunc = function(ev) {
                            var evButton = CBS.getEventTarget(ev);
                            if (evButton.CBSGUIfooterBtn) evButton = evButton.CBSGUIfooterBtn;
                            var evBox = evButton.CBSGUIpopupBox;
                            var maxZIndex = parseInt(CBSGUI.getZIndexOfHighestPopup(true));
                            var ab = evBox.childNodes[0];
                            console.log("highest z index: ", maxZIndex);
                            console.log("current z index: ", ab.style.zIndex);
                            if (evButton.className == 'footerPopupWidgetsMin') {
                                restoreFunc(ev);
                            } else if (ab.style.zIndex == maxZIndex) {
                                minimiseFunc(ev);
                            } else {
                                CBSGUI.highestZIndex = maxZIndex + 1;
                                ab.style.zIndex = CBSGUI.highestZIndex;
                            }
                        }

						/*Show the title in the footer.*/
						if(!CBS.getEl(currentId)){
                            var footerDiv = CBS.cE('div');
                                footerDiv.id = currentId;
                                footerDiv.className = 'footerPopupWidgets';
                                var footerDivText = CBS.cE('span');
                                    footerDivText.appendChild(CBS.cTN(minTitle));
                                    footerDivText.style.position = 'relative';
                                    footerDivText.style.top = '3px';
                                    footerDivText.CBSGUIfooterBtn = footerDiv;
                                footerDiv.appendChild(footerDivText);
                                footerDiv.CBSGUIpopupBox = s;
                                CBS.addEvent(footerDiv, 'click', footerClickFunc);
                            CBS.getEl('minpopuptitles').appendChild(footerDiv);
						}

						/**Create the minimize button**/
						var minbtn = CBS.cE('a');
						    minbtn.className = 'boxMin';
						    minbtn.setAttribute('href', '#');
                            if (footerDiv)
                                minbtn.CBSGUIfooterBtn = footerDiv;
						    CBS.addEvent(minbtn, 'click', minimiseFunc);
						closebtnForm.appendChild(minbtn);

						/**Create the close button**/
						var closebtn = CBS.cE('a');
						    closebtn.className = 'boxClose';
						    closebtn.setAttribute('href', '#');
						    CBS.addEvent(closebtn, 'click', cb);

						    /**Remove the title from the footer.**/
						    CBS.addEvent(closebtn, 'click', function (){ACPMisc.closeDivInFooter(currentId)});
						closebtnForm.appendChild(closebtn);

						titleDivRight.appendChild(closebtnForm);
					}

					ab.appendChild(titleDiv);

					var contentDiv = CBS.cE('div');
					contentDiv.id = 'cbsalertcontent'+CBSGUI.multipleNum;
					contentDiv.className = 'cbsalertcontentadmin';

					//<form action="" method="get" id="cbsalertform">
					var fm = CBS.cE('form');
						fm.setAttribute('action', '');
						fm.setAttribute('method', 'get');
						fm.style.overflow = 'auto';

						fm.id = 'cbsalertform'+CBSGUI.multipleNum;
						if (details.extras) fm.CBSGUIExtras = details.extras;

						//<fieldset class="invisfieldset">
						var fs = CBS.cE('fieldset');
							fs.className = 'invisfieldset';
							if (details.title == 'Social Bookmark This Page') fs.style.height='351px';

							//<div id="cbsalertmsg">
							var am = CBS.cE('div');
								am.id = 'cbsalertmsg'+CBSGUI.multipleNum;
								am.style.paddingLeft='5px';
								am.style.paddingRight='5px';

								if (typeof details.msg == 'object') {
									am.appendChild(details.msg);
								} else {
									if (details.html) {
										try {
											am.innerHTML = details.msg;
										} catch (ex) {
											console.log("An unvalid innerHTML string was supplied for this dialog.");
										}
									} else {
										var amp = CBS.cE('p');
											var lines = details.msg.split("\n");
											for (var i=0, l=lines.length; i<l; i++) {
												amp.appendChild(CBS.cTN(lines[i]));
												amp.appendChild(CBS.cE('br'));
											}

	//										amp.appendChild(CBS.cTN(details.msg));
										am.appendChild(amp);
									}
								}
							//</div>
							fs.appendChild(am);

							//<div class="cbsdialogbtns">
							var db = CBS.cE('div');
								db.className = 'cbsdialogbtns'+classSuffix;

								var cancelbtn;
								//Add each required button to the form
								for (var i=0,l=details.btns.length; i<l; i++) {

									//<input type="button" id="..." onclick="..." />
									var b = CBS.cE('input');
									if (details.btns[i].id) b.id = details.btns[i].id;
									if (details.btns[i].def) {
										b.setAttribute('type', 'submit');
										//Define a callback function for the form itself, if
										// this button is the default.
										fm.CBSGUIDefBtn = b;
										CBS.addEvent(fm, 'submit', function() { fm.CBSGUIDefBtn.click(); });
									} else {
										b.setAttribute('type', 'button');
									}
									if (details.btns[i].cancel) {
										cancelbtn = b;
									}
									b.value = details.btns[i].text;
									CBS.addEvent(b, 'click', cb);

									//Remove the title from the footer.
									if(CBSGUI.multiplePopup){
										CBS.addEvent(b, 'click', function (){ACPMisc.closeDivInFooter(currentId)});
									}

									db.appendChild(b);
								}
							//</div class="cbsdialogbtns">

							//Add the default buttons.
							if (CBSGUI.isalert && !CBSGUI.multiplePopup) {
								fs.appendChild(db);
							} else if(CBSGUI.multiplePopup && !CBSGUI.isalert) {
								fs.appendChild(db);
							} else if (CBSGUI.isAjaxFailed) {
								fs.appendChild(db);
							}
						//</fieldset>
						fm.appendChild(fs);
					//</form>
					contentDiv.appendChild(fm);
					ab.appendChild(contentDiv);
				//</div id="cbsalertbox">
				s.appendChild(ab);
				CBSGUI.Surround = s;
				CBSGUI.AlertBox = ab;
				if (!CBSGUI.AttachedPositionEvent) {
					CBS.addEvent(window, 'resize', CBSGUI.positionDimmer);
					CBS.addEvent(window, 'scroll', CBSGUI.positionDimmer);
					CBSGUI.AttchedPositionEvent = true;
				}
				if (details.title) CBSGUI.makeDraggable(titleDiv, ab, true);
		} catch (e) {
		    CBSGUI.Surround = null;
			console.log("Unable to generate dialog. [Error #2 - ", e, "]");
			return;
		}

		var bdy = CBS.getBody();
		try {
			CBS.hideSelects();

            var dimmerLayer = CBSGUI.getDimmerLayer();
            dimmerLayer.appendChild(s);

			CBSGUI.positionDimmer();

			var listenForEscFunc = function(e){
					var keyCode = (e ? e.keyCode : window.event.keyCode);
					if(keyCode == 27) { //esc
						if (cancelbtn) {
							cancelbtn.click();
						} else {
							CBSGUI.doHide(null, true);
						}
						CBS.removeEventByName(document, 'keydown', 'CBSGUI_'+CBSGUI.AddID);
					}
				};
			CBS.addEvent(document, 'keydown', listenForEscFunc, 'CBSGUI_'+CBSGUI.AddID);
			CBSGUI.AddID++;
		} catch (e) {
			console.log("Unable to display dialog. [Error #1 - ", e, "]");
			return;
		}
		if (details.focusid && CBS.getEl(details.focusid)) CBS.getEl(details.focusid).focus();

		//Now that we've finished, we can call our loadfunc for this dialog
		// if one was specified
		if (details.loadfunc) {
			details.loadfunc();
		}

	},

	/**
	 * Hides the currently shown dialog, then displays the next if another is waiting
	 *
	 */
	doHide : function(target, acp) {
        if (!acp) target = CBSGUI.Surround;

		if (target) {
			//Only hide it if it's actually showing...
			//Remove all children
			CBS.removeChildren(target);

            if (acp)
			    target.parentNode.removeChild(target);
            
			CBS.showSelects();

            if (!acp)
			    CBSGUI.Surround = null;

			CBS.removeEventByName(window, 'keypress', 'CBSGUI_'+CBSGUI.RemoveID);
			CBSGUI.RemoveID++;

			// Hide the dimmer layer
			CBSGUI.hideDimmer();
		}

		//Then show the next dialog if we need to
		CBSGUI.showDialog();
	},

	/**
	 * Creates, then displays, a new dialog from the first Object found in the
	 *  DialogQueue if the call is from the uiser section.
	 *
	 */
	doShow : function() {

		//Multiple pop-ups is not required.
		CBSGUI.multipleNum = 0;

		//Just to make sure that we don't already have one visible...
		if (CBSGUI.Surround) CBSGUI.doHide();

		//If there's a dialog in the queue, get its details
		if (CBSGUI.DialogQueue.length > 0) {
			var details = CBSGUI.DialogQueue.shift();
		} else return; //Otherwise exit this function

		try {
		    // Display the dimmer layer
		    CBSGUI.showDimmer();

			//<div id="cbsalertsurround style="width:...px;height:...px;">
			var s = CBS.getEl('cbsalertsurround');

				//<div id="cbsalertbox" style="width:...px;height:...px;margin-top:...px;margin-left:...px;">
				var ab = CBS.cE('div');
					ab.id = 'cbsalertbox';
					ab.style.left = 0 + "px";
					if (details.width) {
						ab.style.width = details.width + 'px';
					} else {
						ab.style.width = '350px';
					}

					if (!details.title) details.title = '[JavaScript Dialog]';
					//<h1>
					var h1 = CBS.cE('h1');
						h1.id = 'cbsalerttitle' + CBSGUI.multipleNum;
						h1.appendChild(CBS.cTN(details.title));
						try { h1.onselectstart = function(){return false;}; } catch (ex) {}
					//</h1>
					ab.appendChild(h1);

					//<form action="" method="get" id="cbsalertform">
					var fm = CBS.cE('form');
						fm.setAttribute('action', '');
						fm.setAttribute('method', 'get');
						fm.style.overflow = 'auto';
						fm.id = 'cbsalertform' + CBSGUI.multipleNum;
						if (details.extras) fm.CBSGUIExtras = details.extras;

						//<fieldset class="invisfieldset">
						var fs = CBS.cE('fieldset');
							fs.className = 'invisfieldset';
							if (details.title == 'Social Bookmark This Page') fs.style.height='351px';

							//<div id="cbsalertmsg">
							var am = CBS.cE('div');
								am.id = 'cbsalertmsg';
								am.style.paddingLeft='5px';
								am.style.paddingRight='5px';

								if (typeof details.msg == 'object') {
									am.appendChild(details.msg);
								} else {
									if (details.html) {
										try {
											am.innerHTML = details.msg;
										} catch (ex) {
											console.log("An invalid innerHTML string was supplied for this dialog.");
										}
									} else {
										var amp = CBS.cE('p');
											var lines = details.msg.split("\n");
											for (var i=0, l=lines.length; i<l; i++) {
												amp.appendChild(CBS.cTN(lines[i]));
												amp.appendChild(CBS.cE('br'));
											}

	//										amp.appendChild(CBS.cTN(details.msg));
										am.appendChild(amp);
									}
								}
							//</div>
							fs.appendChild(am);

							//<div class="cbsdialogbtns">
							var db = CBS.cE('div');
								db.className = 'cbsdialogbtns';

								//Define the button callback function
								var cb = function(ev) {
										//Determine the target element of this event
//										var target = (ev.target)?ev.target:ev.srcElement;
										var target = (ev.target)?ev.currentTarget:ev.srcElement;
										var targetbtn = target;

										//Then get its parent form
										while (target.tagName.toLowerCase() != 'form' && target.parentNode) {
											target = target.parentNode;
										}

										//Get any extras it might have attached to it
										var extras = target.CBSGUIExtras;

										//This is an unused feature, but it allows the callback to return
										// false to keep the dialog displayed.
										//Could be used to validate input from "prompt()" calls.
										if (details.callback(target, targetbtn, extras) !== false) {
											CBSGUI.doHide();
										}

										//Prevent the form submitting, since that would be Bad
										try {ev.preventDefault();} catch (ex) {return false;}
									};

								var cancelbtn;
								//Add each required button to the form
								for (var i=0,l=details.btns.length; i<l; i++) {
									//<input type="button" id="..." onclick="..." />
									var b = CBS.cE('input');
										if (details.btns[i].id) b.id = details.btns[i].id;
										if (details.btns[i].def) {
											b.setAttribute('type', 'submit');
											//Define a callback function for the form itself, if
											// this button is the default.
											fm.CBSGUIDefBtn = b;
											CBS.addEvent(fm, 'submit', function() { fm.CBSGUIDefBtn.click(); });
										} else {
											b.setAttribute('type', 'button');
										}
										if (details.btns[i].cancel) {
											cancelbtn = b;
										}
										b.value = details.btns[i].text;
										CBS.addEvent(b, 'click', cb);
									db.appendChild(b);
								}
							//</div class="cbsdialogbtns">
							fs.appendChild(db);
						//</fieldset>
						fm.appendChild(fs);
					//</form>
					ab.appendChild(fm);
				//</div id="cbsalertbox">
				s.appendChild(ab);
			//</div id="cbssurround">

			CBSGUI.Surround = s;
			CBSGUI.AlertBox = ab;
			if (!CBSGUI.AttachedPositionEvent) {
				CBS.addEvent(window, 'resize', CBSGUI.positionDimmer);
				CBS.addEvent(window, 'scroll', CBSGUI.positionDimmer);
				CBSGUI.AttchedPositionEvent = true;
			}
			if (details.title) CBSGUI.makeDraggable(h1, ab, true);
		} catch (e) {
			CBSGUI.Surround = null;
			console.log("Unable to generate dialog. [Error #2 - ", e, "]");
			return;
		}

		var bdy = CBS.getBody();
		try {
			CBS.hideSelects();

			CBSGUI.positionDimmer();

			var listenForEscFunc = function(e){
					var keyCode = (e ? e.keyCode : window.event.keyCode);
					if(keyCode == 27) { //esc
						if (cancelbtn) {
							cancelbtn.click();
						} else {
							CBSGUI.doHide();
						}
						CBS.removeEventByName(document, 'keydown', 'CBSGUI_'+CBSGUI.AddID);
					}
				};
			CBS.addEvent(document, 'keydown', listenForEscFunc, 'CBSGUI_'+CBSGUI.AddID);
			CBSGUI.AddID++;
		} catch (e) {
			console.log("Unable to display dialog. [Error #1 - ", e, "]");
			return;
		}
		if (details.focusid && CBS.getEl(details.focusid)) CBS.getEl(details.focusid).focus();

		//Now that we've finished, we can call our loadfunc for this dialog
		// if one was specified
		if (details.loadfunc) {
			details.loadfunc();
		}
	},


	showDimmerAdmin : function() {
		var d = CBS.getEl('dimmerlayer');
		if (!d) return;

		d.style.display = 'block';

        var spinner = CBS.getEl('dimmerlayerspinner');

		var cd = CBS.getClientDimensions();
		var cso = CBS.getClientScrollOffset();
		var surWidth = cd.width;
		var bodyWidth = CBS.getBody().offsetWidth;
		var bodyHeight = CBS.getBody().offsetHeight;

    	d.style.width = bodyWidth + 'px';
    	d.style.height = bodyHeight + 'px';
		d.style.top = cso.y + 'px';
		d.style.left = cso.x + 'px';

		// Position the loading spinner
		spinner.style.display = 'block';
        spinner.style.marginTop = ((cd.height - spinner.offsetHeight) / 2) + 'px';

	},
    /**
     * Displays a grey background to prevent user interaction with the page. Uses a reference counter
	 * to make sure the dimmer is visible until all requestors have finished with it.
     *
     */
    showDimmer : function() {
        if (++CBSGUI.dimmerRefCount == 1) {
            //We've just incremented from zero, so we need to actually show the dialog...
            try {
                var d = CBSGUI.getDimmerLayer();
                d.style.display = 'block';

                if (!CBSGUI.attachedPositionEvent) {
                    CBS.addEvent(window, 'resize', CBSGUI.positionDimmer);
                    CBS.addEvent(window, 'scroll', CBSGUI.positionDimmer);
                    CBSGUI.attachedPositionEvent = true;
                }
                CBSGUI.positionDimmer();
            } catch (e) {}
        }
    },

    hideDimmerAdmin : function() {
        try {
            CBS.getEl('dimmerlayerspinner').style.display = 'none';
            CBS.getEl('dimmerlayer').style.display = 'none';
        } catch (e) {}
    },

    /**
     * Hides the grey backgound provided all requestors have finished with it.
     *
     */
    hideDimmer : function() {
        if (--CBSGUI.dimmerRefCount == 0) {
            //We've just decremented to zero, so we need to hide it...
            try {
                var d = CBSGUI.getDimmerLayer();
                d.style.display = 'none';
            } catch (e) {}
        }
    },

    /**
     * Shows the Ajax progress dialog, and increments a counter to ensure that
     *  the dialog isn't hidden again until all requests have finished
     *
     */
    showLoadingDialog : function() {
        if (++CBSAjax.requestsShowingProgress == 1) {
            CBSGUI.showDimmer();

            if(CBSGUI.acp){
                CBSGUI.showDimmerAdmin();
            }

            // Cause the "loading spinner" to be visible
            try {
                 var s = CBS.getEl('dimmerlayerspinner');
                 s.style.display = 'inline';
            } catch (e) {}
        }
    },

    /**
     * Hides the Ajax progress dialog, and decrements a counter to ensure that
     *  the dialog isn't hidden until all requests have finished
     *
     */
    hideLoadingDialog : function() {
        if (--CBSAjax.requestsShowingProgress == 0) {
            //We've just decremented to zero, so we need to hide it...
            try {
                var d = CBS.getEl('dimmerlayerspinner');
                d.style.display = 'none';
            } catch (e) {}

            CBSGUI.hideDimmer();

            if(CBSGUI.acp){
                CBSGUI.hideDimmerAdmin();
            }

        }
    },

	/**
	 * Shows the next dialog if there isn't one already displayed
	 *
	 */
	showDialog : function() {
		if (CBSGUI.DialogQueue.length <= 0) return;

		if(CBSGUI.acp){ //Call from admin section.
			CBSGUI.doShowAdmin();
		}else{ //Call from user section.
            if (!CBSGUI.Surround) {
			    CBSGUI.doShow();
            }
		}
	},

	/**
	 * Adds a new (generic) dialog to the display queue.
	 * All parameters are required except extras and focusid.
	 *
	 * @param string The message to display.  Can be plain text or HTML (if the html param is true).
	 * @param function the function to call when this dialog is closed.
	 * @param string The dialog title.  Always plain text.
	 * @param boolean Whether the dialog message is HTML or not.  If so, the msg param must include
	 *                 relevant tags to display in a <div>.
	 * @param Array An Array of Objects used to specify the buttons that will be displayed in this dialog.
	 * @param Object An object that gets passed back to the callback function untouched.  Can be used
	 *                to help identify dialogs.
	 * @param string The ID of an element in the dialog to focus when it loads.
	 * @param boolean If true, this dialog is inserted at the beginning of the queue, instead of the end.
	 *                 It still won't override a currently visible dialog, though.
	 * @param integer If specified, this is used as the width of the dialog, otherwise a width of
	 *                    350px is used
	 * @param function a function to call when the dialog is displayed
	 */
	addDialog : function(msg, callback, title, html, btns, extras, focusid, highpriority, width, loadfunc) {
		var newElement = {
				'title': title,
				'msg': msg,
				'html': html,
				'btns': btns,
				'callback': callback,
				'extras': extras,
				'focusid': focusid,
				'width': width,
				'loadfunc': loadfunc
			};
		if (highpriority) {
			//Add to beginning of array
			CBSGUI.DialogQueue.unshift(newElement);
		} else {
			//Add to end of array
			CBSGUI.DialogQueue.push(newElement);
		}
		CBSGUI.DialogQueue = new Array(newElement);

		CBSGUI.showDialog();
	},

	isalert : false,

	/**
	 * Adds a new alert box to the display queue.
	 * All paramters have defaults except msg.
	 *
	 * @param string The message to display.  Can be plain text or HTML (if the html param is true).
	 * @param function the function to call when this dialog is closed.
	 * @param string The dialog title.  Always plain text.
	 * @param boolean Whether the dialog message is HTML or not.  If so, the msg param must include
	 *                 relevant tags to display in a <div>.
	 * @param string The text to display on the "OK" button instead of the default "OK".
	 * @param Object An object that gets passed back to the callback function untouched.  Can be used
	 *                to help identify dialogs.
	 * @param boolean If true, this dialog is inserted at the beginning of the queue, instead of the end.
	 *                 It still won't override a currently visible dialog, though.
	 * @param integer If specified, this is used as the width of the dialog, otherwise a width of
	 *                    350px is used
	 * @param function a function to call when the dialog is displayed
	 * @param boolean If true, allow multiple pop-ups.
	 */
	showAlert : function(msg, callback, title, html, btntext, extras, highpriority, width, loadfunc, isMultiplePopUps) {
		CBSGUI.isalert = true;

		if(isMultiplePopUps){
			CBSGUI.multiplePopup = true;
		}else{
			CBSGUI.multiplePopup = false;
		}

		try {
			if (!title) title = 'JavaScript Message';
			var cb = function (form, targetbtn,extras) {
							if (!callback) return;
							return callback(extras);
						};
			html = !!html;
			var btns = new Array(
									{
										'def': true,
										'cancel': true,
										'id': 'cbsalertokbtn'+CBSGUI.multipleNum,
										'text': (btntext ? btntext : 'OK')
									}
								);
			highpriority = !!highpriority;

			CBSGUI.addDialog(msg, cb, title, html, btns, extras, 'cbsalertokbtn'+CBSGUI.multipleNum, highpriority, width, loadfunc);
		} catch (ex) {
			alert(msg);
			callback(extras);
		}
	},

	/**
	 * Adds a new prompt box to the display queue.
	 * All paramters have defaults except msg.
	 *
	 * @param string The message to display.  Can be plain text or HTML (if the html param is true).
	 * @param function the function to call when this dialog is closed.
	 * @param string The dialog title.  Always plain text.
	 * @param string The text to display in the prompt's input box by default.
	 * @param boolean Whether the dialog message is HTML or not.  If so, the msg param must include
	 *                 relevant tags to display in a <div>.
	 * @param string The text to display on the "OK" button instead of the default "OK".
	 * @param string The text to display on the "Cancel" button instead of the default "Cancel".
	 * @param Object An object that gets passed back to the callback function untouched.  Can be used
	 *                to help identify dialogs.
	 * @param boolean If true, this dialog is inserted at the beginning of the queue, instead of the end.
	 *                 It still won't override a currently visible dialog, though.
	 * @param integer If specified, this is used as the width of the dialog, otherwise a width of
	 *                    350px is used
	 * @param function a function to call when the dialog is displayed
	 * @param boolean If true, allow multiple pop-ups.
	 */
	showPrompt : function(msg, callback, title, def, html, okbtntext, cnclbtntext, extras, highpriority, width, loadfunc, isMultiplePopUps) {
		CBSGUI.isalert = false;

		if(isMultiplePopUps){
			CBSGUI.multiplePopup = true;
		}else{
			CBSGUI.multiplePopup = false;
		}

		try {
			if (!okbtntext) okbtntext = 'OK';
			if (!cnclbtntext) cnclbtntext = 'Cancel';
			if (!title) title = '[JavaScript Request]';
			var cb = function (form, targetbtn,extras) {
							if (!callback) return;
							if (targetbtn.value != cnclbtntext) {
								var ret = CBS.getEl('cbspromptbox').value;
							} else {
								var ret = null;
							}
							callback(ret, extras);
						};
			html = !!html;
			var btns = new Array(
									{
										'def': true,
										'text': okbtntext
									},
									{
										'def': false,
										'cancel': true,
										'text': cnclbtntext
									}
								);
			highpriority = !!highpriority;

			//We need to generate some HTML to display in the prompt dialog with the input box...
			var d = CBS.cE('div');
				if (typeof msg == 'object') {
					d.appendChild(msg);
				} else {
					if (html) {
						d.innerHTML = msg;
					} else {
						var p = CBS.cE('p');
							p.appendChild(CBS.cTN(msg));
						d.appendChild(p);
					}
				}
				var inp = CBS.cE('input');
					inp.id = 'cbspromptbox';
					inp.type = 'text';
					inp.className = 'cbsdialogtxt';
					if (def) inp.value = def;
				d.appendChild(inp);
			msg = d.innerHTML;
			d = null;

			CBSGUI.addDialog(msg, cb, title, true, btns, extras, 'cbspromptbox', highpriority, width, loadfunc);
		} catch (ex) {
			callback(prompt(msg, def), extras);
		}
	},

	/**
	 * Adds a new confirm box to the display queue.
	 * All paramters have defaults except msg.
	 *
	 * @param string The message to display.  Can be plain text or HTML (if the html param is true).
	 * @param function the function to call when this dialog is closed.
	 * @param string The dialog title.  Always plain text.
	 * @param boolean The defaultly selected button.  Use true for "OK" or false for "Cancel".
	 * @param boolean Whether the dialog message is HTML or not.  If so, the msg param must include
	 *                 relevant tags to display in a <div>.
	 * @param string The text to display on the "OK" button instead of the default "OK".
	 * @param string The text to display on the "Cancel" button instead of the default "Cancel".
	 * @param Object An object that gets passed back to the callback function untouched.  Can be used
	 *                to help identify dialogs.
	 * @param boolean If true, this dialog is inserted at the beginning of the queue, instead of the end.
	 *                 It still won't override a currently visible dialog, though.
	 * @param integer If specified, this is used as the width of the dialog, otherwise a width of
	 *                    350px is used
	 * @param function a function to call when the dialog is displayed
	 * @param boolean If true, allow multiple pop-ups.
	 */
	showConfirm : function(msg, callback, title, def, html, okbtntext, cnclbtntext, extras, highpriority, width, loadfunc, isMultiplePopUps) {
		CBSGUI.isalert = false;

		if(isMultiplePopUps){
			CBSGUI.multiplePopup = true;
		}else{
			CBSGUI.multiplePopup = false;
		}

		try {
			if (!okbtntext) okbtntext = 'OK';
			if (!cnclbtntext) cnclbtntext = 'Cancel';
			if (!title) title = '[JavaScript Confirmation]';
			var cb = function (form, targetbtn,extras) {
							if (!callback) return;
							var ret = (targetbtn.value == okbtntext) ? true : false;
							callback(ret, extras);
						};
			html = !!html;
			var btns = new Array(
									{
										'def': def,
										'id': 'cbsconfirmokbtn',
										'text': okbtntext
									},
									{
										'def': !def,
										'cancel': true,
										'id': 'cbsconfirmcnclbtn',
										'text': cnclbtntext
									}
								);
			highpriority = !!highpriority;

			CBSGUI.addDialog(msg, cb, title, html, btns, extras, (def ? 'cbsconfirmokbtn' : 'cbsconfirmcnclbtn'), highpriority, width, loadfunc);
		} catch (ex) {
			callback(confirm(msg), extras);
		}
	},

	/**
	 * Creates a new "tooltip" that appears when the mouse hovers over the element with ID
	 *  hoverel, and moves around as the mouse moves.
	 * All but the first two parameters are optional.
	 *
	 * @param string Hover element's ID
	 * @param string Message to display
	 * @param string Tooltip's title
	 * @param boolean Whether the message is HTML
	 * @param integer Number of pixels horizontally to offset the tooltip by
	 * @param integer Number of pixels vertically to offset the tooltip by
	 * @param integer Width of the tooltip (automatic if not supplied)
	 * @param integer Height of the tooltip (automatic if not supplied)
	 */
	newTooltip : function(hoverel, message, title, msgishtml, xoffset, yoffset, width, height) {
		var el = CBS.getEl(hoverel);
		if (!el) {
			console.log("Element with ID '", hoverel, "' doesn't exist.");
		}

		if (!el.title) el.title = ""; //Stop IE displaying elements' alt tags as titles...

		if (xoffset == null || xoffset == undefined) xoffset = TooltipDefaultXOffset;
		if (yoffset == null || yoffset == undefined) yoffset = TooltipDefaultYOffset;

		if (el.CBSToolTip) {
			el.CBSToolTip.parentNode.removeChild(el.CBSToolTip);
		}
		var div = CBS.cE('div');
			div.className = 'tooltip';
			div.style.position = 'absolute';
			div.style.top = '0px';
			div.style.left = '0px';
			div.style.visibility = 'hidden';
			if (width) div.style.width = width + 'px';
			if (height) div.style.height = height + 'px';

			if (title) {
				var h1 = CBS.cE('h1');
					h1.className = 'tooltiptitle';
					h1.appendChild(CBS.cTN(title));
				div.appendChild(h1);
			}

			var msgdiv = CBS.cE('div');
				msgdiv.className = 'tooltipmsg';
				if (msgishtml) {
					msgdiv.innerHTML = message;
				} else {
					var p = CBS.cE('p');
						p.className = 'tooltipmsgp';
						var lines = message.split("\n");
						for (var i=0, l=lines.length; i<l; i++) {
							p.appendChild(CBS.cTN(lines[i]));
							p.appendChild(CBS.cE('br'));
						}
					msgdiv.appendChild(p);
				}
			div.appendChild(msgdiv);
		CBS.getBody().appendChild(div);
		el.CBSToolTip = div;

		if (!width) {
			var cd = CBS.getClientDimensions();
			var maxWidth = Math.round(cd.width * 0.4);
			if (div.offsetWidth > maxWidth) {
				div.style.width = maxWidth + 'px';
			}
		}

		div.CBSToolTipOffsetWidth = div.offsetWidth;
		div.CBSToolTipOffsetHeight = div.offsetHeight;

		var interval;
		function checkTooltipReferences() {
			if (!el.parentNode) {
				//Parent must've been removed, so remove the tooltip, too
				hideTooltip();
			}
		}

		function hideTooltip() {
			clearInterval(interval);div.style.visibility='hidden';
		}

		CBS.addEvent(el, 'mouseover', function(){interval=setInterval(function(){checkTooltipReferences();},500);div.style.visibility='visible';});
		CBS.addEvent(el, 'mouseout', function(){hideTooltip();});
		CBS.addEvent(el, 'mousemove', function(ev){
			var coords = CBS.getMouseCoords(ev);
			div.style.top = coords.y + yoffset + 'px';
			div.style.left = coords.x + xoffset + 'px';

			var body = CBS.getBody();
			if (div.offsetLeft + div.CBSToolTipOffsetWidth > body.offsetWidth) {
				//-5 on the offset because it's on the other side of the mouse cursor now,
				// so doesn't have to avoid a huge cursor...
				div.style.left = (coords.x - (div.CBSToolTipOffsetWidth + xoffset - 5)) + 'px';
			}
			if (div.offsetTop + div.CBSToolTipOffsetHeight > body.offsetHeight) {
				div.style.top = (coords.y - (div.CBSToolTipOffsetHeight + yoffset)) + 'px';
			}
		});
	},

	/**
	 * Creates a new "tooltip" (when the page loads) that appears when the mouse hovers
	 *  over the element with ID hoverel, and moves around as the mouse moves.
	 * All but the first two parameters are optional.
	 *
	 * @param string Hover element's ID
	 * @param string Message to display
	 * @param string Tooltip's title
	 * @param boolean Whether the message is HTML
	 * @param integer Number of pixels horizontally to offset the tooltip by
	 * @param integer Number of pixels vertically to offset the tooltip by
	 * @param integer Width of the tooltip (automatic if not supplied)
	 * @param integer Height of the tooltip (automatic if not supplied)
	 */
	newTooltipOnLoad : function(hoverel, message, title, msgishtml, xoffset, yoffset, width, height) {
		CBS.addEvent(window, 'domload', function() {
			CBSGUI.newTooltip(hoverel, message, title, msgishtml, xoffset, yoffset, width, height);
		});
	},

	/**
	 * Stores the new state of the specified box in the cbs_boxstate cookie
	 *
	 * @param element Box
	 * @param boolean Open state
	 */
	setBoxStateCookie : function(box, openState) {
		/*
		123456789:1,987654321:0,456789123:0,654987321:1
		*/
		if (/box_([\-0-9]+)_box/.exec(box.id)) {
			var boxCode = RegExp.$1;
			var newStates;

			var curStates = CBS.getCookie('boxstate');
			if (curStates) {
				var statePos = curStates.indexOf(boxCode + ':');
				if (statePos == -1) {
					//Doesn't exist, so append it
					newStates = curStates + ',' + boxCode + ':';
					newStates += (openState ? '1' : '0');
				} else {
					var valPos = curStates.indexOf(':', statePos) + 1;
					newStates = curStates.substring(0, valPos);
					newStates += (openState ? '1' : '0');
					newStates += curStates.substring(valPos+1);
				}
			} else {
				newStates = boxCode + ':';
				newStates += (openState ? '1' : '0');
			}
			CBS.setCookie('boxstate', newStates, (60*60*24*365)); //Save it for a year
		}
	},

	/**
	 * Collapses the specified box, when the specified button is clicked
	 *
	 * @param element Box to collapse
	 * @param element Button that was clicked
	 */
	collapseBox : function(box, button) {
		box.style.display = 'none';
		CBSGUI.setBoxStateCookie(box, false);
		button.alt = button.alt.replace('-', '+');
		button.title = button.title.replace('Collapse', 'Expand');
		button.src = button.src.replace('box-collapse.', 'box-expand.');
	},

	/**
	 * Expands the specified box, when the specified button is clicked
	 *
	 * @param element Box to expand
	 * @param element Button that was clicked
	 */
	expandBox : function(box, button) {
		box.style.display = 'block';
		CBSGUI.setBoxStateCookie(box, true);
		button.alt = button.alt.replace('+', '-');
		button.title = button.title.replace('Expand', 'Collapse');
		button.src = button.src.replace('box-expand.', 'box-collapse.');
	},

	/**
	 * Toggles the collapsed/expanded state of the box associated with the specified button
	 *
	 * @param element Button that was clicked
	 */
	toggleBoxState : function(button) {
		var boxID = button.id + '_box';
		var box = CBS.getEl(boxID);
		try {
			if (box.style.display == 'none') {
				CBSGUI.expandBox(box, button);
			} else {
				CBSGUI.collapseBox(box, button);
			}
		} catch (e) {
			CBSGUI.collapseBox(box, button);
		}
	},

	/**
	 * Adds custom auto-completion functionality to field with the supplied ID.
	 * The "acid" (Auto-Complete ID) indicates what data should be retrieved for
	 *  the drop-down list.
	 * The "delay" is the number of ms to wait before showing/updating the list
	 *  when a character is typed.
	 *
	 * @param string ID of the field to add auto-complete functionality to
	 * @param string Name to rename the field to on the form submit
	 * @param string Auto-complete ID to indicate the data to auto-complete with
	 * @param integer Number of entries to display at a time
	 * @param boolean Whether to submit the form when an entry is selected
	 * @param integer Number of miliseconds to wait before updating/displaying the list of options
	 * @param boolean Whether to match against every word in the suggestions
	 * @param boolean Whether to surround the title in "s before submitting
	 * @param object An object containing other options that have to be coded in specifically
	 */
	addAutoComplete : function(fieldid, fieldname, acid, maxtodisplay, submitonclick, delay, searchallwords, quotebeforesubmitting, otherOptions) {
		if (delay == undefined || delay == null) delay = 100;
		if (maxtodisplay == undefined || maxtodisplay == null) maxtodisplay = 10;
		if (searchallwords == undefined || searchallwords == null) searchallwords = true;
		var acfield = CBS.getEl(fieldid);
		acfield.CBSACValues = new Array();
		acfield.CBSACCached = new Object();
		acfield.CBSACMaxCount = maxtodisplay;

		if (!otherOptions) otherOptions = {};
		if (!otherOptions.respectCategoryRestrictionBoxes) otherOptions.respectCategoryRestrictionBoxes = false;

		if (otherOptions.respectCategoryRestrictionBoxes) {
			//Clear the cache when category restriction boxes change value
			function clearCacheOnCatBoxChange() {
				acfield.CBSACCached = {};
				acfield.CBSACValues = [];
			}
			if (CBS.getEl('prodsearch_incat')) CBS.addEvent(CBS.getEl('prodsearch_incat'), 'change', clearCacheOnCatBoxChange);
			if (CBS.getEl('prodsearch_insubcats')) CBS.addEvent(CBS.getEl('prodsearch_insubcats'), 'change', clearCacheOnCatBoxChange);
		}

		var pF = CBS.getParentForm(acfield);
		pF.CBSACSubmitFunc = function(){
			acfield.name=fieldname;
		};
		CBS.addEvent(pF, 'submit', function(){pF.CBSACSubmitFunc();});

		//Set up the HTML for the options
		var optlistboxcont = CBS.cE('div');
			optlistboxcont.className = 'autocompletecont';
			var optlistbox = CBS.cE('div');
				optlistbox.className = 'autocomplete';
				var optlist = CBS.cE('ul');
				optlistbox.appendChild(optlist);
			optlistboxcont.appendChild(optlistbox);
		CBS.insertAfter(acfield.parentNode, optlistboxcont, acfield);

		/**
		 * Positions the box of auto-complete options properly.
		 * Should/can be called every time the window is resized and whenever
		 *  the contents of the box change
		 *
		 */
		function positionBox() {
		var offsets = CBS.getOffsets(acfield);
//		alert("acfield.offsetTop: "+offsets.top+"\nacfield.offsetHeight: "+acfield.offsetHeight);
//		alert("acfield.offsetTop: "+acfield.offsetTop+"\nacfield.offsetHeight: "+acfield.offsetHeight);
			optlistboxcont.style.top = offsets.top + acfield.offsetHeight + 'px';

			optlistbox.style.width = 'auto';
			offsets = CBS.getOffsets(acfield);
			var listboxLeft = offsets.left;
			var listboxWidth = optlistbox.offsetWidth;
			if (listboxWidth < acfield.offsetWidth) listboxWidth = acfield.offsetWidth;
			//Ensure the box doesn't go off the screen (and overflow:hidden it if it does)...
			var body = CBS.getBody();
			if (listboxLeft + listboxWidth > body.offsetWidth) {
				//Too wide/far to the right
				if (listboxWidth > body.offsetWidth) {
					//Listbox is wider than the body...? o.O
					listboxWidth = body.offsetWidth - 50;
					listboxLeft = 25;
					optlistbox.style.overflow = 'hidden';
				} else {
					//Not wider than the body, but too far right
					//How FAR too right?
					var excess = ((listboxLeft + listboxWidth) - body.offsetWidth) + 50;
					listboxLeft -= excess;
				}
			}

			//IE7 displays it slightly too far to the right. hack++
			if (document.all) {
				listboxLeft -= 9;
			}

			optlistboxcont.style.left = (listboxLeft) + 'px';
			optlistbox.style.width = (listboxWidth) + 'px';
		}

		/**
		 * Returns an Array of auto-complete values that match the search term
		 *  supplied as 'str'
		 *
		 * @param string str
		 * @return Array
		 */
		function getMatchingValues(str) {
			if (searchallwords) {
				var r = new Array();
				var re = new RegExp('(^| )'+CBS.regexQuote(str), "i");
				for (var i=0,l=acfield.CBSACValues.length; i<l; i++) {
					if (re.exec(acfield.CBSACValues[i])) {
						r.push(acfield.CBSACValues[i]);
					}
				}
				return r;
			} else {
				return acfield.CBSACValues.beginswith(str);
			}
		}

		/**
		 * Updates the list of options in the option box to contain only those
		 *  that match the currently entered text
		 *
		 */
		function updateOptions() {
			var str = acfield.value;
			var matches = getMatchingValues(str);
			optlist.innerHTML = '';
			if (matches.length > 0) {
				for (var i=0, l=matches.length; i<l&&i<acfield.CBSACMaxCount; i++) {
					addOptionsEntry(matches[i]);
				}
				if (matches.length > acfield.CBSACMaxCount) {
					addOptionsEntry(null);
				}
				showBox();
			} else {
				hideBox();
			}
		}

		/**
		 * "Scrolls" the selected option off of the list and resets the input
		 *  box to its last updated state (i.e. to what the user last "typed it to")
		 *
		 */
		function scrollOffList() {
			optlistbox.CBSACSelectedItem = null;
			acfield.value = acfield.CBSACLastTypedValue;
		}

		/**
		 * Updates the text box and gives the selected option a "mouse hovering
		 *  over" effect.
		 * Called when an up/down arrow key is pressed
		 *
		 */
		function updateFieldOnKeypress() {
			var v = optlistbox.CBSACSelectedItem.CBSACValue;
			if (v === null) v = acfield.CBSACLastTypedValue;
			tempChangeValue(v);
			handleMouseover(optlistbox.CBSACSelectedItem);
		}

		/**
		 * Handles keypresses for the options list - primarily arrow keys, the
		 *  escape key, return/enter and the tab button.
		 *
		 */
		function handleArrowKeypress(ev) {
			//up: 38; down: 40; esc: 27; return: 13; tab: 9
			var direction = null;
			var preventDefault = false;
			var cur = optlistbox.CBSACSelectedItem;
			var keyCode = (ev ? ev.keyCode : window.event.keyCode);
			switch (keyCode) {
				case 38: //Up arrow
					direction = 'up';
					preventDefault = true;
					break;
				case 40: //Down arrow
					direction = 'down';
					preventDefault = true;
					break;
				case 27: //Esc
					hideBox();
					preventDefault = true;
					break;
				case 13: //Return/enter
					if (cur) {
						if (submitonclick) {
							handleClick(cur.CBSACValue);
						} else {
							hideBox();
						}
						preventDefault = true;
					} else {
						hideBox();
					}
					break;
				case 9: //Tab
					hideBox();
					break;
				default:
					return;
			}
			if (direction == null) {
				if (preventDefault) {
					try {
						ev.preventDefault();
					} catch(e) {
						return false;
					}
				}
				return false;
			}

			if (direction == 'up') {
				//Scroll up
				if (cur == null) {
					//Don't already have one selected, so select the last in the list
					optlistbox.CBSACSelectedItem = optlistbox.firstChild.lastChild;
					updateFieldOnKeypress();
				} else {
					handleMouseout(cur);
					if (cur.previousSibling) {
						//Get the previous LI
						optlistbox.CBSACSelectedItem = cur.previousSibling;
						updateFieldOnKeypress();
					} else {
						//We must already be the first LI
						scrollOffList();
					}
				}
			} else {
				//Scroll down
				if (cur == null) {
					//Don't already have one selected, so select the first in the list
					optlistbox.CBSACSelectedItem = optlistbox.firstChild.firstChild;
					updateFieldOnKeypress();
				} else {
					handleMouseout(cur);
					if (cur.nextSibling) {
						//Get the next LI
						optlistbox.CBSACSelectedItem = cur.nextSibling;
						updateFieldOnKeypress();
					} else {
						//Must be the last already
						scrollOffList();
					}
				}
			}

			if (preventDefault) {
				try {
					ev.preventDefault();
				} catch(e) {
					return false;
				}
			}
			return false;
		}

		/**
		 * Updates the option to show that the mouse is hovering over it and
		 *  that it is selected
		 *
		 * @param element List item to be 'selected'
		 */
		function handleMouseover(li) {
			li.className = CBS.addClass(li.className, 'acmouseover');
			optlistbox.CBSACSelectedItem = li;
		}

		/**
		 * Updates the option to show that the mouse is no longer hovering over
		 *  it and that it is not selected
		 *
		 * @param element List item to be 'unselected'
		 */
		function handleMouseout(li) {
			li.className = CBS.removeClass(li.className, 'acmouseover');
			optlistbox.CBSACSelectedItem = null;
		}

		/**
		 * Handles the user clicking an option in the list
		 *
		 * @param string Value of the option they clicked
		 */
		function handleClick(val) {
			if (val === null) {
				document.location = CBS.makeURL('search', {'q':acfield.value});
			} else {
				useValue(val);
				if (submitonclick) {
					var f = CBS.getParentForm(acfield);
					if (f) {
						//If necessary, quote the name
						if (quotebeforesubmitting) {
							val = '"' + val + '"';
							useValue(val);
						}
						if (f.CBSACSubmitFunc) {
							f.cBSACSubmitFunc();
						}
						f.submit();
					}
				}
			}
		}

		/**
		 * Shows the box of options if it isn't already visible and at least 1
		 *  option exists, then adds various events to control it
		 *
		 */
		function showBox() {
			if(optlistboxcont.style.visibility!='visible'){
				//How many entries do we have? Only worth showing if > 1...
				if (optlistbox.firstChild.childNodes.length > 0) {
					optlistbox.CBSACSelectedItem = null;
					CBS.addEvent(CBS.getBody(), 'click', hideBox, 'CBSAC_hideonbodyclick');
					CBS.removeEventByName(acfield, 'click', 'CBSAC_reshowbox');
//					CBS.addEvent(acfield, 'keypress', handleArrowKeypress);
					CBS.addEvent(acfield, 'keydown', handleArrowKeypress);
					CBS.addEvent(window, 'resize', function(){window.setTimeout(positionBox, 100);}, 'CBSAC_posonresize');

					//Stop every option being 'selected'...
					var li = optlistbox.firstChild.firstChild;
					handleMouseout(li);
					while (li.nextSibling) {
						li = li.nextSibling;
						handleMouseout(li);
					}

					optlistboxcont.style.visibility = 'visible';
					positionBox();
				}
			}
		}

		/**
		 * Hides the box of options and removes the events that were added when
		 *  it was shown
		 *
		 */
		function hideBox() {
			optlistboxcont.style.visibility = 'hidden';
			optlistbox.CBSACSelectedItem = null;
			CBS.removeEventByName(CBS.getBody(), 'click', 'CBSAC_hideonbodyclick');
			CBS.addEvent(acfield, 'click', function(ev){
					showBox();
					//Then stop it bubbling any further, or we end up re-hiding the box when <body> receives the click...
					if (!ev) var ev = window.event;
					ev.cancelBubble = true;
				}, 'CBSAC_reshowbox');
//			CBS.removeEvent(acfield, 'keypress', handleArrowKeypress);
			CBS.removeEvent(acfield, 'keydown', handleArrowKeypress);
			CBS.removeEventByName(window, 'resize', 'CBSAC_posonresize');
		}

		/**
		 * Adds an option with the value 'val', to the list of options
		 *
		 * @param string Value of the new option
		 */
		function addOptionsEntry(val) {
			var li = CBS.cE('li');
				if (val === null) {
					li.appendChild(CBS.cTN('More Results...'));
					li.CBSACValue = null;
					CBS.addEvent(li, 'click', function(){handleClick(null);});
				} else {
					li.appendChild(CBS.cTN(val));
					li.CBSACValue = val;
					CBS.addEvent(li, 'click', function(){handleClick(val);});
				}
				CBS.addEvent(li, 'mouseover', function(){handleMouseover(li);});
				CBS.addEvent(li, 'mouseout', function(){handleMouseout(li);});
			optlist.appendChild(li);
		}

		/**
		 * Temporarily change the value of the auto-complete text box to 'val'
		 *
		 * @param string Value to temporarily change the text box to
		 */
		function tempChangeValue(val) {
			acfield.value = val;
		}

		/**
		 * Use the value 'val' "permanently"
		 *
		 * @param string Value to use "permanently"
		 */
		function useValue(val) {
			acfield.value = val;
			acfield.CBSACLastTypedValue = val;
			hideBox();
		}

		positionBox();

		/**
		 * Retrieves the list of available options using Ajax then displays the results
		 *
		 */
		function getValues() {
			var str = acfield.value;

			acfield.CBSACLastTypedValue = str;

			if (str == '') return;

			//Is this value already present in our cache?
			if (!acfield.CBSACCached[str]) {
				var handler = function(resp) {
					if (resp) {
						var values = resp.data.v;
						for (var i=0,l=values.length; i<l; i++) {
							//Are we already storing this?
							if (acfield.CBSACValues.search(values[i]) === false) {
								acfield.CBSACValues.push(values[i]);
							}
						}

						acfield.CBSACCached[str] = true;
						updateOptions();
					}
				};
				var extra = '';
				if (otherOptions.respectCategoryRestrictionBoxes) {
					if (CBS.getEl('prodsearch_incat') && CBS.getEl('prodsearch_incat').checked) {
						extra += '&incat='+CBS.getEl('prodsearch_incat').value;
						if (CBS.getEl('prodsearch_insubcats') && CBS.getEl('prodsearch_insubcats').checked) {
							extra += '&insubcats=1';
						} else {
							extra += '&insubcats=0';
						}
					}
				}
				CBSAjax.newRequest('ajax.php?f=auto-complete&acid='+acid+'&str='+escape(str)+extra, handler, false, 'get', null, function(){return false;});
			} else {
				updateOptions();
			}
		}

		/**
		 * Called when a key is pressed in the auto-completing field, and updates
		 *  the list of options if necessary, after a small delay
		 *
		 */
		function doOnKeyPress(ev) {
			if (!ev) ev = window.event;
			if (ev.keyCode == 40) { //Down arrow
				//Show the box if it's hidden...
				showBox();
			}

			if (ev.charCode == undefined) {
				if ((ev.keyCode < 65 || ev.keyCode > 90) && ev.keyCode != 8 && ev.keyCode != 46) return; //Not a character pressed (and not backspace/del)
			} else {
				if (ev.charCode == 0 && ev.keyCode != 8 && ev.keyCode != 46) return; //Not a character pressed (and not backspace/del)
			}

			if (acfield.CBSGUIAutoCompleteDelay) {
				window.clearTimeout(acfield.CBSGUIAutoCompleteDelay);
			}
			acfield.CBSGUIAutoCompleteDelay = window.setTimeout(getValues, delay);
		}
		if (!document.all) {
			CBS.addEvent(acfield, 'keypress', doOnKeyPress);
		} else {
			CBS.addEvent(acfield, 'keydown', doOnKeyPress);
		}
	},

	/**
	 * Retrieves the explanation of 'term' via Ajax then displays it in a
	 *  tooltip-style popup to the user.
	 *
	 * @param element Element the user hovered over
	 * @param string Term to retrieve explanation of
	 */
	glossaryTerm : function(el, term, mainEvent) {
		function loadPopup() {
			//Draws the content of the glossary explanation, and replaces mouse
			// events on the glossary term
			function renderPopup(content) {
				if (!el.CBSGlossaryMouseover) return;

				xoffset = GlossaryDefaultXOffset;
				yoffset = GlossaryDefaultYOffset;

				if (el.CBSGlossary) {
					el.CBSGlossary.parentNode.removeChild(el.CBSGlossary);
				}
				var div = CBS.cE('div');
					div.className = 'glossarybox';
					div.style.position = 'absolute';
					div.style.width = '200px';
					div.style.top = (el.offsetTop + xoffset) + 'px';
					div.style.left = (el.offsetLeft + el.offsetWidth + yoffset) + 'px';

					var msgdiv = CBS.cE('div');
						msgdiv.className = 'glossarymsg';
						msgdiv.innerHTML = content;
					div.appendChild(msgdiv);
				CBS.getBody().appendChild(div);
				el.CBSGlossary = div;

				div.CBSGlossaryOffsetWidth = div.offsetWidth;
				div.CBSGlossaryOffsetHeight = div.offsetHeight;

				function movePopup(x,y) {
					div.style.top = y + yoffset + 'px';
					div.style.left = x + xoffset + 'px';

//					var body = CBS.getBody();
//					if (div.offsetLeft + div.CBSGlossaryOffsetWidth > body.offsetWidth) {
//						//-5 on the offset because it's on the other side of the mouse cursor now,
//						// so doesn't have to avoid a huge cursor...
//						div.style.left = (coords.x - (div.CBSGlossaryOffsetWidth + xoffset - 5)) + 'px';
//					}
//					if (div.offsetTop + div.CBSGlossaryOffsetHeight > body.offsetHeight) {
//						div.style.top = (coords.y - (div.CBSGlossaryOffsetHeight + yoffset)) + 'px';
//					}
				}
				function handleMouseMove(ev) {
					var coords = CBS.getMouseCoords(ev);
					movePopup(coords.x, coords.y);
				}

				el.onmouseover = null;
				el.onmouseout = null;
				CBS.addEvent(el, 'mouseover', function(){div.style.visibility='visible';});
				CBS.addEvent(el, 'mouseout', function(){div.style.visibility='hidden';});
				CBS.addEvent(el, 'mousemove', handleMouseMove);

				movePopup(originalCoords.x, originalCoords.y);
			}

			//Handler for the Ajax request's response
			function handler(resp) {
				if (resp) {
					el.style.cursor = 'help';
					var cont = resp.data;
					CBSGUI.glossaryTerms[term] = cont;
					renderPopup(cont);
				}
			}

			//When glossary content is retrieved via Ajax, the value is stored
			// in an object so we don't have to repeatedly request the same
			// content.  Here we determine whether it's been requested or not
			if (typeof CBSGUI.glossaryTerms[term] == 'string') {
				renderPopup(CBSGUI.glossaryTerms[term]);
			} else {
				if (CBSGUI.glossaryTerms[term] === undefined) {
					//(url, handler, showProgress, method, postdata, headers, errorFunc, extraHandlerArgs, allowRetries, handleOwnRespErrs)
					CBSAjax.newRequest('ajax.php?f=glossary&t='+escape(term), handler, false, 'GET');
					CBSGUI.glossaryTerms[term] = true;
					el.style.cursor = 'progress';
				}
			}
		}

		//Before we do anything else, we need to store the current mouse position because otherwise IE forgets it :/
		var originalCoords = CBS.getMouseCoords(mainEvent);

		//Have a short delay here to prevent the Ajax request occurring when the
		// user just quickly moves the mouse over the element.  Once the content
		// is loaded, the mouseover function is replaced, so this delay is never
		// used
		var timer = window.setTimeout(loadPopup, 100);
		el.onmouseout = function(){el.CBSGlossaryMouseover=false;window.clearTimeout(timer);};
		el.CBSGlossaryMouseover = true;
	},

	/**
	 * Sets up the glossary feature by searching for each of the terms specified
	 *  in the 'terms' array within the HTML elements specified in the
	 *  'elementIds' array, and adding mouseover events to any matches so that
	 *  when the mouse cursor is hovered over each term a description of it is
	 *  shown in a tooltip
	 *
	 * @param array HTML element IDs to search for terms in
	 * @param array Array of objects, with each object having a 't' variable
	 *               containing the term, and a boolean 'c' variable indicating
	 *               if it's case sensitive or not
	 */
	setupGlossaryTerms : function(elementIds, terms) {
		function getAllChildren(node, type) {
			if (!type) type = false;
			if (!node) return;
			var children = [];
			for (var i=0,l=node.childNodes.length; i<l; i++) {
				if (!type || node.childNodes[i].nodeType == type) children.push(node.childNodes[i]);
				var grandchildren = getAllChildren(node.childNodes[i], type);
				for (var ii=0,ll=grandchildren.length; ii<ll; ii++) {
					if (!type || grandchildren[ii].nodeType == type) children.push(grandchildren[ii]);
				}
			}
			return children;
		}

		function createSpan(text, term) {
			var span = CBS.cE('span');
			span.className = 'glossaryterm';
			span.appendChild(CBS.cTN(text));
			CBS.addEvent(span, 'mouseover', function(ev){CBSGUI.glossaryTerm(span, term, ev);});
			return span;
		}

		function parseText(termEl, textsElement) {
			var origText = textsElement.nodeValue;
			var text;
			var toReparse = [];

			var term = termEl.t;
			var origTerm = term;
			var caseSensitive = termEl.c;

			if (!caseSensitive) {
				term = term.toLowerCase();
				text = origText.toLowerCase();
			} else {
				text = origText;
			}

			var index = text.indexOf(term);
			var length = term.length;
			if (index >= 0) {
				var pre = origText.substr(0, index);
				var mid = origText.substr(index, length);
				var post = origText.substr(index+length);

				var span = createSpan(mid, origTerm);
				var after = CBS.cTN(post);

				textsElement.nodeValue = pre;
				CBS.insertAfter(textsElement.parentNode, span, textsElement);
				CBS.insertAfter(textsElement.parentNode, after, span);

				parseText(termEl, after);
			}
		}


		function parseForTerm(term) {
			for (var i=0,l=elementIds.length; i<l; i++) {
				var texts = getAllChildren(CBS.getEl(elementIds[i]), 3);
				if (texts) {
					for (var ii=0,ll=texts.length; ii<ll; ii++) {
						parseText(term, texts[ii]);
					}
				}
			}
		}

		for (var i=0,l=terms.length; i<l; i++) {
			parseForTerm(terms[i]);
		}
	},

	/**
	 * Highlights the supplied 'element' (which can be an HTML ID or the element
	 *  itself) by adding the "ajaxhighlight" CSS class to its list of classes,
	 *  then adding "ajaxhighlight1" and replacing that with "ajaxhighlight2",
	 *  3, 4 and 5 every 300 miliseconds. Use this function to highlight parts
	 *  of the page that have been changed by Ajax requests or similar.
	 *
	 * @param string/element Element to highlight
	 */
	ajaxHighlight : function(element) {
		if (typeof element == 'string') {
			element = CBS.getEl(element);
		}
		if (!element) {
			console.log("element is invalid in CBSGUI.ajaxHighlight()");
			return;
		}

		var max = 5;
		var delay = 300;
		var cssglobal = 'ajaxhighlight';
		var cssprefix = 'ajaxhighlight';

		if (element.CBSGUIHL) {
			//already fading on this element..
			clearHL();
			window.clearTimeout(element.CBSGUIHL.timer);
		}

		element.CBSGUIHL = {current:0,timer:null};

		function runHL() {
			clearHL();

			if (element.CBSGUIHL.current < max) {
				if (element.CBSGUIHL.current == 0) {
					element.className = CBS.addClass(element.className, cssglobal);
				}
				element.CBSGUIHL.current++;
				element.className = CBS.addClass(element.className, cssprefix+element.CBSGUIHL.current);
				element.CBSGUIHL.timer = window.setTimeout(runHL, delay);
			} else {
				element.CBSGUIHL.timer = window.setTimeout(clearHL, delay);
			}
		}
		function clearHL() {
			element.className = CBS.removeClass(element.className, cssprefix+element.CBSGUIHL.current);
			if (element.CBSGUIHL.current == max) {
				element.className = CBS.removeClass(element.className, cssglobal);
			}
		}
		runHL();
	},

	/**
	 * Sets up the collapsible side columns. If saveStates is true, cookies are
	 *  set when the side columns are collapsed or expanded
	 *
	 * @param boolean saveStates
	 */
	setupCollapsibleSideColumns : function(saveStates) {
		saveStates = !!saveStates;

		function clickLeft() {
			if (!leftOpenState) {
				//Already collapsed, so expand it
				leftcol.className = CBS.removeClass(leftcol.className, 'leftcolcollapsed');
				leftcol.className = CBS.addClass(leftcol.className, 'leftcol');

				left.className = CBS.removeClass(left.className, 'barcollapsed');

				mainbody.className = CBS.removeClass(mainbody.className, 'mbnoleftcol');
				leftOpenState = true;
			} else {
				leftcol.className = CBS.addClass(leftcol.className, 'leftcolcollapsed');
				leftcol.className = CBS.removeClass(leftcol.className, 'leftcol');

				left.className = CBS.addClass(left.className, 'barcollapsed');

				mainbody.className = CBS.addClass(mainbody.className, 'mbnoleftcol');
				leftOpenState = false;
			}
			saveLeftState();
			setLeftAndRightToMaxHeights();
		}
		function clickRight() {
			if (!rightOpenState) {
				//Already collapsed, so expand it
				rightcol.className = CBS.removeClass(rightcol.className, 'rightcolcollapsed');
				rightcol.className = CBS.addClass(rightcol.className, 'rightcol');

				right.className = CBS.removeClass(right.className, 'barcollapsed');

				mainbody.className = CBS.removeClass(mainbody.className, 'mbnorightcol');
				rightOpenState = true;
			} else {
				rightcol.className = CBS.addClass(rightcol.className, 'rightcolcollapsed');
				rightcol.className = CBS.removeClass(rightcol.className, 'rightcol');

				right.className = CBS.addClass(right.className, 'barcollapsed');

				mainbody.className = CBS.addClass(mainbody.className, 'mbnorightcol');
				rightOpenState = false;
			}
			saveRightState();
			setLeftAndRightToMaxHeights();
		}

		function saveLeftState() {
			if (saveStates)
				CBS.setCookie('leftcolstate', (leftOpenState?1:0), 31536000);
		}
		function saveRightState() {
			if (saveStates)
				CBS.setCookie('rightcolstate', (rightOpenState?1:0), 31536000);
		}

		function setLeftAndRightToMaxHeights() {
			var max = 0, maybe = 0;

			if (leftOpenState) {
				maybe = leftcol.offsetHeight;
				if (maybe > max) max = maybe;
			}

			if (rightOpenState) {
				maybe = rightcol.offsetHeight;
				if (maybe > max) max = maybe;
			}

			maybe = mainbody.offsetHeight;
			if (maybe > max) max = maybe;

			left.style.height = right.style.height = max + 'px';
			lastLeftHeight = lastRightHeight = max;
		}

		var mainbody = CBS.getEl('mainbody');

		var left = CBS.getEl('leftcol_collapse');
		var leftcol = CBS.getEl('leftcol');
		var leftOpenState = (leftcol.className.indexOf('leftcolcollapsed') < 0);
		var lastLeftHeight = 0;
		left.title = "Click to toggle display of the left column";
		CBS.addEvent(left, 'click', clickLeft);

		var right = CBS.getEl('rightcol_collapse');
		var rightcol = CBS.getEl('rightcol');
		var rightOpenState = (rightcol.className.indexOf('rightcolcollapsed') < 0);
		var lastRightHeight = 0;
		right.title = "Click to toggle display of the right column";
		CBS.addEvent(right, 'click', clickRight);

		setLeftAndRightToMaxHeights();
		window.setInterval(setLeftAndRightToMaxHeights, 250);
	}
};
