/*

JQUERY FORM VALIDATION SCRIPT 
Author: Abendago Media Group
Version 1.2.1
NOTE: This script uses operations that require jQuery 1.3 or later

1.1.0 (29/05/2009)
-------------------------------------------------------------------------------------------------------------
	+ Bugfix:	variable placement was improved.
	+ Added:	"validateForm" function can now reference options.errorDiv by class or id.
	+ Added:	support for multiple "waiting" instances. More than one function can add or remove from the integer when it needs to. Ex: waiting for AJAX to return a value.

1.1.1 (2/07/2009)
-------------------------------------------------------------------------------------------------------------
	+ Bugfix:	"fieldsetGroup" function misc fixes.

1.1.2 (2/07/2009)
-------------------------------------------------------------------------------------------------------------
	+ Added:	"regExpressions" function now supports numbers only checking.

1.1.3 (2/07/2009)
-------------------------------------------------------------------------------------------------------------
	+ Bugfix:	error div now empties itself on every run to clear previous data.

1.1.4 (20/07/2009)
-------------------------------------------------------------------------------------------------------------
	+ Bugfix:	"regExpressions" function now checks for object type to avoid checking improper input types. Ex: even if a select box has "password" in its name attribute, that does not mean we should treat it like a password type input.
	+ Bugfix:	error div now hides itself on every run to avoid css padding issues.
	+ Bugfix:	"fieldsetGroup" function now removes error classes from within the fieldset only.
	+ Added:	"validateForm" function now supports ignore attribute. If set to true, it will not run its sub-functions.

1.1.5 (17/08/2009)
-------------------------------------------------------------------------------------------------------------
	+ Updated:	"regExpressions" function now supports multiple email validation when separated by comma.

1.1.6 (24/08/2009)
-------------------------------------------------------------------------------------------------------------
	+ Updated:	script is now a jquery plugin with default variables

1.1.7 (20/09/2009)
-------------------------------------------------------------------------------------------------------------
	+ Updated:	removed all outside functions so that the script is 100% self contained in jquery
	+ Updated:	more settings to choose from, including error messages
	+ Updated:	error messages are now stored into an array versus a string
	+ Updated:	callback now uses the jquery call event to embed the function in our settings
	+ Updated:	upon error, the window scrolls to the top of the error div or first inline error message instead of just 0,0
	+ Added:	"regExpressions" function now supports letters only checking.
	+ Added:	"regExpressions" function now supports no spaces checking.
	+ Added:	"regExpressions" function now supports no punctuation checking.
	+ Added:	inline error messages.
	+ Added:	"applyError" function to handle the error messages.
	+ Bugfix:	remove error class from group types
	+ Bugfix:	modified "regExpressions" function to work with group types

1.1.8 (06/10/2009)
-------------------------------------------------------------------------------------------------------------
	+ Bugfix:	let password types into the regular expression groups
	+ Bugfix:	array indexOf was crashing IE

1.1.9 (07/10/2009)
-------------------------------------------------------------------------------------------------------------
	+ Updated:	Error HTML tags are now one variable with a replacement character
	+ Bugfix:	Now able to have multiple instances per page (was a variable namespace issue)
	+ Bugfix:	When appending errors, group types now support id and class checking. Before you could only supply the tag name and was pretty much limited to fieldset. Note that you can only supply one id or class per tag and it is not W3C compliant to have multiple id's.

1.2.0 (28/10/2009)
-------------------------------------------------------------------------------------------------------------
	+ Updated:	The function "checkName" will now accept an array of needles
	+ Updated:	The function "validate" will not validate a field if the "ignore" attribute is true OR the "disabled" attribute is true
	+ Added:	The function "regExpressions" now supports the ability to force results to comply with a provided value
	+ Added:	The setting "confirmName" is now used to check against input names to classify them as confirmation fields and can be a string or an array of strings

1.2.1 (19/11/2009)
-------------------------------------------------------------------------------------------------------------
	+ Added:	The function "checkIgnore" to fix a typecasting bug with the checking. now checks an array of ignore vars.
	
*/

(function($) {

	// GLOBAL VARS
	
	var options = null; // used throughout the plugin as a container for default and custom variables
	var bWait = 0; // used to avoid finalizing until ajax is complete
	var arrErrorMessage = new Array(); // array of error messages to be printed
	var strSaveEmail = ""; // used to save email for confirmation compare
	var strSavePassword = ""; // used to save password for confirmation compare
	var regEmail = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; // regular expression used to test emails
	var regNumOnly = /^[0-9\s]+$/; // regular expression used to only accept numbers and spaces
	var regAlphaOnly = /^[A-Za-z\s]+$/; // regular expression used to only accept letters and spaces
	var regNoSpaces = /[\s]+/; // regular expression used to deny spaces
	var regNoPunctuation = /^[A-Za-z0-9\s]+$/; // regular expression used to deny punctuation
	
	var defaultSettings = {  
		
		/** ERROR SETTINGS **/
		
		appendErrorBesideField: true, // display the error message inline beside the field
		groupErrorMessage: true, // display all the error messages as a group
		scrollTop: true, // scroll to the top so we can see the errors

		errorDiv: 'div#error', // the container element for the error message
		errorClass: 'error', // the class applied to elements with errors
		errorTitle:	'<h3>You Have Missed Some Required Fields/Steps</h3>', // the title displayed above the list of errors
		
		replaceChar : '[*]', // some messages may need to use dynamic content so make sure to use this symbol were appropriate
	
		errorAppendHTML : '<span class="append">[*] !</span>' , // each appended error message will get this html format
		errorChildHTML : '<li class="child">[*].</li>', // each error message will get this html format
		errorParentHTML : '<ol class="parent">[*]</ol>', // all error messages will be wrapped in this html format
	
		/** ERROR MESSAGE SETTINGS **/

		msg1 : 'The email address "[*]" contains illegal characters',
		msg2 : 'The confirmation email "[*]" does not match "[*]"',
		msg3 : 'This password must be at least [*] characters in length',
		msg4 : 'This confirmation password does not match',
		msg5 : 'This field accepts numbers only',
		msg6 : 'This field accepts letters only',
		msg7 : 'This field does not accept spaces',
		msg8 : 'This field does not accept punctuation',
		msg9 : 'This field value "[*]" does not match the expected result',
		msg10 : 'The provided captcha was incorrect',
		msg11 : 'Please fill in the captcha',

		/** HTML TAG & ATTRIBUTE SETTINGS **/

		groupType: 'fieldset', // the tag that will be used for grouping elements
		loopTypes: 'input,select,textarea,label', // allowable tags when looping fieldset groups

		attrRequired: 'required', // attribute used for required elements
		attrMinSelect: 'min', // attribute used for setting the minimum number of required selections from a group
		attrMaxSelect: 'max', // attribute used for setting the maximum number of required selections from a group
		attrIgnore: ['ignore','disabled'], // attribute used for ignoring elements
		attrNumbersOnly: 'numbersonly', // attribute used to accept only numbers and spaces
		attrLettersOnly: 'lettersonly', // attribute used to accept only letters and spaces
		attrNoSpaces: 'nospaces', // attribute used to deny spaces
		attrNoPunctuation: 'nopunctuation', // attribute used to deny punctuation
		attrForce: 'forcevalue', // attribute used to force a preset result
 
		/** OTHER SETTINGS **/

		callbackFunction: function(){ return true; }, // this function will be run upon successful validation
		timeoutLength: 100, // if for what ever reason a function needs some time to execute, this is the amount of miliseconds it will take to re-finalize
		minPassLength: 6, // the minimum length required for a password field
		emailDelimiter: ',', // the delimiter used to separate emails
		confirmName: 'confirm' // a confirmation element such as password or email will need this string in its name. For example, by default we need "confirm" soo ... "strConfirmPass" or "confirm" would be accepted names. Note: it is not case sensitive.

	};
	
	$.fn.extend({

		validate: function(customSettings) { // main function to initiate our other functions
		
			this.submit(function(){
			
				// default settings
				options = jQuery.extend(defaultSettings,customSettings);

				// reset vars for each submit
				bWait = 0;
				arrErrorMessage.length = 0;
				strSaveEmail = "";
				strSavePassword = "";

				if (options.appendErrorBesideField==true) $(options.errorAppendHTML).remove(); // remove all appended error messages

				$(this).find(options.loopTypes).each(function() { // loop single elements
					
					$(this).removeClass(options.errorClass); // remove all error classes so we can start fresh
					
					if ($(this).checkIgnore()==false) { // ignore elements with the proper attribute
						
						$(this).emptyFields(); // check for empty form elements that are marked required
						$(this).regExpressions(); // do some extra validation ie: email/password checking
						$(this).captcha(); // validate our captcha with AJAX

					}

				});

				$(this).find(options.groupType).each(function() { // loop group elements

					$(this).removeClass(options.errorClass); // remove all error classes so we can start fresh
					
					if ($(this).checkIgnore()==false) { // ignore elements with the proper attribute
						
						$(this).regExpressions(); // do some extra validation ie: email/password checking
						$(this).fieldsetGroup(); // check for fieldsets marked required and make sure at least one child has a value
					
					}

				});

				return $(this).finalize();

			});

		},

		checkIgnore: function () {
			
			var attribute = options.attrIgnore;
			var object = this;

			if ($.isArray(attribute)) {
				$.each(attribute,function(){
					if (object.attr(String(this))=="true") return true;
				});
			} else {
				if (object.attr(attribute)=="true") return true;
			}

			return false;

		},

		applyError: function (msg) { // manages the application of error classes and html
			
			// add error class to the group element and all its allowable children
			this.addClass(options.errorClass).find(options.loopTypes).addClass(options.errorClass);
			
			// add the error message to our message array
			if (jQuery.inArray(msg,arrErrorMessage)==-1) arrErrorMessage.push(msg);

			// if turned on, append the message beside our element
			if (options.appendErrorBesideField==true) {
				
				// create the HTML error message to be appended
				var strAppend = options.errorAppendHTML.replace(options.replaceChar,msg);

				var gType = options.groupType.toLowerCase();
				
				// bad practice to use multiple ID's, but just in case someone does do it we should support it
				var arrID = gType.split("#");
				var strID = (str=arrID[1])? '#'+str:'';

				var arrClass = gType.split(".");
				var strClass = (str=arrClass[1])? '.'+str:'';
				
				// if the current tag is a group type, insert our error message inside the tag otherwise insert it after the tag
				if (this.tag()+strID+strClass == gType) this.append(strAppend); else this.after(strAppend);
				
			}
			
		},
		
		checkName: function (needle) { // check if a form element's name contains the provided array of strings or string

			var haystack = this.attr("name");
			var nFound = 0;

			if (haystack) {
				
				haystack = haystack.toLowerCase();
				
				if ($.isArray(needle)) {
					$.each(needle,function(){
						needle = this.toLowerCase();
						if (haystack.indexOf(needle)!=-1) nFound++;
					});
				} else {
					needle = needle.toLowerCase();
					if (haystack.indexOf(needle)!=-1) nFound++;
				}
			}

			if (nFound>0) return true; else return false;
		},

		tag: function () {
			return this.get(0).tagName.toLowerCase();
		},

		fieldsetGroup: function () { // loop the form groups and check for errors
			
			var req = this.attr(options.attrRequired);
			var min = this.attr(options.attrMinSelect);
			var max = this.attr(options.attrMaxSelect);
			
			if (!min) min = 1; // default is one
			if (!max) max = null; // default is null

			if (req) {
				
				var inc = 0; // set the var "inc" to zero and next we will increase this integer when a match has been found
	
				this.find("input[type='radio'],input[type='checkbox']").each(function(){ if ($(this).attr("checked")) { inc ++; } });
				this.find("input[type='text'],input[type='password'],textarea").each(function(){ if ($(this).val()!="") { inc ++; } });
				this.find("select").each(function(){ if($(this).find("option:first").attr("selected")==false) { inc ++; } });

				if (inc<min) { // if our increment is less than the minimum requirement or more than the maximum requirement, none or not enough matches were found
					
					this.applyError(req);
				
				} else if (max!=null && inc>max) {
					
					this.applyError(req);

				}
			}
		},

		emptyFields: function () { // check for empty values
			
			var req = this.attr(options.attrRequired);
			var type = this.attr("type");
			var value = this.attr("value");
			var check = this.attr("checked");
			var index = this.get(0).selectedIndex;
			
			if(req) {
				
				if (((type=="text"||type=="password"||type=="hidden"||type=="textarea")&&value=="")||((type="select-one")&&index==0)||((type=="radio"||type=="checkbox")&&check==false)) {
					
					this.applyError(req);

				}
			}
		},

		regExpressions: function () { // a collection of expressional functions to find faults in our values
			
			var o = (this.tag()==options.groupType)? this.find("input").eq(0):this;

			var value = $(o).attr("value");
			var type = $(o).attr("type");

			if (value)
			{	
				
				if (type=="text"||type=="password")
				{

					if ($(o).checkName("email")==true && $(o).checkName(options.confirmName)==false) {
						strSaveEmail = value; // save to compare with confirmation
						var arrEmails = value.split(options.emailDelimiter); // split our string of emails into an array
						for (var i=0; i<arrEmails.length; i++) { // loop our array of emails
							var strEmail = arrEmails[i].replace(/\s+/g,''); // remove all spaces
							if (!regEmail.test(strEmail)) this.applyError(options.msg1.replace(options.replaceChar,strEmail));
						}
					}
					
					if ($(o).checkName("email")==true && $(o).checkName(options.confirmName)==true && value != strSaveEmail) {
						this.applyError(options.msg2.replace(options.replaceChar,value).replace(options.replaceChar,strSaveEmail));
					}

					if (type=="password" && $(o).checkName(options.confirmName)==false) {
						strSavePassword = value; // save to compare with confirmation
						if (value.length < options.minPassLength) this.applyError(options.msg3.replace(options.replaceChar,options.minPassLength));
					}
					
					if (type=="password" && $(o).checkName(options.confirmName)==true && value != strSavePassword) {
						this.applyError(options.msg4);
					}
				}


				if (this.attr(options.attrNumbersOnly) && !regNumOnly.test(value)) this.applyError(options.msg5);
				if (this.attr(options.attrLettersOnly) && !regAlphaOnly.test(value)) this.applyError(options.msg6);
				if (this.attr(options.attrNoSpaces) && regNoSpaces.test(value)) this.applyError(options.msg7);
				if (this.attr(options.attrNoPunctuation) && !regNoPunctuation.test(value)) this.applyError(options.msg8);
				if (this.attr(options.attrForce) && this.attr(options.attrForce)!=value) this.applyError(options.msg9.replace(options.replaceChar,value));

			}
		},

		captcha: function () { // abendago specific captcha function
			
			var value = $(this).attr("value");
			
			if (this.checkName("captcha")==true) {
				
				if (value) {
					bWait ++;
					$.get("captcha.php",{"ajax":"yes","captcha":value,"form":this},function(data){
						if (data.substring(0,6)=="window") {
							// it wants to submit but we will just ignore it for now
						} else {
							this.applyError(options.msg10).empty();
							$("#captcha").html(data);
						}
						bWait --;
					});
				} else {
					this.applyError(options.msg11);
				}
			}
		},

		finalize: function () { // after we are done our looping we need to decide how we want to display our collected errors
			
			if (timeout) clearTimeout(timeout); // clear old timeout so we can make a new one / move on
			
			if (bWait > 0) { // if bwait is more than zero that means we are waiting for some ajax to finish
			
				var timeout = setTimeout(function(){$(this).finalize(options)},options.timeoutLength); // try again
				return false;
			
			} else { // we are no longer waiting so check for errors
				
				$(options.errorDiv).empty().hide(); // we must empty the error div before we begin
				
				if (arrErrorMessage.length>0) { // we have some errors
					
					if (options.groupErrorMessage==true) {
						
						var strErrorMessage = "";
						for (var i=0; i<arrErrorMessage.length; i++) strErrorMessage += options.errorChildHTML.replace(options.replaceChar,arrErrorMessage[i]);
						strErrorMessage = options.errorParentHTML.replace(options.replaceChar,strErrorMessage);
						$(options.errorDiv).html(options.errorTitle+strErrorMessage).fadeIn(500); // show the errors on screen
						
					}
						
					if (options.scrollTop==true) {
						
						var offset = 0;
						
						// try and get an offset variable
						if (options.groupErrorMessage==true) offset = $(options.errorDiv).offset().top-10;
						else if (options.appendErrorBesideField==true) offset = $(options.errorAppendHTML+":first").offset().top-10;
		
						window.scrollTo(0,offset); // scroll to the top so we can see the errors
					
					}

					return false;
				
				} else { // no errors, good job :)
					
					return options.callbackFunction.call(this); // return the result of our callback function
				}
			}
		}

	});

})(jQuery);


