(function ($) {
    'use strict';
    var $window = $(window),
            _applyErrorStyle = function ($elem, conf) {
                var $parent = $elem.parent();
                $elem
                        .addClass(conf.errorElementClass)
                        .removeClass('valid');
                if ($parent.hasClass("input-group"))
                    $parent = $parent.parent();

                $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);
                    var $parent = $(this).parent();

                    if ($parent.hasClass("input-group"))
                        $parent = $parent.parent();
                    $(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);
                var $parent = $input.parent();
                if ($parent.hasClass("input-group"))
                    $parent = $parent.parent();
                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 = $parent.find('.' + conf.errorMessageClass + '.jsst-help-block');
                    if ($mess.length == 0) {
                        $mess = $('<span></span>').addClass('jsst-help-block').addClass(conf.errorMessageClass);
                        $mess.appendTo($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, true, 'blur');
                });

        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, true, 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)
                .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('jsst-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} eventType
     * @return {jQuery}
     */
    $.fn.validateInputOnBlur = function (language, conf, attachKeyupEvent, eventType) {
        $.formUtils.eventType = eventType;

        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, eventType);
                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,
                        eventType
                        );

        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
                        .unbind('keyup.validation')
                        .bind('keyup.validation', 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'
                        );

                // 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 null if no validation
         * should take place, returns true if valid or error message if not valid
         *
         * @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 = null;
                    if (eventContext != 'keyup' || validator.validateOnKeyUp) {
                        isValid = validator.validatorFunction(value, $elem, conf, language, $form);
                    }

                    if (!isValid) {
                        validationErrorMsg = null;
                        if (isValid !== null) {
                            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 {
                    throw new Error('Using undefined validator "' + rule + '"');
                }

            }, ' ');

            if (typeof validationErrorMsg == 'string') {
                $elem.trigger('validation', false);
                return validationErrorMsg;
            } else if (validationErrorMsg === null && !conf.addValidClassOnAll) {
                return null;
            } else {
                $elem.trigger('validation', true);
                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
            // since no other returns executed, value is in allowed range
            return ["ok"];
        },
        _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 the 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) {
				var regex = /^[a-zA-Z0-9.'!#$%&‚Äô*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
				return regex.test(email)

                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,
                        steps = $el.valAttr('step') || '',
                        allowsSteps = false;

                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 (steps != "")
                {
                    allowsSteps = 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)) && (!allowsSteps || (val % steps == 0))) {
                    return true;
                }
                if (allowing.indexOf('float') > -1 && val.match(new RegExp('^([0-9]+)\\.([0-9]+)$')) !== null && (!allowsRange || (val >= begin && val <= end)) && (!allowsSteps || (val % steps == 0))) {
                    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);
