/*
 Smart Form Validation 2.2 (John_Smart@GreyDuck.com) http://www.GreyDuck.com

Includes publicly available code from
        Jake Howlett, http://codestore.net
        Cyanide_7, ( web site off line )

You may use and distribute provided that the above lines are not removed.

 Version History
	1.0	Written/Assembled by John Smart on 9/26/2001
	1.1	Created clearValidationMsgs, updated some comments, fixed for dates (date min/max still broken).  5/29/2002
	1.2	Added email validation
	1.3	Added support for textareas 7/18/2002
	1.4	Added integer validation 7/25/2002
	1.41	Fixed min/max for text length msg, fixed date min/max validations 7/29/2002
	1.42	Fixed regressed bug where registerValidationElement wasn't setting .valiRequired properly 8/8/2002
	1.43	Corrected "field must be between 5 and 5 characters long" to "field must be 5 characters long."
	1.5	Added min/max for Checkbox.  9/3/2002
	1.6	Fixed bug that didn't allow dash-delimited dates.  Code now changes valid date values to a slash-delimited format.  (e.g. converts "12-4-02" to "12/4/2002") 12/4/2002
	2.0	No changes since 1.61.  I just documented the flexible way of doing things.  5/2/2003
	2.01	Improved documentation 5/6/2003
	2.1	Improved documentation (removed defunct contact info for Cyanide_7, fixed a typo), added optional argument to formatCurrency, rounding to the nearest decimal digit (default is 2 for backward compatibility). 9/3/2003
	2.2	Added validation type of "datetime" so that values like "10/20/2003 7:53 PM" can be validated.
		Changed formatMMDDYYYY to force leading zeros for single digit month and day.
		Added optional third argument to formatCurrency for denomination symbol.
		standardValidation now changes focus to first invalid element (goValiFirstInvalid) unless
                                goValiFirstInvalid.onInvalid is defined, in which case goValiFirstInvalid.onInvalid.focus() is called. 10/21/2003

The easy, three step way to use this script's validation:
Step 1: Change the form's onSubmit code to "return standardValidation()"
Step 2: For all single-value keyword fields on the form, make sure the first option is a dummy value like "" or "(Select One)"
Step 3: In the form's JS Header, create a JavaScript function named registerValidationElements() that calls this library's 
registerValidationElement function for each object to be validated.  (Your registerValidationElements function will be called
only the first time that standardValidation() is called.)

The flexible way to use this script's validation on your forms:
For all single-value keyword fields on the form, make sure the first option is a dummy value like "" or "(Select One)"
In your onSubmit function,
1. Call clearValidationMsgs() to clear any validation messages that might have existed the previous time you called onSubmit
2. Use validateElement function for each field you'd like to validate.  This returns true if valid.  (The arguments for
validateElement are documented below.)
3. if any of the validateElement calls returned false, alert(validationMsg()) and return false, otherwise return true.

The flexible way allows you to write validation that are dependent on certain criteria, such as not validating a
Price field if a Quantity field is blank.  It also allows you to use your own validation message code... you don't have to
call clearValidationMsgs() or validationMsg() at all if you'd rather write your own UI.

The registerValidationElement and validateElement functions use up to six arguments:
registerValidationElement(o, sName, bRequired[, vMax[, vMin[, sType]]])
validateElement(o[, sName[, bRequired[, vMax[, vMin[, sType]]]]])

o           Object    field to be validated.  e.g. document.forms[0].ZipCode

sName       String    title to use when prompting any invalid values to the user.  e.g. "Zip Code", or "Name"

bRequired   boolean   boolean value indicating whether or not this field is required.  e.g. true, or false

vMax        Number    If sType is "number", "date" or "datetime", maximum value allowed.
                      If sType is "text" then maximum length allowed.
                      If sType is "date" or "datetime" then a date value or date string.
                      Time parts of date values are ignored.
                      Time parts of date strings are not allowed.
                      Date values and date strings are inclusive. (1/1/2000 11:00 PM is considered within a max of "1/1/2000")
                      e.g. 10 or "10/5/2090" or a date object.

vMin        Number    If sType is "number", "date", or "datetime", minimum value allowed.
                      If sType is "text" then minimum length allowed.
                      If sType is "date" or "datetime" then a date value or date string.
                      Time parts of date values are ignored.
                      Time parts of date strings are not allowed.
                      Date values and date strings are inclusive.
                      e.g. 5 or "5/10/1990" or a date object

sType       String    This argument allows you to override the default type of the object.
                      If null or not supplied, the type will be o.type.  This is great for most fields, but
                      in the case of an INPUT field, o.type returns "text".  In some cases, the input in question
                      holds date or numerical values.  You can augment the validation behavior by using "number",
                      "date", "datetime", "percent", "integer", or "email" for this argument.
                      If "datetime", dates without time values are still valid.
                      Note: We've seen issues with fields that have been solved by explicitly passing the type
                            in this argument.  If you're validating checkbox or radio objects, you 
                            shouldn't-but-might have to include "checkbox" or "radio" here.
                      e.g. "number", "date", "datetime", "percent", "integer", "email", "checkbox", "radio", "textarea",
                           "select-one", or "select-multiple"

So an example registerValidationElements would be:
function registerValidationElements() {
	var F = document.forms[0];
	registerValidationElement(F.LastName, "Last Name", true) // input text that can't be empty
	registerValidationElement(F.Age, "Age", false, 150, 1, "integer") // non-required input that must be a valid integer between 1 and 150 inclusive
	registerValidationElement(F.State, "State", true) // keyword field of States that must have a value
	registerValidationElement(F.Interests, "Interests", true) // checkboxes where at least one must be checked
	registerValidaitonElement(F.Phone1, "Home Phone", false, null, 10) // input field that, if filled, must have at least ten characters
	registerValidationElement(F.Discount, "% Discount", false, 100, 0, "percent") // non-required percent value between 0 and 100 inclusive.
	registerValidationElement(F.BirthDate, "Birth Date", false, new Date(), "1/1/1900", "date") // optional date between 1900 and today
	registerValidationElement(F.Email, "Email Address", true, null, null, "email") //required email address field.
}
and as long as you return standardValidation() in the onSubmit, you're done!

Note: This code also works within Lotus Notes clients.

ADDITIONAL JAVASCRIPT FUNCTIONS WITHIN THIS SUBFORM:
***********************************************************************
validateElement is the central code in this JS library.  It returns true if a field is valid, otherwise it adds the field
name to the global validation message strings and returns false.
***********************************************************************
valiTime returns true if a time string is valid
***********************************************************************
valiDate returns true if a date is valid
***********************************************************************
valiEmail returns true if a email is valid
***********************************************************************
validationMsg takes global string variables of invalid field names and concatenates them into a single string to alert to
the user.  (These global string variables are cleared by clearValidationMsgs and appended to by validateElement)
***********************************************************************
clearValidationMsgs clears the global string variables that are used in validationMsg (above).
***********************************************************************
setValidationMsgs first clears (using clearValidationMsgs) then populates (using validateElement) the global string variables
of invalid fields that are used in validationMsg
***********************************************************************
standardValidation calls setValidationMsgs and, if there was a validation issue, calls alert(validationMsg())
If you want something more customized for your form, then instead of calling this function in the onSubmit, write your own function.
***********************************************************************
isNumOutOfRange returns true if the first argument is not between the second and third
***********************************************************************
inputToNumber returns the numeric value of argumentobject.value, or 0 if not a valid number.
***********************************************************************
formatCurrency translates a number into a comma-delimited number, rounded to the specified number of digits, prefixed with a symbol of your choosing.  (default is '$')
For a currency field, put
	this.value = formatCurrency(inputToNumber(this), 0)
in the onBlur to automatically format the fields to currency rounded to the nearest dollar.
You can also use this to format numbers that aren't currency by including a blank string for the denomination symbol, like:
	this.value = formatCurrency(inputToNumber(this), 0, '')
This function isn't called within this script library.  It's just included here as a freebie because its so darn useful.
***********************************************************************
For a percentage field, put
	this.value = formatPercent(inputToNumber(this))
in the onBlur to automatically format the fields to a percentage.  This is also done when validating fields
registered with an sType of "percent".
***********************************************************************/

var goValiFirstInvalidElement; // cleared by clearValidationMsgs, set by setValidationMsg, used by standardValidation
var gsValiPrefix = "\n   - "; // constant
var gsValiEmpty; // list of field names that are required but empty, each prefixed by gsValiPrefix
var gsValiNaN; // list of numeric field names that don't have valid numbers.
var gsValiNotDate;
var gsValiNotTime;
var gsValiNotDateTime;
var gsValiOther; // list of field names and why they are invalid
var gaoValiElements = new Array(); // array of elements registered via registerValidationElement.
var gsDateFormat = "mm/dd/yyyy"; // change this to dd/mm/yyyy as your location requires

function isNumOutOfRange(num, min, max) {
	if (min != null && num < min) return -1;
	if (max != null && num > max) return 1;
	return 0;
}

function inputToNumber(o) { // given an input field, returns the numeric value, or zero if isNaN.
	var r = /[^\-0-9.]/g;
	var n = o.value.replace(r, "");
	return (isNaN(n) ? 0 : Number(n));
}

function formatPercent(n) {
	return (n < 1 ? (n * 100) : n ) + "%";
}

function formatCurrency(n, iDecimals, sSymbol) {
/*	Author: Cyanide_7 (email and web address defunct)
	Found on http://javascript.internet.com/forms/currency-format.html
	altered by adding optional iDecimals argument.  default of 2 rounds to nearest hundredth (i.e. penny), 3 = nearest thousandth, -3 = nearest thousand
	also added optional third argument of denomination symbol.  use '' for no symbol.
*/
	var cents
	var i
	var sign
	var tens

	if (sSymbol === undefined || sSymbol === null) sSymbol = '$'
	
	if (isNaN(n)) n = '0'; else n = n.toString().replace(/\$|\,/g,'')
	sign = (n == (n = Math.abs(n)))
	if (iDecimals == null) iDecimals = 2
	

	var tens = Math.pow(10,iDecimals)
	n = Math.floor(n*tens+0.50000000001)
	if (iDecimals <= 0) cents = ''
	else {
		cents = n%tens
		cents = tens.toString() + cents.toString()
		cents = '.' + cents.substr(cents.length-iDecimals)
	}
	n = Math.floor(n/tens)
	n = n.toString()
	for (var i = 0; i < Math.floor((n.length-(1+i))/3); i++)
		n = n.substring(0, n.length - (4 * i + 3)) + ',' + n.substring(n.length - (4 * i + 3))
	return (((sign)?'':'-') + sSymbol + n + cents)
}

function timeComponents(sTime) {
	var aTime = sTime.match(/^(\d{1,2}):(\d{2})\s{1}([AP]M)$/i);
	
	if (aTime == null) return null;
	
	return(new Array(aTime[1], aTime[2], aTime[3]));
}

function valiTime(timeBits) {
	if (timeBits == null) return false;

	var iHour = Number(timeBits[0]);
	var iMinute = Number(timeBits[1]);
	var sAMPM = timeBits[2];
	
	if (iHour < 1 || iHour > 12) return false;
	if (iMinute < 0 || iMinute > 59) return false;
	return true;
}

function dateComponents(dateStr) {
	var results = new Array();
	
/*	Added functionality for if dateStr is actually a date, not a string */
	if (dateStr.getDate) { // This is a date instead of a string
		results[0] = dateStr.getDate();
		results[1] = dateStr.getMonth() + 1;
		results[2] = dateStr.getFullYear();
		return results;
	}

/*	The following splits a date into day, month and year components
	Used with persmission from Jake Holwett, http://www.codestore.org */

	var datePat = /^(\d{1,2})([\-\/\.])(\d{1,2})\2(\d{2}|\d{4})$/;
	var matchArray = dateStr.match(datePat);

	if (matchArray == null) { 
		return null; 
	}
	//check for two digit years and prepend 20.
	matchArray[4] = (matchArray[4].length == 2) ? '20' + matchArray[4] : matchArray[4];

	// parse date into variables
	if (gsDateFormat.charAt(0)=="d"){ //what format does the server use for dates? 
		results[0] = Number(matchArray[1]);
		results[1] = Number(matchArray[3]);
	} else { 
		results[1] = Number(matchArray[1]);
		results[0] = Number(matchArray[3]); }
	results[2] = Number(matchArray[4]);
	return results;
}

function valiDate(dateBits) { // example usage: if (!valiDate(dateComponents(obj.value))) alert("Bad Date!")
	if (dateBits == null) return false;

	day = dateBits[0];
	month = dateBits[1];
	year = dateBits[2];

	if ((month < 1 || month > 12) || (day < 1 || day > 31)) { // check month range 
		return false;
	} 
	if ((month==4 || month==6 || month==9 || month==11) && day==31) {
		return false;
	}
	if (month == 2) {
		// check for february 29th 
		var isleap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); 
		if (day>29 || (day==29 && !isleap)) {
		return false;
		}
	} 
	return true;
}

function valiUSPhone(phonenumber) {
	// U.S. Phone validation from http://regexlib.com/REDetails.aspx?regexp_id=283 (added here by Jeremy)
	// This regular expression matches 10 digit US Phone numbers in different formats. Some examples are 1)area code in paranthesis. 2)space between different parts of the phone number. 3)no space between different parts of the number. 4)dashes between parts.
	if (phonenumber.match(/^\(?[\d]{3}\)?[\s-]?[\d]{3}[\s-]?[\d]{4}$/)==null) {
		return false; 
	} else {
		return true;
	}
}

function valiEmail(e) {
// Email validation from http://www.perlscriptsjavascripts.com/js/check_email.html
	ok = "1234567890qwertyuiop[]asdfghjklzxcvbnm.@-_QWERTYUIOPASDFGHJKLZXCVBNM";

	for(i=0; i < e.length ;i++) if(ok.indexOf(e.charAt(i))<0)return (false)

	re = /(@.*@)|(\.\.)|(^\.)|(^@)|(@$)|(\.$)|(@\.)/;
	re_two = /^.+\@(\[?)[a-zA-Z0-9\-\.]+\.([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
	if (!e.match(re) && e.match(re_two)) return (true);
	return (false)
}

function leading0s(iValue, iMinDigits) {
	var s = iValue.toString();
	while (s.length < iMinDigits) s = '0' + s;
	return s
}

function formatMMDDYYYY(aiDateBits) {
		if (gsDateFormat.charAt(0)=="d") //what format does the server use for dates?
			return leading0s(aiDateBits[0], 2) + "/" + leading0s(aiDateBits[1], 2) + "/" + aiDateBits[2]
		else
			return leading0s(aiDateBits[1], 2) + "/" + leading0s(aiDateBits[0], 2) + "/" + aiDateBits[2]
}

function registerValidationElement(o, sName, bRequired, nMax, nMin, sType) {
/* adds properties to o for later use in validateElement(), then adds o to the gaoValiElements array */
	o.valiName = (sName ? sName : o.name);
	o.valiType = (sType ? sType : o.type);
	o.valiMin = nMin;
	o.valiMax = nMax;
	if (bRequired) o.valiRequired = true;
	gaoValiElements[gaoValiElements.length] = o;
}

function standardValidation() {
	clearValidationMsgs();
	if (setValidationMsgs()) return true
	else {
		alert(validationMsg())
		if (goValiFirstInvalid) {
			var o = goValiFirstInvalid.onInvalid;
			if (!o) o = goValiFirstInvalid;
			if (o.focus) {
				o.focus()
				if (o.select) o.select()
			}
		}
		return false
	}
}

function clearValidationMsgs() {
	gsValiEmpty = "";
	gsValiNaN = "";
	gsValiNotDate = "";
	gsValiNotTime = "";
	gsValiNotDateTime = "";
	gsValiOther = "";
	goValiFirstInvalid = false
}

function setValidationMsgs() {
	var undefined;
	// don't forget to call clearValidationMsgs first!
	if (gaoValiElements.length == 0) registerValidationElements(); //if gaoValiElements not populated, populate it.
	
	for (var i=0; i < gaoValiElements.length; i++) if (!validateElement(gaoValiElements[i]) && !goValiFirstInvalid)
		goValiFirstInvalid = gaoValiElements[i];
	if (gsValiEmpty) return false;
	if (gsValiNaN) return false;
	if (gsValiNotDate) return false;
	if (gsValiNotTime) return false;
	if (gsValiNotDateTime) return false;
	if (gsValiOther) return false;
	return true;
}

function validationMsg() {
	var sMsg = "";
	
	if (gsValiEmpty) sMsg = "The following fields are required but empty:" + gsValiEmpty + "\n\n";
	if (gsValiNaN) sMsg += "The following numeric fields have invalid values:" + gsValiNaN + "\n\n";
	if (gsValiNotDate) sMsg += "The following date fields have invalid values:" + gsValiNotDate + "\n\n";
	if (gsValiNotTime) sMsg += 'The following time fields have invalid values:' + gsValiNotTime + '\n(valid time format is "hh:mm AM")\n\n';
	if (gsValiNotDateTime) sMsg += 'The following date-time fields have invalid values:' + gsValiNotDateTime + '\n(valid date-time format is "' + gsDateFormat + ' hh:mm AM")\n\n';
	if (gsValiOther) sMsg += "The following fields are invalid:" + gsValiOther;

	return sMsg;
}

function validateElement(o, sName, bRequired, vMax, vMin, sType) {
// only the first argument is required if registerValidationElement was called.
	var r; // regular expression
	var val; // o.value
	var i; //counter
	var iLen; //val.length
	var undefined;
	var sTime = undefined; // time portion of val, used for datetime types of elements
	var sTimeStartMsg = ''; // part of the error message if invalid datetime value
	var sTimeEndMsg = '';
	
	
	if (o === undefined) {
		alert("Error: object argument passed to validateElement is undefined.\n(" + sName + ")");
		return false;
	}

// if any parameters aren't supplied, check for the properties that should have been set during registerValidationElement
	if (sName === undefined) sName = (o.valiName) ? o.valiName : o.name;
	if (bRequired === undefined) bRequired = o.valiRequired;
	if (vMin === undefined) vMin = o.valiMin;
	if (vMax === undefined) vMax = o.valiMax;
	if (sType === undefined) sType = (o.valiType) ? o.valiType : o.type;

// set val and bEmpty	
	switch(sType) {
		case "select-one" :
			val = o.selectedIndex; // use selectedIndex property, if available
			bEmpty = (val < 1); // 0 or -1, with assumption that first choice is blank.
			break;
		case "select-multiple" : 
			val = o.selectedIndex; // use selectedIndex property, if available
			bEmpty = (val == -1);
			break;
		case "text" :
		case "number" :
		case "integer" :
		case "percent" :
		case "date" :
		case "time" :
		case "datetime" :
		case "usphone" :
		case "email" :
		case "textarea" :
			val = o.value
			bEmpty = (val.length == 0);
			break;
		case "radio" :
		case "checkbox" :
			bEmpty = true;
			for (i = 0; i < o.length; i++) {
				if (o[i].checked) {
					val = i;
					bEmpty = false;
					break;
				}
			}
			break;
		default :
			alert("Error: validateElement doesn't know how to deal with valiType = " + sType + "\n(" + sName + ")");
			return false;
	}
// if bEmpty is true, immediately return false or true, depending on if it's required or not.
	if (bEmpty) {
		if (bRequired) {
			gsValiEmpty += gsValiPrefix + sName;
			return false;
		} else return true
	}
// now for the real validation	
	switch (sType) {
		case "text" :
		case "textarea" :
			iLen = val.length;
			if (isNumOutOfRange(iLen, vMin, vMax)) {
			
				if (vMin == null)
					gsValiOther += gsValiPrefix + sName + " must be less than " + vMax + " characters long."
				else if (vMax == null)
					gsValiOther += gsValiPrefix + sName + " must be at least " + vMin + " characters long."
				else if (vMax == vMin)
					gsValiOther += gsValiPrefix + sName + " must be " + vMin + " characters long."
				else
					gsValiOther += gsValiPrefix + sName + " must be between " + vMin + " and " + vMax + " characters long.";

				return false;
			}
			return true;

		case "usphone" :
			if (valiUSPhone(o.value)) return true
			else {
				gsValiOther += gsValiPrefix + sName + " requires a valid phone number: (111) 111-1111";
				return false
			}

		case "email" :
			if (valiEmail(o.value)) return true
			else {
				gsValiOther += gsValiPrefix + sName + " requires a valid email address: username@domain.com";
				return false
			}

		case "percent" :
		case "number" :
		case "integer" : 
			if (sType == "integer")	r = /[, ]/g; // regexp set to look for all commas and spaces.
			else r = /[$,% ]/g; // regexp set to look for all dollar signs, commas, percent signs, and spaces.
			val = val.replace(r,"") // strip them out
			if (isNaN( val )) {
				gsValiNaN += "\n     -- " + sName;
				return false
			} else if (isNumOutOfRange(val, vMin, vMax)) {
				if (vMin == null)			gsValiOther += gsValiPrefix + sName + " must be less than or equal to " + vMax
				else if (vMax == null)	gsValiOther += gsValiPrefix + sName + " must be at least " + vMin
				else 						gsValiOther += gsValiPrefix + sName + " must be between " + vMin + " and " + vMax;
				
				return false
			} else if (sType == "integer" && val != parseInt(val)) {
				gsValiOther += gsValiPrefix + sName + " must be an integer.";
				return false;
			}
			if (sType == "percent") o.value = formatPercent(val); // make sure a % is within the field.  Domino will treat '5' as 500%
			return true;

		case "datetime" :
			var i = val.indexOf(' ');
			if (i != -1) {
				sTime = val.substr(i + 1);
				if (valiTime(timeComponents(sTime))) {
					val = val.substr(0, i)
				} else {
					gsValiNotDateTime += gsValiPrefix + sName;
					return false;
				}
			}
			sTimeStartMsg = (sType != 'datetime' ? '' : ' 12:00 AM');
			sTimeEndMsg = (sType != 'datetime' ? '' : ' 11:59 PM');
	
			// note: no break

		case "date" :
			var aiDateBits = dateComponents(val);
			if (aiDateBits == null) {
				if (sType=='date') gsValiNotDate += gsValiPrefix + sName
				else gsValiNotDateTime += gsValiPrefix + sName;
				return false;
			}
			//Check it is a valid date first
			if (valiDate(aiDateBits) == false) {
				if (sType=='date') gsValiNotDate += gsValiPrefix + sName
				else gsValiNotDateTime += gsValiPrefix + sName;
				return false;
			}
			//Now check whether a range is specified and if in bounds
			var theDate = new Date(aiDateBits[2], parseInt(aiDateBits[1]) - 1, aiDateBits[0]);
			if ( vMin ) {
				var minBits = dateComponents (vMin);
				for (i = 2; i > -1; i--) {
					if (minBits[i] < aiDateBits[i]) break // if year is before, stop.  Don't compare month numbers.
					else if (minBits[i] > aiDateBits[i]) {
						if ( vMax )	gsValiOther += gsValiPrefix + sName + " must be between " + formatMMDDYYYY(minBits) + sTimeStartMsg + " and " + formatMMDDYYYY(dateComponents(vMax)) + sTimeEndMsg 
						else gsValiOther += gsValiPrefix + sName + " must be on or after " + formatMMDDYYYY(minBits) + sTimeStartMsg 
						return false;
					}
				}
			} 
			if ( vMax) {
				var maxBits = dateComponents (vMax);
				for (i = 2; i > -1; i--) {
					if (maxBits[i] > aiDateBits[i]) break
					else if (maxBits[i] < aiDateBits[i]) {
						if ( vMin )	gsValiOther += gsValiPrefix + sName + " must be between " + formatMMDDYYYY(minBits) + sTimeStartMsg  + " and " + formatMMDDYYYY(maxBits) + sTimeEndMsg 
						else		gsValiOther += gsValiPrefix + sName + " must be on or before " + formatMMDDYYYY(maxBits) + sTimeEndMsg
						return false;
					}
				}
			}
			o.value = formatMMDDYYYY(aiDateBits)
			if (sTime !== undefined) o.value += ' ' + sTime
			return true;
		

			
		//--------------------MY CODE-------------------------------------
		case "time" :
			sTime = val;
			if (valiTime(timeComponents(sTime))) {
			} else {
				gsValiNotTime += gsValiPrefix + sName;
				return false;
			}
		//--------------------MY CODE-------------------------------------
		//I ALSO ADDED THE gsValiNotTime VARIABLE IN DIFFERENT PLACES


		case "checkbox" :
			if (vMin || vMax) { // don't bother checking if no need.
				var iChecked = 1;
				for (i = val + 1; i < o.length; i++) if (o[i].checked) iChecked++
				if (isNumOutOfRange(iChecked, vMin, vMax)) {
					if (vMin == null)
						gsValiOther += gsValiPrefix + sName + " must have less than " + (vMax + 1) + " choices selected."
					else if (vMax == null)
						gsValiOther += gsValiPrefix + sName + " must have at least " + vMin + " choices selected."
					else if (vMax == vMin)
						gsValiOther += gsValiPrefix + sName + " must have " + vMin + " choices selected."
					else
						gsValiOther += gsValiPrefix + sName + " must have between " + vMin + " and " + vMax + " choices selected.";

					return false;
				}
			}
			return true;

		default : // select, radio, etc
			return true
	}
}