//-----------------------------------------------------------------------
// Module name   : LFORMVAL.js
// Author        : Paul Battersby
// Creation Date : Nov 13/04
// Description   :
//  This module contains routines to support form validation.
//
//  Example Usage:
//
//  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)
//
//  <SCRIPT LANGUAGE="JavaScript" type="text/javascript" src="lformval.js"></script>
//  <SCRIPT LANGUAGE="JavaScript" >
//    var m_validateStruct = {
//      normalColor : "black",
//      errorColor : "red",
//      fields : {
//        name        : ["lettersOnly",  "required"],
//        email       : ["email",        "required"],
//        password    : ["lettersOnly",  "skip"],
//        tel         : ["numbers"                 ],
//        fax         : ["numbers"                 ],
//
//        workName        : ["lettersOnly",  "required", "work"],
//        workEmail       : ["email",        "required", "work"],
//        workTel         : ["numbers"                 , "work"],
//        workFax         : ["numbers"                 , "work"]
//      }
//    };
//  </SCRIPT>
//
// NOTE: it's also possible to verify that a min and max number of checkboxes
//       has been selected, be sure to document and example
//
// The last column is an optional string to append to the beginning of the
//  field name when reporting an error
//
//  <FORM ACTION="formHandler.asp" METHOD="POST" name="form1" onsubmit="return LFORMVAL_validate(m_validateStruct,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">E-mail</td>
//        <td><input type="text" size="35" maxlength="40" name="email"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">Tel</td>
//        <td><input type="text" size="35" maxlength="40" name="tel"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">FAX</td>
//        <td><input type="text" size="35" maxlength="40" name="fax"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">WORK</td>
//        <td></td>
//      </tr>
//      <tr>
//        <td class="formLabels">Name</td>
//        <td><input type="text" size="35" maxlength="40" name="workName"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">E-mail</td>
//        <td><input type="text" size="35" maxlength="40" name="workEmail"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">Tel</td>
//        <td><input type="text" size="35" maxlength="40" name="workTel"></td>
//      </tr>
//      <tr>
//        <td class="formLabels">FAX</td>
//        <td><input type="text" size="35" maxlength="40" name="workFAX"></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
//    LFORMVAL_initForm("form1",m_validateStruct,"m_validateStruct");
//  </SCRIPT>
//
// $Log: lformval.js $
// Revision 1.15  2008-02-04 21:13:07-04  Battersby
// - added quote in this line:   password    : ["lettersOnly",  "skip"],
//
// Revision 1.14  2008-01-14 14:56:38-04  Battersby
// - added a customBlank error type that can be customized
//
// Revision 1.13  2007-12-07 12:09:31-04  Battersby
// - now trim extra spaces from form field labels during error reporting (needed by FireFox)
//
// Revision 1.12  2007-11-30 18:03:15-04  Battersby
// - forgot an alert() left over during debug
//
// Revision 1.11  2007-11-30 17:00:15-04  Battersby
// - simplified and improved search for form field text in formval_getTextIdNode()
//
// Revision 1.10  2007-11-12 17:07:20-04  Battersby
// - changed error text for "numbers"
//
// Revision 1.9  2007-06-25 09:05:15-04  Battersby
// - it is now possible to indicate form fields that are to be skipped in the validate
// struct that will later be enabled at run time
//
// Revision 1.8  2007-04-24 12:29:46-04  Battersby
// - removal of span was not working properly. Moved the code from formatError to validateOne
//
// Revision 1.7  2007-04-24 11:53:56-04  Battersby
// fixed small documentation error, added a comma to the m_validateStruct example
//
// Revision 1.6  2007-04-19 16:17:46-04  Battersby
// - added ability to prefix error message within the validateStruct
// - added LFORMVAL_customError()
// - added LFORMVAL_setErrorText()
// - form text can now be surrounded wy a <span> which will be excluded from
// error messages
// - * in form text (to indicate required fields) is excluded from error messages
//
// Revision 1.5  2007-02-02 11:44:41-04  Battersby
// - needed to add code to handle form validation both when there is a space between
// the input node and the previous <span> node and when there is not
//
// Revision 1.4  2007-02-01 17:25:00-04  Battersby
// - form validation can now handle case where text is beside form element instead of
// in a <td> of it's own
// - corrected "phone" validation to be a generic phone number with or without
// the area code
//
// Revision 1.3  2006-12-09 18:31:40-04  Battersby
// form validation can now handle the highliting of text that is either beside (as before, which means within the same TR tag) or above (which means within a different TR tag) a text box or text area
//
// Revision 1.2  2006-11-02 11:09:28-04  Battersby
// Modified the documentation
//
//------------------------------------------------------------------------*/

/* some constants for indexing into the validation structure */
var LFORMVAL_VALIDATE_TYPE = 0;

/* used for text etc */
var LFORMVAL_REQUIRED = 1;
var LFORMVAL_NAME_PREFIX = 2;

/* used for check boxes */
var LFORMVAL_MIN_REQUIRED = 1;
var LFORMVAL_MAX_ALLOWED = 2;
var LFORMVAL_LABEL_ID_STRING = 3;

/* determine the type of browser */
var LFORMVAL_isDOM = (document.getElementById ? true : false);
var LFORMVAL_isIE4 = ((document.all && !LFORMVAL_isDOM) ? true : false);
var LFORMVAL_isNS4 = (document.layers ? true : false);

var LFORMVAL_errorMsg = {
  en : {
    allChars       : "",
    lettersOnly    : "may only contain letters, spaces, dots, dashes",
    lettersNumbers : "may only contain letters, numbers, dots, spaces, dashes",
    numbers        : "may only contain numbers, dots, dashes",
    postalCode     : "is not a valid postal code",
    postalZip      : "is not a valid postal/zip code",
    postalGen      : "is not a valid postal/zip code",
    phone          : "is not a valid phone number",
    phoneStrictAreaCode : "must be in the form 111-222-3333",
    phoneDigits    : "must contain between 10 and 11 digits (including area code)",
    internationalPhone : "contains illegal characters",
    internationalPhoneDigits : "please include an area code/country code",
    email          : "is not a valid email address",
    url            : "is not a valid web page address",
    currency       : "may only contain numbers, spaces or a dash",
    currencyGt0    : "must not be 0",
    mastercard     : "is not a valid credit card number",
    numDigits      : "does not have the correct number of digits",
    mastercardNot  : "is not a valid Mastercard number",
    visacard       : "is not a valid credit card number",
    visacardNot    : "is not a valid Visacard number",
    noBlank        : "can not be left blank",
    customBlank    : "",
    mostChars      : "contains illegal character(s)",
    dateyyyymmdd   : "must be in yyyy-mm-dd format",
    dateText       : "must be in this format: Jan 17 2004",
    errorList      : "The following errors were found, please correct them before proceeding:"
  },

  fr : {
    allChars       : "",
    lettersOnly    : "may only contain letters, spaces, dots, dashes",
    lettersNumbers : "may only contain letters, numbers, dots, spaces, dashes",
    numbers        : "may only contain numbers, dots, dashes",
    postalCode     : "n'est pas un code postal valide",
    postalZip      : "is not a valid postal/zip code",
    postalGen      : "is not a valid postal/zip code",
    phone          : "n'est pas un numéro de téléphone valide",
    phoneStrictAreaCode : "must be in the form 111-222-3333",
    internationalPhone : "contains illegal characters",
    internationalPhoneDigits : "please include an area code/country code",
    phoneDigits    : "doit contenir entre 10 et 11 chiffres",
    email          : "n'est pas une adresse courriel valide",
    url            : "is not a valid web page address",
    currency       : "ne doit contenir que des chiffres, des espaces ou un trait",
    currencyGt0    : "ne doit pas être un zéro",
    mastercard     : "n'est pas un numéro de carte de crédit valide",
    numDigits      : "ne contient pas le nombre exact de chiffres",
    mastercardNot  : "le numéro de Mastercard n'est pas valide",
    visacard       : "n'est pas un numéro de carte de crédit valide",
    visacardNot    : "le numéro de Visa n'est pas valide",
    noBlank        : "ne pas laissez en blanc",
    customBlank    : "",

    mostChars      : "contains illegal character(s)",
    dateyyyymmdd   : "must be in yyyy-mm-dd format",
    dateText       : "must be in this format: Jan 17 2004",

    errorList      : "Les erreurs suivantes ont été trouvées, s'il vous plaît, les corriger avant de continuer:"
  }
};

var LFORMVAL_lang = "en"; /* language to use is english by default */

/* this reproduces the Javascript constant names */
var NodeType = {
  ELEMENT_NODE: 1,
  ATTRIBUTE_NODE: 2,
  TEXT_NODE: 3,
  COMMENT_NODE: 8,
  DOCUMENT_NODE: 9,
  DOCUMENT_FRAGMENT_NODE: 11
};

var lformval_customErrors = new Array();

//************************************************************************
// Name   : LFORMVAL_initForm
// Author : Paul Battersby
// Description :
//  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
//
// Pre :
//
// Params :
//  formName       - the value of the "name=" field of the form to be initialized
//  validateStruct - see formval_validateOne()
//  validateStructName - the actual name of the validateStruct structure
//                       likely something like "validateStruct"
//
// 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)
//*************************************************************************/
function LFORMVAL_initForm(formName,validateStruct,validateStructName)
{
  /* loop though every field in the validate structure */
  for ( idName in validateStruct.fields ) {

    /* don't bother with fields that are to be skipped */
    if (validateStruct.fields[idName][LFORMVAL_REQUIRED] == "skip") {
      continue;
    } /* endif */

    /* can't perform live validation of checkboxes so skip the onchange */
    /* initialization for checkbox fields */
    if ( validateStruct.fields[idName][LFORMVAL_VALIDATE_TYPE] != "checkbox" ) {

      eval("document.forms." +
            formName +
            "." +
            idName +
            ".onchange = function anonymous() { formval_validateOne(" + validateStructName + ",this,'" + idName + "') } ");

    } /* endif */

  } /* end for */
} /* end of LFORMVAL_initForm */

//************************************************************************
// Name   : LFORMVAL_setLang
// Author : Paul Battersby
// Description :
//  This allows the caller to set the language (english or french)
//  that is to be used for error messages
//
// Pre    :
//  "lang" - one of {"en","fr"} indicates the desired language for error
//           reporting. english or french
//
// Post   :
//  the language for error reporting has been set
//
// Returns :
//  (nothing)
//*************************************************************************/
function LFORMVAL_setLang(lang)
{
  LFORMVAL_lang = lang;
} /* end of LFORMVAL_setLang */

//************************************************************************
// Name   : LFORMVAL_getRefById
// Author : Paul Battersby
// Description :
//  In a browser independant way, this returns a reference to the HTML
//  element that contains the given "id" value
//
// Pre    :
//  LFORMVAL_isDOM, LFORMVAL_isIE4, LFORMVAL_isNS4 have all been defined
//
//  "id" = the value of the id field in an HTML element
//
// Post   :
//  (nothing)
//
// Returns:
//  reference to an HTML element
//*************************************************************************/
function LFORMVAL_getRefById(id)
{
  if (LFORMVAL_isDOM) { return document.getElementById(id); }
  if (LFORMVAL_isIE4) { return document.all[id]; }
  if (LFORMVAL_isNS4) { return document.layers[id]; }
} /* end of LFORMVAL_getRefById */

//************************************************************************
// Name   : LFORMVAL_splitPhone
// Author : Paul Battersby
// Description :
//  This takes a phone number in this format "111-222-3333", breaks it into
//  3 pieces and returns those pieces in an array
//
// Pre :
//  (nothing)
// Params :
//  phoneNumber - a phone number in this format "111-222-3333"
//
// Post :
//  (nothing)
//
// Returns :
//  a 3 element array containing the phone number
//   [0] = 111
//   [1] = 222
//   [2] = 3333
//*************************************************************************/
function LFORMVAL_splitPhone(phoneNumber)
{
  return phoneNumber.split(/[- ]/);
} /* end of LFORMVAL_splitPhone */

//************************************************************************
// Name   : LFORMVAL_textLimit
// Author : Third Party
// Description :
//  This limits the amount of text that can be entered into a text area
//
// Pre    :
//  "field" - the text area who's max number of characters is to be limited
//  "maxlen" - max number of characters allowed in the text area
//
//    example:   onkeyup="FORMAL_limitChars(this,20)";
//
// Post   :
//    if a cut and paste operation has been performed that exceeds the
//    character limit, a message informing the user that their text has
//    been truncated has been displayed
//
// Returns :
//  (nothing)
//*************************************************************************/
function LFORMVAL_textLimit(field, maxlen)
{
  if (field.value.length > maxlen + 1) {
    alert('your input has been truncated!');
  }
  if (field.value.length > maxlen) {
    field.value = field.value.substring(0, maxlen);
  }

} /* end of LFORMVAL_textLimit */


//************************************************************************
// Name   : LFORMVAL_getCheckedRadioVal
// Author : Paul Battersby
// Description :
//  This determines the value of the checked radio button
//
// Pre    :
//  "radioGroup" - the name of a group of radio buttons
//
// Post   :
//  (nothing)
//
// Returns :
//  the value of the checked radio button
//*************************************************************************/
function LFORMVAL_getCheckedRadioVal(radioGroup)
{
  var selectedRadioValue = "";
  var i;

  /* loop through all the radio buttons */
  for ( i = 0; i < radioGroup.length; i++ )
  {
    /* if we've found the one that is currently checked */
    if ( radioGroup[i].checked )
    {
      selectedRadioValue = radioGroup[i].value;
      break;
    }
  }
  return selectedRadioValue;
} /* end of LFORMVAL_getCheckedRadioVal */

//************************************************************************
// Name   : LFORMVAL_getEntryText
// Author : Paul Battersby
// Description :
//
// Pre    :
//  "entryId" - id of the form entry whose currently selected text is to be returned
//                example:  document.forms.myform.myentry
// Post   :
//  (nothing)
//
// Returns :
//  the currently selected text for the given form entry
//*************************************************************************/
function LFORMVAL_getEntryText(entryId)
{
  return entryId.options[entryId.selectedIndex].text;
} /* end of LFORMVAL_getEntryText */

//************************************************************************
// Name   : LFORMVAL_getRadioText
// Author : Paul Battersby
// Description :
//  This returns the text of the selected radio button to the caller
//
// Pre    :
//  radioId - the id of the radio group
//              ex document.forms.myform.myRadioButtons
// Post   :
//  (nothing)
//
// Returns :
//  Text of the clicked radion button or "" if no radio button has been selected
//*************************************************************************/
function LFORMVAL_getRadioText(radioId)
{
  /* loop through the radio buttons */
  for (i=0; i<radioId.length; i++) {

    /* if we found the one that is checked */
    if (radioId[i].checked) {

      /* return the value to the caller */
      return radioId[i].value;
    } /* end for */
  } /* end for */

  /* no radio button is checked */
  return "";

} /* end of LFORMVAL_getRadioText */

//************************************************************************
// Name   : LFORMVAL_getCheckedText
// Author : Paul Battersby
// Description :
//  This returns the text of the selected check boxes to the caller
//
// Pre    :
//  checkboxId - the id of the checkbox group
//              ex document.myform.myCheckBoxes
// Post   :
//  (nothing)
//
// Returns :
//  an array containing the text of each selected checkbox
//  The length of the array will be 0 if nothing is checked
//*************************************************************************/
function LFORMVAL_getCheckedText(checkboxId)
{
  var checkboxText = new Array();
  var idIndex;
  var textIndex = 0;

  /* loop through all the check boxes */
  for ( idIndex=0; idIndex < checkboxId.length; idIndex++ ) {

    /* if this one is checked, add it to the array */
    if (checkboxId[idIndex].checked) {
      checkboxText[textIndex++] = checkboxId[idIndex].value;
    } /* endif */
  } /* end for */

  return checkboxText;

} /* end of LFORMVAL_getCheckedText */

//************************************************************************
// Name   : LFORMVAL_replaceSingleQuote
// Author : Paul Battersby
// Description :
//  This takes all "'" characters in the given string and replaces them
//  with "`"
//
// Pre    :
//    "oldString" - any string
//
// Post   :
//  (nothing)
//
// Returns :
//  the given string with all single quotes replaced with "`"
//*************************************************************************/
function LFORMVAL_replaceSingleQuote(oldString)
{
  newString = oldString.replace(/\'/g,"`");
  return newString;
} /* end of LFORMVAL_replaceSingleQuote */

//************************************************************************
// Name   : LFORMVAL_getCheckedIndex
// Author : Paul Battersby
// Description :
//  This determines the index of the checked radio button
//
// Pre    :
//  "radioGroup" - the name of a group of radio buttons
//
// Post   :
//  (nothing)
//
// Returns :
//  the index of the checked radio button
//*************************************************************************/
function LFORMVAL_getCheckedIndex(radioGroup)
{
  var selectedRadioIndex = 0;
  var i;

  /* loop through all the radio buttons */
  for ( i = 0; i < radioGroup.length; i++ )
  {
    /* if we've found the one that is currently checked */
    if ( radioGroup[i].checked )
    {
      selectedRadioIndex = i;
      break;
    } /* endif */
  } /* end for */

  return selectedRadioIndex;
} /* end of LFORMVAL_getCheckedVal */

//************************************************************************
// Name   : LFORMVAL_countDigits
// Author : Paul Battersby
// Description :
//  This counts the number of digits in the given string excluding characters,
//  spaces etc.
//
// Pre    :
//  "number" - a number whose digits are to be counted
//
// Post   :
//  (nothing)
//
// Returns :
//  number of digits in "number"
//*************************************************************************/
function LFORMVAL_countDigits(number) {
  var matchesList = new Array();

  /* count the digits */
  matchesList = number.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 matchesList.length;
  } /* endif */

}  /* end of LFORMVAL_countDigits */

//************************************************************************
// Name   : LFORMVAL_formatError
// Author : Paul Battersby
// Description :
//  This formats and error message and returns it to the caller
//
// Pre    :
//  - "LFORMVAL_lang" has been defined
//
//
//  "fieldText" - the text representing the field about which an error
//            is being reported. In the example that follows, this would
//            be "(first name)"
//
//  "errorId" - the id of the error string that is to be reported. This is
//              used as an index into LFORMVAL_errorMsg[]
//  "fieldPrefix" - an optiopnal prefix string to add to the field name
//
// Post   :
//  (nothing)
//
// Returns:
//  An error message
//
//   ex: "- (first name): may only contain letters, spaces or a dash"
//*************************************************************************/
function LFORMVAL_formatError(fieldText,errorId)
{
  var errorString = new String();

  /* if this is not the error list message */
  if ( errorId != "errorList" ) {
    errorString = "\"" + fieldText + "\"- " + LFORMVAL_errorMsg[LFORMVAL_lang][errorId] + "\n";

  /* this is the error list message */
  } else {
    errorString = LFORMVAL_errorMsg[LFORMVAL_lang][errorId] + "\n\n";
  } /* endif */

  return errorString;

} /* end of LFORMVAL_formatError */

//************************************************************************
// Name   : formval_getTextIdNode_old
// Author : Paul Battersby
// Description :
//  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
// Pre :
//  (nothing)
//
// Params :
//  inputNode - the id of a form input tag object (ex: form.myForm.inputId)
//
// Post :
//  (nothing)
// Returns :
//   id of the <td> tag containing the text that precedes the given form element
//*************************************************************************/
function formval_getTextIdNode_old(inputNode)
{
  var parent;
  var tdNode;
  var previousTrNode;

/*
  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
*/

  /* get the parent of the inputNode which will be a <TD> node */
  parent = inputNode.parentNode;

  /* if the first child is not the inputNode, then there must be some text */
  /* beside the input node rather than in the previous <TD> node */
  /* (CASE 3) */
  if ( parent.firstChild != inputNode ) {
    /* this assumes that the text of interest is wrapped in a tag (<SPAN> perhaps) of some sort */
    /* otherwise it won't be possible to alter the colour since text has no style */
    /* attribute, only element tags have attributes */
    tdNode = inputNode.previousSibling;

    /* if the previous element is a text node, we must go back one further */
    /* this likely means that there is a space between the <INPUT> and the <SPAN> */
    if ( tdNode.nodeType == NodeType.TEXT_NODE ) {
      tdNode = tdNode.previousSibling;
    } /* endif */

    return tdNode;
  } /* endif */

  /* get the previous sibling of the <TD> node */
  /* on IE this will be the <TD> node we are looking for */
  /* on NETSCAPE, it will be some inexplicable phantom TEXT node! */
  /*   so on NETSCAPE we have to keep looking */
  tdNode = parent.previousSibling;

  /* if tdNode is null, that means there is no <TD> above the current one */
  /* that is within the same row. We must look to the row above (CASE 2) */
  /* (if this were Netscape the node above the current one would at least be */
  /*  a text node) */
  if ( tdNode == null ) {
    tdNode = parent.parentNode.previousSibling.lastChild;
    return tdNode;

  /* if we found the "<TD>" we're looking for, we're done */
  /* (CASE 1) */
  } else if (tdNode.nodeName == "TD") {
    return tdNode;
  } /* endif */

  /* if we get here we are in Netscape and tdNode points to the phantom <text> */
  /* node. It's previous sibling is either a TD or it's null if there is no previous */
  /* TD within the current TR */
  if ( tdNode.previousSibling != null ) {
    return tdNode.previousSibling
  } /* endif */

  /* must now go to the previous TR and begin searching forward for the */
  /* first TD */
  previousTrNode = parent.parentNode.previousSibling.previousSibling;
  tdNode = previousTrNode.firstChild;

  /* keep skipping over nodes until we find the first TD node */
  while ( tdNode.nodeName != "TD" )
  {
    tdNode = tdNode.nextSibling;
  }
  return tdNode;

} /* end of formval_getTextIdNode_old */

//************************************************************************
// Name   : formval_getTextIdNode
// Author : Paul Battersby
// Description :
//  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
// Pre :
//  (nothing)
//
// Params :
//  inputNode - the id of a form input tag object (ex: form.myForm.inputId)
//
// Post :
//  (nothing)
// Returns :
//   id of the <td> tag containing the text that precedes the given form element
//*************************************************************************/
function formval_getTextIdNode(node)
{
/*
  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
*/

  // look for the closest TR node
  if (node.nodeName != "TR") {
    return formval_getTextIdNode(node.parentNode);

  // found the TR
  } else {

    // get all the nodes in the TR row
    children = node.childNodes;
    for ( var i = 0; i< children.length; i++) {

      // if the first TD node has been found
      if (children[i].nodeName == "TD") {

        // if there is a span inside the TD
        if (children[i].firstChild.nodeName == "SPAN") {
          return children[i].firstChild;

        // no span, just text
        } else {
          return children[i];
        } /* endif */
      } /* endif */
    } /* end for */
  } // endif

} /* end of formval_getTextIdNode */

//************************************************************************
// Name   : formval_validateCheckboxes
// Author : Paul Battersby
// Description :
//  This ensures that the correct number of checkboxes have been selected
//
// Pre :
//  (nothing)
//
// Params :
//` validateStruct - see formval_validateOne()
//  id             - see formval_validateOne()
//  idName         - see formval_validateOne()
//
// Post :
//  (nothing)
//
// Returns :
//  an error message if the number of checked checkboxes is not correct
//   OR
//  an empty string
//*************************************************************************/
function formval_validateCheckboxes(validateStruct,id,idName)
{
  var error = "";

  /* get an array of selected checkboxes */
  var checkBox = LFORMVAL_getCheckedText(id);

  /* get an index into the validateStruct for this checkbox */
  var structIndex = idName;

  /* get the min and max allowed number of seleted checkboxes */
  var minChecked = validateStruct.fields[structIndex][LFORMVAL_MIN_REQUIRED];
  var maxChecked = validateStruct.fields[structIndex][LFORMVAL_MAX_ALLOWED];

  /* get the label text that belongs with the checkboxes */
  var text = LFORMVAL_getRefById(validateStruct.fields[structIndex][LFORMVAL_LABEL_ID_STRING]).innerHTML;

  /* if the number of checked checkboxes is out of range */
  if ( checkBox.length < minChecked ||
       checkBox.length > maxChecked) {

    /* if there is not a range but instead a fixed number that must be checked */
    if (minChecked != maxChecked) {
      error = 'You must select between ' + minChecked + ' & ' + maxChecked + ' "' + text + '" check boxes';
    } else {
      error = 'You must select ' + minChecked + ' "' + text + '" check boxes';
    } /* endif */

  } /* endif */

  return(error);

} /* end of formval_validateCheckboxes */

//************************************************************************
// Name   : formval_validateOne
// Author : Paul Battersby
// Description :
//    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
//
// Pre    :
//  "validateStruct" - a structure that looks like this:
//
//     var validateStruct = {
//       normalColor : "black",
//       errorColor : "red",
//       fields : {
//         surname        : ["lettersOnly",     "required"],
//         firstName      : ["lettersOnly",     "required"],
//         affiliation    : ["lettersNumbers",            ],
//         address        : ["lettersNumbers",  "required"],
//         city           : ["lettersOnly",     "required"],
//         checkboxes     : ["checkbox",     1,1, "checkboxesLabel"]
//       }
//     };
//
//     Where:
//        normalColor - the color to use when the form element contents
//                      passes the validation
//        errorColor  - the color to use when the form element contents
//                      fails the validation
//        fields      - an array of id names of form elements to be validated. This
//                      array indicates the type of information that is acceptable
//                      for the form element, generally the id of the label that accompanies
//                      the form element, the type of data that is permitted by the
//                      form element and an indication as to whether the form
//                      element is required (may be left blank)
//
//                      In the case of a checkbox, you must indicate the minimum,
//                      maximum number of elements that can be checked as well
//                      as the "name" or "id" of the text label that accompanies
//                      the group of check boxes
//
//  * See the file formval-test.htm for an example
//
//  "id" - the id of the form element to be validated typically the object
//         referred to by "this"
//
//  "idName" - 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 "normalColor" color or "errorColor"
//
// Returns :
//  error string if an error occurred, an empty string otherwise
//*************************************************************************/
function formval_validateOne(validateStruct,id,idName) {
    var error = "";
    var idToChange;
    var errorColor = validateStruct.errorColor;
    var normalColor = validateStruct.normalColor;
    var textPrefix;

    textPrefix = validateStruct.fields[idName][LFORMVAL_NAME_PREFIX];

    /* if we are validating a checkbox */
    if (validateStruct.fields[idName][LFORMVAL_VALIDATE_TYPE] == "checkbox") {

      error = formval_validateCheckboxes(validateStruct,id,idName);

      /* get the id of the label that belongs to the checkboxes */
      idToChange = LFORMVAL_getRefById(validateStruct.fields[idName][LFORMVAL_LABEL_ID_STRING]);

    /* we are not validating a checkbox */
    } else {
      var value = new String();

      var structIndex = id.name;
      var validateType = validateStruct.fields[structIndex][LFORMVAL_VALIDATE_TYPE];

      idToChange = formval_getTextIdNode(id);

      value = id.value;

      /* 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 {
        // trim extra spaces (needed by FireFox)
        text = idToChange.innerHTML.replace(/^\s+|\s+$/g,"");
//      } /* endif */

      /* incase there is a <span> or something surrounding the text, remove it */
      text = text.replace(/<[^>]*>/gi,"");

      /* often a "*" precedes the text to indicate a required field */
      /* we don't want that in our error message                    */
      text = text.replace(/^\*/,"");

      /* if we need to add a prefix to the text */
      if (textPrefix != null && textPrefix != "") {
        text = textPrefix + text;
      } /* endif */

      /* if this is a required field */
      if (validateStruct.fields[structIndex][LFORMVAL_REQUIRED] == "required") {

        /* if this field is left blank */
        if ((value.length == 0) || (!value.match(/[^ ]/))) {
          if ( validateType == "customBlank" ) {
            error = LFORMVAL_formatError(text,"customBlank");
          } else {
            error = LFORMVAL_formatError(text,"noBlank");
          } // endif

          /* change the color of given text to indicate an error */
          idToChange.style.color = errorColor;
          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 = normalColor;
        return "";
      } /* endif */

      switch (validateType) {
        case "allChars":
          break;

        case "lettersOnly":
          if ( value.match(/[^a-zA-Z ]/)) {
            error = LFORMVAL_formatError(text,"lettersOnly");
          }; /* endif */
          break;

        case "lettersNumbers" :
          if ( value.match(/[^a-zA-Z \-\.0-9]/)) {
            error = LFORMVAL_formatError(text,"lettersNumbers");
          }; /* endif */
          break;

        case "mostChars" :
          if ( value.match(/[^a-zA-Z 0-9\~\`\!\@\#\$\%\^\&\*\(\)\_\-\+\=\|\\\{\}\[\]\:\;\'\?\/\>\<\,\.]/) ) {
            error = LFORMVAL_formatError(text,"mostChars");
          }; /* endif */
          break;

        case "numbers" :
          if ( value.match(/[^0-9\-\.]/)) {
            error = LFORMVAL_formatError(text,"numbers");
          }; /* endif */
          break;

        case "postalCode" :
          if (!value.match(/^[a-zA-Z]\d[a-zA-Z]\s*\d[a-zA-z]\d$/)) {
            error = LFORMVAL_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 handles by this */

            error = LFORMVAL_formatError(text,"postalZip");
          }; /* endif */
          break;

        case "postalGen" :
          if ( value.match(/[^a-zA-Z 0-9\-]/) ) {
            error = LFORMVAL_formatError(text,"postalGen");
          }; /* endif */
          break;

        case "phone" :
          if ( !value.match(/^(1[- ])?(\d{3}[- ])?\d{3}[- ]\d{4}$/) ) {
            error = LFORMVAL_formatError(text,"phone");
          }; /* endif */
          break;

//        /* if the phone number contains invalid characters */
//        if ( !value.match(/^\+?[0-9 ()-]+[0-9]$/) ) {
//          error = LFORMVAL_formatError(text,"phone");
//        } else {
//
//          /* if there are too few or too many digits */
//          numDigits = LFORMVAL_countDigits(value);
//          if ( numDigits < 7 || numDigits > 11) {
//            error = LFORMVAL_formatError(text,"phoneDigits");
//          }; /* endif */
//        }; /* endif */
          break;

        case "internationalPhone" :
          /* if the phone number contains invalid characters */
          if ( !value.match(/^\+?[0-9 ()-\~]+[0-9]$/) ) {
            error = LFORMVAL_formatError(text,"internationalPhone");
          } else {

            /* if there are too few or too many digits */
            numDigits = LFORMVAL_countDigits(value);
            if ( numDigits < 10) {
              error = LFORMVAL_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 */
          if ( !value.match(/^(1[- ])?\d{3}[- ]\d{3}[- ]\d{4}$/) ) {
            error = LFORMVAL_formatError(text,"phoneStrictAreaCode");
          }; /* endif */
          break;

        case "email":
          /* if basic email validation fails */
          if ( !value.match(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/)) {
            error = LFORMVAL_formatError(text,"email");
          }; /* endif */
          break;

        case "url":
          /* if basic url validation fails */
          if ( !value.match(/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)) {
            error = LFORMVAL_formatError(text,"url");
          }; /* endif */
          break;

        case "currency":
          if ( !value.match(/\$?[0-9-\.]/)) {
            error = LFORMVAL_formatError(text,"currency");
          }; /* endif */
          break;

        case "currencyGt0":
          if ( !value.match(/\$?[0-9-\.]/)) {
            error = LFORMVAL_formatError(text,"currency");

          } else if (value <= 0){
            error = LFORMVAL_formatError(text,"currencyGt0");
          }; /* endif */
          break;

        case "mastercard":
          /* if the mastercard is the wrong length */
          if (LFORMVAL_countDigits(value) != 16 ) {
            error = LFORMVAL_formatError(text,"numDigits");

          /* if the number does not start with the correct digits */
          } else if (!value.match(/^(51|52|53|54|55)/)) {
              error = LFORMVAL_formatError(text,"mastercardNot");

          } else {
            /* if the number fails the credit card math */
            if (!LFORMVAL_validateLuhnFormula(value)) {
              error = LFORMVAL_formatError(text,"mastercard");
            }; /* endif */
          }; /* endif */
          break;

        case "visacard":
          /* if the visacard is the wrong length */
          var numDigits = LFORMVAL_countDigits(value);

          /* if the card does not have the correct number of digits */
          if (numDigits != 16 && numDigits != 13) {
            error = LFORMVAL_formatError(text,"numDigits");

          /* number does  not start with the correct starting digit */
          } else if (!value.match(/^4/)) {
            error = LFORMVAL_formatError(text,"visacardNot");

          } else {
            /* if the number fails the credit card math */
            if (!LFORMVAL_validateLuhnFormula(value)) {
              error = LFORMVAL_formatError(text,"visacard");
            }; /* endif */
          }; /* 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 = LFORMVAL_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 = LFORMVAL_formatError(text,"dateText");
          }; /* endif */
          break;

        case "customBlank":
          // this simply permits a custom message to be displayed when a field
          // is left blank
          break;

        default :
          error = "formval_validateOne: " + validateType + " 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 = normalColor;

    /* there was an error */
    } else {
      /* change the color of given text to indicate an error */
      idToChange.style.color = errorColor;
    }; /* endif */

    return error;

}  /* end of formval_validateOne */

//************************************************************************
// Name   : LFORMVAL_customError
// Author : Paul Battersby
// Description :
//  This allows the caller to add a custom error to the list of errors
//  that will be displayed during form validation
//
// Pre :
//  (nothing)
//
// Params :
//  "validateStruct" - see formval_validateOne()
//  "formElementId"  - the id of the form element to be validated (ex. document.myForm.firstName)
//  "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 it'c colour changed to the error colour
//  specified in the validateStruct
//
// Returns :
//  (nothing)
//*************************************************************************/
function LFORMVAL_customError(validateStruct,formElementId,errorString)
{
    var errorColor = validateStruct.errorColor;
    var normalColor = validateStruct.normalColor;

    idToChange = formval_getTextIdNode(formElementId);

    /* 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 = normalColor;

    /* there was an error */
    } else {
      /* change the color of given text to indicate an error */
      idToChange.style.color = errorColor;

      /* 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";
      lformval_customErrors = lformval_customErrors.concat(errorString);

    } /* endif */

} /* end of LFORMVAL_customError */

//************************************************************************
// Name   : LFORMVAL_validate
// Author : Paul Battersby
// Description :
//  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
//
// Pre    :
//  "validateStruct" - see formval_validateOne()
//  "formId" - the id of the form 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 :
//  false - validation failed (errors were reported)
//  true  - validation succeeded (no errors reported)
//*************************************************************************/
function LFORMVAL_validate(validateStruct,formId) {
  var i;
  var errors = new String();
  var id;
  var idName; /* index into the validateStruct for the field being validated */


  /* loop through the list of fields to be validated */
  for ( idName in validateStruct.fields ) {

    /* if this field is to be skipped, then move on */
    if (validateStruct.fields[idName][LFORMVAL_REQUIRED] == "skip") {
      continue;
    }

    /* if the id doesn't exist, skip it */
    /* this allows the validateStruct to contain entries for which */
    /* some JavaScript has not yet created an HTML element         */
    id = eval("formId." + idName);
//    if ( id == null ) {
//      continue;
//    }; /* endif */

    errors += formval_validateOne(validateStruct,id,idName);

  }; /* end for */

  for (i = 0; i<lformval_customErrors.length; i++) {
    errors += lformval_customErrors[i];
  } /* end for */

  /* if at least one error was detected */
  if (errors.length > 0) {
    alert(LFORMVAL_formatError(text,"errorList") + errors);
  }; /* endif */

  if (errors.length > 0) {

    /* reset the custom errors */
    lformval_customErrors = lformval_customErrors.splice(0,0);

    return false;
  } else {
    return true;
  };
}  /* end of LFORMVAL_validate */

//************************************************************************
// Name   : LFORMVAL_validateLuhnFormula
// Author : copyright 12th May 2003, by Stephen Chapman, Felgall Pty Ltd
//          (reformatted/repackaged by Paul Battersby)
//
// Description :
//  This performs the Luhn Formula on a credit card number to determine
//  it if is a valid Visa or Mastercard number
//
//  Based on ANSI X4.13, the LUHN formula (also known as the modulus 10 -- or
//  mod 10 -- algorithm ) is used to generate and/or validate and verify the
//  accuracy of credit-card numbers.
//
//  Most credit cards contain a check digit, which is the digit at the end of the
//  credit card number. The first part of the credit-card number identifies the
//  type of credit card (Visa, MasterCard, American Express, etc.), and the middle
//  digits identify the bank and customer.
//
//  To generate the check digit, the LUHN formula is applied to the number. To
//  validate the credit-card number, the check digit is figured into the formula.
//
//  Here's how the algorithm works for verifying credit cards; the math is quite
//  simple:
//
//  1) Starting with the second to last digit and moving left, double the value of
//  all the alternating digits.
//
//  2) Starting from the left, take all the unaffected digits and add them to the
//  results of all the individual digits from step 1. If the results from any of
//  the numbers from step 1 are double digits, make sure to add the two numbers
//  first (i.e. 18 would yield 1+8). Basically, your equation will look like a
//  regular addition problem that adds every single digit.
//
//  3) The total from step 2 must end in zero for the credit-card number to be
//  valid.
//
//  The LUHN formula was created in the late 1960s by a group of mathematicians.
//  Shortly thereafter, credit card companies adopted it. Because the algorithm is
//  in the public domain, it can be used by anyone.
//
//  The LUHN formula is also used to check Canadian Social Insurance Number (SIN)
//  validity. In fact, the LUHN formula is widely used to generate the check
//  digits of many different primary account numbers. Almost all institutions that
//  create and require unique account or identification numbers use the Mod 10
//  algorithm.
//
// Pre    :
//  "s" - the credit card number that is to be checked
//
// Post   :
//  (nothing)
//
// Returns :
//  true  - credit card number passes the mathematical check
//  false - credit card fails the mathematical check
//*************************************************************************/
function LFORMVAL_validateLuhnFormula(s) {

  var v = "0123456789";
  var w = "";
  for (var i=0; i < s.length; i++) {
    x = s.charAt(i);
    if (v.indexOf(x,0) != -1)
    w += x;
  }
  var j = w.length / 2;
  if (j < 6.5 || j > 8 || j == 7) return false;
  var k = Math.floor(j);
  var m = Math.ceil(j) - k;
  var c = 0;
  for (var i=0; i<k; i++) {
    a = w.charAt(i*2+m) * 2;
    c += a > 9 ? Math.floor(a/10 + a%10) : a;
  }
  for (var i=0; i<k+m; i++) c += w.charAt(i*2+1-m) * 1;
  return (c%10 == 0);
} /* end of LFORMVAL_validateLuhnFormula */

//************************************************************************
// Name   : LFORMVAL_handleMutEx
// Author : Paul Battersby
// Description :
//  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
//
// Pre    :
//  "muExList" - the list of form elements that are to be considered mutually
//               exclusive.
//  "id" - 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)
//*************************************************************************/
function LFORMVAL_handleMutEx(mutExList,id)
{
  /* if this form element became blank */
  if (LFORMVAL_getRefById(id).value == "") {
    /* enable the other forms from the list */
    for ( i in mutExList ) {
      LFORMVAL_getRefById(mutExList[i]).disabled = false;
    }; /* end for */

  /* text was entered */
  } else {
    /* disable the other forms elements from the list */
    for ( i in mutExList ) {

      /* disable only if not the selected element */
      if ( mutExList[i] != id ) {
        LFORMVAL_getRefById(mutExList[i]).disabled = true;
      }
    }; /* end for */

  }; /* endif */
} /* end of LFORMVAL_handleMutEx */

//************************************************************************
// Name   : LFORMVAL_noEnter
// Author : Paul Battersby
// Description :
//  This prevents the Enter key from being used to submit a form
//
// Pre :
//  This needs to be placed in every text element of a form like this:
//    onkeypress="return LFORMVAL_noEnter()"
//
// Params :
//  (nothing)
//
// Post :
//  (nothing)
//
// Returns :
//  false - if the enter key was pressed
//  true  - otherwise
//*************************************************************************/
function LFORMVAL_noEnter()
{
  return !(window.event && window.event.keyCode == 13);
} /* end of LFORMVAL_noEnter */


//************************************************************************
// Name   : LFORMVAL_setErrorText
// Author : Paul Battersby
// Description :
//  This allows the caller to alter any of the error messages in
//  LFORMVAL_errorMsg. Useful if a different language needs to be used
//  besides the two provided
//
// Pre :
//  (nothing)
//
// Params :
//  lang       - one of {en,fr} to indicate which group of language strings is being
//               modified
//  errorClass - the specific error message that is being modified
//  newMsg     - the new message string
//
// Post :
//  the LFORMVAL_errorMsg array has been updated with the given string
//
// Returns :
//  (nothing)
//*************************************************************************/
function LFORMVAL_setErrorText(lang,errorClass,newMsg)
{
  LFORMVAL_errorMsg[lang][errorClass] = newMsg;
} /* end of LFORMVAL_setErrorText */
