/**
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@lost.co.nz>
@copyright (c) Copyright 2008-2009 Leon Matthews. All rights reserved.
@link http://www.lost.co.nz/
*/

(function()
{
    /**
    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, pattern, type, value, defaultValue;

        valid = true;

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

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

            case 'email':
                // Minimal implementation of RFC2822 restrictions on email addresses
                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)
        value = elem.value;
        value = value.replace(/^\s+|\s+$/g, '');

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

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

        return valid;
    }

    /**
    @param elem
        HTML Input element
    @param bool
    */
    function setInvalidClass(elem, valid)
    {
        if( valid )
        {
            // Remove the class 'form-validation-invalid', if present
            if((' '+elem.className+' ').match(' form-validation-invalid '))
            {
                elem.className = elem.className.replace(
                    ' form-validation-invalid','');
            }
        }
        else
        {
            // 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';
            }
        }
    }

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

        var elem, elems, i, valid;

        valid = false;

        // Check content for elements that have it
        if( this.type == 'text' ||
            this.type == 'textarea' ||
            this.type == 'password' )
        {
            // Content non-empty and match optional 'data-required' rule?
            valid = checkContent(this);
            setInvalidClass(this, valid);
        }
        else if( this.type == 'checkbox')
        {
            // Checkbox checked?
            valid = this.checked;
            setInvalidClass(this, valid);
        }
        else if( this.type == 'radio')
        {
            // At least one member of radio group selected?
            elems = document.getElementsByName(this.name);
            for( i=0; i<elems.length; i++)
            {
                elem = elems[i];
                if( elem.checked )
                {
                    valid = true;
                }
            }

            // Set valid/invalid to all inputs in radio group
            for( i=0; i<elems.length; i++)
            {
                setInvalidClass(this, valid);
            }
        }
    }

    /**
    Unhide optional error message block element.
    */
    function showErrorMessage()
    {
        var message, x, y;

        message = document.getElementById('form-validation-message');
        if( message )
        {
            // Show message
            message.style.display = 'block';

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

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

        form = this;
        invalid = false;

        // Loop through form elements looking an element with the
        // class 'form-validation-invalid'
        for( 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;
        }
    }

    /**
    Add event handlers to all forms in document, if requested
    */
    function init()
    {
        var i, needsValidation, form, j, elem;

        // Loop through all forms in document
        for( i=0; i<document.forms.length; i++)
        {
            needsValidation = false;
            form = document.forms[i];

            // Install event handler on elements with class 'required'
            for( j=0; j < form.elements.length; j++ )
            {
                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;
            }
        }
    }

    // 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);
    }

})();
