/**
 * FormUtilities v2.0
 * A reusable, object-oriented and unobtrusive approach to form maniuplation and validation.
 * @since 15/05/2006
 * v2.0 - added new test for at least one field
 */
function FormUtilities (formId) {
	this.formId = formId;
	this.obj = formId + "FormUtilities";
	eval(this.obj + "=this;");
	this.validations = [];
	this.validationCounter = 0;
	
	// whether inputs should be disabled along with adding the disabled class.
	this.disableDependantInputs = true;
	this.charactersLeftText = 'characters left';
	
	this.config = {
		'dependsClass': 'disabled'
	}
	
	this.init = function ()
	{
		// add validation handler to form
		formObj = this.findObj(this.formId);
		if (typeof(formObj) != 'object') return false;
		actionStr = "return "+this.serialise(this.obj)+".runValidation();";
		eval("action = function () { "+actionStr+" };");
		formObj.onsubmit = action;
		
		return true;
	}
	
	/**
	 * Adds a maximum character limit to a textarea or text input, and counts down the 
	 * number of characters left.
	 */
	this.addCounter = function (inputElementId, characterLimit)
	{
		if (!document.createElement) return false;
		inputElement = this.findObj(inputElementId);
		if (!inputElement) return false;
		counterInputId = inputElementId+'-counter';
		
		
		// create input
		counterInput = document.createElement('input');
		counterInput.setAttribute('readonly','readonly');
		
		// IE doesn't respect readonly, feed it disabled also
		// We don't want Opera to receive disabled attribute though, otherwise field is greyed out.
		isIE = window.ActiveXObject ? true : false; // ActiveX is only used in Internet Explorer
		if (isIE) {
			counterInput.setAttribute('disabled','disabled');
		}
		counterInput.setAttribute('id',counterInputId);
		counterInput.setAttribute('size', 4);
		counterInput.setAttribute('value', characterLimit);
		//counterInput.value = characterLimit;
		
		// create label
		counterText = document.createTextNode(this.charactersLeftText);
		counterLabel = document.createElement('label');
		counterLabel.setAttribute('for',counterInputId);
		counterLabel.appendChild(counterText);
		
		
		// create formelement group
		formElementGroup = document.createElement('div');
		formElementGroup.setAttribute('class','counter-group');
		
		// create counter group
		counterGroup = document.createElement('div');
		counterGroup.setAttribute('class','counter');
		// add input and label to group
		counterGroup.appendChild(counterInput);
		counterGroup.appendChild(counterLabel);
		
		
		// get clone of input/textarea
		clonedInput = inputElement.cloneNode(true);
		
		
		// add cloned input/textarea to group
		formElementGroup.appendChild(clonedInput);
		// add countergroup to formelementgroup
		formElementGroup.appendChild(counterGroup);
		
		// replace input/textarea with group
		inputElement.parentNode.replaceChild(formElementGroup, inputElement);
		
		
		// apply action to cloned input
		actionStr = "return "+this.serialise(this.obj)+".limitCharacters('"+inputElementId+"','"+characterLimit+"','"+counterInputId+"');";
		eval("action = function () { "+actionStr+" };");
		clonedInput.onchange = action;
		clonedInput.onkeyup = action;
		
		// page should be loaded, just call action now
		action();
		
		return true;
	}
	
	this.limitCharacters = function (inputElementId, characterLimit, updateCounterInputId)
	{
		inputElement = this.findObj(inputElementId);
		if (!inputElement) return false;
		
		
		if (inputElement.tagName == 'TEXTAREA' || (inputElement.tagName == 'INPUT' && inputElement.type == 'text')) {
			currentCharCount = inputElement.value.length;
		} else { // not a textarea or text input.... panic!!
			return false;
		}
		
		charsLeft = this.getCharactersLeft(inputElementId, characterLimit);
		
		if (charsLeft < 0) {
			inputElement.value = inputElement.value.substr(0, characterLimit);
			charsLeft = this.getCharactersLeft(inputElementId, characterLimit);
			alert('This field is limited to '+characterLimit+' characters.');
		}
		
		updateCounterInput = this.findObj(updateCounterInputId);
		if (!updateCounterInput) return true;
		
		updateCounterInput.value = charsLeft;
		return true;
	}
	
	this.getCharactersLeft = function (elementId, characterLimit)
	{
		element = this.findObj(elementId);
		if (!element) return false;
		
		if (element.tagName == 'TEXTAREA' || (element.tagName == 'INPUT' && element.type == 'text')) {
			currentCharCount = element.value.length;
		} else { // not a textarea or text input.... panic!!
			return 0;
		}
		
		return parseInt(characterLimit) - parseInt(currentCharCount);
	}
	
	/**
	 * Passes an array of validation objects through to this.addValidation()
	 * @return boolean Returns true if all validations were added successfully,
	 *         returns false if any of the validations failed.
	 */
	this.addValidations = function (validations)
	{
		if (! this.is_array(validations)) return false;
		success = true;
		for (id in validations) {
			success = (this.addValidation (validations[id]))?success:false;
		}
		return success;
	}
	
	/**
	 * Stores validation processes to be executed upon submitting the form.
	 * @param object validation A javascript object containing the following class variables:
	 *        'test': A boolean statement that evaluates to true if the validation is passed.
	 *        'message': A message to relay to the user if validation failed.
	 *        'return-to-id': The id of a HTML element to return to if the validation failed.
	 * @return boolean Success indicator
	 */
	this.addValidation = function (validation)
	{
		if (typeof(validation) != 'object') return false;
		this.validations[this.validationCounter++] = validation;
		return true;
	}
	
	
	/**
	 * Add event handlers for when one element depends on the selected value of another element
	 * @param array elementsIds An array of the ids of elements that depend on the value of dependsId element.
	 *              These elements will be disabled if the dependency is not met.
	 * @param array triggers An array of the ids of elements that should have event triggers placed on them
	 *              in order to update the state of the dependencies.
	 * @param string dependsId The id of the element whose value will be compared with dependsValue in order to
	 *               determine whether the dependency has been met.
	 * @param string dependsValue A string that needs to math the value the dependsId element in order that the
	 *               the dependency be met and the elements in elementsIds will be enabled.
	 */
	this.addElementDependsOn = function (elementsIds, triggers, dependsId, dependsValue)
	{
		depends = this.findObj(dependsId);
		if (!depends) return false;
		
		actionStr = this.serialise(this.obj)+".testElementDependsOn("+this.serialiseArray(elementsIds)+", '"+this.serialise(dependsId)+"', '"+this.serialise(dependsValue)+"');";
		// alert(actionStr); // debugging
		eval("action = function () { "+actionStr+" };");
		//action = actionStr;
		
		// page should be loaded, just call action now
		action();
		//addEvent(window, 'load', action);
		
		for (id in triggers) {
			temp = this.findObj(triggers[id]);
			if (!temp) continue;
			if (temp.type == 'radio' || temp.type == 'checkbox') {
				temp.onfocus = action; // for IE 6 and 7
				temp.onchange = action; // for Mozilla (and to fix IE 6 and 7)
				// still buggy in IE 6 and 7 if input itself is clicked
			} else {
				temp.onchange = action; // for all browsers
			}
		}
	}
	
	/**
	 * Test if dependant element conditions are met, set the class of the element appropriately
	 */
	this.testElementDependsOn = function (elementsIds, dependsId, dependsValue)
	{
		//alert(elementsIds);
		
		depends = this.findObj(dependsId);
		if (!depends) return false;
		
		match = false;
		
		if (depends.type == 'radio' || depends.type == 'checkbox') {
			match = (depends.checked)?true:false;
		} else {
			if (dependsValue)
				match = (depends.value && depends.value == dependsValue)?true:false;
		}
		for (elementId in elementsIds) {
			element = this.findObj(elementsIds[elementId]);
			if (!element) continue;
			if (match) {
				removeClass (element, this.config.dependsClass);
				if (this.disableDependantInputs) this.enableInnerInputs(element);
			} else {
				addClass (element, this.config.dependsClass);
				if (this.disableDependantInputs) this.disableInnerInputs(element);
			} 
		}
		return true;
	}
	
	this.enableInnerInputs = function (element)
	{
		return this.setInnerElementsProperty(element,'disabled','');
	}
	
	this.disableInnerInputs = function (element)
	{
		return this.setInnerElementsProperty(element,'disabled','disabled');
	}
	
	this.setInnerElementsProperty = function (element,property,value)
	{
		if (!element || !element.getElementsByTagName) return false;
		inputs = element.getElementsByTagName('INPUT');
		selects = element.getElementsByTagName('SELECT');
		textareas = element.getElementsByTagName('TEXTAREA');
		
		if (this.is_array(inputs)) allInputs = inputs;
		if (this.is_array(selects) && allInputs.concat) allInputs = allInputs.concat(selects);
		if (this.is_array(textareas) && allInputs.concat) allInputs = allInputs.concat(textareas);
		
		
		for (id in allInputs) {
			if (allInputs[id])
				eval('allInputs[id].'+property+' = value;');
		}
		return true;
	}
	
	
	this.runValidation = function ()
	{
		alertMessage = '';
		IdToReturnTo = '';
		count = 1;
		for (id in this.validations) {
			// alert(this.validations[id].test); // debugging
			test = (this.validations[id].test)?this.validations[id].test:false;
			if (!test) continue;
			message = (this.validations[id].message)?this.validations[id].message:false;
			if (!message) continue;
			returnToId = (this.validations[id]['return-to-id'])?this.validations[id]['return-to-id']:false;
			if (!returnToId) continue;
			
			
			if (eval("("+test+")")) {
				continue;
			} else {
				alertMessage = alertMessage + "\n  "+(count++)+". " + message ;
				if (IdToReturnTo == '') { // only need the first id
					IdToReturnTo = returnToId;
				}
			}
		}
		if (alertMessage != '') {
			alert("Before the form can be submitted, the following problems need to be addressed:"+alertMessage);
			window.location = '#'+IdToReturnTo; // redirect to first failed validation
			return false;
		} else {
			return true;
		}
	}
	
	/**
	 * returns the validation test code for requiring that a radio group be completed
	 */
	this.radioRequiredTest = function (optionsIds)
	{
		if (! this.is_array(optionsIds) ) return 'true'; // return always true validation
		validationStr = '';
		for (id in optionsIds) {
			if (id != 0) validationStr += ' || ';
			validationStr += "(!this.findObj('"+this.serialise(optionsIds[id])+"') || this.findObj('"+this.serialise(optionsIds[id])+"').checked)"
		}
		return validationStr;
	}
	
	/**
	 * returns the validation test code for requiring that a text field or textarea be completed
	 */
	this.textRequiredTest = function (textFieldId)
	{
		return "this.findObj('"+this.serialise(textFieldId)+"').value != ''";
	}
	
	
	/**
	 * returns the validation test code for requiring that 1 of the specified fields is not empty
	 */
	this.atLeastOneTextRequiredTest = function (textFieldIdArray)
	{
		testStr = '';
		isTheFirst = true;
		for (eachFieldKey in textFieldIdArray) {
			if (!isTheFirst) {
				testStr += ' || ';
			} else {
				isTheFirst = false;
			}
			testStr += this.textRequiredTest(this.serialise(textFieldIdArray[eachFieldKey]));
		}
		return testStr;
	}
	
	
	/**
	 * Returns the validation test code for requiring that a select field be completed.
	 * The default option of the select field needs to have a value of '' for this to work.
	 */
	this.selectRequiredTest = function (selectId)
	{
		return "this.findObj('"+this.serialise(selectId)+"').options[this.findObj('"+this.serialise(selectId)+"').selectedIndex].value != ''";
	}
	
	/**
	 * returns the validation test code for testing if the selected option of a select field has a specified value.
	 */
	this.selectEqTest = function (selectId, value)
	{
		return "this.findObj('"+this.serialise(selectId)+"').options[this.findObj('"+this.serialise(selectId)+"').selectedIndex].value == '"+this.serialise(value)+"'";
	}
	
	
	/**
	 * test whether variable is an array
	 */
	this.is_array = function (variable)
	{
		return (
			typeof(variable) == 'object' &&
			variable.length &&
			typeof(variable.length) != 'undefined'
		) ? true : false ;
	}
	
	/**
	 * Serialise a JavaScript array such that it is safe to include in an eval statement.
	 */
	this.serialiseArray = function (arrayToSerialise)
	{
		if (!arrayToSerialise || !this.is_array(arrayToSerialise)) return '[]'; // return empty array
		returnVal = '[';
		for (member in arrayToSerialise) {
			if (member != 0) returnVal += ', '; 
			returnVal += "'"+this.serialise(arrayToSerialise[member])+"'";
		}
		returnVal += ']';
		return returnVal;
	}
	
	
	/**
	 * Escape any variables that are being serialised.
	 * Escape ' and \
	 */
	this.serialise = function (variable)
	{
		return variable;
		// these replacements are carefully ordered,
		// reorder with care!
		variable.replace("\\","\\\\"); // replace \ with \\
		variable.replace("'","\'"); // replace ' with \'
		return variable;
	}
	
	this.findObj = function (n, d) { //v4.01
	  var p,i,x;  if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
		d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
	  if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
	  for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=this.findObj(n,d.layers[i].document);
	  if(!x && d.getElementById) x=d.getElementById(n); return x;
	}

	
	
	
	return this.init(); // complete the initialisation of the object
}