/**
* jQuery Form Validator
* ------------------------------------------
* Created by Victor Jonsson <http://www.victorjonsson.se>
*
* @website http://formvalidator.net/
* @license Dual licensed under the MIT or GPL Version 2 licenses
* @version 2.2.0
*/
(function($) {

    'use strict';

    var $window = $(window),
        _applyErrorStyle = function($elem, conf) {
            $elem
                .addClass(conf.errorElementClass)
                .removeClass('valid')
                .parent()
                    .addClass('has-error')
                    .removeClass('has-success'); // twitter bs

            if(conf.borderColorOnError !== '') {
                $elem.css('border-color', conf.borderColorOnError);
            }
        },
        _removeErrorStyle = function($elem, conf) {
            $elem.each(function() {
                _setInlineErrorMessage($(this), '', conf, conf.errorMessagePosition);
                $(this)
                    .removeClass('valid')
                    .removeClass(conf.errorElementClass)
                    .css('border-color', '')
                    .parent()
                        .removeClass('has-error')
                        .removeClass('has-success')
                        .find('.'+conf.errorMessageClass) // remove inline error message
                            .remove();
            });
        },
        _setInlineErrorMessage = function($input, mess, conf, $messageContainer) {
            var custom = _getInlineErrorElement($input);
            if( custom ) {
                custom.innerHTML = mess;
            }
            else if( typeof $messageContainer == 'object' ) {
                var $found = false;
                $messageContainer.find('.'+conf.errorMessageClass).each(function() {
                    if( this.inputReferer == $input[0] ) {
                        $found = $(this);
                        return false;
                    }
                });
                if( $found ) {
                    if( !mess ) {
                        $found.remove();
                    } else {
                        $found.html(mess);
                    }
                } else {
                    var $mess = $('<div class="'+conf.errorMessageClass+'">'+mess+'</div>');
                    $mess[0].inputReferer = $input[0];
                    $messageContainer.prepend($mess);
                }
            }
            else {
                var $mess = $input.parent().find('.'+conf.errorMessageClass+'.help-block');
                if( $mess.length == 0 ) {
                    $mess = $('<span></span>').addClass('help-block').addClass(conf.errorMessageClass);
                    $mess.appendTo($input.parent());
                }
                $mess.html(mess);
            }
        },
        _getInlineErrorElement = function($input, conf) {
            return document.getElementById($input.attr('name')+'_err_msg');
        },
        _templateMessage = function($form, title, errorMessages, conf) {
            var messages = conf.errorMessageTemplate.messages.replace(/\{errorTitle\}/g, title);
            var fields = [];
            $.each(errorMessages, function(i, msg) {
                fields.push(conf.errorMessageTemplate.field.replace(/\{msg\}/g, msg));
            });
            messages = messages.replace(/\{fields\}/g, fields.join(''));
            var container = conf.errorMessageTemplate.container.replace(/\{errorMessageClass\}/g, conf.errorMessageClass);
            container = container.replace(/\{messages\}/g, messages);
            $form.children().eq(0).before(container);
        };

    /**
    * Assigns validateInputOnBlur function to elements blur event
    *
    * @param {Object} language Optional, will override $.formUtils.LANG
    * @param {Object} settings Optional, will override the default settings
    * @return {jQuery}
    */
    $.fn.validateOnBlur = function(language, settings) {
        this.find('input[data-validation],textarea[data-validation],select[data-validation]')
            .bind('blur.validation', function() {
                $(this).validateInputOnBlur(language, settings);
            });

        return this;
    };

	/*
	 * Assigns validateInputOnBlur function to elements custom event
	 * @param {Object} language Optional, will override $.formUtils.LANG
	 * @param {Object} settings Optional, will override the default settings
	 * * @return {jQuery}	
	 */
	$.fn.validateOnEvent = function(language, settings) {
        this.find('input[data-validation][data-validation-event],textarea[data-validation][data-validation-event],select[data-validation][data-validation-event]')
			.each(function(){
				var $el = $(this),
				    etype = $el.attr("data-validation-event");
				if (etype){
					$el.bind(etype + ".validation", function(){
                		$(this).validateInputOnBlur(language, settings, false, etype);
					});
				}
			});
		return this;
	};

    /**
    * fade in help message when input gains focus
    * fade out when input loses focus
    * <input data-help="The info that I want to display for the user when input is focused" ... />
    *
    * @param {String} attrName - Optional, default is data-help
    * @return {jQuery}
    */
    $.fn.showHelpOnFocus = function(attrName) {
        if(!attrName) {
            attrName = 'data-validation-help';
        }

        // Remove previously added event listeners
        this.find('.has-help-txt')
                .valAttr('has-keyup-event', false)
                .valAttr('backend-valid', false)
                .valAttr('backend-invalid', false)
                .removeClass('has-help-txt');

        // Add help text listeners
        this.find('textarea,input').each(function() {
            var $elem = $(this),
                className = 'jquery_form_help_' + ($elem.attr('name') || '').replace( /(:|\.|\[|\])/g, "" ),
                help = $elem.attr(attrName);

            if(help) {
                $elem
                    .addClass('has-help-txt')
                    .unbind('focus.help')
                    .bind('focus.help', function() {
                        var $help = $elem.parent().find('.'+className);
                        if($help.length == 0) {
                            $help = $('<span />')
                                        .addClass(className)
                                        .addClass('help')
                                        .addClass('help-block') // twitter bs
                                        .text(help)
                                        .hide();

                            $elem.after($help);

                        }
                        $help.fadeIn();
                    })
                    .unbind('blur.help')
                    .bind('blur.help', function() {
                        $(this)
                            .parent()
                            .find('.'+className)
                                .fadeOut('slow');
                    });
            }
        });

        return this;
    };

    /**
    * Validate single input when it loses focus
    * shows error message in a span element 
    * that is appended to the parent element
    *
    * @param {Object} [language] Optional, will override $.formUtils.LANG
    * @param {Object} [conf] Optional, will override the default settings
    * @param {Boolean} [attachKeyupEvent] Optional
    * @param {String} [eventContext]
    * @return {jQuery}
    */
    $.fn.validateInputOnBlur = function(language, conf, attachKeyupEvent, eventContext) {
        if(attachKeyupEvent === undefined)
            attachKeyupEvent = true;
        if(!eventContext)
            eventContext = 'blur';

        if( (this.valAttr('suggestion-nr') || this.valAttr('postpone') || this.hasClass('hasDatepicker')) && !window.postponedValidation ) {
            // This validation has to be postponed 
            var _self = this,
                postponeTime = this.valAttr('postpone') || 200;

            window.postponedValidation = function() {
                _self.validateInputOnBlur(language, conf, attachKeyupEvent);
                window.postponedValidation = false;
            };
            setTimeout(function() {
                if( window.postponedValidation ) {
                    window.postponedValidation();
                }
            }, postponeTime);

            return this;
        }

        language = $.extend({}, $.formUtils.LANG, language || {});
        _removeErrorStyle(this, conf);

        var $elem = this,
            $form = $elem.closest("form"),
            validationRule = $elem.attr(conf.validationRuleAttribute),
            validation = $.formUtils.validateInput(
                            $elem,
                            language,
                            $.extend({}, conf, {errorMessagePosition:'element'}),
                            $form,
                            eventContext
                        );

        $elem.trigger('validation', [validation===null ? null : validation===true]);

        if(validation === true) {
            $elem
                .addClass('valid')
                .parent()
                    .addClass('has-success'); // twitter bs
        } else if(validation !== null) {

            _applyErrorStyle($elem, conf);
            _setInlineErrorMessage($elem, validation, conf, conf.errorMessagePosition);

            if(attachKeyupEvent) {
                $elem.bind('keyup', function() {
                    $(this).validateInputOnBlur(language, conf, false, 'keyup');
                });
            }
        }

        return this;
    };

    /**
     * Short hand for fetching/adding/removing element attributes
     * prefixed with 'data-validation-'
     *
     * @param {String} name
     * @param {String|Boolean} [val]
     * @return string|undefined
     * @protected
     */
    $.fn.valAttr = function(name, val) {
        if( val === undefined ) {
            return this.attr('data-validation-'+name);
        } else if( val === false || val === null ) {
            return this.removeAttr('data-validation-'+name);
        } else {
            if(name.length > 0) name='-'+name;
            return this.attr('data-validation'+name, val);
        }
    };

    /**
     * Function that validate all inputs in given element
     *
     * @param {Object} [language]
     * @param {Object} [conf]
     * @param {Boolean} [displayError] Defaults to true
     */
    $.fn.isValid = function(language, conf, displayError) {

        if ($.formUtils.isLoadingModules) {
            var $self = this;
            setTimeout(function () {
                $self.isValid(language, conf, displayError);
            }, 200);
            return null;
        }

        conf = $.extend({}, $.formUtils.defaultConfig(), conf || {});
        language = $.extend({}, $.formUtils.LANG, language || {});
        displayError = displayError !== false;

        $.formUtils.isValidatingEntireForm = true;
        $.formUtils.haltValidation = false;

        /**
         * Adds message to error message stack if not already in the message stack
         *
         * @param {String} mess
         * @para {jQuery} $elem
         */
        var addErrorMessage = function(mess, $elem) {
            // validate server side will return null as error message before the server is requested
            if(mess !== null) {
                if ($.inArray(mess, errorMessages) < 0) {
                    errorMessages.push(mess);
                }
                errorInputs.push($elem);
                $elem.attr('current-error', mess);
                if( displayError )
                    _applyErrorStyle($elem, conf);
            }
        },

        /** Error messages for this validation */
        errorMessages = [],

        /** Input elements which value was not valid */
        errorInputs = [],

        /** Form instance */
        $form = this,

        /**
         * Tells whether or not to validate element with this name and of this type
         *
         * @param {String} name
         * @param {String} type
         * @return {Boolean}
         */
        ignoreInput = function(name, type) {
            if (type === 'submit' || type === 'button' || type == 'reset') {
                return true;
            }
            return $.inArray(name, conf.ignore || []) > -1;
        };

        // Reset style and remove error class
        if( displayError ) {
            $form.find('.'+conf.errorMessageClass+'.alert').remove();
            _removeErrorStyle($form.find('.'+conf.errorElementClass+',.valid'), conf);
        }

        // Validate element values
        $form.find('input,textarea,select').filter(':not([type="submit"],[type="button"])').each(function() {
            var $elem = $(this);
            var elementType = $elem.attr('type');
            if (!ignoreInput($elem.attr('name'), elementType)) {

                var validation = $.formUtils.validateInput(
                                $elem,
                                language,
                                conf,
                                $form,
                                'submit'
                            );

                $elem.trigger('validation', [validation===true]);

                // Run element validation callback
                if( typeof conf.onElementValidate == 'function' ) {
                    conf.onElementValidate((validation === true), $elem, $form, validation);
                }

                if(validation !== true) {
                    addErrorMessage(validation, $elem);
                } else {
                    $elem
                        .valAttr('current-error', false)
                        .addClass('valid')
                        .parent()
                            .addClass('has-success');
                }
            }

        });

        // Run validation callback
        if( typeof conf.onValidate == 'function' ) {
            var errors = conf.onValidate($form);
            if( $.isArray(errors) ) {
                $.each(errors, function(i, err) {
                    addErrorMessage(err.message, err.element);
                });
            }
            else if( errors && errors.element && errors.message ) {
                addErrorMessage(errors.message, errors.element);
            }
        }

        // Reset form validation flag
        $.formUtils.isValidatingEntireForm = false;

        // Validation failed
        if ( !$.formUtils.haltValidation && errorInputs.length > 0 ) {

            if( displayError ) {
                // display all error messages in top of form
                if (conf.errorMessagePosition === 'top') {
                    _templateMessage($form, language.errorTitle, errorMessages, conf);
                }
                // Customize display message
                else if(conf.errorMessagePosition === 'custom') {
                    if( typeof conf.errorMessageCustom === 'function' ) {
                        conf.errorMessageCustom($form, language.errorTitle, errorMessages, conf);
                    }
                }
                // Display error message below input field or in defined container
                else  {
                    $.each(errorInputs, function(i, $input) {
                        _setInlineErrorMessage($input, $input.attr('current-error'), conf, conf.errorMessagePosition);
                    });
                }

                if(conf.scrollToTopOnError) {
                    $window.scrollTop($form.offset().top - 20);
                }
            }

            return false;
        }

        return !$.formUtils.haltValidation;
    };

    /**
     * @deprecated
     * @param language
     * @param conf
     */
    $.fn.validateForm = function(language, conf) {
        if( window.console && typeof window.console.warn == 'function' ) {
            window.console.warn('Use of deprecated function $.validateForm, use $.isValid instead');
        }
        return this.isValid(language, conf, true);
    }

    /**
    * Plugin for displaying input length restriction
    */
    $.fn.restrictLength = function(maxLengthElement) {
        new $.formUtils.lengthRestriction(this, maxLengthElement);
        return this;
    };

    /**
     * Add suggestion dropdown to inputs having data-suggestions with a comma
     * separated string with suggestions
     * @param {Array} [settings]
     * @returns {jQuery}
     */
    $.fn.addSuggestions = function(settings) {
        var sugs = false;
        this.find('input').each(function() {
            var $field = $(this);

            sugs = $.split($field.attr('data-suggestions'));

            if( sugs.length > 0 && !$field.hasClass('has-suggestions') ) {
                $.formUtils.suggest($field, sugs, settings);
                $field.addClass('has-suggestions');
            }
        });
        return this;
    };

    /**
     * A bit smarter split function
     * @param {String} val
     * @param {Function|String} [func]
     * @param {String} [delim]
     * @returns {Array|void}
     */
    $.split = function(val, func, delim) {
        if( typeof func != 'function' ) {
            // return string
            if( !val )
                return [];
            var values = [];
            $.each(val.split(func ? func:','), function(i,str) {
                str = $.trim(str);
                if( str.length )
                    values.push(str);
            });
            return values;
        } else if( val ) {
            // use callback on each
            if( !delim )
                delim = ',';
            $.each(val.split(delim), function(i, str) {
                str = $.trim(str);
                if( str.length )
                    return func(str, i);
            });
        }
    };

    /**
     * Short hand function that makes the validation setup require less code
     * @param conf
     */
    $.validate = function(conf) {

        var defaultConf = $.extend($.formUtils.defaultConfig(), {
            form : 'form',
			/*
			 * Enable custom event for validation
			 */
            validateOnEvent : true,
            validateOnBlur : true,
            showHelpOnFocus : true,
            addSuggestions : true,
            modules : '',
            onModulesLoaded : null,
            language : false,
            onSuccess : false,
            onError : false,
            onElementValidate : false
        });

        conf = $.extend(defaultConf, conf || {});

        // Add validation to forms
        $(conf.form).each(function(i, form) {

            var $form  = $(form);
            $window.trigger('formValidationSetup', [$form]);

            // Remove all event listeners previously added
            $form.find('.has-help-txt')
                .unbind('focus.validation')
                .unbind('blur.validation');
            $form
                .removeClass('has-validation-callback')
                .unbind('submit.validation')
                .unbind('reset.validation')
                .find('input[data-validation],textarea[data-validation]')
                    .unbind('blur.validation');

            // Validate when submitted
            $form.bind('submit.validation', function() {
                var $form = $(this);

                if($.formUtils.isLoadingModules) {
                    setTimeout(function() {
                        $form.trigger('submit.validation');
                    }, 200);
                    return false;
                }
                var valid = $form.isValid(conf.language, conf);
                if( valid && typeof conf.onSuccess == 'function') {
                    var callbackResponse = conf.onSuccess($form);
                    if( callbackResponse === false )
                        return false;
                } else if ( !valid && typeof conf.onError == 'function' ) {
                    conf.onError($form);
                    return false;
                } else {
                    return valid;
                }
            })
            .bind('reset.validation', function() {
                // remove messages
                $(this).find('.'+conf.errorMessageClass+'.alert').remove();
                _removeErrorStyle($(this).find('.'+conf.errorElementClass+',.valid'), conf);
            })
            .addClass('has-validation-callback');

            if( conf.showHelpOnFocus ) {
                $form.showHelpOnFocus();
            }
            if( conf.addSuggestions ) {
                $form.addSuggestions();
            }
            if( conf.validateOnBlur ) {
                $form.validateOnBlur(conf.language, conf);
                $form.bind('html5ValidationAttrsFound', function() {
                    $form.validateOnBlur(conf.language, conf);
                })
            }
			if( conf.validateOnEvent ){
                $form.validateOnEvent(conf.language, conf);
			}

        });

        if( conf.modules != '' ) {
            if( typeof conf.onModulesLoaded == 'function' ) {
                $window.one('validatorsLoaded', conf.onModulesLoaded);
            }
            $.formUtils.loadModules(conf.modules);
        }
    };

    /**
     * Object containing utility methods for this plugin
     */
    $.formUtils = {

        /**
         * Default config for $(...).isValid();
         */
        defaultConfig :  function() {
            return {
                ignore : [], // Names of inputs not to be validated even though node attribute containing the validation rules tells us to
                errorElementClass : 'error', // Class that will be put on elements which value is invalid
                borderColorOnError : 'red', // Border color of elements which value is invalid, empty string to not change border color
                errorMessageClass : 'form-error', // class name of div containing error messages when validation fails
                validationRuleAttribute : 'data-validation', // name of the attribute holding the validation rules
                validationErrorMsgAttribute : 'data-validation-error-msg', // define custom err msg inline with element
                errorMessagePosition : 'element', // Can be either "top" or "element" or "custom"
                errorMessageTemplate : {
                    container: '<div class="{errorMessageClass} alert alert-danger">{messages}</div>',
                    messages: '<strong>{errorTitle}</strong><ul>{fields}</ul>',
                    field: '<li>{msg}</li>'
                },
                errorMessageCustom: _templateMessage,
                scrollToTopOnError : true,
                dateFormat : 'yyyy-mm-dd',
                addValidClassOnAll : false, // whether or not to apply class="valid" even if the input wasn't validated
                decimalSeparator : '.'
            }
        },

        /**
        * Available validators
        */
        validators : {},

        /**
         * Events triggered by form validator
         */
        _events : {load : [], valid: [], invalid:[]},

        /**
         * Setting this property to true during validation will
         * stop further validation from taking place and form will
         * not be sent
         */
        haltValidation : false,

        /**
         * This variable will be true $.fn.isValid() is called
         * and false when $.fn.validateOnBlur is called
         */
        isValidatingEntireForm : false,

        /**
        * Function for adding a validator
        * @param {Object} validator
        */
        addValidator : function(validator) {
            // prefix with "validate_" for backward compatibility reasons
            var name = validator.name.indexOf('validate_') === 0 ? validator.name : 'validate_'+validator.name;
            if( validator.validateOnKeyUp === undefined )
                validator.validateOnKeyUp = true;
            this.validators[name] = validator;
        },

        /**
         * @var {Boolean}
         */
        isLoadingModules : false,

        /**
         * @var {Object}
         */
        loadedModules : {},

        /**
        * @example
        *  $.formUtils.loadModules('date, security.dev');
        *
        * Will load the scripts date.js and security.dev.js from the
        * directory where this script resides. If you want to load
        * the modules from another directory you can use the
        * path argument.
        *
        * The script will be cached by the browser unless the module
        * name ends with .dev
        *
        * @param {String} modules - Comma separated string with module file names (no directory nor file extension)
        * @param {String} [path] - Optional, path where the module files is located if their not in the same directory as the core modules
        * @param {Boolean} [fireEvent] - Optional, whether or not to fire event 'load' when modules finished loading
        */
        loadModules : function(modules, path, fireEvent) {

            if( fireEvent === undefined )
                fireEvent = true;

            if( $.formUtils.isLoadingModules ) {
                setTimeout(function() {
                    $.formUtils.loadModules(modules, path, fireEvent);
                });
                return;
            }

            var hasLoadedAnyModule = false,
                loadModuleScripts = function(modules, path) {

                    var moduleList = $.split(modules),
                        numModules = moduleList.length,
                        moduleLoadedCallback = function() {
                            numModules--;
                            if( numModules == 0 ) {
                                $.formUtils.isLoadingModules = false;
                                if( fireEvent && hasLoadedAnyModule ) {
                                    $window.trigger('validatorsLoaded');
                                }
                            }
                        };

                    if( numModules > 0 ) {
                        $.formUtils.isLoadingModules = true;
                    }

                    var cacheSuffix = '?__='+( new Date().getTime() ),
                        appendToElement = document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0];

                    $.each(moduleList, function(i, modName) {
                        modName = $.trim(modName);
                        if( modName.length == 0 ) {
                            moduleLoadedCallback();
                        }
                        else {
                            var scriptUrl = path + modName + (modName.substr(-3) == '.js' ? '':'.js'),
                                script = document.createElement('SCRIPT');

                            if( scriptUrl in $.formUtils.loadedModules ) {
                                // already loaded
                                moduleLoadedCallback();
                            }
                            else {

                                // Remember that this script is loaded
                                $.formUtils.loadedModules[scriptUrl] = 1;
                                hasLoadedAnyModule = true;

                                // Load the script
                                script.type = 'text/javascript';
                                script.onload = moduleLoadedCallback;
                                script.src = scriptUrl + ( scriptUrl.substr(-7) == '.dev.js' ? cacheSuffix:'' );
                                script.onreadystatechange = function() {
                                    // IE 7 fix
                                    if( this.readyState == 'complete' || this.readyState == 'loaded' ) {
                                        moduleLoadedCallback();
                                        // Handle memory leak in IE
                                        this.onload = null;
                                        this.onreadystatechange = null;
                                    }
                                };
                                appendToElement.appendChild( script );
                            }
                        }
                    });
                };

            if( path ) {
                loadModuleScripts(modules, path);
            } else {
                var findScriptPathAndLoadModules = function() {
                    var foundPath = false;
                    $('script').each(function() {
                        if( this.src ) {
                            var scriptName = this.src.substr(this.src.lastIndexOf('/')+1, this.src.length);
                            if(scriptName.indexOf('jquery.form-validator.js') > -1 || scriptName.indexOf('jquery.form-validator.min.js') > -1) {
                                foundPath = this.src.substr(0, this.src.lastIndexOf('/')) + '/';
                                if( foundPath == '/' )
                                    foundPath = '';
                                return false;
                            }
                        }
                    });

                    if( foundPath !== false) {
                        loadModuleScripts(modules, foundPath);
                        return true;
                    }
                    return false;
                };

                if( !findScriptPathAndLoadModules() ) {
                    $(findScriptPathAndLoadModules);
                }
            }
        },

        /**
        * Validate the value of given element according to the validation rules
        * found in the attribute data-validation. Will return true if valid,
        * error message otherwise
        *
        * @param {jQuery} $elem
        * @param {Object} language ($.formUtils.LANG)
        * @param {Object} conf
        * @param {jQuery} $form
        * @param {String} [eventContext]
        * @return {String|Boolean}
        */
        validateInput : function($elem, language, conf, $form, eventContext) {

            if( $elem.attr('disabled') )
                return null; // returning null will prevent that the valid class gets applied to the element

            $elem.trigger('beforeValidation');

            var value = $elem.val() || '',
                optional = $elem.valAttr('optional'),

                // test if a checkbox forces this element to be validated
                validationDependsOnCheckedInput = false,
                validationDependentInputIsChecked = false,
                validateIfCheckedElement = false,

                // get value of this element's attribute "... if-checked"
                validateIfCheckedElementName = $elem.valAttr("if-checked");

            // make sure we can proceed
            if (validateIfCheckedElementName != null) {

                // Set the boolean telling us that the validation depends
                // on another input being checked
                validationDependsOnCheckedInput = true;

                // select the checkbox type element in this form
                validateIfCheckedElement = $form.find('input[name="' + validateIfCheckedElementName + '"]');

                // test if it's property "checked" is checked
                if ( validateIfCheckedElement.prop('checked') ) {
                    // set value for validation checkpoint
                    validationDependentInputIsChecked = true;
                }
            }

            // validation checkpoint
            // if empty AND optional attribute is present
            // OR depending on a checkbox being checked AND checkbox is checked, return true
            if ((!value && optional === 'true') || (validationDependsOnCheckedInput && !validationDependentInputIsChecked)) {
                return conf.addValidClassOnAll ? true:null;
            }

            var validationRules = $elem.attr(conf.validationRuleAttribute),

                // see if form element has inline err msg attribute
                validationErrorMsg = true;

            if( !validationRules ) {
                return conf.addValidClassOnAll ? true:null;
            }

            $.split(validationRules, function(rule) {
                if( rule.indexOf('validate_') !== 0 ) {
                    rule = 'validate_' + rule;
                }

                var validator = $.formUtils.validators[rule];

                if( validator && typeof validator['validatorFunction'] == 'function' ) {
                    // special change of element for checkbox_group rule
                    if ( rule == 'validate_checkbox_group' ) {
                        // set element to first in group, so error msg is set only once
                            $elem = $("[name='"+$elem.attr('name')+"']:eq(0)");
                    }

                    var isValid = true;
                    if( eventContext != 'keyup' || validator.validateOnKeyUp ) {
                        isValid = validator.validatorFunction(value, $elem, conf, language, $form);
                    }

                    if(!isValid) {
                        validationErrorMsg =  $elem.attr(conf.validationErrorMsgAttribute+'-'+rule.replace('validate_', ''));
                        if( !validationErrorMsg ) {
                            validationErrorMsg =  $elem.attr(conf.validationErrorMsgAttribute);
                            if( !validationErrorMsg ) {
                                validationErrorMsg = language[validator.errorMessageKey];
                                if( !validationErrorMsg )
                                    validationErrorMsg = validator.errorMessage;
                            }
                        }
                        return false; // breaks the iteration
                    }

                } else {
                    console.warn('Using undefined validator "'+rule+'"');
                }

            }, ' ');

            if( typeof validationErrorMsg == 'string' ) {
                return validationErrorMsg;
            } else {
                return true;
            }
        },

       /**
        * Is it a correct date according to given dateFormat. Will return false if not, otherwise
        * an array 0=>year 1=>month 2=>day
        *
        * @param {String} val
        * @param {String} dateFormat
        * @return {Array}|{Boolean}
        */
        parseDate : function(val, dateFormat) {
            var divider = dateFormat.replace(/[a-zA-Z]/gi, '').substring(0,1),
                regexp = '^',
                formatParts = dateFormat.split(divider),
                matches, day, month, year;

            $.each(formatParts, function(i, part) {
               regexp += (i > 0 ? '\\'+divider:'') + '(\\d{'+part.length+'})';
            });

            regexp += '$';
            
            matches = val.match(new RegExp(regexp));
            if (matches === null) {
                return false;
            }
        
            var findDateUnit = function(unit, formatParts, matches) {
                for(var i=0; i < formatParts.length; i++) {
                    if(formatParts[i].substring(0,1) === unit) {
                        return $.formUtils.parseDateInt(matches[i+1]);
                    }
                }
                return -1;
            };
        
            month = findDateUnit('m', formatParts, matches);
            day = findDateUnit('d', formatParts, matches);
            year = findDateUnit('y', formatParts, matches);
        
            if ((month === 2 && day > 28 && (year % 4 !== 0  || year % 100 === 0 && year % 400 !== 0)) 
            	|| (month === 2 && day > 29 && (year % 4 === 0 || year % 100 !== 0 && year % 400 === 0))
            	|| month > 12 || month === 0) {
                return false;
            }
            if ((this.isShortMonth(month) && day > 30) || (!this.isShortMonth(month) && day > 31) || day === 0) {
                return false;
            }
        
            return [year, month, day];
        },

       /**
        * skum fix. är talet 05 eller lägre ger parseInt rätt int annars får man 0 när man kör parseInt?
        *
        * @param {String} val
        * @param {Number}
        */
        parseDateInt : function(val) {
            if (val.indexOf('0') === 0) {
                val = val.replace('0', '');
            }
            return parseInt(val,10);
        },

        /**
        * Has month only 30 days?
        *
        * @param {Number} m
        * @return {Boolean}
        */
        isShortMonth : function(m) {
            return (m % 2 === 0 && m < 7) || (m % 2 !== 0 && m > 7);
        },

       /**
        * Restrict input length
        *
        * @param {jQuery} $inputElement Jquery Html object
        * @param {jQuery} $maxLengthElement jQuery Html Object
        * @return void
        */
        lengthRestriction : function($inputElement, $maxLengthElement) {
                // read maxChars from counter display initial text value
           var maxChars = parseInt($maxLengthElement.text(),10),
                charsLeft = 0,

               // internal function does the counting and sets display value
               countCharacters = function() {
                   var numChars = $inputElement.val().length;
                   if(numChars > maxChars) {
                       // get current scroll bar position
                       var currScrollTopPos = $inputElement.scrollTop();
                       // trim value to max length
                       $inputElement.val($inputElement.val().substring(0, maxChars));
                       $inputElement.scrollTop(currScrollTopPos);
                   }
                   charsLeft = maxChars - numChars;
                   if( charsLeft < 0 )
                       charsLeft = 0;

                   // set counter text
                   $maxLengthElement.text(charsLeft);
               };

           // bind events to this element
           // setTimeout is needed, cut or paste fires before val is available
           $($inputElement).bind('keydown keyup keypress focus blur',  countCharacters )
               .bind('cut paste', function(){ setTimeout(countCharacters, 100); } ) ;

           // count chars on pageload, if there are prefilled input-values
           $(document).bind("ready", countCharacters);
        },

        /**
        * Test numeric against allowed range
        *
        * @param $value int
        * @param $rangeAllowed str; (1-2, min1, max2)
        * @return array 
        */
        numericRangeCheck : function(value, rangeAllowed) 
        {
            // split by dash
            var range = $.split(rangeAllowed, '-');
            // min or max
            var minmax = parseInt(rangeAllowed.substr(3),10)
            // range ?
            if (range.length == 2 && (value < parseInt(range[0],10) || value > parseInt(range[1],10) ) )
            {   return [ "out", range[0], range[1] ] ; } // value is out of range
            else if (rangeAllowed.indexOf('min') === 0 && (value < minmax ) ) // min
            {  return ["min", minmax]; } // value is below min
            else if (rangeAllowed.indexOf('max') === 0 && (value > minmax ) ) // max
            {   return ["max", minmax]; } // value is above max
            else { return [ "ok" ] ; } // value is in allowed range
        },


        _numSuggestionElements : 0,
        _selectedSuggestion : null,
        _previousTypedVal : null,

        /**
         * Utility function that can be used to create plugins that gives
         * suggestions when inputs is typed into
         * @param {jQuery} $elem
         * @param {Array} suggestions
         * @param {Object} settings - Optional
         * @return {jQuery}
         */
        suggest : function($elem, suggestions, settings) {
            var conf =  {
                css : {
                    maxHeight: '150px',
                    background: '#FFF',
                    lineHeight:'150%',
                    textDecoration : 'underline',
                    overflowX : 'hidden',
                    overflowY : 'auto',
                    border : '#CCC solid 1px',
                    borderTop : 'none',
                    cursor: 'pointer'
                },
                activeSuggestionCSS : {
                    background : '#E9E9E9'
                }
            },
            setSuggsetionPosition = function($suggestionContainer, $input) {
                var offset = $input.offset();
                $suggestionContainer.css({
                    width : $input.outerWidth(),
                    left : offset.left + 'px',
                    top : (offset.top + $input.outerHeight()) +'px'
                });
            };

            if(settings)
                $.extend(conf, settings);

            conf.css['position'] = 'absolute';
            conf.css['z-index'] = 9999;
            $elem.attr('autocomplete', 'off');

            if( this._numSuggestionElements === 0 ) {
                // Re-position suggestion container if window size changes
                $window.bind('resize', function() {
                    $('.jquery-form-suggestions').each(function() {
                        var $container = $(this),
                            suggestID = $container.attr('data-suggest-container');
                        setSuggsetionPosition($container, $('.suggestions-'+suggestID).eq(0));
                    });
                });
            }

            this._numSuggestionElements++;

            var onSelectSuggestion = function($el) {
                var suggestionId = $el.valAttr('suggestion-nr');
                $.formUtils._selectedSuggestion = null;
                $.formUtils._previousTypedVal = null;
                $('.jquery-form-suggestion-'+suggestionId).fadeOut('fast');
            };

            $elem
                .data('suggestions', suggestions)
                .valAttr('suggestion-nr', this._numSuggestionElements)
                .unbind('focus.suggest')
                .bind('focus.suggest', function() {
                    $(this).trigger('keyup');
                    $.formUtils._selectedSuggestion = null;
                })
                .unbind('keyup.suggest')
                .bind('keyup.suggest', function() {
                    var $input = $(this),
                        foundSuggestions = [],
                        val = $.trim($input.val()).toLocaleLowerCase();

                    if(val == $.formUtils._previousTypedVal) {
                        return;
                    }
                    else {
                        $.formUtils._previousTypedVal = val;
                    }

                    var hasTypedSuggestion = false,
                        suggestionId = $input.valAttr('suggestion-nr'),
                        $suggestionContainer = $('.jquery-form-suggestion-'+suggestionId);

                    $suggestionContainer.scrollTop(0);

                    // Find the right suggestions
                    if(val != '') {
                        var findPartial = val.length > 2;
                        $.each($input.data('suggestions'), function(i, suggestion) {
                            var lowerCaseVal = suggestion.toLocaleLowerCase();
                            if( lowerCaseVal == val ) {
                                foundSuggestions.push('<strong>'+suggestion+'</strong>');
                                hasTypedSuggestion = true;
                                return false;
                            } else if(lowerCaseVal.indexOf(val) === 0 || (findPartial && lowerCaseVal.indexOf(val) > -1)) {
                                foundSuggestions.push(suggestion.replace(new RegExp(val, 'gi'), '<strong>$&</strong>'));
                            }
                        });
                    }

                    // Hide suggestion container
                    if(hasTypedSuggestion || (foundSuggestions.length == 0 && $suggestionContainer.length > 0)) {
                        $suggestionContainer.hide();
                    }

                    // Create suggestion container if not already exists
                    else if(foundSuggestions.length > 0 && $suggestionContainer.length == 0) {
                        $suggestionContainer = $('<div></div>').css(conf.css).appendTo('body');
                        $elem.addClass('suggestions-'+suggestionId);
                        $suggestionContainer
                            .attr('data-suggest-container', suggestionId)
                            .addClass('jquery-form-suggestions')
                            .addClass('jquery-form-suggestion-'+suggestionId);
                    }

                    // Show hidden container
                    else if(foundSuggestions.length > 0 && !$suggestionContainer.is(':visible')) {
                        $suggestionContainer.show();
                    }

                    // add suggestions
                    if(foundSuggestions.length > 0 && val.length != foundSuggestions[0].length) {

                        // put container in place every time, just in case
                        setSuggsetionPosition($suggestionContainer, $input);

                        // Add suggestions HTML to container
                        $suggestionContainer.html('');
                        $.each(foundSuggestions, function(i, text) {
                            $('<div></div>')
                                .append(text)
                                .css({
                                    overflow: 'hidden',
                                    textOverflow : 'ellipsis',
                                    whiteSpace : 'nowrap',
                                    padding: '5px'
                                })
                                .addClass('form-suggest-element')
                                .appendTo($suggestionContainer)
                                .click(function() {
                                    $input.focus();
                                    $input.val( $(this).text() );
                                    onSelectSuggestion($input);
                                });
                        });
                    }
                })
                .unbind('keydown.validation')
                .bind('keydown.validation', function(e) {
                    var code = (e.keyCode ? e.keyCode : e.which),
                        suggestionId,
                        $suggestionContainer,
                        $input = $(this);

                    if(code == 13 && $.formUtils._selectedSuggestion !== null) {
                        suggestionId = $input.valAttr('suggestion-nr');
                        $suggestionContainer = $('.jquery-form-suggestion-'+suggestionId);
                        if($suggestionContainer.length > 0) {
                            var newText = $suggestionContainer.find('div').eq($.formUtils._selectedSuggestion).text();
                            $input.val(newText);
                            onSelectSuggestion($input);
                            e.preventDefault();
                        }
                    }
                    else {
                        suggestionId = $input.valAttr('suggestion-nr');
                        $suggestionContainer = $('.jquery-form-suggestion-'+suggestionId);
                        var $suggestions = $suggestionContainer.children();
                        if($suggestions.length > 0 && $.inArray(code, [38,40]) > -1) {
                            if(code == 38) { // key up
                                if($.formUtils._selectedSuggestion === null)
                                    $.formUtils._selectedSuggestion = $suggestions.length-1;
                                else
                                    $.formUtils._selectedSuggestion--;
                                if($.formUtils._selectedSuggestion < 0)
                                    $.formUtils._selectedSuggestion = $suggestions.length-1;
                            }
                            else if(code == 40) { // key down
                                if($.formUtils._selectedSuggestion === null)
                                    $.formUtils._selectedSuggestion = 0;
                                else
                                    $.formUtils._selectedSuggestion++;
                                if($.formUtils._selectedSuggestion > ($suggestions.length-1))
                                    $.formUtils._selectedSuggestion = 0;

                            }

                            // Scroll in suggestion window
                            var containerInnerHeight = $suggestionContainer.innerHeight(),
                                containerScrollTop = $suggestionContainer.scrollTop(),
                                suggestionHeight = $suggestionContainer.children().eq(0).outerHeight(),
                                activeSuggestionPosY = suggestionHeight * ($.formUtils._selectedSuggestion);

                            if( activeSuggestionPosY < containerScrollTop || activeSuggestionPosY > (containerScrollTop+containerInnerHeight)) {
                                $suggestionContainer.scrollTop( activeSuggestionPosY );
                            }

                            $suggestions
                                .removeClass('active-suggestion')
                                .css('background', 'none')
                                .eq($.formUtils._selectedSuggestion)
                                    .addClass('active-suggestion')
                                    .css(conf.activeSuggestionCSS);

                            e.preventDefault();
                            return false;
                        }
                    }
                })
                .unbind('blur.suggest')
                .bind('blur.suggest', function() {
                    onSelectSuggestion($(this));
                });

            return $elem;
        },

       /**
        * Error dialogs
        *
        * @var {Object}
        */
        LANG : {
            errorTitle : 'Form submission failed!',
            requiredFields : 'You have not answered all required fields',
            badTime : 'You have not given a correct time',
            badEmail : 'You have not given a correct e-mail address',
            badTelephone : 'You have not given a correct phone number',
            badSecurityAnswer : 'You have not given a correct answer to the security question',
            badDate : 'You have not given a correct date',
            lengthBadStart : 'The input value must be between ',
            lengthBadEnd : ' characters',
            lengthTooLongStart : 'The input value is longer than ',
            lengthTooShortStart : 'The input value is shorter than ',
            notConfirmed : 'Input values could not be confirmed',
            badDomain : 'Incorrect domain value',
            badUrl : 'The input value is not a correct URL',
            badCustomVal : 'The input value is incorrect',
            badInt : 'The input value was not a correct number',
            badSecurityNumber : 'Your social security number was incorrect',
            badUKVatAnswer : 'Incorrect UK VAT Number',
            badStrength : 'The password isn\'t strong enough',
            badNumberOfSelectedOptionsStart : 'You have to choose at least ',
            badNumberOfSelectedOptionsEnd : ' answers',
            badAlphaNumeric : 'The input value can only contain alphanumeric characters ',
            badAlphaNumericExtra: ' and ',
            wrongFileSize : 'The file you are trying to upload is too large',
            wrongFileType : 'The file you are trying to upload is of wrong type',
            groupCheckedRangeStart : 'Please choose between ',
            groupCheckedTooFewStart : 'Please choose at least ',
            groupCheckedTooManyStart : 'Please choose a maximum of ',
            groupCheckedEnd : ' item(s)',
            badCreditCard : 'The credit card number is not correct',
            badCVV : 'The CVV number was not correct'
        }
    };


    /* * * * * * * * * * * * * * * * * * * * * *
      CORE VALIDATORS
    * * * * * * * * * * * * * * * * * * * * */


    /*
    * Validate email
    */
    $.formUtils.addValidator({
        name : 'email',
        validatorFunction : function(email) {

            var emailParts = email.toLowerCase().split('@');
            if( emailParts.length == 2 ) {
                return $.formUtils.validators.validate_domain.validatorFunction(emailParts[1]) &&
                        !(/[^\w\+\.\-]/.test(emailParts[0]));
            }

            return false;
        },
        errorMessage : '',
        errorMessageKey : 'badEmail'
    });

    /*
    * Validate domain name
    */
    $.formUtils.addValidator({
        name : 'domain',
        validatorFunction : function(val, $input) {

            var topDomains =  ['.ac', '.ad', '.ae', '.aero', '.af', '.ag', '.ai', '.al', '.am', '.an', '.ao',
                        '.aq', '.ar', '.arpa', '.as', '.asia', '.at', '.au', '.aw', '.ax', '.az', '.ba', '.bb',
                        '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bike', '.biz', '.bj', '.bm', '.bn', '.bo',
                        '.br', '.bs', '.bt', '.bv', '.bw', '.by', '.bz', '.ca', '.camera', '.cat', '.cc', '.cd',
                        '.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.clothing', '.cm', '.cn', '.co', '.com',
                        '.construction', '.contractors', '.coop', '.cr', '.cu', '.cv', '.cw', '.cx', '.cy', '.cz',
                        '.de', '.diamonds', '.directory', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.edu', '.ee',
                        '.eg', '.enterprises', '.equipment', '.er', '.es', '.estate', '.et', '.eu', '.fi', '.fj',
                        '.fk', '.fm', '.fo', '.fr', '.ga', '.gallery', '.gb', '.gd', '.ge', '.gf', '.gg', '.gh',
                        '.gi', '.gl', '.gm', '.gn', '.gov', '.gp', '.gq', '.gr', '.graphics', '.gs', '.gt', '.gu',
                        '.guru', '.gw', '.gy', '.hk', '.hm', '.hn', '.holdings', '.hr', '.ht', '.hu', '.id', '.ie',
                        '.il', '.im', '.in', '.info', '.int', '.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo',
                        '.jobs', '.jp', '.ke', '.kg', '.kh', '.ki', '.kitchen', '.km', '.kn', '.kp', '.kr', '.kw',
                        '.ky', '.kz', '.la', '.land', '.lb', '.lc', '.li', '.lighting', '.lk', '.lr', '.ls', '.lt',
                        '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.menu', '.mg', '.mh', '.mil', '.mk', '.ml',
                        '.mm', '.mn', '.mo', '.mobi', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.museum', '.mv',
                        '.mw', '.mx', '.my', '.mz', '.na', '.name', '.nc', '.ne', '.net', '.nf', '.ng', '.ni',
                        '.nl', '.no', '.np', '.nr', '.nu', '.nz', '.om', '.org', '.pa', '.pe', '.pf', '.pg', '.ph',
                        '.photography', '.pk', '.pl', '.plumbing', '.pm', '.pn', '.post', '.pr', '.pro', '.ps', '.pt',
                        '.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se',
                        '.sexy', '.sg', '.sh', '.si', '.singles', '.sj', '.sk', '.sl', '.sm', '.sn', '.so', '.sr',
                        '.st', '.su', '.sv', '.sx', '.sy', '.sz', '.tattoo', '.tc', '.td', '.technology', '.tel', '.tf',
                        '.tg', '.th', '.tips', '.tj', '.tk', '.tl', '.tm', '.tn', '.to', '.today', '.tp', '.tr', '.travel',
                        '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.uno', '.us', '.uy', '.uz', '.va', '.vc', '.ve',
                        '.ventures', '.vg', '.vi', '.vn', '.voyage', '.vu', '.wf', '.ws', '.xn--3e0b707e', '.xn--45brj9c',
                        '.xn--80ao21a', '.xn--80asehdb', '.xn--80aswg', '.xn--90a3ac', '.xn--clchc0ea0b2g2a9gcd', '.xn--fiqs8s',
                        '.xn--fiqz9s', '.xn--fpcrj9c3d', '.xn--fzc2c9e2c', '.xn--gecrj9c', '.xn--h2brj9c', '.xn--j1amh',
                        '.xn--j6w193g', '.xn--kprw13d', '.xn--kpry57d', '.xn--l1acc', '.xn--lgbbat1ad8j', '.xn--mgb9awbf',
                        '.xn--mgba3a4f16a', '.xn--mgbaam7a8h', '.xn--mgbayh7gpa', '.xn--mgbbh1a71e', '.xn--mgbc0a9azcg',
                        '.xn--mgberp4a5d4ar', '.xn--mgbx4cd0ab', '.xn--ngbc5azd', '.xn--o3cw4h', '.xn--ogbpf8fl', '.xn--p1ai',
                        '.xn--pgbs0dh', '.xn--q9jyb4c', '.xn--s9brj9c', '.xn--unup4y', '.xn--wgbh1c', '.xn--wgbl6a',
                        '.xn--xkc2al3hye2a', '.xn--xkc2dl3a5ee0h', '.xn--yfro4i67o', '.xn--ygbi2ammx', '.xxx', '.ye',
                        '.yt', '.za', '.zm', '.zw'],

                ukTopDomains = ['co', 'me', 'ac', 'gov', 'judiciary','ltd', 'mod', 'net', 'nhs', 'nic',
                        'org', 'parliament', 'plc', 'police', 'sch', 'bl', 'british-library', 'jet','nls'],

                dot = val.lastIndexOf('.'),
                domain = val.substring(0, dot),
                ext = val.substring(dot, val.length),
                hasTopDomain = false;

            for (var i = 0; i < topDomains.length; i++) {
                if (topDomains[i] === ext) {
                    if(ext==='.uk') {
                        //Run Extra Checks for UK Domain Names
                        var domainParts = val.split('.');
                        var tld2 = domainParts[domainParts.length-2];
                        for(var j = 0; j < ukTopDomains.length; j++) {
                            if(ukTopDomains[j] === tld2) {
                                hasTopDomain = true;
                                break;
                            }
                        }

                        if(hasTopDomain)
                            break;

                    } else {
                        hasTopDomain = true;
                        break;
                    }
                }
            }

            if (!hasTopDomain) {
                return false;
            } else if (dot < 2 || dot > 57) {
                return $.inArray(val, ['i.net', 'q.com', 'q.net', 'x.com', 'x.org', 'z.com', 'w.org']) > -1;
            } else {
                var firstChar = domain.substring(0, 1),
                    lastChar = domain.substring(domain.length - 1, domain.length);

                if (firstChar === '-' || firstChar === '.' || lastChar === '-' || lastChar === '.') {
                    return false;
                }
                if (domain.split('..').length > 1) {
                    return false;
                }
                if (domain.replace(/[-\da-z\.]/g, '') !== '') {
                    return false;
                }
            }

            if(typeof $input !== 'undefined') {
                $input.val(val);
            }

            return true;
        },
        errorMessage : '',
        errorMessageKey: 'badDomain'
    });

    /*
    * Validate required
    */
    $.formUtils.addValidator({
        name : 'required',
        validatorFunction : function(val, $el, config, language, $form) {
            switch ( $el.attr('type') ) {
                case 'checkbox':
                    return $el.is(':checked');
                case 'radio':
                    return $form.find('input[name="'+$el.attr('name')+'"]').filter(':checked').length > 0;
                default:
                    return $.trim(val) !== '';
            }
        },
        errorMessage : '',
        errorMessageKey: 'requiredFields'
    });

    /*
    * Validate length range
    */
    $.formUtils.addValidator({
        name : 'length',
        validatorFunction : function(val, $el, conf, lang) {
            var lengthAllowed = $el.valAttr('length'),
                type = $el.attr('type');

            if(lengthAllowed == undefined) {
                var elementType = $el.get(0).nodeName;
                alert('Please add attribute "data-validation-length" to '+elementType+' named '+$el.attr('name'));
                return true;
            }

            // check if length is above min, below max or within range.
            var len = type == 'file' && $el.get(0).files !== undefined ? $el.get(0).files.length : val.length,
                lengthCheckResults = $.formUtils.numericRangeCheck(len, lengthAllowed),
                checkResult;

            switch(lengthCheckResults[0])
            {   // outside of allowed range
                case "out":
                    this.errorMessage = lang.lengthBadStart + lengthAllowed + lang.lengthBadEnd;
                    checkResult = false;
                    break;
                // too short
                case "min":
                    this.errorMessage = lang.lengthTooShortStart + lengthCheckResults[1] + lang.lengthBadEnd;
                    checkResult = false;
                    break;
                // too long
                case "max":
                    this.errorMessage = lang.lengthTooLongStart + lengthCheckResults[1] + lang.lengthBadEnd;
                    checkResult = false;
                    break;
                // ok
                default:
                    checkResult = true;
            }
            
            return checkResult;
        },
        errorMessage : '',
        errorMessageKey: ''
    });

    /*
    * Validate url
    */
    $.formUtils.addValidator({
        name : 'url',
        validatorFunction : function(url) {
            // written by Scott Gonzalez: http://projects.scottsplayground.com/iri/
            // - Victor Jonsson added support for arrays in the url ?arg[]=sdfsdf
            // - General improvements made by Stéphane Moureau <https://github.com/TraderStf>
            var urlFilter = /^(https?|ftp):\/\/((((\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])(\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])(\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/(((\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/((\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|\[|\]|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#(((\w|-|\.|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
            if( urlFilter.test(url) ) {
                var domain = url.split('://')[1];
                var domainSlashPos = domain.indexOf('/');
                if(domainSlashPos > -1)
                    domain = domain.substr(0, domainSlashPos);

                return $.formUtils.validators.validate_domain.validatorFunction(domain); // todo: add support for IP-addresses
            }
            return false;
        },
        errorMessage : '',
        errorMessageKey: 'badUrl'
    });

    /*
    * Validate number (floating or integer)
    */
    $.formUtils.addValidator({
        name : 'number',
        validatorFunction : function(val, $el, conf) {
            if(val !== '') {
                var allowing = $el.valAttr('allowing') || '',
                    decimalSeparator = $el.valAttr('decimal-separator') || conf.decimalSeparator,
                    allowsRange = false,
                    begin, end;

                if(allowing.indexOf('number') == -1)
                    allowing += ',number';

                if(allowing.indexOf('negative') > -1 && val.indexOf('-') === 0) {
                    val = val.substr(1);
                }

                if (allowing.indexOf('range') > -1)
                {
                    begin = parseFloat(allowing.substring(allowing.indexOf("[")+1, allowing.indexOf(";")));
                    end = parseFloat(allowing.substring(allowing.indexOf(";")+1,allowing.indexOf("]")));
                    allowsRange = true;
                }

                if( decimalSeparator == ',' ) {
                    if( val.indexOf('.') > -1 ) {
                        return false;
                    }
                    // Fix for checking range with floats using ,
                    val = val.replace(',', '.');
                }

                if(allowing.indexOf('number') > -1 && val.replace(/[0-9]/g, '') === '' && (!allowsRange || (val >= begin && val <= end)) ) {
                    return true;
                }
                if(allowing.indexOf('float') > -1 && val.match(new RegExp('^([0-9]+)\\.([0-9]+)$')) !== null && (!allowsRange || (val >= begin && val <= end)) ) {
                    return true;
                }
            }
            return false;
        },
        errorMessage : '',
        errorMessageKey: 'badInt'
    });

    /*
     * Validate alpha numeric
     */
    $.formUtils.addValidator({
        name : 'alphanumeric',
        validatorFunction : function(val, $el, conf, language) {
            var patternStart = '^([a-zA-Z0-9',
                patternEnd = ']+)$',
                additionalChars = $el.attr('data-validation-allowing'),
                pattern = '';

            if( additionalChars ) {
                pattern = patternStart + additionalChars + patternEnd;
                var extra = additionalChars.replace(/\\/g, '');
                if( extra.indexOf(' ') > -1 ) {
                    extra = extra.replace(' ', '');
                    extra += ' and spaces ';
                }
                this.errorMessage = language.badAlphaNumeric + language.badAlphaNumericExtra + extra;
            } else {
                pattern = patternStart + patternEnd;
                this.errorMessage = language.badAlphaNumeric;
            }

            return new RegExp(pattern).test(val);
        },
        errorMessage : '',
        errorMessageKey: ''
    });

    /*
    * Validate against regexp
    */
    $.formUtils.addValidator({
        name : 'custom',
        validatorFunction : function(val, $el, conf) {
            var regexp = new RegExp($el.valAttr('regexp'));
            return regexp.test(val);
        },
        errorMessage : '',
        errorMessageKey: 'badCustomVal'
    });

    /*
    * Validate date
    */
    $.formUtils.addValidator({
        name : 'date',
        validatorFunction : function(date, $el, conf) {
            var dateFormat = 'yyyy-mm-dd';
            if($el.valAttr('format')) {
                dateFormat = $el.valAttr('format');
            }
            else if( conf.dateFormat ) {
                dateFormat = conf.dateFormat;
            }

            return $.formUtils.parseDate(date, dateFormat) !== false;
        },
        errorMessage : '',
        errorMessageKey: 'badDate'
    });


    /*
    * Validate group of checkboxes, validate qty required is checked
    * written by Steve Wasiura : http://stevewasiura.waztech.com
    * element attrs
    *    data-validation="checkbox_group"
    *    data-validation-qty="1-2"  // min 1 max 2
    *    data-validation-error-msg="chose min 1, max of 2 checkboxes"
    */
    $.formUtils.addValidator({
        name : 'checkbox_group',
        validatorFunction : function(val, $el, conf, lang, $form)
        {   // preset return var
            var checkResult = true;
            // get name of element. since it is a checkbox group, all checkboxes will have same name
            var elname = $el.attr('name');
            // get count of checked checkboxes with this name
            var checkedCount = $("input[type=checkbox][name^='"+elname+"']:checked", $form).length;
            // get el attr that specs qty required / allowed
            var qtyAllowed = $el.valAttr('qty');
            if (qtyAllowed == undefined) {
                var elementType = $el.get(0).nodeName;
                alert('Attribute "data-validation-qty" is missing from '+elementType+' named '+$el.attr('name'));
            }
            // call Utility function to check if count is above min, below max, within range etc.
            var qtyCheckResults = $.formUtils.numericRangeCheck(checkedCount, qtyAllowed) ;
            // results will be array, [0]=result str, [1]=qty int
            switch(qtyCheckResults[0] ) {   
                // outside allowed range
                case "out":
                    this.errorMessage = lang.groupCheckedRangeStart + qtyAllowed + lang.groupCheckedEnd;
                    checkResult = false;
                    break;
                // below min qty
                case "min":
                    this.errorMessage = lang.groupCheckedTooFewStart + qtyCheckResults[1] + lang.groupCheckedEnd;
                    checkResult = false;
                    break;
                // above max qty
                case "max":
                    this.errorMessage = lang.groupCheckedTooManyStart + qtyCheckResults[1] + lang.groupCheckedEnd;
                    checkResult = false;
                    break;
                // ok
                default:
                    checkResult = true;
            }
            
        return checkResult;
        
        }
     //   errorMessage : '', // set above in switch statement
     //   errorMessageKey: '' // not used
    });

})(jQuery);
