/**
 * This file contains a library of common functions used throughout TCMS.  Other JavaScript
 * files should not be expected to function without it.  It depends on jQuery.
 *
 * @author Daniel J. Summers <daniel@djs-consulting.com>
 * @package TCMS
 * @subpackage View
 */
var tcms = {
	

	/**
	 * Get the jQuery-compliant ID from the given ID.
	 *
	 * @param psId The ID to escape.
	 * @return The escaped ID with a hash-sign applied.
	 */
	jqId : function(psId) {
		return "#" + psId.replace(/:/g,"\\:").replace(/\./g,"\\.");
	},

	/**
	 * Add an event handler to an object.
	 * 
	 * @param poObject The object to which the event handler should be added.
	 * @param psEventType The event which should be handled.
	 * @param poFunction The name of the function to handle the event.
	 */
	addEventHandler : function(poObject, psEventType, poFunction) {
	
		if (poObject.addEventListener){ 
			poObject.addEventListener(psEventType, poFunction, false); 
			return true; 
		}
		else if (poObject.attachEvent) {
			var r = poObject.attachEvent("on" + psEventType, poFunction);
			return r;
		}
		else {
			return false;
		} 
	},

	/**
	 * Remove all white space from both sides of a string.
	 * 
	 * @param psString The string to trim.
	 * @return The trimmed string.
	 */
	trim : function(psString) {
		return this.ltrim(this.rtrim(psString));
	},

	/**
	 * Remove white space from the front (left side) of a string.
	 * 
	 * @param psString The string to trim.
	 * @return The trimmed string.
	 */
	ltrim : function(psString) {
		while (psString.substring(0,1) == " ") {
			psString = psString.substring(1);
		}
		return psString;
	},

	/**
	 * Remove white space from the back (right side) of a string.
	 * 
	 * @param psString The string to trim.
	 * @return The trimmed string.
	 */
	rtrim : function(psString) {
		while (psString.substring(psString.length - 1) == " ") {
			psString = psString.substring(0, psString.length - 1);
		}
		return psString;
	},

	/**
	 * Set a field with the error classes.
	 *
	 * @param psFieldId The ID of the error field.
	 * @param psMessage A message to display (optional).
	 * @return boolean False.
	 */
	errorField : function(psFieldId, psMessage) {
		if ("" != psMessage) alert(psMessage);
		if ("" != psFieldId) {
			$(this.jqId(psFieldId)).addClass("errorField");
			$(this.jqId(psFieldId)).focus();
		}
		return false;
	},

	/**
	 * Remove the error class from a field.
	 * 
	 * @param psFieldId The ID of the good field.
	 * @return boolean True.
	 */
	validField : function(psFieldId) {
		$(this.jqId(psFieldId)).removeClass("errorField");
		return true;
	},

	/**
	 * Shortcut for "document.getElementById".
	 * 
	 * @return Element The specified element.
	 */
	getElement : function(psElementId) {
		return document.getElementById(psElementId);
	},

	/**
	 * Validate a string field, ensuring it has some text.
	 * 
	 * @param psElementId The ID of the HTML element to validate.
	 * @param psErrorMessage The message to show the user if it's blank.
	 * @return boolean True if it's good, false if not.
	 */
	validateString : function(psElementId, psErrorMessage) {
	
		if ("" == trim($(this.jqId(psElementId)).val())) {
			return this.errorField(psElementId, psErrorMessage);
		}
	
		return this.validField(psElementId);
	},

	/**
	 * Ensure that at least one box in a set has been selected.
	 * 
	 * @param psElementName The name of the option group to validate.
	 * @param psErrorMessage The error message to display if one isn't selected. (Passing a
	 *            blank error message causes the function to simply return true or false.)
	 * @param psLabelName The name of the label to change if there is an error.
	 * @param psClassName The CSS class name of the label.
	 * @return boolean True if one is selected, false if none are.
	 */
	validateOptionGroup : function(psElementName, psErrorMessage, psLabelName, psClassName) {
	
		var oOpts = document.getElementsByName(psElementName);
	
		for (i = 0; i < oOpts.length; i++) {
			if (oOpts[i].checked) {
				if ("" != psLabelName) {
					return this.validField(psLabelName);
				}
				else {
					return true;
				}
			}
		}
	
		return this.errorField(psLabelName, psErrorMessage);
	},

	/**
	 * Validate a dropdown list, ensuring the top option is not selected.
	 * 
	 * @param psElementId The ID of the element to validate.
	 * @param psErrorMessage The error message to display if the top option is selected.
	 * @return boolean True if a selection has been made, false if not.
	 */
	validateDropdown : function(psElementId, psErrorMessage) {
	
		if (0 == $(this.jqId(psElementId)).attr("selectedIndex")) {
			return errorField(psElementId, psErrorMessage);
		}
	
		return validField(psElementId);
	},

	/**
	 * Validate a numeric string with a set length.
	 * 
	 * @param psElementId The ID of the element to validate.
	 * @param piLength The length of the string.
	 * @param psErrorMessage The error message to display if it is not valid.
	 * @return boolean True if it's valid, false if not.
	 */
	validateNumericString : function(psElementId, piLength, psErrorMessage) {
	
		var sValue = this.trim($(this.jqId(psElementId)).val());
	
		if ((sValue.length == piLength) && (this.isNumeric(sValue))) {
			return this.validField(psElementId);
		}
	
		return this.errorField(psElementId, psErrorMessage);
	},

	/**
	 * Validate a string that should contain a decimal number.
	 * 
	 * @param psElementId The ID of the element to validate.
	 * @param pfMaxValue The maximum value of the field.
	 * @param psFieldName The display name of the field (used in error messages).
	 * @return boolean True if it's valid, false if not.
	 */
	validateDecimalString : function(psElementId, pfMaxValue, psFieldName) {
	
		var sValue = this.trim($(this.jqId(psElementId)).val());
	
		if (!this.isDecimal(sValue)) {
			return this.errorField(psElementId, psFieldName + " must contain a decimal number");
		}
	
		if (parseFloat(sValue) > pfMaxValue) {
			return this.errorField(psElementId, psFieldName + " cannot exceed " + pfMaxValue);
		}
	
		return this.validField(psElementId);
	},

	/**
	 * Validate a date/time from the standard input fields.
	 * 
	 * @param psPrefix The prefix for the fields.
	 * @param psId The ID (suffix) for the fields.
	 * @param pbValidateDate Whether to validate the date portion.
	 * @param pbValidateTime Whether to validate the time portion.
	 * @param pbOptional Whether this date/time field is optional (will not flag a completely
	 * 			empty fieldset as an error).
	 * @param psFieldName The name of the field to be displayed in error messages.
	 * @return boolean True if valid, false if not.
	 */
	validateStandardDateTime : function(psPrefix, psId, pbValidateDate, pbValidateTime,
			pbOptional, psFieldName) {
	
		// Check for an empty date, and whether that's OK.
		if (       (((pbValidateDate) && (this.emptyStandardDate(psPrefix, psId))) || (!pbValidateDate))
				&& (((pbValidateTime) && (this.emptyStandardTime(psPrefix, psId))) || (!pbValidateTime))) {
			if (pbOptional) {
				return true;
			}
			else {
				if (pbValidateDate) {
					return this.errorField(psPrefix + "Month" + psId, psFieldName
							+ " is a required field.");
				}
				else {
					return this.errorField(psPrefix + "Hour" + psId, psFieldName
							+ " is a required field.");
				}
			}
		}
	
		if (pbValidateDate) {
			if (!this.validateStandardDate(psPrefix, psId, psFieldName + " date is invalid.")) {
				return false;
			}
			this.validField(psPrefix + "Month" + psId);
			this.validField(psPrefix + "Day"   + psId);
			this.validField(psPrefix + "Year"  + psId);
		}
	
		if (pbValidateTime) {
			if (null == this.validateStandardTime(psPrefix, psId, psFieldName)) {
				return false;
			}
			this.validField(psPrefix + "Hour"   + psId);
			this.validField(psPrefix + "Minute" + psId);
			this.validField(psPrefix + "AmPm"   + psId);
		}
	
		return true;
	},

	/**
	 * Determine if a standard input date is empty.
	 * 
	 * @param psPrefix The prefix of the fields.
	 * @param psId The ID (suffix) of the fields.
	 * @return boolean True if empty, false if not.
	 */
	emptyStandardDate : function(psPrefix, psId) {
	
		return (   ("0" == $(this.jqId("txt" + psPrefix + "Month" + psId)).val())
				&& (""  == $(this.jqId(        psPrefix + "Day"   + psId)).val())
				&& (""  == $(this.jqId(        psPrefix + "Year"  + psId)).val())); 
	},

	/**
	 * Determine if a standard time is empty.
	 * 
	 * @param psPrefix The prefix of the fields.
	 * @param psId The ID (suffix) of the fields.
	 * @return boolean True if empty, false if not.
	 */
	emptyStandardTime : function(psPrefix, psId) {
	
		return (   ("" == $(this.jqId(psPrefix + "Hour"   + psId)).val())
				&& ("" == $(this.jqId(psPrefix + "Minute" + psId)).val()));
	},

	/**
	 * Validate a date from the input fields.
	 * 
	 * @param psPrefix The prefix for the standard date field.
	 * @param psId The ID (suffix) for the standard date field.
	 * @param psMessage The message to display if the date is not valid.
	 * @return boolean True if valid, false if not.
	 */
	validateStandardDate : function(psPrefix, psId, psMessage) {
	
		var sMonthId = "txt" + psPrefix + "Month" + psId;
		var sDayId   =         psPrefix + "Day"   + psId;
		var sYearId  =         psPrefix + "Year"  + psId;
	
		// Ensure that a month is selected.
		if (!this.validateDropdown(sMonthId, psMessage)) {
			return false;
		}
	
		// Ensure that a day has been entered.
		if (!this.validateNumericString(sDayId, Math.max($(this.jqId(sDayId)).val().length, 1),
				psMessage)) {
			return false;
		}
	
		// Ensure that a year has been entered.
		if (!this.validateNumericString(sYearId, 4, psMessage)) {
			return false;
		}
	
		// Ensure that the day/month/year combination is valid.
		var iDay  = parseInt($(this.jqId(sDayId )).val());
		var iYear = parseInt($(this.jqId(sYearId)).val());
	
		var iNumDays = 0;
	
		switch ($(this.jqId(sMonthId)).val()) {
	
		case "January":  case "March":    case "May":       case "July":
		case "August":   case "October":  case "December":
			iNumDays = 31;
			break;
			
		case "April":  case "June":  case "September":  case "November":
			iNumDays = 30;
			break;
			
		case "February":
			iNumDays = 28;
			if (0 == (iYear % 100)) {
				if (0 == (iYear % 400)) {
					iNumDays = 29;
				}
			}
			else if (0 == (iYear % 4)) {
				iNumDays = 29;
			}
		}
		if ((1 > iDay) || (iNumDays < iDay)) {
			return this.errorField(sDayId, psMessage);
		}
	
		return this.validField(sDayId);
	},

	/**
	 * Validate the given time, and if valid, return a 24-hour time.
	 *  
	 * @param psPrefix The prefix for the standard time fields.
	 * @param psId The ID (suffix) for the standard time fields.
	 * @param psFieldName The name of the field being validated.
	 * @return integer The hour in 24-hour format (null if invalid).
	 */
	validateStandardTime : function(psPrefix, psId, psFieldName) {
	
		var iHour   = parseInt($(this.jqId(        psPrefix + "Hour"   + psId)).val());
		var iMinute = parseInt($(this.jqId(        psPrefix + "Minute" + psId)).val());
		var sAmPm   =          $(this.jqId("txt" + psPrefix + "AmPm"   + psId)).val();
	
		if ((isNaN(iHour)) || (1 > iHour) || (12 < iHour)) {
			this.errorField(psPrefix + "Hour" + psId, psFieldName + " hour is invalid.");
			return null;
		}
		else {
			this.validField(psPrefix + "Hour" + psId);
		}
	
		if ((isNaN(iMinute)) || (0 > iMinute) || (59 < iMinute)) {
			this.errorField(psPrefix + "Minute" + psId, psFieldName + " minutes are invalid.");
			return null;
		}
		else {
			this.validField(psPrefix + "Minute" + psId);
		}
	
		if ("pm" == sAmPm) {
			iHour += 12;
		}
		else if (("am" == sAmPm) && (12 == iHour)) {
			iHour = 0;
		}
	
		// Return this time in 24-hour format.
		return (iHour * 100) + iMinute;
	},

	/**
	 * Validate a time range, given hour/minute/am-pm for two times.
	 * 
	 * @param psStartPrefix The prefix for the standard time "start" field.
	 * @param psEndPrefix The prefix for the standard time "end" field.
	 * @param boolean True if both times are valid, and start time is less than end time.
	 */
	validateTimeRange : function(psStartPrefix, psEndPrefix, psId) {
	
		// Validate start time, and get it as a 24-hour time.
		var iStartTime = this.validateStandardTime(psStartPrefix, psId, "Start Time");
	
		if (iStartTime == null) {
			return false;
		}
	
		// Validate end time, and get it as a 24-hour time.
		var iEndTime = this.validateStandardTime(psEndPrefix, psId, "End Time");
	
		if (iEndTime == null) {
			return false;
		}
	
		// Ensure the start time is less than the end time.
		if (iStartTime >= iEndTime) {
			alert("The starting time cannot be after the ending time.");
			return false;
		}
	
		// It's good!
		return true;
	},

	/**
	 * Checks a string for all numeric characters.
	 * 
	 * @param psString The string to check.
	 * @return True if all characters are numbers, false otherwise.
	 */
	isNumeric : function(psString) {
	
		var sNumbers = "0123456789";
		for (var x = 0; x < psString.length; x++) {
			if (0 > sNumbers.indexOf(psString.charAt(x))) {
				return false;
			}
		}
	
		return true;
	},

	/**
	 * Checks a string for all numeric or decimal characters.
	 *
	 * @param psString The string to check.
	 * @return True if all characters are numbers or a decimal point, false otherwise.
	 */
	isDecimal : function(psString) {
	
		var sDecimals = "0123456789.";
		for (var x = 0; x < psString.length; x++) {
			if (0 > sDecimals.indexOf(psString.charAt(x))) {
				return false;
			}
		}
	
		return true;
	},

	/**
	 * Enable an element.
	 * 
	 * @param psString The ID of the element to enable.
	 */
	enable : function(psString) {
		$(this.jqId(psString)).removeAttr("disabled");
	},

	/**
	 * Disable an element.
	 * 
	 * @param psString The ID of the element to disable.
	 */
	disable : function(psString) {
		$(this.jqId(psString)).attr("disabled", "disabled");
	},

	/**
	 * Clone a row, appending a new ID to the IDs of all input and select elements, and filling
	 * a hidden ID field with the new ID.
	 * 
	 * @param psRowToClone The ID of the row to clone.
	 * @param psNewIdField The ID of the field that contains the counter for new rows.  (This will
	 * 			decrement this counter as well.)
	 * @return HTMLTableRowElement The cloned row.
	 */
	cloneRow : function(psRowToClone, psNewIdField) {
	
		var oRow = this.getElement(psRowToClone).cloneNode(true);
	
		// Get the ID for this row.
		$(this.jqId(psNewIdField)).val(parseInt($(this.jqId(psNewIdField)).val()) - 1);
		var iNewId = parseInt($(this.jqId(psNewIdField)).val());
	
		// Append new IDs to all input fields; for the one whose name contains "Id[]", set the
		// value to the new ID.
		var oInputs = oRow.getElementsByTagName("input");
	
		for (var iIndex = 0; iIndex < oInputs.length; iIndex++) {
			oInputs[iIndex].id += iNewId;
			oInputs[iIndex].value = "";
			if (0 <= oInputs[iIndex].name.indexOf("Id[]")) {
				// Set the ID.
				oInputs[iIndex].value = iNewId;
			}
			// Uncheck any checkboxes
			if ("checkbox" == oInputs[iIndex].type) {
				oInputs[iIndex].checked = "";
			}
		}
	
		// Append new IDs to all select fields, and set their selected indexes to 0.
		var oSelects = oRow.getElementsByTagName("select");
	
		for (var iIndex = 0; iIndex < oSelects.length; iIndex++) {
			oSelects[iIndex].id += iNewId;
			oSelects[iIndex].selectedIndex = 0;
		}
	
		// Append new IDs to the "for" attributes of all labels (if not blank).
		var oLabels = oRow.getElementsByTagName("label");
	
		for (var iIndex = 0; iIndex < oLabels.length; iIndex++) {
			if ("" != oLabels[iIndex].htmlFor) {
				oLabels[iIndex].htmlFor += iNewId;
			}
		}
		
		// Append new IDs to all divs.
		var oDivs = oRow.getElementsByTagName("div");
		
		for (var iIndex = 0; iIndex < oDivs.length; iIndex++) {
			oDivs[iIndex].id += iNewId;
		}
	
		// Append the new ID to the row.
		oRow.id += iNewId;
	
		return oRow;
	},

	/**
	 * Open a window with help.
	 * 
	 * @param psUrl The URL for the help page.
	 */
	showHelp : function(psUrl) {
		window.open($(this.jqId("url")).val() + "/help/" + psUrl, "helpWindow",
			"height=600px,width=450px,toolbar=0,menubar=0,scrollbars=1");
	}
};

// ----
// OLD FUNCTIONS BELOW
// ----
/**
 * Get the jQuery-compliant ID from the given ID.
 * 
 * @param psId The ID to escape.
 * @return The escaped ID with a hash-sign applied.
 */
function jqId(psId) {
	return "#" + psId.replace(/:/g,"\\:").replace(/\./g,"\\.");
}

/**
 * Add an event handler to an object.
 * 
 * @param poObject The object to which the event handler should be added.
 * @param psEventType The event which should be handled.
 * @param poFunction The name of the function to handle the event.
 */
function addEventHandler(poObject, psEventType, poFunction) {
	
	if (poObject.addEventListener){ 
		poObject.addEventListener(psEventType, poFunction, false); 
		return true; 
	}
	else if (poObject.attachEvent) {
		var r = poObject.attachEvent("on" + psEventType, poFunction);
		return r;
	}
	else {
		return false;
	} 
}

/**
 * Remove all white space from both sides of a string.
 * 
 * @param psString The string to trim.
 * @return The trimmed string.
 */
function trim(psString) {
	return ltrim(rtrim(psString));
}

/**
 * Remove white space from the front (left side) of a string.
 * 
 * @param psString The string to trim.
 * @return The trimmed string.
 */
function ltrim(psString) {
	while (psString.substring(0,1) == " ") {
		psString = psString.substring(1);
	}
	return psString;
}

/**
 * Remove white space from the back (right side) of a string.
 * 
 * @param psString The string to trim.
 * @return The trimmed string.
 */
function rtrim(psString) {
	while (psString.substring(psString.length - 1) == " ") {
		psString = psString.substring(0, psString.length - 1);
	}
	return psString;
}

/**
 * Set a field with the error classes.
 * 
 * @param psFieldId The ID of the error field.
 * @param psMessage A message to display (optional).
 * @return false
 */
function errorField(psFieldId, psMessage) {
	if ("" != psMessage) alert(psMessage);
	$(jqId(psFieldId)).addClass("errorField");
	$(jqId(psFieldId)).focus();
	return false;
}

/**
 * Remove the error class from a field.
 * 
 * @param psFieldId The ID of the good field.
 */
function validField(psFieldId) {
	$(jqId(psFieldId)).removeClass("errorField");
	return true;
}

/**
 * Shortcut for "document.getElementById".
 * 
 * @return Element The specified element.
 */
function getElement(psElementId) {
	return document.getElementById(psElementId);
}

/**
 * Validate a string field, ensuring it has some text.
 * 
 * @param psElementId The ID of the HTML element to validate.
 * @param psErrorMessage The message to show the user if it's blank.
 * @return True if it's good, false if not.
 */
function validateString(psElementId, psErrorMessage) {
	
	if ("" == trim($(jqId(psElementId)).val())) {
		return errorField(psElementId, psErrorMessage);
	}
	
	return validField(psElementId);
}

/**
 * Ensure that at least one box in a set has been selected.
 * 
 * @param psElementName The name of the option group to validate.
 * @param psErrorMessage The error message to display if one isn't selected. (Passing a
 *            blank error message causes the function to simply return true or false.)
 * @param psLabelName The name of the label to change if there is an error.
 * @param psClassName The CSS class name of the label.
 * @return True if one is selected, false if none are.
 */
function validateOptionGroup(psElementName, psErrorMessage, psLabelName, psClassName) {
	
	var oOpts = document.getElementsByName(psElementName);
	
	for (i = 0; i < oOpts.length; i++) {
		if (oOpts[i].checked) {
			if ("" != psLabelName) {
				$(jqId(psLabelName)).removeClass("errorField");
			}
			return true;
		}
	}
	
	if ("" < psErrorMessage) {
		alert(psErrorMessage);
	}
	if ("" != psLabelName) {
		$(jqId(psLabelName)).addClass("errorField");
	}
	return false;
}

/**
 * Validate a dropdown list, ensuring the top option is not selected.
 * 
 * @param psElementId The ID of the element to validate.
 * @param psErrorMessage The error message to display if the top option is selected.
 * @return True if a selection has been made, false if not.
 */
function validateDropdown(psElementId, psErrorMessage) {
	
	if (0 == $(jqId(psElementId)).attr("selectedIndex")) {
		return errorField(psElementId, psErrorMessage);
	}
	
	return validField(psElementId);
}

/**
 * Validate a numeric string with a set length.
 * 
 * @param psElementId The ID of the element to validate.
 * @param piLength The length of the string.
 * @param psErrorMessage The error message to display if it is not valid.
 * @return True if it's valid, false if not.
 */
function validateNumericString(psElementId, piLength, psErrorMessage) {
	
	var sValue = trim($(jqId(psElementId)).val());
	
	if ((sValue.length == piLength) && (isNumeric(sValue))) {
		return validField(psElementId);
	}
	
	return errorField(psElementId, psErrorMessage);
}

/**
 * Quickly validates a date in the form "YYYY-MM-DD".
 * 
 * @param psElementId The ID of the element to validate.
 * @param psErrorMessage The error message to display if it is not valid.
 * @return True if valid, false if not.
 */
function validateDate(psElementId, psErrorMessage) {
	
	if (validateDateString($(jqId(psElementId)).val())) {
		return validField(psElementId);
	}
	
	return errorField(psElementId, psErrorMessage);
}

/**
 * Quickly validates a date in the form "YYYY-MM-DD".
 * 
 * @param psValue The text with the date to validate.
 * @return True if valid, false if not.
 */
function validateDateString(psValue) {
	
	var oPieces = psValue.split("-");
	
	if ((3 == oPieces.length)
			&& (isNumeric(oPieces[0]))
			&& ((1 <= oPieces[1]) && (12 >= oPieces[1]))
			&& ((1 <= oPieces[2]) && (31 >= oPieces[2]))) {
		// Why does the date have to be in this format? ;(
		sDate = oPieces[1] + "/" + oPieces[2] + "/" + oPieces[0];
		if (!isNaN(parseInt(Date.parse(sDate)))) {
			// Good enough for government work.
			return true;
		}
	}
	
	// Must not be good if we got here...
	return false;
}

/**
 * Validate a date/time from the standard input fields.
 * 
 * @param psPrefix The prefix for the fields.
 * @param psId The ID (suffix) for the fields.
 * @param pbValidateDate Whether to validate the date portion.
 * @param pbValidateTime Whether to validate the time portion.
 * @param pbOptional Whether this date/time field is optional (will not flag a completely
 * 			empty fieldset as an error).
 * @param psFieldName The name of the field to be displayed in error messages.
 * @return boolean True if valid, false if not.
 */
function validateStandardDateTime(psPrefix, psId, pbValidateDate, pbValidateTime, pbOptional,
		psFieldName) {
	
	// Check for an empty date, and whether that's OK.
	if ((((pbValidateDate) && (emptyStandardDate(psPrefix, psId))) || (!pbValidateDate))
			&& (((pbValidateTime) && (emptyStandardTime(psPrefix, psId))) || (!pbValidateTime))) {
		if (pbOptional) {
			return true;
		}
		else {
			if (pbValidateDate) {
				return errorField(psPrefix + "Month" + psId, psFieldName + " is a required field.");
			}
			else {
				return errorField(psPrefix + "Hour" + psId, psFieldName + " is a required field.");
			}
		}
	}
	
	if (pbValidateDate) {
		if (!validateStandardDate(psPrefix, psId, psFieldName + " date is invalid.")) {
			return false;
		}
		validField(psPrefix + "Month" + psId);
		validField(psPrefix + "Day"   + psId);
		validField(psPrefix + "Year"  + psId);
	}
	
	if (pbValidateTime) {
		if (null == validateStandardTime(psPrefix, psId, psFieldName)) {
			return false;
		}
		validField(psPrefix + "Hour"   + psId);
		validField(psPrefix + "Minute" + psId);
		validField(psPrefix + "AmPm"   + psId);
	}
	
	return true;
}

/**
 * Determine if a standard input date is empty.
 * 
 * @param psPrefix The prefix of the fields.
 * @param psId The ID (suffix) of the fields.
 * @return boolean True if empty, false if not.
 */
function emptyStandardDate(psPrefix, psId) {
	
	return (   ("0" == $(jqId("txt" + psPrefix + "Month" + psId)).val())
			&& ("" == $(jqId(psPrefix + "Day"   + psId)).val())
			&& ("" == $(jqId(psPrefix + "Year"  + psId)).val())); 
}

/**
 * Determine if a standard time is empty.
 * 
 * @param psPrefix The prefix of the fields.
 * @param psId The ID (suffix) of the fields.
 * @return boolean True if empty, false if not.
 */
function emptyStandardTime(psPrefix, psId) {
	
	return (   ("" == $(jqId(psPrefix + "Hour"   + psId)).val())
			&& ("" == $(jqId(psPrefix + "Minute" + psId)).val()));
}

/**
 * Validate a date from the input fields.
 * 
 * @param psPrefix The prefix for the standard date field.
 * @param psId The ID (suffix) for the standard date field.
 * @param psMessage The message to display if the date is not valid.
 * @return True if valid, false if not.
 */
function validateStandardDate(psPrefix, psId, psMessage) {
	
	var sMonthId = "txt" + psPrefix + "Month" + psId;
	var sDayId   = psPrefix + "Day"   + psId;
	var sYearId  = psPrefix + "Year"  + psId;
	
	// Ensure that a month is selected.
	if (!validateDropdown(sMonthId, psMessage)) {
		return false;
	}
	
	// Ensure that a day has been entered.
	if (!validateNumericString(sDayId, Math.max($(jqId(sDayId)).val().length, 1), psMessage)) {
		return false;
	}
	
	// Ensure that a year has been entered.
	if (!validateNumericString(sYearId, 4, psMessage)) {
		return false;
	}
	
	// Ensure that the day/month/year combination is valid.
	var iDay  = parseInt($(jqId(sDayId )).val());
	var iYear = parseInt($(jqId(sYearId)).val());
	
	var iNumDays = 0;
	
	switch ($(jqId(sMonthId)).val()) {
	
	case "January":
	case "March":
	case "May":
	case "July":
	case "August":
	case "October":
	case "December":
		iNumDays = 31;
		break;
	case "April":
	case "June":
	case "September":
	case "November":
		iNumDays = 30;
		break;
	case "February":
		iNumDays = 28;
		if (0 == (iYear % 100)) {
			if (0 == (iYear % 400)) {
				iNumDays = 29;
			}
		}
		else if (0 == (iYear % 4)) {
			iNumDays = 29;
		}
	}
	if ((1 > iDay) || (iNumDays < iDay)) {
		return errorField(sDayId, psMessage);
	}
	
	return validField(sDayId);
}

/**
 * Validate the given time, and if valid, return a 24-hour time.
 *  
 * @param psPrefix The prefix for the standard time fields.
 * @param psId The ID (suffix) for the standard time fields.
 * @param psFieldName The name of the field being validated.
 * @return integer The hour in 24-hour format (null if invalid).
 */
function validateStandardTime(psPrefix, psId, psFieldName) {
	
	var iHour   = parseInt($(jqId(psPrefix + "Hour"   + psId)).val());
	var iMinute = parseInt($(jqId(psPrefix + "Minute" + psId)).val());
	var sAmPm   = $(jqId("txt" + psPrefix + "AmPm" + psId)).val();
	
	if ((isNaN(iHour)) || (1 > iHour) || (12 < iHour)) {
		errorField(psPrefix + "Hour" + psId, psFieldName + " hour is invalid.");
		return null;
	}
	else {
		validField(psPrefix + "Hour" + psId);
	}
	
	if ((isNaN(iMinute)) || (0 > iMinute) || (59 < iMinute)) {
		errorField(psPrefix + "Minute" + psId, psFieldName + " minutes are invalid.");
		return null;
	}
	else {
		validField(psPrefix + "Minute" + psId);
	}
	
	if ("pm" == sAmPm) {
		iHour += 12;
	}
	else if (("am" == sAmPm) && (12 == iHour)) {
		iHour = 0;
	}
	
	// Return this time in 24-hour format.
	return (iHour * 100) + iMinute;
}

/**
 * Validate a 24-hour time in the form "HH:MM".
 * 
 * @param psValue The text with the time to validate.
 * @return True if valid, false if not.
 */
function validateTimeString(psValue) {
	
	var oPieces = psValue.split(":");
	
	if ((2 == oPieces.length)
			&& (isNumeric(oPieces[0]))
			&& (isNumeric(oPieces[1]))
			&& ((0 <= oPieces[0]) && (23 >= oPieces[0]))
			&& ((0 <= oPieces[1]) && (59 >= oPieces[1]))) {
		// It's good!
		return true;
	}
	
	// Not so much...
	return false;
}

/**
 * Validate a time range, given hour/minute/am-pm for two times.
 * 
 * @param psStartPrefix The prefix for the standard time "start" field.
 * @param psEndPrefix The prefix for the standard time "end" field.
 * @param boolean True if both times are valid, and start time is less than end time.
 */
function validateTimeRange(psStartPrefix, psEndPrefix, psId) {
	
	// Validate start time, and get it as a 24-hour time.
	var iStartTime = validateStandardTime(psStartPrefix, psId, "Start Time");
	
	if (iStartTime == null) {
		return false;
	}
	
	// Validate end time, and get it as a 24-hour time.
	var iEndTime = validateStandardTime(psEndPrefix, psId, "End Time");
	
	if (iEndTime == null) {
		return false;
	}
	
	// Ensure the start time is less than the end time.
	if (iStartTime >= iEndTime) {
		alert("The starting time cannot be after the ending time.");
		return false;
	}
	
	// It's good!
	return true;
}

/**
 * Checks a string for all numeric characters.
 * 
 * @param psString The string to check.
 * @return True if all characters are numbers, false otherwise.
 */
function isNumeric(psString) {
	
	var sNumbers = "0123456789";
	
	for (x = 0; x < psString.length; x++) {
		if (sNumbers.indexOf(psString.charAt(x)) < 0) {
			return false;
		}
	}
	
	return true;
}

/**
 * Enable an element.
 * 
 * @param psString The ID of the element to enable.
 */
function enable(psString) {
	$(jqId(psString)).removeAttr("disabled");
}

/**
 * Disable an element.
 * 
 * @param psString The ID of the element to disable.
 */
function disable(psString) {
	$(jqId(psString)).attr("disabled", "disabled");
}

/**
 * Test whether a checkbox is checked or not.
 * 
 * @param psElementId The ID of the element to test.
 * @return True if checked, false if cleared.
 */
function isChecked(psElementId) {
	return $(jqId(psElementId)).attr("checked");
}

/**
 * Moves an individual from an Available list to an Assigned list.
 */
function moveRight() {
	
	var oAvail = getElement("txtAvailable");
	var oAsg = getElement("txtAssigned");
	
	if (oAvail.selectedIndex >= 0) {
		var iIndex = oAvail.selectedIndex;
		var oOption = oAvail.options[iIndex];
		oAvail.remove(iIndex);
		try {
			// IE doesn't like this.
			oAsg.add(oOption, null);
		}
		catch (oException) {
			// IE likes this better.
			oAsg.add(oOption);
		}
		if (oAvail.options.length <= iIndex) {
			oAvail.selectedIndex = iIndex;
		}
	}
}

/**
 * Moves an individual from an Assigned list to an Available list.
 */
function moveLeft() {
	
	var oAvail = getElement("txtAvailable");
	var oAsg = getElement("txtAssigned");
	
	if (oAsg.selectedIndex >= 0) {
		var oOption = oAsg.options[oAsg.selectedIndex];
		oAsg.remove(oAsg.selectedIndex);
		try {
			// IE doesn't like this.
			oAvail.add(oOption, null);
		}
		catch (oException) {
			// IE likes this better.
			oAvail.add(oOption);
		}
	}
	
}

/**
 * Multiple select boxes do not get posted with the form. This creates an array
 * of input elements with the values in the box specified.
 * 
 * @param psSelectBox
 *            The ID of the select box.
 * @param psFormId
 *            The ID of the form.
 * @param psName
 *            The name the array should have.
 */
function convertMultiSelectToArray(psSelectBox, psFormId, psName) {
	
	var oBox = getElement(psSelectBox);
	
	for (i = 0; i < oBox.options.length; i++) {
		var oInput = document.createElement("input");
		oInput.type = "hidden";
		oInput.name = psName + "[]";
		oInput.value = oBox.options[i].value;
		getElement(psFormId).appendChild(oInput);
	}
}

/**
 * Clone a row, appending a new ID to the IDs of all input and select elements, and filling
 * a hidden ID field with the new ID.
 * 
 * @param psRowToClone The ID of the row to clone.
 * @param psNewIdField The ID of the field that contains the counter for new rows.  (This will
 * 			decrement this counter as well.)
 * @return HTMLTableRowElement The cloned row.
 */
function cloneRow(psRowToClone, psNewIdField) {
	
	var oRow = getElement(psRowToClone).cloneNode(true);
	
	// Get the ID for this row.
	$(jqId(psNewIdField)).val(parseInt($(jqId(psNewIdField)).val()) - 1);
	var iNewId = parseInt($(jqId(psNewIdField)).val());
	
	// Append new IDs to all input fields; for the one whose name contains "Id[]", set the
	// value to the new ID.
	var oInputs = oRow.getElementsByTagName("input");
	
	for (var iIndex = 0; iIndex < oInputs.length; iIndex++) {
		oInputs[iIndex].id += iNewId;
		oInputs[iIndex].value = "";
		if (0 <= oInputs[iIndex].name.indexOf("Id[]")) {
			// Set the ID.
			oInputs[iIndex].value = iNewId;
		}
		// Uncheck any checkboxes
		if ("checkbox" == oInputs[iIndex].type) {
			oInputs[iIndex].checked = "";
		}
	}
	
	// Append new IDs to all select fields, and set their selected indexes to 0.
	var oSelects = oRow.getElementsByTagName("select");
	
	for (var iIndex = 0; iIndex < oSelects.length; iIndex++) {
		oSelects[iIndex].id += iNewId;
		oSelects[iIndex].selectedIndex = 0;
	}
	
	// Append new IDs to the "for" attributes of all labels (if not blank).
	var oLabels = oRow.getElementsByTagName("label");
	
	for (var iIndex = 0; iIndex < oLabels.length; iIndex++) {
		if ("" != oLabels[iIndex].htmlFor) {
			oLabels[iIndex].htmlFor += iNewId;
		}
	}
	
	// Append the new ID to the row.
	oRow.id += iNewId;
	
	return oRow;
}

/**
 * Runs an AJAX request.
 * 
 * @param psUrl The URL of the page to retrieve.
 * @param poFunction The function to handle the return data.
 * @return False if the object can't be instantiated
 */
var oXmlRequest;
function runAjaxRequest(psUrl, poFunction) {
	
	try {
		// Everything but IE
		oXmlRequest = new XMLHttpRequest();
	} catch (e) {
		try {
			// IE
			oXmlRequest = new ActiveXObject("Msxml2.XMLHTTP");
		} catch (e) {
			try {
				oXmlRequest = new ActiveXObject("Microsoft.XMLHTTP");
			} catch (e) {
				alert("Sorry - your browser does not support the lookup designed for this page.");
				return false;
			}
		}
	}
	
	oXmlRequest.onreadystatechange = poFunction;
	oXmlRequest.open("GET", psUrl, true);
	oXmlRequest.send(null);
}

/**
 * Parse a string into an XML document (used with AJAX requests).
 * 
 * @param psString
 *            The string with the XML text.
 * @return A DOM document.
 */
function parseXmlDocument(psString) {
	
	var doc;
	
	if (document.implementation.createDocument) {
		// Not IE.
		var parser = new DOMParser();
		doc = parser.parseFromString(psString, "text/xml");
	}
	else {
		 if (window.ActiveXObject) {
			// IE.
			doc = new ActiveXObject("Microsoft.XMLDOM");
			doc.async="false";
			doc.loadXML(psString);
		}
	}
	return doc;
}

/**
 * Open a window with help.
 * 
 * @param string
 *            The URL for the help page.
 */
function showHelp(psUrl) {
	window.open(getElement("url").value + "/help/" + psUrl,
		"helpWindow",
		"height=600px,width=450px,toolbar=0,menubar=0,scrollbars=1");
}

