/**
* JavaScript Form Validation
*
* @author David Miles
* @created 11/04/2010
* @revised 11/06/2010
*/
/**
* Console fix
*/
if(console === undefined){
var console = { log: function(msg){ alert("Console: " + msg); }};
}
/**
* Page load event
*/
window.onload = function(){
/**
* Submit button click event
*/
document.getElementById("tpsReportSubmit").onclick = function(){
var validator = new FormValidator(document.getElementById("tpsReport"), document.getElementById("messages"));
/*
* Custom validation handlers
*/
// Email validation
validator.handler({
"fieldName": "email",
"handler": function(f, e){
var regEx = new RegExp("^\\w+@\\w+\\.\\w{2,3}$", "i");
return regEx.test(e.value);
},
"message": "Email must be valid"
});
// Password match validation
validator.handler({
"fieldName": "password",
"handler": function(f, e){
var password = f.password.value;
var confirm = f.confirmPassword.value;
return password === confirm;
},
"message": "Passwords must match"
});
// Password length validation
validator.handler({
"fieldName": "password",
"handler": function(f, e){
return e.value.length >= 6
},
"message": "Password must be at least 6 characters"
});
// Program run time
validator.handler({
"fieldName": "programRunTime",
"handler": function(f, e){
return !isNaN(e.value);
},
"message": "Program run time must be a number"
});
// Number of errors
validator.handler({
"fieldName": "numErrors",
"handler": function(f, e){
return !isNaN(e.value);
},
"message": "Number of errors must be a number"
});
// Validate the form
var isValid = validator.validate();
// Valid form?
var output = "";
if(isValid){
// Display success message
document.getElementById("tpsReport").addClass("hidden");
document.getElementById("messages").addClass("success");
output = "<strong>Successfully submitted form!</strong>";
}else{
// Display errors
var errors = validator.getErrors();
output += "<ul>";
for(var i = 0; i < validator.getNumErrors(); i++){
output += "<li>" + errors[i] + "</li>";
}
output += "</ul>";
}
document.getElementById("messages").innerHTML = output;
// Return to stop submission
return isValid;
};
};
/**
* FormValidator class
*/
function FormValidator(form, messages){
// Private fields {{{
var errors = [];
var customValidation = [];
// }}}
// Private methods {{{
/**
* Get the associated label for an element
*
* @param element e Element to find the label for
*/
function getElementLabel(e){
// If we're dealing with a radio button, we get the label by using the name
// of this form element. Otherwise, we use the ID.
var label = e.type != "radio" ? document.getLabelById(e.id) : document.getLabelById(e.name);
// To prevent undefined errors, create a new label if one wasn't found
if(!label){
label = document.createElement("label");
}
return label;
}
/**
* Remove the error classes from a form element and its label
*
* @param element e Element to remove an error from
*/
function removeErrorOnElement(e){
e.removeClass("error");
getElementLabel(e).removeClass("error");
}
/**
* Add the error classes to a form element and its label
*
* @param element e Element to add an error for
*/
function addErrorOnElement(e){
e.addClass("error");
getElementLabel(e).addClass("error");
}
// }}}
// Public methods {{{
/**
* Validates the form
*
* @return boolean
*/
this.validate = function(){
// Array of checked elements
var elementErrors = [];
// Loop through all form elements
for(var i = 0; i < form.elements.length; i++){
// Current element
var e = form.elements[i];
// If this element already has an error, move along
// Note: This should only occur for radio button groups
if(elementErrors.exists(e.name)){
continue;
}
// Check for required fields
if(e.type == "text" || e.type == "select-one" || e.type == "password"){
// Reset this element
removeErrorOnElement(e);
if(e.hasClass("required") && e.value.trim() == ""){
addErrorOnElement(e);
errors.push("Required field missing: " + getElementLabel(e).innerHTML);
elementErrors.push(e.name);
}
}else if(e.type == "radio" && e.hasClass("required")){
// Reset element
removeErrorOnElement(e);
// Get the NodeList for this radio button group
// Note: If we use the element, we can't check the group's value and would
// have to manually loop through the group. This is cumbersome, so it's
// easier to get the NodeList instance and call getSelectedValue() to return
// the selected value of this group.
var nodeList = eval("form." + e.name);
if(nodeList.getSelectedValue() == undefined){
addErrorOnElement(e);
errors.push("Required field missing: " + getElementLabel(e).innerHTML);
elementErrors.push(e.name);
}
}
// Run custom validation if no errors exist for this element
for(var j = 0; j < customValidation.length && !elementErrors.exists(e.name); j++){
if(customValidation[j].fieldName == e.name && customValidation[j].hasRun == false){
// Run custom validation handlers, and pass in the form object and the
// current element object
var result = customValidation[j].handler(form, document.getElementById(customValidation[j].fieldName));
// Make sure we do not run this custom validation handler a second time
customValidation[j].hasRun = true;
// Add error on this field if the result of the handler was boolean false
if(!result){
addErrorOnElement(e);
errors.push(customValidation[j].message);
elementErrors.push(e.name);
}
}
}
}
return errors.length == 0;
};
/**
* Return the number of errors
*
* @return integer
*/
this.getNumErrors = function(){
return errors.length;
};
/**
* Return the errors array
*
* @return array
*/
this.getErrors = function(){
return errors;
};
/**
* Add a custom validation handler
*
* @param object params Custom validation parameters in object form -- fieldName, handler and message properties
*/
this.handler = function(params){
customValidation.push({
"fieldName": params.fieldName,
"handler": params.handler,
"message": params.message,
"hasRun": false
});
};
// }}}
}
/**
* Checks if an element has a certain class
*
* @param string class CSS class to search for
* @return boolean
*/
Element.prototype.hasClass = function(cssClass){
return new RegExp("\\b" + RegExp.escape(cssClass.trim()) + "\\b").test(this.className.trim());
};
/**
* Add a class to an element
*
* @param string class CSS class to add
*/
Element.prototype.addClass = function(cssClass){
if(!this.hasClass(cssClass)){
this.className = this.className.trim() + " " + cssClass.trim();
}
};
/**
* Remove a class from an element
*
* @param string class CSS class to remove
*/
Element.prototype.removeClass = function(cssClass){
if(this.hasClass(cssClass)){
var regEx = new RegExp("\\b" + RegExp.escape(cssClass.trim()) + "\\b");
this.className = this.className.replace(regEx, "");
}
};
/**
* Checks if a value exists in an array already
*
* @param mixed value Value to check for
* @return boolean
*/
Array.prototype.exists = function(value){
for(var i = 0; i < this.length; i++){
if(this[i] === value){
return true;
}
}
return false;
}
/**
* Get the selected value of a node list (radio button list)
*
* @return string
*/
NodeList.prototype.getSelectedValue = function(){
for(var i = 0; i < this.length; i++){
if(this.item(i).checked){
return this.item(i).value;
}
}
};
/**
* IE Fix: HTMLCollection is the equivalent of NodeList for IE
*
* @return string
*/
HTMLCollection.prototype.getSelectedValue = NodeList.prototype.getSelectedValue;
/**
* Find a label based on an ID
*
* @param string id ID to find the label for
* @return element
*/
document.getLabelById = function(id){
var labels = document.getElementsByTagName("label");
for(var i = 0; i < labels.length; i++){
if(labels[i].getAttribute("for") === id){
return labels[i];
}
}
};
/**
* Escape a string to be used within a regular expression
* Found at StackOverflow:
* http://stackoverflow.com/questions/494035/how-do-you-pass-a-variable-to-a-regular-expression-javascript
*
* @param string str String to be escaped
* @return string
*/
RegExp.escape = function(str) {
return str.replace(/([.?*+^$[\]\\(){}-])/g, "\\$1");
};
/**
* Trims whitespace on both ends of a string
*
* @return string
*/
String.prototype.trim = function(){
return this.replace(/^\s*/, "").replace(/\s*$/, "");
};