// ==============================================================================
//                     JavaScript Form Validation Libarary
// ==============================================================================
// a DentedReality Production 2001,2002,2003
// written by Beau Lebens <beau@dentedreality.com.au>
// See: http://www.dentedreality.com.au/jsvalidation/
// Version 1.3

// Validate the fields on a form using specific, custom attributes
// required="true" => this field must have something in it. Use this
//  in combination with another rule normally.
// validate="true" => validate this field, provided it has a rule
// rule="rule" => use the rule specified to validate this input "value"
//  Options for "rule":
//    "txt"   = Text Only (and spaces)
//    "num"   = Numbers Only (allow decimal)
//    "int"   = Integers only
//    "phone"   = Phone number, (0-9()-+)
//    "ssn"     = Social Security Number (0-9-)
//    "date"    = Date Format (dd/mm/yyyy)
//    "email"   = Email addresses
//    "alnum"   = Alpha-Numeric Values
//    "curr"    = Currency ($xxxx..."."xx)
//    "addr"    = Address characters (a-zA-Z0-9-,#)
//    "name"    = Most likely characters in a name  (a-zA-Z,.-)
//    "free"    = Freeform text (a-zA-Z0-9+-_()[]/&)
//    "nobadsql"  = Denies harmful SQL syntax
//    "notnull" = Must enter something
//    RegExp    = Any valid regular expression you may specify
//          ie. "/^[\d]{0,3}.[\d]{0,3}.[\d]{0,3}$/" = an IP number
// error="Extended String" => the error message if validation fails. (without a
//  newline character at the end or a bullet at the start, they are added)
// ==============================================================================


// Default Operational Values, Modify these to change default actions of
// the data validation library.

// This is the rule to use to validate a value if none is specified.
var defaultValidationRule = "free";

// This is the first part of the error message, set to blank to just show a list
// of errors.
var defaultErrorPrefix  = "This form cannot be submitted with the following errors:";

// [ELEMENT] is replaced with the field name when displayed.
var defaultErrorMessage = "The value in the '[ELEMENT]' field is invalid.";

// Tied fields that need to have requirements removed after validation is complete
// if an error has been found
var removeRequireds = [],
    removeValidates = [];

// Set this to true to display debugging messages
// This can be done in the validateForm call:
// validateForm(formelement, {'debug':true})
var debug = false;

// ==============================================================================
//                 ACTUAL VALIDATION CODE
// ==============================================================================

// Actual code for determining and then applying a regular expression
// to the designated form element. Returns true if the pattern matches,
// false if it doesn't.
function validateField(el, rule, isObj) {
  // This is a switch used for reversing the desired regular
  // expression result.
  // Set to "true" if matching a regexp successfully should
  // result in a failure.
  invertResult = false;

  // Check if they supplied a custom RegExp or just a rule name
  if (rule.charAt(0) != "/") {
    // These are the pre-defined rules available, you can
    // edit this list by using one as an example.
    switch (rule.toLowerCase()) {
      case "txt" :          // Text only allowed
        re = /^[a-z ]*$/i
        break;
      case "num" :          // Only numbers (allow decimal)
        re = /^[\d\.]*$/
        break;
      case "int" :          // Integers only
        re = /^\d*$/
        break;
      case "phone" :        // Phone number characters
        re = /^(\d+[\-\+\. ]?)?(\(?\d{3}\)?[\-\+\. ]?)?\d{3}[\-\+\. ]?\d{4}$/
        break;
      case "ssn" :          // Social Security Number
        re = /^\d{3}-?\d{2}-?\d{4}$/
        break;
      case "date" :         // Well-formed Dates
        re = /^((\d{1,2}(\/|-)){0,2}(\d{2}|\d{4}))$/
        // re = /^((\d{1,2}(\/|-)\d{1,2}(\/|-)(\d{2}|\d{4})))$/
        break;
      case "email" :        // Something valid-ish for an email
        //re = /^[a-z\d\.\-_]+@[a-z\d\.\-_]{2,}\.[a-z]{2,10}$/i   original validation
          re = /^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$/i   //fixed to disallow consecutive dots
        break;
      case "alnum" :        // Alpha-numeric characters
        re = /^[\w ]*$/i
        break;
      case "curr" :         // Currency (using "$", "," and ".")
        re = /^\$?\d*,?\.?\d{0,2}$/i
        break;
      case "addr" :         // Base Address rule
        re = /^[\w- \.,#]+$/i
        break;
      case "name" :         // Good for validating names
        re = /^[\w- \.,]+$/i
        break;
      case "nobadsql" :     // Denies SQL which could be harmful
        re = /((delete|drop|update|replace|kill|lock) )/gi
        invertResult = true;
        break;
      case "notnull" :      // Requires *anything*
        re = /.+/
        break;
      case "free" :         // Freeform, text, num and some chars
      default :             // Default - See "free"
        re = /.*/
    }
  } // This means they specified a RegExp of their own, which should be
    // in the form "/<RegExp>/" and needs to be "eval"ed before using.
    else {
    re = eval(rule);
  }

  var val = (isObj ? el.value : el.form[el.getAttribute('name')].value),
      req = (isObj ? el.requires : el.getAttribute('requires')),
      vld = (isObj ? el.validates : el.getAttribute('validates'));

  eval('req = ' + req);
  eval('vld = ' + vld);

  addRequirement(el.form, val, req);
  addValidation(el.form, val, vld);

  // If debugging, display the match made by the rule regex
  if (debug) {
    var regex = re.toString(),
        value = val.toString(),
        match = val.match(re),
        result = re.test(val).toString(),
        invert = invertResult.toString();

    alert ('regex: ' + regex + '\n'
           + 'value: ' + value + '\n'
           + 'match: ' + (match && match.toString ? match.toString() : match) + '\n'
           + 'result: ' + result + '\n'
           + 'invert: ' + invert);
  }

  // Do the actual regular expression testing against the string
  // and return the result.
  if (re.test(val)) {
    // If this rule uses result inversion, then return the opposite
    if (invertResult === true) {
      return false;
    } else {
      return true;
    }
  } else {
    // If this rule uses result inversion, then return the opposite
    if (invertResult === true) {
      if (val == ''
          && ((isObj && !el.required)
              || (el.getAttribute && el.getAttribute('required') != 'true'))) {
        return false;
      } else {
        return true;
      }
    } else {
      if (val == ''
          && ((isObj && !el.required)
              || (el.getAttribute && el.getAttribute('required') != 'true'))) {
        return true;
      } else {
        return false;
      }
    }
  }
}

function addRequirement(frm, val, req) {
  if (req && typeof (req) == 'object' && req[val]) {
    if (typeof (req[val]) == 'string') {
      req[val] = [req[val]];
    }

    if (typeof (req[val]) == 'object') {
      for (var i = 0; i < req[val].length; i++) {
        var req_fld = frm[req[val][i]];

        if (!req_fld) {
          continue;
        }

        if (isRadioOrCheckbox(req_fld)) {
          req_fld = req_fld[0];
        }

        removeRequireds[removeRequireds.length] = req[val][i];

        if (req_fld.setAttribute) {
          req_fld.setAttribute('required', 'true');
        }

        req_fld.required = true;

        if (debug) {
          alert('adding requirement to ' + req[val][i] + ' from value "' + val + '"');
        }
      }
    }
  }
}

function addValidation(frm, val, vld) {
  if (vld && typeof (vld) == 'object' && vld[val]) {
    if (typeof (vld[val]) == 'string') {
      vld[val] = [vld[val]];
    }

    if (typeof (vld[val]) == 'object') {
      for (var i = 0; i < vld[val].length; i++) {
        var vld_fld = frm[vld[val][i]];

        if (!vld_fld) {
          continue;
        }

        if (isRadioOrCheckbox(vld_fld)) {
          vld_fld = vld_fld[0];
        }

        removeValidates[removeValidates.length] = vld[val][i];

        if (vld_fld.setAttribute) {
          vld_fld.setAttribute('validate', 'true');
        }

        vld_fld.validate = true;

        if (debug) {
          alert('adding validation to ' + vld[val][i] + ' from value "' + val + '"');
        }
          
      }
    }
  }
}

function removeRequirements(frm) {
  for (var i = 0; i < removeRequireds.length; i++) {
    var fld = frm[removeRequireds[i]];

    if (!fld) {
      continue;
    }

    if (isRadioOrCheckbox(fld)) {
      fld = fld[0];
    }

    if (fld.removeAttribute) {
      fld.removeAttribute('required');
    }

    fld.required = null;
  }

  removeRequireds = [];
}

function removeValidations(frm) {
  for (var i = 0; i < removeValidates.length; i++) {
    var fld = frm[removeValidates[i]];

    if (!fld) {
      continue;
    }

    if (isRadioOrCheckbox(fld)) {
      fld = fld[0];
    }

    if (fld.removeAttribute) {
      fld.removeAttribute('validate');
    }

    fld.validate = null;
  }

  removeValidates = [];
}

function isRadioOrCheckbox(fld) {
  if (fld && fld.length && fld[0] && fld[0].tagName
      && fld[0].tagName.toLowerCase() == 'input') {
    return true;
  }

  return false;
}

// This is the mainline function which needs to be called on form submission to
// check required fields
function validateForm(frm) {
  // Defaults
  var useAlert = false,
      useDivError = true,
      returnErrors = true;

  // set in function call like:
  // validateForm('theformid', {useAlert:true});
  // validateForm(formelement, {useDivError:false});
  // validateForm('theformid', {useAlert:true, useDivError:false});
  if (arguments.length > 1) {
    var opt = arguments[1];

    for (var o in opt) {
      eval(o + '=' + opt[o].toString());
    }
  }

  if (typeof(frm) == 'string') {
    frm = document.forms[frm];
  }

  if (!frm) {
    return false;
  }

  var errors = [defaultErrorPrefix];
  var checkedFields = []

  var fields = frm.getElementsByTagName('*'),
      true_fields = ['input', 'textarea', 'select'];

  // Loop through every element in the form specified.
  for (var idx = 0; idx < fields.length; idx++) {
    if (!fields[idx] || !fields[idx].tagName
        || !in_array(fields[idx].tagName.toLowerCase(), true_fields)) {
      continue;
    }

    var curr_error = '', rule = '', msg  = '';

    // Set default validation rule if none is specified
    var rule = fields[idx].getAttribute('rule');
    if (!rule) {
      rule = defaultValidationRule;
    }

    var fld = fields[idx],
        name = fld.getAttribute('name');

    // ensure the form_element exists and hasn't already been validated
    if (fld && typeof(fld) != 'undefined' &&
        !in_array(name, checkedFields)) {
      if (fld.type == 'radio') {
        // radio buttons are validated differently

        var radio_flds = frm[name];

        // check this field's validity and store any error message
        curr_error = radio_valid(frm, radio_flds, name, rule);

        // set the className of these fields accordingly to indicate errors
        error_class(name, curr_error);
        for (var i = 0; i < radio_flds.length; i++) {
          error_class(radio_flds[i], curr_error);
          error_class(radio_flds[i].id, curr_error);
        }
      } else if (fld.type == 'checkbox') {
        // checkboxes are also validated differently

        var cb_flds = frm[name];

        // check this field's validity and store any error message
        curr_error = cb_valid(frm, cb_flds, name, rule);

        // set the className of these fields accordingly to indicate errors
        error_class(name, curr_error);
        for (var i = 0; i < cb_flds.length; i++) {
          error_class(cb_flds[i], curr_error);
          error_class(cb_flds[i].id, curr_error);
        }
      } else
        if (// ensure the form element exists
            fld && typeof(fld) != 'undefined' && fld.getAttribute

            // make sure the field is supposed to be validated
            && fld.getAttribute('validate')) {
        // check this field's validity and store any error message
        curr_error = input_valid(fld, name, rule);

        // set the className of this field accordingly to indicate errors
        error_class(fld, curr_error);
        error_class(name, curr_error);
      }

      // add this field to the checkedFields array to avoid checking it again
      checkedFields[checkedFields.length] = name;

      if (curr_error != '') {
        errors[errors.length] = curr_error;
      }

      if (useDivError) {
        // make sure no errors are displaying in the errorDiv
        clear_children(name);

        // there was an error
        if (curr_error != '') {
          // display the error in an error div
          if (fld.getAttribute('useerror')) {
            var err_name = fld.getAttribute('useerror'),
                err_fld = fld.form[err_name],
                err_alt = get_error_msg(err_fld, err_name);
            div_error(err_alt, err_fld, err_name);
          } else {
            div_error(curr_error, fld, name);
          }
        }
      }
    }
  }

  // This checks the current error message array length. If it is more than 1
  // (which means an error occurred and an error string was added) then it
  // displays the error and returns false to prevent the form submission
  if (errors.length > 1) {
    // Consider implementing customisable pop-up window here rather than basic
    // alert. Customise window title, colours
    if (useAlert) {
      alert(errors.join('\n'));
    }

    if (useDivError) {
      for (var i = 0; i < fields.length; i++) {
        if (fields[i].className.match(/\bJSVerror\b/)) {
          var curr_url = window.location.href.toString();
          curr_url = curr_url.substring(0, curr_url.indexOf('#'));
          window.location.href = curr_url + '#top';
          fields[i].focus();
          break;
        }
      }
    }

    if (returnErrors) {
      return errors;
    }

    removeRequirements(frm);
    removeValidations(frm);

    return false;
  } // If the current error message is the same as the default, then that means
    // nothing has gone wrong, so just return true and allow the form to be
    // submitted and be processed as per normal.
    else {
    if (returnErrors) {
      return [];
    }

    return true;
  }
}

// returns true or false based on whether any one field of radio type has been
// checked
function radio_valid(frm, flds, name, rule) {
  if (!frm
      || !flds || !flds[0] || !flds[0].getAttribute
      || flds[0].getAttribute('required') != 'true') {
    return '';
  }

  var valid = false;
  var error = get_error_msg(flds[0], name);

  var requires = flds[0].getAttribute('requires'),
      validates = flds[0].getAttribute('validates');

  for (var i = 0; i < flds.length; i++) {
    if (flds[i].checked) {
      flds[i].requires = requires;
      flds[i].validates = validates;

      valid = validateField(flds[i], rule, true);

      if (debug) {
        alert ('radio_valid for ' + name + ': ' + valid.toString());
      }

      break;
    }
  }

  if (!valid) {
    return error;
  } else {
    return '';
  }
}

function cb_valid(frm, flds, name, rule) {
  if (!frm || !flds) {
    return '';
  } else if (!flds[0] || !flds[0].getAttribute
      || (flds[0].getAttribute('required') != 'true'
          && flds[0].getAttribute('validate') != 'true')) {
    if (!flds.getAttribute
        || (flds.getAttribute('required') != 'true'
            && flds.getAttribute('validate') != 'true')) {
      return '';
    } else {
      flds = [flds];
    }
  }

  var valid = false;
  var error = get_error_msg(flds[0], name);

  var val = '', req = false;
  for (var i = 0; i < flds.length; i++) {
    if (flds[i].getAttribute('required')) {
      req = true;
    }

    if (flds[i].checked) {
      if (flds[i].getAttribute) {
        if (flds[i].getAttribute('requires')) {
          flds[i].requires = flds[i].getAttribute('requires');
        }

        if (flds[i].getAttribute('validates')) {
          flds[i].validates = flds[i].getAttribute('validates');
        }
      }

      if (val != '') {
        val += ',';
      }

      val += flds[i].value;
    }
  }

  var fld = {
    'form' : frm,
    'name' : name,
    'value' : val,
    'required' : req
  };

  var valid = validateField(fld, rule, true);

  if (debug) {
    alert ('cb_valid for ' + name + ': ' + valid.toString());
  }

  if (!valid) {
    return error;
  } else {
    return '';
  }
}

function input_valid(fld, name, rule) {
  if (!fld || !fld.form) {
    return '';
  }

  // And set the default error message
  // Replaces [ELEMENT] with the field name in the default
  // error message string if it is used.
  var error = get_error_msg(fld, name);

  // If the validation fails, add this error message
  // to the running message.
  var is_valid = validateField(fld, rule),
      is_required = Boolean(fld.getAttribute('required')),
      is_null = Boolean(fld.form[name].value.toString() == ''),
      is_undefined = Boolean((typeof fld.form[name].value).toString() == 'undefined');
      
  if (debug) {
    alert ('input_valid for ' + name + ': ' + is_valid.toString() + '\n'
         + 'required: ' + is_required.toString() + '\n'
         + 'null: ' + is_null.toString() + '\n'
         + 'undef: ' + is_undefined.toString() + '\n'
         + 'required & (null | undef): ' + Boolean(is_required && (is_null || is_undefined)).toString());
  }

  if (!is_valid || (is_required && (is_null || is_undefined))) {
    return error;
  }

  return '';
}

function get_error_msg(fld, name) {
  if (!fld || !name) {
    return false;
  }

  if (isRadioOrCheckbox(fld)) {
    fld = fld[0];
  }

  if (!fld || !fld.getAttribute) {
    return false;
  }

  var error = fld.getAttribute('error');

  if (!error) {
    return defaultErrorMessage.replace('\[ELEMENT\]', name);
  } else {
    return error;
  }
}

// display an error message above the field that has the error
function div_error(error, fld, name) {
  var errorDiv,
      errorId = name + 'error',
      errorDiv = document.getElementById(errorId);

  if (errorDiv) {
    clear_children(errorDiv);
  } else {
    errorDiv = document.createElement('div');
    errorDiv.id = errorId;

    var el;
    if (isRadioOrCheckbox(fld)) {
      el = fld[0];
    }

    if (!el) {
      el = fld;
    }

    while (el.parentNode.className
           && el.parentNode.className.match(/\bfloat|input\b/)) {
      el = el.parentNode;
    }

    el.parentNode.insertBefore(errorDiv, el);
  }

  error_class(errorDiv, error);

  if (error && error != '') {
    errorDiv.appendChild(document.createTextNode(error));
  }
}

// clear all childNodes of a given element
function clear_children(el) {
  if (typeof(el) == 'string') {
    el = document.getElementById(el + 'error');
  }

  if (el
      && el.childNodes
      && el.childNodes.length > 0) {
    if (el.childNodes.length <= 10) {
      for (var i = 0; i < el.childNodes.length; i++) {
        el.removeChild(el.childNodes[i]);
      }
    } else {
      // time saving option for elements with a large number of childNodes
      el.innerHTML = '';
    }

    el.className = '';
  }
}

function error_class(el, err) {
  if (el && typeof(el) == 'string') {
    var tmp = document.getElementById(el+'_label');

    if (!tmp) {
      tmp = document.getElementById(el+'label');
    }

    el = tmp;
  }

  if (!el) {
    return;
  }

  var error = (err != '');
  var access = typeof(el.className) != 'undefined';
  var matched = (el.className.search(/\bJSVerror\b/) > -1);

  if (error // error occurred
      && access // can access className
      && !matched) { // doesn't already have the class
    el.className += ' JSVerror';
  } else if (!error // no error occurred
             && access // can access className
             && matched) { // class nees to be removed
    el.className = el.className.replace(/ *\bJSVerror\b/g, '');
  }
}

// Returns true or false based on whether the specified string is found
// in the array.
// This is based on the PHP function of the same name.
function in_array(stringToSearch, arrayToSearch) {
  for (s = 0; s < arrayToSearch.length; s++) {
    thisEntry = arrayToSearch[s];
    thisRE = new RegExp('^'+stringToSearch+'$');
    if (thisEntry && thisEntry.match && thisEntry.match(thisRE)) {
      return true;
      exit;
    }
  }
  return false;
}

// ==============================================================================
