Files
PetitTetonMeteor/imports/util/validator.js

551 lines
24 KiB
JavaScript

/*!
* Validator v0.11.5 for Bootstrap 3, by @1000hz
* Copyright 2016 Cina Saffary
* Licensed under http://opensource.org/licenses/MIT
*
* https://github.com/1000hz/bootstrap-validator
*/
/*!
* Modified by Wynne Crisman 10/2016.
* Modifications Copyright 2016 Wynne Crisman
* Modifications licensed under http://opensource.org/licenses/MIT
*
* Added commenting and formatting to tabs - wtf spaces?! (side note: If you are coding in Notepad or vim, please seek a therapist's help immediately. Perhaps you haven't heard that there are free tools (Notepad++, Emacs) that do formatting for you, or you can go all out and use an actual integrated development environment like a professional developer who values their time might.)
* Added semicolons; While not strictly required, they help with readability (knowing, without thinking, where a line ends is really handy when you code all day) and are required by some pretty printing tools for JS.
* Removed code that used && as an if block - this is really hard to read, and will confuse even seasoned developers - there is no reason to do it (any performance improvement is due entirely to crappy VM implementations - there is no hard evidence this is worth the aggravation).
* Added an optional parameter to validate(..) - a callback function may be passed now. The callback will be provided with a boolean parameter 'isValid' indicating whether the form validated. This allows developers to easily force a validation and take an action on the result (submit the form?).
* Added documentation at the top of this file to provide basic usage information.
* Added highlighting on input-group elements in addition to form-group elements.
*/
/*
* USAGE INFO
*
* Getting Started:
* Create a form along with any fields in HTML '<form id='myform'>...</form>', then in your javascript block call: $('#myform').validator(); to initialize the validator.
* Your form elements may specify additional standard validation tags to help the validator know what to do (for example add 'required' to any form element to tell the validator which elements must have data).
* Form elements should be put into form-groups, placing related elements into a common group. Form groups are simply Div's with the class='form-group'.
* Useful Functions:
* $(form).validator() - Initializes the validator for a form. This does not return the validator instance, but it does attach it to the form element.
* $(form).data('bs.validator') - Gets the validator object for the given form. The validator object is stored as data attached to the form with the 'bs.validator' key.
* validate(fn) - Forces validation to occur and takes an optional callback which will be passed a flag (boolean) indicating the success of the validation (isValid).
* reset() - Resets the form's validation status. Clears all error information, without turning validation off.
* update() - Updates the collection of fields that require validation. Call this after making changes to the form, including initializing any form elements that may generate HTML (such as Select2).
*/
+function ($) {
'use strict';
// VALIDATOR CLASS DEFINITION
// ==========================
//Gets the value of the HTML element.
function getValue($el) {
if($el.is('[type="checkbox"]')) {
return $el.prop('checked')
}
else if($el.is('[type="radio"]')) {
return !!$('[name="' + $el.attr('name') + '"]:checked').length
}
else {
return $el.val();
}
}
//Gets the actual element to perform the validation on. This returns the input element if the input element isn't a surrogate for a hidden form element.
//Some widget libraries (such as Select2) hide the actual element that holds the data, and use custom graphical elements for the control and display logic.
//We need the actual element that holds the value, and not the display elements which fire the events we are interested in (focusout, etc).
function getActualWidget($el) {
if($el.hasClass('select2-search__field')) { //Handle Select2 multi-select controls.
//Select2 creates a structure of span elements whose parent span has the .select2 class and whose sibling is a hidden select element that contains the actual selection to be validated.
return $el.parents('.select2').siblings('select');
}
else {
return $el;
}
}
//The opposite of getActualWidget($el). Gets the visible surrogate widget for the actual hidden form widget.
//The actual hidden widget holds the value and acts like a form element when submitting the form, while the surrogate has the display and functionality desired by the view.
function getSurrogate($el) {
if($el.hasClass('select2-hidden-accessible')) { //Handle Select2 multi-select controls.
//Select2 creates a structure of span elements whose parent span has the .select2 class and whose sibling is a hidden select element that contains the actual selection to be validated.
return $el.siblings('.select2').find('.select2-selection');
}
else {
return $el;
}
}
var Validator = function(element, options) {
this.options = options;
this.validators = $.extend({}, Validator.VALIDATORS, options.custom);
this.$element = $(element);
this.$btn = $('button[type="submit"], input[type="submit"]')
.filter('[form="' + this.$element.attr('id') + '"]')
.add(this.$element.find('input[type="submit"], button[type="submit"]'));
this.update();
//Register for the events (uses a namespace for easy de-registration).
this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.onInput, this));
this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this));
this.$element.on('reset.bs.validator', $.proxy(this.reset, this));
//TODO: What is '[data-match]' ????
//This will find some kind of matching elements in the form and when the validation event is detected on the match target (retrieved from the data-match element's 'match' data), a validation event will also be fired on the data-match element.
this.$element.find('[data-match]').each(function() {
var $this = $(this);
var target = $this.data('match');
//Register an event handler on the match target element and if the match target element has a value, then fire the validator event on the data-match element also.
$(target).on('input.bs.validator', function(e) {
if(getValue($this)) $this.trigger('input.bs.validator');
});
});
//Force validation on any elements that start with values (ignore those that don't).
//Filter the HTML elements we will be validating to get the set that contains values (ignore those that don't have values), then trigger a 'focusout' event on those elements (forcing validation as we start the Validator up).
this.$inputs.filter(function() {
return getValue($(this))
}).trigger('focusout');
//Diable automatic native validation.
this.$element.attr('novalidate', true);
//Update the submit elements based on the current error state.
this.toggleSubmit();
};
Validator.VERSION = '0.11.5';
Validator.INPUT_SELECTOR = ':input:not([type="hidden"], [type="submit"], [type="reset"], button, .select2-search__field)'
Validator.SURROGATE_SELECTOR = '.select2 .select2-selection';
Validator.FOCUS_OFFSET = 20;
Validator.DEFAULTS = {
delay: 500,
html: false,
disable: true,
focus: true,
custom: {},
errors: {
match: 'Does not match',
minlength: 'Not long enough'
},
feedback: {
success: 'glyphicon-ok',
error: 'glyphicon-remove'
}
};
//The default set of validators to be used.
Validator.VALIDATORS = {
'native': function($el) {
var el = $el[0];
if(el.checkValidity) {
return !el.checkValidity() && !el.validity.valid && (el.validationMessage || "error!");
}
},
'match': function($el) {
var target = $el.data('match');
return $el.val() !== $(target).val() && Validator.DEFAULTS.errors.match;
},
'minlength': function($el) {
var minlength = $el.data('minlength');
return $el.val().length < minlength && Validator.DEFAULTS.errors.minlength;
}
};
//Collects a list of HTML elements that require validation using the INPUT_SELECTOR option supplied when the Validator was created, and the default data-validate attribute that can be supplied in the HTML tags.
Validator.prototype.update = function() {
this.$inputs = this.$element.find(Validator.INPUT_SELECTOR)
.add(this.$element.find('[data-validate="true"]'))
.not(this.$element.find('[data-validate="false"]'));
this.$surrogates = this.$inputs.filter('.select2-hidden-accessible').map(function(el) {
return getSurrogate($(this));
});
return this;
};
//An event handler that will perform validation on a HTML element when that element fires an event.
Validator.prototype.onInput = function(e) {
var self = this;
var $surrogate = $(e.target); //Wrapper the event target with a jquery object.
var $el = getActualWidget($surrogate); //Get the actual (hidden) form widget if there is one, otherwise $el will equal (===) $surrogate.
var deferErrors = e.type !== 'focusout';
//If the event target is not in the set of HTML elements that require validation, then ignore it.
if(!this.$inputs.is($el)) return;
//Validate the HTML element and update the submit button's state as necessary.
this.validateInput($el, $surrogate, deferErrors).done(function() {
self.toggleSubmit();
})
};
//Runs a complete validation. Returns this validator (does not return the result of the validation since the validation is performed in the future).
//@param fn An optional function that will be run after validating, and which will be passed a boolean indicating whether the form passed validation (isValid). [Added by Wynne Crisman 10/2016]
// Allows the user to validate and do something based on the form's validity without any complicated gyrations:
// $form.data('bs.validator').validate(function(isValid) { if(isValid) { /* Allow form submittal. */ }});
Validator.prototype.validate = function(fn) {
var self = this;
//Create a collection of promises by validating each input HTML element, then update the submit logic based on the presence of errors and set the focus to the first element with an error.
$.when(this.$inputs.map(function(el) {
var $el = $(this); //Wrapper the event target with a jquery object.
var $surrogate = getSurrogate($el); //Gets the surrogate widget used to manage the display and functionality. Will === $el if there isn't a surrogate.
return self.validateInput($el, $surrogate, false);
})).then(function() {
self.toggleSubmit();
self.focusError();
//Call the callback, passing whether the form is valid (boolean).
if(fn instanceof Function) fn(!self.isIncomplete() && !self.hasErrors());
});
return this;
};
//Validates the value of an HTML element and returns a promise.
Validator.prototype.validateInput = function($el, $surrogate, deferErrors) {
//var value = getValue($el);
var prevErrors = $el.data('bs.validator.errors'); //Get the errors from a previous run of the validator.
var errors;
if($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]');
//Create a validator event indicating we are about to validate.
var e = $.Event('validate.bs.validator', {relatedTarget: $el[0]});
//Fire the event.
this.$element.trigger(e);
//If the event handlers flag that we shouldn't validate then stop here.
if(e.isDefaultPrevented()) return;
var self = this;
//Run the validators on our HTML element and handle any errors.
return this.runValidators($el).done(function(errors) {
//Save the errors by attaching them to the HTML element.
$el.data('bs.validator.errors', errors);
//If there were no errors then call clearErrors() to remove the error styling on the view, otherwise we do have errors and we either show them immediately (change styling & HTML) or defer the showing of them until later if told to defer.
!errors.length ? self.clearErrors($el) : (deferErrors ? self.defer($el, self.showErrors) : self.showErrors($el));
//If this is the first run of the validator for this element (prevErrors == undefined), or the previous errors are the same as the new errors (nothing changed), then fire an event notifying listeners of the change in error status.
if(!prevErrors || errors.toString() !== prevErrors.toString()) {
if(errors.length) {
e = $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors});
}
else {
e = $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors});
}
//Fire the event.
self.$element.trigger(e);
}
//Update the view's submit elements based on the error state of the form.
self.toggleSubmit();
//Fire an event on the form indicating the related element was validated (irregardless of whether it has errors or not).
self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}));
})
};
//Returns a Promise where the result is the array of errors (error messages) found by the validator. The promise will be passed an empty array if there are no errors.
Validator.prototype.runValidators = function($el) {
var errors = [];
var deferred = $.Deferred();
//If using deferred validation then reject (avoid recursive calls?).
if($el.data('bs.validator.deferred')) $el.data('bs.validator.deferred').reject();
//Set that validation is deferred (avoid recursive calls?).
$el.data('bs.validator.deferred', deferred);
function getValidatorSpecificError(key) {
return $el.data(key + '-error');
}
function getValidityStateError() {
var validity = $el[0].validity;
return validity.typeMismatch ? $el.data('type-error')
: validity.patternMismatch ? $el.data('pattern-error')
: validity.stepMismatch ? $el.data('step-error')
: validity.rangeOverflow ? $el.data('max-error')
: validity.rangeUnderflow ? $el.data('min-error')
: validity.valueMissing ? $el.data('required-error')
: null;
}
function getGenericError() {
return $el.data('error');
}
function getErrorMessage(key) {
return getValidatorSpecificError(key)
|| getValidityStateError()
|| getGenericError();
}
//For each validator in the validator hashmap (key value pair), call the function f(key,validator) using this as the context (so it has access to 'this' from this method).
//See the VALIDATORS hashmap defined above (used as the default set of validator functions).
$.each(this.validators, $.proxy(function(key, validator) {
var error;
//If the HTML element has a value OR is required, AND there is data attached to the HTML element for the current validator OR the validator is 'native', AND the validator produces an error.
if((getValue($el) || $el.attr('required')) && ($el.data(key) || key == 'native') && (error = validator.call(this, $el))) {
//Allow generic or state or specific validator errors to trump the validation error.
error = getErrorMessage(key) || error;
//If the error is not in the list then add it. Use jquery.inArray instead of indexOf since indexOf does not exist in some IE versions.
if(!~$.inArray(error, errors)) errors.push(error);
}
}, this));
//Get errors from the server if a 'remote' URL is provided (allow it to check the element's value and supply additional errors), and then resolve the promise with the collected errors.
//If there are no errors AND the HTML element has a value AND the HTML element has 'remote' data associated, then make an AJAX call sometime in the future to pass the server the element name and value, otherwise resolve the promise passing the error list.
if(!errors.length && getValue($el) && $el.data('remote')) {
this.defer($el, function() {
var data = {};
//Assign elementName = elementValue to the data object.
data[$el.attr('name')] = getValue($el);
//Make an AJAX GET call to the URL stored in the element's 'remote' data, passing the object containing the element's name and its value. If there is an error in the AJAX call then add the error to the error list.
$.get($el.data('remote'), data)
.fail(function(jqXHR, textStatus, error) {
errors.push(getErrorMessage('remote') || error)
})
.always(function() {
deferred.resolve(errors)
})
})
}
else deferred.resolve(errors);
//Return the promise that will be passed the collected errors (array of strings) when the error checking is complete.
return deferred.promise();
};
//Changes the focus to the first element with an error.
Validator.prototype.focusError = function() {
if(!this.options.focus) return;
var $input = this.$element.find(".has-error:first"); // :input
if($input.length === 0) return;
//Find the input field if it is a input group or form group that has the error markings.
if($input.hasClass('form-group') || $input.hasClass('input-group')) {
$input = $input.find('input');
}
//If this is a select2 control then look for the child of a sibling that has the .select2-selection class and use it instead.
if($input.hasClass('select2-hidden-accessible')) {
$input = $input.parent().find('.select2-selection');
}
$('html, body').animate({scrollTop: $input.offset().top - Validator.FOCUS_OFFSET}, 250);
$input.focus();
};
//Alters the display to highlight errors in the form.
Validator.prototype.showErrors = function($el) {
var method = this.options.html ? 'html' : 'text';
var errors = $el.data('bs.validator.errors');
var $group = $el.closest('.form-group,.input-group');
var $block = $group.find('.help-block.with-errors');
var $feedback = $group.find('.form-control-feedback');
if(!errors.length) return;
errors = $('<ul/>')
.addClass('list-unstyled')
.append($.map(errors, function(error) {
return $('<li/>')[method](error)
}));
if($block.data('bs.validator.originalContent') === undefined)
$block.data('bs.validator.originalContent', $block.html());
$block.empty().append(errors);
//Add the 'has-error' and 'has-danger' classes to the grouping.
$group.addClass('has-error has-danger');
//If this is a select2 control then look for the child of a sibling that has the .select2-selection class and use it instead.
if($el.hasClass('select2-hidden-accessible')) {
$el.parent().find('.select2-selection').addClass('has-error has-danger');
}
//If the group has the 'has-feedback' class then remove any success markings and add error markings.
if($group.hasClass('has-feedback')) {
$feedback.removeClass(this.options.feedback.success);
$feedback.addClass(this.options.feedback.error);
$group.removeClass('has-success');
}
};
//Clears the display of all error information.
Validator.prototype.clearErrors = function($el) {
var $group = $el.closest('.form-group,.input-group');
var $block = $group.find('.help-block.with-errors');
var $feedback = $group.find('.form-control-feedback');
$block.html($block.data('bs.validator.originalContent'));
$group.removeClass('has-error has-danger has-success');
//Clean the sibling controls for select2.
$el.parent().find('.select2 .select2-selection').removeClass('has-error has-danger has-success');
$group.hasClass('has-feedback')
&& $feedback.removeClass(this.options.feedback.error)
&& $feedback.removeClass(this.options.feedback.success)
&& getValue($el)
&& $feedback.addClass(this.options.feedback.success)
&& $group.addClass('has-success');
};
//Returns whether the form has any errors.
//Warning: Calling this externally is dangerous because validation is asynchronous and this call will not wait for validation to finish.
// Better to call '$form.data('bs.validator').validate(function(isValid) {...})' instead to ensure validation is done.
Validator.prototype.hasErrors = function() {
function fieldErrors() {
return !!($(this).data('bs.validator.errors') || []).length;
}
return !!this.$inputs.filter(fieldErrors).length;
};
//Returns whether the form is not complete (appears this is due to a text input containing only spaces when a value is required).
//Warning: Calling this externally is dangerous because validation is asynchronous and this call will not wait for validation to finish.
// Better to call '$form.data('bs.validator').validate(function(isValid) {...})' instead to ensure validation is done.
Validator.prototype.isIncomplete = function() {
function fieldIncomplete() {
var value = getValue($(this));
return !(typeof value == "string" ? $.trim(value) : value);
}
return !!this.$inputs.filter('[required]').filter(fieldIncomplete).length;
};
//Prevents form submittal (by setting the preventDefault flag on the event) if the form is not valid or complete.
Validator.prototype.onSubmit = function(e) {
this.validate();
if(this.isIncomplete() || this.hasErrors()) e.preventDefault();
};
//Enables or disables the submit button based on the validation state of the form.
Validator.prototype.toggleSubmit = function() {
if(!this.options.disable) return;
this.$btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
};
//Defers the callback function so that it runs sometime in the future. Only one callback may be deferred at any given time. How far into the future it will be deferred depends on the 'delay' option.
//Used internally to defer validation as the user interacts with the form such that we don't validated constantly (for every character typed).
Validator.prototype.defer = function($el, callback) {
//Wrapper the callback so it's 'this' variable will be the validator.
callback = $.proxy(callback, this, $el);
//If there isn't a delay then run the callback immediately.
if(!this.options.delay) return callback();
//Clear any callback already being delayed.
window.clearTimeout($el.data('bs.validator.timeout'));
//Delay the execution of the callback.
$el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay));
};
//Resets the form to its state, removing all validator modifications (but not removing the validator). Returns the validator reference.
Validator.prototype.reset = function() {
this.$element.find('.form-control-feedback').removeClass(this.options.feedback.error).removeClass(this.options.feedback.success);
this.$inputs
.removeData(['bs.validator.errors', 'bs.validator.deferred'])
.each(function() {
var $this = $(this);
var timeout = $this.data('bs.validator.timeout');
window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout');
});
this.$element.find('.help-block.with-errors')
.each(function() {
var $this = $(this);
var originalContent = $this.data('bs.validator.originalContent');
$this
.removeData('bs.validator.originalContent')
.html(originalContent);
});
this.$btn.removeClass('disabled');
this.$element.find('.has-error, .has-danger, .has-success').removeClass('has-error has-danger has-success');
return this;
};
//Removes the validator completely and resets the state.
Validator.prototype.destroy = function() {
this.reset();
//Remove attributes, data, and event handlers from all elements.
this.$element
.removeAttr('novalidate')
.removeData('bs.validator')
.off('.bs.validator');
//Remove event handlers from input elements.
this.$inputs.off('.bs.validator');
this.options = null;
this.validators = null;
this.$element = null;
this.$btn = null;
return this;
};
// VALIDATOR PLUGIN DEFINITION
// ===========================
//Plugin constructor.
function Plugin(option) {
return this.each(function() {
var $this = $(this);
var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option);
var data = $this.data('bs.validator');
if(!data && option == 'destroy') return;
if(!data) $this.data('bs.validator', (data = new Validator(this, options)));
if(typeof option == 'string') data[option]();
});
}
var old = $.fn.validator;
//JQuery integration.
$.fn.validator = Plugin;
$.fn.validator.Constructor = Validator;
// VALIDATOR NO CONFLICT
// =====================
$.fn.validator.noConflict = function() {
$.fn.validator = old;
return this;
};
// VALIDATOR DATA-API
// ==================
$(window).on('load', function() {
$('form[data-toggle="validator"]').each(function() {
var $form = $(this);
Plugin.call($form, $form.data());
});
});
}(jQuery);