/**
 * Forms.js
 * Form validation. By including this script, the validation code attaches itself
 * automatically to all forms on a page and executes on form submission. This
 * script is fairly dependent on the form HTML and the following assumptions are
 * made:
 * - The form is broken into "fields" which can contain multiple form elements.
 *   These fields are contained in <li> inside <ol> inside <fieldset>s and no form
 *   elements appear outside these structures.
 * - The fields are labelled to indicate which are required and / or who's values
 *   must follow particular patterns. These requirements are indicated by class 
 *   names on the field <li> tags. The class names can be found below as values of
 *   the FormObj.tests object (and can be modified if needed).
 * - The name of the field is the first text node within the first <label> element
 *   in each field <li>.
 * - If the form has any required fields, there is a hidden <div> within the <form>
 *   that contains the text of a message that will appear if any required fields are
 *   empty.  The class of this <div> must be the concatenation of 
 *   FormObj.properties.ErrorPrefix and FormObj.tests.Required.
 *   The text within this <div> will be followed by a list of the labels of the 
 *   fields that are required but are empty.
 * - If any fields have any pattern requirements, there is a hidden <div> within the
 *   field <li> that contains the text of a message that will appear if the field's
 *   form elements do not match the pattern. The class of this <div> must be the
 *   concatenation of FormObj.properties.ErrorPrefix and the appropriate 
 *   FormObj.tests value.  The test within this div will appear by itself.
 * - For any fields that trigger failure, this script will apply a class to the 
 *   field <li> with the value FormObj.properties.ErrorPrefix and the appropriate 
 *   FormObj.tests value. The class FormObj.properties.GenericError is also applied to
 *   all problem fields. By setting styles to these classes in your css, you can make
 *   the problem fields highlight is some way.
 * - Any further validation of the form values beyond what this script is capable
 *   of will be stored in a method of the form with the name "doExtraValidation"
 *   which is called after this script's validation and which must return true if
 *   the form passes the extra validation or false if it fails:
 *   
 *   // in showError() line, this.elements['bar'].parentNode is assumed to be a ref
 *   // to the field <li> of the 'bar' element and 'error-myerror' is a class to 
 *   // apply to this field to show that the error occured
 *   myFormObj.doExtraValidation = function() {
 *     var errorMessage = '';
 *     if (this.elements['foo'].checked && this.elements['bar'].value == '') {
 *       this.elements['bar'].parentNode.showError( { name: 'myerror' } );
 *       errorMessage += 'If foo is checked, then bar must be filled in.\n';
 *     }
 *     if (errorMessage) {
 *       this.showErrorMessage(errorMessage);
 *       return false;
 *     }
 *     else {
 *       return true;
 *     }
 *   }
 *   
 * - Include Dialog.js to make the error messages appear in a customizable dialog
 *   box instead of an alert.
 */
 
var FormObj = {

  // class names that can be applied to form elements to specify required fields or value requirements;
  // new tests can be added to this list by adding objects to FormObj.tests with name, classAddition, and test properties
  // and they will automagically be applied
  tests: {
    
    // For text fields, a non-whitespace value must be entered
    // For checkboxes, checkbox must be checked
    // For selects, an option must be selected
    // For radio buttons, one button in a group must be selected (one, some, or all buttons in a group can have the class)
    Required: {
      name: 'required',
      classAddition: '',
      test: "value === ''"
    },
    
    // Value must be in the form of an email address (ie foo-bar@foo.bar)
    PatternEmail: {
      name: 'pattern-email',
      classAddition: '',
      test: "value != '' && value.search(/^\\w+((\\.|-)\\w+)*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+$/) == -1"
    },
    
    // Value must be in the form of a US zipcode (ie. 12345 or 12345-1234)
    PatternZipCode: {
      name: 'pattern-zipcode',
      classAddition: '',
      test: "value != '' && value.search(/^\\d{5}(-\\d{4})?$/) == -1"
    },
    
    // Value must be in the form of a CDN postal code (ie a1b-2c3 or a1b2c3) regardless of capitalization
    PatternPostalCode: {
      name: 'pattern-postalcode',
      classAddition: '',
      test: "value != '' && value.search(/^[a-zA-Z]\\d[a-zA-Z](-| )?\\d[a-zA-Z]\\d$/) == -1"
    },
    
    //Value must be in the form of a US zipcode or a CDN postal code
    PatternZipOrPostal: {
      name: 'pattern-postalzipcode',
      classAddition: '',
      test: "value != '' && value.search(/^\\d{5}(-\\d{4})?$/) == -1 && value.search(/^[a-zA-Z]\\d[a-zA-Z](-| )?\\d[a-zA-Z]\\d$/) == -1"
    },
    
    // Value must be in the form yyyymmdd (all numbers)
    PatternDate: {
      name: 'pattern-date',
      classAddition: '',
      test: "value != '' && (value.length != 8 || value.search(/^\\d+$/) == -1 || parseInt(value.substring(4,6)) > 12 || parseInt(value.substring(6,8)) > 31)"
    },
    
    // Value must contain only numbers (spaces, hyphens, and + also allowed so this can be applied to international phone numbers)
    PatternNumbers: {
      name: 'pattern-numbers',
      classAddition: '',
      test: "value != '' && value.search(/^(\\d|-|\\+| )+$/) == -1"
    },
    
    // PatternFreeform requires a RegExp object to be applied to the 'pattern' property of the field
    // The value of the field will be compared to this RegExp
    // ie. to limit the value of the foo field to between 4 and 12 characters, use
    // <script> document.getElementById('foo').pattern = new RegExp('^.{4,12}$'); </script>
    PatternFreeform: {
      name: 'pattern-freeform',
      classAddition: '',
      test: "value != '' && field.pattern && value.search(field.pattern)"
    },
    
    // Match requires the name (not the id) of the matching field appended to the class name
    // ie. 'match-password1' means that the current field's value must equal the value of the password1 field
    Match: {
      name: 'match',
      classAddition: '-\\S+',
      test: "field.form.elements[field.className.match(new RegExp(FormObj.tests.Match.name + '-(\\\\S+)'))[1]] && value != FormObj.Field.Validator.cleanTextValue(field.form.elements[field.className.match(new RegExp(FormObj.tests.Match.name + '-(\\\\S+)'))[1]].value)"
    }
  },
  
  properties: {

    // determines if all the error messages are displayed in a popup (true) or just a generic houston-we-have-a-problem message is shown (false);
    // for the generic message, the innerHTML of the first child <div> of the <form> with a class of FormObj.properties.ErrorGeneric is used
    showAllErrorMessagesInPopup: true,
    
    // prefix for error messages classes; a full error class name would be this prefix + a class name from above (ie 'error-pattern-email')
    ErrorPrefix: 'error-',
    
    // instead of specifying css for all the possible error message classes, you can just apply your css to the ErrorGeneric class
    // which is always applied to any field that causes an error along with the specific error class
    ErrorGeneric: 'error-generic',
    
    // in the absence of any child <div> of the <form> with a class of FormObj.properties.ErrorGeneric,
    // the following will appear as the error message when showAllErrorMessagesInPopup is false
    ErrorMessageGeneric: 'There is a problem with the form data. Please fix the highlighted fields and resubmit the form.',
    
    // in the absence of any child <div> of the <form> with a class of (FormObj.properties.Error + FormObj.properties.Required),
    // the following will appear as the error message when required fields are missing and will be followed by a list of the missing required fields
    ErrorMessageRequired: 'Missing required fields:',
    
    // in the absence of any child <div> of a field <li> with a class of (FormObj.properties.Error + any FormObj.properties error name),
    // the following will appear as the error message when a field fails any error test other than a required test
    // and will be followed by the name of the field
    ErrorMessageOther: ' is not in the correct format.'
  },

  /**
   * Register any event listeners, register any child elements with appropriate
   * classes and do any other object initialization.  In this case, call
   * validation on form submit and register FormObj.Field methods on form fields
   * of form.
   */
  initialize: function() {
    
    // event listener registration
    this.behavior = function() {
      addEvent(this, 'submit', FormObj.doBasicValidation);
    };
    this.behavior();
    
    // register child classes
    var fields = this.getFields()
    registerMethods(fields, FormObj.Field);
    
    // add reference from all fields back to this form
    for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
      fields[fieldIndex].form = this;
    }
  },

  /**
   * Called by an event listener attached to form submit. This function has
   * been named so that the event listener can be unattached from the event if
   * needed. Because it is being attached using addEvent(), avoid using the 'this'
   * keyword because it won't work in IE - use e.target instead.
   */
  doBasicValidation: function(e) {
    e = window.event ? fixIEEvent(window.event) : e;
    var form = e.target;
    var fields = form.getFields();
    
    // clean up from past validations: clear all error classes on all fields
    for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
      fields[fieldIndex].clearAllErrors();
    }
    
    // get any errors with form data
    var errors = form.getErrors();
    if (errors.length) {
      var errorMessage = '';
      
      if (FormObj.properties.showAllErrorMessagesInPopup) {
        
        // put missing required fields before any other errors
        errors.sort(function(a,b) { return (a.failedTest == FormObj.tests.Required && b.failedTest != FormObj.tests.Required) ? -1 : 1; });
  
        // report missing required fields before any other errors
        if (errors[0].failedTest == FormObj.tests.Required) {
          
          // get missing-required-fields error message
          var errorMessageDivs = DOMExtender.DOMElementsByClassName(FormObj.properties.ErrorPrefix + FormObj.tests.Required.name, form, 'div');
          errorMessage += ((errorMessageDivs.length) ? errorMessageDivs[0].innerHTML : FormObj.properties.ErrorMessageRequired) + '\n';
          
          // for each missing-required-fields error, turn on error class on missing fields and append field name to the error message
          for (var errorIndex = 0; errorIndex < errors.length; errorIndex++) {
            var error = errors[errorIndex];
            if (error.failedTest == FormObj.tests.Required) {
              error.field.showError(FormObj.tests.Required);
              errorMessage += error.field.getLabel() + '\n';
            }
          }
          
          // separate error messages with a blank line
          errorMessage += '\n';
        }
        
        // for all other errors, turn on error class on missing fields and append field-specific error message to the error message
        for (var errorIndex = 0; errorIndex < errors.length; errorIndex++) {
          var error = errors[errorIndex];
          if (error.failedTest != FormObj.tests.Required) {
            error.field.showError(error.failedTest);
            var errorMessageDivs = DOMExtender.DOMElementsByClassName(FormObj.properties.ErrorPrefix + error.failedTest.name, error.field, 'div');
            errorMessage += ((errorMessageDivs.length) ? errorMessageDivs[0].firstChild.nodeValue : error.field.getLabel() + FormObj.properties.ErrorMessageOther) + '\n\n';
          }
        }
      }
      else {
        var errorMessageDivs = DOMExtender.DOMElementsByClassName(FormObj.properties.ErrorGeneric, form, 'div');
        errorMessage += ((errorMessageDivs.length) ? errorMessageDivs[0].innerHTML : FormObj.properties.ErrorMessageGeneric) + '\n';
      }
      
      // show error message
      Dialog.show(errorMessage, [ { label: 'Ok', onclick: Dialog.hide } ]);
      
      // don't submit the form
      e.stopPropagation();
      e.preventDefault();
    }
    
    // if there is any extra validation method attached to the form, do those as well
    else if (FormObj.doExtraValidation && FormObj.doExtraValidation() == false) {
      e.stopPropagation();
      e.preventDefault();
    }
  },
  
  /**
   * Get a list of all form fields. This is different from form elements in
   * that fields include the element, label, hidden error messages, etc. As per
   * the current XSL, fields are contained in <li> inside <ol> inside <fieldset>s.
   * @Returns An array of fields that make up the form.
   */
  getFields: function() {
    var lis = this.getElementsByTagName('li');
    var fields = new Array();
    
    // field <li> elements have <fieldset> elements as grandparents
    for (var liIndex = 0; liIndex < lis.length; liIndex++) {
      li = lis.item(liIndex);
      if (li.parentNode.parentNode.nodeName.toLowerCase() == 'fieldset') {
        fields.push(li);
      }
    }
    return fields;
  },

  /**
   * Returns an array of all elements in a form that failed validation and the
   * name of the test that failed for each.
   * To apply a test to a form element apply a special class name to the form
   * element tag.  See FormObj.tests for class names and meanings.
   * @Returns An array of objects of the form { field, failedTest }.
   *          field = reference to the form field.
   *          failedTest = the FormObj.tests name of the test that failed.
   *          Or null if all tests validated.
   */   
  getErrors: function() {
    var errors = [];
    
    // see FormObj.Field.Validator.inputRadio for this variable
    this.radioGroups = [];
    
    // loop thru fields and test for errors on each
    // storing any error messages in an array along with the field reference
    var fields = this.getFields();
    for (var i = 0; i < fields.length; i++) {
      var error = fields[i].getErrors();
      if (isDefined(error)) {
        errors.push({ field: fields[i], failedTest: error });
      }
    }
    return errors;
  }
};

FormObj.Field = {

  /**
   * Cache to store array of elements after first call to getElements().
   */
  elements: null,

  /**
   * Reference to form containing the field.
   */
  form: null,
  
  /**
   * Register any event listeners, register any child elements with appropriate
   * classes and do any other object initialization.
   */
  initialize: function() { /* do nothing */ },

  /**
   * Returns an array of all the form elements that make up the field.
   * @Returns An array of all elements in the field.
   */
  getElements: function() {
    if (this.elements) {
      return this.elements;
    }
    var inputs = $A(this.getElementsByTagName('input'));
    var selects = $A(this.getElementsByTagName('select'));
    var textareas = $A(this.getElementsByTagName('textarea'));
    return this.elements = (new Array()).concat(inputs, selects, textareas);
  },

  /**
   * Returns the label of the field.
   * @Returns The first text node of the first label element in the field
   */
  getLabel: function() {
    return this.getElementsByTagName('label')[0].firstChild.nodeValue;
  },
  
  /**
   * Applies an error class to the field.
   * @Param errorType One of the FormObj.tests error types (optional).
   */
  showError: function(errorType) {
    if (errorType) {
      Element.addClassName(this, FormObj.properties.ErrorPrefix + errorType.name);
    }
    Element.addClassName(this, FormObj.properties.ErrorGeneric);
  },

  /**
   * Removes an error class from the field.
   * @Param errorType One of the FormObj.tests error types.
   */
  clearError: function(errorType) {
    Element.removeClassName(this, FormObj.properties.ErrorPrefix + errorType.name);
    Element.removeClassName(this, FormObj.properties.ErrorGeneric);
  },

  /**
   * Removes all error classes (FormObj.properties.ErrorGeneric and all classes
   * starting with FormObj.properties.ErrorPrefix) from the field.
   */
  clearAllErrors: function() {
    var classNames = this.className.split(' ');
    for (var i = classNames.length - 1; i >= 0; i--) {
      if (classNames[i] == FormObj.properties.ErrorGeneric || classNames[i].indexOf(FormObj.properties.ErrorPrefix) == 0) {
        Element.removeClassName(this, classNames[i]);
      }
    }
  },

  /**
   * Returns the name of the first validation test that fails on a form field.
   * All form elements in a field must pass the test for the field to pass.
   * See FormObj.getInvalidFields() for more details.
   * @Returns The FormObj.tests name of the test that failed.
   *          Or null if all tests validated.
   */   
  getErrors: function() {
    var elements = this.getElements();
    for (var elementIndex = 0; elementIndex < elements.length; elementIndex++) {
      var element = elements[elementIndex];
      var error = FormObj.Field.Validator[element.tagName.toLowerCase()](this, element);
      if (error) {
        return error;
      }
    }
  }
};

/**
 * Methods to validate form elements. All FormObj.tests validations are applied
 * to <input type=text>, <input type="password"> and <textarea> elements. 
 * All other form elements only have FormObj.tests.Required validation applied.
 * Called thru FormObj.Field.getErrors(). Not intended to be called directly.
 */
FormObj.Field.Validator = {

  input: function(field, element) {
    switch (element.type.toLowerCase()) {
      case 'password':
      case 'text':
        return FormObj.Field.Validator.textarea(field, element);
      case 'checkbox':  
        return FormObj.Field.Validator.inputCheckbox(field, element);
      case 'radio':
        return FormObj.Field.Validator.inputRadio(field, element);
    }
  },

  inputCheckbox: function(field, element) {
    /* JJ-2007.7.18-required field bug fixes */
    if (FormObj.Field.Validator.requiresTest(field, FormObj.tests.Required.name)) {
      for (var checkboxIndex=element.form[element.name].length-1; checkboxIndex>=0; checkboxIndex--) {
        var checkbox = element.form[element.name][checkboxIndex];
        if (checkbox.checked) {
          return null;
        }
      }
    /* -- */
      return FormObj.tests.Required;
    }
  },

  /**
   * Note: This function tests a whole radio button group (all radio buttons with 
   * the same name as the radio button passed into the function) instead of just a
   * single radio button.  The radioGroups associative array contains flags that
   * prevents radio button sets from being tested multiple times.
   */
  inputRadio: function(field, element) {
    if (FormObj.Field.Validator.requiresTest(field, FormObj.tests.Required.name) && 
        (!element.form.radioGroups || !element.form.radioGroups[element.name]) ) {
      element.form.radioGroups[element.name] = true;
      /* JJ-2007.7.18-required field bug fixes */
      for (var radioIndex=element.form[element.name].length-1; radioIndex>=0; radioIndex--) {
        var radio = element.form[element.name][radioIndex];
      /*  --  */
        if (radio.checked) {
          return null;
        }
      }
      return FormObj.tests.Required;
    }
  },

  textarea: function(field, element) {
    
    // trim any leading or trailing whitespace from value in element
    var value = FormObj.Field.Validator.cleanTextValue(element.value);
    
    // iterate thru all FormObj.tests items and apply them all
    for (testName in FormObj.tests) {
      var test = FormObj.tests[testName];
      if (FormObj.Field.Validator.requiresTest(field, test.name + test.classAddition) && eval(test.test)) {
        return test;
      }
    }
  },
  
  select: function(field, element) {
    if (FormObj.Field.Validator.requiresTest(field, FormObj.tests.Required.name) && element.value == '') {
      return FormObj.tests.Required;
    }
  },
  
  requiresTest: function(field, testName) {
    return Element.hasClassName(field, testName);
  },
  
  cleanTextValue: function(value) {
    return value.replace(/^\s+/, '').replace(/\s+$/, '');
  }
};


/**********************************************************************
   Functions from DOMExtender.js needed for this script
 *********************************************************************/

if (!isDefined(DOMExtender)) {
  var DOMExtender = new Object();
  DOMExtender.DOMElementsByClassName = function(clsName, parentEle, tagName, fn){
    var found = new Array();
    var re = new RegExp('\\b'+clsName+'\\b', 'i');
    var list = parentEle.getElementsByTagName(tagName);
    for (var i = 0; i < list.length; ++i) {
      if (list[i].className.search(re) != -1) {
        found[found.length] = list[i];
        if (fn) fn(list[i]);
      }
    }
    return found;
  }
}

/**********************************************************************
   Functions from Prototype needed for this script
 *********************************************************************/
/* 
if (!isDefined(window.Prototype)) {
  Function.prototype.bind = function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  }

  var $A = Array.from = function(iterable) {
    if (!iterable) return [];
    if (iterable.toArray) {
      return iterable.toArray();
    } else {
      var results = [];
      for (var i = 0, length = iterable.length; i < length; i++)
        results.push(iterable[i]);
      return results;
    }
  }
  
  if (!window.Element) {
    Element = null;
    var Element = new Object();
  }
  
  Element.hasClassName = function(element, className) {
    var elementClassName = element.className;
    if (elementClassName.length == 0) return false;
    if (elementClassName == className ||
        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
      return true;
    return false;
  };
  
  Element.addClassName = function(element, classNameToAdd) {
    if (Element.hasClassName(element, classNameToAdd)) {
      return;
    }
    element.className = element.className.split(' ').concat(classNameToAdd).join(' ');
  };
  
  Element.removeClassName = function(element, classNameToRemove) {
    if (!Element.hasClassName(element, classNameToRemove)) {
      return;
    }
    element.className = element.className.replace(new RegExp('\\s*' + classNameToRemove + '\\s*', 'g'), '');
  };
} */


/**********************************************************************
   Functions from Dialog needed for this script
 *********************************************************************/

if (!isDefined(Dialog)) {
  var Dialog = {
    show: function(message) {
      alert(message);
    }
  }
}


/**********************************************************************
   Utility functions - generic so should be moved to somewhere central
 *********************************************************************/

function isDefined(property) {
  return (typeof property != 'undefined');
}


/**********************************************************************
   Basic Event Registration
   has issues - see http://www.quirksmode.org/blog/archives/2005/08/addevent_consid.html
 *********************************************************************/

function addEvent(object, eventName, functionRef) {
  if (isDefined(window.addEventListener)) {
    object.addEventListener(eventName, functionRef, false);
  }
  else if (isDefined(window.attachEvent)) {
    object.attachEvent('on' + eventName, functionRef);
  }
}


/**********************************************************************
   Register methods defined on a class to a list of elements
 *********************************************************************/

registerMethods = function(elements, classRef) {
  for (var elementIndex = 0; elementIndex < elements.length; elementIndex++) {
    element = elements[elementIndex]
    for (property in classRef) {
      if (classRef[property] instanceof Function) {
        element[property] = classRef[property].bind(element);
      }
    }
    
    // call initialize method to do any object initialization
    if (element.initialize) {
      element.initialize();
    }
  }
};


/**********************************************************************
   Fix IE events
 *********************************************************************/

fixIEEvent = function(e) {
  e.stopPropagation = fixIEEvent.stopPropagation;
  e.preventDefault = fixIEEvent.preventDefault;
  e.relatedTarget = e.fromElement || e.toElement;
  e.pageX = event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
  e.pageY = event.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
  e.target = e.srcElement;
  return e;
}
   
fixIEEvent.stopPropagation = function() {
  this.cancelBubble = true;
}
   
fixIEEvent.preventDefault = function() {
  this.returnValue = false;
}



// attach Form methods to all forms when document finishes loading
addEvent(window, 'load', function() { registerMethods(document.getElementsByTagName('form'), FormObj); } );

