/* jqBootstrapValidation * A plugin for automating validation on Twitter Bootstrap formatted forms. * * v1.3.6 * * License: MIT - see LICENSE file * * http://ReactiveRaven.github.com/jqBootstrapValidation/ */ (function( $ ){ var createdElements = []; var defaults = { options: { prependExistingHelpBlock: false, sniffHtml: true, // sniff for 'required', 'maxlength', etc preventSubmit: true, // stop the form submit event from firing if validation fails submitError: false, // function called if there is an error when trying to submit submitSuccess: false, // function called just before a successful submit event is sent to the server semanticallyStrict: false, // set to true to tidy up generated HTML output autoAdd: { helpBlocks: true }, filter: function () { // return $(this).is(":visible"); // only validate elements you can see return true; // validate everything } }, methods: { init : function( options ) { var settings = $.extend(true, {}, defaults); settings.options = $.extend(true, settings.options, options); var $siblingElements = this; var uniqueForms = $.unique( $siblingElements.map( function () { return $(this).parents("form")[0]; }).toArray() ); $(uniqueForms).bind("submit", function (e) { var $form = $(this); var warningsFound = 0; var $inputs = $form.find("input,textarea,select").not("[type=submit],[type=image]").filter(settings.options.filter); $inputs.trigger("submit.validation").trigger("validationLostFocus.validation"); $inputs.each(function (i, el) { var $this = $(el), $controlGroup = $this.parents(".control-group").first(); if ( $controlGroup.hasClass("warning") ) { $controlGroup.removeClass("warning").addClass("error"); warningsFound++; } }); $inputs.trigger("validationLostFocus.validation"); if (warningsFound) { if (settings.options.preventSubmit) { e.preventDefault(); } $form.addClass("error"); if ($.isFunction(settings.options.submitError)) { settings.options.submitError($form, e, $inputs.jqBootstrapValidation("collectErrors", true)); } } else { $form.removeClass("error"); if ($.isFunction(settings.options.submitSuccess)) { settings.options.submitSuccess($form, e); } } }); return this.each(function(){ // Get references to everything we're interested in var $this = $(this), $controlGroup = $this.parents(".control-group").first(), $helpBlock = $controlGroup.find(".help-block").first(), $form = $this.parents("form").first(), validatorNames = []; // create message container if not exists if (!$helpBlock.length && settings.options.autoAdd && settings.options.autoAdd.helpBlocks) { $helpBlock = $('
'); $controlGroup.find('.controls').append($helpBlock); createdElements.push($helpBlock[0]); } // ============================================================= // SNIFF HTML FOR VALIDATORS // ============================================================= // *snort sniff snuffle* if (settings.options.sniffHtml) { var message = ""; // --------------------------------------------------------- // PATTERN // --------------------------------------------------------- if ($this.attr("pattern") !== undefined) { message = "Not in the expected format"; if ($this.data("validationPatternMessage")) { message = $this.data("validationPatternMessage"); } $this.data("validationPatternMessage", message); $this.data("validationPatternRegex", $this.attr("pattern")); } // --------------------------------------------------------- // MAX // --------------------------------------------------------- if ($this.attr("max") !== undefined || $this.attr("aria-valuemax") !== undefined) { var max = ($this.attr("max") !== undefined ? $this.attr("max") : $this.attr("aria-valuemax")); message = "Too high: Maximum of '" + max + "'"; if ($this.data("validationMaxMessage")) { message = $this.data("validationMaxMessage"); } $this.data("validationMaxMessage", message); $this.data("validationMaxMax", max); } // --------------------------------------------------------- // MIN // --------------------------------------------------------- if ($this.attr("min") !== undefined || $this.attr("aria-valuemin") !== undefined) { var min = ($this.attr("min") !== undefined ? $this.attr("min") : $this.attr("aria-valuemin")); message = "Too low: Minimum of '" + min + "'"; if ($this.data("validationMinMessage")) { message = $this.data("validationMinMessage"); } $this.data("validationMinMessage", message); $this.data("validationMinMin", min); } // --------------------------------------------------------- // MAXLENGTH // --------------------------------------------------------- if ($this.attr("maxlength") !== undefined) { message = "Too long: Maximum of '" + $this.attr("maxlength") + "' characters"; if ($this.data("validationMaxlengthMessage")) { message = $this.data("validationMaxlengthMessage"); } $this.data("validationMaxlengthMessage", message); $this.data("validationMaxlengthMaxlength", $this.attr("maxlength")); } // --------------------------------------------------------- // MINLENGTH // --------------------------------------------------------- if ($this.attr("minlength") !== undefined) { message = "Too short: Minimum of '" + $this.attr("minlength") + "' characters"; if ($this.data("validationMinlengthMessage")) { message = $this.data("validationMinlengthMessage"); } $this.data("validationMinlengthMessage", message); $this.data("validationMinlengthMinlength", $this.attr("minlength")); } // --------------------------------------------------------- // REQUIRED // --------------------------------------------------------- if ($this.attr("required") !== undefined || $this.attr("aria-required") !== undefined) { message = settings.builtInValidators.required.message; if ($this.data("validationRequiredMessage")) { message = $this.data("validationRequiredMessage"); } $this.data("validationRequiredMessage", message); } // --------------------------------------------------------- // NUMBER // --------------------------------------------------------- if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "number") { message = settings.builtInValidators.number.message; if ($this.data("validationNumberMessage")) { message = $this.data("validationNumberMessage"); } $this.data("validationNumberMessage", message); } // --------------------------------------------------------- // EMAIL // --------------------------------------------------------- if ($this.attr("type") !== undefined && $this.attr("type").toLowerCase() === "email") { message = "Not a valid email address"; if ($this.data("validationValidemailMessage")) { message = $this.data("validationValidemailMessage"); } else if ($this.data("validationEmailMessage")) { message = $this.data("validationEmailMessage"); } $this.data("validationValidemailMessage", message); } // --------------------------------------------------------- // MINCHECKED // --------------------------------------------------------- if ($this.attr("minchecked") !== undefined) { message = "Not enough options checked; Minimum of '" + $this.attr("minchecked") + "' required"; if ($this.data("validationMincheckedMessage")) { message = $this.data("validationMincheckedMessage"); } $this.data("validationMincheckedMessage", message); $this.data("validationMincheckedMinchecked", $this.attr("minchecked")); } // --------------------------------------------------------- // MAXCHECKED // --------------------------------------------------------- if ($this.attr("maxchecked") !== undefined) { message = "Too many options checked; Maximum of '" + $this.attr("maxchecked") + "' required"; if ($this.data("validationMaxcheckedMessage")) { message = $this.data("validationMaxcheckedMessage"); } $this.data("validationMaxcheckedMessage", message); $this.data("validationMaxcheckedMaxchecked", $this.attr("maxchecked")); } } // ============================================================= // COLLECT VALIDATOR NAMES // ============================================================= // Get named validators if ($this.data("validation") !== undefined) { validatorNames = $this.data("validation").split(","); } // Get extra ones defined on the element's data attributes $.each($this.data(), function (i, el) { var parts = i.replace(/([A-Z])/g, ",$1").split(","); if (parts[0] === "validation" && parts[1]) { validatorNames.push(parts[1]); } }); // ============================================================= // NORMALISE VALIDATOR NAMES // ============================================================= var validatorNamesToInspect = validatorNames; var newValidatorNamesToInspect = []; do // repeatedly expand 'shortcut' validators into their real validators { // Uppercase only the first letter of each name $.each(validatorNames, function (i, el) { validatorNames[i] = formatValidatorName(el); }); // Remove duplicate validator names validatorNames = $.unique(validatorNames); // Pull out the new validator names from each shortcut newValidatorNamesToInspect = []; $.each(validatorNamesToInspect, function(i, el) { if ($this.data("validation" + el + "Shortcut") !== undefined) { // Are these custom validators? // Pull them out! $.each($this.data("validation" + el + "Shortcut").split(","), function(i2, el2) { newValidatorNamesToInspect.push(el2); }); } else if (settings.builtInValidators[el.toLowerCase()]) { // Is this a recognised built-in? // Pull it out! var validator = settings.builtInValidators[el.toLowerCase()]; if (validator.type.toLowerCase() === "shortcut") { $.each(validator.shortcut.split(","), function (i, el) { el = formatValidatorName(el); newValidatorNamesToInspect.push(el); validatorNames.push(el); }); } } }); validatorNamesToInspect = newValidatorNamesToInspect; } while (validatorNamesToInspect.length > 0) // ============================================================= // SET UP VALIDATOR ARRAYS // ============================================================= var validators = {}; $.each(validatorNames, function (i, el) { // Set up the 'override' message var message = $this.data("validation" + el + "Message"); var hasOverrideMessage = (message !== undefined); var foundValidator = false; message = ( message ? message : "'" + el + "' validation failed " ) ; $.each( settings.validatorTypes, function (validatorType, validatorTemplate) { if (validators[validatorType] === undefined) { validators[validatorType] = []; } if (!foundValidator && $this.data("validation" + el + formatValidatorName(validatorTemplate.name)) !== undefined) { validators[validatorType].push( $.extend( true, { name: formatValidatorName(validatorTemplate.name), message: message }, validatorTemplate.init($this, el) ) ); foundValidator = true; } } ); if (!foundValidator && settings.builtInValidators[el.toLowerCase()]) { var validator = $.extend(true, {}, settings.builtInValidators[el.toLowerCase()]); if (hasOverrideMessage) { validator.message = message; } var validatorType = validator.type.toLowerCase(); if (validatorType === "shortcut") { foundValidator = true; } else { $.each( settings.validatorTypes, function (validatorTemplateType, validatorTemplate) { if (validators[validatorTemplateType] === undefined) { validators[validatorTemplateType] = []; } if (!foundValidator && validatorType === validatorTemplateType.toLowerCase()) { $this.data("validation" + el + formatValidatorName(validatorTemplate.name), validator[validatorTemplate.name.toLowerCase()]); validators[validatorType].push( $.extend( validator, validatorTemplate.init($this, el) ) ); foundValidator = true; } } ); } } if (! foundValidator) { $.error("Cannot find validation info for '" + el + "'"); } }); // ============================================================= // STORE FALLBACK VALUES // ============================================================= $helpBlock.data( "original-contents", ( $helpBlock.data("original-contents") ? $helpBlock.data("original-contents") : $helpBlock.html() ) ); $helpBlock.data( "original-role", ( $helpBlock.data("original-role") ? $helpBlock.data("original-role") : $helpBlock.attr("role") ) ); $controlGroup.data( "original-classes", ( $controlGroup.data("original-clases") ? $controlGroup.data("original-classes") : $controlGroup.attr("class") ) ); $this.data( "original-aria-invalid", ( $this.data("original-aria-invalid") ? $this.data("original-aria-invalid") : $this.attr("aria-invalid") ) ); // ============================================================= // VALIDATION // ============================================================= $this.bind( "validation.validation", function (event, params) { var value = getValue($this); // Get a list of the errors to apply var errorsFound = []; $.each(validators, function (validatorType, validatorTypeArray) { if (value || value.length || (params && params.includeEmpty) || (!!settings.validatorTypes[validatorType].blockSubmit && params && !!params.submitting)) { $.each(validatorTypeArray, function (i, validator) { if (settings.validatorTypes[validatorType].validate($this, value, validator)) { errorsFound.push(validator.message); } }); } }); return errorsFound; } ); $this.bind( "getValidators.validation", function () { return validators; } ); // ============================================================= // WATCH FOR CHANGES // ============================================================= $this.bind( "submit.validation", function () { return $this.triggerHandler("change.validation", {submitting: true}); } ); $this.bind( [ "keyup", "focus", "blur", "click", "keydown", "keypress", "change" ].join(".validation ") + ".validation", function (e, params) { var value = getValue($this); var errorsFound = []; $controlGroup.find("input,textarea,select").each(function (i, el) { var oldCount = errorsFound.length; $.each($(el).triggerHandler("validation.validation", params), function (j, message) { errorsFound.push(message); }); if (errorsFound.length > oldCount) { $(el).attr("aria-invalid", "true"); } else { var original = $this.data("original-aria-invalid"); $(el).attr("aria-invalid", (original !== undefined ? original : false)); } }); $form.find("input,select,textarea").not($this).not("[name=\"" + $this.attr("name") + "\"]").trigger("validationLostFocus.validation"); errorsFound = $.unique(errorsFound.sort()); // Were there any errors? if (errorsFound.length) { // Better flag it up as a warning. $controlGroup.removeClass("success error").addClass("warning"); // How many errors did we find? if (settings.options.semanticallyStrict && errorsFound.length === 1) { // Only one? Being strict? Just output it. $helpBlock.html(errorsFound[0] + ( settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "" )); } else { // Multiple? Being sloppy? Glue them together into an UL. $helpBlock.html("" + ( settings.options.prependExistingHelpBlock ? $helpBlock.data("original-contents") : "" )); } } else { $controlGroup.removeClass("warning error success"); if (value.length > 0) { $controlGroup.addClass("success"); } $helpBlock.html($helpBlock.data("original-contents")); } if (e.type === "blur") { $controlGroup.removeClass("success"); } } ); $this.bind("validationLostFocus.validation", function () { $controlGroup.removeClass("success"); }); }); }, destroy : function( ) { return this.each( function() { var $this = $(this), $controlGroup = $this.parents(".control-group").first(), $helpBlock = $controlGroup.find(".help-block").first(); // remove our events $this.unbind('.validation'); // events are namespaced. // reset help text $helpBlock.html($helpBlock.data("original-contents")); // reset classes $controlGroup.attr("class", $controlGroup.data("original-classes")); // reset aria $this.attr("aria-invalid", $this.data("original-aria-invalid")); // reset role $helpBlock.attr("role", $this.data("original-role")); // remove all elements we created if (createdElements.indexOf($helpBlock[0]) > -1) { $helpBlock.remove(); } } ); }, collectErrors : function(includeEmpty) { var errorMessages = {}; this.each(function (i, el) { var $el = $(el); var name = $el.attr("name"); var errors = $el.triggerHandler("validation.validation", {includeEmpty: true}); errorMessages[name] = $.extend(true, errors, errorMessages[name]); }); $.each(errorMessages, function (i, el) { if (el.length === 0) { delete errorMessages[i]; } }); return errorMessages; }, hasErrors: function() { var errorMessages = []; this.each(function (i, el) { errorMessages = errorMessages.concat( $(el).triggerHandler("getValidators.validation") ? $(el).triggerHandler("validation.validation", {submitting: true}) : [] ); }); return (errorMessages.length > 0); }, override : function (newDefaults) { defaults = $.extend(true, defaults, newDefaults); } }, validatorTypes: { callback: { name: "callback", init: function ($this, name) { return { validatorName: name, callback: $this.data("validation" + name + "Callback"), lastValue: $this.val(), lastValid: true, lastFinished: true }; }, validate: function ($this, value, validator) { if (validator.lastValue === value && validator.lastFinished) { return !validator.lastValid; } if (validator.lastFinished === true) { validator.lastValue = value; validator.lastValid = true; validator.lastFinished = false; var rrjqbvValidator = validator; var rrjqbvThis = $this; executeFunctionByName( validator.callback, window, $this, value, function (data) { if (rrjqbvValidator.lastValue === data.value) { rrjqbvValidator.lastValid = data.valid; if (data.message) { rrjqbvValidator.message = data.message; } rrjqbvValidator.lastFinished = true; rrjqbvThis.data("validation" + rrjqbvValidator.validatorName + "Message", rrjqbvValidator.message); // Timeout is set to avoid problems with the events being considered 'already fired' setTimeout(function () { rrjqbvThis.trigger("change.validation"); }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst } } ); } return false; } }, ajax: { name: "ajax", init: function ($this, name) { return { validatorName: name, url: $this.data("validation" + name + "Ajax"), lastValue: $this.val(), lastValid: true, lastFinished: true }; }, validate: function ($this, value, validator) { if (""+validator.lastValue === ""+value && validator.lastFinished === true) { return validator.lastValid === false; } if (validator.lastFinished === true) { validator.lastValue = value; validator.lastValid = true; validator.lastFinished = false; $.ajax({ url: validator.url, data: "value=" + value + "&field=" + $this.attr("name"), dataType: "json", success: function (data) { if (""+validator.lastValue === ""+data.value) { validator.lastValid = !!(data.valid); if (data.message) { validator.message = data.message; } validator.lastFinished = true; $this.data("validation" + validator.validatorName + "Message", validator.message); // Timeout is set to avoid problems with the events being considered 'already fired' setTimeout(function () { $this.trigger("change.validation"); }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst } }, failure: function () { validator.lastValid = true; validator.message = "ajax call failed"; validator.lastFinished = true; $this.data("validation" + validator.validatorName + "Message", validator.message); // Timeout is set to avoid problems with the events being considered 'already fired' setTimeout(function () { $this.trigger("change.validation"); }, 1); // doesn't need a long timeout, just long enough for the event bubble to burst } }); } return false; } }, regex: { name: "regex", init: function ($this, name) { return {regex: regexFromString($this.data("validation" + name + "Regex"))}; }, validate: function ($this, value, validator) { return (!validator.regex.test(value) && ! validator.negative) || (validator.regex.test(value) && validator.negative); } }, required: { name: "required", init: function ($this, name) { return {}; }, validate: function ($this, value, validator) { return !!(value.length === 0 && ! validator.negative) || !!(value.length > 0 && validator.negative); }, blockSubmit: true }, match: { name: "match", init: function ($this, name) { var element = $this.parents("form").first().find("[name=\"" + $this.data("validation" + name + "Match") + "\"]").first(); element.bind("validation.validation", function () { $this.trigger("change.validation", {submitting: true}); }); return {"element": element}; }, validate: function ($this, value, validator) { return (value !== validator.element.val() && ! validator.negative) || (value === validator.element.val() && validator.negative); }, blockSubmit: true }, max: { name: "max", init: function ($this, name) { return {max: $this.data("validation" + name + "Max")}; }, validate: function ($this, value, validator) { return (parseFloat(value, 10) > parseFloat(validator.max, 10) && ! validator.negative) || (parseFloat(value, 10) <= parseFloat(validator.max, 10) && validator.negative); } }, min: { name: "min", init: function ($this, name) { return {min: $this.data("validation" + name + "Min")}; }, validate: function ($this, value, validator) { return (parseFloat(value) < parseFloat(validator.min) && ! validator.negative) || (parseFloat(value) >= parseFloat(validator.min) && validator.negative); } }, maxlength: { name: "maxlength", init: function ($this, name) { return {maxlength: $this.data("validation" + name + "Maxlength")}; }, validate: function ($this, value, validator) { return ((value.length > validator.maxlength) && ! validator.negative) || ((value.length <= validator.maxlength) && validator.negative); } }, minlength: { name: "minlength", init: function ($this, name) { return {minlength: $this.data("validation" + name + "Minlength")}; }, validate: function ($this, value, validator) { return ((value.length < validator.minlength) && ! validator.negative) || ((value.length >= validator.minlength) && validator.negative); } }, maxchecked: { name: "maxchecked", init: function ($this, name) { var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); elements.bind("click.validation", function () { $this.trigger("change.validation", {includeEmpty: true}); }); return {maxchecked: $this.data("validation" + name + "Maxchecked"), elements: elements}; }, validate: function ($this, value, validator) { return (validator.elements.filter(":checked").length > validator.maxchecked && ! validator.negative) || (validator.elements.filter(":checked").length <= validator.maxchecked && validator.negative); }, blockSubmit: true }, minchecked: { name: "minchecked", init: function ($this, name) { var elements = $this.parents("form").first().find("[name=\"" + $this.attr("name") + "\"]"); elements.bind("click.validation", function () { $this.trigger("change.validation", {includeEmpty: true}); }); return {minchecked: $this.data("validation" + name + "Minchecked"), elements: elements}; }, validate: function ($this, value, validator) { return (validator.elements.filter(":checked").length < validator.minchecked && ! validator.negative) || (validator.elements.filter(":checked").length >= validator.minchecked && validator.negative); }, blockSubmit: true } }, builtInValidators: { email: { name: "Email", type: "shortcut", shortcut: "validemail" }, validemail: { name: "Validemail", type: "regex", regex: "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\\.[A-Za-z]{2,4}", message: "Not a valid email address" }, passwordagain: { name: "Passwordagain", type: "match", match: "password", message: "Does not match the given password" }, positive: { name: "Positive", type: "shortcut", shortcut: "number,positivenumber" }, negative: { name: "Negative", type: "shortcut", shortcut: "number,negativenumber" }, number: { name: "Number", type: "regex", regex: "([+-]?\\\d+(\\\.\\\d*)?([eE][+-]?[0-9]+)?)?", message: "Must be a number" }, integer: { name: "Integer", type: "regex", regex: "[+-]?\\\d+", message: "No decimal places allowed" }, positivenumber: { name: "Positivenumber", type: "min", min: 0, message: "Must be a positive number" }, negativenumber: { name: "Negativenumber", type: "max", max: 0, message: "Must be a negative number" }, required: { name: "Required", type: "required", message: "This is required" }, checkone: { name: "Checkone", type: "minchecked", minchecked: 1, message: "Check at least one option" } } }; var formatValidatorName = function (name) { return name .toLowerCase() .replace( /(^|\s)([a-z])/g , function(m,p1,p2) { return p1+p2.toUpperCase(); } ) ; }; var getValue = function ($this) { // Extract the value we're talking about var value = $this.val(); var type = $this.attr("type"); if (type === "checkbox") { value = ($this.is(":checked") ? value : ""); } if (type === "radio") { value = ($('input[name="' + $this.attr("name") + '"]:checked').length > 0 ? value : ""); } return value; }; function regexFromString(inputstring) { return new RegExp("^" + inputstring + "$"); } /** * Thanks to Jason Bunting via StackOverflow.com * * http://stackoverflow.com/questions/359788/how-to-execute-a-javascript-function-when-i-have-its-name-as-a-string#answer-359910 * Short link: http://tinyurl.com/executeFunctionByName **/ function executeFunctionByName(functionName, context /*, args*/) { var args = Array.prototype.slice.call(arguments).splice(2); var namespaces = functionName.split("."); var func = namespaces.pop(); for(var i = 0; i < namespaces.length; i++) { context = context[namespaces[i]]; } return context[func].apply(this, args); } $.fn.jqBootstrapValidation = function( method ) { if ( defaults.methods[method] ) { return defaults.methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); } else if ( typeof method === 'object' || ! method ) { return defaults.methods.init.apply( this, arguments ); } else { $.error( 'Method ' + method + ' does not exist on jQuery.jqBootstrapValidation' ); return null; } }; $.jqBootstrapValidation = function (options) { $(":input").not("[type=image],[type=submit]").jqBootstrapValidation.apply(this,arguments); }; })( jQuery );