//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//	clsFormValidator.js
//	Class definition file for FormValidator class
//
//	Copyright 2001 by Gary Pajor / New Media Now
//	All rights reserved.
//	www.sitetoolz.com
//	Please email support@sitetoolz.com for terms of use.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Class: FormValidator 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Dependencies:
//			Classes:	FieldDataObject
//						CharChecker
//						FieldValueRule
//
//			Definitions for these classes included in this file.
//


FormValidator.ON = true;

// ~~~ Constructor method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function FormValidator(f,fields,fieldLabels,reqFields,fldNamePrefix,showAlerts,setDoCharCheckOnFields) {
	if ( arguments.length == 0 ) return; // takes care of 'throw-away' object created below for Nav3 bug
	this.form = f;
	this.fldSetupOK = FV_setupFields(f,fields,fieldLabels,reqFields,fldNamePrefix,showAlerts,setDoCharCheckOnFields);
	//var fldSetupOK = FV_setupFields(f,fields,fieldLabels,reqFields,fldNamePrefix,showAlerts,setDoCharCheckOnFields);
	//if ( fldSetupOK == false ) this.fldSetupOK = false;
}

// Create 'throw-away' FormValidator object to create prototype object for the class
// (bug fix for Navigator 3).
new FormValidator();

// Set prototype methods
FormValidator.prototype.toString 			= FormValidator_toString;
FormValidator.prototype.dumpFDO 			= FormValidator_dumpFDO;
FormValidator.prototype.showFieldsByProp 	= FV_showFieldsByProp;
FormValidator.prototype.setFieldProperty 	= FormValidator_setFieldProperty;
FormValidator.prototype.getFieldProperty 	= FormValidator_getFieldProperty;
FormValidator.prototype.newFieldRule 		= FormValidator_newFieldRule;
FormValidator.prototype.addFieldRule 		= FormValidator_addFieldRule;
FormValidator.prototype.validateForm 		= FormValidator_validateForm;
FormValidator.prototype.fieldRulesOff		= FormValidator_fieldRulesOff;

// ~~~~~~ Class: FormValidator - Define method functions ~~~~~~~~~~~~~~~~~~~~~~~~
function FormValidator_toString() {
	var str = "'Form Validator' object dump:\n";
	for ( var n in this ) { 
		var propValue = this[n];
		if ( propValue != null && propValue.constructor == Function ) continue;
		str += "  " + n + ": " + propValue + "\n"; }
	return str;
}

function FormValidator_validateForm() {
	FV_trimFieldValueSpaces(this.form);
	if ( !FormValidator.ON ) return true;
	if ( !this.fldSetupOK ) {
		var alStr = "--- ALERT ---\nFrom: 'Validate Form' routine.\n";
		alStr += "Because of error(s) within the 'Setup Fields' initialization routine, ";
		alStr += "the form cannot be validated here in the browser.\n";
		alStr += "However, you can still submit the form by clicking <OK>; ";
		alStr += "otherwise, click <Cancel>.";
		return confirm(alStr);
	}
	if ( !FV_checkBlanks(this.form) ) return false;
	if ( !FV_checkFieldValueChars(this.form) ) return false;
	if ( !FV_checkFieldValueRules(this.form) ) return false;
	return true;
}

function FormValidator_newFieldRule(ruleType) {
	var fvr = new FieldValueRule(ruleType);
	fvr.form = this.form;
	return fvr;
}

// For a given field 'fieldName' in form 'f', set the 'rule' property (of the
// field's Field Data Object) to the FieldValueRule Object 'ruleObj'; also, 
// set the 'doRuleCheck' property to 'true' automatically.  This is done on the
// reasonable assumption that if a rule is being added to a field (particularly if
// it's the first rule being added), then you probably want to do the rule check
// on the field value.
function FormValidator_addFieldRule(fieldName,ruleObj) {
	if ( this.form.fldDataObjs == null ) return;
	if ( ruleObj.constructor != Array ) ruleObj = new Array(ruleObj);
	var fieldDataObj = this.form.fldDataObjs[fieldName];
	if ( !fieldDataObj  ) {
		alert("ERROR in 'Add Rule to Field':\nAttempting to add rule on field '" + fieldName + "'.\nField does not exist.");
		return;
	}
	if ( !fieldDataObj.rule ) fieldDataObj.rule = new Array;
	for ( var i=0; i<ruleObj.length; i++ ) {
		fieldDataObj.rule[fieldDataObj.rule.length] = ruleObj[i];
	}
	//setFieldProperty(f,fieldName,"rule",ruleObj);
	this.setFieldProperty(fieldName,"doRuleCheck",true);
}

function FormValidator_fieldRulesOff() {
	var fldDataObjs = this.form.fldDataObjs;
	if ( fldDataObjs == null ) return;
	for ( var i in fldDataObjs ) {
		this.setFieldProperty( fldDataObjs[i].name,"doRuleCheck",false );
	}
}

// Dump field data objects
// If given string argument of field name, show just that field data object
// otherwise show all Field Data Objects sequentially in separate alert messages.
// (Note: all Field Data Objects reside in a 'container', i.e. an array stored
// as the value of the form 'f' property 'fldDataObjs'.)
function FormValidator_dumpFDO(fieldName) {
	if ( this.form.fldDataObjs == null ) return;
	var str = "'Field Data' object dump:\n";
	if ( fieldName ) {
		if ( this.form.fldDataObjs[fieldName] == null ) {
			alert(str+"Field Data object not found for field: " + fieldName +"\nField does not exist.");
			return;
		}
		alert( "'Form Validator' instance method:\nDump 'Field Data Object' for field: " + fieldName + "\n\n" + this.form.fldDataObjs[fieldName] );
		//for ( var i in this.form.fldDataObjs[fieldName] ) {
		//	str += "  " + i + ": " + this.form.fldDataObjs[fieldName][i] + "\n";
		//}
		//alert(str);
		return;
	}
	for ( var n in this.form.fldDataObjs ) {
		alert( "'Form Validator' instance method:\nDump 'Field Data Objects',\none object per alert screen:\n\n" + this.form.fldDataObjs[n] );
		//for ( var i in this.form.fldDataObjs[n] ) {
		//	str += "  " + i + ": " + this.form.fldDataObjs[n][i] + "\n";
		//}
		//alert(str);
		//str = "'Field Data' object dump:\n";
	}
}
// For a given form f, field name, property name, and property value,
// get the Field Data Object for the field, and set its 'property name' to 'property value'.
function FormValidator_setFieldProperty(fieldName,propName,propValue) {
	if ( this.form.fldDataObjs == null ) return;
	if ( ! this.form.fldDataObjs[fieldName] ) {
		alert("ERROR in 'Set Field Property':\nAttempting to set property '" + propName + "' on field '" + fieldName + "'.\nField does not exist.");
		return;
	}
	this.form.fldDataObjs[fieldName][propName] = propValue;
}

// For a given form f, field name, and property name,
// get the Field Data Object for the field, and
// return the value of its 'property name'.
function FormValidator_getFieldProperty(fieldName,propName) {
	if ( this.form.fldDataObjs == null ) return;
	if ( ! this.form.fldDataObjs[fieldName] ) {
		alert("ERROR in 'Get Field Property':\nAttempting to get property '" + propName + "' on field '" + fieldName + "'.\nField does not exist.");
		return;
	}
	return this.form.fldDataObjs[fieldName][propName];
}
// ~~~ Class: FormValidator - END method function definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// ~~~~~~ Class: FormValidator - Define private helper/utility functions ~~~~~~~~~~~~~~~~~~~~~~~~
// Called in 'validateForm()'
function FV_trimFieldValueSpaces(f) {
	for ( var i=0; i<f.elements.length; i++ ) {
		var field = f.elements[i];
		if ( (field.type != "text") && (field.type != "textarea ") ) continue;
		field.value = FV_trimLeadingSpaces(field.value);
		field.value = FV_trimTrailingSpaces(field.value);
	}
}

function FV_trimLeadingSpaces(str) {
	if ( !FV_isblank( str.charAt(0) ) ) return str;
	var strLength = str.length;
	var i;
	for ( i=0; i<strLength; i++ ) {
		if ( FV_isblank( str.charAt(i) ) ) continue;
		break;
	}
	return str.substring(i,strLength);
}

function FV_trimTrailingSpaces(str) {
	var strLength = str.length;
	if ( !FV_isblank( str.charAt(strLength-1) ) ) return str;
	for ( var i=(strLength-1); i > -1; i-- ) {
		if ( FV_isblank( str.charAt(i) ) ) continue;
		break;
	}
	return str.substring(0,(i+1));
}

function FV_isblank(str) {
	if ( str == null ) return true;
	for ( var i=0; i<str.length; i++ ) {
		var c = str.charAt(i);
		if ( ( c != ' ' ) && ( c != '\n' ) && ( c != '\t' ) ) return false;
	}
	return true;
}

function FV_checkBlanks(f) {
	var emptyFound = false;
	var fieldDataObj;
	var fieldObj;
	var fieldObjLabel; // used below in alert message
	var fieldType;
	for ( var i in f.fldDataObjs ) {
		fieldDataObj = f.fldDataObjs[i];
		// If the field is not required, go on to next field data object in loop
		if ( !fieldDataObj.reqd ) continue;
		fieldObj = fieldDataObj.field;
		fieldObjLabel = fieldDataObj.label;
		fieldType = fieldDataObj.type;
		if ( fieldDataObj.isBlank() ) {
			emptyFound = true;
			break;
		}
	}
	if ( emptyFound ) {
		alert(fieldDataObj.emptyFieldAlertPrefix + fieldObjLabel);
		if ( (fieldType != "checkbox") && (fieldType != "radio") ) fieldObj.focus();
		return false;
	}
	return true;
}



function FV_checkFieldValueChars(f) {
	var errorFound = false;
	var fieldDataObj;
	var charChecker;
	var fieldDataObj;
	for ( var i in f.fldDataObjs ) {
		fieldDataObj = f.fldDataObjs[i];
		if ( !fieldDataObj.doCharCheck ) continue;
		charChecker = fieldDataObj.charChecker || f.charChecker;
		if ( !charChecker ) continue;
		if ( !charChecker.validate(fieldDataObj.field.value) ) {
			errorFound = true;
			break;
		}
	}
	if ( errorFound ) {
		alert( "Invalid value entered.\nField: " + fieldDataObj.label + "\nAllowable characters are:\n" + charChecker.allowDescStr );
		// Note: Method 'focus()' only supported by form elements of type 'text' and
		// 'textarea'; not checking field type here since 'doCharCheck' is a property
		// set on field data objects of type 'text' only; thus, invoking the 'focus()' method
		// on the field here is guaranteed to work, i.e. it will not result in a runtime error.
		fieldDataObj.field.focus();
		return false;
	}
	return true;
}

function FV_checkFieldValueRules(f) {
	var errorFound = false;
	var ruleObj;
	var ruleErrorStr;
	var fieldDataObj;
	for ( var i in f.fldDataObjs ) {
		
		fieldDataObj = f.fldDataObjs[i];
		if ( !fieldDataObj.doRuleCheck ) continue;
		
		ruleObj = fieldDataObj.rule;
		// If no rule set for field, move to next field data object in loop
		if ( ruleObj == null ) continue;
		if ( ruleObj.constructor != Array ) ruleObj = new Array(ruleObj);
		
		var numRules = ruleObj.length;
		for ( var j=0; j<numRules; j++ ) {
		
			// If current ruleObj element null, move on to next next element in rulObj array.
			if ( ruleObj[j] == null ) {
				alert("Checking rule " + (j+1) + " of " + numRules + " for field: " + fieldDataObj.label + "\nNo 'Rule Object' to examine.");
				continue;
			}
			
			// Check that current element is object of type FieldValueRule
			// Alert if not and go on to next element in rulObj array..
			if ( ruleObj[j].constructor != FieldValueRule ) {
				alert("Checking rule " + (j+1) + " of " + numRules + " for field: " + fieldDataObj.label + "\nThis rule is not a 'Field Value Rule' object.\nThis 'rule' will therefore be ignored.");
				continue;
			}
			
			if ( ruleObj[j].type == "basic" ) {
				
				// If current ruleObj element's 'rule' is null, move on to next next element in rulObj array.
				if ( !ruleObj[j].rule || ruleObj[j].rule == null ) {
					alert("Checking rule " + (j+1) + " of " + numRules + " for field: " + fieldDataObj.label + "\nRule Object's 'rule' property not defined.");
					continue;
				}
				
			}
			
			if ( ruleObj[j].type == "uniquelist" ) {
				
				// Verify that the list being compared against exists in the form
				var uniqueListFieldObj = f[ruleObj[j].uniqueList];
				if ( uniqueListFieldObj == null ) {
					var fieldLabel = fieldDataObj.label || fieldDataObj.name;
					alert("'Check Field Value Rules' error: " +
						  "\nAttempting to test uniqueness rule on field: " + fieldLabel +
						  "\nProblem with the 'select-list' field that's being tested against: " +
						  "\n  Field label: " + ruleObj[j].uniqueListLabel +
						  "\n  Field name: " + ruleObj[j].uniqueList +
						  "\nField not defined in form, so 'unique-in-list' rule cannot be tested or enforced."
						  );
					continue;
				}
				
			}
			
			// 'checkAlways' is a boolean property that determines if the rule check
			// is run on the field depending on the field value being blank or not.
			// The default value is 'false', so that the rule check is run if and
			// only if the field value is not blank. This allows for value rule checks
			// on non-required fields.  If set to 'true', then the rule check is
			// always run, regardless of the field value: blank or not-blank.  This
			// allows for setting up a rule, for example, that makes the field
			// required based on the value of another field in the form, e.g. the value
			// of a select list may determine whether a text field is required or not.
			if ( !ruleObj[j].checkAlways && fieldDataObj.isBlank() ) continue;
			
			// If the field value obeys the rule, great; go on to next rule
			if ( ruleObj[j].validValue(fieldDataObj.field) ) continue;
			
			// If the field value does not obey the rule, then we end up here.
			errorFound = true;
			ruleErrorStr = ruleObj[j].errStr;
			break;
		}
		// break out of 'outer' loop if we broke out of 'inner' loop due to error
		if ( errorFound ) break;
	}
	if ( errorFound ) {
		ruleErrorStr = ruleErrorStr || "(Text describing rule not defined for this field.)";
		alert( "Invalid value entered.\nField: " + fieldDataObj.label + "\nRule: " + ruleErrorStr );
		if ( (fieldDataObj.type != "checkbox") && (fieldDataObj.type != "radio") ) fieldDataObj.field.focus();
		return false;
	}
	return true;
}

function FV_setupFields(f,fields,fieldLabels,reqFields,fldNamePrefix,showAlerts,setDoCharCheckOnFields) {

	if ( fields.constructor != Array ) fields = new Array(fields);
	if ( fieldLabels.constructor != Array ) fieldLabels = new Array(fieldLabels);
	
	// Be sure reqFields is an array
	if ( reqFields != null ) { // i.e. js code: " var reqFields = ''; "
		if ( reqFields.constructor != Array ) { 
			if ( reqFields ) { // i.e. js code: " var reqFields = 'fname'; "
				reqFields = new Array(reqFields); // makes reqFields an array of length 1
			} else { // i.e. js code: " var reqFields = ''; "
				reqFields = new Array; // makes reqFields an array of length 0
			}
			// Note on above 'if' block: if we had js code " var reqFields = ''; " and the above
			// block was simply 'reqFields = new Array(reqFields)', then reqFields would be an array
			// of size 1, with the single element being ''.  As a result, all the 'propert-setting' functions
			// coming up below that use 'reqFields' would cause errors since the field objects hash they each use
			// would select an undefined field object and try to access properties on it.
			// Otherwise, we'd have to modify each of the 'property-setting' functions to check
			// that each field data object is not null (tedious); this way is cleaner.
		}
	} else { // i.e. js code: " var reqFields; "
		reqFields = new Array; // makes reqFields an array of length 0
	}
	
	// Prepend 'field name prefix' to each field name in 'fields'
	if ( fldNamePrefix != null ) {
		for ( var i=0;i<fields.length;i++) { fields[i] = fldNamePrefix + fields[i]; }
	}
	
	// Prepend 'field name prefix' to each field in 'reqFields' ONLY IF 'reqFields'
	// not equal to 'fields', i.e. if reqFields does not contain same reference to array
	// as 'fields'.
	if ( fields != reqFields && fldNamePrefix != null ) {
		for ( var i=0;i<reqFields.length;i++) { reqFields[i] = fldNamePrefix + reqFields[i]; }
	}
	
	// Make default value of setDoCharCheckOnFields 'true'
	if ( setDoCharCheckOnFields == null ) setDoCharCheckOnFields = true;
	
	// Container for the Field Data Objects created in next loop.
	// Implemented as a hash, where each key is the
	// name of the field, and the value is the field's
	// associated 'fieldData' object
	var fldDataObjs = new Object(); 
	
	// Create FieldData objects for each field
	var fldSetupOK = true;
	for ( var i=0; i<fields.length; i++ ) {
		var formFieldObj = eval( "f." + fields[i] ); // field object
		if ( formFieldObj == null ) {
			alert("ERROR in 'Setup Fields':\n'Field Data Object' cannot be created on a non-existent form field.\nField does not exist: " + fields[i]);
			fldSetupOK = false;
			break;
			//return false;
		}
		// For each formFieldObject, create an associated 'FieldDataObject' object,
		// and store it in the fldDataObjs container hash. The 'FieldDataObject' lets
		// us create and store data (or, 'metadata') about each field object; the other
		// option of creating and setting properties directly on the form field objects
		// creates run-time problems, especially in Netscape Navigator
		fldDataObjs[fields[i]] = new FieldDataObject(formFieldObj);
	}
	if ( !fldSetupOK ) return false;
	// Store the Field Data Objects container in
	// a new form property 'fldDataObjs' for access later
	f.fldDataObjs = fldDataObjs;
								
	
	// Attach field labels to field data objects
	if ( fields.length != fieldLabels.length ) {
		alert("ERROR alert from 'Attach Field Labels':\nPossible field name/label mismatch\nsince the 'fields' array and the 'labels' array\nare not the same size.\nAs a result, fields will not be labelled.");
	} else {
		FV_attachFieldLabels(f,fields,fieldLabels);
	}
	
	// Mark the required fields as 'required' by creating a 'reqd'
	// property in each field object and setting its value to 'true'
	FV_tagReqFields(f,reqFields);

	// Attach 'empty field alert prefixes' to field data objects
	FV_attachEmptyFieldAlertPrefix(f,reqFields);

	// Set 'doCharCheck' property of all fields in the 'reqFields' array
	// that are of type 'text'; this eliminates the need for manually setting this
	// property with 'endless' lines like:
	// 		f.field1.doCharCheck 	= true;
	//		f.field2.doCharCheck 	= true;
	//		... etc.
	if ( setDoCharCheckOnFields == true ) FV_tagDoCharCheckFields(f,fields);
	
	// Set 'charChecker' property of the form to the default
	// Character Checker ( default CharChecker object constructor called
	// here with no arguments, thus defaulting to CharChecker class defaults)
	// Also, for any field that has its 'doCharCheck' property set (i.e. set
	// to 'true') AND does not have its own 'charChecker' property set with
	// an instance of the CharChecker class, will inherit this CharChecker object
	// of its container form.
	f.charChecker = new CharChecker;
	
	// Debugging tool:
	// If 'showAlerts' switch (argument) set to 'true',
	// dump and join each Field Data Object on a given property to show results
	// of each major property-setting function performed above.
	if ( showAlerts == true ) {
		var properties = new Array('label','reqd','emptyFieldAlertPrefix','doCharCheck');
		for ( var i=0;i<properties.length;i++) {
			FV_showFieldsByProp(f,properties[i]);
		}
		alert(f.charChecker.allowStr);
	}
	return true;
}


// 'Attach' human-readable user-friendly labels to fields
// by creating 'label' property and setting its value to a corresponding value
// in argument 'fieldLabels'.
// Args: 	f				The form containing the fields
// 			fields			Array of field names in form f, where each
// 							field name is one we want to label
// 			fieldLabels		Array of labels, in which each element corresponds
// 							to the element in the 'fields' array; e.g.
// 							label 'fieldLabel[i]' is the label for field 'fields[i]'
function FV_attachFieldLabels(f,fields,fieldLabels) {
	if ( fields.length != fieldLabels.length ) {
		alert("ERROR alert from 'Attach Field Labels':\nPossible field name/label mismatch\nsince the 'fields' array and the 'labels' array\nare not the same size.\nAs a result, fields will not be labelled.");
		return;
	}
	// Note: f has 'fldDataObjs' property set to array 'fldDataObjs':
	//  e.g. identity: f.fldDataObjs = fldDataObjs;
	for ( var i=0; i<fields.length; i++ ) {
		var fldDataObj = f.fldDataObjs[fields[i]];
		fldDataObj.label = fieldLabels[i];
	}
}

// 'Tag' fields by creating 'reqd' property and setting its value to 'true'
// Args: 	f				The form containing the fields
// 			reqFields		Array of field names in form f, where each
// 							field name is a required field
function FV_tagReqFields(f,reqFields) {
	// Note: f has 'fldDataObjs' property set to array 'fldDataObjs':
	//  e.g. identity: f.fldDataObjs = fldDataObjs;
	for ( var i=0; i<reqFields.length; i++ ) {
		var fldDataObj = f.fldDataObjs[reqFields[i]];
		fldDataObj.reqd = true;
	}
}

// This 'prefix' is used to build the complete 'missing value' alert message
// when a required field is found to be blank. The message begins with this
// prefix and is finished with the value of the field object 'label' property
function FV_attachEmptyFieldAlertPrefix(f,reqFields) {

	var textboxAlert 		= "Please enter a value for: ";				 // For text/textarea boxes
	var listAlert 			= "Please select a value for: ";			 // For select lists (single or multiple)
	var multiCheckboxAlert 	= "Please select at least one option for: "; // For multiple checkbox/radio button input fields that have same name
	
	// Note: f has 'fldDataObjs' property set to array 'fldDataObjs':
	//  e.g. identity: f.fldDataObjs = fldDataObjs;
	for ( var i=0; i<reqFields.length; i++ ) {
		var fldDataObj = f.fldDataObjs[reqFields[i]];
		var fieldObj = fldDataObj.field;
		var fieldType = fldDataObj.type;
		
		if ( ( fieldType == "text" ) || ( fieldType == "textarea" ) || ( fieldType == "password" ) )
			fldDataObj.emptyFieldAlertPrefix = textboxAlert;

		if ( ( fieldType == "select-one" ) || ( fieldType == "select-multiple" ) )
			fldDataObj.emptyFieldAlertPrefix = listAlert;

		if ( (fieldType == "checkbox") || (fieldType == "radio") )
			fldDataObj.emptyFieldAlertPrefix = multiCheckboxAlert;
	}
}

// 'Tag' fields by creating 'doCharCheck' property and setting its value to 'true'
// Do this ONLY on fields of type 'text'.
// Args: 	f				The form containing the fields
// 			reqFields		Array of field names in form f, where each
// 							field name is a required field
function FV_tagDoCharCheckFields(f,reqFields) {
	// Note: f has 'fldDataObjs' property set to array 'fldDataObjs':
	//  e.g. identity: f.fldDataObjs = fldDataObjs;
	for ( var i=0; i<reqFields.length; i++ ) {
		var fldDataObj = f.fldDataObjs[reqFields[i]];
		if ( (fldDataObj.type == "text") || ( fldDataObj.type == "password" ) ) fldDataObj.doCharCheck = true;
	}
}


// 'Show Fields By Property'
// Debugging aid.
// For a given form object 'f' and field property name 'fldProp',
// returns an alert message where each line gives the
// field name and the value of the given property; if property value is
// not defined, then the value shown is 'undefined'. If a field is not
// named, then the default string '(Undefined field name)' is used.
// Args: 	f				The form containing the fields
// 			property		Name of the field object property to report on for
// 							each field in the form f

// ***!!! 
// Can be called as a function, or
// invoked as a method 'showFieldsByProp()' on a FormValidator object
// !!! ***
function FV_showFieldsByProp(f,fldProp) {
	// 'Redefine' arguments if function called as an instance method
	// Note: if 'f' is reference to form object as when function called as
	// a function, then 'f.constructor' is 'undefined'; so, if it IS defined,
	// then 'f' must refer to an instance of the 'FormValidator' class, so the
	// function arguments need to be redefined:
	if ( f.constructor ) {
		fldProp = f;
		f = this.form;
	}
	// --- End instance method massaging ---------------------
	if ( !fldProp ) return;
	var msg = "Fields by property '" + fldProp + "'\n";
	for ( var i=0; i<f.elements.length; i++ ) {
		var fieldName = f.elements[i].name;
		if ( !f.fldDataObjs[fieldName] ) continue;
		fieldName = fieldName || "(Undefined field name)";
		var propVal = f.fldDataObjs[fieldName][fldProp] || f.elements[i][fldProp];
		msg += "  " + fieldName + " - " + propVal + "\n";
	}
	alert(msg);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	End class definition: FormValidator
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~






//--------------------------------------------------------------------------------






//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Class: CharChecker -- character checker
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

CharChecker.defAllowStr 	= "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
CharChecker.defAllowDescStr = "letters [a-zA-Z]\ndigits [0-9]\nhyphen [-]\nunderscore [_]";
// ~~~ Constructor method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function CharChecker( allowableCharsStr, allowableDescStr ) {
	this.allowStr 		= allowableCharsStr || CharChecker.defAllowStr;
	this.allowDescStr 	= allowableDescStr || CharChecker.defAllowDescStr;
}

// Create 'throw-away' CharChecker object to create prototype object for the class
// (bug fix for Navigator 3).
new CharChecker;

// Set prototype methods
CharChecker.prototype.validate 			= CharChecker_validate;
CharChecker.prototype.toString			= CharChecker_toString;
CharChecker.prototype.appendAllowChars 	= CharChecker_appAllowChars;
CharChecker.prototype.appendAllowDesc 	= CharChecker_appAllowDesc;

// ~~~~~~ Define method functions ~~~~~~~~~~~~~~~~~~~~~~~~
function CharChecker_toString() {
	var str = "'CharChecker' object dump:\n";
	for ( var n in this ) { 
		var propValue = this[n];
		if ( propValue != null && propValue.constructor == Function ) continue;
		//if ( n == "validValue" ) continue;
		str += "    " + n + ": " + propValue + "\n"; }
	return str;
}

function CharChecker_validate(str) {
	if ( str == null ) return;
	var validValue = true;
	// Next line: modStr not used in this implementation; could be used
	// in alert msge to user in which the original value 'str' is referenced
	//var modStr = str.toLowerCase(); // modified string, preserve original
	for ( var i=0; i<str.length; i++ ) {
		if ( this.allowStr.indexOf( str.charAt(i) ) == -1 ) {
			validValue = false;
			break;
		}
	}
	if ( !validValue ) return false;
	return true;
}
// Append allowable characters string with characters given in arg 'str'
function CharChecker_appAllowChars(str) {
	this.allowStr += str;
}
// Append allowable characters description string with string given in arg 'str'
function CharChecker_appAllowDesc(str) {
	this.allowDescStr += str;
}
// ~~~ END method function definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	End class definition: CharChecker
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~






//--------------------------------------------------------------------------------






//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Class: FieldValueRule 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// ~~~ Constructor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function FieldValueRule(type) {
	// type: basic (default), uniquelist, function, (others to be defined)
	this.type = type || 'basic';
	this.rule = '';
	this.checkAlways = false;
	this.errStr = '';
}
// Create 'throw-away' FieldValueRule object to create prototype object for the class
// (bug fix for Navigator 3).
new FieldValueRule();

// Set prototype methods
FieldValueRule.prototype.toString = FieldValueRule_toString;
FieldValueRule.prototype.validValue = FieldValueRule_validValue;

// ~~~~~~ Define method functions ~~~~~~~~~~~~~~~~~~~~~~~~
function FieldValueRule_toString() {
	var str = "'Field Value Rule' object dump:\n";
	for ( var n in this ) { 
		var propValue = this[n];
		if ( propValue != null && propValue.constructor == Function ) continue;
		//if ( n == "validValue" ) continue;
		str += "    " + n + ": " + propValue + "\n"; }
	return str;
}

function FieldValueRule_validValue(fieldObj) {
	var value = fieldObj.value;
	if ( !this.caseSensitive && value != null ) value = value.toLowerCase();
	var isValidValue = true;
	
	// ~~ Rule type 'basic' ~~
	if ( this.type == "basic" ) {
		//alert(this.rule);
		return eval( this.rule );
	}
	
	// ~~ Rule type 'function' ~~
	if ( this.type == "function" ) return eval( this.rule(value) );
	
	// ~~ Rule type 'uniquelist' ~~
	if ( this.type == "uniquelist" ) {
		var uniqueListFieldName = this.uniqueList;
		var uniqListFieldObj = this.form[uniqueListFieldName];
		var optionObjProp = this.listOptionProp || "value"; // 'value' (default) or 'text'
		//f.dogname.ruleUniqueList = "dogs";
		//f.dogname.ruleListOptionProp = "value";
		value = FVR_escapeChar(value);
		var excludeValue = FVR_escapeChar(this.excludeTestValue);
		for ( var j=0; j < uniqListFieldObj.options.length; j++ ) {
			var listItemValue = uniqListFieldObj.options[j][optionObjProp];
			if ( !this.caseSensitive && value != null ) listItemValue = listItemValue.toLowerCase();
			
			listItemValue = FVR_escapeChar(listItemValue);
			// LHS: 'left hand side' of subRule expression
			// Note: using "\"" instead of "'" since FVR_escapeChar() escapes " and not '
			var LHS = "\"" + listItemValue + "\"";
			
			// Note: using " as string delimiter and not ' since FVR_escapeChar()
			// escapes " and not '
			if ( eval( LHS + " == \"" + excludeValue + "\"" ) ) continue;
			
			// Note: using " as string delimiter and not ' since FVR_escapeChar()
			// escapes " and not '
			var subRule = LHS + " != \"" + value + "\"";
			
			// Debugging--------
			//  alert(subRule+"\n"+eval(subRule)); 
			//  alert(LHS);
			
			if ( eval( subRule ) ) continue;
			isValidValue = false;
			break; // break out of this 'inner' loop
		}
		return isValidValue;
	}
	// ~~ End rule type 'uniquelist' ~~
	
	return true;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 	FieldValueRule helper function(s)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// FVR_escapeChar:
// Escapes backslash (\) and double-quote (") with backslash
//	Synopsis:
// 		aStr = escapeChar(aStr);
//		var escStr = escapeChar(str);
//	Arg:
//		string
//	Returns:
//		String with " and \ escaped with normal JavaScript escape sequence:
//		e.g. thi\s ... becomes this: thi\\s
//		e.g. t"hi"s ... ecomes this: t\"hi\"s
//		e.g. t"h\i"s ... ecomes this: t\"h\\i\"s
// Note: this obviously could be implemented using regex's, but we're assuming
// browsers with JavaScript versions less than 1.2 in which regex's are not supported.
function FVR_escapeChar(str) {
	if ( str == null ) return str;
	var escThese = new Array('\\','"');
	var escStr = ''; // escaped string; each char of 'str' is stuffed into this
	for ( var i=0; i<escThese.length; i++ ) {
		for ( var j=0; j<str.length; j++ ) {
			var c = str.charAt(j);
			if ( c == escThese[i] ) {
				//alert("match");
				escStr += '\\' + c;
			} else {
				escStr += c;
			}
		}
		str = escStr; // store transformed string back into string for next loop iteration
		escStr = ''; // reset escStr for next loop iteration
	}
	return str;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 	End FieldValueRule helper function(s)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	End class definition: FieldValueRule
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~






//--------------------------------------------------------------------------------






//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Class: FieldDataObject
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// ~~~ Constructor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function FieldDataObject(fieldObj) {
	//if ( arguments.length == 0 ) return; // takes care of 'throw-away' object created below for Nav3 bug
	if ( fieldObj == null ) return;
	this.field = fieldObj;
	
	/*	Note: If a field object represents
		two or more input elements with the same name, i.e. an 'array' of
		checkboxes/radiobuttons with same name, then its properties such
		as 'name', 'type', etc. are undefined. To access these properties,
		need to look at one of the actual elements in the field element array;
		it's guaranteed that there's at least one element in these cases, so
		look at that first element to grab the field properties we need.
	*/
	this.name = fieldObj.name || fieldObj[0].name;
	this.type = fieldObj.type || fieldObj[0].type;
}
// Create 'throw-away' FieldDataObject object to create prototype object for the class
// (bug fix for Navigator 3).
new FieldDataObject;

// Set prototype methods
FieldDataObject.prototype.toString = FieldDataObject_toString;
FieldDataObject.prototype.isBlank = FieldDataObject_isBlank;

// ~~~~~~ Define method functions ~~~~~~~~~~~~~~~~~~~~~~~~
function FieldDataObject_toString() {
	var str = "'Field Data Object' object dump:\n";
	for ( var n in this ) { 
		var propValue = this[n];
		if ( propValue != null && propValue.constructor == Function ) continue;
		str += "    " + n + ": " + propValue + "\n"; }
	return str;
}

function FieldDataObject_isBlank() {
	var isBlank = false; // status/result variable to return
	var fieldObj = this.field;
	var fieldType = this.type;
	if ( ( fieldType == "text" ) || ( fieldType == "textarea" ) || ( fieldType == "password" ) ) {
		return FDO_isblank( fieldObj.value );
	}
	if ( ( fieldType == "select-one" ) || ( fieldType == "select-multiple" ) ) {
		if ( fieldObj.selectedIndex == -1 ) {
			isBlank = true;
		} else {
			for( var j=0; j<fieldObj.options.length; j++ ) {
				if ( fieldObj.options[j].selected && FDO_isblank( fieldObj.options[j].value ) ) {
					isBlank = true;
					break;
				}
			}
		}
	}
	if ( (fieldType == "checkbox") || (fieldType == "radio") ) {
		if ( fieldObj.length  ) {
			isBlank = true;
			for ( var j=0; j<fieldObj.length; j++ ) {
				if ( !fieldObj[j].checked ) continue;
				isBlank = false;
				break;
			}
		} else {
			if ( !fieldObj.checked ) isBlank = true;
		}
	}
	return isBlank;
}
// ~~~~~~ End method function definitions ~~~~~~~~~~~~~~~~~~~~~~~~

// Private function used in this class; called in method FieldDataObject_isBlank()
// Argument: scalar string value
// Returns: true if string is blank, empty, etc.; false otherwise
function FDO_isblank(str) {
	if ( str == null ) return true;
	for ( var i=0; i<str.length; i++ ) {
		var c = str.charAt(i);
		if ( ( c != ' ' ) && ( c != '\n' ) && ( c != '\t' ) ) return false;
	}
	return true;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	End class definition: FieldDataObject
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



