﻿// Form42 v1.1 - form validation the Q42 way
// 
// Validates an html form based on validation rules, that are applied by setting classNames on form fields.
// 
// Features:
// 
// - Live validation of form fields, meaning fields will be validated while you type
// - Additional active/valid/invalid classNames are applied to validated elements in real time, to allow styling through css
// - Validation rules are applied by classNames on elements, so no addional script is required
// - Invalid characters are blocked while you type
// - Integration requires no additional script
// - Multiple validation rule classNames are allowed per form field, and final validation is based on all rules together
// 
// Usage:
// 
// Step 1: Include the form42.js script in your HTML page. Example:
// 
//   <script type="text/javascript" src="form42.js"></script>
// 
// Step 2: Validate the form. Example:
// 
//    <form ... class="form42-validate">
//
// Optionally you can add a form42-error-[elementId] className, which will place the resulting error message
// inside the element with its id attribute matching [elementId]. Additionally, when an error is shown, this
// element gains the className "visible", so you can style them both. Example:
//
//   <form ... class="form42-validate form42-error-myErrorMessage">
//   <div id="myErrorMessage"></div>
//
// And then in your css file add something in the lines of this:
//
// #myErrorMessage { display:none; }
// #myErrorMessage.visible { display:block; }
// 
// Step 3: Apply validation rules by applying one or more validation className to your elements. Examples:
// 
//   <input type="text" name="age" class="form42-integer" />
//   <input type="text" name="emailaddress" class="form42-email" />
//   <input type="checkbox" name="agreeToTerms" class="form42-required" />
//   <input type="password" name="password1" class="form42-minlength-3 form42-maxlength-10 form42-alnumhyphen" />
//   <input type="password" name="password2" class="form42-equal-password1 form42-minlength-3 form42-maxlength-10 form42-alnumhyphen" />
//   <textarea name="comments" class="form42-required"></textarea>
// 
// Todo:
//   
// - Check radio buttons for required


/** Form42 singleton object that does all the form validation work
  */
var Form42 =
{
  // collection of all rules
  rules: {},
  // collection of all live elementValidators
  elementValidators: {},
  // form error fields to place the message in
  formErrorFields: {},
  // helper for uniqueID
  uniqueIDCounter: 0,
  
  /** Initializes all validation rules for forms and elements
    */
  init: function()
  {
    var els = Form42.getElementsByTagNames(document, ["input","textarea","select","form"]),
        formInfo;
    for (var i=0; i<els.length; i++)
    {
      if (Form42.getField(els[i]))
      {
        if (els[i].tagName == "FORM")
        {
          Agent.addEventListener(els[i], "submit", Form42.onSubmitListener);
          errorId = els[i].className.replace(/.*form42-error-([^\b\s]+).*/g, "$1");
          if (errorId != els[i].className)
            Form42.formErrorFields[Form42.getUniqueId(els[i])] = errorId;
        }
        else
          Form42.validateElement(null, els[i]);
      }
    }
  },
  /** retrieves a static array of elements based on tagNames
    * @rootEl the root element in which to look for elements
    * @tagNameArray array consisting of tagNames
    * @return an array of elements inside rootEl that have their tagName in tagNameArray
    */
  getElementsByTagNames: function(rootEl, tagNameArray)
  {
    var arr = [];
    for (var i=0; i<tagNameArray.length; i++)
      for (var tmp = rootEl.getElementsByTagName(tagNameArray[i]), j=0; j<tmp.length; arr.push(tmp[j++]));
    return arr;
  },
  /** the listener that gets attached to forms and validates. If the form is invalid, it prevents submit.
    */
  onSubmitListener: function(evt)
  {
    //check if it was firsttime
    var formEl = evt.target || evt.srcElement;
    if (formEl.className.indexOf("form42-validate-first") != -1)
      formEl.className = formEl.className.replace("form42-validate-first", "");
        
    var el = !Agent.IE? evt.target : event.srcElement;    
    for (; el && el.nodeName != "FORM"; el = el.parentNode);
    if (!Form42.validate(el))
      Agent.preventDefault(evt);
  },
  /** validates a form. If invalid it shows an error message and focusses the first invalid field.
    * @el the form element
    * @return true if the form is valid
    */
  validate: function(el)
  {
    var els = Form42.getElementsByTagNames(el, ["input","textarea","select"]),
        invalids = [], 
        errorMessage = "",
        i,
        lang = "en",
        langEl = el;
    while (langEl && !langEl.lang)
      langEl = langEl.parentNode;
    if (langEl) lang = langEl.lang;
    for (i=0; i<els.length; i++)
    {
      curInvalids = Form42.validateElement(null, els[i]);
      invalids = invalids.concat(curInvalids);
    }
    for (i=0; i<invalids.length; i++)
      errorMessage += invalids[i].getMessage(lang) + "\n";
    if (errorMessage == "")
      return true;

    var errorId = Form42.formErrorFields[Form42.getUniqueId(el)];
    if (errorId)
    {
      var errorEl = document.getElementById(errorId);
      errorEl.innerHTML = errorMessage.replace(/\n/g,"<br/>");
      errorEl.className = errorEl.className.replace(/visible/gi, "") + " visible";
    }
    else
      alert(errorMessage);

    invalids[0].el.focus();
  },
  /** gets a unique id for this element
    * @el the element to get a unique id for
    * @return a unique id
    */
  getUniqueId: function(el)
  {
    if (!el.uniqueID)
      el.uniqueID = this.uniqueIDCounter++;
    return el.uniqueID;
  },
  /** validates an element based on the form rules applied to it through classNames
    * @evt browser event object that fired this method to run
    * @el the element to validate instead of the srcElement of the evt (optional)
    * returns an array of elementValidators that did not validate for this element
    */
  validateElement: function(evt, el)
  {
    var el = Form42.getField(el? el : !Agent.IE? evt.target : event.srcElement), 
        invalids = [];
    if (el)
    {
      for (var fs = Form42.getElementValidators(el),i=0; i<fs.length; i++)
      {
        if (!fs[i].validate())
          invalids.push(fs[i]);
      }
      Form42.updateValidState(el, invalids.length == 0);
    }
    return invalids;
  },
  /** gets called on element focus, and updates the active state based on if the element has focus or not
    * @evt browser event object that fired this method to run
    * @deActive boolean to explicitly set the state to inactive (used by element blur handler)
    */
  activate: function(evt, deActivate)
  {
    var el = Form42.getField(!Agent.IE? evt.target : event.srcElement);
    if (el)
    {
      Form42.updateActiveState(el, !deActivate);
      Form42.validateElement(evt);
    }
  },
  /** gets called on element blur, removes the actime className and validates the element
    * @evt browser event object that fired this method to run
    */
  deactivate: function(evt)
  {
    Form42.activate(evt, true);
  },
  /** validates the key that is pressed for a validated element
    * @evt browser event object that fired this method to run
    */
  keyPressListener: function(evt)
  {
    var el = Form42.getField(!Agent.IE? evt.target : event.srcElement);
    if (el)
      for (var fs = Form42.getElementValidators(el),i=0; i<fs.length; i++)
        fs[i].validateInput(!Agent.IE? evt : event);
  },
  /** retrieves the element that has validation classNames applied to it
    * @el the element to verify
    * returns the element that has validation classNames applied to it
    */
  getField: function(el)
  {
    if (el.nodeType == 1 && el.className.indexOf("form42-")!=-1)
      return el;
  },
  /** retrieves an array of elementValidators for this element
    * @el the element to get the active elementValidator for
    * returns an array of elementValidators for this element
    */
  getElementValidators: function(el)
  {
    var id = this.getUniqueId(el),
        fs = this.elementValidators[id];
    if (!fs)
    {
      fs = [];      
      var matches = el.className.match(/form42-[^\b\s]+/g), i;
      if (matches)
      {
        for (i=0; i<matches.length; i++)
        {
          var ruleName = fullName = matches[i].replace(/.*form42-([^\b\s]+)[\b\s]?.*/g,"$1"),
              rule = null, 
              f;
          while (!rule)
          {
            rule = this.rules[ruleName];
            var pos = ruleName.lastIndexOf("-");
            if (pos >= 0)
              ruleName = ruleName.substring(0, pos);
            else
              break;
          }
          
          if (rule)
          {
            f = new ElementValidator(el, rule);
            f.info = "" + fullName.substring(ruleName.length + 1);
            fs.push(f);
          }
        }
      }
    }
    this.elementValidators[id] = fs;
    return fs;
  },
  /** applies a form42 specific className to this element based on its valid state
    * @el the element to apply the className to
    * @isValid boolean indicating the valid state
    */
  updateValidState: function(el, isValid)
  {
    el.className = el.className.replace(/form42-(in)?valid/gi, "") + " form42-" + (isValid? "valid" : "invalid");
  },
  /** applies a form42 specific className to this element based on its active state
    * @el the element to apply the className to
    * @isActive boolean indicating the active state
    */
  updateActiveState: function(el, isActive)
  {
    el.className = el.className.replace(/form42-active/gi,"") + (isActive? " form42-active" : "");
  },
  /** adds a validation rule to form42, so elements can apply this className to them
    * @rule a Form42Rule instance
    */
  addRule: function(rule)
  {
    var ruleNames = rule.name.split(",");
    for (var i=0; i<ruleNames.length; i++)      
      this.rules[ruleNames[i]] = rule;
  }
};

/** Agent offers some crossBrowser methods
  */
var Agent =
{
  IE: (navigator.appName == "Microsoft Internet Explorer"),
  FF: (navigator.appName == "Netscape"),
  OP: (navigator.appName== "Opera"),
  SF: (navigator.appName== "Safari"),

  addEventListener: function(el, strEventName, listener)
  {
    if (this.IE || this.OP)
      el.attachEvent("on" + strEventName, listener);
    else
      el.addEventListener(strEventName, listener, true);
  },
  preventDefault : function(evt)
  {
    if (this.IE)
      evt.returnValue = false;
    else
      evt.preventDefault();
  }
};

/** ElementValidator class. Each instance validates a single single element using a single Form42Rule
  * @el the element to validate, either of type input, textarea or select
  * @rule the Form42Rule instance to validate with
  */
function ElementValidator(el, rule)
{
  this.el = el;
  this.rule = rule;
  if (this.rule.maxLength > 0)
    this.el.setAttribute("maxLength", this.rule.maxLength);
  Agent.addEventListener(el, "focus", Form42.activate);
  Agent.addEventListener(el, "blur", Form42.deactivate);
  Agent.addEventListener(el, "keypress", Form42.keyPressListener);
  Agent.addEventListener(el, "keyup", Form42.validateElement);
  Agent.addEventListener(el, "change", Form42.validateElement);
}
ElementValidator.prototype =
{
  /** validates the key that was pressed based on Form42Rule.inputPattern, and prevents if invalid
    * @evt browser event object that fired this method to run
    */
  validateInput: function(evt)
  {
    var charCode = !Agent.IE? evt.which : event.keyCode;
    if (charCode)
    {
      var charStr = String.fromCharCode(charCode) + "";
      if (!evt.ctrlKey && !evt.altKey)
      { 
        if (charCode > 31)
        {
          if (this.rule.upperCase)
            charStr = charStr.toUpperCase();
          if (this.rule.lowerCase)
            charStr = charStr.toLowerCase();
          if (!charStr.match(this.rule.inputPattern))
            Agent.preventDefault(evt);
        }
        this.validate();
      }
    }    
  },
  /** validates the value based on Form42Rule.validationPattern
    * @evt browser event object that fired this method to run
    * @returns true if it validates
    */
  validate: function()
  {
    var isValid = this.rule.preValidator? this.rule.preValidator(this) : true;
    var v = this.el.value;

    if (this.rule.upperCase)
      v = v.toUpperCase();
    if (this.rule.lowerCase)
      v = v.toLowerCase();        
    if (this.rule.replacement)
      v = v.replace(this.rule.validationPattern, this.rule.replacement);
    if (v != this.el.value)
      this.el.value = v;
    if (this.rule.minLength > 0)
      isValid = isValid && v.length >= this.rule.minLength;
    
    return isValid && v.match(this.rule.validationPattern);
  },
  /** returns the error message for this element and rule
    * @lang the language of the message, defaults to english
    * @returns the error message
    */
  getMessage: function(lang)
  {
    var fieldName = this.el.getAttribute("name");
    var ruleName = this.rule.name.replace(/,.*/,"");
    if (this.info.length > 0)
      ruleName += " " + this.info;
    var langMsg = this.rule.message[lang];
    if (!langMsg)
      langMsg = this.rule.message["en"];
      
    if (ruleName == "required")
      ruleName = "ingevuld";
    if (ruleName == "cijfer")
      ruleName = "een cijfer";
      
    return langMsg.replace(/\$fieldName/gi, fieldName? fieldName : "").replace(/\$ruleName/gi, ruleName);
  }
};
Agent.addEventListener(window, "load", Form42.init);

// Form42Rule - form validation rule class for Form42
// 
// Creates an instance of a Form42Rule, which is automatically added to Form42 and is then ready for use.
// Validation is done by regular expressions. Input validation can be used for validating the input while 
// the user types, and regular validation is done when the system requires it to. 
// 
// Simple usage:
// 
//   new Form42Rule(strName, inputPattern, validationPattern);
// 
// Where 
//   
//   strName is a string representing the name of the rule
//   inputPattern is the regular expression to match each pressed key by
//   validationPattern is the regular expression to match the entire value by
// 
// Advanced usage:
// 
//   var rule = new Form42Rule(strName, inputPattern, validationPattern);
// 
//   Then you can set one or more of the following properties:
// 
//   minLength
//   maxLength
//   upperCase
//   lowerCase
//   replacement
//   preValidator
// 
//   For example:
// 
//   rule.minLength = 3;
//   rule.maxLength = 21;
//   rule.upperCase = true;
//   rule.lowerCase = false;
//   rule.replacement = "foo $1 $2 bar"; // use this as regular expression replacement when value matches validationPattern
// 
// Very advanced usage:  
//   
//   rule.prevalidator = function(elementValidator)
//   {
//     // do something with the elementValidator here, which exposes 
//     // .el (the element) and .info (the remainder of the className, like "3" for "form42-minlength-3")
//   }
// 
// Changing the language for error messages can best be explained by javascript examples:
// 
//   Form42.rule.email.message.fr = "This email does not validate in French!";
//   Form42.rule.myrule.message.qq = "Some qq language here for the myrule rule";
//   Form42.rule.minlength.message.qq = "Some qq language here for the minlength rule";
// 
//   You can use $fieldName and $ruleName in the message, which will get replaced by respectively the value
//   of the name attribute on the element, or the rule name with extra info as specified in the className.


/** Form42Rule class. Each instance represents a simple or complex rule to validate an element with
  * @name name of the rule
  * @inputPattern regular expression pattern to match input with (optional, use null to ignore)
  * @validationPattern regular expression pattern to match the value with (optional, use null to ignore)
  */
function Form42Rule(name, inputPattern, validationPattern)
{
  this.name = name;
  if (inputPattern)
    this.inputPattern = inputPattern;
  if (validationPattern)
    this.validationPattern = validationPattern;
    
  this.message = {"en":"The field $fieldName must be of type $ruleName.", "nl":"Het veld $fieldName moet $ruleName zijn."};
  Form42.addRule(this);
}
Form42Rule.prototype =
{
  inputPattern: /.*/,
  validationPattern: /.*/
};

// Add rules
new Form42Rule("alnumhyphen", /[A-Za-z0-9-_]/, /^[A-Za-z0-9-_]*$/);
new Form42Rule("alnumhyphenat", /[A-Za-z0-9-_@]/, /^[A-Za-z0-9-_@]*$/);
new Form42Rule("alphabetic", /[A-Za-z]/, /^[A-Za-z]*$/);
new Form42Rule("alphanumeric", /[A-Za-z0-9]/, /^[A-Za-z0-9]*$/);
new Form42Rule("alphaspace", /[A-Za-z0-9-_ ]/, /^[A-Za-z0-9-_ ]*$/);
new Form42Rule("double", /[0-9-+.]/, /^([-+]?[0-9]+(\.[0-9]+)?)?$/);
new Form42Rule("double-comma", /[0-9-+,]/, /^([-+]?[0-9]+(\,[0-9]+)?)?$/);
new Form42Rule("empty", null, /^$/);
new Form42Rule("numeric", /\d/, /^\d*$/);
new Form42Rule("cijfer", /[0-9+-]/, /^([-+]?[0-9]+)?$/);
new Form42Rule("email", /[A-Za-z0-9._%-@]/, /^([A-Z0-9._%\-\+]+@(?:[A-Z0-9-]+\.)+(?:[A-Z]{2,4}|museum))?$/i);

// required
var r = new Form42Rule("required", null, /^.+$/);
r.preValidator = function(obj)
{   
  return (obj.el.type == "checkbox")? obj.el.checked : true;
};

// minlength
r = new Form42Rule("minlength");
r.preValidator = function(obj)
{
  var reg = new RegExp("^.{" + parseInt(obj.info) + ",}$");
  return obj.el.value.match(reg);
};

// maxlength
r = new Form42Rule("maxlength");
r.preValidator = function(obj)
{
  obj.el.setAttribute("maxLength", parseInt(obj.info));
  return true;
  //var reg = new RegExp("^.{0," + parseInt(obj.info) + "}$");
  //return obj.el.value.match(reg);
};

// date (supports any format, like dd-mm-yyyy, or mm/dd/yy or ddmmyyyy or ...)
r = new Form42Rule("date", /[\d\/-]/);
r.preValidator = function(obj)
{
  // prevalidate based on the date format
  var i = obj.info, v = obj.el.value, reg = i.replace(/[mdy]/g,"\\d").replace(/\//g,"\\/"),
      m = v.match(reg), mm, dd, yyyy, yPos, date, reg, result = v.match("^(" + reg + ")?$");
  if (m)
  {
    dd = v.substr(i.indexOf("dd"), 2) * 1;
    mm = v.substr(i.indexOf("mm"), 2) * 1;
    yPos = i.indexOf("yyyy");
    yyyy = ((yPos > -1)?  v.substr(yPos, 4) : "20" + v.substr(i.indexOf("yy"), 2)) * 1;
    date = new Date(yyyy, mm-1, dd);
    result = (date.getFullYear() == yyyy && date.getMonth() == mm - 1 && date.getDate() == dd);
  }  
  return (yyyy == "0000" && dd == "00" && mm == "00") || result;
};

// equals
r = new Form42Rule("equal");
r.preValidator = function(obj)
{
  var el = document.getElementById(obj.info);
  if (!el)
    el = document.getElementsByName(obj.info)[0];  
  return (el && el.value==obj.el.value);
};


