//////////////////////////////////////////////////////////////////////////////////////////////
///
/// Form validation script designed for use with the perl program 'uniformmail.pl'
///
/// Copyright James D H Turner 2004
/// Published by Cathonian Software and SKARO.NET
///   http://www.skaro.net
///
/// This script is only available as part of the 'Uniform Mail' package. You are, therefore,
/// bound by the license terms of that package.
/// You may use this script without the perl program 'uniformmail.pl' but a link to skaro.net
/// if still required. It should read something like
///   'form validation by skaro.net'
/// or
///   'validation script by skaro.net'
///
/// You may modify this script but you may not redistribute original or modified versions.
///
/// This script may not be suitable for interactive forms that make use of the following events :-
///   onclick, onchange, onkeydown
/// These events are used for clearing error-highlighting.
/// Generally, events should be handled correctly but thorough testing is advised.
///
/// This notice must not be modified or deleted. You may remove other comments.
///
//////////////////////////////////////////////////////////////////////////////////////////////


// Testing flag
var UFM_TESTING = false;

// Highlighting colors. If a label is found it is highlighted, otherwise the control is highlighted.
// Also see ufm_highlightControl
var UFM_HL_COLOR            = '';        // text       color of control
var UFM_HL_COLOR_LABEL      = '';        // text       color of label
var UFM_HL_BACKGROUND       = '#FF0000'; // background color of control.
var UFM_HL_BACKGROUND_LABEL = '#FF0000'; // background color of label

function ufm_highlightControl(ctrl,hl,focusCtrl)
// Highlight specified control.
// hl is a boolean. Highlighting is set if hl is true and cleared if false.
// If focusCtrl is true the control is focused.
// Looks for an associated label and colors it red. The label must wrap around the control.
// If no label is found :-
//   the control is colored red
//   for some browser/control combinations this may have no effect e.g. Firefox/radio/checkbox
//
// If a control requires special handling, assign a handler to the event ufm_onhighlight
{ // If necessary and possible, focus the control
  if (focusCtrl && ctrl.focus) ctrl.focus();

  // If assigned, call the custom handler for this control
  if (ctrl.ufm_onhighlight) return ctrl.ufm_onhighlight(hl);

  // Look for a label
  var tmp   = ctrl.parentNode;
  var LABEL = false;
  if ((tmp) && (tmp.nodeName) && (tmp.nodeName.toLowerCase() == 'label')) {
    ctrl  = tmp;
	LABEL = true;
    // If necessary and possible, bring the label into view
    if (focusCtrl && ctrl.scrollIntoView) ctrl.scrollIntoView(true);
  }
  
  with (ctrl.style)
    if (hl) {
      if (LABEL) { color = UFM_HL_COLOR_LABEL; backgroundColor = UFM_HL_BACKGROUND_LABEL }
	        else { color = UFM_HL_COLOR;       backgroundColor = UFM_HL_BACKGROUND       }
    }
    else { color = ''; backgroundColor = '' }
}

function ufm_ungrabEvent()
// Clear highlight, restore grabbed event.
// Unless false is passed as a parameter, the original event is called if one was assigned.
{ ufm_highlightControl(this,false);
  
  var tmp = 'this.' + this.ufm_grabbedName;
  eval(tmp + ' = this.ufm_grabbedEvent;') // restore event

  this.ufm_grabbedEvent = null;
  this.ufm_grabbedName  = '';

  var callEvent = ((arguments.length == 0) || arguments[0]); // params tested this way to avoid Firefox bug
  if (callEvent && eval(tmp)) eval(tmp + '()');              // call restored event
}

function ufm_ungrabRadio()
// Call ufm_ungrabEvent for each radio button in the group.
// The original onclick event is only called for this.
{ var tmp = eval('this.form.' + this.name);
  var ctrl;
  if (tmp) for (var i = 0; i < tmp.length; i++) {
    ctrl = tmp[i]; if (ctrl) {
      ctrl.ufm_radioFudge = ufm_ungrabEvent;
	  ctrl.ufm_radioFudge(ctrl == this);
}}}

function ufm_grabEvent(ctrl,eventName,handler)
// Replace the named event with a new handler. Call ufm_ungrabEvent() to restore the old handler.
// If handler is not specified, ufm_ungrabEvent is assumed.
// Does nothing if the event has already been grabbed.
{ if (!handler) handler = ufm_ungrabEvent;

  var tmp = 'ctrl.' + eventName;

//Quit if handler has already been assigned to the event.
  if (eval(tmp) == handler) return;

  ctrl.ufm_grabbedEvent = eval(tmp);
  ctrl.ufm_grabbedName  = eventName;
  eval(tmp + ' = handler;' )
}

function ufm_validateCheckbox(cb) { return cb.checked }

function ufm_validateRadioGroup(rb)
// rb = radio button : return true if any in its group is checked 
{ var tmp = eval('rb.form.' + rb.name);
  if (tmp) for (var i = 0; i < tmp.length; i++) if (tmp[i].checked) return 1
}

function ufm_validateSelectOne(sel)
// Checks the text of the selected index.
// If the index is zero or the first and last chars are '-' returns false.
{ var i   = sel.selectedIndex; if (i == 0) return 0;
  var txt = sel.options[i].text;
  var len = txt.length;

  if ((len == 0) || ((txt.charAt(0) == '-') && (txt.charAt(len-1) == '-'))) return 0;

  return 1;
}

function ufm_validateSelectMulti(sel)
{ return (sel.selectedIndex >= 0) }

function ufm_confirm(ctrl)
// ctrl should be of type 'text' or 'password'.
// Scans the form that owns ctrl for fields of identical name/type.
// Returns false if another field with the same name/type contains a different value.
// Used to validate confirmation fields e.g. ensure email addresses are entered identically twice.
{ var tp = ctrl.type.toLowerCase();
  with (ctrl.form)
    for (var i = 0; i < elements.length; i++) if (elements[i] != ctrl ) with (elements[i]) {
      if ((name == ctrl.name) && (type.toLowerCase() == tp) && (value != ctrl.value)) return false
    }

  return true
}

function ufm_getPrefixCode(p,fn)
// Identify type of field by prefix in p
// If the prefix is followed by '?', the result is incremented by 1 and the field treated as optional.
// e.g. ufm_getPrefixCode('@?test','test') returns 8.
// If p and fn do not match, zero is returned.
// Prefixes are :-
//   '-'   any decimal integer      : result  1
//   '+'   positive decimal integer : result  3
//   '#'   any hex integer          : result  5
//   '@'   required email address   : result  7
//   '[n]' minimum length of field  : result -n ('?' modifier does not apply)
{ if ((typeof(p) != 'string') || (typeof(fn) != 'string') || (p.length < 2) || (p.indexOf(fn) < 1)) return 0;

  ch = p.charAt(0); p = p.substring(1);
  var opt = 0; if (p.charAt(0) == '?') { p = p.substring(1); opt = 1 }

  if (ch == '[') {
    var tmp = p.split(']',2); if (tmp.length < 2) return 0;
    p   = tmp[1];
	tmp = parseInt(tmp[0]); if (isNaN(tmp) || (tmp < 0)) return 0;
  }

  if (p  != fn)  return 0;
  if (ch == '-') return 1 + opt;
  if (ch == '+') return 3 + opt;
  if (ch == '#') return 5 + opt;
  if (ch == '@') return 7 + opt;
  if (ch == '[') return -tmp;

  return 0;
}

function ufm_isInteger(str,pfc)
// private function for use by ufm_validateText
{ while ((str.length > 1) && (str.charAt(0) == ' ')) str = str.substring(1);
  if    ((pfc <= 2)       && (str.charAt(0) == '-')) str = str.substring(1);

  if (str == '') return false;
  
  var vc = '0123456789'; if (pfc >= 5) vc = vc + 'abcdefABCDEF';

mainloop:  
  for (var i = 0; i < str.length; i++) {
    for (var j = 0; j < vc.length; j++) if (str.charAt(i) == vc.charAt(j)) continue mainloop;
	return false;
  }
  return true;
}

function ufm_isEmail(str)
// private function for use by ufm_validateText
{ var i = -1; return ((str.length >= 5) && ((i = str.indexOf("@")) > -1) && (i < str.lastIndexOf("."))) }

function ufm_validateText(ctrl,pfc)
// validate a control according to a prefix code. See ufm_getPrefixCode
// pfc = 0..8 are recognised. Positive even values are optional.
// pfc < 0 specifies a minimum length.
// 0 returns true if ctrl.value != ''
{ if (!ufm_confirm(ctrl)) return false;           // fields of same name/type must be identical.

  var tmp = ctrl.value;
  if (!pfc)     return (tmp != '');               // if pfc = 0 simply ensure field is not blank.
  if (pfc <  0) return (tmp.length >= -pfc);      // test length only
  if ((pfc % 2 == 0) && (tmp == '')) return true; // blank is ok if the field is optional.
  if (pfc <= 6) return ufm_isInteger(tmp,pfc);
  if (pfc <= 8) return ufm_isEmail  (tmp);
  
  return false; // prefix code not recognised
}

function ufm_validateControl(ctrl,pfc,focusCtrl)
// This function recognises controls of the following types :-
//   checkbox, file, password, radio, select-multiple, select-one, text, textarea.
// Custom controls can be validated through the event ctrl.ufm_onvalidate().
// TRUE is returned for unrecognised controls (e.g. buttons).
// pfc is the prefix code as returned by ufm_getPrefixCode() - it applies only to text-like fields.
// If ctrl data is invalid :-
//   ufm_highlightControl() is called.
//   false is returned.
{ var ok = true;
  var tp = ctrl.type.toLowerCase();

  if (ctrl.ufm_onvalidate) ok = ctrl.ufm_onvalidate(pfc)
  else with (ctrl) {
    if (tp == 'text') ok = ufm_validateText(ctrl,pfc)
	else {
      if (pfc > 0) pfc = 0; // only a minimum length is allowed for other text-like controls
      if ((tp == 'textarea') || (tp == 'file')) ok = ufm_validateText(ctrl,pfc)
	  else if (tp == 'password') {
        if (pfc == 0) pfc = -6; // default minimum length of password
        ok = ufm_validateText(ctrl,pfc)
	  }
	  else if (tp == 'checkbox') ok = ufm_validateCheckbox(ctrl)
	  else if (tp == 'radio')    ok = ufm_validateRadioGroup(ctrl)
      else if (tp == 'select-one')      ok = ufm_validateSelectOne(ctrl)
      else if (tp == 'select-multiple') ok = ufm_validateSelectMulti(ctrl)
	  else return true; // return true if control type is not recognised
	}
  }

  ufm_highlightControl(ctrl,!ok,focusCtrl && !ok);

  if (!ok) {
    // Assign an event handler to clear the highlight when the user changes the value.
	// Existing event handlers are saved and will be restored and called as needed.
    if (tp == 'radio') ufm_grabEvent(ctrl,'onclick',ufm_ungrabRadio)
	else { if (tp == 'checkbox')        tp = 'onclick'
      else if (tp == 'select-one')      tp = 'onchange'
      else if (tp == 'select-multiple') tp = 'onchange'
      else tp = 'onkeydown';
      ufm_grabEvent(ctrl,tp);
  }}
  
  return ok;
}

function ufm_validate(form,params)
// params may be one or more string values (i.e. this function accepts more than two parameters).
// If params is absent, all controls on the form are validated.
// params identify controls on the form by name.
// Prefixes such as '@' may be used to identify special fields such as email addresses or integers.
// See examples below and the function ufm_getPrefix code for more information on prefixes.
// If params = '!' the named fields that follow are excluded from validation (unless prefixed).
//
// e.g. ufm_validate(forms[0],'test');       Only input fields named 'test' are validated.
//      ufm_validate(forms[0],'!','test');   All  input fields except those named 'test' are validated.
//      ufm_validate(forms[0],'!','test','@email');
//        All fields except those named 'test' are validated.
//        All fields named 'email' are treated as an email address.
//
// NOTES : forms[] is an array property of the document object.
//       : If two text or password fields share the same name, they must contain the same data,
//         otherwise both are considered invalid.
{ var x, tmp, ctrl, pfc, include, exclude, found;
  var ok = true;

//Determine the nature of params
  if (params == '!') exclude = true; else if (typeof(params) == 'string') include = true;

  for (var i = 0; i < form.elements.length; i++) {
    ctrl = form.elements[i];
	pfc  = 0;

//  If necessary, check control to see if it is to be included in the validation.
//  Inclusion list starts with arguments[1] but exclusion list starts with arguments[2]
	x = 0; if (include) x = 1; else if (exclude) x = 2;
    if (x) {
      found = false;
      for (; x < arguments.length; x++ ) {
        tmp   = arguments[x];
        pfc   = ufm_getPrefixCode(tmp,ctrl.name); if (pfc)   break; // control must be validated
        found = (tmp == ctrl.name);               if (found) break;
      }
//    Fields specified with a prefix are validated irrespective of include & exclude values.
      if (!pfc) if ((found && exclude) || (!found && include)) continue;
	}

//  Validate the control. If it fails it will be highlighted. The first control to fail is focused.
    if (!ufm_validateControl(ctrl,pfc,ok)) ok = false;
  }

  if (!ok) alert('Please complete all required fields.'); else if (UFM_TESTING) alert('Success - the form validates.');

  return ok;
}

function ufm_unhighlightControls(form,focusFirst)
// Clear the highlight on all controls
// If focusFirst is true, the first control is focused.
{ for (var i = 0; i < form.length; i++) {
	ufm_highlightControl(form.elements[i],false,focusFirst && (i == 0))
}}
