// wForms - a javascript extension to web forms.
// v0.91 - April 04 2005.
// Copyright (c) 2005 Cédric Savarese <pro@4213miles.com>
// This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>

// Change Log:
// v0.91 : 	A form id is no longuer a requirement. An id is randomly generated if necessary. 
//			Fixed a bug in the input validation: non visible fields were being validated.
//			Fixed a bug in checkVisibility(): function was breaking if ran on a removed element. Now returns false. 
// 			Fixed a Firefox inconsistency with the handling of the onclick event triggered by the LABEL element (see getSrcElement utility function)
//
// TO DO:
// REPEAT/REMOVE Behaviors
//		Add <span> to generated links (for css image replacement)
//		Move the title/link texts to a WFORMS object property (for localization purpose).
// VALIDATION Behavior
// 		Complete validation code for:	"validate-alphanumeric"
// 										"validate-time"
//								 		add text length validation ?
// 
// Known Problems:
// 		Safari 1.x:  	Validation Disabled. No way of knowing if a field is visible or not (cf. checkVisibility())
// 		IE 5.2 Mac: 	Validation Disabled. currentStyle.display returns an empty string in checkVisibility() causing non-visible fields to get validated. 
//						Buggy Rendering of the Repeat behavior 
//						Counter Field of Repeat Behavior not submitted (probably setting the name attribute didn't work)
// 		IE 5.0 PC:		Repeat behavior doesn't work. All field are created as TEXT input ? To be checked again.


function wFORMS() { // wFORMS Class Constructor

	// Private Instance Members
	var wu = new wUTILITY();
	var self = this;
	// Public Instance Members
	// CSS class name definitions. You may change these values if you prefer to use a different naming convention.
	this.classNamePrefix_switch			= "switch";
	this.classNamePrefix_offState		= "offstate";
	this.classNamePrefix_onState		= "onstate";
	this.className_repeat 				= "repeat";
	this.className_delete 				= "removeable";
	this.className_required 			= "required";
	this.className_validationError_msg 	= "errorMsg";
	this.className_validationError_fld	= "error";
	this.classNamePrefix_validation 	= "validate";
	this.className_duplicateLink 		= "duplicateLink";
	this.className_removeLink 			= "removeLink";
	this.className_activeFieldHint 		= "field-hint";
	this.className_inactiveFieldHint 	= "field-hint-inactive";

	// id attribute suffixes
	this.idSuffix_fieldHint 			= "-H";
	this.idSuffix_fieldLabel			= "-L";
	this.idSuffix_fieldError			= "-E";
	this.idSuffix_repeatCounter			= "-RC";

	// Behavior configuration options
	this.preserveRadioName				= true; 		// if true, Repeat behavior will preserve name attributes for radio input. 

	// Form validation function name. May be overidden if you need to run your own validation routine (but make sure to run formValidation() in it).
	this.functionName_formValidation = "this.formValidation";
	
	// Error messages. This array may be overwritten in a separate js file for localization purpose.
	this.arrErrorMsg = new Array(); 
	this.arrErrorMsg[0] = "This field is required. "; // required
	this.arrErrorMsg[1] = "The text must use alphabetic characters only (a-z, A-Z). Numbers are not allowed. "; 	// validate_alpha
	this.arrErrorMsg[2] = "This does not appear to be a valid email address.";									// validate_email
	this.arrErrorMsg[3] = "Please enter an integer.";															// validate_integer
	this.arrErrorMsg[4] = "Please enter a float (ex. 1.9) .";
	this.arrErrorMsg[5] = "Unsafe password. Your password should be between 4 and 12 characters long and use a combinaison of upper-case and lower-case letters.";
	this.arrErrorMsg[6] = "";
	
	this.utilities = wu;	
	
	// -------------------------------------------------------------------------------------------------------------------------
	// Private Instance Methods
	// -------------------------------------------------------------------------------------------------------------------------

	// -------------------------------
	// Switch Behavior Private Methods
	// -------------------------------

	// The switch scope limits the element tree on which the switch can operate.
	// Because of interference issue, a SWITCH contained in a REPEATed block
	// should not be allowed to operate outside of it.
	function switchScope(n) {
		while(n) {
			 if (n.className && ( (' '+n.className+' ').indexOf(' '+self.className_repeat+' ') != -1 || (' '+n.className+' ').indexOf(' '+self.className_delete+' ') != -1)) 
				return n;
			 if (n.tagName.toUpperCase() == "FORM")
				return n;
			 n = n.parentNode;
		}
		return null; // should not happen. A form should exists.
	}
	
	// Recursive loop within the scope to switch classes
	function switchState(n, oldStateClass, newStateClass) {		
		if(n.nodeType != 1) return;
		if(n.className && n.className.indexOf(oldStateClass) != -1)  		
			n.className = n.className.replace(oldStateClass, newStateClass);
		for (var i=0;i<n.childNodes.length;i++) 
			switchState(n.childNodes[i], oldStateClass, newStateClass);
	}
	
	// -----------------------------
	// Repeat/Remove Private Methods
	// -----------------------------
	function removeRepeatCountSuffix(str) {
		return str.substr(0,str.lastIndexOf('-'));
	}
	function replicateTree(srcNode,dupParentNode, idSuffix) {
	
		switch(srcNode.nodeType) {
			case 1:	// ELEMENT-NODE
				if(	srcNode.className.indexOf(self.className_duplicateLink) != -1 ||
					srcNode.className.indexOf(self.className_removeLink) != -1  ) 							
					return null; // Exclude the 'duplicate/remove' links
				// Create Element/'<br />
	
				if(document.all && !window.opera) { 
					// IE Specific : see http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/name_2.asp
					var tagHtml = srcNode.tagName;
					if(srcNode.name) 					
						if (srcNode.tagName.toUpperCase()=="INPUT" && srcNode.type.toLowerCase()=="radio" && self.preserveRadioName)
							tagHtml += " NAME='" + srcNode.name + "' ";
						else
							tagHtml += " NAME='" + removeRepeatCountSuffix(srcNode.name) + idSuffix + "' ";
					if(srcNode.type) 
						tagHtml += " TYPE='" + srcNode.type + "' ";
					if(srcNode.selected) 
						tagHtml += " SELECTED='SELECTED' ";
					if(srcNode.checked)
						tagHtml += " CHECKED='CHECKED' ";
					if(navigator.appVersion.indexOf("MSIE") != -1 && navigator.appVersion.indexOf("Windows") == -1) // IE5 Mac
						var newNode = document.createElement(tagHtml);
					else
						var newNode = document.createElement("<" + tagHtml + "></"+ srcNode.tagName + ">"); 
					newNode.type = srcNode.type; // nail it down for IE5 ?
				}
				else
					var newNode = document.createElement(srcNode.tagName); 
	
				// get attributes										
				for(var i=0; i< srcNode.attributes.length; i++) {
	
					if(	srcNode.attributes[i].specified || // in IE, the attributes array contains all attributes in the DTD
						srcNode.attributes[i].nodeName.toLowerCase() == 'value' ) { // attr.specified buggy in IE?  
	
						if(	srcNode.attributes[i].nodeName.toLowerCase() == "id" || 
							srcNode.attributes[i].nodeName.toLowerCase() == "name" ||
							srcNode.attributes[i].nodeName.toLowerCase() == "for") {
														
							if(srcNode.attributes[i].nodeValue.indexOf(self.idSuffix_fieldHint) != -1)  {
								//leave the field hint suffix at the end of the id.
								var value = srcNode.attributes[i].nodeValue;
								value= removeRepeatCountSuffix(value.substr(0,value.indexOf(self.idSuffix_fieldHint))) + idSuffix + self.idSuffix_fieldHint;
							}
							else {
								if (srcNode.tagName.toUpperCase()=="INPUT" && srcNode.getAttribute('type',false).toLowerCase()=="radio" &&
									srcNode.attributes[i].nodeName.toLowerCase() == "name" && self.preserveRadioName) {
									var value = srcNode.attributes[i].nodeValue;						
								}
								else {
									var value = removeRepeatCountSuffix(srcNode.attributes[i].nodeValue) + idSuffix;
								}
							}
						} else {
							// Do not copy the value attribute for text/textarea/password/file input
							if(srcNode.attributes[i].nodeName.toLowerCase() == "value" &&
							  (srcNode.tagName.toUpperCase()=='TEXTAREA' || 
								(srcNode.tagName.toUpperCase()=='INPUT' &&  
								  (srcNode.type.toLowerCase() == 'text' || srcNode.type.toLowerCase() == 'password' || srcNode.type.toLowerCase() == 'file'))))
								var value='';   
							else
								var value = srcNode.attributes[i].nodeValue;
						}
						switch(srcNode.attributes[i].nodeName.toLowerCase()) {
							case "class":
								newNode.className = value; 
								break;
							default:
								newNode.setAttribute(srcNode.attributes[i].name, value, 0);//setAttribute(newNode, srcNode.attributes[i].name, value);
						}
					}
				}				
				break;
			case 3: // TEXT-NODE
				var newNode = document.createTextNode(srcNode.data); 
				break;
		}
		if(dupParentNode) dupParentNode.appendChild(newNode);
		for(var i=0; i<srcNode.childNodes.length;i++) {
			replicateTree(srcNode.childNodes[i],newNode,idSuffix);
		}
		return newNode;
	}
	

	
	function checkOneRequired(n) {	
		var v=null;
		if(n.nodeType != 1) return false;
		if(n.tagName.toUpperCase() == "INPUT") {
			switch(n.type.toLowerCase()) {
				case "checkbox":
					v = n.checked; 
					break;
				case "radio":
					v = n.checked; 
					break;
				default:
					v = n.getAttribute("value");
			}
		} else v = n.getAttribute("value");
		if(v && !self.isEmpty(v)) {
			return true;
		}
		for(var i=0; i<n.childNodes.length;i++) {
			if(checkOneRequired(n.childNodes[i])) return true;
		}
		return false;
	}

	//------------------------------------------------------------------------------------------------------------------------
	// Privileged Instance Methods
	//------------------------------------------------------------------------------------------------------------------------
	this.refreshAllStates = function(fId) {
		var f=document.getElementById(fId);
		if(!f) return;
		// loop through the fields
		var x = wu.getElements(f);
		for (var i=0;i<x.length;i++) {
			// add switch/state behavior
			if (x[i].tagName.toUpperCase() == "SELECT" && wu.isEventHandled(x[i]) ) 
				this.refreshState(null,x[i]);
			if (x[i].className && x[i].className.indexOf(this.classNamePrefix_switch) != -1) {			
				switch(x[i].tagName.toUpperCase()) {
					case "OPTION":
						break;
					default:
						this.refreshState(null,x[i]);
						break;
				}
			}
		}
	}
	this.refreshState = function(e) {
		if(!e && arguments.length==2)
			var srcE = arguments[1];
		else
			var srcE = wu.getSrcElement(e);
		switch(srcE.tagName.toUpperCase()) {
			case "SELECT":
				var selectedStateClass="";
				var localScope = switchScope(srcE);
				for(var i=0;i<srcE.options.length;i++) {
					if(srcE.options[i].className.indexOf(self.classNamePrefix_switch) != -1 ) {
						var s = srcE.options[i].className;
						s = s.substr(s.indexOf(self.classNamePrefix_switch)).split(" ")[0].substr(self.classNamePrefix_switch.length);
						var offStateClass = self.classNamePrefix_offState + s;
						var onStateClass = self.classNamePrefix_onState + s;				
						if(i==srcE.selectedIndex) {					
							//alert("switch ON " + srcE.tagName+ ': ' + offStateClass + ' to ' + onStateClass);
							switchState(localScope, offStateClass, onStateClass);
							selectedStateClass = onStateClass; // prevents further switching off 
						}
						else if(onStateClass != selectedStateClass) {
							//alert("switch OFF " + srcE.tagName+ ': ' + offStateClass + ' to ' + onStateClass);
							switchState(localScope, onStateClass, offStateClass);
						}
					}			
				}
				break;
			case "INPUT":	
				if(srcE.type.toLowerCase() == 'radio') {
					// Go through the radio group.
					for(var i=0;i <srcE.form[srcE.name].length;i++) { 
						var r = srcE.form[srcE.name][i];
						if(r.className && r.className.indexOf(self.classNamePrefix_switch)!=-1) {
							var s = r.className;
							s = s.substr(s.indexOf(self.classNamePrefix_switch)).split(" ")[0].substr(self.classNamePrefix_switch.length);
							var offStateClass = self.classNamePrefix_offState + s;
							var onStateClass = self.classNamePrefix_onState + s;	

							if(r.checked) 
								switchState(switchScope(r), offStateClass, onStateClass);
							else {								
								switchState(switchScope(r), onStateClass, offStateClass); 
							}						
						}
					}
				} else {
					var s = srcE.className;
					s = s.substr(s.indexOf(self.classNamePrefix_switch)).split(" ")[0].substr(self.classNamePrefix_switch.length);
					var offStateClass = self.classNamePrefix_offState + s;
					var onStateClass = self.classNamePrefix_onState + s;	

					if(srcE.checked) { // && !srcE.defaultChecked
						switchState(switchScope(srcE), offStateClass, onStateClass);
					}
					else
						switchState(switchScope(srcE), onStateClass, offStateClass); 
				}
				break;
		}	
	}	
	// --------------------------
	// Repeat Behavior Methods
	// --------------------------
	this.duplicateFieldGroup = function(e) {
		var srcE = wu.getSrcElement(e);
		// Get Element to duplicate.
		var sourceNode = srcE.parentNode;
		
		
		while (sourceNode && (!sourceNode.className || (' '+sourceNode.className+' ').indexOf(' '+self.className_repeat+' ') == -1)) {
			sourceNode = sourceNode.parentNode;
		}	
		if (sourceNode && sourceNode.className.indexOf(self.className_repeat) != -1) {
			// Extract row counter information
			counterField = document.getElementById(sourceNode.id + self.idSuffix_repeatCounter);
			if(!counterField) return; // should not happen.
			var rowCount = parseInt(counterField.value) + 1;
			// Prepare id suffix
			var suffix = "-" + rowCount.toString()
			// duplicate node tree 
			var dupTree = replicateTree(sourceNode, null, suffix);  //  sourceNode.cloneNode(true); 
			// find insert point in DOM tree (after existing repeated element)
			var insertNode = sourceNode.nextSibling;
			while(insertNode && insertNode.className && insertNode.className.indexOf(self.className_delete) != -1) {
				insertNode = insertNode.nextSibling;
			}
			
			sourceNode.parentNode.insertBefore(dupTree,insertNode);	 // Buggy rendering in IE5/Mac
			// if(navigator.appVersion.indexOf("MSIE") != -1 && navigator.appVersion.indexOf("Windows") == -1)			
			//
			
			// the copy is not duplicable, it's removeable
			dupTree.className = sourceNode.className.replace(self.className_repeat,self.className_delete);
			// re-add behaviors
			if(!dupTree.id) dupTree.id = wu.randomId() + suffix;  //  createAttribute()
			self.addBehaviors(dupTree.id);
			
			// Save new row count 
			document.getElementById(sourceNode.id + self.idSuffix_repeatCounter).value = rowCount;

				
		}
		return wu.XBrowserPreventEventDefault(e);
	}
	this.removeFieldGroup = function(e) {
		var srcE = wu.getSrcElement(e);	// Get Element to remove.
		var delNode = srcE.parentNode;

		while (delNode && (' '+delNode.className+' ').indexOf(' '+self.className_delete+' ') == -1) {
			delNode = delNode.parentNode;
		}
		delNode.parentNode.removeChild(delNode);
		return wu.XBrowserPreventEventDefault(e);
	}
	// --------------------------
	// Field Hint  Methods
	// --------------------------
	this.activateFieldHint = function(e) {
		var srcE = wu.getSrcElement(e);
		var fh = document.getElementById(srcE.id +  self.idSuffix_fieldHint);
		if(fh) fh.className = fh.className.replace(self.className_inactiveFieldHint, self.className_activeFieldHint);
	}
	this.desactivateFieldHint = function(e) {
		var srcE = wu.getSrcElement(e);
		var fh = document.getElementById(srcE.id +  self.idSuffix_fieldHint);
		if(fh) fh.className = fh.className.replace(self.className_activeFieldHint,self.className_inactiveFieldHint);
	}
	// --------------------------
	// FORM VALIDATION Method
	// --------------------------
	this.formValidation = function(e) {		
		var srcE = wu.getSrcElement(e);
		var x = wu.getElements(srcE); //srcE.elements;  
		var nbTotalErrors = 0;
		for (var i=0;i<x.length;i++) {
			var nbErrors = 0;			
			var isVisible = false;
			if ((' '+x[i].className+' ').indexOf(' '+self.className_required+' ') != -1) {				
				var v = true;
				if(wu.checkVisibility(x[i])) 
					isVisible = true;
				if(isVisible) {
					switch(x[i].tagName.toUpperCase()) {
						case "INPUT":
							switch(x[i].getAttribute("type").toUpperCase()) {
								case "CHECKBOX":
									v = x[i].checked; 
									break;
								case "RADIO":
									v = x[i].checked; 
									break;
								default:
									v = !self.isEmpty(x[i].value);
							}
							break;
						case "SELECT":
							v = !self.isEmpty(x[i].options[x[i].selectedIndex].value);
							break;
						case "TEXTAREA":
							v = !self.isEmpty(x[i].value);
							break;
						case "FIELDSET":
							v = checkOneRequired(x[i]);
							break;
						case "DIV":
							v = checkOneRequired(x[i]);
							break;
						case "SPAN":
							v = checkOneRequired(x[i]);
							break;
					}
				}
				if(!v) {
					// flag error
					self.showError(x[i],self.arrErrorMsg[0]);
					nbErrors++;
				}
			}

			// input validation
			if (x[i].className.indexOf(self.classNamePrefix_validation) != -1) {
				if(!isVisible) // may not have been checked if field not required
				if(wu.checkVisibility(x[i])) 
					isVisible = true;
				if(isVisible) {
					var arrClasses = x[i].className.split(" ");
					for (j=0;j<arrClasses.length;j++) {
						switch(arrClasses[j]) {
							case "validate-alpha":
								if(!self.isAlpha(x[i].value)) {
									self.showError(x[i],self.arrErrorMsg[1]);
									nbErrors++;
								}
								break;
							case "validate-date":
								if(!self.isDate(x[i].value)) {
									// flag error
									self.showError(x[i],self.arrErrorMsg[1]);
									nbErrors++;
								}
								break;
							case "validate-time":
								/* NOT IMPLEMENTED */
								break;
							case "validate-email":
								if(!self.isEmail(x[i].value)) {
									// flag error
									self.showError(x[i],self.arrErrorMsg[2]);
									nbErrors++;
								}
								break;
							case "validate-integer":
								if(!self.isInteger(x[i].value)) {
									// flag error
									self.showError(x[i],self.arrErrorMsg[3]);
									nbErrors++;
								}					
								break;
							case "validate-float":
								if(!self.isFloat(x[i].value)) {
									// flag error
									self.showError(x[i],self.arrErrorMsg[4]);
									nbErrors++;
								}
								break;
							case "validate-strongpassword": // NOT IMPLEMENTED
								if(!self.isPassword(x[i].value)) {
									// flag error
									self.showError(x[i],self.arrErrorMsg[5]);
									nbErrors++;
								}
								break;
						}
					}
				}
			}		 
			if(nbErrors>0) {
				nbTotalErrors+= nbErrors;
			} else {
				var rErrClass = new RegExp(self.className_validationError_fld,"gi");
				x[i].className = x[i].className.replace(rErrClass,"");
				var fe = document.getElementById(x[i].id +  self.idSuffix_fieldError);
				if(fe) fe.parentNode.removeChild(fe);
			} 
		}
		if (nbTotalErrors > 0) {
			alert(nbTotalErrors + " Required Field(s) need to be filled out.");
			return wu.XBrowserPreventEventDefault(e);
		}
		return true;
	}
	this.isEmpty = function(s) {
		var regexpWhitespace = /^\s+$/;
		return  ((s == null) || (s.length == 0) || regexpWhitespace.test(s));
	}
	this.isAlpha = function(s) {
		var regexpAlphabetic = /^[a-zA-Z]+$/; // Add ' and - 
		return self.isEmpty(s) || regexpAlphabetic.test(s);
	}
	this.isDate = function(s) {
		var testDate = new Date(s);
		return self.isEmpty(s) || !isNaN(testDate);
	}
	this.isEmail = function(s) {
		var regexpEmail = /\w{1,}[@]\w{1,}([.](\w{1,})){1,2}$/;
		return self.isEmpty(s) || regexpEmail.test(s);
	}
	this.isInteger =function(s) {
		var regexp = /^[+]?\d+$/;
		return self.isEmpty(s) || regexp.test(s);
	}
	this.isFloat = function(s) {		
		return self.isEmpty(s) || !isNaN(parseFloat(s));
	}
	// NOT IMPLEMENTED
	this.isPassword = function(s) {
		// Matches strong password : at least 1 upper case latter, one lower case letter. 4 characters minimum. 12 max.
		//var regexp = /^(?=.*[a-z])(?=.*[A-Z])(?!.*\s).{4,12}$/;  // <= breaks in IE5/Mac
		return self.isEmpty(s);
	}
	this.showError = function (n,errorMsg) {		
		if(n.className.indexOf(self.className_validationError_fld)!= -1) return;
		if (!n.id) n.id = wu.randomId(); // we'll need an id here.		
		// Add error flag to the field
		n.className += " " + self.className_validationError_fld;
		// Prepare error message
		var msgNode = document.createTextNode(" " + errorMsg);
		// Find error message placeholder.
		var fe = document.getElementById(n.id +  self.idSuffix_fieldError);
		if(!fe) { // create placeholder.
			fe = document.createElement("div"); 
			fe.setAttribute('id', n.id +  self.idSuffix_fieldError);			
			// attach the error message after the field label if possible
			var fl = document.getElementById(n.id +  self.idSuffix_fieldLabel);
			if(fl)
				fl.parentNode.insertBefore(fe,fl.nextSibling);
			else
				// otherwise, attach it after the field tag.
				n.parentNode.insertBefore(fe,n.nextSibling);
		}
		// Finish the error message.
		fe.appendChild(msgNode);  	
		fe.className += " " + self.className_validationError_msg;
	}
}

//------------------------------------------------------------------------------------------------------------------------
// WFORMS Public Methods
//------------------------------------------------------------------------------------------------------------------------
wFORMS.prototype.onLoadHandler = function() {		
	for (var i=0;i<document.forms.length;i++) {
		if(!document.forms[i].id) document.forms[i].id = this.utilities.randomId();
		this.addBehaviors(document.forms[i].id);
	}
}
wFORMS.prototype.addBehaviors = function (fId) {

	var f=document.getElementById(fId);
	if(!f) return;

	var thisForm; 				// Pointer to keep track of the current form being processed.
	var wu = this.utilities;	// Utiltiy class instance
	wu.resetEventList();
	
	// loop through the fields
	var x = wu.getElements(f);
	
	for (var i=0;i<x.length;i++) {

		// add form validation behavior
		if(x[i].tagName.toUpperCase()=="FORM") {			
			wu.XBrowserAddHandler(x[i],'submit',eval(this.functionName_formValidation));
			thisForm = x[i];	// Pointer to keep track of the current form being processed.
		}
		// add fieldhint behavior
		var fh = document.getElementById(x[i].id + this.idSuffix_fieldHint);			
		if(fh) {		
			wu.XBrowserAddHandler(x[i],'focus',this.activateFieldHint);
			wu.XBrowserAddHandler(x[i],'blur',this.desactivateFieldHint);			
		}
		// add switch/state behavior
		if (x[i].className && x[i].className.indexOf(this.classNamePrefix_switch) != -1) {
			switch(x[i].tagName.toUpperCase()) {
				case "OPTION":
					var sel = x[i].parentNode;	// Get to the SELECT
					if(sel.tagName.toUpperCase() == "OPTGROUP") sel = sel.parentNode; // try again.
					if(!wu.isEventHandled(sel)) {
						wu.XBrowserAddHandler(sel,'change',this.refreshState);
				   	}
					break;
				case "INPUT":
					if(x[i].type && x[i].type.toLowerCase() == 'radio') {
						// Add the onclick event on radio inputs of the same group
						for (var j=0;j<thisForm[x[i].name].length;j++) {
							wu.XBrowserAddHandler(thisForm[x[i].name][j],'click',this.refreshState);
						}
					} else
						wu.XBrowserAddHandler(x[i],'click',this.refreshState);
					break;
				default:					
					wu.XBrowserAddHandler(x[i],'click',this.refreshState);
					break;
			}
		}
		// add duplication behavior
		if (x[i].className && (' '+x[i].className+' ').indexOf(' '+this.className_repeat+' ') != -1) {
			// this element to be duplicated.
			// add duplicate action
			var actionNode = document.createElement("a"); 
			var textNode = document.createTextNode("Repeat");
			actionNode.setAttribute('href',"#");	
			actionNode.className = this.className_duplicateLink;
			actionNode.setAttribute('title',"Repeats the preceding field or field group.");	
			if(x[i].tagName.toUpperCase()=="TR") {
				// find the last TD
				var n = x[i].lastChild;	
				while(n && n.nodeType != 1)  
					n = n.previousSibling;
				if(n && n.nodeType == 1) 
					n.appendChild(actionNode);
				// Else Couldn't find the TD. Table row malformed ?
			} else
				x[i].appendChild(actionNode);
			actionNode.appendChild(textNode); 
			
			// Add hidden counter field if necessary
			var counterField = document.getElementById(x[i].id + this.idSuffix_repeatCounter);
			if (!counterField) {
				if(document.all && !window.opera) { // IE Specific :-(
					// see http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/name_2.asp
					var counterFieldId = x[i].id + this.idSuffix_repeatCounter;
					if(navigator.appVersion.indexOf("MSIE") != -1 && navigator.appVersion.indexOf("Windows") == -1) // IE5 Mac
						counterField = document.createElement("INPUT NAME=\"" + counterFieldId + "\"");
					else
						counterField = document.createElement("<INPUT NAME=\"" + counterFieldId + "\"></INPUT>"); 					
					counterField.type='hidden';
					counterField.id = counterFieldId; 
					counterField.value = "1";
				}
				else {
					counterField = document.createElement("INPUT"); 
					counterField.setAttribute('type','hidden');
					counterField.setAttribute('value','1');
					counterField.setAttribute('name', x[i].id + this.idSuffix_repeatCounter);
					counterField.setAttribute('id', x[i].id + this.idSuffix_repeatCounter); 
				}
				thisForm.appendChild(counterField);
			}
			// Add event handler			
			wu.XBrowserAddHandler(actionNode,'click',this.duplicateFieldGroup);			
		}	
		// add remove behavior
		if (x[i].className && (' '+x[i].className+' ').indexOf(' '+this.className_delete+' ') != -1) {
			// this element can be removed
			// add remove action
			var actionNode = document.createElement("a"); 
			var textNode = document.createTextNode("Remove");
			actionNode.setAttribute('href',"#");	
			actionNode.className = this.className_removeLink;
			actionNode.setAttribute('title',"Removes the preceding field or field group.");	
			if(x[i].tagName.toUpperCase()=="TR") {
				// find the last TD
				var n = x[i].lastChild;	
				while(n && n.nodeType != 1)  
					n = n.previousSibling;
				if(n && n.nodeType == 1) 
					n.appendChild(actionNode);
				// Else Couldn't find the TD. Table row malformed ?
			} else
				x[i].appendChild(actionNode);
			actionNode.appendChild(textNode); 			
			wu.XBrowserAddHandler(actionNode,'click',this.removeFieldGroup);			
		}	
	 }
	 this.refreshAllStates(fId);
}


// *************************************************************************************************************
// UTILITY CLASS
// *************************************************************************************************************
function wUTILITY() {
	// Event Handler utility list
	this.handlerList = new Array(); 
}

// Cross-Browser event handler management.
// adapted from Andy Smith's (http://weblogs.asp.net/asmith/archive/2003/10/06/30744.aspx)
wUTILITY.prototype.XBrowserAddHandler = function (target,eventName,handlerName) {
	if(!target) return;
	if (target.addEventListener) { 
		target.addEventListener(eventName, function(e){eval(handlerName)(e);}, false);
	} else if (target.attachEvent) { 
		target.attachEvent("on" + eventName, function(e){eval(handlerName)(e);});
		} else { 
		// THIS CODE NOT TESTED 
		var originalHandler = target["on" + eventName]; 
		if (originalHandler) { 
		  target["on" + eventName] = function(e){originalHandler(e);eval(handlerName)(e);}; 
		} else { 
		  target["on" + eventName] = eval(handlerName); 
		} 
	} 
	// Keep track of added handlers.
	this.handlerList[this.handlerList.length] = target;  
}
//
wUTILITY.prototype.isEventHandled = function(n) {
	for(var i=0; i < this.handlerList.length; i++) {
		if(this.handlerList[i].id==n.id)
			return true;
	}
	return false;
}
wUTILITY.prototype.resetEventList = function() {
	this.handlerList = new Array(); 
}

// Activating an Alternate Stylesheet (thx to: http://www.howtocreate.co.uk/tutorials/index.php?tut=0&part=27)
// Use this to activate a CSS Stylesheet that shouldn't be used if javascript is turned off.
// The stylesheet rel attribute should be 'alternate stylesheet'. The title attribute should be set.
wUTILITY.prototype.activateStylesheet = function(sheetref) {
	if(document.getElementsByTagName) {
		var ss=document.getElementsByTagName('link');
	} else if (document.styleSheets) {
		var ss = document.styleSheets;
	}
	for(var i=0;ss[i];i++ ) {
		if(ss[i].href.indexOf(sheetref) != -1) {
			ss[i].disabled = true;
			ss[i].disabled = false;
			
		}
	}
	/* 
	// Dev Note: Attaching the css using javascript doens't work in Safari.
	// even w/ forced repaint (thx to http://neo.dzygn.com/archive/2004/09/forcing-safari-to-repaint
	var objHead = document.getElementsByTagName('head');
	if (objHead[0]) {
	var objCSS = objHead[0].appendChild(document.createElement('link'));
		objCSS.rel = 'stylesheet';
		objCSS.href = sheetref;
		objCSS.type = 'text/css';
 	}
	*/
}
// Generates a random ID
wUTILITY.prototype.randomId = function () {
	var rId = "";
	for (var i=0; i<6;i++)
		rId += String.fromCharCode(97 + Math.floor((Math.random()*24)))
	return rId;
}
// returns all child elements of a node.
wUTILITY.prototype.getElements = function(n, list) {
	if(!list) list = new Array();
	if(n.nodeType==1) {
		list[list.length]= n;
		for(var i=0; i<n.childNodes.length;i++) 
			this.getElements(n.childNodes[i], list);
		return list;
	}
}
// Returns the event's source element 
wUTILITY.prototype.getSrcElement = function(e) {	
	if(!e) 
		e = window.event;	
	if(e.target)
		var srcE = e.target;
	else
		var srcE = e.srcElement;
	if(srcE.nodeType == 3) srcE = srcE.parentNode; // safari weirdness		
	if(srcE.tagName.toUpperCase()=='LABEL') { 
		// when clicking a label, firefox fires the input onclick event
		// but the label remains the source of the event. In Opera and IE 
		// the source of the event is the input element. Which is the 
		// expected behavior, I suppose.		
		if(srcE.getAttribute('for')) {
			srcE = document.getElementById(srcE.getAttribute('for'));
		}
	}
	return srcE;
}
// Cancel the default execution of an event.
wUTILITY.prototype.XBrowserPreventEventDefault = function(e) {
	if(!e) e = window.event;
	if (e.preventDefault) e.preventDefault();
	else e.returnValue = false;
	return false;
}
wUTILITY.prototype.checkVisibility = function(n) {
	// check if any of the element's ancestors is not visible.
	if(window.getComputedStyle) {
		var isVisible = window.getComputedStyle(n,"").getPropertyValue("display").toLowerCase()!="none";
		isVisible = isVisible && window.getComputedStyle(n,"").getPropertyValue("visibility").toLowerCase()!="hidden";
		// add visiblity!=collapse ?
	}
	else if(n.currentStyle) {		
		if(n.currentStyle.display=='') return false; // effectively disable validation on IE5/Mac.
		var isVisible = n.currentStyle.display.toLowerCase() != "none";
		isVisible = isVisible && n.currentStyle.visibility.toLowerCase() !="hidden";
	}
	else {
		// return false; // effectively disable validation for Safari
		// try to focus or select...
		// This code not finalized (doesn't work for Safari).
		try {			
				if(n.focus) alert(n.focus());
				if(n.select) alert(n.select());
			} 
		catch (e) {
				// focus failed. Either not a field, or not visible.
				alert("not a visible field. " + e);
				return false;
			}		
			return true; // focus ok. The field is definitively visible.
	}
	if(!n.parentNode) { return false; } ; // should not happen, unless we checking some removed elements.
	if (n.parentNode.tagName.toUpperCase()=="BODY" || !isVisible)
		return isVisible;
	return this.checkVisibility(n.parentNode);
}


// =======================================================================================================================
// LET's GO

var wf = new wFORMS();
// Attach JS only stylesheet.
wf.utilities.activateStylesheet('wforms-jsonly.css'); 
// onLoad event handler
wf.utilities.XBrowserAddHandler(window,'load',function() { wf.onLoadHandler();}  );
	

