/*
 * $Id: form.js,v 1.5 2010/06/18 15:52:37 andrew Exp $
 *
 * Author:
 *	Andrew Saunders http://andrew.saunders.me.uk
 *
 * Description:
 *	Handles the client side validation of forms
 *
 * History:
 *	2008-08-06 AJS	- Declaration of v1.0 (had loads of beta, tests)
 *			  performs basic checking for text and selections.
 *	2008-09-02 AJS	- added suggest caching (inp.suggest_cache[inp.value] = text) to reduce fetches
 *
 * Required:
 *	common.js	- required BEFORE this include, for onLoad function.
 *	dom.js		- required for DOM accessing functions.
 *	rpc.js		- required for suggest input.
 *
 * Functions:
 *	Form.Init(form)
 *		- initialises the form, adding the callback functions to the onsubmit, reset button and so on...
 *
 *	Form.Default(form)
 *		- sets all the input fields to default value, or initial value if no default value was given.
 *
 *	Form.Reset(form)
 *		- resets the form to initial state.
 *
 *	Form.Validate(form)
 *		- performs the validation, returns true if its valid (submittable), else false.
 *
 *	Form.SetState(element, state, errorMsg)
 *		- sets the state of this input element's field to either ok, req, err. and
 *		  displays the errorMSg if its err.
 *
 *	Form.Suggest(inp, args, e)
 *		- called form Form.CT when they type keys, and suggest is enabled.
 *		  performs a rpc request for a json array of strings which are displayed and replace the curreont
 *		  input box when clicked, selected by keyboard
 *
 *	Form.CT(element, args, event)
 *		- called from onkeyup or onchange on text input to check the validity of modified text input.
 *		  element.value is validated and Form.SetState is used to signal user of input state.
 *		  event (new) is passed to be used for suggest inputs.
 *
 *	Form.CS(element, args)
 *		- called from onchange on a select to check the validity of the modified selection.
 *		  select option is validate and Form.SetState is called to set the state.
 *
 *	Form.CR(element, args)
 *		- called for radio buttons.  * NOT DONE *
 *
 *	Form.CC(element, args)
 *		- called for check boxes.  * NOT DONE *
 *
 *	Form.CD(element, args)
 *		- called for date input.  * NOT DONE *
 *
 *	... more to follow.
 */

if (!window.trace) { function trace(s) { } }
var Form = {
// variables for internal flaggin while calling callback functions
	flag:0,	// flag (1-initialising, 2-defaulting, 3-validating, 4-bluring, 5-suggesting)
	initing:false,		// flag that we're initialising
	isValid:false,		// flag to state if form is valid or not
	defaulting:false,	// flag to state if we are performing 'default' button
	validating:false,	// flag to state if we are validating, ie clicked 'submit' button
	cleanup:false,		// flag to state we're cleaning up the form for final submition
	blured:false,		// flag to state we are in a 'blured' state on the element
	suggesting:false,	// flag to state we're performing suggesting
	suggest_fetching:false,	// flat to state we are fetching a suggest list from the server
	suggest_cache:new Array(),

	// Form.init - initialises the form, adding the callback functions to the onsubmit, reset button and so on...
	init:function(form) {
		trace('init:function(form="'+form+'") {');

		For(form.$e('a', 'help'), function(a) {
			trace('added popHelp to "'+a.href+'"');
			a.onclick = popHelp;
		});
	
	// add our form validation to the onsubmit function, it one doesnt exist
		if (form.onsubmit == undefined) {
			form.onsubmit = function() {
				try {
					Form.validate(form);
				}
				catch(e) { }

				if (Form.isValid) {
					Form.cleanup = true;
					Form.touchAll(form);
				}

				trace('} return ('+Form.isValid+')');
				return Form.isValid;
			};
		}

	// add our form reset code
		if (form.onreset == undefined) {
			form.onreset = function() {
				var result = Form.doReset(form);
				trace('} return ('+result+')');
				return result;
			};
		}

	// for 1st time, lets hit the reset button to trigger all element checks
		Form.initing=true;
		Form.touchAll(form);
		Form.initing=false;

		trace('} - init:function(form="'+form+'")');
	},


	TF:function(l) {
		trace('TF:function(l) {');

		var fs = l.parentNode;

	// find the fieldset
		while (fs && (fs.tagName != 'FIELDSET'))
			fs = fs.parentNode;

		l = fs.firstChild;
		while (l && (l.tagName != 'LEGEND'))
			l = l.nextSibling;

		var div = l.nextSibling, img = l.firstChild;

	// find the div
		while (div && (div.tagName != 'DIV'))
			div = div.nextSibling;

	// find the img
		while (img && (img.tagName != 'IMG'))
			img = img.nextSibling;

	// fail if we could not find them both
		if (!div || !img) {
			trace('} - ');
			return;
		}

		if (div.style.display == 'block') {
			div.style.display = 'none';
			img.src = '/img/open.gif';
		}
		else {
			div.style.display = 'block';
			img.src = '/img/close.gif';
		}

		trace('} - TF:function(l)');
	},

	touch:function(element) {
		trace('touch:function(element) {');

		if ((element.type == 'text') || (element.type == 'password')) {
			if (element.onblur && !Form.validating)
				element.onblur();

			if (element.onkeyup)
				element.onkeyup();
		}
		else if ((element.type == 'radio')) {
			if (element.onclick)
				element.onclick();

		}
		else {
			if (element.onchange)
				element.onchange();
		}

		trace('} - touch:function(element)');
	},

	// Form.touchAll - performs a touch (onkeyup, onchange, ...) on all elements to trigger their check code (if it exists), used within defaulting, resetting and validating the form.
	touchAll:function(form) {
		trace('touchAll:function(form) {');

	// call all the elements onkeyup, onchange if they exists
		try {
			For(form.elements, Form.touch);
		}
		catch(e) { }

		trace('} - touchAll:function(form)');
	},

	// Form.doDefault - sets all the input fields to default value, or initial value if no default value was given.
	doDefault:function(form) {
		trace('doDefault:function(form) {');

	// set 'Form.Defaulting' flag to tell all functions to just default and finish
		Form.defaulting = true;

	// touch all the elements
		Form.touchAll(form);

	// unset the 'Form.Defaulting' flag
		Form.defaulting = false;

		trace('} - doDefault:function(form)');
	},

	// Form.doReset - resets the form to initial state.
	doReset:function(form) {
		trace('doReset:function(form) {');

	// actually call the reset function
	// simply touch all elements.
		setTimeout(
			function() {
				Form.touchAll(form);
			}, 0);

		trace('} - doReset:function(form)');
	},

	// Form.validate - performs the validation, returns true if its valid (submittable), else false.
	validate:function(form) {
		trace('validate:function(form) {');

	// set 'Form.Validating' flag to tell all functions to just default and finish
		Form.validating = true;

	// we start of as valid, then an element will set us as not valid
		Form.isValid = true;

	// touch all the elements
		Form.touchAll(form);

	// unset the 'Form.Validating' flag
		Form.validating = false;

		trace('} - validate:function(form)');
	},

	// Form.setState - sets the state of this input element's field to either ok, req, err. and displays the errorMSg if its err.
	setState:function(element, state, errorMsg) {
		trace('setState:function(element, state, errorMsg) {');

	//state = 0-ok, 1-req, 2-err
	// find the paragraph holding this element, used for css styling and location of image, error elements
		var para = element.parentNode, img, err;

		while (para && (para.tagName != 'P'))
			para = para.parentNode;

		if (!para) {
			trace('} - ');
			return;	// unable to process element with no paragraph!
		}

	// find the image and error text elements
		For(para.childNodes, function(subElement) {
			if (subElement.tagName == 'IMG')
				img = subElement;

			if (subElement.tagName == 'STRONG')
				err = subElement;
		});

		var msg = (state ? err.innerHTML : '');
		msg = msg.substr(0, msg.length-4);

		if (state && (Form.validating || (Form.blured && (state > 1)))) {
			if (Form.isValid) {
				element.focus();
				Form.isValid = false;
			}

			msg = errorMsg ? errorMsg : 'Required Entry Missing';
			//state = 2;
		}

		var stateStr = ['ok','req','err'][state];

		para.className = 'i '+stateStr;

		if (err) {
			err.className = msg ? 'err' : '';
			err.innerHTML = msg ? msg+'<br>':'';
		}

		if (img)
			img.src = '/img/f'+stateStr+'.png';

	// this removes googleToolbar's anoying auto fill color
		if (state == 2)
			element.style.backgroundColor='';

		trace('} - setState:function(element, state, errorMsg)');
	},

	// Form.cT - called from onkeyup or onchange on text input to check the validity of modified text input. element.value is validated and Form.SetState is used to signal user of input state. event (new) is passed to be used for suggest inputs.
	cT:function(element, args, event) {
		trace('cT:function(element, args, event) {');

	// args = {
	//	r	bool	- if element is required
	//	v	string	- regexp validation
	//	d	string	- default value
	//	e	string	- error message displayed if an error is present
	//	s	string	- suggest data
	//	h	string	- hint (displayed in the input field when present and blank!)
	//	b	bool	- if element is losing focus
	//	f	bool	- focus me in init

	// are we performing onblur ?
		if (args.b) {
			setTimeout(function() {
				Form.blured = true;
				if (element.onkeyup)
					element.onkeyup();
				Form.blured = false;
			}, 100);

			if (args.h && (element.value == '') && !Form.cleanup) {
				addClass(element, 'h');
				element.value = args.h;
			}

			trace('} - ');
			return;
		}

		if (args.h || Form.cleanup) {
			if (hasClass(element, 'h')) {
				element.value = '';
				remClass(element, 'h');
			}

//			trace('} - ');
//			return;
		}

		var ev = hasClass(element, 'h') ? '' : element.value;

		if (args.s != undefined) {
			Form.suggest(element, args, event);
		}

		if (Form.initing) {
			if (args.f)
				element.focus();
		}

		if (args.kf)
			element.focus();

		if (Form.defaulting)
			ev = element.value = args.d || '';
	
	// state of validation
		var state = 0;	// 0-ok, 1-req, 2-err

	// if its required, and there is no value currently, then its 1-req
		if (args.r && !ev)
			state = 1;	// req-required, but missing

	// otherwise, if its not required and there is a value, its 0-ok
		else if (!args.r && !ev)
			state = 0;	// ok-not required and not present

	// otherwise, if there is a validation rule, and its not valid, its 2-err
		else if (args.v && !ev.match(new RegExp(args.v)))
			state = 2;	// err-validatable and invalid

	// lets set the state!
		this.setState(element, state, args.e ? args.e : 'Invalid Entry');

		trace('} - cT:function(element, args, event)');
	},

	// Form.cS - called from onchange on a select to check the validity of the modified selection. select option is validate and Form.SetState is called to set the state.
	cS:function(element, args) {
		trace('cS:function(element, args) {');

		if (Form.defaulting) {
			for (var c = 0; c < element.options.length; c++) {
				if (element.options[c].value == (args.d||0)) {
					element.selectedIndex = c;
					break;
				}
			}
		}

		var state = 0, idx = element.selectedIndex;
//		if (idx)
//			alert(
		if (args.r && !idx)
			state = 1;

		Form.setState(element, state, args.e ? args.e : 'Invalid Selection');

		trace('} - cS:function(element, args)');
	},

	// Form.cR - called for radio buttons.  * NOT DONE *
	cR:function(element, args) {
		trace('cR:function(element, args) {');

	// call the first onclick only.
		if (args.i) {
			trace('} - ');
			return element.form.elements[element.name][0].onclick();
		}

		var idx = false;

	// find the element that is checked (if any!)
		for (var c = 0; c < element.form.elements[element.name].length; c++) {
			if (Form.defaulting)
				if (args.d && (element.form.elements[element.name][c].value == args.d)) {
					element.form.elements[element.name][c].checked = true;
					idx = c;
					break;
				}
				else if (element.form.elements[element.name][c].checked)
					element.form.elements[element.name][c].checked = false;

			if (element.form.elements[element.name][c].checked) {
				idx = c;
				break;
			}
		}

		Form.setState(element, args.r&&!(idx!==false) ? 1:0, args.e ? args.e : 'Invalid Selection');

		trace('} - cR:function(element, args)');
	},

	// Form.cT - called for tick buttons.  * NOT DONE *
	cC:function(element, args) {
		trace('cC:function(element, args) {');


		trace('} - cC:function(element, args)');
	},

	// Form.cD - called for date input.  * NOT DONE *
	cD:function(element, args) {
		trace('cD:function(element, args) {');


		trace('} - cD:function(element, args)');
	},

	// Form.suggest - called by Form.cT when its required to have a suggestion drop-down box.
	suggest:function(inp, args, e) {
		trace('suggest:function(inp, args, e) {');

		if (Form.initing) {
			inp.setAttribute('autocomplete', 'off');
			inp.old_value = inp.value;
			//Form.suggest_cache = new Array();
			trace('} - ');
			return;
		}

		var div = $('form_suggest');

	// check if it was a onchange event, if so, remove the suggest box now
		if (Form.blured) {
			var div = $('form_suggest');

			if (div)
				Form.suggest_end();

			trace('} - ');
			return;
		}

		// check if we are already fetching
		if (Form.suggest_fetching) {
			trace('} - ');
			return;
		}

		if (div) {
		// grab the key that was pressed (onkeyup event)
			var key = window.event ? event.keyCode : (e ? e.keyCode : false);

		// TODO: perform keyboard navigation on existing suggest box... (up,down,enter)
			if (key == 40) {
			// highlight next suggest item
				var next = div.firstChild;

				if (div.active) {
					var next = div.active.nextSibling;
				}
				if (next) {
					if (div.active)
						div.active.className = '';
					next.className = 'select';
					div.active = next;
					inp.value = div.active.innerHTML;
				}

				trace('} - ');
				return;
			}
			else if (key == 38) {
			// highlight prev suggest item
				var prev = div.lastChild;

				if (div.active) {
					var prev = div.active.previousSibling;
				}
				if (prev) {
					if (div.active)
						div.active.className = '';
					prev.className = 'select';
					div.active = prev;
					inp.value = div.active.innerHTML;
				}

				trace('} - ');
				return;
			}

			else if (key == 13) {
				Form.suggest_end();
				trace('} - ');
				return;
			}
		}

	// check if they've actually entered anything different!
		if ((Form.defaulting || Form.validating || (inp.value == inp.old_value) || !inp.value)) {
			trace('} - ');
			return;
		}

	// remember the new value
		inp.old_value = inp.value;

		if (Form.suggest_cache[inp.name+inp.value] != undefined) {
			Form.suggest_callback(inp, Form.suggest_cache[inp.name+inp.value]);

			trace('} - ');
			return;
		}
		
	// signal we are fetcing a suggest, so dont do any other fetching (ignore user!)
		Form.suggesting = true;
		Form.suggest_fetching = true;

	// build up our fetch url {args.s}?rnd=0.001&name={inp.name}&q={inp.value} args.s has replacable evals (%varname%)
		var url = args.s, start = 0, end, name;

	// for each find of %
		while ((start = url.indexOf('%', start)) != -1) {
		// if its %%, replace it with %
			if (start && url[start + 1] == '%')
				url = replace('%%', '%');
			else {
			// extract the name within %name%, and try to eval it
				end = url.indexOf('%', start + 1);
				name = url.substr(start + 1, end - start - 1);
				try {
					url = url.replace('%'+name+'%', eval(name))
				}
				catch(e) { }
			}
			start++;
		}

// we use time to skip cache after 5 minutes, its enough!
		var cache_skip = new Date();
		cache_skip = Math.floor((cache_skip.getTime()));
//		cache_skip = Math.floor((cache_skip.getTime() / (1000 * 60 * 5)));
		url = url+((url.indexOf('?') != -1) ? '&' : '?') + 'n='+escape(inp.name)+'&q='+escape(inp.value)+'&'+cache_skip;

		rpc({url:url,
			onSuccess:function(result) {
				trace('onSuccess:function(result) {');

				Form.suggest_callback(inp, result.text);
			}
		});
/*			rpc({url:s+((s.indexOf('?')!=-1)?'&':'?')+'rnd='+Math.random()+'&name='+escape(i.name)+'&v='+escape(i.value),
			     onSuccess:function(a){
			     trace('onSuccess:function(a){');

				suggest2(j,a.text);
				j.suggestrpc=0;
			     }
			    })
		setTimeout(function(){

	
			}, 250);
*/
/*

*/

		trace('} - suggest:function(inp, args, e)');
	},

	suggest_callback:function(inp, text) {
		trace('suggest_callback:function(inp, text) {');

		Form.suggest_end();

		Form.suggest_cache[inp.name+inp.value] = text;


	// create variable representing the fetched text
		var entries;

		try {
			entries = eval(text);
		}
		catch(e) {
			trace('} - ');
			return;
		}

		if (entries.length == 0) {
			trace('} - ');
			return;
		}

	// create one
		var div = create('DIV', {'id':'form_suggest'});

	// set the div under the input, and clear it
		div.style.width = inp.clientWidth+'px';
		div.style.position = 'absolute';
		var pos = absPos(inp);
		if (ie && !ie8) {
			div.style.marginTop = (inp.clientHeight+9)+"px";
			div.style.marginLeft = (-inp.clientWidth-6)+"px";
		}
		div.innerHTML = '';

	// add our text
		For(entries, function(entry) {
		// entry={'id':'...','value':'...'}
			var p = create('P', {'innerHTML':entry[0], 'title':entry[0]+(entry[1]?' ('+entry[1]+')':''), 'className':'eee'});

			p.onmousedown = function() {
				this.parentNode.active = this;
				inp.value = this.innerHTML;
				Form.suggest_end();
				inp.onblur();
			};

//p.style.class='hover';
			p.onmouseover = function() {
				if (this.parentNode.active) {
					this.parentNode.active.className='';
				}
				this.parentNode.active = p;
				this.className='select';
			};

			p.onmouseout = function() {
				this.className='';
			};

			div.appendChild(p);
		});

		inp.parentNode.appendChild(div);
//		insertAfter(inp, div);
//		insertAfter(inp, document.createElement('BR'));

		trace('} - suggest_callback:function(inp, text)');
	},

	suggest_end:function() {
		trace('suggest_end:function() {');

	// clear the suggest fetching flag!
		Form.suggest_fetching = false;
		Form.suggesting = false;

	// locate or create our suggest box
		var div = $('form_suggest');

		if (div) {
		// remove it
//			div.parentNode.removeChild(div.previousSibling);
			div.parentNode.removeChild(div);
		}

		trace('} - suggest_end:function()');
	},

//	tableAdd:function(el, max) {
//	// find the row we're on
//		var tr = el.parentNode;
//
//		while (tr && (tr.tagName != 'TR'))
//			tr = tr.parentNode;
//
//		if (!tr)
//			return;	// cant find the tr to copy
//
//		if (tr.parentNode.childNodes.length >= max)
//			return;	// no more elements allowed!
//
//		var newtr = document.createElement('TR');
//
// for (var i=0; i<tr.cells.length; i++) {
//   var oldCell = tr.cells[i];
//   var newCell = document.createElement("TD");
//   newCell.innerHTML = oldCell.innerHTML;
//   newtr.appendChild(newCell);
// }
//
//// find and change all inputs to have arrayed in ALL
//	//	newtr.class = tr.class;
//	//	newtr.innerHTML = tr.innerHTML;
//		tr.parentNode.appendChild(newtr);
////		alert(tr.);
//
//
//
////<td>	<p style="width: 25em;" class="i req">
////		<span>P<u>r</u>oduct Description:</span>
////		<img src="/img/freq.png" alt="req">
////		<br><strong class=""></strong>
////		<input autocomplete="off" name="product" class="text" tabindex="3" style="width: 25em;" accesskey="r" size="80" onblur="Form.cT(this, {b:1}, event)" onkeyup="Form.cT(this, {r:1,v:'...+',e:'Too small, enter valid product description name',s:'query_product.php?for=%inp.form.for.value%'}, event)" type="text">
////	</p>
////&nbsp;</td><td>	<p style="width: 8em;" class="i req">
////		<span><u>E</u>nter Size:</span>
////		<img src="/img/freq.png" alt="req">
////		<br><strong class=""></strong>
////		<input name="size" class="text" tabindex="4" style="width: 8em;" accesskey="e" onblur="Form.cT(this, {b:1}, event)" onkeyup="Form.cT(this, {r:1,v:'^.+$',e:'Size is required'}, event)" type="text">
////	</p>
////&nbsp;</td><td>	<p style="width: 4.3em;" class="i ok">
////		<span><u>Q</u>ty:</span>
////		<img src="/img/fok.png" alt="req">
////		<br><strong class=""></strong>
////		<input name="qty" class="text" tabindex="5" value="1" style="width: 4.3em;" accesskey="q" maxlength="3" size="3" onblur="Form.cT(this, {b:1}, event)" onkeyup="Form.cT(this, {r:1,v:'^ *[0-9]+ *$',d:'1',e:'Invalid Quantity'}, event)" type="text">
////	</p>
////&nbsp;</td><td><a href="#" onclick="try { Form.tableAdd(this); } catch(e) { }; return false;">(+)</a>	</td>
////
//
//
//
//	},

	zz:0
};

// small onload function which calls Form.Init with EVERY form on the page
addOnLoad(function() {
	trace('addOnLoad(function() {');

// call Form.Init for each form we find
	For($e('form'), Form.init);

	trace('} - addOnLoad(function()');
});



