/*
ForX - Forms Extension Framework.
Copyright (C) 2001  Claus Augusti (caugusti@formatvorlage.de)

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

var IE = 1;
var DOM = 2;
var container;
var model;

function forxInit() {
	try {		
		// only IE 5.x and Netscape 6/Mozilla support this application
		if (navigator.appName == "Microsoft Internet Explorer" && navigator.appVersion.indexOf("MSIE 5") != -1) {
			model = IE;
		}
		if (navigator.appName == "Netscape" && parseInt(navigator.appVersion) >= 5) {
			model = DOM;
		}
		
		if (model == null) {
			//alert("ForX can only be used with IE 5.x, Mozilla and Netscape 6");
			return;
		}

		container = new forxContainer();	
		container.build(document);	
				
	} catch (exception) {
		alert("forxInit():exception="+exception);
	}	
}


function forxException(msg) {
	this.message = "ForX Exception:\n" + msg;
}

function forxContainer() {
}
forxContainer.prototype.forms = new Array();


forxContainer.prototype.build = function build(root) {		
	var forms = root.getElementsByTagName("form");
	for (var i=0; i < forms.length; i++) {
		try {	
			if (forms[i].getAttribute("forx:enabled") != "true") { 
				continue; 
			}
			
			var form = new forxForm(forms[i]);			
			form.buildForm(forms[i]);
			form.hideTemplates();
			form.validate();
			this.forms[forms[i].getAttribute("forx:id")] = form;
		} catch (exception) {
			alert("forxContainer.build():exception="+exception);
		}
	}
}

/** 
 * This object holds a model of a ForX form
 */
function forxForm(root) {
	this.required_fields = new Array();
	this.groups = new Array();
	this.immediates = new Array();
	this.marker = "";
	this.form_root = root;	
	this.doReview = false;
	this.reviewCount = 0;

}
forxForm.prototype.isHighlighting = false;
forxForm.prototype.checkField = checkField;
forxForm.prototype.isRequired = isRequired;
forxForm.prototype.validate = validate;
forxForm.prototype.buildForm = buildForm;
forxForm.prototype.hideTemplates = hideTemplates;
forxForm.prototype.validateElement = validateElement;
forxForm.prototype.setMarker = setMarker;

function forxGroup(container_element) {
	this.container = container_element;
	this.members = new Array();
}

/**
 * This method determines if a given element is required right now.
 * The value of the required attribute can be a literal boolean value or an expression,
 * which needs to eval true to mark the given element as being required.
 */
function isRequired(element) {	
	// element isn't required if it has no forx:required attribute
	var attr = element.getAttribute("forx:required");
	if (attr.length == 0) {return false;}
	
	if (attr == "true") {return true;}
	if (attr == "false") return false;
	// any and all are used for groups only, so we return false first
	//if (attr == "any" || attr == "all") return false;
	
	// check for a regular expression
	if (attr.charAt(0) == "/" && attr.charAt(attr.length-1) == "/") { return true; }
	
	// now we can only have a conditional requirement
	var re = /^\s?if\s+([^!=]+)([!=]?=)(['"])?([^'"]+)(['"])?$/;
	re.exec(attr);
	var value;	
	
	// should elements better be identified by their XHTML name attribute?
	var src = getElementByForxId(this.form_root, RegExp.$1);
	//var src = document.getElementById(RegExp.$1);	
	if (src == null) { return false;}
	var source = src.value;
	
	// use a literal or an element's value?
	if (RegExp.$5 == "'" || RegExp.$5 == '"') {		
		value = RegExp.$4;
	} else {
		var target = getElementByForxId(this.form_root, RegExp.$4);
		//var target = document.getElementById(RegExp.$4);
		if (target != null) {
			value = target.value;
		} else {
			value = "";
		}
	}
	
	// easier to wrap in a try/catch than doing object detection
	try {	
		var src_type = src.getAttribute("type").toLowerCase();
		var src_node_name = src.nodeName.toLowerCase();
		// handle text fields and textareas
		if (src_type == "text" || src_node_name == "textarea") {
			if (src.value == null) { 
				return false;
			} else {
				return eval("(source "+RegExp.$2+" value)");
			}
		} 
		
		// handle checkboxes and radiobuttons
		if (src_type == "checkbox" || src_type == "radio") {	
			if (value == "CHECKED") {
				return src.checked;
			} else {
				return eval("(source "+RegExp.$2+" value)");
			}
		}		
		
		// handle lists and popups
		if (src_node_name == "select") {
			if (src.selectedIndex == -1) {
				return false;
			} else {
				return eval("(source "+RegExp.$2+" value)");
			}
				
		}
	} catch (exception ) {
		alert("isRequired().exception="+exception);
	}
}

/**
 * A utility method to find elements by their ForX id attribute
 */
function getElementByForxId(form_root, id) {
	var elems = getElementsByAttributeName("forx:id", form_root);	
	var target;
	for (var i=0; i < elems.length; i++) {
		if (elems[i].getAttribute("forx:id") == id) {			
			target = elems[i];
			break;
		}
	}
	return target;
}

/**
 * The whole form is validated here.
 * Returns true or false depending on validity.
 */
function validate() {
	var validated = true;
	for (var i=0; i < this.required_fields.length; i++) {
		if (this.validateElement(this.required_fields[i], true) == false) {
			validated = false;
		}
	}
	
	// validate groups
	for (var i in this.groups) {
		var group = this.groups[i];
		var r = group.container.getAttribute("forx:required");
		group.container.style.display = "none";
		if(r == null || r.length == 0) {continue;}
		var required = this.isRequired(group.container) 
		var cnt = 0;
		for (var j=0; j < group.members.length; j++) {
			// validate
			if (this.validateElement(group.members[j], required) == true) {	
				// we need a counter for the all/any check
				cnt++;
			}		
		}
		
		var req_attr = group.container.getAttribute("forx:grouprequires");
		if (req_attr == null || req_attr.length == 0) {req_attr = "any";}
		
		var group_valid = (req_attr == "any" && cnt > 0) || (req_attr == "all" && cnt == group.members.length)
			|| (parseInt(req_attr) == cnt);
		
		//alert("req_attr="+req_attr+", valid="+group_valid+", required="+required+", group_root="+group.container.getAttribute("name") );
		
		if (required) {
			if (group_valid) {
				group.container.style.display = "none";
				for (var j=0; j < group.members.length; j++) {
					this.setMarker(group.members[j], false );
				}			
			} else {
				group.container.style.display = "block";
			}
		} else {
			group.container.style.display = "none";
			for (var j=0; j < group.members.length; j++) {
				this.setMarker(group.members[j], false );
			}
		}
		if (!group_valid && required) {validated = false;}
	}
	
	// toggle the reminder element which notifies the users that not all required fields or groups are valid
	var reminder = document.getElementById("forx:remind");
	if (reminder != null) {
		(!validated) ? reminder.style.display = "block" : reminder.style.display = "none";
	}
	
	// wanna do something else before we return?
	if (!validated) {
		return false;
	} else {
		return true;
	}
}

/** 
 * This function is called right before a form is submitted. 
 */
function onSubmitForm(event) {
	if (model == IE) {
		var target = window.event.srcElement;
		var event = window.event;
	}
	if (model == DOM) {
		var event = event;
		var target = event.target;
	}
	var form = getForm(target);
	if (!form) return;
	var forx_form = container.forms[form.getAttribute("forx:id")];
	var valid = forx_form.validate();
	
	// do we have a review element and has it been shown already?
	if (valid && forx_form.doReview) {
		if (forx_form.reviewCount == 0) {
			forx_form.reviewCount++;
			var review_e = document.getElementById("forx:review");
			if (review_e == null) {return forx_form.validate();}
			review_e.style.display = "block";
			return false;
		} 
		
		if (forx_form.reviewCount == 1) {
			var review_e = document.getElementById("forx:review");
			if (review_e != null) {
				review_e.style.display = "none";
			}
		}		
	} else {
		forx_form.reviewCount = 0;
		var review_e = document.getElementById("forx:review");
		if (review_e != null) {
			review_e.style.display = "none";
		}	
	}
	return forx_form.validate();	
}


/**
 * Here the ForX form object is created from the XHTML form. 
 * At this point, all required templates should be defined
 */
function buildForm(form_root) {
	this.marker = document.getElementById("forx:marker").cloneNode(true);	
	if (!this.marker) return;
	
	// build required fields 
	var req = getElementsByAttributeName("forx:required", form_root);	
	var marker_ = this.marker.cloneNode(true);
	for (var i=0; i < req.length; i++) {
		var element = req[i];		
		var e_n = element.nodeName.toLowerCase();
		if (e_n != "input" && e_n != "textarea" && e_n != "select") {continue;}
		if (element.getAttribute("forx:required") == "false") { continue; }
		this.required_fields[this.required_fields.length] = element;
	}
	
	// build groups
	var gr = getElementsByAttributeName("forx:group", form_root);
	var members = getElementsByAttributeName("forx:member", form_root); 
	for (var i=0; i < gr.length; i++) {
		var g_name = gr[i].getAttribute("forx:group");
		var highlight_on = (gr[i].getAttribute("forx:highlight") == "true");
		for (var j=0; j < members.length; j++) {
			var g;
			if (members[j].getAttribute("forx:member") == g_name) {			
				if (this.groups[g_name] == null) {
					g = new forxGroup(gr[i]);
					this.groups[g_name] = g;
				}
				if (highlight_on) {
					gr[i].onmouseover = highlight; 
				}
				g.members[g.members.length] = members[j];
				members[j].setAttribute("forx:required", "true");
			}	
		} 
	}
		
	// build immediate validation
	var im = getElementsByAttributeName("forx:immediate", form_root);
	for (i=0; i < im.length; i++) {
		if (im[i].getAttribute("forx:immediate") == "true") {
			im[i].onblur = checkField;
			this.immediates[im[i].getAttribute("forx:id")] = true;
		} else {
			this.immediates[im[i].getAttribute("forx:id")] = false;
		}
	}
	
	this.doReview = (form_root.getAttribute("forx:review") == "true");
	form_root.onsubmit = onSubmitForm;
}

/** 
 * This function is called for immediately checked elements
 */
function checkField(event) {
	if (model == IE) {
		var target = window.event.srcElement;
	}
	if (model == DOM) {
		var target = event.target;
	}
	var form = getForm(target);
	if (form == null) return;
	
	var forx_form = container.forms[form.getAttribute("forx:id")]
	if (forx_form == null) return;
	var target_id = target.getAttribute("forx:id");
	// check if field is really required and wants to be checked immediately
	if (forx_form.isRequired(target) && forx_form.immediates[target_id] == true ) {
		forx_form.validateElement(target, true); 
	}
	//return false;
}

/**
 * This is the main validation method called by checkField() via an event handler
 * or by the validate() method right before the form is submitted.
 * The element is marked after validation here.
 */
function validateElement(element, mark) {
	try{
	
	if (!this.isRequired(element) ) { 
		if (mark) {this.setMarker(element, false);}
		return true;
	}

	// get content type, handle regexp content type
	var re = /^\/(.+)\/$/;
	var ctype = element.getAttribute("forx:ctype");
	var reg_p;
	if (re.test(ctype)) {
		reg_p = RegExp.$1;
		ctype = "regexp";
	}
	if (ctype == null || eval("window.is_"+ctype+"== null")) {ctype = "any";}
	
	var e_type = element.getAttribute("type").toLowerCase();
	var e_node_name = element.nodeName.toLowerCase();
	
	// handle checkboxes
	if (e_type == "checkbox") {
		var check = eval("is_"+ctype+"(element.value, reg_p)") && (element.checked == true);	
		if (mark) {this.setMarker(element, !check);}
		return check;
	}
	
	// handle text fields, textareas and select elements
	if (e_type == "text" || e_node_name == "textarea" || e_node_name == "select") {
		var check = eval("is_"+ctype+"(element.value, reg_p)");
		if (mark) {this.setMarker(element, !check);}
		return check;
	}
	
	// handle radioboxes
	if (e_type == "radio") {
		var radios = getElementsByAttributeName("name", this.form_root);
		var radio_group = new Array();
		for (var i=0; i < radios.length; i++) {
			if (radios[i].getAttribute("name") == element.getAttribute("name") ) {
				radio_group[radio_group.length] = radios[i];	
			}
		}
		// find a selected radiobutton and check against content type
		for (var i=0, group_check = false; i < radio_group.length; i++) {
			if (radio_group[i].checked == true) {
				group_check = eval("is_"+ctype+"(element.value, reg_p)"); break;
			}	
		}
		// mark all buttons of a group
		if (mark) {
			for (var i=0; i < radio_group.length; i++) {this.setMarker(radio_group[i], !group_check);}
		}	
		return group_check;
	}
		
	return false;
	} catch (exception) {
		alert("validateElement().exception:"+exception);
	}
}

/**
 * This function hides all elements used a templates 
 */
function hideTemplates() {
	var e;
	if ( (e = document.getElementById("forx:marker")) != null) {
		e.style.display = "none";
	}
	if ( (e = document.getElementById("forx:remind")) != null) {
		e.style.display = "none";
	}
	if ( (e = document.getElementById("forx:review")) != null) {
		e.style.display = "none";
	}
}


/** 
 * Sets the marker of a required element according to the show_marker argument.
 */
function setMarker(element, show_marker) {
		if (element.parentNode == null || this.marker == null) return;
		var e_n = element.nodeName.toLowerCase();

		if (show_marker) {   
			if (element.previousSibling && element.previousSibling.className == "forx") return;	
			element.parentNode.insertBefore(this.marker.cloneNode(true), element);
		} else {
			if (element.previousSibling && element.previousSibling.className == "forx") {
				element.parentNode.removeChild(element.previousSibling);
			}
		}				
		
}

function highlight(event) {
	try {
	if (model == IE) {
		var target = window.event.srcElement;
	}
	if (model == DOM) {
		var target = event.target;
	}
	var form = getForm(target);
	if (form == null) return;
	if (form.isHighlighting) { return;}
	
	var forx_form = container.forms[form.getAttribute("forx:id")]
	if (forx_form == null) return;

	
	var g_n = target.getAttribute("forx:group");
	if (g_n.length == 0) { return; }
	var group = forx_form.groups[g_n];
	if (group == null) {return;}
	
	for (var i=0; i < group.members.length; i++) {
		group.members[i].style.backgroundColor = "red";
		setTimeout("deHighlight('"+g_n+"')", 3000);
	}
	} catch (exception) {}
}

function deHighlight(group) {	
	for (i in container.forms) {
		var form = container.forms[i];
		for (j in form.groups) {
			if (j == group) {
				var g = form.groups[group];
				form.isHighlighting = false;		
				for (var i=0; i < g.members.length; i++) {		
					g.members[i].style.backgroundColor = "";					
				}
			}
		}		
	}
}



/** 
 * Content type validation functions
 */

function is_any(str) {
	return (str.length > 0);
}

function is_text(str) {
	var re = /^[a-zA-Z0-9,\.\-_@\S\s]+$/;
	return re.test(str);
}

function is_email(str) {
	var re = /^\S+@\S+\.\S+$/;
	return re.test(str);
}

function is_number(str) {
	if (str.length == 0) { return false; }	// this is needed because isNaN on an empty string gives false
	str = str.replace(/,/, ".");
	return !isNaN(str);
}

function is_float(str) {
	str = str.replace(/,/, ".");
	return (parseFloat(str)==str);
}

function is_int(str) {
	var re = /^\d+$/;
	return (parseInt(str)==str);
}

function is_date(date) {
	var re = /^\d\d\/\d\d\/\d\d\d\d$/;
	return re.test(date);	
}

function is_name(name) {
	var re = /^\S+.*,.*$/;
	return re.test(name);	
}

function is_phone(str) { 
	var re = /^\+?\s?\d+\s?\(?\d+\)?\s?(\d+\s?)+$/
	return re.test(str);
}

function is_regexp(str, reg) {
	var re = new RegExp(reg);
	return re.test(str);
} 
/** 
 * END content types
 */


/**
 * 	Utility methods
 */
 

/**
 * Removes trailing and leading whitespace in a string.
 */
function removeWhitespace(str) {
	var re = /^(\s+)(\S.+\s?\S+)(\s+)$/;
	return str.replace(re, "$2");s
}


/**
 * DOM Utility Methods
 */

/**
 * IE5, NN6
 * Returns an Array containing all elements with an given name
 */
function getElementsByAttributeName(attr, element) {
	var elements = new Array();
	traverse(element);
	
	function traverse(element) {
		try {
			if (element.getAttribute && element.getAttribute(attr)) {
				elements[elements.length] = element;
				//alert("add element with "+attr+" attribute");
			}
		} catch (ex) {
			//alert("An exception occured accessing the DOM:"+ex);
		}
			if (element.childNodes && element.childNodes.length > 0) {
				for (var i=0; i < element.childNodes.length; i++) {
					traverse(element.childNodes[i]);
				}
			}
		
		
	}
	return elements;
}

function removeChildNodes(ref_node) {
	for (var i=0; i < ref_node.childNodes.length; i++) {
		ref_node.removeChild(ref_node.childNodes.item(i));
	}
}

/*
 * Explorer 5, Netscape 6, Mozilla
 * return the form (-parent) element of a given element. Needed because of a Mozilla/Netscape 6 bug, the form 
 * is not properly reflected into the attribute as required by old DOM
 */
function getForm(element) {
	if (element == null) { return; }
	while (element.nodeName != "FORM") {
	if (element.parentNode) {
			element = element.parentNode;
		}
	}
	return element;
}