// File Name:	validator.js
//
// Author:		Steve Lincoln, Senior Technologist at InfoSphere, Inc.
//
// Dynamic field validation JavaScript.
//  
// Fields are validated at three different times:
//   1. Keystrokes may be filtered within a field.
//   2. Field is validated in onChange event.
//   3. All fields are validated in onSubmit event.
//
// Adapted from original work by Matthew Frank at www.siteexperts.com.
// See http://www.siteexperts.com/tips/functions/ts21/page1.asp.
//
// Supported Form object properties:
//   boolean VALIDATE   - are form fields to be validated? 
//   boolean NOTRIM     - should form fields be left untrimmed?
//   String  VALBUTTONS - list of "submit" and "image" buttons for which validation
//						  will be done.  If this property is not specified for a form,
//						  all "submit" and "image" buttons will perform validation
//
// Supported Form Element object properties:
//   String  NAME       - name displayed in field validation failure messages
//   Boolean REQUIRED   - is the field required?
//   String  REQUIRED_MSG - message displayed when required field is empty
//   number  MIN_LENGTH - minimum number of characters for the field
//   number  MAX_LENGTH - maximum number of characters for the field
//   String  DEFAULT    - default value when none is entered
//   Boolean INTEGER    - is the field an integer?
//   Boolean SIGNED     - is the field signed?
//   Boolean FLOAT	    - is the field a floating point number
//   Boolean MONEY      - is the field a monetary amount
//   number  MIN	    - minimum value for the field
//   number  MAX        - maximum value for the field
//   Boolean ZIP        - is the field a US ZIP code?
//   Boolean TIME24	    - is the field a military (24-hour) time?
//   Boolean EMAIL      - is the field an e-mail address?
//   Boolean TELEPHONE  - is the field a telephone number?
//   Boolean DATE       - is the field a date?
//   RegExp  REGEXP     - regular expression to validate field
//   String  REGEXP_MSG - message displayed when field fails REGEXP validation
//   RegExp  FILTER     - regular expression to filter keystrokes
//   String  AND        - comma-separated list of additional required fields
//   String  AND_MSG    - message displayed when field fails AND validation
//   String  OR         - comma-separated list of "one or more of" fields
//   String  OR_MSG     - message displayed when field fails OR validation
//   Boolean NOVALONSUBMIT - bypass validation(field onchange) on form submission,
//                           for Select objects with 'onChange=submit()' event.
//
// Supported Form Button oject properties:
//   Boolean NOVAL      - bypass all field validation when button is pressed,
//                        for Cancel buttons.
//
// Programmer Notes:
// This script replaces the onChange events for fields.  Any existing onChange
// logic that was specified for fields is executed after the field validation
// logic.  The same is true for form onSubmit events.  Also, the onKeyPress
// event is overridden on fields which have implicit (INTEGER, FLOAT, MONEY,
// TIME24, ZIP, DATE) or explicit (FILTER) keystroke filters. Field validation is
// bypassed if the field's disabled or readonly attribute is set.
//
// Since JavaScript variables are untyped, I am using a prefix to indicate
// the type: i = integer, b = boolean, o = object, s = string, a = array.

// Display an appropriate validation failure alert box.
function val_alert(oElement, sType) {
	oElement.form.submitting = false;
	switch (sType) {
		case "REQUIRED":
			if (oElement.REQUIRED_MSG) alert(oElement.REQUIRED_MSG);
			else if (oElement.NAME) alert(oElement.NAME+" is required.");
			else alert("This field is required.");
			break;
		case "INTEGER":
		case "FLOAT":
			if (oElement.NAME) alert(oElement.NAME+" is not a valid number.");
			else alert(oElement.value+" is not a valid number.");
			break;
		case "MONEY":
			if (oElement.NAME) alert(oElement.NAME+" is not a valid monetary amount.\nExamples of valid monetary amount: \n\t0.34 \n\t24.56");
			else alert(oElement.value+" is not a valid monetary amount.\nExamples of valid monetary amount: \n\t0.34 \n\t24.56");
			break;	
		case "REGEXP":
			if (oElement.REGEXP_MSG) alert(oElement.REGEXP_MSG);
			else if (oElement.NAME) alert(oElement.NAME+" is not valid.");
			else alert(oElement.value+" is not valid.");
			break;
		case "ZIP":
			if (oElement.NAME) alert(oElement.NAME+" is not in a valid ZIP code format.");
			else alert(oElement.value+" is not in a valid ZIP code format.");
			break; 	
		case "DATE":
			if (oElement.NAME) alert(oElement.NAME+" is not in a valid date format (MM/DD/YYYY).");
			else alert(oElement.value+" is not in a valid date format (MM/DD/YYYY).");
			break;
		case "MIN_MAX":
			if (oElement.NAME) alert(oElement.NAME+" must be between "+oElement.MIN+" and "+oElement.MAX+".");
			else alert("This must be between "+oElement.MIN+" and "+oElement.MAX+".");
			break;
		case "TIME24":
			if (oElement.NAME) alert(oElement.NAME+" is not a valid time.");
			else alert(oElement.value+" is not a valid time.");
			break;
		case "EMAIL":
			if (oElement.NAME) alert(oElement.NAME+" is not a valid e-mail address.");
			else alert("This is not a valid e-mail address.");
			break;	
		case "AND":
			if (oElement.AND_MSG) alert(oElement.AND_MSG);
			else alert("Other field(s) is/are required with this one.");
			break;
		case "OR":
			if (oElement.OR_MSG) alert(oElement.OR_MSG);
			else alert("This or some other field(s) is/are required.");
			break;
		case "MIN_LENGTH":
			var l = oElement.value.length;
			if (oElement.NAME) alert(oElement.NAME+" must be at least "+oElement.MIN_LENGTH+" characters long.  You only entered "+l+" characters.");
			else alert("This field must be at least "+oElement.MIN_LENGTH+" characters long.  You only entered "+l+" characters.");
			break;	
		case "MAX_LENGTH":
			var l = oElement.value.length;
			if (oElement.NAME) alert(oElement.NAME+" cannot be longer than "+oElement.MAX_LENGTH+" characters.  You entered "+l+" characters.");
			else alert("This field cannot be longer than "+oElement.MAX_LENGTH+" characters.  You entered "+l+" characters.");
			break;	
		case "TELEPHONE":
			if (oElement.NAME) alert(oElement.NAME+" is not in a valid Telephone format. A valid Telephone number follows the format (817) 555-1212.");
			else alert(oElement.value+" is not in a valid Telephone format. A valid Telephone number follows the format (817) 555-1212.");
			break; 	
	}
	if(oElement.type != "hidden") {
        if (oElement.focus) oElement.focus(); // put the cursor on it
        if (oElement.select) oElement.select(); // highlight it
    }
	return false;				
}
// Validate all form fields
function val_onsubmit() { // "this" is the form calling onsubmit
	// Don't validate the fields if the form's VALIDATE property is not set.
	if (!this.VALIDATE) 
		if (this._onsubmit) return this._onsubmit();
		else return true;
	// Validate all the form's fields by executing each field's onchange(). 
    var i,j;
	var bSuccess = true;
	this.submitting = true;
	for (i=0; i<this.elements.length; i++) {
		var oElement = this.elements[i];
		if (oElement.NOVAL) continue;
		var sDefault = oElement.DEFAULT;
		var sValue = oElement.valueOf();
		if (sDefault && !sValue) oElement.value = sDefault;
		else if (!oElement.NOVALONSUBMIT) bSuccess = oElement.onchange();
		if (bSuccess==false) break; // report only the first error detected
	}
	if (bSuccess==false) return false;
	// All individual field validations passed.  
	// Time to check inter-field validations (AND and OR).
	for (i=0; i<this.elements.length; i++) {
		var oElement = this.elements[i];
		if (oElement.NOVAL) continue;
		var sValue = oElement.valueOf();
		if (sValue) sValue = sValue.trim();
		// AND (a comma-separated string of field names on the same form)
		if (oElement.AND && sValue) {
			var aAnd = oElement.AND.split(/,/); // delimited String to Array conversion
			for (j=0; j<aAnd.length; j++) {
				var oElement2 = this.elements[aAnd[j]];
				var sValue2 = oElement2.valueOf();
				if (sValue2) sValue2 = sValue2.trim();
				if (oElement2 && !sValue2) return val_alert(oElement,"AND");
			}
		}
		// OR (a comma-separated string of field names on the same form)
		if (oElement.OR && !sValue) {
			var aOr = oElement.OR.split(/,/); // delimited String o Array conversion
			var bFoundOne = false;
			for (j=0; j<aOr.length; j++) {
				var oElement2 = this.elements[aOr[j]];
				var sValue2 = oElement2.valueOf();
				if (sValue2) sValue2 = sValue2.trim();
				if (sValue2) { bFoundOne = true; break; }
			}
			if (!bFoundOne) return val_alert(oElement,"OR");
		}
	}
	// all field and inter-field validations passed !
	if (this._onsubmit) return this._onsubmit();
	else return true;	
}
// Validate a form field.
function val_onchange() { // "this" is the element being validated
	if (this.disabled || this.readonly) return true;
	// trim leading and trailing spaces from field, unless form.NOTRIM is set.
	if (this.value)
		if (!this.form.NOTRIM) this.value = this.value.trim();
	var sValue = this.valueOf(); // this.value is not robust enough
	if (sValue) sValue = sValue.trim();
	// REQUIRED
	if (this.REQUIRED && !sValue)
		return val_alert(this,"REQUIRED");
	// MIN_LENGTH
	if (this.MIN_LENGTH && sValue)
		if (sValue.length<this.MIN_LENGTH*1) return val_alert(this,"MIN_LENGTH");
	// MAX_LENGTH
	if (this.MAX_LENGTH && sValue)
		if (sValue.length>this.MAX_LENGTH*1) return val_alert(this,"MAX_LENGTH");		
	// INTEGER
	if (this.INTEGER && sValue) {
		if (this.SIGNED)
			if (!/^[+-]?\d+$/.test(sValue)) return val_alert(this,"INTEGER");
			else;
		else if (!/^\d+$/.test(sValue)) return val_alert(this,"INTEGER");
	}
	// MONEY
	if (this.MONEY && sValue) {
		if (this.SIGNED)
			if (!/^[+-]?\d*(\.\d{1,2})?$/.test(sValue)) return val_alert(this,"MONEY");
			else;
		else if (!/^\d*(\.\d{1,2})?$/.test(sValue)) return val_alert(this,"MONEY");
	}		
	// FLOAT
	if (this.FLOAT && sValue) {
		if (this.SIGNED)
			if (!/^[+-]?\d+\.?\d*$/.test(sValue)) return val_alert(this,"FLOAT");
			else;
		else if (!/^\d+\.?\d*$/.test(sValue)) return val_alert(this,"FLOAT");
	}
	// EMAIL
	if (this.EMAIL && sValue)
		if (!/^[\w_-]+(\.[\w_-]+)*@[\w_-]+(\.[\w_-]+)*\.\w{2,4}$/.test(sValue)) return val_alert(this,"EMAIL");																
	// REGEXP
	if (this.REGEXP && sValue) {
		if (typeof this.REGEXP=="string") {		
			var re = new RegExp(this.REGEXP);
			if (!re.test(sValue)) return val_alert(this,"REGEXP");
		} else if (!this.REGEXP.test(sValue)) return val_alert(this,"REGEXP");
	}
	// ZIP
	if (this.ZIP && sValue)
		if (!/^\d{5}$|^\d{5}-\d{4}$/.test(sValue)) return val_alert(this,"ZIP");
	// DATE
	if (this.DATE && sValue)
		if (!/^[01]?\d[\/\-.][0123]?\d[\/\-.]\d{4}$/.test(sValue)) return val_alert(this,"DATE");
	// UPPERCASE
	if (this.UPPERCASE && this.value) this.value = this.value.toUpperCase();
	// MIN
	if (this.MIN && sValue)
		if (sValue*1 < this.MIN*1) return val_alert(this,"MIN_MAX");
	// MAX
	if (this.MAX && sValue)
		if (sValue*1 > this.MAX*1) return val_alert(this,"MIN_MAX");
	// TIME24
	if (this.TIME24 && sValue)
		if (!/^[01]?\d:?[0-5]\d:?[0-5]\d$/.test(sValue) && 
			!/^2[0-3]:?[0-5]\d:?[0-5]\d$/.test(sValue)) return val_alert(this,"TIME24");
	// TELEPHONE
	if (this.TELEPHONE && sValue)
		if (!/^(\d{3}|\(\d{3}\))[-._ ]?\d{3}[-._ ]+\d{4}$/.test(sValue)) return val_alert(this,"TELEPHONE");
	// all the field validations passed !
	if (this._onchange && !this.form.submitting) return this._onchange();
	else return true;				
}
// Filter a field's keystrokes.		
function val_iekeypress() { // "this" is the element being filtered
	var sKey = String.fromCharCode(event.keyCode);
	var re = (typeof this.FILTER=="string") ? new RegExp(this.FILTER) : this.FILTER;	
	if (sKey!="\r" && !re.test(sKey)) {
		event.returnValue = false;
		return false;
	}
}
function val_nskeypress(e) { // "this" is the element being filtered
	var sKey = String.fromCharCode(e.which); // "e" is the event argument passed
	if (sKey!="\r" && sKey!="\b") {
		var re = (typeof this.FILTER=="string") ? new RegExp(this.FILTER) : this.FILTER;
		return re.test(sKey);
	}
}
// Return the field's value.
function valueOf() {
	switch (this.type) {
		case "text": 
		case "textarea": 
		case "file": 
		case "password": 
		case "hidden": return this.value; break;
		case "radio":
		case "checkbox":
			if (this.checked) return (this.value)?this.value:true;
			else return null;
			break;
		case "select-one":
			if (this.selectedIndex < 0) {
				if (this.options[0]==null) return null; // in case of an empty list
				return this.options[0].value; // browser bug fix
			}
			else return this.options[this.selectedIndex].value; break;
		// What about "select-multiple" ???
		default: return null;
	}
}
// If the element is not in the list of VALBUTTONS, disable field validations in
// its onclick event.
function val_button(x) {
	var inList = false;
	for (i=0; i<x.form.aButtons.length; i++) {
		if (x.name==x.form.aButtons[i]) {
			inList = true; break;
		}
	}
	if (!inList)
		x.onclick = function () { x.form.VALIDATE=false; };
}
// Loop through forms and fields, replacing onsubmit, onchange, onkeypress methods.
if (document.forms && window.RegExp) { // make sure we can do regular expressions (JS1.2)
	for (i=0; i<document.forms.length; i++) {
		oForm = document.forms[i];
		// if the form's VALIDATE attribute is set ...
		if (oForm.VALIDATE) {
			// replace the form's onsubmit method
			oForm._onsubmit = oForm.onsubmit;
			oForm.onsubmit = val_onsubmit;
			oForm.submitting = false;
			if (oForm.VALBUTTONS) oForm.aButtons = oForm.VALBUTTONS.split(/,/);
			for (j=0; j<oForm.elements.length; j++) {
				oElement = oForm.elements[j];
				// If a non-submit element has NOVAL set, leave its methods alone.
				if (oElement.type != "submit" && oElement.NOVAL) continue;
				// replace the element's onchange method
				oElement._onchange = oElement.onchange;
				oElement.onchange = val_onchange;
				// add a valueOf method to the element
				oElement.valueOf = valueOf;
				// put an implicit filter on INTEGER elements	
				if (oElement.INTEGER)
					if (oElement.SIGNED) oElement.FILTER = /[0-9+-]/;
					else oElement.FILTER = /[0-9]/;
				// put an implicit filter on TIME24 elements
				if (oElement.TIME24) oElement.FILTER = /[0-9:]/;
				// put an implicit filter on MONEY and FLOAT elements
				if (oElement.FLOAT || oElement.MONEY)
					if (oElement.SIGNED) oElement.FILTER = /[0-9+-\.]/;
					else oElement.FILTER = /[0-9\.]/;
				// put an implicit filter on ZIP elements
				if (oElement.ZIP) oElement.FILTER = /[0-9-]/;
				// put an implicit filter on DATE elements
				if (oElement.DATE) oElement.FILTER = /[0-9\/\-.]/;
				// put an implicit filter on EMAIL elements
				if (oElement.EMAIL) oElement.FILTER=/[\w_@\.-]/;	
				// put an implicit filter on TELEPHONE elements
				if (oElement.TELEPHONE) oElement.FILTER=/[\(\)\d-._ ]/;	
				// replace the element's onkeypress method if its FILTER attribute is set,
				// either implicitly or explicitly.
				if (oElement.FILTER)
					if (document.all) // IE only
						oElement.onkeypress = val_iekeypress;
					else // Netscape
						oElement.onkeypress = val_nskeypress;
				// If "submit" button has NOVAL property, disable field validations on submit.
				if (oElement.type=="submit" && oElement.NOVAL)
					oElement.onclick = function () { this.form.VALIDATE=false; };
				// If form has VALBUTTONS property, disable field validations on "submit"
				// buttons that are NOT in the list.
				if (oForm.VALBUTTONS && oElement.type=="submit") val_button(oElement);
				// if the element has a DEFAULT value, set it to it now.
				var sDefault = oElement.DEFAULT;
				if (sDefault && !oElement.value) oElement.value = sDefault;
			}
			// If the form has VALBUTTONS property, find all "<input type=image>" elements
			// and disable field validations for those that are NOT in the list.  For some
			// reason, "input type=image" elements are specifically excluded from the
			// document.forms[].elements[] collection, nor are they in the document.images[]
			// collection.  So, we have to use document.getElementsByTagName() to find them.
			if (oForm.VALBUTTONS) {
				var cElements = document.getElementsByTagName("INPUT");
				for (k=0; k<cElements.length; k++) {
					var oElement = cElements.item(k);
					if (oElement.type=="image") val_button(oElement);
				}
			}	
		}
	}
	// Add a trim method to String objects.
	String.prototype.trim = function () {
		return this.replace(/^\s+/,"").replace(/\s+$/,"");
	}
}	
