//-----------------------------------------------------------------------
// Module name   : GtformmakerVal
// Author        : Paul Battersby
// Creation Date : 09/11/09
//  NOTE: this requires mootools
//  NOTE: this requires gtformmaker/gtformmakerVal_json.js BEFORE
//        GtformmakerVal is included
//
//  This module contains routines to support form validation.
//
//  NOTE: for this to work, the text that accompanies the form field, MUST be
//        wrapped in a <td> or <span> or <div> or something otherwise it's not
//        possible to alter it's colour (can only set the colour of an elementNode
//        tag, NOT a textNode)
//
//  NOTE: there must be no blank line (perhaps no blank space either) between
//        the <td> of the label and the <span> or <div>
//        AND each label + form field MUST be within their own <tr>
//        This means that you can not have 2 fields in the same row.
//
//        Eventually I'll need to find a way to fix this
//
//
//  The following form might be kept in an external _json.js file so that it can
//  be shared by other classes, even a PHP class. As such, some of the
//  parameters are intended to facilitate the sharing of this structure
//  by other classes
//
//  <SCRIPT LANGUAGE="JavaScript" >
//    var gtformmaker_info =
//    {
//      "normalColour" : "#000000",
//      "errorColour"  : "#ff0000",
//
//      "formCfg" :
//      [
//        {
//          "name"           : "name",
//          "label"          : "* Name:",
//          "verifyType"     : "mostChars",
//          "verifyRequired" : true
//        },
//
//        {
//          "name"           : "phone",
//          "label"          : "* Phone",
//          "verifyType"     : "mostChars",
//          "verifyRequired" : false
//        },
//
//        {
//          "name"           : "password",
//          "label"          : "* Password",
//          "verifyType"     : "mostChars",
//          "verifyRequired" : true
//        },
//
//        {
//          "name"     : "website",
//          "verifyType"   : "mostChars",
//          "verifyRequired" : false,
//          "skipLformval" : true
//        },
//
//        {
//          "name"       : "check_me[]",
//          "label"      : "Check Me",
//          "verifyType" : "checkbox",
//          "minMaxChecked" : "1-2",
//        },
//
//        {
//          "name"       : "check_me[]",
//          "verifyType" : "checkbox",
//          "skipLformval" : true
//        },
//
//        {
//          "name"           : "email",
//          "label"          : "Email",
//          "verifyType"     : "mostChars",
//          "verifyRequired" : true
//        }
//      ]
//    }
//  </SCRIPT>
//
//  Explanation of fields
//  =====================
//
//    label          - text that appears beside or above the form element
//                     It is this label that will change colour on error
//    name           - the "name" field in the form for this form element
//    verifyType     - the type of form verification to be performed
//                     See errorMsg[] for a list of verification types
//    verifyRequired - (optional) if set to true, this indcates a field that is
//                     required (can not be left blank)
//    skipLformval   - indicates that this field is to be ignored by
//                     lformval.js likely because this field is used
//                     by something else that is also sharing the configuration
//                     record
//
//  Special instructions for checkboxes
//  ===================================
//
//  Checkboxes can be part of a group such that more than one checkbox can
//  be checked and the list of checked values can be managed as an array
//  of values by PHP
//
//  To do this, checkboxes need to be given the same name with different
//  indexes. Example : myCheck[]
//
//  For validation purposes, only the first checkbox in the group needs
//  to have verifyRequired = true. The rest MUST have skipLformval = true
//  because they will all be validated at the same time as the first
//  in the group. Checkbox validation is simply to count the number of
//  checkboxes that have been checked and compare it to the configured range
//
//     minMaxChecked - either the minimum number of checkboxes that must
//                     be selected, OR a range like this "2-4" indicating
//                     in this case, that between 2 and 4 check boxes
//                     must be selected
//  USAGE:
/*
   <head>
     <SCRIPT LANGUAGE="JavaScript" type="text/javascript" src="mootools-core.js"></script>
     <SCRIPT LANGUAGE="JavaScript" type="text/javascript" src="gtformmaker/gtformmakerVal_json.js"></script>
     <SCRIPT LANGUAGE="JavaScript" type="text/javascript" src="gtformmaker/gtformmakerVal.js"></script>
     <SCRIPT LANGUAGE="JavaScript" type="text/javascript" src="myformval_json.js"></script>

     <SCRIPT LANGUAGE="JavaScript" type="text/javascript">
       var oFormval = new GtformmakerVal();
     </script>
   </head>

   <!-- NOTE: to include server side validation from gtformmakerVal.php, set the action to ""
        and see usage instructions in gtformmakerVal.php -->
   <body>
    <form name="contactUs" action="formHandler.php" method="POST" onsubmit="return oFormval.validate(gtformmaker_info.formCfg,this)">
      <table>
        <tr>
          <td class="formLabels">HOME</td>
          <td></td>
        </tr>
        <tr>
          <td class="formLabels">Name</td>
          <td><input type="text" size="35" maxlength="40" name="mame"></td>
        </tr>
        <tr>
          <td class="formLabels">Phone</td>
          <td><input type="text" size="35" maxlength="40" name="phone"></td>
        </tr>
        <tr>
          <td class="formLabels">Website</td>
          <td><input type="text" size="35" maxlength="40" name="website"></td>
        </tr>

        <tr>
          <td colspan='2'>Check at least one of the <span id='counters'>Counters</span> below</td>
        </tr>

        <tr>
          <td class="formLabels">One</td>
          <td><input type="text" size="35" maxlength="40" name="check_me[0]" value="1"></td>
        </tr>
        <tr>
          <td class="formLabels">Two</td>
          <td><input type="text" size="35" maxlength="40" name="check_me[1]" value="2"></td>
        </tr>
        <tr>
          <td class="formLabels">Email</td>
          <td><input type="text" size="35" maxlength="40" name="website"></td>
        </tr>
      </table>
      <p align="center"><input type="submit" value="Submit Comments"></p>
    </form>

    <SCRIPT LANGUAGE="JavaScript" >
        // initialize the form for use with the validation structure
        oFormval.initForm("form1",gtformmaker_info.formCfg);
                                           OR
        oFormval.initForm("form1",gtformmaker_info.formCfg,"#777777","#cc0000");
                                           OR
        oFormval.initForm("form1",gtformmaker_info.formCfg,
                                  gtformmaker_info.normalColour,
                                  gtformmaker_info.errorColour);
    </SCRIPT>
   </body>
*/
//  $Log: gtformmakerVal.js $
//  Revision 1.3  2009-10-04 13:42:29-04  Battersby
//  - replaced all mention of Lformval with GtformmakerVal
//
//  Revision 1.2  2009-09-14 11:43:26-04  Battersby
//  - now automatically skips validation for html, rawhtml
//
//  Revision 1.1  2009-09-14 09:52:09-04  Battersby
//  Initial revision
//
//------------------------------------------------------------------------

//---------------------------- INCLUDE FILES -----------------------------

if (!GtformmakerVal) { // ensure this does not get multiply included
var GtformmakerVal = new Class({
  Implements: [Events, Options],

//----------------------------- CONSTANTS --------------------------------


//----------------------------- VARIABLES --------------------------------
  options : {
    lang : "en", // one of {"en","fr"} for English, French

    normalColour : "#000000", // normal text colour
    errorColour  : "#ff0000", // error colour

    noEnter : false, // true = prevent enter from submitting the form

    // function to be called when something is complete
    onComplete : Class.empty
  },

  errorMsg : gtformmakerVal_errorMsg, // validation error messages from json file

  /* this reproduces the Javascript constant names */
  NodeType : {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_FRAGMENT_NODE: 11
  },

  customErrors : [],

  initFormComplete : false, // used to ensure initForm() was called

  // true indicates that the form validation is to be skipped entirely
  disabled : false,

//----------------------------- FUNCTIONS --------------------------------

  //************************************************************************
  // Name   : disable
  //  (boolean) disable - true  = validation will be skipped
  //                      false = validation will NOT be skipped
  //
  // Returns : (nothing)
  //************************************************************************
  disable : function(disable) {
    this.disabled = disable;
  },

  //************************************************************************
  // Name   : splitPhone
  //  This takes a phone number in this format "111-222-3333", breaks it into
  //  3 pieces and returns those pieces in an array
  //
  //  phoneNumber - a phone number in this format "111-222-3333"
  //
  // Returns :
  //  a 3 element array containing the phone number
  //   [0] = 111
  //   [1] = 222
  //   [2] = 3333
  //************************************************************************
  splitPhone : function(phoneNumber) {
    return phoneNumber.split(/[- ]/);
  },

  //************************************************************************
  // Name   : getRadioIndex
  //  This determines the index of the checked radio button
  //
  //  This assumes the radio buttons are created like this:
  //    <input type="checkbox" name="radioGroupName" value="1">
  //    <input type="checkbox" name="radioGroupName" value="2">
  //              ..
  //
  //  (obj) radioGroupObj - pointer to the radio group (ex document.myform.radioGroupName)
  //
  // Returns : (int) the index of the checked radio button or -1 if no button checked
  //************************************************************************
  getRadioIndex : function(radioGroupObj) {
    var i;

    /* loop through all the radio buttons */
    for ( i = 0; i < radioGroupObj.length; i++ )
    {
      /* if we've found the one that is currently checked */
      if ( radioGroupObj[i].checked )
      {
        return i;
      } /* endif */
    } /* end for */

    return -1;
  },

  //************************************************************************
  // Name   : getRadioVal
  //  This determines the value of the checked radio button
  //
  //  This assumes the radio buttons are created like this:
  //    <input type="checkbox" name="radioGroupName" value="1">
  //    <input type="checkbox" name="radioGroupName" value="2">
  //              ..
  //
  //  (obj) radioGroupObj - pointer to the radio group (ex document.myform.radioGroupName)
  //
  // Returns : (string) the value of the checked radio button
  //************************************************************************
  getRadioVal : function(radioGroupObj) {
    var index;

    // get the selected index
    index = this.getRadioIndex(radioGroupObj);

    // return "" if no index selected, otherwise return the value
    return (index == -1) ? "" : radioGroupObj[index].value;
  },

  //************************************************************************
  // Name   : getCheckboxVal
  //  This returns the text of the selected check boxes to the caller
  //
  //  This assumes the checkboxes are created like this:
  //    <input type="checkbox" name="checkGroupName[]" value="1">
  //    <input type="checkbox" name="checkGroupName[]" value="2">
  //              ..
  //
  //  (obj) checkBoxObj - pointer to the checkbox group (ex document.myform.checkboxGroupName)
  //
  // Returns : (array) an array containing the text of each selected checkbox
  //                   The length of the array will be 0 if nothing is checked
  //************************************************************************
  getCheckboxVal : function(checkboxObj) {
    var checkboxText = [];
    var i;
    var textIndex = 0;

    /* loop through all the check boxes */
    for ( i=0; i < checkboxObj.length; i++ ) {

      /* if this one is checked, add it to the array */
      if (checkboxObj[i].checked) {
        checkboxText[textIndex++] = checkboxObj[i].value;
      } /* endif */
    } /* end for */

    return checkboxText;
  },

  //************************************************************************
  // Name   : countCheckBoxes
  //
  //  This counts the number of checkboxes that belong to the same group
  //  and are currently checked
  //
  //  (obj) formObj      - the form object containing the form being validated
  //  (array) validateDesc - see formval_validateOne()
  //
  // Returns : (int) number of checked checkboxes from this group
  //************************************************************************
  countCheckBoxes : function(formObj,validateDesc) {
    var checkGroup = validateDesc.name;
    var count = 0;
    var fieldName;
    var element;

    // remove the brackets and the bracket contents
    checkGroup = checkGroup.replace(/\[.*\]/,"");

    // put one bracket back
    checkGroup = checkGroup + "[";

    // loop through the form looking for members of the checkGroup
    for ( i = 0; i < formObj.elements.length; i++ ) {
      element = formObj.elements[i];
      // if this the right checkbox, count it
      if ( (element.type == "checkbox") &&
          (element.name == validateDesc.name) &&
          (element.checked)) {
        count++;
      } // endif
    } // endfor
    return count;

  },

  //************************************************************************
  // Name   : replaceSingleQuote
  //
  //  This takes all "'" characters in the given string and replaces them
  //  with "`"
  //
  //  "oldString" - any string
  //
  // Returns : (string) the given string with all single quotes replaced with "`"
  //************************************************************************
  replaceSingleQuote : function(oldString) {
    return oldString.replace(/\'/g,"`");
  },

  //************************************************************************
  // Name   : countDigits
  //  This counts the number of digits in the given string excluding characters,
  //  spaces etc.
  //
  //  (string) numberString - a number string whose digits are to be counted
  //
  // Returns : (int) number of digits in "numberString"
  //************************************************************************
  countDigits : function(numberString) {
    var matchesList = [];

    /* count the digits */
    matchesList = numberString.match(/\d/g);

    /* if there are no digits at all */
    if ( matchesList === null ) {
      return 0;

    /* return the length of the array returned by the match() method */
    /* which corresponds to the number of digits in the number */
    } else {
      return matchesListString.length;
    } /* endif */

  },

  //************************************************************************
  // Name   : handleMutEx
  //  If the given form element contains data, this sets all other form elements
  //  in the list to disabled
  //
  //  If the given form element is empty, it sets all other form elements in
  //  the list to enabled
  //
  //  "muExList" - the list of form elements objects (or ids) that are to be considered
  //               mutually exclusive.
  //  "changedElement" - the id of the form element that was just changed either by text being
  //                     added or completely deleted
  //
  // Post   :
  //  if the given form element is now empty, all the form elements in the
  //  list have been enabled.
  //
  //  if the given form element is not empty, all the form elements in the list
  //  (except the given form element) are now disabled
  //
  // Returns: (nothing)
  //************************************************************************
  handleMutEx : function(mutExList,changedElement) {

    // if this form element became blank
    if ($(changedElement).getValue() == "") {
      /* enable the other forms from the list */
      for ( i = 0; i < mutExList.length; i++ ) {
        $(mutExList[i]).disabled = false;
      }; /* end for */

    /* text was entered */
    } else {
      /* disable the other forms elements from the list */
      for ( i = 0; i < mutExList.length; i++ ) {

        /* disable only if not the selected element */
        if ( $(mutExList[i]) != $(changedElement) ) {
          $(mutExList[i]).disabled = true;
        } // endif
      } // end for

    } // endif
  },

  //************************************************************************
  // Name   : setErrorText
  //
  //  This allows the caller to create or alter any of the error messages in
  //  errorMsg. Useful if a different language needs to be used
  //  besides the two provided
  //
  //  (string) lang       - one of {en,fr} to indicate which group of language strings is being
  //                         modified
  //  (string) errorClass - the specific error message that is being modified
  //  (string) newMsg     - the new message string
  //
  // Returns : (nothing)
  //************************************************************************
  setErrorText : function(lang,errorClass,newMsg) {

    // if there is no support for the given language, create it
    if (!$defined(this.errMsg[lang])) {
      this.errMsg[lang] = [];
    } // endif

    // set the new message
    this.errorMsg[lang][errorClass] = newMsg;
  },

  //************************************************************************
  // Name   : _getTextIdNode
  //  This starts from the id of a form input tag object (form.myForm.inputId)
  //  and returns the id of the text that precedes that form element via
  //  recursively calling itself until successful
  //
  //  (element) node - the id of a form input tag object (ex: form.myForm.inputId)
  //  (string)  labelText - the text label that belongs to the form field
  //              (the node containing this text is the one we're trying to find)
  //  (boolean) foundForm - indicates that we've already found the form tag
  //
  // Returns : id of the <td> tag containing the text that precedes the given form element
  //
  // Returns : (nothing)
  //************************************************************************
  _getTextIdNode : function(node,labelText,foundForm) {
    var children;
    var i;
    var nodeText;
    var sibling;

  /*
    for most form items, we have this structure (CASE 1)
    <tr>
      <td>      <-- parent.previousSibling
        some text
      </td>
      <td>      <-- parent
        <input> <-- inputNode
      </td>
    <tr>

    for a text area, we might have this instead (CASE 2)

    <tr>
      <td>      <-- parent.parentNode.previousSibling.lastChild
        some text
      </td>
    </tr>
    <tr>        <-- parent.parentNode
      <td>      <-- parent
        <input> <-- inputNode

    for some forms, we may also have this (CASE 3)
    <tr>
      <td>      <-- parent
        some text <input> <-- inputNode
      </td> /|\
    <tr>     |
            `-- parent.firstChild
  */

    // if we haven't found the form tag yet
    if (!foundForm) {
      node = $(node).getParent("form");
      foundForm = true;
    } // endif

    // for debugging via the firefox console
  /*
    if ( node ) {
      switch (node.nodeName) {
        case "#text" :
          console.log(node.nodeName + ": " + node.nodeValue);
          break;

        case "INPUT" :
          console.log(node.nodeName + ": " + node.id);
          break;
        default :
          console.log(node.nodeName);
      } // end switch

    } else {
      console.log("null nodename");
    } // endif
  */
    // if the node has children
    if ( node.hasChildNodes()) {

      // loop through all the children
      children = node.childNodes;
      for ( i = 0; i < children.length ; i++ ) {
        node = this._getTextIdNode(children[i],labelText,foundForm);

        // if we found our target node
        if ( node ) {
          return node;
        } // endif
      } // end for

    // no children, so it might be the target node
    } else {

      if ( node ) {
        // if this is the text node that we want
        if ((node.nodeName == "#text") && (node.nodeValue.indexOf(labelText) != -1) ) {
          return node.parentNode;

        // this is not a text node, or not the one we want
        } else {
          return null;
        } // endif
      } // endif
    } // endif

    // desired node not yet found
    return null;
  },


  //************************************************************************
  // Name   : customError
  //
  //  This allows the caller to add a custom error to the list of errors
  //  that will be displayed during form validation
  //
  //  "validateStruct" - see formval_validateOne()
  //  "fieldObj"       - see formval_validateOne()
  //  "errorString"    - the custom error
  //
  // Post :
  //  the custom error has been added to the list of errors to be displayed
  //  and the text for the form element has had its colour changed to the error colour
  //  specified in the validateStruct
  //
  // Returns : (nothing)
  //************************************************************************
  customError : function(validateStruct,fieldObj,errorString) {
    // find where this field is within the validate struct
    for ( var structIndex = 0; structIndex < validateStruct.length; structIndex++ ) {

      // if the names match, we found what we're looking for
      if (validateStruct[structIndex]["name"] == fieldObj.attributes.name) {
        break;
      } // endif
    } // end for

    // get the text element matching the given label
    idToChange = this._getTextIdNode(fieldObj,validateStruct[structIndex].label);

    /* get the actual text belonging to this element */
    /* for error reporting */

    /* if this is already a text node, get it's data */
    if ( idToChange.nodeType == NodeType.TEXT_NODE ) {
      text = idToChange.data;
    /* otherwise get the inner html */
    } else {
      text = idToChange.innerHTML;
    } /* endif */

    /* if there was no error */
    if ( errorString == "" ) {
      /* make sure the color of this is normal to indicate no error */
      idToChange.style.color = this.options.normalColour;

    /* there was an error */
    } else {
      /* change the color of given text to indicate an error */
      idToChange.style.color = this.options.errorColour;

      /* often a "*" precedes the text to indicate a required field */
      /* we don't want that in our error message                    */
      text = text.replace(/^\*/,"");

      /* incase there is a <span> or something surrounding the text, remove it */
      text = text.replace(/<[^>]*>/gi,"");

      /* format the error string and add it to the custom error list  */
      errorString = "\"" + text + "\"- " + errorString + "\n";
      this.customErrors = this.customErrors.concat(errorString);

    } /* endif */

  },

  //************************************************************************
  // Name   : formatError
  //  This formats and error message and returns it to the caller
  //
  //  (string) fieldText - the text representing the field about which an error
  //                is being reported. In the example that follows, this would
  //                be "(first name)"
  //
  //  (string) errorId - the id of the error string that is to be reported. This is
  //              used as an index into errorMsg[]
  //
  // Returns: (string) An error message
  //           ex: "- (first name): may only contain letters"
  //************************************************************************
  formatError : function(fieldText,errorId) {
    var errorString = "";

    /* if this is not the error list message */
    if ( errorId != "errorList" ) {
      errorString = '"' + fieldText + '"- ' + this.errorMsg[this.options.lang][errorId] + "\n";

    /* this is the error list message */
    } else {
      errorString = this.errorMsg[this.options.lang][errorId] + "\n\n";
    } /* endif */

    return errorString;
  },

  //************************************************************************
  // Name   : _stripLabel
  //  This takes a label from a form description and does the following for use
  //  in error messages:
  //    - trim leading and trailing spaces
  //    - remove any html tags from the label
  //    - remove leading or trailing "*"
  //
  //  (string) label - label string to be stripped
  //
  // Returns : (string) stripped label
  //************************************************************************
  _stripLabel : function(label) {
    // trim leading/trailing spaces
    label = label.replace(/^\s+|\s+$/g,"");

    /* incase there is a <span> or something surrounding the text, remove it */
    label = label.replace(/<[^>]*>/gi,"");

    /* often a "*" precedes or follows the label to indicate a required field */
    /* we don't want that in our error message                                */
    label = label.replace(/^\*/,"");
    label = label.replace(/\*$/,"");

    return label;
  },

  //************************************************************************
  // Name   : _validateCheckboxes
  //
  //  This ensures that the correct number of checkboxes have been selected
  //
  //  (obj) formObj      - see validate()
  //  (array) validateDesc - see validateOne()
  //
  // Returns :
  //  an error message if the number of checked checkboxes is not correct
  //   OR
  //  an empty string
  //************************************************************************
  _validateCheckboxes : function(formObj,validateDesc) {
    var error = "";

    // checkbox values are give as a comma separated list, count them
    var count = this.countCheckedBoxes(formObj,validateDesc);

    // if a minMax was specified
    var minMax = validateDesc.minMaxChecked;
    if (minMax) {
      // minMax looks like this "1" or "1-2"
      minMax = minMax.split("-");
      label = this._stripLabel(validateDesc.label);

      // if only 1 limit was given, user must select exactly the given
      // number of items
      if (minMax.length == 1) {
        // if the wrong number of checkboxes have been selected
        if (count != minMax[0]) {
          error = '"' + label + '"' + "- you must select " + minMax[0] + " check box";

          // make the error message plural
          if (minMax[0] > 1) {
            error += "es"
          } // endif
          error += "\n";
        } // endif

      // user has a range of choices
      } else {
        // too many or too few selected
        if (count < minMax[0] || count > minMax[1] ) {
          error = '"' + label + '"' + "- you must select between " + minMax[0] + " & " + minMax[1] + " check boxes\n";
        } // endif
      } // endif

    } // endif

    return(error);

  },

  //************************************************************************
  // Name   : _validateRadio
  //  This ensures that the correct number of checkboxes have been selected
  //
  //  (obj) formObj      - see validate()
  //  (obj) validateDesc - see validateOne()
  //
  // Returns :
  //  an error message if the number of checked checkboxes is not correct
  //   OR
  //  an empty string
  //*************************************************************************/
  _validateRadio : function(formObj,validateDesc) {
    var error = "";
    var i;
    var element;

    // if a radio button MUST be selected
    if (validateDesc.verifyRequired) {
      // loop through the form looking for members of the checkGroup
      for ( i = 0; i < formObj.elements.length; i++ ) {
        element = formObj.elements[i];
        // if this the right checkbox, count it
        if ( (element.type == "radio") &&
            (element.name == validateDesc.name) &&
            (element.checked)) {
          return ""; // indicate no error
        } // endif
      } // endfor

      // indicate an error
      label = this._stripLabel(validateDesc.label);
      error = '"' + label + '"' + "- you must make a selection\n";
    } // endif

    return(error);

  },

  //************************************************************************
  // Name   : validateOne
  //
  //    This determines if a single form element contains valid information
  //    If the indicated form element is not correct, the color of the label
  //    accompanying that form element is set to an error color and an error
  //    message is returned to the caller. Otherwise, the form element is set
  //    to a "normal" color and an empty string is returned to the caller
  //
  //  "validateStruct" - see header for format
  //    * See the file formval-test.htm for an example
  //
  //  "fieldObj" - the form element to be validated typically the object
  //               referred to by "this"
  //                          OR
  //               the form being validated in the case of a checkbox
  //
  //  "structIndex" - index into the validateStruct for the field being validated
  //
  // Post   :
  //  the color of the label associated with the form element has either been
  //  set to this.options.errorColour color or this.options.normalColour
  //
  // Returns : (string) error string if an error occurred, an empty string otherwise
  //************************************************************************
  validateOne : function(fieldObj,validateDesc) {
    var error = "";
    var idToChange;
    var verifyType;
    var value;
    var text;

    // if validation has been disabled, return "no error" indication
    if (this.disabled) {
      return "";
    } // endif

    // get the node whose text is to be changed to report an error
    idToChange = this._getTextIdNode(fieldObj,validateDesc.label);

    if ( idToChange === null ) {
      alert("formval_validateOne failed to find 'label':'" + validateDesc.label) + "'";
      return false;
    } // endif

    /* if we are validating a checkbox */
    if (validateDesc.formType == "checkBox") {
      error = this._validateCheckboxes(fieldObj,validateDesc);

    // if we are validating a radio button group
    } else if (validateDesc.formType == "radio") {
      error = this._validateRadio(fieldObj,validateDesc);

    /* we are not validating a checkbox */
    } else {

      // file defaults to allowing all characters
      if (validateDesc.formType == "file") {
        verifyType = "allChars";

      // not a file
      } else {
        verifyType = validateDesc.verifyType;

        // if this field is not to be verified
        if ( !verifyType) {
          return "";
        } // endif
      } // endif

      value = fieldObj.value;

      /* get the actual text belonging to this element */
      /* for error reporting */
      text = this._stripLabel(validateDesc.label);

      /* if this is a required field */
      if (validateDesc.verifyRequired) {

        /* if this field is left blank */
        if ((value.length == 0) || (!value.match(/[^ ]/))) {
          if ( verifyType == "customBlank" ) {
            error = this.formatError(text,"customBlank");
          } else {
            error = this.formatError(text,"noBlank");
          } // endif

          /* change the color of given text to indicate an error */
          idToChange.style.color = this.options.errorColour;
          return error;
        } /* endif */

      /* this is not a required field */
      } else if (value.length == 0) {
        /* restore the normal color incase a bad field has been erased */
        /* from a not required field */
        idToChange.style.color = this.options.normalColour;
        return "";
      } // endif

      switch (verifyType.toLowerCase()) {
        case "allchars":
          break;

        case "lettersonly":
          if ( value.match(/[^a-zA-Z ]/)) {
            error = this.formatError(text,"lettersOnly");
          } /* endif */
          break;

        case "lettersnumbers" :
          if ( value.match(/[^a-zA-Z \-\.0-9]/)) {
            error = this.formatError(text,"lettersNumbers");
          } /* endif */
          break;

        case "mostchars" :
          if ( value.match(/[^a-zA-Z 0-9\~\`\!\@\#\$\%\^\&\*\(\)\_\-\+\=\|\\\{\}\[\]\:\;\'\?\/\>\<\,\.]/) ) {
            error = this.formatError(text,"mostChars");
          } /* endif */
          break;

        case "numbers" :
          if ( value.match(/[^0-9\-\.]/)) {
            error = this.formatError(text,"numbers");
          }; /* endif */
          break;

        case "postalcode" :
          if (!value.match(/^[a-zA-Z]\d[a-zA-Z]\s*\d[a-zA-z]\d$/)) {
            error = this.formatError(text,"postalCode");
          }; /* endif */
          break;

        case "postalzip" :
          if (!value.match(/^[a-zA-Z]\d[a-zA-Z]\s*\d[a-zA-z]\d$/)
          && !value.match(/^[0-9]{5}$/)
          ) {
            /* NOTE: there is now a 9 digit US zip code not handled by this */

            error = this.formatError(text,"postalZip");
          }; /* endif */
          break;

        case "postalgen" :
          // if we match anything other than a valid postal code character
          if ( value.match(/[^a-zA-Z 0-9\-]/) ) {
            error = this.formatError(text,"postalGen");
          }; /* endif */
          break;

        case "phone" :
          if ( !value.match(/^(1[- ])?(\d{3}[- ])?\d{3}[- ]\d{4}(\s*([xX ]|(ext)|(EXT))?\s*\d+)?$/) ) {
            error = this.formatError(text,"phone");
          }; /* endif */
          break;

        case "phonegen" :
          // if we match anything other than a valid phone digit (with an option a extension)
          if ( value.match(/[^ 0-9\-\(\)xX]/) ) {
            error = this.formatError(text,"mostChars");
          }; /* endif */
          break;

        case "internationalphone" :
          /* if the phone number contains invalid characters */
          /* (may contain an extension) */
          if ( !value.match(/^\+?[0-9 ()-\~]+[0-9](\s*([xX ]|(ext)|(EXT))\s*\d+)?$/) ) {
            error = this.formatError(text,"internationalPhone");
          } else {

            /* if there are too few or too many digits */
            numDigits = this.countDigits(value);
            if ( numDigits < 10) {
              error = this.formatError(text,"internationalPhoneDigits");
            }; /* endif */
          }; /* endif */
          break;

        case "phonestrictareacode" :
          /* if the phone number is not in this format */
          /* 1-111-222-3333 */
          /* or 1 111 222 3333 */
          /* or 111-222-3333 */
          /* or 111 222 3333 */
          /* or 111 222 3333 x123*/
          /* or 111 222 3333 ext 123 etc */
          if ( !value.match(/^(1[- ])?\d{3}[- ]\d{3}[- ]\d{4}(\s*([xX ]|(ext)|(EXT))?\s*\d+)?$/) ) {
            error = this.formatError(text,"phoneStrictAreaCode");
          }; /* endif */
          break;

        case "email":
          /* if basic email validation fails */
          if ( !value.match(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/)) {
            error = this.formatError(text,"email");
          }; /* endif */
          break;

        case "url":
          /* if basic url validation fails */
          if ( !value.match(/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)) {
            error = this.formatError(text,"url");
          }; /* endif */
          break;

        case "currency":
          if ( !value.match(/\$?[0-9-\.]/)) {
            error = this.formatError(text,"currency");
          }; /* endif */
          break;

        case "currencygt0":
          if ( !value.match(/\$?[0-9-\.]/)) {
            error = this.formatError(text,"currency");

          } else if (value <= 0){
            error = this.formatError(text,"currencyGt0");
          }; /* endif */
          break;

        case "dateyyyymmdd":
          /* allowable formats yyyymmdd yyyy.mm.dd yyyy-mm-dd yyyy/mm/dd yyyy\mm\dd */
          if ( !value.match(/^[0-9]{4}[.-\/\\]?[0-9]{2}[.-\/\\]?[0-9]{2}/)) {
            error = this.formatError(text,"dateyyyymmdd");
          }; /* endif */
          break;

        case "datetext":
          /* allowable formats "Jan 1 2004" "Jan 01 2004" "Jan 1, 2004" "Jan 01, 2004" */
          if ( !value.match(/^[a-zA-Z]{3}[, ]?[0-9]{1,2}[ ,]?[0-9]{4}/)) {
            error = this.formatError(text,"dateText");
          }; /* endif */
          break;

        case "password" :
          var minLength = validateDesc.minLength;

          // pwd too short
          if (value.length < minLength) {
            error = this.formatError(text,"passwordLength");
          } // endif
          break;

        case "customblank":
          // this simply permits a custom message to be displayed when a field
          // is left blank
          break;

        default :
          error = "formval_validateOne: " + verifyType + " is an unknown validation type";

      } // end switch
    } // endif

    /* if there was no error */
    if ( error == "" ) {
      /* make sure the color of this is normal to indicate no error */
      idToChange.style.color = this.options.normalColour;

    /* there was an error */
    } else {
      /* change the color of given text to indicate an error */
      idToChange.style.color = this.options.errorColour;
    } /* endif */

    return error;
  },

  //************************************************************************
  // Name   : validate
  //
  //  Using the same structure described in formval_validateOne, this
  //  calls formval_validateOne() for every field in the "fields" array
  //  and builds a list of errors which is then placed in an alert()
  //  message
  //
  //  "validateStruct" - see formval_validateOne()
  //  "formObj" - the form object to be validated (ex. document.forms.myForm)
  //
  // Post   :
  //  if formval_validateOne returned any error messages, they have been formatted
  //  and placed in an alert() box
  //
  // Returns : (boolean) false - validation failed (errors were reported)
  //                     true  - validation succeeded (no errors reported)
  //************************************************************************
  validate : function(validateStruct,formObj) {
    var i;
    var errors = new String();
    var id;
    var idName; /* index into the validateStruct for the field being validated */
    var name;

    // if validation is to be skipped, then skip it
    if (this.disabled) {
      return true;
    } // endif

    // ensure form was properly initialized
    if (!this.initFormComplete) {
      alert("GtformmakerVal::validate : GtformmakerVal::initForm() must be called before GtformmakerVal::validate()")
      return false;
    } // endif

    if (!validateStruct) {
      alert("GtformmakerVal::validate : validateStruct is unknown");
      return false;
    } // endif

    if (!formObj) {
      alert("GtformmakerVal::validate : formObj is unknown");
      return false;
    } // endif

    /* loop through the list of fields to be validated */
    for ( i = 0; i < validateStruct.length; i++) {

      /* if this field is to be skipped, then move on */
      if (validateStruct[i].verifySkip ||
          validateStruct[i].skipLformval ||
          (validateStruct[i].formType == "submit") ||
          (validateStruct[i].formType == "html") ||
          (validateStruct[i].formType == "rawhtml")
          ) {
        continue;
      }

      if ((validateStruct[i].formType == "checkBox") || (validateStruct[i].formType == "radio")) {
        // set the field obj to the form object instead
        fieldObj = formObj;
      } else {
        // get the form field
        fieldObj = formObj[validateStruct[i].name];
      } // endif

      errors += this.validateOne(fieldObj,validateStruct[i]);

    }; /* end for */

    for (i = 0; i< this.customErrors.length; i++) {
      errors += this.customErrors[i];
    } /* end for */

    /* if at least one error was detected, alert the user */
    if (errors.length > 0) {
      alert(this.formatError("","errorList") + errors);
    }; /* endif */

    if (errors.length > 0) {
      /* reset the custom errors */
      this.customErrors = this.customErrors.splice(0,0);

      return false;
    } else {
      return true;
    };
  },

  //************************************************************************
  // Name   : initForm
  //
  //  This prepares the form for use with the FORMVAL routines. It walks through
  //  fields belonging to the validate struct with the given structName and
  //  adds an on change handler
  //
  //  formName       - the value of the "name=" field of the form to be initialized
  //  validateStruct - see formval_validateOne()
  //
  // Post :
  //  the form fields found in the structure referred to validateStructName
  //  have had the "onchange" handler modified to call LFORMVAL_vaidateOne()
  //  upon any change
  //
  // Returns : (nothing)
  //************************************************************************
  initForm : function(formName,validateStruct) {
    var i;
    var idName;
    var eventString;
    var structEntry;

    /* loop though every field in the validate structure */
    for ( i = 0; i < validateStruct.length; i++ ) {

      /* if this field is to be skipped, then move on */
      if (validateStruct[i].skipLformval) {
        continue;
      }

      idName = validateStruct[i].name;

      // if there is no id name, skip it
      if ( !idName ) {
        continue;
      } // endif

      // can't perform live validation of checkboxes and radio (only when form is submitted)
      // so skip the onchange initialization for checkbox fields
      if ( (validateStruct[i].verifyType != "checkbox") &&
          (validateStruct[i].verifyType != "radio")) {

        // add a change event to the form element
        $(document.forms[formName][idName]).addEvent("change",function(event,valStructEntry) {
          this.validateOne(event.target,valStructEntry);
        }.bindWithEvent(this,validateStruct[i]));

        // if the enter key is not permitted to submit the form
        if (this.options.noEnter) {
          var element = $(document.forms[formName][idName]);
          var tagName = element.get("tag");
          var inputType = element.type;

          // if this is a text box
          if (tagName == "input" && inputType == "text") {
            element.addEvent("keypress",function(event) {
              return !(event.key == "enter");
            });
          } // endif

        } // endif

      } // endif
    } // end for

    this.initFormComplete = true;
  },

  //************************************************************************
  // Name   : initialize (constructor)
  //
  //  (object) options - (optional) configuration options. See options declaration
  //                     above for the available options
  //
  // Returns : (nothing)
  //*************************************************************************
  initialize : function(options) {
    this.setOptions(options);
  }

});
} // endif
