/*! * Guards JavaScript jQuery Plugin v0.7.2 * https://github.com/on-site/guards.js * * Copyright 2010-2013, On-Site.com, http://www.on-site.com/ * Licensed under the MIT license. * * Includes code for email and phone number validation from the jQuery * Validation plugin. http://docs.jquery.com/Plugins/Validation * * Date: Mon Feb 25 03:47:45 2013 -0800 */ /** * This plugin is initially inspired by the standard Validation jQuery * plugin (http://docs.jquery.com/Plugins/Validation). * * To guard forms with this plugin, you must specify a set of guards * via $.guards.add(selector).using(guard) or * $.guard(selector).using(guard). These guards are then invoked from * the first one specified to the last one specified. * * Example usage: * * $(function() { * // Change the default error tag wrapper to a div. * $.guards.defaults.tag = "div"; * * // Enable the submit guard hook for the form with the "myForm" id. * $("#myForm").enableGuards(); * * // Guard that fields with "required" class have a value. * $.guard(".required").using("required"); * * // Guard that the text fields don't have the value "invalid" or "bad". * $.guard(":text").using(function(value, element) { * return $.inArray(value, ["invalid", "bad"]) == -1; * }).message("Don't use the keyword 'invalid' or 'bad'."); * * // Guard that fields with "email" class specify at least one * // value, but only show 1 error message if none is specified (but * // still highlight all of the fields). * $.guard(".email").using("oneRequired") * .message("Please specify at least one email.").grouped(); */ (function($) { $.guard = function(selector) { return $.guards.add(selector); }; $.guard.version = "0.7.2"; $.Guards = function() { var self = this; this._guards = []; this.options = { stackErrors: false }; this.constants = { notChecked: "" }; var defineGuard = function(aggregator, validator) { return function() { var args = $.makeArray(arguments); return function(value, element) { return self[aggregator](value, function(v) { return self[validator].apply(self, $.merge([v], args)); }); }; }; }; var minMaxMessage = function(formatting, minMaxFormat) { return function(options) { if (self.isNullOrUndefined(options)) { options = {}; } if (!$.isFunction(minMaxFormat)) { minMaxFormat = function(x) { return x; }; } var minDefined = !self.isNullOrUndefined(options.min); var maxDefined = !self.isNullOrUndefined(options.max); if (minDefined && maxDefined) { return self.format(formatting.minAndMax, minMaxFormat(options.min), minMaxFormat(options.max)); } if (minDefined) { return self.format(formatting.min, minMaxFormat(options.min)); } if (maxDefined) { return self.format(formatting.max, minMaxFormat(options.max)); } if (formatting.invalid) { return formatting.invalid; } return self.defaults.messages.undefined; }; }; var arrayMessage = function(formatting) { return function(array) { return self.format(formatting, $.map(array, function(x, i) { return $.trim("" + x); }).join(", ")); }; }; this.defaults = { grouped: false, guard: "required", guards: { allow: defineGuard("isAllValid", "isAllowed"), always: defineGuard("isAllValid", "always"), different: defineGuard("passThrough", "isDifferent"), disallow: defineGuard("isAllValid", "isDisallowed"), email: defineGuard("isAllValid", "isValidEmail"), "float": defineGuard("isAllValid", "isValidFloat"), "int": defineGuard("isAllValid", "isValidInt"), moneyUS: defineGuard("isAllValid", "isValidMoneyUS"), never: defineGuard("isAllValid", "never"), oneRequired: defineGuard("isAnyValid", "isPresent"), phoneUS: defineGuard("isAllValid", "isValidPhoneUS"), required: defineGuard("isAllValid", "isPresent"), same: defineGuard("passThrough", "isSame"), string: defineGuard("isAllValid", "isValidString") }, invalidClass: "invalid-field", messageClass: "error-message", messages: { allow: arrayMessage("Please enter one of: #{0}."), always: "There was an error.", different: "These values must all be different.", disallow: arrayMessage("Please don't enter: #{0}."), email: "Please enter a valid E-mail address.", "float": minMaxMessage({ minAndMax: "Please enter a number from #{0} to #{1}.", min: "Please enter a number no less than #{0}.", max: "Please enter a number no greater than #{0}.", invalid: "Please enter a number." }), "int": minMaxMessage({ minAndMax: "Please enter a number from #{0} to #{1}.", min: "Please enter a number no less than #{0}.", max: "Please enter a number no greater than #{0}.", invalid: "Please enter a number." }), moneyUS: minMaxMessage({ minAndMax: "Please enter a dollar amount from #{0} to #{1}.", min: "Please enter a dollar amount no less than #{0}.", max: "Please enter a dollar amount no greater than #{0}.", invalid: "Please enter a dollar amount." }, function(x) { return x.toFixed(2); }), never: "There was an error.", oneRequired: "Specify at least one.", phoneUS: "Please enter a valid phone number.", required: "This field is required.", same: "These values must all match.", string: minMaxMessage({ minAndMax: "Please enter a string with length #{0} to #{1}.", min: "Please enter a string with length at least #{0}.", max: "Please enter a string with length no greater than #{0}." }), "undefined": "Please fix this field." }, style: { field: { "background-color": "#ffff66" }, message: { color: "#ff0000", "margin-left": "10px" } }, tag: "span", target: function(errorElement) { var last = $(this).filter(":last"); if (last.is(":radio,:checkbox")) { last = $(last[0].nextSibling); } errorElement.insertAfter(last); return false; } }; }; $.Guards.prototype.version = "0.7.2"; // Really old jQuery doesn't have isArray, so use this alias // instead. $.Guards.prototype.isArray = $.isArray; if (!$.Guards.prototype.isArray) { var ARRAY_CONSTRUCTOR = [].constructor; var JQUERY_CONSTRUCTOR = jQuery; $.Guards.prototype.isArray = function(obj) { // Simplistic, but good enough for guards. return obj.constructor == ARRAY_CONSTRUCTOR || obj.constructor == JQUERY_CONSTRUCTOR; }; } // Alias for console.log, but check that such a thing exists. $.Guards.prototype.log = function(message) { if (console && console.log) { console.log(message); } }; // Utility method to trigger live events, but works against any // jQuery version that supports live events. $.Guards.prototype.on = function(selector, event, callback) { if ($.fn.on) { $(document).on(event, selector, callback); } else if ($.fn.delegate) { $(document).delegate(selector, event, callback); } else if ($.fn.live) { $(selector).live(event, callback); } else { this.log("Could not bind live handlers, probably because jQuery is too old."); } }; // Implementation of $.enableGuards(selector); $.Guards.prototype.enableGuards = function(selector) { var self = this; this.on(selector, "submit", function() { return self.guard($(this)); }); }; // Implementation of $.liveGuard(selector); $.Guards.prototype.liveGuard = function(selector) { var self = this; this.enableGuards(selector); this.on(selector, "change blur", function(e) { var $element = $(e.target); if (!$element.is(":guardable")) { return; } self.applyGuards(function(guard) { if (guard.isGrouped()) { if (guard.appliesTo($element)) { return $element.parents("form:first").find(":guardable"); } else { return false; } } else { return $element; } }); }); }; /** * Format all arguments into the first argument. This is a * convenience function similar to the C sprintf function, though * only with simple replacements. Replacements are formatted like * #{i} where i is a zero based index into the additional * arguments passed in to format beyond the first. * * Additional parameters not used will be ignored. * * Including formatting requests for parameters that don't exist * will throw an exception. * * The first argument must be the string that needs to be * formatted. Additional arguments are formatted into that * string. * * If any of the arguments to the format string include a string * that matches the #{i} format, the result could be erroneous. * * Example: $.guards.format("#{2} #{0} #{1}", "hello", "world", 3); // "3 hello world" * Example: $.guards.format("#{0} #{1}", "hello", "world", 3); // "hello world". * Example: $.guards.format("#{2} #{0} #{1}", "hello", "world"); // throws exception */ $.Guards.prototype.format = function() { var str = arguments[0]; if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { var regex = "\\#\\{" + (i - 1) + "\\}"; str = str.replace(new RegExp(regex, "g"), arguments[i]); } } if (/\#\{\d+\}/.test(str)) { throw new Error("Unmatched formatting found!"); } return str; }; /** * Add a style element to the document head which will style * elements with errors and their error messages. This will use * $.guards.defaults.style.field and * $.guards.defaults.style.message to determine what styling to * use. These defaults are initialized to a yellow background for * the invalid fields, and red color with a small left margin for * error messages. The selectors used to style these are * determined by $.guards.defaults.invalidClass and * $.guards.defaults.messageClass. * * There are 2 optional arguments allowed. The first is a * selector scope to use, and the second is overrides for styling. * Either, both or neither arguments are allowed. * * With a changed selector scope, the selector for the styles is * scoped to the given value. This can be useful for different * styling on different forms. Note that the keys to the object * in the "field" and "message" keys are used as css styles, and * the values to those keys are the values for those styles. * * The custom style overrides can be used to change the field, * message or both styles. * * Example: $.guards.style(); * Example: $.guards.style("#myForm"); * Example: $.guards.style({ field: { "color": "#ff0000" } }); * Example: $.guards.style({ message: { "color": "#ff6666" } }); * Example: $.guards.style("#myForm", { field: { "color": "#ff0000" }, message: { "color": "#ff6666" } }); */ $.Guards.prototype.style = function() { $("head").append(this.styleHtml.apply(this, arguments)); }; /** * Retrieve the style html as a string to use for the * $.guards.style() function. The documentation for that function * applies to this as well. */ $.Guards.prototype.styleHtml = function() { var fieldStyle = {}; var messageStyle = {}; var fieldSelector = "." + this.defaults.invalidClass; var messageSelector = "." + this.defaults.messageClass; var selectorScope, styles; if (this.defaults.style && this.defaults.style.field) { fieldStyle = this.defaults.style.field; } if (this.defaults.style && this.defaults.style.message) { messageStyle = this.defaults.style.message; } if (arguments.length == 1) { if (typeof(arguments[0]) == "string") { selectorScope = arguments[0]; } else { styles = arguments[0]; } } else if (arguments.length == 2) { selectorScope = arguments[0]; styles = arguments[1]; } if (styles && styles.field) { fieldStyle = styles.field; } if (styles && styles.message) { messageStyle = styles.message; } var result = ""; return result; }; /** * This guard test method is intended to always fail, thus it * returns false no matter what. */ $.Guards.prototype.always = function(value) { return false; }; /** * Return whether or not the value exists in the given allowed * list. The allowed parameter must be an array of valid values. * Blank is considered invalid unless it exists in the list. * Whitespace is ignored. */ $.Guards.prototype.isAllowed = function(value, allowed) { value = $.trim(value); return $.inArray(value, $.map(allowed, function(x, i) { return $.trim("" + x); })) != -1; }; /** * If the given values is an array, this will return false if the * given fn returns false for any value in the array. If the * given values is not an array, the result of calling the given * fn on that value is returned directly. * * Example: $.guards.isAllValid([true, false, true], function(x) { return x; }); // false * Example: $.guards.isAllValid(true, function(x) { return x; }); // true */ $.Guards.prototype.isAllValid = function(values, fn) { if (this.isArray(values)) { var result = true; $.each(values, function(i, x) { if (!fn(x)) { result = false; return false; } }); return result; } return fn(values); }; /** * If the given values is an array, this will return true if the * given fn returns true for any value in the array. If the given * values is not an array, the result of calling the given fn on * that value is returned directly. * * Example: $.guards.isAllValid([false, false, true], function(x) { return x; }); // true * Example: $.guards.isAllValid(false, function(x) { return x; }); // false */ $.Guards.prototype.isAnyValid = function(values, fn) { if (this.isArray(values)) { var result = false; $.each(values, function(i, x) { if (fn(x)) { result = true; return false; } }); return result; } return fn(values); }; /** * Return true if the value is null, undefined, an empty string, * or a string of just spaces. */ $.Guards.prototype.isBlank = function(value) { return this.isNullOrUndefined(value) || $.trim(value) == ""; }; /** * Return whether all the values in the given array are different. */ $.Guards.prototype.isDifferent = function(values) { if (values.length < 2) { return true; } var found = {}; var result = true; $.each(values, function(i, x) { if (found[x] === true) { result = false; return false; } found[x] = true; }); return result; }; /** * Return whether or not the value doesn't exist in the given * disallowed list. The disallowed parameter must be an array of * invalid values. Blank is considered valid unless it exists in * the list. Whitespace is ignored. */ $.Guards.prototype.isDisallowed = function(value, disallowed) { return !this.isAllowed(value, disallowed); }; /** * Return true if the value is null or undefined. */ $.Guards.prototype.isNullOrUndefined = function(value) { return value === null || value === undefined; }; /** * Return the negation of calling isBlank(value). */ $.Guards.prototype.isPresent = function(value) { return !this.isBlank(value); }; /** * Return whether all the values in the given array are the same. */ $.Guards.prototype.isSame = function(values) { if (values.length < 2) { return true; } var value = values[0]; var result = true; $.each(values, function(i, x) { if (x != value) { result = false; return false; } }); return result; }; /** * Return true if the given value is greater than or equal to * options.min (if options.min is defined) and less than or equal * to options.max (if options.max is defined). */ $.Guards.prototype.isInRange = function(value, options) { if (this.isNullOrUndefined(options)) { options = {}; } var bigEnough = this.isNullOrUndefined(options.min) || value >= options.min; var smallEnough = this.isNullOrUndefined(options.max) || value <= options.max; return bigEnough && smallEnough; }; /** * Return whether or not the value is a valid integer. * Appropriate options are min, max, both or neither. Blank is * valid as a number. */ $.Guards.prototype.isValidInt = function(value, options) { value = $.trim(value); if (value == "") { return true; } if (!/^(-|\+)?\d+$/.test(value)) { return false; } value = parseInt(value, 10); return this.isInRange(value, options); }; /** * Return whether or not the value is a valid float. Appropriate * options are min, max, both or neither. Blank is valid as a * number. */ $.Guards.prototype.isValidFloat = function(value, options) { value = $.trim(value); if (value == "") { return true; } if (!/^(-|\+)?(\d+)?\.?\d+$/.test(value)) { return false; } value = parseFloat(value); return this.isInRange(value, options); }; /** * Validates the given value is a valid US money value. It * optionally accepts min and max to specify the minimum or * maximum values. Blank is a valid money. */ $.Guards.prototype.isValidMoneyUS = function(value, options) { value = $.trim(value); if (value == "") { return true; } if (!/^\$?(-|\+)?\$?([\d,]+)?\.?\d+$/.test(value)) { return false; } // Only allow 1 $. var $i = value.indexOf("$"); if ($i >= 0 && value.indexOf("$", $i + 1) >= 0) { return false; } // Ensure if there are commas they are every 3 digits if (value.indexOf(",") >= 0 && !/^\$?(-|\+)?\$?[1-9]\d{0,2}(,\d{3,3})+(\.\d+)?$/.test(value)) { return false; } // Ensure no more than 2 digits after decimal if (value.indexOf(".") >= 0 && /\.\d{3,}$/.test(value)) { return false; } value = parseFloat(value.replace(/[\$,]/g, "")); return this.isInRange(value, options); }; /** * Validates the given value is a valid email. If options is * passed with allowDisplay as true, display emails will be * considered valid. A display email differs from a regular email * in that it can be contained with < and > with some text ahead * of that. Thus "John Doe " would be valid. */ $.Guards.prototype.isValidEmail = function(value, options) { if (options && options.allowDisplay) { var result = /.*\<([^>]+)\>\s*$/.exec(value); if (result) { value = result[1]; } } return value == "" || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\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])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value); }; /** * Validates the given value is a valid US phone number. */ $.Guards.prototype.isValidPhoneUS = function(value) { value = value.replace(/\s+/g, ""); return value == "" || value.length > 9 && value.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/); }; /** * Return whether or not the value is a valid string. Appropriate * options are min or max (or both). Whitespace is not * considered. */ $.Guards.prototype.isValidString = function(value, options) { value = $.trim(value); return this.isValidInt("" + value.length, options); }; /** * This guard test method is intended to never fail, thus it * returns true no matter what. It is intended to be used to set * up a guard that is triggered manually via triggerError(). */ $.Guards.prototype.never = function(value) { return true; }; /** * This is a utility function to act like isAnyValid or * isAllValid, except instead of aggregating the function results, * it passes the arguments on to the function and returns the * results. It makes the argument an array always. * * Example: $.guards.passThrough([true, false, true], function(x) { return x[1]; }); // false * Example: $.guards.passThrough(true, function(x) { return x[0]; }); // true */ $.Guards.prototype.passThrough = function(values, fn) { if (!this.isArray(values)) { values = [values]; } return fn(values); }; /** * Guard all elements with the specified jQuery selector. Using * is implicitly called with $.guards.defaults.guard, which * defaults to "required". Note that it is simpler to use * $.guard(selector) instead of $.guards.add(selector). * * Example: $.guards.add(".validPhone").using("phoneUS"); * Example: $.guards.add(".custom").using(function(value, element) { * return value != "invalid"; * }).message("Don't use the keyword 'invalid'."); * Example: $.guards.add(".custom").grouped().using(function(values, elements) { * return $.inArray("invalid", values) == -1; * }).target("#custom-error-location").tag("div") * .message("Don't use the keyword 'invalid'."); */ $.Guards.prototype.add = function(selector) { var guard = new $.Guard(selector, this); this._guards.push(guard); return guard; }; /** * Clear all errors on the form's guard fields, then invoke each * guard on the fields in order and guard them, adding errors * along the way as needed. Once done, focus the first visible * field with an error. */ $.Guards.prototype.guard = function(form) { var fields = form.guardableFields().clearErrors(); var result = this.applyGuards(function(guard) { return fields; }); fields.filter(":visible:has-error").eq(0).focus(); return result; }; /** * Apply all the guards to the fields returned from the given * callback. The callback will receive the guard, and is expected * to return the fields to guard against. If it returns false, * that guard is skipped and does not affect the return value. */ $.Guards.prototype.applyGuards = function(callback) { var result = true; var self = this; $.each(this._guards, function(index, guard) { var fields = callback(guard); if (fields !== false && !self.test(guard, fields)) { result = false; } }); return result; }; /** * Use the given guard to test the given guarded fields. Errors * will be applied if the field doesn't have an error yet. */ $.Guards.prototype.test = function(guard, fields) { if (guard._grouped) { return guard.test(fields); } var result = true; fields.each(function() { if (!guard.test(this)) { result = false; } }); return result; }; $.Guard = function(selector, guards) { this._guards = guards || $.guards; this._selector = selector; this._grouped = this._guards.defaults.grouped; this._tag = this._guards.defaults.tag; this._messageClass = this._guards.defaults.messageClass; this._invalidClass = this._guards.defaults.invalidClass; this._target = this._guards.defaults.target; this.using(this._guards.defaults.guard); }; /** * Guard inputs using a specified guard. The guard may be either * a string or a function. When it is a string, it must match one * of the pre-defined guards defined in $.guards.defaults.guards. * The function is expected to have 2 arguments. The first is the * value of the element being guarded, and the second is the * actual element. If grouped is true, it will be an array of all * matched values and all matched elements (the order of values * will match the order of elements). Radio buttons are passed as * separate values and elements, but the value of each will be the * same. Specifically, the value of the checked radio button is * the value used, unless none are checked, in which case * $.guards.constants.notChecked will be used (which is predefined * as an empty string). * * Note that the message is implicitly set when this method is * called. If the guard is a string, the message will be set to * $.guards.defaults.messages[guard]. If it is a function, it * will be set to $.guards.defaults.messages.undefined. * * Example: $.guard(".required").using("required"); * Example: $.guard(".required").using(function(value, element) { * return $.inArray("invalid", values) == -1; * }); */ $.Guard.prototype.using = function(guard) { if (typeof(guard) == "string") { var args = []; if (arguments.length > 1) { args = $.makeArray(arguments).slice(1); } var fn = this._guards.defaults.guards[guard]; if (this._guards.isNullOrUndefined(fn)) { throw new Error("There is no standard guard named '" + guard + "'"); } this._guard = fn.apply(this._guards.defaults.guards, args); var message = this._guards.defaults.messages[guard]; if ($.isFunction(message)) { message = message.apply(this._guards.defaults.messages, args); } return this.message(message); } this._guard = guard; return this.message(this._guards.defaults.messages.undefined); }; /** * Specify a precondition for this guard. The precondition should * be a function that accepts the element and element value as the * parameters, like a custom guard function. The precondition is * executed before the guard when any given input is about to be * guarded. If the precondition returns false explicitly, the * guard will not be executed and the field will be considered * valid. Any other return value means the precondition passed * (even no return). If the guard is grouped, the parameters will * be the array of values and elements (like for a custom guard * function). * * // Only require this if #other_element is checked. * Example: $.guard(".required").using("required").precondition(function(value, element) { * return $("#other_element").is(":checked"); * }); * * @since 0.4 */ $.Guard.prototype.precondition = function(fn) { this._precondition = fn; return this; }; /** * Return whether or not this guard is grouped. */ $.Guard.prototype.isGrouped = function() { return this._grouped; }; /** * Specify whether to group element guarding by passing all values * and elements at once instead of one at a time. When grouped, * only 1 error message is added, and it is added after the last * element. This defaults to $.guards.defaults.grouped. If an * argument is passed, the value is used as the grouped value, * otherwise invoking this method will set grouped to true. * * Example: $.guard(".required").using("required").grouped(); * Example: $.guard(".required").using("required").grouped(true); */ $.Guard.prototype.grouped = function() { if (arguments.length == 0) { return this.grouped(true); } this._grouped = arguments[0]; return this; }; /** * Set the type of tag to surround the error message with * (defaults to $.guards.defaults.tag, which defaults to span). * * Example: $.guard(".required").using("required").tag("div"); */ $.Guard.prototype.tag = function(tag) { this._tag = tag; return this.resetMessageFn(); }; $.Guard.prototype.messageClass = function(messageClass) { this._messageClass = messageClass; return this.resetMessageFn(); }; /** * Set the error message to display on errors. If using is called * with a string, this is implicitly invoked using * $.guards.defaults.messages[usingValue]. If using is called * with a function, this is implicitly invoked using * $.guards.defaults.messages.undefined. * * Example: $.guard(".required").using("required").message("Enter something!"); */ $.Guard.prototype.message = function(message) { this._message = message; return this.resetMessageFn(); }; $.Guard.prototype.invalidClass = function(invalidClass) { this._invalidClass = invalidClass; return this; }; $.Guard.prototype.resetMessageFn = function() { var self = this; return this.messageFn(function() { return $('<' + self._tag + ' class="' + self._messageClass + '"/>').html(self._message); }); }; $.Guard.prototype.messageFn = function(messageFn) { this._messageFn = messageFn; return this; }; $.Guard.prototype.errorElement = function() { return this._messageFn(); }; $.Guard.prototype.attachError = function(elements, errorElement) { if (this._target && $.isFunction(this._target)) { var result = this._target.call(elements, errorElement); if (result !== false) { errorElement.appendTo($(result).eq(0)); } } else if (this._target) { errorElement.appendTo($(this._target).eq(0)); } else { throw new Error("The target must be a function or selector!"); } }; /** * Set the target for where error messages should be appended to. * By default, the error is placed after the error element, but * when a target is specified, the error is appended within. The * target may be either a selector, function, element or set of * elements, however, only the first element is used as the target * location for errors. If a function is specified, it will be * called when there is a new error with the invalid element (or * set of elements if it is a grouped guard) as the "this" * reference. The returned value should be a single element, * though if an array of elements is returned (or a jQuery * selected set of elements), only the first element will be used * as the target. Alternatively the function can take a single * argument that specifies the error element to add to the DOM, * and the function is expected to add the element and return * false (indicating that it has taken care of adding the error * element). * * The default target is a function that appends the error after * the last element and returns false. The default can be changed * via $.guards.defaults.target. * * Example: $.guard(".required").using("required").target("#my-errors"); * Example: $.guard(".required").using("required").target(function() { return $(this).nextAll(".error:eq(0)"); }); * Example: $.guard(".required").using("required").target(function(errorElement) { * errorElement.appendTo($("#myErrors")); * return false; * }); */ $.Guard.prototype.target = function(target) { this._target = target; return this; }; /** * Determine if this guard applies to the given element (or * elements). */ $.Guard.prototype.appliesTo = function(element) { return $(element).filter(this._selector).size() > 0; }; /** * Using this guard, test the given element. If this guard is * grouped, the element is expected to actually be all field * elements. Returns false but doesn't apply the guard if there * are already errors detected on the element(s). Returns true if * the selector defined for this guard doesn't apply to this * element(s). Otherwise, applies the guard and adds an error if * it fails. */ $.Guard.prototype.test = function(element) { var $elements = $(element).filter(this._selector); if ($elements.size() == 0) { return true; } if (!this._guards.options.stackErrors && $elements.hasErrors()) { return false; } var result; // Grouped expects a group of elements, while non-grouped // expects a single element. if (this._grouped) { var values = []; var elements = []; $elements.each(function() { values.push($(this).inputValue(this._guards)); elements.push(this); }); if (this._precondition && this._precondition(values, elements) === false) { result = true; } else { result = this._guard(values, elements); } } else { var value = $elements.inputValue(this._guards); if (this._precondition && this._precondition(value, element) === false) { result = true; } else { result = this._guard(value, element); } } if (!result) { this.triggerError($elements); } return result; }; /** * Explicitly trigger the error for this guard on all the elements * provided to this function. The elements are wrapped with a * jQuery object, so they may be a single element, a list of * elements, a jQuery selected set of elements, or even a valid * jQuery selector. Note that the elements don't have to be valid * for this guard to be applied. */ $.Guard.prototype.triggerError = function(elements) { if (this._grouped) { $(elements).addSingleError(this); } else { $(elements).addError(this); } return this; } $.GuardError = function(guard, element, errorElement, linked) { this._guard = guard; this._element = element; this._errorElement = errorElement; this._linked = linked; this._cleared = false; }; /** * Clear this error and any errors linked with it (grouped guards * and radio buttons cause all elements involved to be linked). */ $.GuardError.prototype.clear = function() { if (this._cleared) { return; } this._errorElement.remove(); var index = $.inArray(this, this._element.errors); if (index >= 0) { this._element.errors.splice(index, 1); } if (!$(this._element).hasErrorsWithInvalidClass(this._guard._invalidClass)) { $(this._element).removeClass(this._guard._invalidClass); } this._cleared = true; while (this._linked.length > 0) { this._linked.shift().clear(); } }; /** * Find any applicable fields for this selected item. Applicable * fields are any inputs, textareas or selects. */ $.fn.guardableFields = function() { return this.find(":guardable"); }; /** * Return the result of guarding the selected form. */ $.fn.guard = function() { return $.guards.guard(this); }; /** * Explicitly trigger the given guard's error all the selected * elements. Note that the selected elements don't have to be * valid for this guard to be applied. This is equivalent to * calling guard.triggerError($this); */ $.fn.triggerError = function(guard) { guard.triggerError(this); }; /** * Add a single error message, but mark every selected element as * in error pointing to the single error message. This differs * from addError because addError will add a new error message for * each selected element instead of just 1. */ $.fn.addSingleError = function(guard) { if (this.size() == 0) { $.guards.log("Attempted to add error to nothing."); return this; } var element = guard.errorElement(); guard.attachError(this, element); this.addClass(guard._invalidClass); var linked = []; return this.each(function() { if (!this.errors) { this.errors = []; } var error = new $.GuardError(guard, this, element, linked); linked.push(error); this.errors.push(error); }); }; /** * Add an error message to each of the selected elements, with an * optional error target to place it. The target can be a * selector, though it will use the first selected element as the * target. */ $.fn.addError = function(guard) { var radiosAdded = {}; return this.each(function() { var $this = $(this); if ($this.is(":radio")) { var name = $this.attr("name"); if (radiosAdded[name]) { return; } radiosAdded[name] = true; var radios = $("input[name='" + name + "']:radio", $this.parents("form")); radios.addSingleError(guard); } else { $this.addSingleError(guard); } }); }; /** * Obtain all errors attached to the selected elements. */ $.fn.errors = function() { var result = []; this.each(function() { if (this.errors && this.errors.length > 0) { result.push.apply(result, this.errors); } }); return result; }; /** * Clear errors attached to the selected elements. */ $.fn.clearErrors = function() { $.each(this.errors(), function(index, error) { error.clear(); }); return this; }; /** * Determine if any errors exist in the selected elements. */ $.fn.hasErrors = function() { return this.errors().length > 0; }; $.fn.hasErrorsWithInvalidClass = function(invalidClass) { var result = false; $.each(this.errors(), function(i, error) { if (error._guard._invalidClass == invalidClass) { result = true; return false; } }); return result; }; /** * Obtain the value of the first selected input. This differs * from val() in that it will properly get the value of a set of * radio buttons. */ $.fn.inputValue = function(guards) { guards = guards || $.guards; if (this.is(":radio")) { var checked = $("input[name='" + this.attr("name") + "']:radio:checked", this.parents("form")); if (checked.size() == 0) { return guards.constants.notChecked; } return checked.val(); } if (this.is(":checkbox")) { if (this.is(":checked")) { return this.val(); } return guards.constants.notChecked; } return this.val(); }; /** * Enable guards of this form by attaching a submit button to it * that returns the result of calling guard(). This will block * any other submit event handlers and prevent the form from being * submitted if guarding fails. */ $.fn.enableGuards = function() { return this.submit(function() { return $(this).guard(); }); }; /** * Enable guards for any form that matches the given selector. * This uses live events to catch submit on forms matching the * selector. */ $.enableGuards = function(selector) { $.guards.enableGuards(selector); }; /** * Live guard the form(s) in the given selector. This will bind * live on change and blur events that will guard the elements * when they change. It will also guard the form when it is * submitted. */ $.liveGuard = function(selector) { $.guards.liveGuard(selector); }; $.extend($.expr[":"], { "has-error": function(x) { return new Boolean(x.errors && x.errors.length > 0).valueOf(); }, "guardable": function(x) { return x.tagName.toLowerCase() == "input" || x.tagName.toLowerCase() == "textarea" || x.tagName.toLowerCase() == "select"; } }); $.guards = new $.Guards(); $(function() { // Clear errors when the user expresses intent to fix the // errors. var clearFn = function() { $(this).clearErrors(); }; $.guards.on(":has-error", "change", clearFn); $.guards.on(":has-error:radio,:has-error:checkbox", "mouseup", clearFn); $.guards.on("select:has-error", "mousedown", clearFn); // Make sure we don't clear it if there was no error when the // keydown happened, otherwise a submit on enter will have the // error flash and then go away on the keyup. $.guards.on(":has-error", "keydown", function() { this.clearable = true; }); $.guards.on(":has-error", "keyup", function() { if (this.clearable) { this.clearable = false; $(this).clearErrors(); } }); }); })(jQuery);