﻿// ReForm v0.0.2
//////////////////////////////////////////////////////////////////////////////////////////////

InputField.prototype.IsTextField = function () { return this.type == 'TEXT' || this.type == 'PASSWORD'; };
InputField.prototype.IsCheckBoxField = function () { return this.type == 'CHECKBOX'; };
InputField.prototype.IsRadioField = function () { return this.type == 'RADIO'; };
InputField.prototype.IsButton = function () { return this.type == 'BUTTON'; };
InputField.prototype.IsHidden = function () { return this.type == 'HIDDEN'; };
InputField.prototype.IsTextArea = function () { return this.input.tagName == 'TEXTAREA'; };
InputField.prototype.IsButton = function () { return this.type == 'SUBMIT' || this.type == 'RESET'; };
InputField.prototype.IsSelectField = function () { return this.input.tagName == 'SELECT'; };
InputField.prototype.IsElementGroup = function () { return this.input.getAttribute(Reform._rfElementGroup) != null; };
InputField.prototype.GetWrapperElement = function () 
{ 
    var rfWrapperElementID = this.input.getAttribute(Reform._rfWrapperElementAttribute);
    if (null != rfWrapperElementID) return document.getElementById(rfWrapperElementID);
    else return null;
};

InputField.prototype.GetElementGroup = function ()
{
    var group = null;
    
    if (null != this.input)
    {
        var parent = this.input.parentNode;
        while (null != parent && parent.tagName.toUpperCase() != "BODY")
        {
            var rfElementGroup = parent.getAttribute(this._rfElementGroup);
            if (null != rfElementGroup)
            {
                group = parent;
                break;
            }
            else
                parent = parent.parentNode;
        }       
    }
    
    return group;
};

function InputField(el)
{
    this.input = el;
    
    this.type = el.getAttribute('type');
    if (null != this.type) this.type = this.type.toUpperCase(); 
    
    return this;
};

//////////////////////////////////////////////////////////////////////////////////////////////////////

var Reform = {

    // globals
    _firstRun : true,

    // defaults
    RequireAllFields : true,
    WrapAllFields : true,
    DefaultWrapperTag : 'div',
    DefaultTitleTag : 'h1',
    ValidateOnStartup : false,
    EnableValidation : true, // override this in onclick events of elements that call submit, but need to skip validation
    InPlaceValidationEvent : 'keyup', // other values that make sense: change, none
    EnableInPlaceValidation : false,
    ShowTipsOnFocus : false,
    
    // constant attributes (don't override these, it'll screw things up)
    _rfElementGroup : 'rf-element-group',
    _rfTitle : 'rf-title', // a field's title
    _rfTitleTag : 'rf-title-tag', // the tag to use for title of group elements (fields are always wrapped with label)
    _rfWrap : 'rf-wrap', // flag - wrap the field? (defaults to true)
    _rfWrapperElementAttribute : 'rf-wrapper-element-id', // id of the element added as a wrapper for the field
    _rfWrapperTag : 'rf-wrapper-tag', // what tag to use when wrapping a field (defaults to DIV)
    _rfRequired : 'rf-required', // flag - is field required? (defaults to true) 
    _rfRegex : 'rf-regex', // a regex to match the field's input to
    _rfEmptyValue : 'rf-empty-value', // the empty value to add to a select field
    _rfTip : 'rf-tip',

    // class names - overridable by markup
    TitleClass : 'rf-title-class',
    FieldClass : 'rf-field',
    WrapperClass : 'rf-wrapper-class',
    WrapperFocusClass : 'rf-wrapper-focus-class',
    RequiredClass : 'rf-required-class',
    MissingClass : 'rf-missing-class',
    InvalidClass : 'rf-invalid-class',

    /////////////////////////////////////////////////////////////////////////////////////////////
    // find a regex in the const regex expressions array. if none found, return the key
    _findRegex : function(key)
    {
        var regex = key;
        var i = 0;
        
        var RF_CONST_REGEX = new Array 
            (
                'EMAIL', '[a-zA-Z]@[a-zA-Z].', 
                'PHONE', '[0-9]'
            );
            
        while (i<RF_CONST_REGEX.length)
        {
            if (key.toUpperCase() == RF_CONST_REGEX[i])
            {
                regex = RF_CONST_REGEX[i+1];
                break;
            }
            i += 2;
        }
        
        return regex;
    },

    //////////////////////////////////////////////////////////////////////////////////////////////
    _hookInputFields : function(inputs) {
        for (var i=0; i<inputs.length; i++)
        {
            var field = new InputField(inputs[i]);
            var input = inputs[i];
            var group = field.GetElementGroup(); 
            
            // some field types should be ignored
            if (!field.IsButton() && !field.IsHidden())
            {
                // if is a select field and has the 'rf-empty-value' attribute, add the empty value
                if (field.IsSelectField())
                {
                    this._addEmptyItemToSelectField(input);
                }
                        
                // if has the 'rf-title' attribute, add a <label for= />        
                var titleElement = (null == group) ? this._addTitleToField(input) : this._addTitleToField(group);
                        
                // if has the 'rf-wrap' attribute, wrap with the 'rf-wrapper-tag' tag
                // if element is a part of a group, wrap the group and not the element    
                if (null != group)
                {
                    var rfWrapperTag = group.getAttribute(this._rfWrapperElementAttribute);
                    if (null == rfWrapperTag) // only wrap if not already wrapped
                        this._setupFieldWrapper(group, null);
                }
                else
                    this._setupFieldWrapper(input, titleElement);

                // setup focus and blur event for showing tips using the FormFieldTips class (reform-tips.js)
                if (FormFieldTips && this.ShowTipsOnFocus)
                {
                        var tip = input.getAttribute(this._rfTip);
                        if (null != tip)
                        {
                            tip = tip.replace(/<script/g,'');
                            FormFieldTips.AddFieldTip(input, tip);
                        }
                }
                
                // if has the 'rf-required' set to true, or global var defaultIsRequired is true, add a 'required' css class
                this._setupRequiredField(input);       
                                
                // if the rf-regex attribute exists, hook keyup to regex validation event
                this._setupRegexFields(input);          
            }
        }
    },
    
    _setupRequiredField : function(el)
    {
        var field = new InputField(el);
        var group = field.GetElementGroup();
        if (null != group) field = new InputField(group);
        var reqElement = (null != group) ? group : el;
        
        var rfRequired = reqElement.getAttribute(this._rfRequired);
        var required = (field.IsTextField() || field.IsTextArea() || field.IsCheckBoxField() || field.IsElementGroup() || field.IsSelectField()) && this.RequireAllFields;
        if (null != rfRequired) required = (rfRequired.toUpperCase() != 'FALSE');
        
        if (required) 
        {
            var rfRequiredClass = reqElement.getAttribute(this.RequiredClass);
            if (null == rfRequiredClass) rfRequiredClass = this.RequiredClass;
            var wrapper = field.GetWrapperElement();
            this._addClass(null == wrapper ? reqElement : wrapper, rfRequiredClass);
            YAHOO.util.Dom.addClass(reqElement, this._rfRequired);

            // hook in-place validation
            if (this.EnableInPlaceValidation)
            {
                YAHOO.util.Event.addListener(reqElement, this.InPlaceValidationEvent, this._handleRequiredFieldValidate, this);
                if (field.IsSelectField() || field.IsCheckBoxField() || field.IsTextField()) YAHOO.util.Event.addListener(el, 'change', this._handleRequiredFieldValidate, this);
                if (field.IsCheckBoxField()) YAHOO.util.Event.addListener(el, 'click', this._handleRequiredFieldValidate, this);
                if (field.IsElementGroup())
                {
                    var groupElements = this._getGroupElements(group);
                    YAHOO.util.Event.addListener(groupElements, 'click', this._handleRequiredFieldValidate, this);//, group, true);        
                }
            }
            
            this._validateRequiredField(el);
        } 
    },

    _setupRegexFields : function(el)
    {
        var rfRegex = el.getAttribute(this._rfRegex);
        if (null != rfRegex)
        {
            YAHOO.util.Dom.addClass(el, this._rfRegex);
            
            if (el.tagName.toUpperCase() == 'INPUT' && this.EnableInPlaceValidation) YAHOO.util.Event.addListener(el, this.InPlaceValidationEvent, this._handleRegexFieldValidate, this);
            this._validateRegexField(el);
        }   
    },

    _setupFieldWrapper : function(el, titleElement)
    {
        var field = new InputField(el);
        
        var rfWrap = el.getAttribute(this._rfWrap);
        var wrap = (!field.IsButton());
        if (null != rfWrap) wrap &= (rfWrap.toUpperCase() != 'FALSE');
        else wrap &= this.WrapAllFields;
            
        if (wrap)
        {
            var rfWrapperTag = el.getAttribute(this._rfWrapperTag);
            if (null == rfWrapperTag) rfWrapperTag = this.DefaultWrapperTag;
            var rfWrapperClass = el.getAttribute(this.WrapperClass);
            if (null == rfWrapperClass) rfWrapperClass = this.WrapperClass;
            var rfMissClass = el.getAttribute(this.MissingClass);
            if (null == rfMissClass) rfMissClass = this.MissingClass;
            var rfInvClass = el.getAttribute(this.InvalidClass);
            if (null == rfInvClass) rfInvClass = this.InvalidClass;
            
            // create wrapper
            var wrapper = document.createElement(rfWrapperTag);
            wrapper.id = el.id + '-wrapper';
            wrapper.className = rfWrapperClass;
            wrapper.setAttribute(this.MissingClass, rfMissClass);
            wrapper.setAttribute(this.InvalidClass, rfInvClass);        
            
            // remove attributes from inside
            //el.removeAttribute(MissingClass);
            //el.removeAttribute(InvalidClass);
            //el.removeAttribute(_rfWrapperTag);
            //el.removeAttribute(WrapperFocusClass);
            
            // create reference to wrapper in field    
            el.removeAttribute(this._rfWrapperElementAttribute);
            el.setAttribute(this._rfWrapperElementAttribute, wrapper.id);
            
            
            // if already added a title (<label for= />), wrap that, else -- wrap the input
            if (null != titleElement)
            {
                titleElement.parentNode.insertBefore(wrapper, titleElement);
                wrapper.appendChild(titleElement);                
            }
            else
            {
                // if checkbox, see if it is wrapped by a label. if so, the wrapper goes around that
                if (el.parentNode.tagName == 'LABEL')
                {
                    var elementToWrap = el.parentNode;
                    elementToWrap.parentNode.insertBefore(wrapper, elementToWrap);
                    wrapper.appendChild(elementToWrap);
                }
                else
                {
                    // wrap only the input itself
                    el.parentNode.insertBefore(wrapper, el);
                    wrapper.appendChild(el);
                }
            }
            
            // add onfocus and onblur events to the wrapper element
            var elementsToBind = field.IsElementGroup() ? el.getElementsByTagName('input') : el;
            YAHOO.util.Event.addListener(elementsToBind, 'focus', this._handleWrapperFocus, wrapper);
            YAHOO.util.Event.addListener(elementsToBind, 'blur', this._handleWrapperBlur, wrapper);
        }
    },

    _getGroupElements : function(group)
    {
        return group.getElementsByTagName('input');
    },

    _groupHasValue : function(group)
    {
        var groupElements = this._getGroupElements(group);
        for (var i=0; i<groupElements.length; i++)
        {
            if (groupElements[i].checked)
                return true;
        }
        return false;
    },

    // constructor
    Init: function()
    {    
        // find all inputs that are not a part of a group and hook them
        this._hookInputFields(document.getElementsByTagName('input'));//GetStandAloneInputs()); 
        this._hookInputFields(document.getElementsByTagName('textarea'));    
        this._hookInputFields(document.getElementsByTagName('select'));
        // TODO: add RadioGroup tags' child inputs
        
        // hook validation method to form's submit
        var forms = document.body.getElementsByTagName('form');
        YAHOO.util.Event.addListener(forms, 'submit', this.ValidateForm, this, true);
        
        // first run over
        this._firstRun = false;
    },

    // validate form
    ValidateForm : function(e)
    {   
        var isFormValid = true;
        
        if (this.EnableValidation)
        {
            // get required inputs
            var requiredFields = YAHOO.util.Dom.getElementsByClassName(this._rfRequired);
            var regexFields = YAHOO.util.Dom.getElementsByClassName(this._rfRegex);
                      
            // loop over required inputs
            for (var i=0; i<requiredFields.length; i++)
            {
                isFormValid &= this._validateRequiredField(requiredFields[i]);
            }

            // loop over regex inputs
            for (var i=0; i<regexFields.length; i++)
            {
                isFormValid &= this._validateRegexField(regexFields[i]);
            }
            
            if (!isFormValid) YAHOO.util.Event.stopEvent(e);
        }
        else this.EnableValidation = true;
        
        return isFormValid;
    },

    _addEmptyItemToSelectField : function(el)
    {
        var rfEmptyValue = el.getAttribute(this._rfEmptyValue);
        if (null != rfEmptyValue)
        {
            var opt = new Option(rfEmptyValue);
            el.options.add(opt, 0);
            if (!this._selectHasSelectedValue(el))
            {
                el.options[0].selected = true;//'selected';
            }
        }
    },

    _addTitleToField : function(el)
    {
        var rfTitle = el.getAttribute(this._rfTitle);
        var field = new InputField(el);

        if (null != rfTitle)
        {
            if (field.IsElementGroup())
            {
                var rfTitleTag = el.getAttribute(this._rfTitleTag);
                if (null == rfTitleTag) rfTitleTag = this.DefaultTitleTag;
                
                // only add title if group was already wrapped, and then only add it once
                if (null != el.getAttribute(this._rfWrapperElementAttribute))
                {                 
                    var title = el.parentNode.getElementsByTagName(rfTitleTag);
                    if (title.length == 0)
                    {
                        title = document.createElement(rfTitleTag);
                        title.innerHTML = rfTitle;
                        el.parentNode.insertBefore(title, el);
                    }
                
                    return el;
                }
            }
            else
            {
                var div = document.createElement('div');
                div.id = el.id + '-title';
                div.className = this.TitleClass;
                div.innerHTML = '<label for="' + el.id + '">' + rfTitle + '</label>';
                el.parentNode.insertBefore(div, el);
                var div2 = document.createElement('div');
                div2.className = this.FieldClass;
                div2.appendChild(el);
                div.appendChild(div2);
                
                // add reference for label in input element
                el.setAttribute(this._rfWrapperElementAttribute, div.id);
                
                return div;
            }
        }
        else return null;
    },

    // event handler to validate required fields
    // only here for scope adjustment
    _handleRequiredFieldValidate : function(e, reformObj)
    {
        reformObj._validateRequiredField(this);
    },

    // event handler for regex field validation
    // only here for scope adjustment
    _handleRegexFieldValidate : function(e, reformObj)
    {
        return reformObj._validateRegexField(this);
    },

    _handleWrapperFocus : function(e, el)
    {
        var field = new InputField(this);
        var group = field.GetElementGroup();
        var element = (null == group) ? this : group;
        var rfWrapperFocusClass = element.getAttribute(this.WrapperFocusClass);
        if (null == rfWrapperFocusClass) rfWrapperFocusClass = this.WrapperFocusClass ;
        YAHOO.util.Dom.addClass(el, rfWrapperFocusClass);
    },

    _handleWrapperBlur : function(e, el)
    {
        var field = new InputField(this);
        var group = field.GetElementGroup();
        var element = (null == group) ? this : group;
        var rfWrapperFocusClass = element.getAttribute(this.WrapperFocusClass);
        if (null == rfWrapperFocusClass) rfWrapperFocusClass = this.WrapperFocusClass ;
        YAHOO.util.Dom.removeClass(el, rfWrapperFocusClass); 
    },
 
    // validate regex field
    _validateRegexField : function(el)
    {
        // if field was already found to be missing, skip regex validation
        var rfMissClass = null == wrapper ? el.getAttribute(this.MissingClass) : wrapper.getAttribute(this.MissingClass);
        if (null == rfMissClass) rfMissClass = this.MissingClass;     
        if (YAHOO.util.Dom.hasClass(el, rfMissClass)) return;
        
        // get the field
        var field = new InputField(el);
        var isFieldValid = true;
        if (el.value != '')
        {
            var rfRegex = el.getAttribute(this._rfRegex);
            if (null != rfRegex) isFieldValid = new RegExp(this._findRegex(rfRegex)).test(el.value);        
        }

        // set invalid class if input doesnt match regex
        var highlight =  this._firstRun ? this.ValidateOnStartup : true;
        if (highlight) 
        {
            var wrapper = field.GetWrapperElement();
            var rfInvClass = null == wrapper ? el.getAttribute(this.InvalidClass) : wrapper.getAttribute(this.InvalidClass);
            if (null == rfInvClass) rfInvClass = this.InvalidClass;             
            if (!isFieldValid) 
            {
                this._addClass(null == wrapper ? el : wrapper, rfInvClass);
            }
            else 
            {
                this._removeClass(null == wrapper ? el : wrapper, rfInvClass);
            }
        }
        
        return isFieldValid;
    },

    // validate a required field
    _validateRequiredField : function(el)
    {
        var field = new InputField(el);        
        
        var rfEmptyValue = el.getAttribute(this._rfEmptyValue);
        var rfChecked = el.checked;
        
        var isFieldValid = true;

        // is it a group? does it have a value?
        if (field.IsElementGroup()) isFieldValid = this._groupHasValue(el);       

        // is it missing?
        if (null != rfEmptyValue) isFieldValid = (el.value != rfEmptyValue);
        isFieldValid &= (el.value != '');
          
        // is it a required, unchecked checkbox?
        if (field.IsCheckBoxField()) isFieldValid = rfChecked;
            
        // set missing class if input is missing and required
        var highlight =  this._firstRun ? this.ValidateOnStartup : true;
        if (highlight) 
        {    
            var wrapper = field.GetWrapperElement();
            var rfMissClass = null == wrapper ? el.getAttribute(this.MissingClass) : wrapper.getAttribute(this.MissingClass);
            if (null == rfMissClass) rfMissClass = this.MissingClass;        
            if (!isFieldValid) 
            {
                this._addClass(null == wrapper ? el : wrapper, rfMissClass);
            }
            else 
            {
                this._removeClass(null == wrapper ? el : wrapper, rfMissClass);
            }
        }
                   
        return isFieldValid;
    },

    // add a class based on attribute
    _addClass : function(el, attr, def)
    {
        var attrValue = el.getAttribute(attr);
        var altValue = null != def ? def : attr;
        if (null != attrValue) YAHOO.util.Dom.addClass(el, attrValue);
        else YAHOO.util.Dom.addClass(el, altValue);
    },

    // remove a class based on attribute
    _removeClass : function(el, attr, def)
    {
        var attrValue = el.getAttribute(attr);
        var altValue = null != def ? def : attr;
        if (null != attrValue) YAHOO.util.Dom.removeClass(el, attrValue);
        else YAHOO.util.Dom.removeClass(el, altValue);
    },

    // check if a select field has a selected value
    _selectHasSelectedValue : function(el)
    {
        var rc = false;
        if (null != el || 'undefined' != el)
        {
            for (var i=0; i<el.options.length; i++)
            {        
                var selected = el.options[i].getAttribute("selected");
                if (null != selected)
                {
                    rc = true;
                    break;
                }
            }
        }
        
        return rc;
    }
}

YAHOO.util.Event.addListener(window, 'load', Reform.Init, Reform, true);