/*
This JavaScript handles forms - some user interface improvements, and validation
Written by Matt Dolan, May 2010
*/


var FormNanny = Class.create({

	fieldFocus: function (ev) {
		var ip = Event.element(ev),
			div = Event.findElement(ev,'.mod_form-row');
		if (!div) return false;
		
		div.addClassName('focused');
		
		var ipOffset = ip.cumulativeOffset(),
			ipDims = ip.getDimensions();
		// show notes
		div.notes.each(function(notes){
			notes.show();
			notes.displaced = false;
			notes.setStyle({left:ipOffset.left+ipDims.width+'px',
							top:ipOffset.top - notes.getHeight()+'px'});
		},this);
	},

	fieldBlur: function (ev) {
		var div = Event.findElement(ev,'.mod_form-row');
		if (!div) return false;		
		div.removeClassName('focused');

		// hide notes
		div.notes.invoke('hide');
	},
	
	fieldChange: function (ev) {
		var el = Event.element(ev);
		el.addClassName('modified');	
	},
	
	checkboxChange: function (ev) {
		var div = Event.findElement(ev,'.mod_form-row');
		div.toggleClassName('checked');
	},

	moveNotes: function (ev) {
		var el = Event.findElement(ev,'.mod_form-notes');
		if (el.displaced)
			el.setStyle({top:parseInt(el.style.top)+el.getHeight()+'px'});
		else
			el.setStyle({top:parseInt(el.style.top)-el.getHeight()+'px'});
		el.displaced = !el.displaced;
	},
	
	/* The RECAPTCHA is hidden until the user starts filling in the form. Cos it looks nicer that way. */
	showRecaptcha: function (ev) {
		if (window.Effect && Effect.Morph) {
			new Effect.BlindDown(this.recaptchaDiv);
		} else {
			this.recaptchaDiv.show();
		}
		this.form.select('input, textarea, select').invoke('stopObserving','keyup',this.showRecaptchaBound);
	},
	
	initRow: function (div) {
		if (!this.fieldFocusBound) 
			this.fieldFocusBound = this.fieldFocus.bind(this);
		if (!this.fieldBlurBound) 
			this.fieldBlurBound = this.fieldBlur.bind(this);
		if (!this.fieldChangeBound) 
			this.fieldChangeBound = this.fieldChange.bind(this);
		if (!this.checkboxChangeBound)
			this.checkboxChangeBound = this.checkboxChange.bind(this);
		div.select('input, textarea, select').each(function(input){
			input.observe('focus',this.fieldFocusBound)
				.observe('blur',this.fieldBlurBound)
				.observe('change',this.fieldChangeBound);
			if (input.type=='checkbox') {
				if (input.checked)
					div.addClassName('checked');
				input.observe('change',this.checkboxChangeBound);
			}
		},this);
		
		div.notes = div.select('.mod_form-notes')
		div.notes.each(function(notes){
			Element.insert(document.body,notes.hide());
			notes.observe('mouseover',this.moveNotes.bind(this));
		},this);
	},
	
	clearInvalid: function (ev) {
		var el = Event.element(ev),
			div = el.up('div.mod_form-row');
		if (div)
			div.removeClassName('invalid');
		el.stopObserving('blur',this.clearInvalidBound);
	},
	
	validationFailed: function (el,blank) {
		
		// numberbox is within a div
		var label = (el.hasClassName('numberBox')) ? el.up().previous('label') : el.previous('label');
		if (label)
			var name = label.innerHTML.stripTags().replace(/[*:\s-]+$/,'').strip();
		if (blank)
			alert('\''+name+'\' is required');
		else
			alert("Something is wrong with the value you entered for '"+name+"'.\n\nPlease check it and try again.");
		
		// scroll window to element, if it's not already within the viewport
		var elTop = el.cumulativeOffset().top,
			domOff = document.viewport.getScrollOffsets(),
			domHeight = document.viewport.getHeight();		
		if (elTop<domOff.top || elTop>domOff.top+domHeight)
			window.scrollTo(domOff.left,Math.max(elTop-100,0));

		(el.activate&&el.activate()) || el.focus();
		el.up('div.mod_form-row').addClassName('invalid');
				
		if (!this.clearInvalidBound)
			this.clearInvalidBound = this.clearInvalid.bind(this);
		
		el.observe('blur',this.clearInvalidBound);
		return false;
	},
	
	formSubmit: function (ev) {
		var inputs = this.form.select('input','textarea', 'select'),
			regex,
			fields = inputs.collect(function(el){ return el.name.match(/^form[0-9]+_/);}).uniq();
		
		// ensure no rows are marked as invalid
		this.rows.invoke('removeClassName','invalid');
		
		for (var i=0;i<inputs.length;i++) {
			if (inputs[i].hasAttribute('_mod_form_req') && inputs[i].value=='') {
				Event.stop(ev);
				this.validationFailed(inputs[i],true);
				return false;
				
			// if the field is not required, only validate it's not empty
			} else if (inputs[i].getValue() && inputs[i].hasAttribute('_mod_form_val')) {
				switch (inputs[i].readAttribute('_mod_form_val')) {
					case '__email':
						regex = /^([a-zA-Z0-9_\-\.+&]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
						break;
					case '__web':
						//regex = /^((https?|ftp):\/\/)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/
						regex = /^([a-z]{3,}):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?!^=%&amp;:/~\+#]*[\w\-\@?!^=%&amp;/~\+#])?$/
						break;
					case '__phone': 
						regex = /^[0-9+ #.()]{5,}$/; // at least 5 characters
						break;
					case '__regex':
						var r = inputs[i].readAttribute('_mod_form_valregex'),
							mods = '',
							m;
						if (!r) continue;
						if (m = r.match(/^\/(.*?)\/([igm]*)$/i)) {
							r = m[1];
							mods = m[2];
						}
						try {
							regex = new RegExp(r,mods);
						} catch (e) { } // if the regex throws an error, we'll just have to ignore it
						break;
				}
				if (regex && !inputs[i].value.match(regex)) {
					Event.stop(ev);
					this.validationFailed(inputs[i]);
					return false;
				}
			} // if
		} // for
		
		var recaptchaEl = $('recaptcha_response_field');
		if (recaptchaEl && !recaptchaEl.value.match(/[^\s]+/)) {
			Event.stop(ev);
			alert("Please enter the two words displayed. This helps fight spam.\n\nIf you can't read the words, click the 'circular arrows' button to get new ones.");
			recaptchaEl.focus();
			return false;
		}
	},
	
	
	clearSubmittedData: function(ev) {
		var el = Event.element(ev),
			m = el.href.match(/[?&]form=([0-9]+)/);
			
		if (!m) return;
		
		Event.stop(ev);

		var formID = m[1];

		new Ajax.Request('/_mod/form',{method:'post',parameters:{
									action: 'clearSubmittedData',
									form: formID,
									_comm_mode: 'ajax'
								},
								onComplete: this.submittedDataCleared.bind(this,el)
							});
							
		// clear form
		el.up('form').select('input[type=text], input[type=checkbox], input[type=radio], textarea').each(function(el){ 
			try { 
				el.value = '';
				el.removeClassName('invalid');
			} catch (err) { console.error(err); }
		});
		el.hide();
		try { $('mod_form_'+this.formID+'_msg').hide(); } catch (err) {}
	},
	
	submittedDataCleared: function (el,r) {
		if (!r.headerJSON || !r.headerJSON.success) {
			alert('An error occurred attempting to clear the form');
			el.show();
			el.up('form').reset();
		}
	},
	
	initialize: function (form) {
		this.form = $(form);
		this.form.observe('submit',this.formSubmit.bind(this));
		
		this.formID = this.form.form.value;
		
		this.form.select('.numberBox').each(function(el){ new NumberBox(el); });
		
		this.form.select('.mod_form-notes').invoke('addClassName','floating');
		
		this.rows = this.form.select('.mod_form-row');
		this.rows.each(this.initRow,this);
		
		$$('.mod_form-forget').invoke('observe','click',this.clearSubmittedData.bind(this));
		
		this.recaptchaDiv = $('form'+this.formID+'_recaptcha');
		if (this.recaptchaDiv && !this.recaptchaDiv.hasClassName('recaptcha_visible')) {
			this.recaptchaDiv.hide();
			this.showRecaptchaBound = this.showRecaptcha.bind(this);
			this.form.select('input, textarea, select').invoke('observe','keyup',this.showRecaptchaBound);
		}
		
		if ($('recaptcha_reload'))
			$('recaptcha_reload').observe('click',function(ev) { Event.stop(ev); Recaptcha.reload(); });
		if ($('recaptcha_switch_audio'))
			$('recaptcha_switch_audio').observe('click',function(ev) { Event.stop(ev); Recaptcha.switch_type('audio'); });
		if ($('recaptcha_switch_image'))
			$('recaptcha_switch_image').observe('click',function(ev) { Event.stop(ev); Recaptcha.switch_type('image'); });
		if ($('recaptcha_help'))
			$('recaptcha_help').observe('click',function(ev) { Event.stop(ev); Recaptcha.showhelp(); });
	}
	

});


/* Find forms */
document.observe('dom:loaded',function() {
	$$('form.mod_form').each(function(form) { form.mod_form_nanny = new FormNanny(form); });
});




/*********************************
*** Number Box 
**********************************/
if (typeof NumberBox == 'undefined')
NumberBox = Class.create({
	
	getValue: function () { return this.el.value; },
	
	initialize: function (el,options) {
		if (!options) options = {};
		
		this.el = el = $(el);
		if (!el || !Object.isElement(el) || el.numberBoxInitialised) return false;
	
		/* parameters - can be passed as (in order of priority)
			- properties of the input DOM object
			- in the options object passed when initialising - e.g. NumberBox(el,{min:0,max:10,delta:2})
			- as attributes of the HTML tag  - e.g. <input type="text" minvalue="0" maxvalue="10" delta="2" />
		*/
		el.minValue = el.minValue || (options.min || (el.readAttribute('_nb_min') || 0));
		el.maxValue = el.maxValue || (options.max || el.readAttribute('_nb_max'));
		el.delta = el.delta || (options.delta || (el.readAttribute('_nb_delta') || 10));
		
		el.observe('keydown',this.numberBoxKeyDown.bind(this))
			 .observe('blur',this.numberBoxBlur.bind(this))
			 .observe('keyup',this.numberBoxKeyUp.bind(this))
			 .observe('keypress',this.numberBoxKeyPress.bind(this))
			 .observe('change',this.numberBoxChange.bind(this))
			 .observe('focus',this.numberBoxFocus.bind(this));
		el.addClassName('numberBox');
		var inputHeight = el.getHeight();
		this.wrapper = new Element('div',{'class':'numberBoxWrap'});
		el.insert({before:this.wrapper});
		this.wrapper.insert(el);
		
		el.numberBoxUp = new Element('div',{'class':'numberBoxUp',tabindex:0})
								.observe('click',this.numberBoxUp.bind(this))
								.observe('keypress',this.numberBoxNudgeUpKeyPress.bind(this))
								.observe('blur',this.numberBoxNudgeBlur.bind(this));
		el.numberBoxDown = new Element('div',{'class':'numberBoxDown',tabindex:0})
								.observe('click',this.numberBoxDown.bind(this))
								.observe('keypress',this.numberBoxNudgeDownKeyPress.bind(this))
								.observe('blur',this.numberBoxNudgeBlur.bind(this));
								
		el.insert({after:new Element('div',{'class':'numberBoxArrows',style:(inputHeight)?'height:'+inputHeight+'px':''})
								.insert(el.numberBoxUp)
								.insert(el.numberBoxDown)});
		el.value = el.value.replace(/[^0-9]/g,'');
		this.autosize();
		// avoid it getting initialised twice!
		el.numberBoxInitialised = true;
	},
	
	autosize: function () {
		var width = 0;
		if (this.el.value.length>4)
			width = this.el.value.length*7;
		this.el.setStyle({width:((width>30)?width:30)+'px'});
	},
	
	numberBoxChange: function (ev) { this.autosize(); },
	
	numberBoxKeyDown: function (ev) {
		var k = ev.keyCode;
		// only permitted whitelisted keys
		if ( 
				(k==8) || // backspace
				(k==9) || // tab
				(k==13) || // return
				(k==16) || // shift
				(k==18) || // alt
				((k>=35)&&(k<=36)) || // page up/down
				((k>=37)&&(k<=40)) || // arrows
				(k==46) || // delete
				((k>=48)&&(k<=57)) || // 0-9
				((k>=96)&&(k<=105)) // 0-9 on number pad
			)
			return;
		else {
			//window.console && console.log && console.log('Key disallowed for NumberBox: %o (%o)',String.fromCharCode(k),k);
			Event.stop(ev);
		}
	},
	numberBoxKeyUp: function (ev) { this.autosize(); },
	numberBoxKeyPress: function (ev) {
		var keyCode = (ev.keyCode) ? ev.keyCode : ev.charCode;
		if (keyCode==Event.KEY_DOWN || keyCode==Event.KEY_UP) {
			this.numberBoxNudge((keyCode==Event.KEY_DOWN),(ev.shiftKey));
			Event.stop(ev);
		}
	},


	numberBoxFocus: function (ev) {
		this.tbHasFocus = true;
		this.fireChangeOnBlur = false;
	},
	numberBoxBlur: function (ev) {
		this.tbHasFocus = false;
		if (parseInt(this.el.value,10)<this.el.minValue)
			this.el.value = this.el.minValue;
		else if (this.el.maxValue && parseInt(this.el.value,10)>this.el.maxValue)
			this.el.value = this.el.maxValue;
			
		if (this.fireChangeOnBlur) {
			this.fire('change');
			this.fireChangeOnBlur = false;
		}
	},
	
	numberBoxUp: function (ev) { 
		this.numberBoxNudge(); 
		Event.stop(ev); 
	},
	numberBoxDown: function (ev) { 
		this.numberBoxNudge(true); 
		Event.stop(ev); 
	},
	
	numberBoxNudge: function (down,singleStep) {
		if (this.el.value=='') {
			this.el.value = (down && this.el.maxValue) ? this.el.maxValue : this.el.minValue;
		} else {
			var curVal = parseInt(this.el.value);
			var delta = (Object.isUndefined(singleStep)||!singleStep) ? 1 : this.el.delta;
			if (down==true)
				delta = -delta;
			if (curVal+delta>=this.el.minValue && (!this.el.maxValue || curVal+delta<=this.el.maxValue))
				this.el.value = curVal+delta;
		}
		this.fireChangeOnBlur = true; // will fire 'change' on textbox blur or nudge button blur
	},
	
	numberBoxNudgeBlur: function (ev) {
		if (this.fireChangeOnBlur) {
			this.fire('change');
			this.fireChangeOnBlur = false;
		}
	},
	
	numberBoxNudgeUpKeyPress: function (ev) {
		var KEY_SPACE = 32;
		var keyCode = (ev.keyCode) ? ev.keyCode : ev.charCode;
		if (keyCode==KEY_SPACE || keyCode==Event.KEY_RETURN) {
			this.numberBoxNudge(false,(ev.shiftKey));
			Event.stop(ev);
		}
	},
	numberBoxNudgeDownKeyPress: function (ev) {
		var KEY_SPACE = 32;
		var keyCode = (ev.keyCode) ? ev.keyCode : ev.charCode;
		if (keyCode==KEY_SPACE || keyCode==Event.KEY_RETURN) {
			this.numberBoxNudge(true,(ev.shiftKey));
			Event.stop(ev);
		}
	},
	
	// native event firing
	// from http://jehiah.cz/archive/firing-javascript-events-properly
	fire: function (eventName){
		if (document.createEventObject) { // dispatch for IE
			var evt = document.createEventObject();
			return this.el.fireEvent('on'+eventName,evt)
		} else {  // dispatch for firefox + others
			var evt = document.createEvent("HTMLEvents");
			evt.initEvent(eventName, true, true ); // event type,bubbling,cancelable
			return !this.el.dispatchEvent(evt);
		}
	}
});
