/*
Validator v3.3.1
(c) Yair Even Or
https://github.com/yairEO/validator
*/
;(function(root, factory){
var define = define || {};
if( typeof define === 'function' && define.amd )
define([], factory);
else if( typeof exports === 'object' && typeof module === 'object' )
module.exports = factory();
else if(typeof exports === 'object')
exports["FormValidator"] = factory();
else
root.FormValidator = factory();
}(this, function(){
function FormValidator( settings, formElm ){
this.data = {}; // holds the form fields' data
this.DOM = {
scope : formElm
};
this.settings = this.extend({}, this.defaults, settings || {});
this.texts = this.extend({}, this.texts, this.settings.texts || {});
this.settings.events && this.events();
}
FormValidator.prototype = {
// Validation error texts
texts : {
invalid : 'inupt is not as expected',
short : 'input is too short',
long : 'input is too long',
checked : 'must be checked',
empty : 'please put something here',
select : 'Please select an option',
number_min : 'too low',
number_max : 'too high',
url : 'invalid URL',
number : 'not a number',
email : 'email address is invalid',
email_repeat : 'emails do not match',
date : 'invalid date',
time : 'invalid time',
password_repeat : 'passwords do not match',
no_match : 'no match',
complete : 'input is not complete'
},
// default settings
defaults : {
alerts : true,
events : false,
regex : {
url : /^(https?:\/\/)?([\w\d\-_]+\.+[A-Za-z]{2,})+\/?/,
phone : /^\+?([0-9]|[-|' '])+$/i,
numeric : /^[0-9]+$/i,
alphanumeric : /^[a-zA-Z0-9]+$/i,
email : {
illegalChars : /[\(\)\<\>\,\;\:\\\/\"\[\]]/,
filter : /^.+@.+\..{2,6}$/ // exmaple email "steve@s-i.photo"
}
},
classes : {
item : 'field',
alert : 'alert',
bad : 'bad'
}
},
// Tests (per type)
// each test return "true" when passes and a string of error text otherwise
tests : {
sameAsPlaceholder : function( field, data ){
if( field.getAttribute('placeholder') )
return data.value != field.getAttribute('placeholder') || this.texts.empty;
else
return true;
},
hasValue : function( value ){
return value ? true : this.texts.empty;
},
// 'linked' is a special test case for inputs which their values should be equal to each other (ex. confirm email or retype password)
linked : function(a, b, type){
if( b != a ){
// choose a specific message or a general one
return this.texts[type + '_repeat'] || this.texts.no_match;
}
return true;
},
email : function(field, data){
if ( !this.settings.regex.email.filter.test( data.value ) || data.value.match( this.settings.regex.email.illegalChars ) ){
return this.texts.email;
}
return true;
},
// a "skip" will skip some of the tests (needed for keydown validation)
text : function(field, data){
var that = this;
// make sure there are at least X number of words, each at least 2 chars long.
// for example 'john F kenedy' should be at least 2 words and will pass validation
if( data.validateWords ){
var words = data.value.split(' ');
// iterate on all the words
var wordsLength = function(len){
for( var w = words.length; w--; )
if( words[w].length < len )
return that.texts.short;
return true;
};
if( words.length < data.validateWords || !wordsLength(2) )
return this.texts.complete;
return true;
}
if( data.lengthRange && data.value.length < data.lengthRange[0] ){
return this.texts.short;
}
// check if there is max length & field length is greater than the allowed
if( data.lengthRange && data.lengthRange[1] && data.value.length > data.lengthRange[1] ){
return this.texts.long;
}
// check if the field's value should obey any length limits, and if so, make sure the length of the value is as specified
if( data.lengthLimit && data.lengthLimit.length ){
while( data.lengthLimit.length ){
if( data.lengthLimit.pop() == data.value.length ){
return true;
}
}
return this.texts.complete;
}
if( data.pattern ){
var regex, jsRegex;
switch( data.pattern ){
case 'alphanumeric' :
regex = this.settings.regex.alphanumeric
break;
case 'numeric' :
regex = this.settings.regex.numeric
break;
case 'phone' :
regex = this.settings.regex.phone
break;
default :
regex = data.pattern;
}
try{
jsRegex = new RegExp(regex).test(data.value);
if( data.value && !jsRegex ){
return this.texts.invalid;
}
}
catch(err){
console.warn(err, field, 'regex is invalid');
return this.texts.invalid;
}
}
return true;
},
number : function( field, data ){
var a = data.value;
// if not not a number
if( isNaN(parseFloat(a)) && !isFinite(a) ){
return this.texts.number;
}
// not enough numbers
else if( data.lengthRange && a.length < data.lengthRange[0] ){
return this.texts.short;
}
// check if there is max length & field length is greater than the allowed
else if( data.lengthRange && data.lengthRange[1] && a.length > data.lengthRange[1] ){
return this.texts.long;
}
else if( data.minmax[0] && (a|0) < data.minmax[0] ){
return this.texts.number_min;
}
else if( data.minmax[1] && (a|0) > data.minmax[1] ){
return this.texts.number_max;
}
return true;
},
// Date is validated in European format (day,month,year)
date : function( field, data ){
var day, A = data.value.split(/[-./]/g), i;
// if there is native HTML5 support:
if( field.valueAsNumber )
return true;
for( i = A.length; i--; ){
if( isNaN(parseFloat( data.value )) && !isFinite(data.value) )
return this.texts.date;
}
try{
day = new Date(A[2], A[1]-1, A[0]);
if( day.getMonth()+1 == A[1] && day.getDate() == A[0] )
return true;
return this.texts.date;
}
catch(er){
return this.texts.date;
}
},
time : function( field, data ){
var pattern = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/;
if( pattern.test(data.value) )
return true;
else
return this.texts.time;
},
url : function( field, data ){
// minimalistic URL validation
if( !this.settings.regex.url.test(data.value) )
return this.texts.url;
return true;
},
hidden : function( field, data ){
if( data.lengthRange && data.value.length < data.lengthRange[0] )
return this.texts.short;
if( data.pattern ){
if( data.pattern == 'alphanumeric' && !this.settings.regex.alphanumeric.test(data.value) )
return this.texts.invalid;
}
return true;
},
select : function( field, data ){
return data.value ? true : this.texts.select;
},
checkbox : function( field, data ){
if( field.checked ) return true;
return this.texts.checked;
}
},
/**
* bind events on form elements
* @param {Array/String} types [description]
* @param {Object} formElm [optional - form element, if one is not already defined on the instance]
* @return {[type]} [description]
*/
events : function( types, formElm ){
var that = this;
types = types || this.settings.events;
formElm = formElm || this.DOM.scope;
if( !formElm || !types ) return;
if( types instanceof Array )
types.forEach(bindEventByType);
else if( typeof types == 'string' )
bindEventByType(types)
function bindEventByType( type ){
formElm.addEventListener(type, function(e){
that.checkField(e.target)
}, true);
}
},
/**
* Marks an field as invalid
* @param {DOM Object} field
* @param {String} text
* @return {String} - useless string (should be the DOM node added for warning)
*/
mark : function( field, text ){
if( !text || !field )
return false;
var that = this;
// check if not already marked as 'bad' and add the 'alert' object.
// if already is marked as 'bad', then make sure the text is set again because i might change depending on validation
var item = this.closest(field, '.' + this.settings.classes.item),
alert = item.querySelector('.'+this.settings.classes.alert),
warning;
if( this.settings.alerts ){
if( alert )
alert.innerHTML = text;
else{
warning = '
' + text + '
';
item.insertAdjacentHTML('beforeend', warning);
}
}
item.classList.remove(this.settings.classes.bad);
// a delay so the "alert" could be transitioned via CSS
setTimeout(function(){
item.classList.add( that.settings.classes.bad );
});
return warning;
},
/* un-marks invalid fields
*/
unmark : function( field ){
var warning;
if( !field ){
console.warn('no "field" argument, null or DOM object not found');
return false;
}
var fieldWrap = this.closest(field, '.' + this.settings.classes.item);
if( fieldWrap ){
warning = fieldWrap.querySelector('.'+ this.settings.classes.alert);
fieldWrap.classList.remove(this.settings.classes.bad);
}
if( warning )
warning.parentNode.removeChild(warning);
},
/**
* removes unmarks all fields
* @return {[type]} [description]
*/
reset : function( formElm ){
var fieldsToCheck,
that = this;
formElm = formElm || this.DOM.scope;
fieldsToCheck = this.filterFormElements( formElm.elements );
fieldsToCheck.forEach(function(elm){
that.unmark(elm);
});
},
/**
* Normalize types if needed & return the results of the test (per field)
* @param {String} type [form field type]
* @param {*} value
* @return {Boolean} - validation test result
*/
testByType : function( field, data ){
data = this.extend({}, data); // clone the data
var type = data.type;
if( type == 'tel' )
data.pattern = data.pattern || 'phone';
if( !type || type == 'password' || type == 'tel' || type == 'search' || type == 'file' )
type = 'text';
return this.tests[type] ? this.tests[type].call(this, field, data) : true;
},
prepareFieldData : function( field ){
var nodeName = field.nodeName.toLowerCase(),
id = Math.random().toString(36).substr(2,9);
field["_validatorId"] = id;
this.data[id] = {};
this.data[id].value = field.value.replace(/^\s+|\s+$/g, ""); // cache the value of the field and trim it
this.data[id].valid = true; // initialize validity of field
this.data[id].type = field.getAttribute('type'); // every field starts as 'valid=true' until proven otherwise
this.data[id].pattern = field.getAttribute('pattern');
// Special treatment
if( nodeName === "select" )
this.data[id].type = "select";
else if( nodeName === "textarea" )
this.data[id].type = "text";
/* Gather Custom data attributes for specific validation:
*/
this.data[id].validateWords = field.getAttribute('data-validate-words') || 0;
this.data[id].lengthRange = field.getAttribute('data-validate-length-range') ? (field.getAttribute('data-validate-length-range')+'').split(',') : [1];
this.data[id].lengthLimit = field.getAttribute('data-validate-length') ? (field.getAttribute('data-validate-length')+'').split(',') : false;
this.data[id].minmax = field.getAttribute('data-validate-minmax') ? (field.getAttribute('data-validate-minmax')+'').split(',') : false; // for type 'number', defines the minimum and/or maximum for the value as a number.
this.data[id].validateLinked = field.getAttribute('data-validate-linked');
return this.data[id];
},
/**
* Find the closeset element, by selector
* @param {Object} el [DOM node]
* @param {String} selector [CSS-valid selector]
* @return {Object} [Found element or null if not found]
*/
closest : function(el, selector){
var matchesFn;
// find vendor prefix
['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn){
if( typeof document.body[fn] == 'function' ){
matchesFn = fn;
return true;
}
return false;
})
var parent;
// traverse parents
while (el) {
parent = el.parentElement;
if (parent && parent[matchesFn](selector)) {
return parent;
}
el = parent;
}
return null;
},
/**
* MDN polyfill for Object.assign
*/
extend : function( target, varArgs ){
if( !target )
throw new TypeError('Cannot convert undefined or null to object');
var to = Object(target),
nextKey, nextSource, index;
for( index = 1; index < arguments.length; index++ ){
nextSource = arguments[index];
if( nextSource != null ) // Skip over if undefined or null
for( nextKey in nextSource )
// Avoid bugs when hasOwnProperty is shadowed
if( Object.prototype.hasOwnProperty.call(nextSource, nextKey) )
to[nextKey] = nextSource[nextKey];
}
return to;
},
/* Checks a single form field by it's type and specific (custom) attributes
* {DOM Object} - the field to be checked
* {Boolean} silent - don't mark a field and only return if it passed the validation or not
*/
checkField : function( field, silent ){
// skip testing fields whom their type is not HIDDEN but they are HIDDEN via CSS.
if( field.type !='hidden' && !field.clientWidth )
return { valid:true, error:"" }
field = this.filterFormElements( [field] )[0];
// if field did not pass filtering or is simply not passed
if( !field )
return { valid:true, error:"" }
// this.unmark( field );
var linkedTo,
testResult,
optional = field.className.indexOf('optional') != -1,
data = this.prepareFieldData( field ),
form = this.closest(field, 'form'); // if the field is part of a form, then cache it
// check if field has any value
/* Validate the field's value is different than the placeholder attribute (and attribute exists)
* this is needed when fixing the placeholders for older browsers which does not support them.
*/
// first, check if the field even has any value
testResult = this.tests.hasValue.call(this, data.value);
// if the field has value, check if that value is same as placeholder
if( testResult === true )
testResult = this.tests.sameAsPlaceholder.call(this, field, data );
data.valid = optional || testResult === true;
if( optional && !data.value ){
return { valid:true, error:"" }
}
if( testResult !== true )
data.valid = false;
// validate by type of field. use 'attr()' is proffered to get the actual value and not what the browsers sees for unsupported types.
if( data.valid ){
testResult = this.testByType(field, data);
data.valid = testResult === true ? true : false;
}
// if this field is linked to another field (their values should be the same)
if( data.valid && data.validateLinked ){
if( data['validateLinked'].indexOf('#') == 0 )
linkedTo = document.body.querySelector(data['validateLinked'])
else if( form.length )
linkedTo = form.querySelector('[name=' + data['validateLinked'] + ']');
else
linkedTo = document.body.querySelector('[name=' + data['validateLinked'] + ']');
testResult = this.tests.linked.call(this, field.value, linkedTo.value, data.type );
data.valid = testResult === true ? true : false;
}
if( !silent )
this[data.valid ? "unmark" : "mark"]( field, testResult ); // mark / unmark the field
return {
valid : data.valid,
error : data.valid === true ? "" : testResult
};
},
/**
* Only allow certain form elements which are actual inputs to be validated
* @param {HTMLCollection} form fields Array [description]
* @return {Array} [description]
*/
filterFormElements : function( fields ){
var i,
fieldsToCheck = [];
for( i = fields.length; i--; ) {
var isAllowedElement = fields[i].nodeName.match(/input|textarea|select/gi),
isRequiredAttirb = fields[i].hasAttribute('required'),
isDisabled = fields[i].hasAttribute('disabled'),
isOptional = fields[i].className.indexOf('optional') != -1;
if( isAllowedElement && (isRequiredAttirb || isOptional) && !isDisabled )
fieldsToCheck.push(fields[i]);
}
return fieldsToCheck;
},
checkAll : function( formElm ){
if( !formElm ){
console.warn('element not found');
return false;
}
var that = this,
result = {
valid : true, // overall form validation flag
fields : [] // array of objects (per form field)
},
fieldsToCheck = this.filterFormElements( formElm.elements );
// get all the input/textareas/select fields which are required or optional (meaning, they need validation only if they were filled)
fieldsToCheck.forEach(function(elm, i){
var fieldData = that.checkField(elm);
// use an AND operation, so if any of the fields returns 'false' then the submitted result will be also FALSE
result.valid = !!(result.valid * fieldData.valid);
result.fields.push({
field : elm,
error : fieldData.error,
valid : !!fieldData.valid
})
});
return result;
}
}
return FormValidator;
}));