/*!
* Copyright (c) 2009 Francesco Mele jsbeans@francescomele.com
*
* This Software is licenced under the LGPL Licence (GNU Lesser General
* Public License).
* In addition to the LGPL Licence the Software is subject to the
* following conditions:
*
* i every modification must be public and comunicated to the Author
* ii every "jsbean" added to this library must be self consistent
* except for the dependence from jsbeans-x.x.x.js
* iii copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* A form's validator
* @namespace jsbeans
* @class Validator
* @static
* */
jsbeans.Validator = {
/**
* Default messages for default asserts.
* @property messages
* @type JSON
* @static
* */
messages: {
prefix: "Errors occurred in the following fields:\n--------------------------\n",
required: "is required",
requiredByName: "at least one is required",
integer: "must be an integer",
email: "is not a valid email address",
date: "is not a valid date",
maxValue: "exceedes the maximum value allowed for the field",
minValue: "is less than the minimum value allowed for the field",
maxLen: "is too long",
minLen: "is too short",
numeric: "must be a number",
invalid: "is not valid"
},
/**
* Default options are:
* <pre>
* errorClass: null, // the style class to apply in case of invalid value
* // default function invoked in case of at least one invalid value.
* // It get an <Array<JSON>> and this options as arguments
* callback: function(errors, options) {
* jsbeans.Validator.manageErrors(errors, options);
* },
* propagate: true, // if false validation will stop at first error
* focusOnFirst: true, // if true focus will be applied in first invalid input
* extraFunctions: null, // a list of functions for extra validation, useful for custom validation
* locale: "en" // default locale
* </pre>
* @property options
* @type JSON
* @static
* */
options: {
errorClass: null,
callback: function(errors, options) {
jsbeans.Validator.manageErrors(errors, options);
},
propagate: true,
focusOnFirst: true,
extraFunctions: null,
locale: "en"
},
/**
* Loads a locale and sets it for error messages.
* @method setLocale
* @param locale {String} a string representing a locale. It loads <code><jsbeans_script_location>/Validator_<locale>.js</code>.
* @static
* */
setLocale: function(locale) {
// there's no need to load the default
if (locale != "en") {
jsbeans.load("Validator_" + locale);
jsbeans.Validator.options.locale = locale;
}
},
/**
* @property _errorClassCache
* @type any
* @private
* @static
* */
_errorClassCache: null,
/**
* Use this method instead of overriding <code class="param">jsbeans.Validator.options</code> one by one or passing options on each <code class="methd">validate</code> invocation.
* @method init
* @param options {JSON} same as <code class="param">options</code>
* @static
* */
init: function(options) {
this.options.errorClass = options.errorClass || this.options.errorClass;
this.options.callback = options.callback || this.options.callback;
this.options.propagate = typeof(options.propagate) != "undefined" ? options.propagate : this.options.propagate;
this.options.focusOnFirst = typeof(options.focusOnFirst) != "undefined" ? options.focusOnFirst : this.options.focusOnFirst;
this.options.locale = options.locale || this.options.locale;
},
/**
* An Array of errors. Each error is a JSON in the form.
* <pre>
* object: <DOM>, // the invalid input DOM Object
* label: <String>, // the label for <code class="param">object</code> as returned by <code class="methd">getLabelFor</code>
* type: <String>, // a string representing the name of the validation ('required', 'maxLen', ...)
* message: <String> // the string containing the error message
* </pre>
* @property errors
* @type Array<JSON>
* @static
* */
errors: new Array(),
/**
* Main method.<br/>
* Sample:
* <pre>
* <form action="an_action" onsubmit="<strong>return validate();</strong>">
* <label for="firstname">First Name<label>: <input type="text" id="firstname" /><br/>
* <label for="email">Email address<label>: <input type="text" id="email" /><br/>
* <label for="age">First Name<label>: <input type="text" id="age" />
* </form>
* <script type="text/javascript">
* function <strong>validate()</strong> {
* return jsbeans.Validator.validate({
* firstname: ["required"],
* email: ["required", "email"],
* age: ["minValue=18"]
* });
* }
* </script>
* </pre>
* @method validate
* @param inputs {JSON} a JSON with the id of the input to validate as key and an Array of validation's asserts as value.
* @param [options] {JSON} override of default <code class="param">jsbeans.Validator.options</code>
* @return {boolean} true if there are no errors
* @static
* */
validate: function(inputs /*, options*/) {
var options = arguments[1] || {};
this.options.errorClass = options.errorClass || this.options.errorClass;
this.options.callback = options.callback || this.options.callback;
//
this.options.propagate = typeof(options.propagate) != "undefined" ? options.propagate : this.options.propagate;
var oPragt = this.options.propagate;
this.options.focusOnFirst = typeof(options.focusOnFirst) != "undefined" ? options.focusOnFirst : this.options.focusOnFirst;
//
this.options.extraFunctions = typeof(options.extraFunctions) != "undefined" ? options.extraFunctions : null;
var oExtras = this.options.extraFunctions;
this.options.locale = options.locale || this.options.locale;
this.errors = new Array();
var obj, label, asserts;
outer: for (var input in inputs) {
obj = document.getElementById("" + input);
label = this.getLabelFor("" + input);
asserts = inputs[input];
for (var i = 0, assert; assert = asserts[i]; i++) {
var arg = null;
if (assert.indexOf("=") != -1) {
arg = assert.substring(assert.indexOf("=") + 1);
assert = assert.substring(0, assert.indexOf("="));
}
// register the current number of errors
var before = this.errors.length;
if (!this._invoke(obj, assert, arg)) {
// if current number of errors is different, the actual assert has registered an error, otherwise
// this is the default
if (before == this.errors.length) {
this.errors.push({object: obj, label: label, type: assert, message: this._getMessage(assert)});
}
if (oPragt == false) {
break outer;
}
}
else {
this._clearErrorClass(obj);
}
}
}
if (oExtras != null) {
var f;
var isFunction = function(fn) {
return !!fn && typeof fn != "string" && !fn.nodeName && fn.constructor != Array && /function/i.test( fn + "" );
};
var extrasLen = oExtras.length;
for (var i = 0; i < extrasLen; i++) {
f = oExtras[i];
if (isFunction(f)) {
try {
f.apply();
}
catch(e) {
alert(e.message);
}
}
else {
try {
eval("(" + f + ")");
}
catch(e) {
alert(e.message);
}
}
}
}
if (this.errors.length > 0) {
options = this.options;
eval("" + options.callback(this.errors, options));
return false;
}
return true;
},
/**
* For mandatory fields.
* @method assertRequired
* @param input {DOM} the input to validate
* @return {boolean} false if input is empty (trimmed) and checkbox or radio are not checked
* @static
* */
assertRequired: function(obj) {
if (obj.type.toLowerCase() == "checkbox" || obj.type.toLowerCase() == "radio") {
return obj.checked;
}
return ! (obj.value == null || ((obj.value || "").replace(/^\s+|\s+$/g, "")) == "");
},
/**
* For mandatory fields with same 'name' attribute of the given one. Each field is checked using {@method} <code class="methd">assertRequired</code> method.<br>
* Useful for input of type radio and checkbox.
* @method assertRequiredByName
* @param input {DOM} one of the input whose name is used to retrieve all the inputs to be validated
* @return {boolean} true if there is at least one valid field
* @static
* */
assertRequiredByName: function(obj, arg) {
var name = obj.getAttribute('name');
if (typeof name != 'undefined' && name != null && name != '') {
var inputs = document.getElementsByName(name);
var areValid = inputs.length;
for (var i = 0; i < inputs.length; i++) {
if (this.assertRequired(inputs[i]) == false) {
areValid--;
}
}
if (areValid == 0) {
var labelFor = arg || obj.id;
this.errors.push({object: obj, label: this.getLabelFor(labelFor), type: "requiredByName", message: this._getMessage("requiredByName")});
}
return areValid != 0;
}
// no inputs found as no 'name' attribute exists
return true;
},
/**
* Validation of email address
* @method assertEmail
* @param input {DOM} the input to validate
* @return {boolean} true for valid email address
* @static
* */
assertEmail: function(obj) {
var re = /^[^\s()<>@,;:\/]+@\w[\w\.-]+\.[a-z]{2,}$/i;
return obj.value == "" || re.test(obj.value);
},
/**
* Validation for Date with optional check for format, min date (after) and max date (before).
* @method assertDate
* @param input {DOM} the input to validate
* @param options {JSON}
* <pre>
* format: <String>, // only dd/MM/yyyy and MM/dd/yyyy allowed, default dd/MM/yyyy if browser's language is "IT", MM/dd/yyyy otherwise
* min: <String>, // a string representing a date in the same format of 'format' option, default 01/01/1970
* max: <String>, // a string representing a date in the same format of 'format' option, default 01/01/3000
* </pre>
* For <code>min</code> and <code>max</code> options you can use the string <code>today</code> as special value or the name of a global function returning the time (Date object or milliseconds) to check against.
* @return {boolean}
* @static
* */
assertDate: function(obj, options) {
var val = (obj.value || "").replace(/^\s+|\s+$/g, "");
if (val == "") {
return true;
}
var getDefaultFormat = function() {
var res = "dd/MM/yyyy";
var lang = null;
if (typeof navigator.userLanguage != "undefined") {
lang = navigator.userLanguage.toUpperCase();
}
else if (typeof navigator.language != "undefined") {
lang = navigator.language.toUpperCase();
}
if (lang != null && lang.indexOf("IT") != -1) {
res = "dd/MM/yyyy";
}
else {
res = "MM/dd/yyyy";
}
return res;
};
var options = arguments[1];
if (typeof(options) == "undefined") {
// defaults
options = {
format: getDefaultFormat()
,min: "01/01/1970"
,max: "01/01/3000"
};
}
else {
options = eval("(" + options + ")");
options.format = typeof(options.format) != "undefined" ? options.format : getDefaultFormat();
options.min = typeof(options.min) != "undefined" ? options.min : "01/01/1970";
options.max = typeof(options.max) != "undefined" ? options.max : "01/01/3000";
}
// try some optimizations
var oFor = options.format;
var oMin = options.min;
var oMax = options.max;
var separator = oFor.indexOf("/") != -1 ? "/" : "-";
var sp = val.split(separator);
if (sp.length != 3) {
return false;
}
var dd, mm, yy;
if (oFor.indexOf("d") == 0) {
dd = sp[0];
mm = sp[1];
}
else {
dd = sp[1];
mm = sp[0];
}
yy = sp[2];
if (yy.length != 4) {
return false;
}
var _safeParseInt = function(v) {
// workaround for a parseInt bug (or feature?): parseInt("0w") = 0
// @see jsbeans.string.parseInt
if (v == null || v == "") {
return NaN;
}
var re = /[0-9]/;
for (var i = 0; i < v.length; i++) {
if (!re.test(v.charAt(i))) {
return NaN;
}
}
while (v.substring(0).indexOf("0") == 0) {
v = v.substring(1);
}
return v == "" ? 0 : parseInt(v);
};
yy = _safeParseInt(yy);
if (isNaN(yy)) {
return false;
}
mm = _safeParseInt(mm);
if (isNaN(mm) || mm > 12) {
return false;
}
dd = _safeParseInt(dd);
if (isNaN(dd) || dd > 31) {
return false;
}
if (dd > 30 && (mm == 4 || mm == 6 || mm == 9 || mm == 11)) {
return false;
}
var isLeap = false;
if ((yy % 4 == 0) || (yy % 100 == 0) || (yy % 400 == 0)) {
if (dd > 29 && mm == 2) {
return false;
}
}
else {
if (dd > 28 && mm == 2) {
return false;
}
}
var _floor = function(ms) {
var res;
if (ms) {
// a Date has been passed
if (ms.constructor && ("" + ms.constructor).indexOf("Date") != -1) {
res = ms;
}
// assuming ms are milliseconds
else {
res = new Date(ms);
}
}
else {
res = new Date();
}
res.setHours(0);
res.setMinutes(0);
res.setSeconds(0);
return res;
};
// now check for min and max
// from any type to integer yyyyMMdd
var _getComparableFormat = function(v) {
if (v == "today") {
var today = _floor();
var _yy = today.getFullYear();
var _mm = today.getMonth() + 1;
if (_mm < 10) {
_mm = "0" + _mm;
}
var _dd = today.getDate();
if (_dd < 10) {
_dd = "0" + _dd;
}
}
else if (v.constructor && ("" + v.constructor).indexOf("Date") != -1) {
var _yy = v.getFullYear();
var _mm = v.getMonth() + 1;
if (_mm < 10) {
_mm = "0" + _mm;
}
var _dd = v.getDate();
if (_dd < 10) {
_dd = "0" + _dd;
}
}
else {
var vs = v.split(separator);
if (oFor.indexOf("d") == 0) {
_dd = vs[0];
_mm = vs[1];
}
else {
_dd = vs[1];
_mm = vs[0];
}
_yy = vs[2];
_yy = _safeParseInt(_yy);
_mm = _safeParseInt(_mm);
_dd = _safeParseInt(_dd);
if (_mm < 10) {
_mm = "0" + _mm;
}
if (_dd < 10) {
_dd = "0" + _dd;
}
}
return parseInt("" + _yy + _mm + _dd);
};
var dateTime = _getComparableFormat(val);
// if min or max are functions, we just use them to get time, otherwise they are strings representing dates in the current format
var fMin = window[oMin];
if (typeof fMin == "function") {
var min = _getComparableFormat(_floor(fMin()));
}
else {
var min = _getComparableFormat(oMin);
}
//
var fMax = window[oMax];
if (typeof fMax == "function") {
var max = _getComparableFormat(_floor(fMax()));
}
else {
var max = _getComparableFormat(oMax);
}
if (dateTime < min || dateTime > max) {
return false;
}
return true;
},
/**
* Validation for integers.
* @method assertInteger
* @param input {DOM} the input to validate
* @return {boolean} false if <code class="param">input</code>'s value isn't a number (as returned by javascript's native <code>isNaN</code>) and it contains dots or commas.
* @static
* */
assertInteger: function(obj) {
if (obj.value.indexOf(".") != -1 || obj.value.indexOf(",") != -1) {
return false;
}
return !isNaN(obj.value);
},
/**
* Validation for numeric, integers included, using javascript's native <code>isNaN</code> function.
* @method assertNumeric
* @param input {DOM}
* @return {boolean}
* @static
* */
assertNumeric: function(obj) {
return !isNaN(obj.value);
},
/**
* Checks if {@param} <code class="param">input</code>'s value is less or equals than {@param} <code class="param">maxValue</code> using javascript's native <code>parseFloat</code> function.
* @method assertMaxValue
* @param input {DOM} the input to validate
* @param maxValue {Float}
* @return {boolean}
* @static
* */
assertMaxValue: function(obj, maxValue) {
var val = (obj.value || "").replace(/^\s+|\s+$/g, "");
if (val == "") {
return true;
}
return parseFloat(val) <= parseFloat(maxValue);
},
/**
* Checks if {@param} <code class="param">input</code>'s value is greater or equals than {@param} <code class="param">minValue</code> using javascript's native <code>parseFloat</code> function.
* @method assertMinValue
* @param input {DOM} the input to validate
* @param minValue {Float}
* @return {boolean}
* @static
* */
assertMinValue: function(obj, minValue) {
var val = (obj.value || "").replace(/^\s+|\s+$/g, "");
if (val == "") {
return true;
}
return parseFloat(val) >= parseFloat(minValue);
},
/**
* Checks if {@param} <code class="param">input</code>'s value length is less or equals than {@param} <code class="param">maxLen</code>.
* @method assertMaxLen
* @param input {DOM} the input to validate
* @param maxLen {Integer | String} Both Integers or Strings may be used as they will be parsed by javascript's native <code>parseInt</code> function.
* @return {boolean}
* @static
* */
assertMaxLen: function(obj, maxLen) {
var val = (obj.value || "").replace(/^\s+|\s+$/g, "");
if (val == "") {
return true;
}
return val.length <= parseInt(maxLen);
},
/**
* Checks if {@param} <code class="param">input</code>'s value length is greater or equals than {@param} <code class="param">minLen</code>.
* @method assertMinLen
* @param input {DOM} the input to validate
* @param minLen {Integer | String} Both Integers or Strings may be used as they will be parsed by javascript's native <code>parseInt</code> function.
* @return {boolean}
* @static
* */
assertMinLen: function(obj, minLen) {
var val = (obj.value || "").replace(/^\s+|\s+$/g, "");
if (val == "") {
return true;
}
return val.length >= parseInt(minLen);
},
/**
* Default function delegated to print errors. It uses an <code>alert</code>.
* @method manageErrors
* @param errors {Array<JSON>} see <code class="param">jsbeans.Validator.errors</code> for details.
* @param options {JSON} see <code class="">jsbeans.Validator.options</code> for details.
* @static
* */
manageErrors: function(errors, options) {
if (errors != null && errors.length > 0) {
var errorClass = options.errorClass || this.options.errorClass;
var res = jsbeans.Validator.messages.prefix;
var loc = jsbeans.Validator.messages[jsbeans.Validator.options.locale];
if (typeof loc != "undefined" && typeof loc["prefix"] != "undefined") {
res = loc["prefix"];
}
var focus1st = this.options.focusOnFirst == true;
for (var i = 0, error; error = errors[i]; i++) {
if (error.object == null || typeof error.object == "undefined") {
// this should happen only in development stage or, for those with less experience, in runtime-generated environments
var msg = "[dev-warning] Validation error on field " + error.label;
if (typeof JSON != "undefined" && typeof JSON.stringify != "undefined") {
msg += " (" + JSON.stringify(error) + ")";
}
alert(msg);
continue;
}
if (i == 0 && focus1st) {
error.object.focus();
}
res += (typeof error.label == "undefined" ? error.message : ("\"" + error.label + "\" " + error.message)) + "\n";
// adds errorClass if required
// Note that it adds errorClass as many times as error is found, then clears them if input is valid
if (errorClass != null) {
error.object.className = (typeof error.object.className != "undefined") ? (error.object.className + " " + errorClass) : "" + errorClass;
}
}
alert(res);
}
},
/**
* Brings <code class="param">jsbeans.Validator.options</code> to defaults.
* @method reset
* @static
* */
reset: function() {
this._errorClassCache = this.options.errorClass;
this.options.errorClass = null;
this.options.callback = function(errors, options) {
jsbeans.Validator.manageErrors(errors, options);
};
this.options.propagate = true;
this.options.locale = "en";
},
/**
* @method _clearErrorClass
* @param errors {DOM}
* @private
* @static
* */
_clearErrorClass: function(obj) {
obj.className = obj.className ? obj.className.split(this.options.errorClass).join("") : "";
if (this._errorClassCache != null) {
obj.className = obj.className.split(this._errorClassCache).join("");
}
},
/**
* A convenince method to get a human readble label for inputs. It always tries to return something in the following order:
* <ol>
* <li>looks in the page for a <code>label</code> tag with the 'for' attribute set to {@param} <code class="param">id</code></li>
* <li>if there's no DOM Object with given <code class="param">id</code>, the <code class="param">id</code> itself is returned</li>
* <li>looks for the 'title' attribute of the DOM Object as returned by a <code>document.getElementById</code> call</li>
* <li>looks for the 'name' attribute of the DOM Object as returned by a <code>document.getElementById</code> call</li>
* <ol>
* Result is trimmed to avoid unwanted characters such as spaces, newlines, ...
* @method getLabelFor
* @param id {String} the DOM Object id
* @return {String}
* @static
* */
getLabelFor: function(id) {
var trim = function(s) {
return s.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
var labels = document.getElementsByTagName("label");
for (var i = 0, l; l = labels[i]; i++) {
try {
var f = l.getAttributeNode("for").nodeValue;
if (f != null && f == id) {
return trim(l.innerHTML);
}
}
catch(e) {}
}
var o = document.getElementById(id);
if (o == null) {
return id;
}
var _val = function(obj, attrName) {
var res = null;
var node = o.getAttributeNode(attrName);
if (node != null && typeof(node) != "undefined") {
var v = node.nodeValue;
if (v != null && typeof(v) != "undefined" && v != "") {
res = v;
}
}
return res;
};
var res = _val(o, "title");
if (res == null) {
res = _val(o, "name");
}
return res != null ? trim(res) : id;
},
/**
* @method _invoke
* @param object {DOM}
* @param type {String}
* @param arg {String}
* @private
* @static
* */
_invoke: function(obj, type, arg) {
try {
if (obj == null) {
throw {message: "Input to validate is null!"};
}
var _func = "assert" + type.charAt(0).toUpperCase() + type.substring(1);
var res = false;
if (typeof jsbeans.Validator[_func] != "undefined") {
if (arg != null) {
res = eval("jsbeans.Validator." + _func + "(obj,arg)");
}
else {
res = eval("jsbeans.Validator." + _func + "(obj)");
}
}
else if (typeof jsbeans.Validator[type][_func] != "undefined") {
if (arguments[2]) {
var arg = arguments[2];
res = eval("jsbeans.Validator." + type + "." + _func + "(obj,arg)");
}
else {
res = eval("jsbeans.Validator." + type + "." + _func + "(obj)");
}
}
// no method found in jsbeans.Validator
else {
throw {message: "Missing method!"};
}
}
catch(e) {
var m = "Invoke error: " + e.message;
if (typeof console != "undefined") {
console.log(m);
}
else {
alert(m);
}
}
return res;
},
/**
* @method _getMessage
* @param key {String}
* @return {String}
* @private
* @static
* */
_getMessage: function(key) {
var l = "";
if (jsbeans.Validator.options.locale != null) {
l = jsbeans.Validator.options.locale;
}
var msgs = jsbeans.Validator.messages;
// current locale for default validation
if (typeof(msgs[l]) != "undefined" && typeof(msgs[l][key]) != "undefined") {
return msgs[l][key];
}
// current locale for extensions
if (typeof(jsbeans.Validator[key]) != "undefined" && typeof(jsbeans.Validator[key].messages) != "undefined" && typeof(jsbeans.Validator[key].messages[l]) != "undefined") {
return jsbeans.Validator[key].messages[l];
}
// default locale: en
if (typeof(msgs.en) != "undefined" && typeof(msgs.en[key]) != "undefined") {
return msgs.en[key];
}
// default locale for extensions
if (typeof(jsbeans.Validator[key]) != "undefined" && typeof(jsbeans.Validator[key].messages) != "undefined" && typeof(jsbeans.Validator[key].messages.en) != "undefined") {
return jsbeans.Validator[key].messages.en;
}
// straigth message
if (typeof(msgs[key]) != "undefined") {
return msgs[key];
}
/*
try {
return eval("jsbeans.Validator." + key + ".messages['" + key + "']");
} catch(e) {
try {
return eval("jsbeans.Validator." + key + ".messages['" + key + "']");
} catch(e1) {}
}
*/
// when everything else fails
return msgs["invalid"];
}
};