/**
* approve.js 0.0.5
* A simple validation library that doesn't interfere.
* Author: Charl Gottschalk
* @license: MIT
*/
/** @namespace approve */
;(function(root, factory) { // eslint-disable-line no-extra-semi
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
// Also create a global in case some scripts
// that are loaded still are looking for
// a global even when an AMD loader is in use.
return (root.approve = factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is self)
root.approve = factory();
}
}(this, function(root) {
/** @constructor */
var approve = {};
/**
* ApproveJs version
* @memberOf approve
* @ignore
*/
approve.VERSION = '0.0.5';
/**
* Default tests.<br>
* Each test has at least three members.<br>
* <code>validate()</code> - the method which is called when testing a value.<br>
* <code>message</code> - the property that holds the default error message.<br>
* <code>expects</code> - the property that is either false if the test expects no parameters, or an array of strings representing the names of the expected parameters.<br>
* Each test either returns a boolean or an object.
* @memberOf approve
* @namespace approve.tests
*/
approve.tests = {
/**
* Checks if a value is present.
* @example
* approve.value('some value', {required: true});
* @function required
* @memberOf approve.tests
* @inner
*/
required: {
validate: function(value) {
return !!value;
},
message: '{title} is required',
expects: false
},
/**
* Checks if a value is a valid email address.
* @example
* approve.value('some value', {email: true});
* @function email
* @memberOf approve.tests
* @inner
*/
email: {
regex: /^((([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, // eslint-disable-line no-control-regex
validate: function(value) {
return this.regex.test(value);
},
message: '{title} must be a valid email address',
expects: false
},
/**
* Checks if a value is a valid web address.
* @example
* approve.value('some value', {url: true});
* @function url
* @memberOf approve.tests
* @inner
*/
url: {
regex: /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} must be a valid web address',
expects: false
},
/**
* Checks if a value is a valid credit card number.
* @example
* approve.value('some value', {cc: true});
* @function cc
* @memberOf approve.tests
* @inner
*/
cc: {
regex: /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} must be a valid credit card number',
expects: false
},
/**
* Checks if a value contains both letters and numbers.
* @example
* approve.value('some value', {alphaNumeric: true});
* @function alphaNumeric
* @memberOf approve.tests
* @inner
*/
alphaNumeric: {
regex: /^[A-Za-z0-9]+$/i,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} may only contain [A-Za-z] and [0-9]',
expects: false
},
/**
* Checks if a value contains only numbers.
* @example
* approve.value('some value', {numeric: true});
* @function numeric
* @memberOf approve.tests
* @inner
*/
numeric: {
regex: /^[0-9]+$/,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} may only contain [0-9]',
expects: false
},
/**
* Checks if a value contains only letters.
* @example
* approve.value('some value', {alpha: true});
* @function alpha
* @memberOf approve.tests
* @inner
*/
alpha: {
regex: /^[A-Za-z]+$/,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} may only contain [A-Za-z]',
expects: false
},
/**
* Checks if a value is a valid decimal.
* @example
* approve.value('some value', {decimal: true});
* @function decimal
* @memberOf approve.tests
* @inner
*/
decimal: {
regex: /^\s*(\+|-)?((\d+(\.\d+)?)|(\.\d+))\s*$/,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} must be a valid decimal',
expects: false
},
/**
* Similar to 'decimal', but for currency values.
* @example
* approve.value('some value', {currency: true});
* @function currency
* @memberOf approve.tests
* @inner
*/
currency: {
regex: /^\s*(\+|-)?((\d+(\.\d\d)?)|(\.\d\d))\s*$/,
validate: function(value) {
return this.regex.test(value);
},
message: '{title} must be a valid currency value',
expects: false
},
/**
* Checks if a value is a valid ipv4 or ipv6 address.
* @example
* approve.value('some value', {ip: true});
* @function ip
* @memberOf approve.tests
* @inner
*/
ip: {
regex: {
ipv4: /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/,
ipv6: /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i
},
validate: function(value) {
return this.regex.ipv4.test(value) || this.regex.ipv6.test(value);
},
message: '{title} must be a valid IP address',
expects: false
},
/**
* Checks if a value is a minimum of n characters.
* @param {Integer} min - The minimum allowed length.
* @example
* approve.value('some value', {min: 5});
* @function min
* @memberOf approve.tests
* @inner
*/
min: {
validate: function(value, pars) {
return typeof value === 'string' && value.length >= pars.min;
},
message: '{title} must be a minimum of {min} characters',
expects: ['min']
},
/**
* Checks if a value is a maximum of n characters.
* @param {Integer} max - The maximum allowed length.
* @example
* approve.value('some value', {max: 20});
* @function max
* @memberOf approve.tests
* @inner
*/
max: {
validate: function(value, pars) {
return typeof value === 'string' && value.length <= pars.max;
},
message: '{title} must be a maximum of {max} characters',
expects: ['max']
},
/**
* Checks if a value's length is between a minimum and maximum.
* @param {Integer} min - The minimum allowed length.
* @param {Integer} max - The maximum allowed length.
* @example
* var rule = {
* range: {
* min: 5,
* max: 20
* }
* };
* approve.value('some value', rule);
* @function range
* @memberOf approve.tests
* @inner
*/
range: {
validate: function(value, pars) {
return typeof value === 'string' && value.length >= pars.min && value.length <= pars.max;
},
message: '{title} must be a minimum of {min} and a maximum of {max} characters',
expects: ['min', 'max']
},
/**
* Checks if a value is the same as the value of another.
* This test gets the value from a DOM <input/> element.
* @param {String} field - The id of the DOM <input/> element to test against.
* @example
* var rule = {
* equal: 'password'
* };
* approve.value('some value', rule);
* @function equal
* @memberOf approve.tests
* @inner
*/
equal: {
validate: function(value, pars) {
return '' + value === '' + pars.value;
},
message: '{title} must be equal to {field}',
expects: ['value', 'field']
},
/**
* Checks if a value passes a given regular expression.
* @param {RegExp} regex - The regular expression to test against. <a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/RegExp" target="_blank">MDN</a>
* @example
* var rule = {
* format: /^[A-Za-z0-9]+$/i
* };
* approve.value('some value', rule);
* @function format
* @memberOf approve.tests
* @inner
*/
format: {
validate: function(value, pars) {
if (Object.prototype.toString.call(pars.regex) === '[object RegExp]') {
return pars.regex.test(value);
}
console.error('approve.tests.format(value, regex): regex is not a valid regular expression.');
return false;
},
message: '{title} did not pass the [{regex}] test',
expects: ['regex']
}
};
/**
* A helper function for formatting strings:
* @example
* this._format('i can speak {language} since i was {age}', {language:'javascript',age:10});
* @example
* this._format('i can speak {0} since i was {1}', 'javascript',10});
* @memberOf approve
* @ignore
*/
approve._format = function(text, col) {
col = typeof col === 'object' ? col : Array.prototype.slice.call(arguments, 1);
return text.replace(/\{\{|\}\}|\{(\w+)\}/g, function (m, n) {
if (m == "{{") { return "{"; }
if (m == "}}") { return "}"; }
return col[n];
});
};
/**
* Returns an array of formatted error messages returned by tests that return objects instead of booleans.
* @example
* this._formatMessages(['array', 'of', 'errors'], title);
* @param {Array} errors - The array of unformatted errors returned by the test's result.
* @param {String} title - The title to replace the {title} placeholder with.
* @return {Array} The formatted errors
* @memberOf approve
* @ignore
*/
approve._formatMessages = function(errors, rule, rules, title) {
var format = this._getFormat(rule, rules, title);
for (var i = errors.length - 1; i >= 0; i--) {
errors[i] = this._format(errors[i], format).trim();
}
return errors;
};
/**
* Returns format object to correctly format an error message with the correct test values.
* @example
* this._getFormat(rule, rules, title);
* @param {String} rule - The current rule being processed.
* @param {Object} rules - The rules object for the value being tested.
* @param {String} title - The title to replace the {title} placeholder with.
* @return {Object} The object used to format an error message
* @memberOf approve
* @ignore
*/
approve._getFormat = function(rule, rules, title) {
var format = {};
// Does the test for the rule expect parameters?
if (Array.isArray(this.tests[rule].expects)) {
// The test for the rule expects paramaters.
// Loop through expected paramaters for the rule's test.
var expects = '';
for (var i = this.tests[rule].expects.length - 1; i >= 0; i--) {
expects = this.tests[rule].expects[i];
// Check if the rule object has the required parameter.
if (rules[rule].hasOwnProperty(expects)) {
// Add the expected parameter's format to the parameter value.
format[expects] = rules[rule][expects];
}
// Expected parameter not present, is the constraint formattable?
if (/^[A-Za-z0-9]+$/i.test(rules[rule])) {
format[expects] = rules[rule];
}
}
}
// Check if the rule has a name property?
// This is used to format the message with the field name.
if (rules.hasOwnProperty('title')) {
// Format the message to include the field name.
format.title = rules.title;
} else {
// Format the message to include the provided title paramater as the field name.
format.title = title;
}
// Return the formatted message.
return format;
};
/**
* Returns the correctly formatted message representing the current test's failure.
* @example
* this._message(rule, rules, title);
* @param {String} rule - The current rule being processed.
* @param {Object} rules - The rules object for the value being tested.
* @param {String} title - The title to replace the {title} placeholder with.
* @return {String} The correctly formatted error message
* @memberOf approve
* @ignore
*/
approve._message = function(rule, rules, title) {
// Does the provided rule have a custom message?
if (rules[rule].hasOwnProperty('message')) {
// The rule has a custom message, return it.
return rules[rule].message;
}
else {
// The rule does not have a custom message.
// Get the default message from the tests.
var message = this.tests[rule].message,
format = this._getFormat(rule, rules, title);
return this._format(message, format).trim();
}
};
/**
* Executes the tests based on given rules to validate a given value.<br><br>
* Returns an object with at least two properties:<br>
* <code>approved</code> : Boolean - <code>true</code> if test succeeded, otherwise <code>false</code>.<br>
* <code>errors</code> : Array of String - holds a list of formatted errors.
* @example
* var result = approve.value('some value', {test: constraints});
* if (result.approved) {
* // Value is approved - do something
* } else {
* // Do something with the errors
* result.each(function(error) {
* console.log(error);
* });
* }
* @param {String|Integer} value - The value to test against the rules.
* @param {Object} rules - The constraints for the value being tested.
* @param {String} [title] - The title to replace the {title} placeholder in error messages with.
* @return {Object} The object containing the result of the tests performed.
* @memberOf approve
*/
approve.value = function(value, rules, title) {
// If rules is not an object, we cannot continue.
if (typeof rules !== 'object') {
console.error('approve.value(value, rules): rules is not a valid object.');
}
// Instantiate a new result object.
var result = {
approved: true,
errors: [],
// Provides easy access to the loop for the errors.
each: function(fn) {
var isFunc = fn && fn.constructor && fn.call && fn.apply;
for (var i = this.errors.length - 1; i >= 0; i--) {
if (isFunc) {
fn(this.errors[i]);
}
}
}
};
// Loop through given rules.
for (var rule in rules) {
if (rules.hasOwnProperty(rule)) {
title = title || '';
// Check if rule exists in tests.
if (this.tests.hasOwnProperty(rule)) {
// Create a pars object for required parameters.
var pars = {};
// Does the test for this rule expect any paramaters?
if (Array.isArray(this.tests[rule].expects)) {
// This test expects paramaters.
// Loop through the test's expected parameters and add the values from the rule.
for (var i = this.tests[rule].expects.length - 1; i >= 0; i--) {
var expects = this.tests[rule].expects[i];
// Check if the rule object has the required parameter.
if (rules[rule].hasOwnProperty(expects)) {
// Add the expected parameter value to the pars object.
pars[expects] = rules[rule][expects];
} else {
// Set the parameter to the rule's value.
pars[expects] = rules[rule];
}
}
}
// Does the rule have config?
if (rules[rule].hasOwnProperty('config')) {
// Add the config to the pars object.
pars.config = rules[rule].config;
}
// Test the value.
var ret = this.tests[rule].validate(value, pars);
// Check if the returned value is an object.
if(typeof ret === 'object')
{
// An object was returned.
// Check if the test was successful.
if (!ret.valid) {
// The test failed, set the result object properties.
result.approved = false;
result.errors.push(this._message(rule, rules, title));
}
// Add the error messages returned by the resluting object.
result.errors = result.errors.concat(this._formatMessages(ret.errors, rule, rules, title));
// Merge any properties from the resulting object with the main result to be returned.
for (var prop in ret) {
if (ret.hasOwnProperty(prop)) {
result[prop] = ret[prop];
}
}
} else {
// Check if the returned value is a boolean
if (typeof ret !== 'boolean') {
// We don't process if it's not a boolean or object.
result.approved = false;
result.errors.push(this._message(rule, rules, title));
} else {
if (!ret) {
// The test failed, set the result object properties.
result.approved = false;
result.errors.push(this._message(rule, rules, title));
}
}
}
}
}
}
// Return the result object.
return result;
};
/**
* Used to add custom tests.
* @example
* var test = {
* expects: false,
* message: '{title} did not pass the test.',
* validate: function(value) {
* return this.strength(value);
* },
* };
* approve.addTest(test, 'test_name');
* @param {Object} obj - The test object to add.
* @param {String} name - The name of the test.
* @return void
* @memberOf approve
*/
approve.addTest = function(obj, name) {
// If obj is not a valid object, we cannot continue.
if (typeof obj !== 'object') {
console.error('approve.addTest(obj, name): obj is not a valid object.');
}
try {
// Check if the test name already exists.
if (!this.tests.hasOwnProperty(name)) {
// The name does not exist, add it to the tests.
this.tests[name] = obj;
}
} catch (e) {
console.error('approve.addTest(): ' + e.message);
}
};
/**
* Checks if a value is a strong password string.
* @example
* var rule = {
* strength: {
* min: 8,
* bonus: 10
* }
* };
* approve.value('some value', rule);
* @return {Object} An object with various properties relating to the value's score.
* @function strength
* @memberOf approve.tests
* @inner
*/
var strength = {
/**
* The minimum length a password must be.
*/
minimum: 8,
/**
* The minimum length a password must be for a bonus point.
*/
minimumBonus: 10,
/**
* The text representing the strength of a password.
*/
messages: {
0: 'Very Weak',
1: 'Weak',
2: 'Better',
3: 'Almost',
4: 'Acceptable',
5: 'Strong',
6: 'Very Strong'
},
/**
* The default error message.
*/
message: '{title} did not pass the strength test.',
/**
* Expects the 'min' and 'bonus' parameters.
*/
expects: ['min', 'bonus'],
/**
* Default error messages
* @type {Object}
*/
errors: {
isMinimum: '{title} must be at least {min} characters',
hasLower: '{title} must have at least 1 lower case character',
hasUpper: '{title} must have at least 1 upper case character',
hasNumber: '{title} must have at least 1 number',
hasSpecial: '{title} must have at least 1 special character'
},
/**
* Returns an object containing the score of a value.
* @param {String} text - The text to score.
* @return {Object} The score of the text.
*/
score: function(text) {
// Create the object that represents the score of the text
var score = {
value: 0,
isMinimum: false,
hasLower: false,
hasUpper: false,
hasNumber: false,
hasSpecial: false,
isBonus: false,
strength: 0
};
// If text is longer than minimum give 1 point.
if (text.length > this.minimum){
score.value++;
score.isMinimum = true;
} else {
score.value = 1;
score.isMinimum = false;
}
// If text has lowercase characters give 1 point.
if ( text.match(/[a-z]/) ) {
if(score.isMinimum) {
score.value++;
}
score.hasLower = true;
}
// If text has uppercase characters give 1 point.
if ( text.match(/[A-Z]/) ) {
if(score.isMinimum) {
score.value++;
}
score.hasUpper = true;
}
// If text has at least one number give 1 point.
if (text.match(/\d+/)) {
if(score.isMinimum) {
score.value++;
}
score.hasNumber = true;
}
// If text has at least one special caracther give 1 point.
if ( text.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/) ) {
if(score.isMinimum) {
score.value++;
}
score.hasSpecial = true;
}
// If text is longer than minimumBonus give another 1 point.
if (text.length > this.minimumBonus) {
score.value++;
score.isBonus = true;
}
// Set the percentage value.
score.strength = Math.ceil((score.value / 6) * 100);
// Return the score object.
return score;
},
/**
* Returns an object containing the score and validation of a value.
* @param {String} text - The text to score.
* @return {Object} The score and validation of the text.
*/
strength: function (text) {
var result = {
message: this.messages[0],
minimum: this.minimum,
minimumBonus: this.minimumBonus,
score: {},
valid: false,
errors: []
};
result.score = this.score(text);
result.message = this.messages[result.score.value];
if (!result.score.isMinimum) {
result.errors.push(this.errors.isMinimum);
}
if (!result.score.hasLower) {
result.errors.push(this.errors.hasLower);
}
if (!result.score.hasUpper) {
result.errors.push(this.errors.hasUpper);
}
if (!result.score.hasSpecial) {
result.errors.push(this.errors.hasSpecial);
}
if (!result.score.hasNumber) {
result.errors.push(this.errors.hasNumber);
}
if (result.score.value > 4) {
result.valid = true;
}
return result;
},
/**
* The method that is called by ApproveJs to perform the test.
* @param {String} value - The value to test.
* @return {Object} The result object of the test.
*/
validate: function(value, pars) {
this.minimum = pars.min || this.minimum;
this.minimumBonus = pars.bonus || this.minimumBonus;
if (pars.hasOwnProperty('config') && pars.config.hasOwnProperty('messages')) {
for (var message in pars.config.messages) {
if (pars.config.messages.hasOwnProperty(message)) {
this.errors[message] = pars.config.messages[message];
}
}
}
return this.strength(value);
},
};
approve.tests.strength = strength;
/*
* Return the main ApproveJs object.
*/
return approve;
}));