/**
* BootstrapValidator (https://github.com/nghuuphuoc/bootstrapvalidator)
*
* A jQuery plugin to validate form fields. Use with Bootstrap 3
*
* @author http://twitter.com/nghuuphuoc
* @copyright (c) 2013 - 2014 Nguyen Huu Phuoc
* @license MIT
*/
(function($) {
var BootstrapValidator = function(form, options) {
this.$form = $(form);
this.options = $.extend({}, BootstrapValidator.DEFAULT_OPTIONS, options);
this.dfds = {}; // Array of deferred
this.results = {}; // Validating results
this.invalidField = null; // First invalid field
this.$submitButton = null; // The submit button which is clicked to submit form
this._init();
this.STATUS_NOT_VALIDATED = 'NOT_VALIDATED';
this.STATUS_VALIDATING = 'VALIDATING';
this.STATUS_INVALID = 'INVALID';
this.STATUS_VALID = 'VALID';
};
// The default options
BootstrapValidator.DEFAULT_OPTIONS = {
// The form CSS class
elementClass: 'bootstrap-validator-form',
// Default invalid message
message: 'This value is not valid',
// Shows ok/error/loading icons based on the field validity.
// This feature requires Bootstrap v3.1.0 or later (http://getbootstrap.com/css/#forms-control-validation).
// Since Bootstrap doesn't provide any methods to know its version, this option cannot be on/off automatically.
// In other word, to use this feature you have to upgrade your Bootstrap to v3.1.0 or later.
//
// Examples:
// - Use Glyphicons icons:
// feedbackIcons: {
// valid: 'glyphicon glyphicon-ok',
// invalid: 'glyphicon glyphicon-remove',
// validating: 'glyphicon glyphicon-refresh'
// }
// - Use FontAwesome icons:
// feedbackIcons: {
// valid: 'fa fa-check',
// invalid: 'fa fa-times',
// validating: 'fa fa-refresh'
// }
feedbackIcons: {
valid: null,
invalid: null,
validating: null
},
// The submit buttons selector
// These buttons will be disabled to prevent the valid form from multiple submissions
submitButtons: 'button[type="submit"]',
// The custom submit handler
// It will prevent the form from the default submission
//
// submitHandler: function(validator, form) {
// - validator is the BootstrapValidator instance
// - form is the jQuery object present the current form
// }
submitHandler: null,
// Live validating option
// Can be one of 3 values:
// - enabled: The plugin validates fields as soon as they are changed
// - disabled: Disable the live validating. The error messages are only shown after the form is submitted
// - submitted: The live validating is enabled after the form is submitted
live: 'enabled',
// Map the field name with validator rules
fields: null
};
BootstrapValidator.prototype = {
constructor: BootstrapValidator,
/**
* Init form
*/
_init: function() {
var that = this,
options = {
trigger: this.$form.attr('data-bv-trigger'),
message: this.$form.attr('data-bv-message'),
submitButtons: this.$form.attr('data-bv-submitbuttons'),
live: this.$form.attr('data-bv-live'),
fields: {},
feedbackIcons: {
valid: this.$form.attr('data-bv-feedbackicons-valid'),
invalid: this.$form.attr('data-bv-feedbackicons-invalid'),
validating: this.$form.attr('data-bv-feedbackicons-validating')
}
},
validator,
i = 0,
v, // Validator name
enabled,
optionName,
optionValue,
html5Attrs;
this.$form
// Disable client side validation in HTML 5
.attr('novalidate', 'novalidate')
.addClass(this.options.elementClass)
// Disable the default submission first
.on('submit.bootstrapValidator', function(e) {
e.preventDefault();
that.validate();
})
.on('click', this.options.submitButtons, function() {
that.$submitButton = $(this);
})
// Find all fields which have either "name" or "data-bv-field" attribute
.find('[name], [data-bv-field]').each(function() {
var $field = $(this),
field = $field.attr('name') || $field.attr('data-bv-field');
$field.attr('data-bv-field', field);
options.fields[field] = $.extend({}, {
trigger: $field.attr('data-bv-trigger'),
message: $field.attr('data-bv-message'),
container: $field.attr('data-bv-container'),
selector: $field.attr('data-bv-selector'),
validators: {}
}, options.fields[field]);
for (v in $.fn.bootstrapValidator.validators) {
validator = $.fn.bootstrapValidator.validators[v];
enabled = $field.attr('data-bv-' + v.toLowerCase()) + '';
html5Attrs = {};
if (('function' == typeof validator.enableByHtml5
&& (html5Attrs = validator.enableByHtml5($(this)))
&& (enabled != 'false'))
|| ('undefined' == typeof validator.enableByHtml5 && ('' == enabled || 'true' == enabled)))
{
// Try to parse the options via attributes
validator.html5Attributes = validator.html5Attributes || ['message'];
options.fields[field]['validators'][v] = $.extend({}, html5Attrs == true ? {} : html5Attrs, options.fields[field]['validators'][v]);
for (i in validator.html5Attributes) {
optionName = validator.html5Attributes[i];
optionValue = $field.attr('data-bv-' + v.toLowerCase() + '-' + optionName.toLowerCase());
if (optionValue) {
if ('true' == optionValue) {
optionValue = true;
} else if ('false' == optionValue) {
optionValue = false;
}
options.fields[field]['validators'][v][optionName] = optionValue;
}
}
}
}
});
this.options = $.extend(true, this.options, options);
//console.log(this.options);
for (var field in this.options.fields) {
this._initField(field);
}
this._setLiveValidating();
},
/**
* Init field
*
* @param {String} field The field name
*/
_initField: function(field) {
if (this.options.fields[field] == null || this.options.fields[field].validators == null) {
return;
}
this.dfds[field] = {};
this.results[field] = {};
var fields = this.getFieldElements(field);
// We don't need to validate non-existing fields
if (fields == null) {
delete this.options.fields[field];
delete this.dfds[field];
return;
}
fields.attr('data-bv-field', field);
// Create help block elements for showing the error messages
var $field = $(fields[0]),
$parent = $field.parents('.form-group'),
// Allow user to indicate where the error messages are shown
$message = this.options.fields[field].container ? $parent.find(this.options.fields[field].container) : this._getMessageContainer($field);
$field.data('bootstrapValidator.messageContainer', $message);
for (var validatorName in this.options.fields[field].validators) {
if (!$.fn.bootstrapValidator.validators[validatorName]) {
delete this.options.fields[field].validators[validatorName];
continue;
}
this.results[field][validatorName] = this.STATUS_NOT_VALIDATED;
$('')
.css('display', 'none')
.attr('data-bv-validator', validatorName)
.html(this.options.fields[field].validators[validatorName].message || this.options.fields[field].message || this.options.message)
.addClass('help-block')
.appendTo($message);
}
// Prepare the feedback icons
// Available from Bootstrap 3.1 (http://getbootstrap.com/css/#forms-control-validation)
if (this.options.feedbackIcons
&& this.options.feedbackIcons.validating && this.options.feedbackIcons.invalid && this.options.feedbackIcons.valid)
{
$parent.addClass('has-feedback');
var $icon = $('').css('display', 'none').addClass('form-control-feedback').attr('data-bv-field', field).insertAfter($(fields[fields.length - 1]));
// The feedback icon does not render correctly if there is no label
// https://github.com/twbs/bootstrap/issues/12873
if ($parent.find('label').length == 0) {
$icon.css('top', 0);
}
}
if (this.options.fields[field]['enabled'] == null) {
this.options.fields[field]['enabled'] = true;
}
// Whenever the user change the field value, mark it as not validated yet
var that = this,
type = fields.attr('type'),
event = ('radio' == type || 'checkbox' == type || 'file' == type || 'SELECT' == fields[0].tagName) ? 'change' : 'keyup';
fields.on(event + '.bootstrapValidator', function() {
that.updateStatus($field, that.STATUS_NOT_VALIDATED, null);
});
},
/**
* Get the element to place the error messages
*
* @param {jQuery} $field The field element
* @returns {jQuery}
*/
_getMessageContainer: function($field) {
var $parent = $field.parent();
if ($parent.hasClass('form-group')) {
return $parent;
}
var cssClasses = $parent.attr('class');
if (!cssClasses) {
return this._getMessageContainer($parent);
}
cssClasses = cssClasses.split(' ');
var n = cssClasses.length;
for (var i = 0; i < n; i++) {
if (/^col-(xs|sm|md|lg)-\d+$/.test(cssClasses[i]) || /^col-(xs|sm|md|lg)-offset-\d+$/.test(cssClasses[i])) {
return $parent;
}
}
return this._getMessageContainer($parent);
},
/**
* Enable live validating
*/
_setLiveValidating: function() {
if ('enabled' == this.options.live) {
var that = this;
for (var field in this.options.fields) {
(function(f) {
var fields = that.getFieldElements(f);
if (fields) {
var type = fields.attr('type'),
trigger = that.options.fields[field].trigger
|| that.options.trigger
|| (('radio' == type || 'checkbox' == type || 'file' == type || 'SELECT' == fields[0].tagName) ? 'change' : 'keyup'),
events = trigger.split(' ').map(function(item) {
return item + '.bootstrapValidator';
}).join(' ');
fields.on(events, function() {
that.validateField(f);
});
}
})(field);
}
}
},
/**
* Disable/Enable submit buttons
*
* @param {Boolean} disabled
*/
_disableSubmitButtons: function(disabled) {
if (!disabled) {
this.$form.find(this.options.submitButtons).removeAttr('disabled');
} else if (this.options.live != 'disabled') {
// Don't disable if the live validating mode is disabled
this.$form.find(this.options.submitButtons).attr('disabled', 'disabled');
}
},
/**
* Called when all validations are completed
*/
_submit: function() {
if (!this.isValid()) {
if ('submitted' == this.options.live) {
this.options.live = 'enabled';
this._setLiveValidating();
}
// Focus to the first invalid field
if (this.invalidField) {
this.getFieldElements(this.invalidField).focus();
}
return;
}
this._disableSubmitButtons(true);
// Call the custom submission if enabled
if (this.options.submitHandler && 'function' == typeof this.options.submitHandler) {
// Turn off the submit handler, so user can call form.submit() inside their submitHandler method
this.$form.off('submit.bootstrapValidator');
this.options.submitHandler.call(this, this, this.$form, this.$submitButton);
} else {
// Submit form
this.$form.off('submit.bootstrapValidator').submit();
}
},
// --- Public methods ---
/**
* Retrieve the field elements by given name
*
* @param {String} field The field name
* @returns {null|jQuery[]}
*/
getFieldElements: function(field) {
var fields = this.$form.find(this.options.fields[field].selector || '[name="' + field + '"]');
return (fields.length == 0) ? null : fields;
},
/**
* Validate the form
*
* @return {BootstrapValidator}
*/
validate: function() {
if (!this.options.fields) {
return this;
}
this._disableSubmitButtons(true);
for (var field in this.options.fields) {
this.validateField(field);
}
this._submit();
return this;
},
/**
* Validate given field
*
* @param {String} field The field name
*/
validateField: function(field) {
if (!this.options.fields[field]['enabled']) {
return;
}
var that = this,
fields = this.getFieldElements(field),
$field = $(fields[0]),
validators = this.options.fields[field].validators,
validatorName,
validateResult;
// We don't need to validate disabled field
if (fields.length == 1 && fields.is(':disabled')) {
delete this.options.fields[field];
delete this.dfds[field];
return;
}
for (validatorName in validators) {
if (this.dfds[field][validatorName]) {
this.dfds[field][validatorName].reject();
}
// Don't validate field if it is already done
if (this.results[field][validatorName] == this.STATUS_VALID || this.results[field][validatorName] == this.STATUS_INVALID) {
continue;
}
this.results[field][validatorName] = this.STATUS_VALIDATING;
validateResult = $.fn.bootstrapValidator.validators[validatorName].validate(this, $field, validators[validatorName]);
if ('object' == typeof validateResult) {
this.updateStatus($field, this.STATUS_VALIDATING, validatorName);
this.dfds[field][validatorName] = validateResult;
validateResult.done(function(isValid, v) {
// v is validator name
delete that.dfds[field][v];
that.updateStatus($field, isValid ? that.STATUS_VALID : that.STATUS_INVALID, v);
if (isValid && 'disabled' == that.options.live) {
that._submit();
}
});
} else if ('boolean' == typeof validateResult) {
this.updateStatus($field, validateResult ? this.STATUS_VALID : this.STATUS_INVALID, validatorName);
}
}
},
/**
* Check the form validity
*
* @returns {Boolean}
*/
isValid: function() {
var field, validatorName;
for (field in this.results) {
if (this.options.fields[field] == null || !this.options.fields[field]['enabled']) {
continue;
}
for (validatorName in this.results[field]) {
if (this.results[field][validatorName] == this.STATUS_NOT_VALIDATED || this.results[field][validatorName] == this.STATUS_VALIDATING) {
return false;
}
if (this.results[field][validatorName] == this.STATUS_INVALID) {
this.invalidField = field;
return false;
}
}
}
return true;
},
/**
* Update field status
*
* @param {String|jQuery} field The field name or field element
* @param {String} status The status
* Can be 'NOT_VALIDATED', 'VALIDATING', 'INVALID' or 'VALID'
* @param {String|null} validatorName The validator name. If null, the method updates validity result for all validators
* @return {BootstrapValidator}
*/
updateStatus: function(field, status, validatorName) {
var $field = ('string' == typeof field) ? this.getFieldElements(field) : field,
that = this,
field = $field.attr('data-bv-field'),
$parent = $field.parents('.form-group'),
$message = $field.data('bootstrapValidator.messageContainer'),
$errors = $message.find('.help-block[data-bv-validator]'),
$icon = $parent.find('.form-control-feedback[data-bv-field="' + field + '"]');
// Update status
if (validatorName) {
this.results[field][validatorName] = status;
} else {
for (var v in this.options.fields[field].validators) {
this.results[field][v] = status;
}
}
// Show/hide error elements and feedback icons
switch (status) {
case this.STATUS_VALIDATING:
this._disableSubmitButtons(true);
$parent.removeClass('has-success').removeClass('has-error');
// TODO: Show validating message
validatorName ? $errors.filter('.help-block[data-bv-validator="' + validatorName + '"]').hide() : $errors.hide();
if ($icon) {
$icon.removeClass(this.options.feedbackIcons.valid).removeClass(this.options.feedbackIcons.invalid).addClass(this.options.feedbackIcons.validating).show();
}
break;
case this.STATUS_INVALID:
this._disableSubmitButtons(true);
$parent.removeClass('has-success').addClass('has-error');
validatorName ? $errors.filter('[data-bv-validator="' + validatorName + '"]').show() : $errors.show();
if ($icon) {
$icon.removeClass(this.options.feedbackIcons.valid).removeClass(this.options.feedbackIcons.validating).addClass(this.options.feedbackIcons.invalid).show();
}
break;
case this.STATUS_VALID:
validatorName ? $errors.filter('[data-bv-validator="' + validatorName + '"]').hide() : $errors.hide();
// If the field is valid
if ($errors.filter(function() {
var display = $(this).css('display'), v = $(this).attr('data-bv-validator');
return ('block' == display) || (that.results[field][v] != that.STATUS_VALID);
}).length == 0
) {
this._disableSubmitButtons(false);
$parent.removeClass('has-error').addClass('has-success');
if ($icon) {
$icon.removeClass(this.options.feedbackIcons.invalid).removeClass(this.options.feedbackIcons.validating).addClass(this.options.feedbackIcons.valid).show();
}
}
break;
case this.STATUS_NOT_VALIDATED:
default:
this._disableSubmitButtons(false);
$parent.removeClass('has-success').removeClass('has-error');
validatorName ? $errors.filter('.help-block[data-bv-validator="' + validatorName + '"]').hide() : $errors.hide();
if ($icon) {
$icon.removeClass(this.options.feedbackIcons.valid).removeClass(this.options.feedbackIcons.invalid).removeClass(this.options.feedbackIcons.validating).hide();
}
break;
}
return this;
},
// Useful APIs which aren't used internally
/**
* Reset the form
*
* @param {Boolean} resetFormData Reset current form data
* @return {BootstrapValidator}
*/
resetForm: function(resetFormData) {
var field, $field, type;
for (field in this.options.fields) {
this.dfds[field] = {};
this.results[field] = {};
$field = this.getFieldElements(field);
// Mark field as not validated yet
this.updateStatus($field, this.STATUS_NOT_VALIDATED, null);
if (resetFormData) {
type = $field.attr('type');
('radio' == type || 'checkbox' == type) ? $field.removeAttr('checked').removeAttr('selected') : $field.val('');
}
}
this.invalidField = null;
this.$submitButton = null;
// Enable submit buttons
this._disableSubmitButtons(false);
return this;
},
/**
* Enable/Disable all validators to given field
*
* @param {String} field The field name
* @param {Boolean} enabled Enable/Disable field validators
* @return {BootstrapValidator}
*/
enableFieldValidators: function(field, enabled) {
this.options.fields[field]['enabled'] = enabled;
this.updateStatus(field, this.STATUS_NOT_VALIDATED, null);
return this;
}
};
// Plugin definition
$.fn.bootstrapValidator = function(options) {
return this.each(function() {
var $this = $(this), data = $this.data('bootstrapValidator');
if (!data) {
$this.data('bootstrapValidator', (data = new BootstrapValidator(this, options)));
}
if ('string' == typeof options) {
data[options]();
}
});
};
// Available validators
$.fn.bootstrapValidator.validators = {};
$.fn.bootstrapValidator.Constructor = BootstrapValidator;
}(window.jQuery));