/**
Unobtrusive client-side HTML form validation.

Installs itself on every form in document that has at least one required input.

The basic required input is simple an HTML input element, of type text, that has
the class 'required'.  This basic kind of required input requires only any non-
blank input.

More strict validation can be specified by using another class name that has
'required' as a suffix, and adding support for it to the validateTextInput()
event handler.  Currently supported is the self-explanatory 'required-email'.
It is not an error to specify multiple general and specific 'required' classes,
if required for stylistic purposes.  The most specific will be used for
validation.

On validation failures two actions are taken.  Firstly, to any input elements
that have failed validation, the class 'form-validation-invalid' is added.
Secondly, the HTML element with the ID of 'form-validation-message' is unhidden,
but only if it exists.

Future directions:
------------------
Error messages:
	- Instead of having a single error message that is 'un-hidden' on error, we
	could instead use an empty div as an anchor point in which to print various
	messages.

	- We could insert messages before or after the failed form fields directly,
	prehaps using labels, and some combination of names/ids/for attributes.

Other validation types?
	- Phone numbers (eg. international format)
	- URLs (but how to support both relative and absolute URLs?)
	- Price (well, loosly numeric, I guess)
	- Duplicate, eg. 'Email addresses must match'
	- Terms and condition checkbox acceptance

@author Leon Matthews <leon@messiah.co.nz>
@copyright (c) Copyright 2008 Leon Matthews. All rights reserved.
@link http://www.messiah.co.nz/
*/

(function()
{
	// Initialisation -- have init() run once page has finished loading
	var $;
	if( $ && $(document).ready )		// jQuery
		$(document).ready( init );
	else if( window.addEventListener)	// DOM Level-2
		window.addEventListener('load', init, false);
	else if( window.attachEvent)		// Sigh... Internet Explorer
		window.attachEvent('onload', init);

	/**
	Add event handlers to all forms in document, if requested
	*/
	function init()
	{
		// Loop through all forms in document
		for( var i=0; i<document.forms.length; i++)
		{
			var needsValidation = false;
			var form = document.forms[i];

			// Install event handler on elements with class 'required'
			for( var j=0; j < form.elements.length; j++ )
			{
				var elem = form.elements[j];
				if( (' '+elem.className).match(' required') )
				{
					// Add event handler
					elem.onchange = validateElement;
					needsValidation = true;
				}
			}

			// This form has at least one required element, add event handler
			if( needsValidation )
			{
				form.onsubmit = validateForm;
			}
		}
	}

	/**
	Check content of element.
	Behaviour of method is customisable using a custom, HTML5 style,
	attribute 'data-required'.
	@param HTMLFormElement
	@return bool
	*/
	function checkContent(elem)
	{
		var valid = true;


		// Default: Any non-whitespace character
		var pattern = new RegExp('\\S');

		// Extract type of required field from custom input attribute
		var type = elem.getAttribute('data-required');
		switch(type)
		{
			case null:
				break;

			case 'email':
				// Minimal implementation of RFC2822 restrictions on email addresses
				var pattern = new RegExp(
					'[a-z0-9\\.\\~\\+\\-\\=\\_]+' +	// Username
					'@' +							// @
					'[a-z0-9-]+' +					// Host name (first part)
					'(\\.[a-z0-9-]+)+',				// Multiple dot name parts
					'i');
				break;

			default:
				alert('Form Validation Warning: Unknown value for' +
				'data-required attrubute: "'+type+'"');
		}

		// Extract value and trim whitespace (for robustness)
		var value = elem.value;
		value = value.replace(/^\s+|\s+$/g, '');

		// Test value against default value (stored in alt tag)
		var defaultValue = elem.getAttribute('alt');
		if( defaultValue == value )
			valid = false;

		// Test value against our pattern
		if( ! pattern.test(value) )
			valid = false;


		return valid;
	}

	/**
	Event handler for each element in form.
	*/
	function validateElement()
	{
		// Validate Input Contents
		//////////////////////////

		var valid = false;

		// Check content for elements that have it
		if( this.type == 'text' ||
			this.type == 'textarea' ||
			this.type == 'password' )
		{
			valid = checkContent(this);
		}
		else if( this.type = 'checkbox')
		{
			valid = this.checked;
		}

		// Add and remove the class 'form-validation-invalid' from the element
		if( valid )
		{
			removeInvalidClass(this);
		}
		else
		{
			addInvalidClass(this);
		}
	}

	function removeInvalidClass(elem)
	{
		// Remove the class 'form-validation-invalid', if present
		if((' '+elem.className+' ').match(' form-validation-invalid '))
		{
			elem.className = elem.className.replace(
				' form-validation-invalid','');
		}
	}

	function addInvalidClass(elem)
	{
		// Add the class 'form-validation-invalid' to the element
		// (if it's not already there)
		if( ! (' '+elem.className+' ').match(' form-validation-invalid '))
			elem.className = elem.className+' form-validation-invalid';
	}

	/**
	OnSubmit event handler for HTMLFormElement.
	Installed by init() on any form that has at least one required element.
	*/
	function validateForm()
	{
		var form = this;
		var invalid = false;

		// Loop through form elements looking an element with the
		// class 'form-validation-invalid'
		for( var i=0; i<form.elements.length; i++)
		{
			elem = form.elements[i];

			// Text field and has our OnChange handler installed?
			if( elem.onchange == validateElement )
			{
				// Re-validate
				elem.onchange();

				// If element does not validate, the whole form is invalidated
				if( (' '+elem.className+' ').match(' form-validation-invalid '))
					invalid = true;
			}
		}

		// Show error message
		if( invalid )
		{
			showErrorMessage();
			return false;
		}
	}

	/**
	Unhide optional error message block element.
	*/
	function showErrorMessage()
	{
		var message = document.getElementById('form-validation-message');
		if( message )
		{
			// Show message
			message.style.display = 'block';

			// Scroll browser window to message?
			if( window.pageYOffset > message.offsetTop )
			{
				var x = message.offsetLeft;
				var y = message.offsetTop;
				window.scrollTo(x,y);
			}
		}

		alert('Please ensure that you have filled out all of the required fields\nand have accepted our terms of service.');
	}

})();
