/*
Script: Validation.js

	The Validation class is a simple object you connect to a form that contains inputs/selects/textareas that have some (or none at all) specific custom attributes.
	
	In the tradition of Explicit Is Better Than Implicit - Validation will not attempt to do *anything* to the form other than test the values of elements within the
	form that you (the author of the HTML) want to have validated before a form submission.
	
	Anything that happens pre/during/post validation is *your* call. Validation is just here to give some minor assurance that your form has been vetted client-side,
	but that it is absolutely, in no way any kind of replacement for server-side validation.

Author:
	Chris Ashurst - http://www.telestatic.net

License:
	DWTFYWWI License.
*/

/*
Class:
	<Validation>
	
Implements:
	<Options>, <Events>
	
Syntax:
	> var myValidator = new Validation(elForm[, options]);
	
Arguments:
	elForm - required, the form element to hook.
	options - optional, see options below.

Options:
	None - Events only, see below.

Events:
	onElementValidating - (function) fires before an element goes through validation.
						Passes the element as an argument.
						
	onElementValid - (function) fires if the element successfully passes validation.
						Passes the element as an argument.
						
	onElementInvalid - (function) fires if the element fails validation.
						Passes the element and the associated error message as an argument.
						
	onFormValid - (function) fires if the form as a whole successfully passes validation.
	
	
	onFormInvalid - (function) fires if the form as a whole fails validation.
						Passes a list of error messages for each element that failed validation.
						
Properties:
	validate - (function) validates the entire form or a single element
	elements - (array) an array elements that the validation object will process
	messages - (array) an array of string messages for each element that failed validation
	
Notes:
	For best use, you should hook the call to validate your form on the onsubmit event.
	
	This class requires custom attributes to be placed into your markup. This means your (X)HTML will *not* validate. There are a couple of ways around this - The first
	being to create a custom doctype that extends whatever DTD you plan to use (easiest). The second method is to populate the attributes from another script/namespace
	on page load, which will keep your markup clean and valid, but the tradeoff is that your page may run slower. Again, your call.
	
		<input
			type="text|password"
			[required="true|false"]
			[subtype="lzfulldate|lzmonth|lzday|fulldate|month|day|year|fullyear|phone|alpha|integer|float|zipcode|email|general|regex"]
			[minlength="[1-n]"]
			[maxlength="[1-n]"]
		/>
		
		<input type="radio|checkbox"
			[required="true|false"]
		/>
		
		<select
			[required="true|false"]
			[allowfirst="true|false"]
		></select>
		
		<textarea
			[required="true|false"]
			[subtype="lzfulldate|lzmonth|lzday|fulldate|month|day|year|fullyear|phone|alpha|integer|float|zipcode|email|general|regex"]
			[minlength="[1-n]"]
			[maxlength="[1-n]"]
		></textarea>
		
	If the "required" attribute on an element is not specified, then the validator will exclude the element from validation. If the "required"
	attribute is "false", validation will only occur on the element *if* it has a value.
	
	Selects are validated if an option is selected. The only other validation control for a Select is if the "allowfirst" attribute
	is present and its value is "true". If this is the case, the first Option (at index zero) will not be valid for selection.
	
	Radios/Checkboxes work in a similar fashion to Selects. If a Radio is checked, then it is valid - likewise with Checkboxes.
	
	Textareas/Text/Password inputs have multiple validation types. See the functions after Validation._validateSelect in this js
	file to get an idea of what's available.
*/

var Validation = new Class({

	options: {
		onElementValidating: Class.empty,
		onElementValid: Class.empty,
		onElementInvalid: Class.empty,
		onFormValid: Class.empty,
		onFormInvalid: Class.empty,
		tokenField: "Pole",
		tokenNotFieldProperly: "nie jest wypełnione poprawnie",			// nie jest wypełnione poprawnie
		tokenGeneral: "pole wymagane", 
		tokenLzfulldate: "lzfulldate",
		tokenLzmonth: "lzmonth",
		tokenLzday: "lzday",
		tokenFulldate: "Pełna data",
		tokenMonth: "miesiąc",
		tokenDay: "dzień",
		tokenYear: "rok",
		tokenFullyear: "YYYY",
		tokenPhone: "numer telefonu",
		tokenAlpha: "alpha",
		tokenInteger: "liczba całkowita",
		tokenFloat: "liczba z przecinkiem",
		tokenZipcode: "kod pocztowy",
		tokenEmail: "e-mail",
		tokenGeneral: "pole wymagane",
		tokenRegex: "wyrażenia regularne",
		tokenMustBeChecked: "musi być zaznaczone",
		tokenMustBeSelected: "musi być wybrane"
	},
	
	initialize: function(elForm, options) {
		this.setOptions(options);

		if ($type(elForm) == "string") elForm = $(elForm);
		
		this.elements = $$("form#" + elForm.id + " input, select, textarea").filter("[required!=]");
		
		this.messages = [];
		
		this.validators = {
			"lzfulldate": this._lzFullDate,
			"lzmonth": this._lzMonth,
			"lzday": this._lzDay,
			"fulldate": this._fullDate,
			"month": this._month,
			"day": this._day,
			"year": this._year,
			"fullyear": this._fullYear,
			"phone": this._phone,
			"alpha": this._alpha,
			"integer": this._integer,
			"float": this._float,
			"zipcode": this._zipCode,
			"email": this._email,
			"general": this._general,
			"regex": this._regex
		}
		
		this.validatorsNames = {
			"lzfulldate": this.options.tokenLzfulldate,
			"lzmonth": this.options.tokenLzmonth,
			"lzday": this.options.tokenLzday,
			"fulldate": this.options.tokenFulldate,
			"month": this.options.tokenMonth,
			"day": this.options.tokenDay,
			"year": this.options.tokenYear,
			"fullyear": this.options.tokenFullyear,
			"phone": this.options.tokenPhone,
			"alpha": this.options.tokenAlpha,
			"integer": this.options.tokenInteger,
			"float": this.options.tokenFloat,
			"zipcode": this.options.tokenZipcode,
			"email": this.options.tokenEmail,
			"general": this.options.tokenGeneral,
			"regex": this.options.tokenRegex		
		}
		
		this._failures = [];
	},
	
	/*
	Property: validate
		Validate the entire form, or a single element

	Arguments:
		el - (optional) the value of the name attribute on the element you want to validate
	*/
	validate: function(el) {
		if (!$defined(el)) return this._validateForm();
		return this._validateElement(el);
	},

	_validateElement: function(el) {
		var elementValidated = false;
		var elementsBackup = this.elements;
		if ($type(el) != "string") return false;

		this.elements = this.elements.filter(function(i) {
			return (i.name == el) ? i : null;
		});
		
		if (this.elements.length > 0) elementValidated = this._validateForm();
		
		this.elements = elementsBackup;
		
		return elementValidated;
	},
	
	_validateForm: function() {
		this._failures = [];
		this.messages = [];
		this.elements.each(function(el) {
		
			if(el.getProperty('id')){
				el=($(el.getProperty('id')));
			}
			var required = (el.getProperty("required") == "true") ? true : false;
			
			this._onElementValidating(el);

			switch (el.get('tag')) {
				case "input":
				case "textarea":
					switch(el.type) {
						case "radio":
						case "checkbox":
							this._validateRadioCheckbox(el, required)
							break;
						default:
							this._validateTextArea(el, required);
							break;
					}
					break;
				case "select":
					this._validateSelect(el, required);
					break;
			}
		}, this);
		
		if (this._failures.contains(false)) {
			return this._onFormInvalid();
		}
		
		return this._onFormValid();
	},
	
	// If a named Radio has one of its elements checked, it is implicitly valid, since the user does not have control over the value
	// of a Radio. Same deal with Checkboxes, except obviously you can check multiple boxes if they have the same name
	_validateRadioCheckbox: function(rc, required) {
		var rcChecked = false;
		var fieldName = "";
		
//		$$("form#" + rc.form.id + " input").filterByAttribute("name", "=", rc.name).filterByAttribute("type", "=", rc.type).each(function(el) {
		$$("form#" + rc.form.id + " input").filter("[name="+rc.name+"]").filter("[type="+rc.type+"]").each(function(el) {
			if(fieldName=="") fieldName = el.getProperty("fname");
			if (el.checked) rcChecked = true;
		});
				
		if (!rcChecked) {
			if(fieldName=="")fieldName=rc.name;
			this._onElementInvalid(rc, this.options.tokenField+": '" + fieldName + "' "+this.options.tokenMustBeChecked);
		} else {
			this._onElementValid(rc);
		}
	},

	// Validate a Text input or Textarea against its defined subtype (default to "general" if subtype is missing)
	// Will also pitch a fit if subtype="regex" and no regex attribute is found
	_validateTextArea: function(it, required) {
		var subType = ($defined(it.getProperty("subtype"))) ? it.getProperty("subtype") : "general";
		var fieldName = ($defined(it.getProperty("fname"))) ? it.getProperty("fname") : it.name;
		if (required || (!required && it.get('value').length > 0)) {
			if (subType == "regex" && !$defined(it.getProperty("regex"))) {
				subType = "general";

				this._onElementInvalid(it, this.options.tokenField+": '" + fieldName + "' attempting to use regex without regex pattern attribute");
			}
			
			if (!this._validateFilter(subType, it)) {
				this._onElementInvalid(it, this.options.tokenField+": '" + fieldName + "' "+this.options.tokenNotFieldProperly+" (" + this.validatorsNames[subType] +")");
			} else {
				var minLength = (it.getProperty("minlength") != -1) ? parseInt(it.getProperty("minlength")) : null;
				var maxLength = (it.getProperty("maxlength") != -1) ? parseInt(it.getProperty("maxlength")) : null;
				
				if (it.type == "textarea") {
					// textareas deny they have a maxlength attribute unless you beat it out of them
					maxLength = ($chk(it.attributes.maxlength)) ? it.attributes.maxlength.nodeValue : maxLength;
				}
				
				var validMinMax = this._testMinMax(it.get('value').length, minLength, maxLength);
				
				if ([validMinMax.vMin, validMinMax.vMax, validMinMax.vEqual].contains(false)) {
					var errMinMax = "pomiêdzy " + minLength + " a " + maxLength;

					if ($chk(minLength) && !$chk(maxLength)) errMinMax = "co najmiej " + minLength;
					if (!$chk(minLength) && $chk(maxLength)) errMinMax = "co najwyżej " + maxLength;
					if (!validMinMax.vEqual) errMinMax = "dokładnie " + minLength;

					this._onElementInvalid(it, "Liczba znaków Pola: '" + fieldName + "' musi być " + errMinMax + " ");
				} else {
					this._onElementValid(it);
				}
			}
		} else if (!required && it.get('value').length <= 0) {
			this._onElementValid(it);
		}
	},
	
	// Validate a Select. Is the first option allowed? No? Then error. Otherwise, the Select is implicitly valid
	_validateSelect: function(s, required) {
		var allowFirst = ($defined(s.getProperty("allowfirst")) && s.getProperty("allowfirst") == "true") ? true : false;
		var fieldName = ($defined(s.getProperty("fname"))) ? s.getProperty("fname") : s.name;
		
		if (required && (!allowFirst && s.options.selectedIndex == 0)) {
			this._onElementInvalid(s, this.options.tokenField+": '" + fieldName + "' "+this.options.tokenMustBeSelected);
		} else {
			this._onElementValid(s);
		}
	},
	
	_validateFilter: function(subType, el) {
		if (subType == "regex") return this.validators["regex"](el);
		return this.validators[subType](el.get('value'));
	},
	
	// Test whether or not the element contains a value which is greater than <minlength> and less than or equal to <maxlength>
	// (providing one or both are defined). If minlength and maxlength are equal, then the length of the value in the element
	// must be *exactly* the number of characters that both minlength and maxlength are set to.
	_testMinMax: function(valueLength, minLength, maxLength) {
		var validMinMax = { vMin: true, vMax: true, vEqual: true };
		
		if ($chk(minLength) && $chk(maxLength)) {
			if (minLength == maxLength) {
				if (valueLength != maxLength) validMinMax.vEqual = false;
				return validMinMax;
			}
		}
		
		if ($chk(minLength) && (valueLength < minLength)) validMinMax.vMin = false;
		if ($chk(maxLength) && (valueLength > maxLength)) validMinMax.vMax = false;
		
		return validMinMax;
	},
	
	// Test full date with leading zeros and forward-slash separators
	_lzFullDate: function(value) {
		return /^(0[1-9]|1[012])\/(0[1-9]|[12][0-9]|3[01])\/(?:18|19|20|21)\d\d$/.test(value);
	},
	
	// Test month with leading zeros from 01-12 
	_lzMonth: function(value) {
		return /^(0[1-9]|1[012])$/.test(value);
	},
	
	// Test day with leading zeroes from 01-31
	_lzDay: function(value) {
		return /^(0[1-9]|[12][0-9]|3[01])$/.test(value);
	},
	
	// Test full date with no leading zeros and forward-slash separators
	_fullDate: function(value) {
			
		return /^(?:18|19|20|21)\d\d (0[1-9]|1[012]) (0[1-9]|[12][0-9]|3[01])$/.test(value)
//		return /^([1-9]|1[012])\/([1-9]|[12][0-9]|3[01])\/(?:18|19|20|21)\d\d$/.test(value)
	},
	
	// Test month with no leading zeros from 1-12
	_month: function(value) {
		return /^([1-9]|1[012])$/.test(value);
	},

	// Test day with no leading zeros from 1-31
	_day: function(value) {
		return /^([1-9]|[12][0-9]|3[01])$/.test(value);
	},
	
	// Tests for two-digit year from 00-99
	_year: function(value) {
		return /^\d{2}$/.test(value);
	},
	
	// Tests for four-digit year from 1800-2199
	_fullYear: function(value) {
		return /^(?:18|19|20|21)\d\d$/.test(value);
	},
	
	// Tests for any conceivable, logical combination of a standard US phone number
	// ...Except for area code in parens, followed by exchange code in parens.
	_phone: function(value) {
		return /^((\(0-\d\d\) \d\d\d \d\d \d\d)|(([\+\(]*\d[\s-\)]*){9,12}))$/.test(value);
//		return /^((((\d\s)|\d)?[\(\-\.\s]\s?)?\d{3}\s?[\)\-\.\s]?\s?)?\d{3}\s?[\.\-\s]?\s?\d{4}$/.test(value);
	},
	
	// Tests for any combination of text without numbers or punctuation other than space
	_alpha: function(value) {
		return /^[a-z\s-_]+$/i.test(value);
	},
	
	// Tests for a whole numbe, including "n.00", since .00 is still a whole number
	_integer: function(value) {
		return /^-?\d{1,3}(,?\d{3})*(\.00)?$/.test(value);
	},
	
	// Tests for a float/double, without leading zeros on the left of the decimal, and including "n.00"
	_float: function(value) {
		return /^[-+]?[0-9]+\.?[0-9]*$/.test(value);
	},
	
	// Tests for five-digit zipcode, or five-digit zipcode followed by four digits separated from the first five
	// with either a hyphen, space or just the whole 9 digits glommed together
	_zipCode: function(value) {
		return /^(\d{2}[-\s]\d{3}$)|(\d{5}$)/.test(value);
	},
	
	// Tests for a valid email address, matching all ccTLD and gTLD (plus sTLD, uTLD and iTLD)
	_email: function(value) {
		return /^[\w\-]+(\.[\w\-]+)*@[\w\-]+\.([\w\-]+\.)*[a-z]{2,}$/i.test(value);
	},
	
	// Tests for any alphanumeric mix with common punctuation, except for double-quote, angle-brackets, tilde and vertical pipe
	_general: function(value) {
//		return /^[^\']+$/i;
		return /^[.\w\s\S]+$/i.test(value);
		return /^[\.\:\,\;\*\(\!\?\'\)\/\\\$\%\&\+\-\_\#\'\@\w\d\s\"\<\>\|\~±êæñ¶¿¼ó³¡ÊÆÑ¦¯¬Ó£=]+$/i.test(value);
		return /^[\.\:\,\;\*\(\!\?\'\)\/\\\$\%\&\+\-\_\#\'\@\w\d\s]+$/i.test(value);
	},
	
	// Tests against a custom regex from the regex attribute on the element
	_regex: function(el) {
		var value = el.get('value');
		var pattern = new RegExp(el.getProperty("regex"));
		return pattern.test(value);
	},
	
	// Called before each element starts validation, and fires the onElementValidating hook (if defined)
	_onElementValidating: function(el) {
		this.fireEvent("onElementValidating", el);
	},
	
	// Called after each element has passed validation, and fires the onElementValid hook (if defined)
	_onElementValid: function(el) {
		this.fireEvent("onElementValid", el);
	},
	
	// Called after each element has failed validation, and fires the onElementInvalid hook (if defined)
	_onElementInvalid: function(el, err) {
		this._failures.include(false);
		this.messages.include(err);

		this.fireEvent("onElementInvalid", [el, err]);
	},
	
	// Called after the entire form has passed validation, and fires the onFormValid hook (if defined)
	_onFormValid: function() {
		this.fireEvent("onFormValid");
		return true;
	},
	
	// Called after the entire form has one or more elements that failed validation, and fires the onFormInvalid hook (if defined)
	_onFormInvalid: function() {
		this.fireEvent("onFormInvalid", [this.messages]);
		return false;
	}

});

Validation.implement(new Options, new Events);

 
