/**
 * 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*$/, "");
};