/*
Name: formValidator 1.52,  http://slingfive.com/pages/code/formValidator/
Desc: automates most basic form validation tasks, via custom attributes
Author: Rob Eberhardt, Slingshot Solutions, http://slingfive.com/
Created: ~2002
Revision History:
	2004-03-24: 1st public release; added "friendly" public interface procs; fixed new minLength bug
	2004-02-20: fixed minlength check when value is null (value+''.length)
	2004-01-26: fixed istime check, improved to handle shorthand times
	2004-01-22: fixed required & minValue/maxValue bugs
	

====CHECKS====
data types:
	date, datetime, time, boolean, numeric, integer, float
data formats:
	email, phone, ssn, postal code/zipcode, creditcard

====OTHER OPTIONS====
required, minlength, minValue, maxValue



====USAGE====
see formValidation.html demo

1.set datatype attribute on elements:  
		<input type="text" name="NumTeeth" datatype="numeric" />

2.set minValue/maxValue attributes on elements:  
		<input type="text" name="NumTeeth" datatype="numeric" minValue="0" />

3.set required attribute on required elements: 
		<input type="text" name="NumTeeth" datatype="numeric" required="true" />

4.call validateField to check datatype during data entry (optional): 
		<input type="text" name="NumTeeth" datatype="numeric" onchange="validateField(this)" />

5.call validateForm when ready to run full check: 
		form1.onsubmit = new function(){
			return validateForm(form1)
		}


*/


/*
================================================================
public interfaces
================================================================
*/

function validateForm(p_oForm){
	return fnValidateForm(p_oForm);
}
function validateField(p_oField){
	return fnCheckValidField(p_oField);
}
function showFailedField(p_oField, p_strErrMessage){
	return showErrField(p_oField, p_strErrMessage);
}









/*
================================================================
private interfaces
================================================================
*/


// loops through form, checks each field with fnCheckRequiredField && fnCheckValidField,
// raises err alerts if(needed, returns overall validation status as boolean
function fnValidateForm(oForm){
	for(var e=0; e<oForm.elements.length; e++){
		var elem = oForm.elements[e];
		if(!fnCheckRequiredField(elem)){
			return false;
		}else if(!fnCheckValidField(elem)){
			return false;
		}else if(!fnCheckMinLengthField(elem)){
			return false;
		}else if(!fnCheckValueRange(elem)){
			return false;
		}
	}
	return true;	
}

// check if(required field has any data;
function fnCheckRequiredField(oFld){
	if(oFld.getAttribute("required") && oFld.value == ""){
		showErrField(oFld, "(This field is required)");
		return false;
	}
	return true;
}

//check if field's data matches its datatype, also checks max/min values;
function fnCheckValidField(oFld){
	var strFldDataType, bReturn, strMsgDetail;
	strFldDataType = oFld.getAttribute("datatype");
	bReturn = true;
	if(strFldDataType && strFldDataType != "" && oFld.value && oFld.value != ""){
		switch(strFldDataType.toLowerCase()) {
			case "float":
			case "numeric":	{bReturn = isNumeric(oFld.value);
				strMsgDetail = '\r\n' + "(value must be numeric)";
				break;
			}
			case "integer":	{bReturn = isInteger(oFld.value);
				strMsgDetail = '\r\n' + "(value must be an integer [whole number])";
				break;
			}
			case "date":
			case "datetime": {bReturn = isDate(oFld.value);
				strMsgDetail = '\r\n' + "(value must be a date)";
				if(bReturn && (oFld.type == "text" || oFld.tagName == "TEXTAREA")){oFld.value = FormatShortDate(cDate(oFld.value))}
				break;
			}
			case "time": {bReturn = isTime(oFld.value);
				strMsgDetail = '\r\n' + "(value must be a time)";
				if(bReturn && (oFld.type == "text" || oFld.tagName == "TEXTAREA")){oFld.value = cTime(oFld.value)}
				break;
			}
			case "boolean": {bReturn = isBoolean(oFld.value);
				strMsgDetail = '\r\n' + "(value must be boolean [true/false])";
				if(bReturn && (oFld.type == "text" || oFld.tagName == "TEXTAREA")){oFld.value = cBool(oFld.value)}
				break;
			}
			case "email": {bReturn = isEmail(oFld.value);
				strMsgDetail = '\r\n' + "(value must be in format 'UserName@DomainName.xxx')";
				break;
			}
			case "phone": {bReturn = isPhone(oFld.value);
				strMsgDetail = '\r\n' + "(these formats will work: ###.###.####, (###) ###-####)";
				break;
			}
			case "ssn":		{bReturn = isSSN(oFld.value);
				strMsgDetail = '\r\n' + "(format as ###-##-#### or #########)";
				break;
			}
			case "creditcard":		{bReturn = isCreditCard(oFld.value);
				strMsgDetail = '\r\n' + "(enter a valid number formatted as ####-####-####-####)";
				break;
			}
			case "postalcode":
			case "zipcode":	{bReturn = isPostalCode(oFld.value); break;}
		}

		if(!fnCheckValueRange(oFld)){return false};	// check min/max values
	}

	
	if(!bReturn){showErrField(oFld, strMsgDetail)};
	return bReturn;
}



// check that field's data meets its minlength;
function fnCheckMinLengthField(oFld){
	var iMinLength = oFld.getAttribute("minlength");
	if(iMinLength!=null && isNumeric(iMinLength) && (oFld.value+'').length < cNum(iMinLength)){
		showErrField(oFld, "\r\n\(Enter at least " + iMinLength + " characters)");
		return false;
	}
	return true;
}


// check field's value against max/min values allowed
// no real datatype checking, simply rely on javascript's type assumptions
function fnCheckValueRange(oFld){
	if(oFld.value && oFld.value!=""){
		var minValue = oFld.getAttribute("minValue");
		var maxValue = oFld.getAttribute("maxValue");

		if(minValue && maxValue && (oFld.value < minValue || oFld.value > maxValue)){	// check full range 
			showErrField(oFld, "\r\n\(Value must be in the range " + minValue + " to " + maxValue + ")");
			return false;
		}else if(minValue && oFld.value < minValue){	// if checking min val
			showErrField(oFld, "\r\n\(Value must be " + minValue + " or higher)");
			return false;
		}else if(maxValue&& oFld.value > maxValue){	// check max val
			showErrField(oFld, "\r\n\(Value must be " + maxValue + " or lower)");
			return false;
		}
	}
	return true;
}


//=====================
// utils
//=====================



// raise error && explanation to user, highlight + focus field;
function showErrField(oFld, strMsgDetail){
	if(!strMsgDetail){strMsgDetail = ''}
	var strMsg, strElemHandle = fnGetFieldHandle(oFld);
	oFld.runtimeStyle.backgroundColor="#ff3030";
	if(oFld.tagName == "SELECT"){
		strMsg = "Please make a valid selection for '" + strElemHandle + "'." + '\r\n';
	}else{;
		strMsg = "Please enter valid data for '" + strElemHandle + "'." + '\r\n';
	}
	window.alert(strMsg + strMsgDetail, "Validation Error");
	oFld.runtimeStyle.backgroundColor="";
	window.setTimeout("document.all." + oFld.uniqueID + ".focus()", 0);
}

// gets Something to call the field, tries: 1.Title, 2.Name, 3:Id
function fnGetFieldHandle(oFld){
	var strElemHandle = oFld.title;
	if(strElemHandle == ""){strElemHandle = oFld.name;}
	if(strElemHandle == ""){strElemHandle = oFld.id;}
	return strElemHandle;
}




//=====================
// data-type checks
//=====================

function isDate(val){
	var dt = new Date(val);
	var dt1 = new Date(100,1,1);
	dt1.setFullYear(0);	// years <=0 are forced/assumed to 4 digit otherwise
	var dt2 = new Date(9999,12,31);
	return  dt1 < dt && dt < dt2;
}
function isDateTime(val){
	return isDate(val);	// ?
}
function isTime(val){
	var bRet = false;
	//TODO: check that >0
  if(val.length>1 || isInteger(val)){// single alpha chars get through Date.parse()
		if(val.length<=5 && val.search(/[ ]*[ap]m/gi)!=-1){val = val.replace(/([ ]*[ap]m)/gi, ':00$1');}	// has am/pm, add minutes

		var dt = new Date();
		var parsedMS = Date.parse('' + FormatShortDate2(dt) + ' ' + val);	// year can't be 1st
		dt.setTime(parsedMS);
		bRet = isDate(dt) || (isInteger(val) && val>=0 && val<24);
	}
	return bRet;
}

function isNumeric(val){
	return !isNaN(val);
}
function isInteger(val){
	return isNumeric(val) && val.indexOf(".")==-1;
}
function isFloat(val){
	return isNumeric(val);
}
function isBoolean(val){	// true/false, 'true/false', #s, y/n
	return val==true || ((''+val).toLowerCase()=="true" || (''+val).toLowerCase()=="false") || isNumeric(val) || (''+val).toLowerCase()=='y' || (''+val).toLowerCase()=='n';
}






//=====================
// data-format checks
//=====================


function isEmail(val){
// regex based on http://regexlib.com/REDetails.aspx?regexp_id=356
	var re = new RegExp(/^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/);
	return (re.exec(val)!=null);
}
function isSSN(val){
// regex based on http://www.regexlib.com/REDetails.aspx?regexp_id=535
	var re = new RegExp(/^(?!000)([0-6]\d{2}|7([0-6]\d|7[012]))([ -])?(?!00)\d\d\3(?!0000)\d{4}$/);
	return (re.exec(val)!=null);
}
function isPhone(val){
// regex based on http://regexlib.com/REDetails.aspx?regexp_id=245
	var re = new RegExp(/^([0-1]([\s-./\\])?)?(\(?[2-9]\d{2}\)?|[2-9]\d{3})([\s-./\\])?(\d{3}([\s-./\\])?\d{4}|[a-zA-Z0-9]{7})$/);
	return (re.exec(val)!=null);
}
function isPostalCode(val){
	//regex based on http://regexlib.com/REDetails.aspx?regexp_id=23
	var re = new RegExp(/^(\d{5}-\d{4}|\d{5}|[A-Z]\d[A-Z] \d[A-Z]\d)$/);
	return (re.exec(val)!=null);
}
function isCreditCard(val){
// regex based on http://regexlib.com/REDetails.aspx?regexp_id=67 (also considered 455, 49)
// "the four major credit cards"
	var re = new RegExp(/^((4|5)\d{3}-?\d{4}-?\d{4}-?\d{4}|(4|5)\d{15}|(6011)-?\d{4}-?\d{4}-?\d{4}|(6011)-?\d{12}|(3\d{3})-\d{6}-\d{5}|(3\d{14}))$/);
	return (re.exec(val)!=null);
}



/*  the OLD checks......

// hugely modified from code found at http;//www.4guysfromrolla.com/webtech/051999-1.shtml;
// checks validity of email address;
function isEmail2(theAddress){
	var bRet = true;
	var iAt, iLastDot;
	iAt = theAddress.indexOf("@");
	iLastDot = theAddress.lastIndexOf(".");
	//--- chk length; a@b.cd should be the shortest an address could be;
	//--- chk format has at least one "@" (with something before it);
	//--- has Only one "@";
	//--- has at least one ".",  After the "@",  && with something between;
	//--- has no more than 4 chars after last "." (like ".info");
	//--- has at least 2 chars after last "." (like ".us");
	if(theAddress.length<6  || iAt<2  || iAt!=theAddress.lastIndexOf("@")  || iLastDot-1<=iAt  || (len(theAddress) - iLastDot) > 4  || (theAddress.length-iLastDot)<2){
		bRet = false;
	//--- has no "_" after the "@";
	}else if(theAddress.indexOf("_")!=-1 && theAddress.lastIndexOf("_") > inStrRev(theAddress, "@")){
		bRet = false;
	}else{ //--- chk validity of each char (alphanumeric, || these; "_.@-" );
		for(var i=1; i< theAddress.length; i++){
			var curChar = theAddress.substring(i, 1);
			if(!isNumeric(curChar) && (curChar.toLowerCase()<"a" || lcase(curChar)>"z") &&  curChar!="_" && curChar!="." && curChar!="@" && curChar!="-"){
					bRet = false;
					break;
			}
		}
	}
	return bRet;
}

function isZipCode(val){
	//TODO; need validation logic here;
	if(val == ""){return false}
	return (isInteger(val) && val.length==5);
}


*/





//=====================
// data types
//=====================
function cNum(val){
	if(!isNumeric(val)){return val}
	return new Number(val);
}
function cInt(val){
	if(!isNumeric(val)){return val}
	return parseInt(val);
}
function cFloat(val){
	if(!isNumeric(val)){return val}
	return parseFloat(val);
}
function cBool(val){	// true/false, 'true/false', #s, y/n
	return val==true || (''+val).toLowerCase()=='true' || (''+val).toLowerCase()=='y' || (isNumeric(val) && val!=0);
}
function cDate(val){
	return new Date(val);	//.getVarDate()
}
function cTime(val){
	if(!isTime(val)){return val};

	if(isInteger(val) && val>=0 && val<24){val += ':00';}	//if single number, add minutes
	if(val.length<=5 && val.search(/[ ]*[ap]m/gi)!=-1){val = val.replace(/([ ]*[ap]m)/gi, ':00$1');}	// has am/pm, add minutes
	
	var dtq = new Date();
	var parsedMS = Date.parse('' + FormatShortDate2(dtq) + ' ' + val);	// year can't be 1st
	dtq.setTime(parsedMS);


	//get parts of time & prep for format
	var strHours = dtq.getHours() % 12;
	var strMinutes = '0'+ dtq.getMinutes();
	var strSeconds = '0'+ dtq.getSeconds();
	strMinutes = (strMinutes).substring(strMinutes.length-2);
	strSeconds = (strSeconds).substring(strSeconds.length-2);	
	var strMeridian = (dtq.getHours() >11 ? 'pm' : 'am');
	if(strHours==0){strHours=12};	// fix for our confused clock

	
	//assemble formatted time
	var strRet = '' + strHours + ':' + strMinutes + (strSeconds !=0 ? ':' + strSeconds : '') + ' ' + strMeridian;
	
	return strRet;
}




//=====================
// formatting 
//=====================


function FormatShortDate2(val) {
	if(!isDate(val)){return val};
	var dt = new Date(val);
//	var nFullyear = (''+val).substring(0,3);
//	if(isInteger(nFullyear)){dt.setFullYear(nFullyear)};	// fix 2 digit assumption
	var strRet = '' + (dt.getMonth()+1) + '/' + dt.getDate() + '/' + dt.getFullYear();
	return strRet;
}
function FormatShortDate(val) {
	if(!isDate(val)){return val};
	var dt = new Date(val);
//	var nFullyear = (''+val).substring(0,3);
//	if(isInteger(nFullyear)){dt.setFullYear(nFullyear)};	// fix 2 digit assumption
	var strRet = '' + dt.getFullYear() + '/' + (dt.getMonth()+1) + '/' + dt.getDate();
	return strRet;
}







//=====================
// backwards-compatibility
//=====================
function subShowErrField(oFld, strMsgDetail) {
	return showErrField(oFld, strMsgDetail);
}
