UNPKG

formvalidation

Version:

The best jQuery plugin to validate form fields. Support Bootstrap, Foundation, Pure, SemanticUI, UIKit and custom frameworks

1,126 lines (1,006 loc) 99.5 kB
/** * FormValidation (http://formvalidation.io) * The best jQuery plugin to validate form fields. Support Bootstrap, Foundation, Pure, SemanticUI, UIKit and custom frameworks * * @author https://twitter.com/nghuuphuoc * @copyright (c) 2013 - 2015 Nguyen Huu Phuoc * @license http://formvalidation.io/license/ */ // Register the namespace window.FormValidation = { AddOn: {}, // Add-ons Framework: {}, // Supported frameworks I18n: {}, // i18n Validator: {} // Available validators }; if (typeof jQuery === 'undefined') { throw new Error('FormValidation requires jQuery'); } (function($) { var version = $.fn.jquery.split(' ')[0].split('.'); if ((+version[0] < 2 && +version[1] < 9) || (+version[0] === 1 && +version[1] === 9 && +version[2] < 1)) { throw new Error('FormValidation requires jQuery version 1.9.1 or higher'); } }(jQuery)); (function($) { // TODO: Remove backward compatibility in v0.7.0 /** * Constructor * * @param {jQuery|String} form The form element or selector * @param {Object} options The options * @param {String} [namespace] The optional namespace which is used for data-{namespace}-xxx attributes and internal data. * Currently, it's used to support backward version * @constructor */ FormValidation.Base = function(form, options, namespace) { this.$form = $(form); this.options = $.extend({}, $.fn.formValidation.DEFAULT_OPTIONS, options); this._namespace = namespace || 'fv'; this.$invalidFields = $([]); // Array of invalid fields this.$submitButton = null; // The submit button which is clicked to submit form this.$hiddenButton = null; // Validating status this.STATUS_NOT_VALIDATED = 'NOT_VALIDATED'; this.STATUS_VALIDATING = 'VALIDATING'; this.STATUS_INVALID = 'INVALID'; this.STATUS_VALID = 'VALID'; this.STATUS_IGNORED = 'IGNORED'; // Determine the event that is fired when user change the field value // Most modern browsers supports input event except IE 7, 8. // IE 9 supports input event but the event is still not fired if I press the backspace key. // Get IE version // https://gist.github.com/padolsey/527683/#comment-7595 var ieVersion = (function() { var v = 3, div = document.createElement('div'), a = div.all || []; while (div.innerHTML = '<!--[if gt IE '+(++v)+']><br><![endif]-->', a[0]) {} return v > 4 ? v : !v; }()); var el = document.createElement('div'); this._changeEvent = (ieVersion === 9 || !('oninput' in el)) ? 'keyup' : 'input'; // The flag to indicate that the form is ready to submit when a remote/callback validator returns this._submitIfValid = null; // Field elements this._cacheFields = {}; this._init(); }; FormValidation.Base.prototype = { constructor: FormValidation.Base, /** * Check if the number of characters of field value exceed the threshold or not * * @param {jQuery} $field The field element * @returns {Boolean} */ _exceedThreshold: function($field) { var ns = this._namespace, field = $field.attr('data-' + ns + '-field'), threshold = this.options.fields[field].threshold || this.options.threshold; if (!threshold) { return true; } var cannotType = $.inArray($field.attr('type'), ['button', 'checkbox', 'file', 'hidden', 'image', 'radio', 'reset', 'submit']) !== -1; return (cannotType || $field.val().length >= threshold); }, /** * Init form */ _init: function() { var that = this, ns = this._namespace, options = { addOns: {}, autoFocus: this.$form.attr('data-' + ns + '-autofocus'), button: { selector: this.$form.attr('data-' + ns + '-button-selector') || this.$form.attr('data-' + ns + '-submitbuttons'), // Support backward disabled: this.$form.attr('data-' + ns + '-button-disabled') }, control: { valid: this.$form.attr('data-' + ns + '-control-valid'), invalid: this.$form.attr('data-' + ns + '-control-invalid') }, err: { clazz: this.$form.attr('data-' + ns + '-err-clazz'), container: this.$form.attr('data-' + ns + '-err-container') || this.$form.attr('data-' + ns + '-container'), // Support backward parent: this.$form.attr('data-' + ns + '-err-parent') }, events: { formInit: this.$form.attr('data-' + ns + '-events-form-init'), formError: this.$form.attr('data-' + ns + '-events-form-error'), formSuccess: this.$form.attr('data-' + ns + '-events-form-success'), fieldAdded: this.$form.attr('data-' + ns + '-events-field-added'), fieldRemoved: this.$form.attr('data-' + ns + '-events-field-removed'), fieldInit: this.$form.attr('data-' + ns + '-events-field-init'), fieldError: this.$form.attr('data-' + ns + '-events-field-error'), fieldSuccess: this.$form.attr('data-' + ns + '-events-field-success'), fieldStatus: this.$form.attr('data-' + ns + '-events-field-status'), localeChanged: this.$form.attr('data-' + ns + '-events-locale-changed'), validatorError: this.$form.attr('data-' + ns + '-events-validator-error'), validatorSuccess: this.$form.attr('data-' + ns + '-events-validator-success'), validatorIgnored: this.$form.attr('data-' + ns + '-events-validator-ignored') }, excluded: this.$form.attr('data-' + ns + '-excluded'), icon: { valid: this.$form.attr('data-' + ns + '-icon-valid') || this.$form.attr('data-' + ns + '-feedbackicons-valid'), // Support backward invalid: this.$form.attr('data-' + ns + '-icon-invalid') || this.$form.attr('data-' + ns + '-feedbackicons-invalid'), // Support backward validating: this.$form.attr('data-' + ns + '-icon-validating') || this.$form.attr('data-' + ns + '-feedbackicons-validating'), // Support backward feedback: this.$form.attr('data-' + ns + '-icon-feedback') }, live: this.$form.attr('data-' + ns + '-live'), locale: this.$form.attr('data-' + ns + '-locale'), message: this.$form.attr('data-' + ns + '-message'), onError: this.$form.attr('data-' + ns + '-onerror'), onSuccess: this.$form.attr('data-' + ns + '-onsuccess'), row: { selector: this.$form.attr('data-' + ns + '-row-selector') || this.$form.attr('data-' + ns + '-group'), // Support backward valid: this.$form.attr('data-' + ns + '-row-valid'), invalid: this.$form.attr('data-' + ns + '-row-invalid'), feedback: this.$form.attr('data-' + ns + '-row-feedback') }, threshold: this.$form.attr('data-' + ns + '-threshold'), trigger: this.$form.attr('data-' + ns + '-trigger'), verbose: this.$form.attr('data-' + ns + '-verbose'), fields: {} }; this.$form // Disable client side validation in HTML 5 .attr('novalidate', 'novalidate') .addClass(this.options.elementClass) // Disable the default submission first .on('submit.' + ns, function(e) { e.preventDefault(); that.validate(); }) .on('click.' + ns, this.options.button.selector, function() { that.$submitButton = $(this); // The user just click the submit button that._submitIfValid = true; }); if (this.options.declarative === true || this.options.declarative === 'true') { // Find all fields which have either "name" or "data-{namespace}-field" attribute this.$form .find('[name], [data-' + ns + '-field]') .each(function () { var $field = $(this), field = $field.attr('name') || $field.attr('data-' + ns + '-field'), opts = that._parseOptions($field); if (opts) { $field.attr('data-' + ns + '-field', field); options.fields[field] = $.extend({}, opts, options.fields[field]); } }); } this.options = $.extend(true, this.options, options); // Normalize the err.parent option if ('string' === typeof this.options.err.parent) { this.options.err.parent = new RegExp(this.options.err.parent); } // Support backward if (this.options.container) { this.options.err.container = this.options.container; delete this.options.container; } if (this.options.feedbackIcons) { this.options.icon = $.extend(true, this.options.icon, this.options.feedbackIcons); delete this.options.feedbackIcons; } if (this.options.group) { this.options.row.selector = this.options.group; delete this.options.group; } if (this.options.submitButtons) { this.options.button.selector = this.options.submitButtons; delete this.options.submitButtons; } // If the locale is not found, reset it to default one if (!FormValidation.I18n[this.options.locale]) { this.options.locale = $.fn.formValidation.DEFAULT_OPTIONS.locale; } // Parse the add-on options from HTML attributes if (this.options.declarative === true || this.options.declarative === 'true') { this.options = $.extend(true, this.options, { addOns: this._parseAddOnOptions() }); } // When pressing Enter on any field in the form, the first submit button will do its job. // The form then will be submitted. // I create a first hidden submit button this.$hiddenButton = $('<button/>') .attr('type', 'submit') .prependTo(this.$form) .addClass('fv-hidden-submit') .css({ display: 'none', width: 0, height: 0 }); this.$form .on('click.' + this._namespace, '[type="submit"]', function(e) { // #746: Check if the button click handler returns false if (!e.isDefaultPrevented()) { var $target = $(e.target), // The button might contain HTML tag $button = $target.is('[type="submit"]') ? $target.eq(0) : $target.parent('[type="submit"]').eq(0); // Don't perform validation when clicking on the submit button/input // which aren't defined by the 'button.selector' option if (that.options.button.selector && !$button.is(that.options.button.selector) && !$button.is(that.$hiddenButton)) { that.$form.off('submit.' + that._namespace).submit(); } } }); for (var field in this.options.fields) { this._initField(field); } // Init the add-ons for (var addOn in this.options.addOns) { if ('function' === typeof FormValidation.AddOn[addOn].init) { FormValidation.AddOn[addOn].init(this, this.options.addOns[addOn]); } } this.$form.trigger($.Event(this.options.events.formInit), { bv: this, // Support backward fv: this, options: this.options }); // Prepare the events if (this.options.onSuccess) { this.$form.on(this.options.events.formSuccess, function(e) { FormValidation.Helper.call(that.options.onSuccess, [e]); }); } if (this.options.onError) { this.$form.on(this.options.events.formError, function(e) { FormValidation.Helper.call(that.options.onError, [e]); }); } }, /** * Init field * * @param {String|jQuery} field The field name or field element */ _initField: function(field) { var ns = this._namespace, fields = $([]); switch (typeof field) { case 'object': fields = field; field = field.attr('data-' + ns + '-field'); break; case 'string': fields = this.getFieldElements(field); fields.attr('data-' + ns + '-field', field); break; default: break; } // We don't need to validate non-existing fields if (fields.length === 0) { return; } if (this.options.fields[field] === null || this.options.fields[field].validators === null) { return; } var validatorName; for (validatorName in this.options.fields[field].validators) { if (!FormValidation.Validator[validatorName]) { delete this.options.fields[field].validators[validatorName]; } } if (this.options.fields[field].enabled === null) { this.options.fields[field].enabled = true; } var that = this, total = fields.length, type = fields.attr('type'), updateAll = (total === 1) || ('radio' === type) || ('checkbox' === type), trigger = this._getFieldTrigger(fields.eq(0)), events = $.map(trigger, function(item) { return item + '.update.' + ns; }).join(' '); for (var i = 0; i < total; i++) { var $field = fields.eq(i), row = this.options.fields[field].row || this.options.row.selector, $parent = $field.closest(row), // Allow user to indicate where the error messages are shown // Support backward container = ('function' === typeof (this.options.fields[field].container || this.options.fields[field].err || this.options.err.container)) ? (this.options.fields[field].container || this.options.fields[field].err || this.options.err.container).call(this, $field, this) : (this.options.fields[field].container || this.options.fields[field].err || this.options.err.container), $message = (container && container !== 'tooltip' && container !== 'popover') ? $(container) : this._getMessageContainer($field, row); if (container && container !== 'tooltip' && container !== 'popover') { $message.addClass(this.options.err.clazz); } // Remove all error messages and feedback icons $message.find('.' + this.options.err.clazz.split(' ').join('.') + '[data-' + ns + '-validator][data-' + ns + '-for="' + field + '"]').remove(); $parent.find('i[data-' + ns + '-icon-for="' + field + '"]').remove(); // Whenever the user change the field value, mark it as not validated yet $field.off(events).on(events, function() { that.updateStatus($(this), that.STATUS_NOT_VALIDATED); }); // Create help block elements for showing the error messages $field.data(ns + '.messages', $message); for (validatorName in this.options.fields[field].validators) { $field.data(ns + '.result.' + validatorName, this.STATUS_NOT_VALIDATED); if (!updateAll || i === total - 1) { $('<small/>') .css('display', 'none') .addClass(this.options.err.clazz) .attr('data-' + ns + '-validator', validatorName) .attr('data-' + ns + '-for', field) .attr('data-' + ns + '-result', this.STATUS_NOT_VALIDATED) .html(this._getMessage(field, validatorName)) .appendTo($message); } // Init the validator if ('function' === typeof FormValidation.Validator[validatorName].init) { FormValidation.Validator[validatorName].init(this, $field, this.options.fields[field].validators[validatorName]); } } // Prepare the feedback icons if (this.options.fields[field].icon !== false && this.options.fields[field].icon !== 'false' && this.options.icon && this.options.icon.valid && this.options.icon.invalid && this.options.icon.validating && (!updateAll || i === total - 1)) { // $parent.removeClass(this.options.row.valid).removeClass(this.options.row.invalid).addClass(this.options.row.feedback); // Keep error messages which are populated from back-end $parent.addClass(this.options.row.feedback); var $icon = $('<i/>') .css('display', 'none') .addClass(this.options.icon.feedback) .attr('data-' + ns + '-icon-for', field) .insertAfter($field); // Store the icon as a data of field element (!updateAll ? $field : fields).data(ns + '.icon', $icon); if ('tooltip' === container || 'popover' === container) { (!updateAll ? $field : fields) .on(this.options.events.fieldError, function() { $parent.addClass('fv-has-tooltip'); }) .on(this.options.events.fieldSuccess, function() { $parent.removeClass('fv-has-tooltip'); }); $field // Show tooltip/popover message when field gets focus .off('focus.container.' + ns) .on('focus.container.' + ns, function() { that._showTooltip($field, container); }) // and hide them when losing focus .off('blur.container.' + ns) .on('blur.container.' + ns, function() { that._hideTooltip($field, container); }); } if ('string' === typeof this.options.fields[field].icon && this.options.fields[field].icon !== 'true') { $icon.appendTo($(this.options.fields[field].icon)); } else { this._fixIcon($field, $icon); } } } // Prepare the events fields .on(this.options.events.fieldSuccess, function(e, data) { var onSuccess = that.getOptions(data.field, null, 'onSuccess'); if (onSuccess) { FormValidation.Helper.call(onSuccess, [e, data]); } }) .on(this.options.events.fieldError, function(e, data) { var onError = that.getOptions(data.field, null, 'onError'); if (onError) { FormValidation.Helper.call(onError, [e, data]); } }) .on(this.options.events.fieldStatus, function(e, data) { var onStatus = that.getOptions(data.field, null, 'onStatus'); if (onStatus) { FormValidation.Helper.call(onStatus, [e, data]); } }) .on(this.options.events.validatorError, function(e, data) { var onError = that.getOptions(data.field, data.validator, 'onError'); if (onError) { FormValidation.Helper.call(onError, [e, data]); } }) .on(this.options.events.validatorSuccess, function(e, data) { var onSuccess = that.getOptions(data.field, data.validator, 'onSuccess'); if (onSuccess) { FormValidation.Helper.call(onSuccess, [e, data]); } }); // Set live mode this.onLiveChange(fields, 'live', function() { if (that._exceedThreshold($(this))) { that.validateField($(this)); } }); fields.trigger($.Event(this.options.events.fieldInit), { bv: this, // Support backward fv: this, field: field, element: fields }); }, /** * Check if the field is excluded. * Returning true means that the field will not be validated * * @param {jQuery} $field The field element * @returns {Boolean} */ _isExcluded: function($field) { var ns = this._namespace, excludedAttr = $field.attr('data-' + ns + '-excluded'), // I still need to check the 'name' attribute while initializing the field field = $field.attr('data-' + ns + '-field') || $field.attr('name'); switch (true) { case (!!field && this.options.fields && this.options.fields[field] && (this.options.fields[field].excluded === 'true' || this.options.fields[field].excluded === true)): case (excludedAttr === 'true'): case (excludedAttr === ''): return true; case (!!field && this.options.fields && this.options.fields[field] && (this.options.fields[field].excluded === 'false' || this.options.fields[field].excluded === false)): case (excludedAttr === 'false'): return false; case (!!field && this.options.fields && this.options.fields[field] && 'function' === typeof this.options.fields[field].excluded): return this.options.fields[field].excluded.call(this, $field, this); case (!!field && this.options.fields && this.options.fields[field] && 'string' === typeof this.options.fields[field].excluded): case (excludedAttr): return FormValidation.Helper.call(this.options.fields[field].excluded, [$field, this]); default: if (this.options.excluded) { // Convert to array first if ('string' === typeof this.options.excluded) { this.options.excluded = $.map(this.options.excluded.split(','), function(item) { // Trim the spaces return $.trim(item); }); } var length = this.options.excluded.length; for (var i = 0; i < length; i++) { if (('string' === typeof this.options.excluded[i] && $field.is(this.options.excluded[i])) || ('function' === typeof this.options.excluded[i] && this.options.excluded[i].call(this, $field, this) === true)) { return true; } } } return false; } }, /** * Get a field changed trigger event * * @param {jQuery} $field The field element * @returns {String[]} The event names triggered on field change */ _getFieldTrigger: function($field) { var ns = this._namespace, trigger = $field.data(ns + '.trigger'); if (trigger) { return trigger; } var type = $field.attr('type'), name = $field.attr('data-' + ns + '-field'), event = ('radio' === type || 'checkbox' === type || 'file' === type || 'SELECT' === $field.get(0).tagName) ? 'change' : this._changeEvent; trigger = ((this.options.fields[name] ? this.options.fields[name].trigger : null) || this.options.trigger || event).split(' '); // Since the trigger data is used many times, I need to cache it to use later $field.data(ns + '.trigger', trigger); return trigger; }, /** * Get the error message for given field and validator * * @param {String} field The field name * @param {String} validatorName The validator name * @returns {String} */ _getMessage: function(field, validatorName) { if (!this.options.fields[field] || !FormValidation.Validator[validatorName] || !this.options.fields[field].validators || !this.options.fields[field].validators[validatorName]) { return ''; } switch (true) { case !!this.options.fields[field].validators[validatorName].message: return this.options.fields[field].validators[validatorName].message; case !!this.options.fields[field].message: return this.options.fields[field].message; case (!!FormValidation.I18n[this.options.locale] && !!FormValidation.I18n[this.options.locale][validatorName] && !!FormValidation.I18n[this.options.locale][validatorName]['default']): return FormValidation.I18n[this.options.locale][validatorName]['default']; default: return this.options.message; } }, /** * Get the element to place the error messages * * @param {jQuery} $field The field element * @param {String} row * @returns {jQuery} */ _getMessageContainer: function($field, row) { if (!this.options.err.parent) { throw new Error('The err.parent option is not defined'); } var $parent = $field.parent(); if ($parent.is(row)) { return $parent; } var cssClasses = $parent.attr('class'); if (!cssClasses) { return this._getMessageContainer($parent, row); } if (this.options.err.parent.test(cssClasses)) { return $parent; } return this._getMessageContainer($parent, row); }, /** * Parse the add-on options from HTML attributes * * @returns {Object} */ _parseAddOnOptions: function() { var ns = this._namespace, names = this.$form.attr('data-' + ns + '-addons'), addOns = this.options.addOns || {}; if (names) { names = names.replace(/\s/g, '').split(','); for (var i = 0; i < names.length; i++) { if (!addOns[names[i]]) { addOns[names[i]] = {}; } } } // Try to parse each add-on options var addOn, attrMap, attr, option; for (addOn in addOns) { if (!FormValidation.AddOn[addOn]) { // Add-on is not found delete addOns[addOn]; continue; } attrMap = FormValidation.AddOn[addOn].html5Attributes; if (attrMap) { for (attr in attrMap) { option = this.$form.attr('data-' + ns + '-addons-' + addOn.toLowerCase() + '-' + attr.toLowerCase()); if (option) { addOns[addOn][attrMap[attr]] = option; } } } } return addOns; }, /** * Parse the validator options from HTML attributes * * @param {jQuery} $field The field element * @returns {Object} */ _parseOptions: function($field) { var ns = this._namespace, field = $field.attr('name') || $field.attr('data-' + ns + '-field'), validators = {}, validator, v, // Validator name attrName, enabled, optionName, optionAttrName, optionValue, html5AttrName, html5AttrMap; for (v in FormValidation.Validator) { validator = FormValidation.Validator[v]; attrName = 'data-' + ns + '-' + v.toLowerCase(), enabled = $field.attr(attrName) + ''; html5AttrMap = ('function' === typeof validator.enableByHtml5) ? validator.enableByHtml5($field) : null; if ((html5AttrMap && enabled !== 'false') || (html5AttrMap !== true && ('' === enabled || 'true' === enabled || attrName === enabled.toLowerCase()))) { // Try to parse the options via attributes validator.html5Attributes = $.extend({}, { message: 'message', onerror: 'onError', onsuccess: 'onSuccess', transformer: 'transformer' }, validator.html5Attributes); validators[v] = $.extend({}, html5AttrMap === true ? {} : html5AttrMap, validators[v]); for (html5AttrName in validator.html5Attributes) { optionName = validator.html5Attributes[html5AttrName]; optionAttrName = 'data-' + ns + '-' + v.toLowerCase() + '-' + html5AttrName, optionValue = $field.attr(optionAttrName); if (optionValue) { if ('true' === optionValue || optionAttrName === optionValue.toLowerCase()) { optionValue = true; } else if ('false' === optionValue) { optionValue = false; } validators[v][optionName] = optionValue; } } } } var opts = { autoFocus: $field.attr('data-' + ns + '-autofocus'), err: $field.attr('data-' + ns + '-err-container') || $field.attr('data-' + ns + '-container'), // Support backward excluded: $field.attr('data-' + ns + '-excluded'), icon: $field.attr('data-' + ns + '-icon') || $field.attr('data-' + ns + '-feedbackicons') || (this.options.fields && this.options.fields[field] ? this.options.fields[field].feedbackIcons : null), // Support backward message: $field.attr('data-' + ns + '-message'), onError: $field.attr('data-' + ns + '-onerror'), onStatus: $field.attr('data-' + ns + '-onstatus'), onSuccess: $field.attr('data-' + ns + '-onsuccess'), row: $field.attr('data-' + ns + '-row') || $field.attr('data-' + ns + '-group') || (this.options.fields && this.options.fields[field] ? this.options.fields[field].group : null), // Support backward selector: $field.attr('data-' + ns + '-selector'), threshold: $field.attr('data-' + ns + '-threshold'), transformer: $field.attr('data-' + ns + '-transformer'), trigger: $field.attr('data-' + ns + '-trigger'), verbose: $field.attr('data-' + ns + '-verbose'), validators: validators }, emptyOptions = $.isEmptyObject(opts), // Check if the field options are set using HTML attributes emptyValidators = $.isEmptyObject(validators); // Check if the field validators are set using HTML attributes if (!emptyValidators || (!emptyOptions && this.options.fields && this.options.fields[field])) { opts.validators = validators; return opts; } else { return null; } }, /** * Called when all validations are completed */ _submit: function() { var isValid = this.isValid(); if (isValid === null) { return; } var eventType = isValid ? this.options.events.formSuccess : this.options.events.formError, e = $.Event(eventType); this.$form.trigger(e); // Call default handler // Check if whether the submit button is clicked if (this.$submitButton) { isValid ? this._onSuccess(e) : this._onError(e); } }, // ~~~~~~ // Events // ~~~~~~ /** * The default handler of error.form.fv event. * It will be called when there is a invalid field * * @param {jQuery.Event} e The jQuery event object */ _onError: function(e) { if (e.isDefaultPrevented()) { return; } if ('submitted' === this.options.live) { // Enable live mode this.options.live = 'enabled'; var that = this; for (var field in this.options.fields) { (function(f) { var fields = that.getFieldElements(f); if (fields.length) { that.onLiveChange(fields, 'live', function() { if (that._exceedThreshold($(this))) { that.validateField($(this)); } }); } })(field); } } // Determined the first invalid field which will be focused on automatically var ns = this._namespace; for (var i = 0; i < this.$invalidFields.length; i++) { var $field = this.$invalidFields.eq(i), autoFocus = this.isOptionEnabled($field.attr('data-' + ns + '-field'), 'autoFocus'); if (autoFocus) { // Focus the field $field.focus(); break; } } }, /** * Called after validating a field element * * @param {jQuery} $field The field element * @param {String} [validatorName] The validator name */ _onFieldValidated: function($field, validatorName) { var ns = this._namespace, field = $field.attr('data-' + ns + '-field'), validators = this.options.fields[field].validators, counter = {}, numValidators = 0, data = { bv: this, // Support backward fv: this, field: field, element: $field, validator: validatorName, result: $field.data(ns + '.response.' + validatorName) }; // Trigger an event after given validator completes if (validatorName) { switch ($field.data(ns + '.result.' + validatorName)) { case this.STATUS_INVALID: $field.trigger($.Event(this.options.events.validatorError), data); break; case this.STATUS_VALID: $field.trigger($.Event(this.options.events.validatorSuccess), data); break; case this.STATUS_IGNORED: $field.trigger($.Event(this.options.events.validatorIgnored), data); break; default: break; } } counter[this.STATUS_NOT_VALIDATED] = 0; counter[this.STATUS_VALIDATING] = 0; counter[this.STATUS_INVALID] = 0; counter[this.STATUS_VALID] = 0; counter[this.STATUS_IGNORED] = 0; for (var v in validators) { if (validators[v].enabled === false) { continue; } numValidators++; var result = $field.data(ns + '.result.' + v); if (result) { counter[result]++; } } // The sum of valid fields now also include ignored fields if (counter[this.STATUS_VALID] + counter[this.STATUS_IGNORED] === numValidators) { // Remove from the list of invalid fields this.$invalidFields = this.$invalidFields.not($field); $field.trigger($.Event(this.options.events.fieldSuccess), data); } // If all validators are completed and there is at least one validator which doesn't pass else if ((counter[this.STATUS_NOT_VALIDATED] === 0 || !this.isOptionEnabled(field, 'verbose')) && counter[this.STATUS_VALIDATING] === 0 && counter[this.STATUS_INVALID] > 0) { // Add to the list of invalid fields this.$invalidFields = this.$invalidFields.add($field); $field.trigger($.Event(this.options.events.fieldError), data); } }, /** * The default handler of success.form.fv event. * It will be called when all the fields are valid * * @param {jQuery.Event} e The jQuery event object */ _onSuccess: function(e) { if (e.isDefaultPrevented()) { return; } // Submit the form this.disableSubmitButtons(true).defaultSubmit(); }, // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Abstract methods // Need to be implemented by sub-class that supports specific framework // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Specific framework might need to adjust the icon position * * @param {jQuery} $field The field element * @param {jQuery} $icon The icon element */ _fixIcon: function($field, $icon) { }, /** * Create a tooltip or popover * It will be shown when focusing on the field * * @param {jQuery} $field The field element * @param {String} message The message * @param {String} type Can be 'tooltip' or 'popover' */ _createTooltip: function($field, message, type) { }, /** * Destroy the tooltip or popover * * @param {jQuery} $field The field element * @param {String} type Can be 'tooltip' or 'popover' */ _destroyTooltip: function($field, type) { }, /** * Hide a tooltip or popover * * @param {jQuery} $field The field element * @param {String} type Can be 'tooltip' or 'popover' */ _hideTooltip: function($field, type) { }, /** * Show a tooltip or popover * * @param {jQuery} $field The field element * @param {String} type Can be 'tooltip' or 'popover' */ _showTooltip: function($field, type) { }, // ~~~~~~~~~~~~~~ // Public methods // ~~~~~~~~~~~~~~ /** * Submit the form using default submission. * It also does not perform any validations when submitting the form */ defaultSubmit: function() { var ns = this._namespace; if (this.$submitButton) { // Create hidden input to send the submit buttons $('<input/>') .attr({ 'type': 'hidden', name: this.$submitButton.attr('name') }) .attr('data-' + ns + '-submit-hidden', '') .val(this.$submitButton.val()) .appendTo(this.$form); } // Submit form this.$form.off('submit.' + ns).submit(); }, /** * Disable/enable submit buttons * * @param {Boolean} disabled Can be true or false * @returns {FormValidation.Base} */ disableSubmitButtons: function(disabled) { if (!disabled) { this.$form .find(this.options.button.selector) .removeAttr('disabled') .removeClass(this.options.button.disabled); } else if (this.options.live !== 'disabled') { // Don't disable if the live validating mode is disabled this.$form .find(this.options.button.selector) .attr('disabled', 'disabled') .addClass(this.options.button.disabled); } return this; }, /** * Retrieve the field elements by given name * * @param {String} field The field name * @returns {null|jQuery[]} */ getFieldElements: function(field) { if (!this._cacheFields[field]) { if (this.options.fields[field] && this.options.fields[field].selector) { // Look for the field inside the form first var f = this.$form.find(this.options.fields[field].selector); // If not found, search in entire document this._cacheFields[field] = f.length ? f : $(this.options.fields[field].selector); } else { this._cacheFields[field] = this.$form.find('[name="' + field + '"]'); } } return this._cacheFields[field]; }, /** * Get the field value after applying transformer * * @param {String|jQuery} field The field name or field element * @param {String} validatorName The validator name * @returns {String} */ getFieldValue: function(field, validatorName) { var $field, ns = this._namespace; if ('string' === typeof field) { $field = this.getFieldElements(field); if ($field.length === 0) { return null; } } else { $field = field; field = $field.attr('data-' + ns + '-field'); } if (!field || !this.options.fields[field]) { return $field.val(); } var transformer = (this.options.fields[field].validators && this.options.fields[field].validators[validatorName] ? this.options.fields[field].validators[validatorName].transformer : null) || this.options.fields[field].transformer; return transformer ? FormValidation.Helper.call(transformer, [$field, validatorName, this]) : $field.val(); }, /** * Get the namespace * * @returns {String} */ getNamespace: function() { return this._namespace; }, /** * Get the field options * * @param {String|jQuery} [field] The field name or field element. If it is not set, the method returns the form options * @param {String} [validator] The name of validator. It null, the method returns form options * @param {String} [option] The option name * @return {String|Object} */ getOptions: function(field, validator, option) { var ns = this._namespace; if (!field) { return option ? this.options[option] : this.options; } if ('object' === typeof field) { field = field.attr('data-' + ns + '-field'); } if (!this.options.fields[field]) { return null; } var options = this.options.fields[field]; if (!validator) { return option ? options[option] : options; } if (!options.validators || !options.validators[validator]) { return null; } return option ? options.validators[validator][option] : options.validators[validator]; }, /** * Get the validating result of field * * @param {String|jQuery} field The field name or field element * @param {String} validatorName The validator name * @returns {String} The status. Can be 'NOT_VALIDATED', 'VALIDATING', 'INVALID', 'VALID' or 'IGNORED' */ getStatus: function(field, validatorName) { var ns = this._namespace; switch (typeof field) { case 'object': return field.data(ns + '.result.' + validatorName); case 'string': /* falls through */ default: return this.getFieldElements(field).eq(0).data(ns + '.result.' + validatorName); } }, /** * Check whether or not a field option is enabled * * @param {String} field The field name * @param {String} option The option name, "verbose", "autoFocus", for example * @returns {Boolean} */ isOptionEnabled: function(field, option) { if (this.options.fields[field] && (this.options.fields[field][option] === 'true' || this.options.fields[field][option] === true)) { return true; } if (this.options.fields[field] && (this.options.fields[field][option] === 'false' || this.options.fields[field][option] === false)) { return false; } return this.options[option] === 'true' || this.options[option] === true; }, /** * Check the form validity * * @returns {Boolean|null} Returns one of three values * - true, if all fields are valid * - false, if there is one invalid field * - null, if there is at least one field which is not validated yet or being validated */ isValid: function() { for (var field in this.options.fields) { var isValidField = this.isValidField(field); if (isValidField === null) { return null; } if (isValidField === false) { return false; }