/*
 *  FormValidator.js
 *  2009 06 18 - Brandon McKinney
 
 *  Provides form validation based on advanced rules (regex).
 
 	VALIDATION RULES
 	required	-	Requires a value of some kind in the field
 	numbers		-	Requires a value to be numeric (can contain a . though)
 	letters		-	Requires only letters in the value
 	<#			-	Field value (numeric) is less than #
 	>#			-	Field value (numeric) is greater than #
 	length=#	-	Field value length is exactly # characters long
 	length<#	-	Field value length is less than # characters long
 	length>#	-	Field value length is greater than # characters long
 	depend=		-	Field is required if other field(s) filled in. Accepts a comma-separated list of form field names.
 	format=0A*	-	Field value must match the format provided. (ex. 000-000-0000 must match 3 #'s, a dash, 3 #'s, a dash and 4 #'s). Use '0' for numeric; 'A' for a-z (case-insensitive).
	regex=		-	Field value matched against a provided regular expression.
	
	RULES FOR PRE-PROCESSING FIELD FORMATTING (convert to ucase, etc.)
	upper		-	Convert to UPPERCASE
	lower		-	Convert to lowercase
	padleft=#	-	Add # of spaces to the beginning
	padright=#	-	Add # of spaces to the end
	pad=#		-	Add # of spaces to the beginning and end
	reverse		-	Reverse the string (reverse -> esrever)
 */



/*
 *  BEGIN FormValidator CLASS
 */
function FormValidator(frmName){
	this.frm = document.forms[frmName];
	this.fields = new Array();
	this.dupFields = new Array();
	this.respObj = new Object;

	this.loadFormFields();
	this.initDuplicates();
}



// CHANGE THE NAME OF THE FORM USED AND RELOAD THE FIELDS
FormValidator.prototype.setFormName = function(fname){
	this.frm = document.forms[fname];
	this.loadFormFields();
};



// LOAD AN ARRAY CONTAINING THE FORM FIELD ELEMENTS (INPUT,TEXTAREA,SELECT)
FormValidator.prototype.loadFormFields = function(){
	this.fields = new Array();
	var htmlFormTags = ['input', 'textarea', 'select'];
	for(i=0; i<htmlFormTags.length; i++){
		var tags = this.frm.getElementsByTagName(htmlFormTags[i]);
		for(ii=0; ii<tags.length; ii++){
			this.fields.push(tags[ii]);
		}
	}
};



// UTILITY FUNCTION TO ADD A CLASS TO A PASSED ELEMENT
FormValidator.prototype.addClass = function(el, cl){
	el.className = el.className + ' ' + cl;
};



// UTILITY FUNCTION TO REMOVE A CLASS FROM A PASSED ELEMENT
FormValidator.prototype.removeClass = function(el, cl){
	el.className = el.className.replace(new RegExp('[ ]*'+cl, 'g'), '');
};



// FIND DUPLICATABLE GROUPS IN THE FORM AND ADD A BUTTON TO DUPLICATE THEM
FormValidator.prototype.initDuplicates = function(){
	var dups = getChildrenByClass(this.frm, 'duplicatable');
	for (i=0; i<dups.length; i++){
		dups[i].innerHTML = '<div class="duplicatableRow">' + dups[i].innerHTML + '</div><!--dupEnd-->';
		this.dupFields[dups[i].id] = dups[i].innerHTML;
	}
};



// THE USER TRIGGERED A DUPLICATE, SO DUPLICATE IT
FormValidator.prototype.makeDuplicate = function(fieldID){
	var frmVals = new Array();
	var frmTags = this.frm.getElementsByTagName('*');
	for (i=0; i<frmTags.length; i++){
		if (frmTags[i].name){
			switch (frmTags[i].nodeName.toLowerCase()){
				case 'input':
				case 'textarea':
				case 'select':
					var ar = new Array();
					ar['name'] = frmTags[i].name;
					ar['val'] = frmTags[i].value;
					frmVals.push(ar);
			}
		}
	}
	var dupFieldContainer = document.getElementById(fieldID);
	var childs = getChildrenByClass(dupFieldContainer, 'duplicatableRow');
	var rowcnt = (childs.length*1) + 1;
	var newfield = this.dupFields[fieldID];
	newfield = newfield.replace(/(name[\s]*=[\s]*['"]?[a-zA-Z0-9_-]+)(['"]? )/i, '$1' + rowcnt + '$2');
	dupFieldContainer.innerHTML += newfield;
	this.loadFormFields();
	for (i=0; i<frmVals.length; i++){
		var obj = eval('this.frm.' + frmVals[i]['name']);
		obj.value = frmVals[i]['val'];
	}
};



// PERFORM THE VALIDATION AND GENERATE A RESPONSE OR SUBMIT THE FORM
FormValidator.prototype.validate = function(respObjID){
	var useAlert = false;
	if (typeof(respObjID) == 'undefined'){ 
		useAlert = true;
	} else {
		this.respObj = document.getElementById(respObjID);
	}

	if (this.fields.length > 0){
		var err = '';
		for(i=0; i<this.fields.length; i++){
			var hasErr = false;
			if (this.fields[i].getAttribute('rule')){
				var rules = this.fields[i].getAttribute('rule').replace(/[^\\];$/, '');
				if (rules){
					// replace escaped semicolons with a code
					rules = rules.replace(/\\;/, '~~semicolon~~');
					// split rules on semicolons
					var ruleArr = rules.split(';');
					var hasLen = false;
					if (this.fields[i].value.length > 0) hasLen = true;

					// for each rule
					for(ri=0; ri<ruleArr.length; ri++){
						rKey = ruleArr[ri].replace(/^[ \t\r\n]+|[ \t\r\n]+$/, '');
						// re-insert the semicolons
						rKey = rKey.replace(/~~semicolon~~/, ';');

						/*
						 * RULES LIST
						 */
						// if required
						if (rKey.match(/^required$/i)){
							if (!hasLen) {
								err += "\n" + this.fields[i].getAttribute('label') + " is required.\n";
								hasErr = true;
							}



						// if numbers
						} else if (rKey.match(/^numbers$/i)){
							if (this.fields[i].value.match(/[^0-9\.]/)){
								err += "\n" + this.fields[i].getAttribute('label') + " must be a number value.\n";
								hasErr = true;
							}



						// if letters
						} else if (rKey.match(/^letters$/i)){
							if (this.fields[i].value.match(/[^a-zA-Z]/)){
								err += "\n" + this.fields[i].getAttribute('label') + " must contain only letters.\n";
								hasErr = true;
							}



						// if <  (assumes numeric)
						} else if (rKey.match(/^<[0-9]+/ig) && hasLen){
							rVal = rKey.replace(/^</, '');
							if (this.fields[i].value >= rVal) {
								err += "\n" + this.fields[i].getAttribute('label') + " must be less than " + rVal + ".\n";
								hasErr = true;
							}



						// if >  (assumes numeric)
						} else if (rKey.match(/^>[0-9]+/ig) && hasLen){
							rVal = rKey.replace(/^>/, '');
							if (this.fields[i].value <= rVal) {
								err += "\n" + this.fields[i].getAttribute('label') + " must be greater than " + rVal + ".\n";
								hasErr = true;
							}



						// if length=  (treats as text)
						} else if (rKey.match(/^length=[0-9]+/ig) && hasLen){
							rVal = rKey.replace(/^length=/, '');
							if (this.fields[i].value.length != rVal) {
								err += "\n" + this.fields[i].getAttribute('label') + " must be " + rVal + " characters long.\n";
								hasErr = true;
							}



						// if length>  (treats as text)
						} else if (rKey.match(/^length>/ig) && hasLen){
							rVal = rKey.replace(/^length>/, '');
							if (this.fields[i].value.length <= rVal){
								err += "\n" + this.fields[i].getAttribute('label') + " must be at least " + rVal + " characters long.\n";
								hasErr = true;
							}



						// if length<  (treats as text)
						} else if (rKey.match(/^length</ig) && hasLen){
							rVal = rKey.replace(/^length</, '');
							if (this.fields[i].value.length >= rVal){
								err += "\n" + this.fields[i].getAttribute('label') + " must be fewer than " + rVal + " characters long.\n";
								hasErr = true;
							}



						// if depend=  (field required if another field or set of fields are filled in)
						} else if (rKey.match(/^depend=/ig)){
							var df = rKey.replace(/^depend=/, '').split(',');
							var isRequired = true;
							for (x=0; x<df.length; x++){
								var obj = eval('this.frm.'+df[x].replace(/^[ \t\r\n]+|[ \t\r\n]$/, ''));
								if (!obj.value || this.fields[i].value){
									isRequired = false;
								}
							}
							if (isRequired){
								err += "\n" + this.fields[i].getAttribute('label') + " is required with your current input.\n";
								hasErr = true;
							}



						// if format=  (field must match a specified formatting)
						} else if (rKey.match(/^format=/ig) && hasLen){
							var fmt = rKey.replace(/^format=/i, '');
							var fitFormat = true;
							// loop thru the characters and check the format of each
							for (c=0; c<fmt.length; c++){
								if (typeof(this.fields[i].value[c]) != 'undefined'){
									switch (fmt[c].toUpperCase()){
										case 'A':
											if (!this.fields[i].value[c].match(/^[a-zA-Z]$/)) fitFormat = false;
											break;
										case '0':
											if (!this.fields[i].value[c].match(/^[0-9]$/)) fitFormat = false;
											break;
										case '*':
											break;
										default:
											if (!this.fields[i].value[c].match(new RegExp(fmt[c]))) fitFormat = false;
									}
								}
							}
							if (!fitFormat){
								err += "\n" + this.fields[i].getAttribute('label') + " does not fit the required format.\n";
								hasErr = true;
							}



						// if regex=  (match based on a regular expression)
						} else if (rKey.match(/^regex=/ig) && hasLen){
							var regex = rKey.replace(/^regex=/i, '');
							if (!this.fields[i].value.match(new RegExp(regex))){
								err += "\n" + this.fields[i].getAttribute('label') + " does not fit the required format.\n";
								hasErr = true;
							}




						/*
						 *  FORMATTING RULES
						 *  These rules don't validate, they just provide quick ways to modify form fields before submitting.
						 */
						// ucase  (convert to uppercase)
						} else if (rKey.match(/^ucase/ig)){
							this.fields[i].value = this.fields[i].value.toUpperCase();
							
						
						
						// lcase  (convert to lower case)
						} else if (rKey.match(/^lcase/ig)){
							this.fields[i].value = this.fields[i].value.toLowerCase();
							
							
							
						// padleft=  (add # of spaces to the left)
						} else if (rKey.match(/^padleft=/ig)){
							var num = rKey.replace(/^padleft=/, '');
							for (cnt=0; cnt<num; cnt++){
								this.fields[i].value = ' ' + this.fields[i].value;
							}
						
						
						
						// padright=  (add # of spaces to the right)
						} else if (rKey.match(/^padright=/ig)){
							var num = rKey.replace(/^padright=/, '');
							for (cnt=0; cnt<num; cnt++){
								this.fields[i].value = this.fields[i].value + ' ';
							}
						
						
						
						// pad=  (add # of spaces to the left and right)
						} else if (rKey.match(/^pad=/ig)){
							var num = rKey.replace(/^pad=/, '');
							for (cnt=0; cnt<num; cnt++){
								this.fields[i].value = ' ' + this.fields[i].value + ' ';
							}
							
						
						
						// reverse  (reverse the value)
						} else if (rKey.match(/^reverse/ig)){
							var newval = '';
							for (cnt=0; cnt<this.fields[i].value.length; cnt++){
								newval = this.fields[i].value[cnt] + newval;
							}
							this.fields[i].value = newval;
						}
						
						
						
						
						
					} // end for each rule
					// if there was an error found
					if (hasErr){
						this.addClass(this.fields[i], 'validatorErrorField');
					} else{
						this.removeClass(this.fields[i], 'validatorErrorField');
					}
				} // end for each field on the form
			} // end check if there is a rule
		}
		
		// if we passed an id to the validate function, write the err to there
		// otherwise, just do a basic alert box
		if (err.length > 0) {
			if (useAlert){
				alert(err);
			} else {
				err = err.replace(/^[ \t\r\n]|[ \t\r\n]$/, '');
				this.respObj.innerHTML = err.replace(/\n/g, "<br />\n");
				this.addClass(this.respObj, 'validatorResponse');
			}
		} else {
			if (!useAlert) this.removeClass(this.respObj, 'validatorResponse');
			//alert('submitting form');
			this.frm.submit();
		}
	}
	return false;
};
/*
 *  END FormValidator CLASS
 */


function getChildrenByClass(el,cls){
	var objs = el.getElementsByTagName('*');
	var retArr = new Array();
	for (i=0; i<objs.length; i++){
		if (objs[i].className.match(new RegExp(cls, 'i'))) retArr.push(objs[i]);
	}
	return retArr;
}