 /**
 * @author Robert
 */

var fabrikForm = new Class( {

	initialize : function(id) {
		this.id = id;
		this.options = Object.extend( {
			'admin':false,
			'postMethod':'post',
			'primaryKey':null,
			'error':'',
			'delayedEvents':false,
			'updatedMsg':'Form saved',
			'liveSite':'',
			'pages':[],
			'page_save_groups':[],
			'start_page':0,
			'ajaxValidation':false,
			'customJsAction':'',
			'plugins':[],
			'ajaxmethod':'post',
			'mooversion':1.1
		}, arguments[1] || {});
		this.options.pages = $H(this.options.pages);
		this.subGroups = $H({});
		this.lang = Object.extend( {
			'validation_altered_content' :'The validation has altered your content:',
			'validating' :'Validating',
			'success' :'Success',
			'nodata': 'No data',
			validation_error:'Validation error',
			form_saved:'Form saved'
		}, arguments[2] || {});
		this.currentPage = this.options.start_page;
		this.formElements = $H({});
		this.delGroupJS = $H({});
		this.duplicateGroupJS = $H({});
		this.listenTo = $A([]);
		this.bufferedEvents = $A([]);
		this.duplicatedGroups = $H();
		this.clickDeleteGroup = this.deleteGroup.bindAsEventListener(this);
		this.clickDuplicateGroup = this.duplicateGroup.bindAsEventListener(this);
	
		this.fx = {};
		this.fx.elements = [];
		this.fx.validations = {};
		window.addEvent('domready', this.setUpAll.bindAsEventListener(this));	
	},
	
	setUpAll: function()
	{
		this.setUp();
		this.winScroller = new Fx.Scroll(window);
		this.watchAddOptions();
		$H(this.options.hiddenGroup).each(function(v, k){
			if(v == true){
				var subGroup = $('group'+k).getElement('.fabrikSubGroup');
				this.subGroups.set(k, subGroup.cloneWithIds());
				this.hideLastGroup(k, subGroup);
			}
		}.bind(this));
		
		// get an int from which to start incrementing for each repeated group id
		// dont ever decrease this value when deleteing a group as it will cause all sorts of
		// reference chaos with cascading dropdowns etc
		this.repeatGroupMarkers = $H({});
		this.form.getElements('.fabrikGroup').each(function(group){
			var id = group.id.replace('group', '');
			var c = group.getElements('.fabrikSubGroup').length;
			this.repeatGroupMarkers.set(id, c);
		}.bind(this));
		
		//testing prev/next buttons
		var v = this.options.editable === true ? 'form':'details';
		var editopts = {
        option: 'com_fabrik',
        'view': v,
        'controller':'form',
        'task':'getNextRecord',
        'fabrik': this.id,
        'rowid': this.form.getElement('input[name=rowid]').value,
        'format': 'raw',
        'task':'paginate',
        'dir':1
      };
		['.previous-record', '.next-record'].each(function(b, dir){
			editopts.dir = dir;
			if(this.form.getElement(b)){
				
				var myAjax = new Ajax('index.php', {
					method :this.options.ajaxmethod,
					data:editopts,
					onComplete : function(r) {
						oPackage.stopLoading(this.getBlock());
						r = Json.evaluate(r);
						this.update(r);
						this.form.getElement('input[name=rowid]').value = r.post.rowid;
					}.bind(this)
				});
				
				this.form.getElement(b).addEvent('click', function(e){
					myAjax.options.data.rowid = this.form.getElement('input[name=rowid]').value;
					new Event(e).stop();
					oPackage.startLoading(this.getBlock(), 'loading');
					myAjax.request();
				}.bind(this));
			}			
		}.bind(this));
	},

	watchAddOptions : function() {
		this.fx.addOptions = [];
		this.getForm().getElements('.addoption').each( function(d) {
		var a = d.getParent().getElement('.toggle-addoption');
		var mySlider = new Fx.Slide(d, {
			duration :500
		});
		mySlider.hide();
		a.addEvent('click', function(e) {
		new Event(e).stop();
		mySlider.toggle();
		});
		});
	},

	setUp : function() {
		this.form = this.getForm();
		this.watchGroupButtons();
		if (this.options.editable) {
			this.watchSubmit();
		}
		this.createPages();
		this.watchClearSession();
	},

	getForm : function() {
		this.form = $(this.getBlock());
		return this.form;
	},
	
	getBlock: function(){
		return this.options.editable == true ? 'form_' + this.id : 'details_' + this.id;
	},

	// id is the element or group to apply the fx TO, triggered from another
	// element
	addElementFX : function(id) {
		id = id.replace('fabrik_trigger_', '');
		if (id.slice(0, 6) == 'group_') {
			id = id.slice(6, id.length);
			var k = id;
			var c = $(id);
		} else {
			id = id.slice(8, id.length);
			k = 'element' + id;
			if (!$(id)) {
				return;
			}
			c = $(id).findClassUp('fabrikElementContainer');
		}
		if (c) {
			// c will be the <li> element - you can't apply fx's to this as it makes the
			// DOM squiffy with
			// multi column rows, so get the li's content and put it inside a div which
			// is injected into c
			// apply fx to div rather than li - damn im good
			if ((c).getTag() == 'li') {
				var fxdiv = new Element('div').adopt(c.getChildren());
				c.empty();
				fxdiv.injectInside(c);
			} else {
				fxdiv = c;
			}
	
			var opts = {
				duration :800,
				transition :Fx.Transitions.Sine.easeInOut
			};
			this.fx.elements[k] = {};
			this.fx.elements[k].css = fxdiv.effect('opacity', opts);
			if ($type(fxdiv) != false) {
				this.fx.elements[k]['slide'] = new Fx.Slide(fxdiv, opts);
			} else {
				this.fx.elements[k]['slide'] = null;
			}
		}
	},

	doElementFX : function(id, method) {
		id = id.replace('fabrik_trigger_', '');
		if (id.slice(0, 6) == 'group_') {
			id = id.slice(6, id.length);
			//wierd fix?
			if (id.slice(0, 6) == 'group_') id = id.slice(6, id.length);
			var k = id;
			var groupfx = true;
		} else {
			groupfx = false;
			id = id.slice(8, id.length);
			k = 'element' + id;
		}
		var fx = this.fx.elements[k];
		if (!fx) {
			return;
		}
		var fxElement = groupfx ? fx.css.element : fx.css.element.findClassUp('fabrikElementContainer');
		switch (method) {
		case 'show':
			fxElement.removeClass('fabrikHide');
			fx.css.set(1);
			fx.css.element.show();
			break;
		case 'hide':
			fxElement.addClass('fabrikHide');
			fx.css.set(0);
			fx.css.element.hide();
			break;
		case 'fadein':
			fxElement.removeClass('fabrikHide');
			if (fx.css.lastMethod !== 'fadein') {
				fx.css.element.show();
				fx.css.start(0, 1);
			}
			break;
		case 'fadeout':
			if (fx.css.lastMethod !== 'fadeout') {
				fx.css.start(1, 0).chain( function() {
				fx.css.element.hide();
				fxElement.addClass('fabrikHide');
				});
			}
			break;
		case 'slide in':
			fx.slide.slideIn();
			break;
		case 'slide out':
			fx.slide.slideOut();
			fxElement.removeClass('fabrikHide');
			break;
		case 'slide toggle':
			fx.slide.toggle();
			//fxElement.toggleClass('fabrikHide');
			break;
		}
		fx.lastMethod = method;
		this.runPlugins('onDoElementFX', null);
	},

	watchClearSession : function() {
		if (this.form && this.form.getElement('.clearSession')) {
			this.form.getElement('.clearSession').addEvent('click', function(e) {
				new Event(e).stop();
				this.form.getElement('input[name=task]').value = 'removeSession';
				this.clearForm();
				this.form.submit();
			}.bind(this));
		}
	},

	createPages : function() {
		if (this.options.pages.keys().length > 1) {
			//wrap each page in its own div
			this.options.pages.each(function(page, i){
				var p = new Element('div', {'class': 'page', 'id':'page_' + i});
				p.injectBefore($('group'+page[0]));
				page.each(function(group){
					p.adopt($('group'+group));
				});
			});
			if ($('fabrikSubmit' + this.id)) {
				$('fabrikSubmit' + this.id).disabled = "disabled";
			}
			this.form.getElement('.fabrikPagePrevious').disabled = "disabled";
			this.form.getElement('.fabrikPageNext').addEvent('click',
					this._doPageNav.bindAsEventListener(this, [ 1 ]));
			this.form.getElement('.fabrikPagePrevious').addEvent('click',
					this._doPageNav.bindAsEventListener(this, [ -1 ]));
			this.setPageButtons();
			this.hideOtherPages();
		}
	},

	_doPageNav : function(e, dir) {
		
		this.form.getElement('.fabrikMainError').addClass('fabrikHide');
		//if tip shown at bottom of long page and next page shorter we need to move the tip to
		//the top of the page to avoid large space appearing at the bottom of the page.
		if($type(document.getElement('.tool-tip')) !== false){
			document.getElement('.tool-tip').setStyle('top',0);
		}
		var url = this.options.liveSite
				+ 'index.php?option=com_fabrik&controller=form&format=raw&task=ajax_validate&form_id='
				+ this.id;
		oPackage.startLoading(this.getBlock(), 'validating');
		
		// only validate the current groups elements, otherwise validations on 
		//other pages cause the form to show an error.
		
		var groupId = this.options.pages.get(this.currentPage.toInt());
		
		var d = $H(this.getFormData());
		d.set('controller', 'form');
		d.set('task', 'ajax_validate');
		d.set('fabrik_postMethod', 'ajax');
		d.set('format', 'raw');
		
		d = this._prepareRepeatsForAjax(d);

		var myAjax = new Ajax(url, {
			method :this.options.ajaxmethod,
			data :d,
			onComplete : function(r) {
			oPackage.stopLoading(this.getBlock());
			r = Json.evaluate(r);
			if (this._showGroupError(r, d) == false) {
				this.changePage(dir);
				this.saveGroupsToDb();
			}
			new Fx.Scroll(window).toElement(this.form);
			}.bind(this)
		}).request();
	
		var event = new Event(e).stop();
	},

	saveGroupsToDb : function() {
		if(!this.runPlugins('saveGroupsToDb', null)){
			return;
		}
		var orig = this.form.getElement('input[name=format]').value;
		var origprocess = this.form.getElement('input[name=task]').value;
		this.form.getElement('input[name=format]').value = 'raw';
		this.form.getElement('input[name=task]').value = 'savepage';
	
		var url = this.options.liveSite
				+ 'index.php?option=com_fabrik&format=raw&page=' + this.currentPage;
		oPackage.startLoading(this.getBlock(), 'saving page');
		var data = this.getFormData();
		new Ajax(url, {
			method :this.options.ajaxmethod,
			data :data,
			onComplete : function(r) {
				if(!this.runPlugins('onCompleteSaveGroupsToDb', null)){
					return;
				}
				this.form.getElement('input[name=format]').value = orig;
				this.form.getElement('input[name=task]').value = origprocess;
				if (this.options.postMethod == 'ajax') {
					oPackage.sendMessage(this.getBlock(), 'updateRows', 'ok', json);
				}
				oPackage.stopLoading(this.getBlock());
			}.bind(this)
		}).request();
	},
	
	changePage : function(dir) {
		if(!this.runPlugins('onChangePage', null)){
			return;
		}
		this.currentPage = this.currentPage.toInt();
		// hide all error messages ($$$ rob why would we want to do that? - commneting out)
		//this.form.getElements('.fabrikError').addClass('fabrikHide');
		if (this.currentPage + dir >= 0 && this.currentPage + dir < this.options.pages.keys().length) {
			this.currentPage = this.currentPage + dir;
			if (!this.pageGroupsVisible()){
				this.changePage(dir);
			}
		}
		
		this.setPageButtons();
		$('page_' + this.currentPage).setStyle('display', '');
		this.hideOtherPages();
		if(!this.runPlugins('onEndChangePage', null)){
			return;
		}
	},
	
	pageGroupsVisible: function()
	{
		var visible = false;
		this.options.pages.get(this.currentPage).each(function(gid){
			if ($('group'+gid).getStyle('display') != 'none'){
				visible = true;
			}
		});
		return visible;	
	},
	
	/**
	 * hide all groups except those in the active page
	 */
	hideOtherPages:function(){
		this.options.pages.each( function(gids, i) {
			if (i != this.currentPage) {
	  		$('page_' + i).setStyle('display', 'none');
	  	}
		}.bind(this));
	},

	setPageButtons : function() {
		if (this.currentPage == this.options.pages.keys().length - 1) {
			if ($('fabrikSubmit' + this.id))
				$('fabrikSubmit' + this.id).disabled = "";
			this.form.getElement('.fabrikPageNext').disabled = "disabled";
			this.form.getElement('.fabrikPageNext').setStyle('opacity', 0.5);
		} else {
			this.form.getElement('.fabrikPageNext').disabled = "";
			this.form.getElement('.fabrikPageNext').setStyle('opacity', 1);
		}
		if (this.currentPage === 0) {
			this.form.getElement('.fabrikPagePrevious').disabled = "disabled";
			this.form.getElement('.fabrikPagePrevious').setStyle('opacity', 0.5);
		} else {
			this.form.getElement('.fabrikPagePrevious').disabled = "";
			this.form.getElement('.fabrikPagePrevious').setStyle('opacity', 1);
		}
	},

	addElements : function(a) {
		a = $H(a);
		a.each(function(elements, gid){
			elements.each(function(el){
				if ($type(el) !== false) {
					this.addElement(el, el.options.element, gid);
				}
			}.bind(this));
		}.bind(this));
	},

	addElement: function(oEl, elId, gid){
		elId = elId.replace('[]', '');
		oEl.form = this;
		oEl.groupid = gid;
		this.formElements.set(elId, oEl);
		try {
			oEl.attachedToForm();
		} catch (err) {
		}
	},
	
	// we have to buffer the events in a pop up window as
	// the dom inserted when the window loads appears after the ajax evalscripts
	
	dispatchEvent : function(elementType, elementId, action, js) {
		if (!this.options.delayedEvents) {
			var el = this.formElements.get(elementId);
			if (el && js != '') {
				el.addNewEvent(action, js);
			}
		} else {
			this.bufferEvent(elementType, elementId, action, js);
		}
	},

	bufferEvent : function(elementType, elementId, action, js) {
		this.bufferedEvents.push( [ elementType, elementId, action, js ]);
	},
	
	// call this after the popup window has loaded
	processBufferEvents : function() {
		this.setUp();
		this.options.delayedEvents = false;
		this.bufferedEvents.each( function(r) {
		// refresh the element ref
				var elementId = r[1];
				var el = this.formElements.get(elementId);
				el.element = $(elementId);
				this.dispatchEvent(r[0], elementId, r[2], r[3]);
				}.bind(this));
		},
	
		action : function(task, element) {
		var oEl = this.formElements.get(el);
		eval('oEl.' + task + '()');
	},
	
	triggerEvents: function(el){
		this.formElements.get(el).fireEvents(arguments[1]);
	},

	/**
	 * @param string
	 *          element id to observe
	 * @param string
	 *          error div for element
	 * @param string
	 *          parent element id - eg for datetime's time field this is the date
	 *          fields id
	 */
	watchValidation : function(id, triggerEvent) {
		if (this.options.ajaxValidation == false) {
			return;
		}
		if ($(id).className == 'fabrikSubElementContainer') {
			// check for things like radio buttons & checkboxes
			
			$(id).getElements('.fabrikinput').each(
					function(i) {
					i.addEvent(triggerEvent, this.doElementValidation.bindAsEventListener(this, [true]));
					}.bind(this));
			return;
		}
		$(id).addEvent(triggerEvent, this.doElementValidation.bindAsEventListener(this, [false]));
	},

	// as well as being called from watchValidation can be called from other
	// element js actions, e.g. date picker closing
	doElementValidation : function(event, subEl, replacetxt) {
		if (this.options.ajaxValidation == false) {
			return;
		}
		replacetxt = $type(replacetxt) == false ? '_time' : replacetxt;
		if ($type(event) == 'event' || $type(event) == 'object') { // type object in
			var e = new Event(event);
			var id = e.target.id;
		// for elements with subelements eg checkboxes radiobuttons
			if (subEl == true) {
				id = $(e.target).findClassUp('fabrikSubElementContainer').id;
			}
		} else {
			// hack for closing date picker where it seems the event object isnt
			// available
			id = event;
		}
	// for elements with subelements eg checkboxes radiobuttons
		/*if (subEl == true) {
			id = $(e.target).findClassUp('fabrikSubElementContainer').id;
		}*/
		if($type($(id)) === false){
			return;
		}
		if($(id).getProperty('readonly') === true || $(id).getProperty('readonly') == 'readonly'){
			//stops date element being validated 
			//return;
		}
		var el = this.formElements.get(id);
		if (!el) {
			//silly catch for date elements you cant do the usual method of setting the id in the 
			//fabrikSubElementContainer as its required to be on the date element for the calendar to work
			id = id.replace(replacetxt, '');
			el = this.formElements.get(id);
			if(!el){
				return;
			}
		}
		if(!this.runPlugins('onStartElementValidation', event)){
			return;
		}
		el.setErrorMessage(this.lang.validating, 'fabrikValidating');
		
		var d = $H(this.getFormData());
		d.set('controller', 'form');
		d.set('task', 'ajax_validate');
		d.set('fabrik_postMethod', 'ajax');
		d.set('format', 'raw');

		d = this._prepareRepeatsForAjax(d);
	
		var origid = el.origId ? el.origId : id;
		el.options.repeatCounter = el.options.repeatCounter ? el.options.repeatCounter : 0;
		var url = this.options.liveSite + 'index.php?option=com_fabrik&form_id=' + this.id;
		var myAjax = new Ajax(url, {
			method :this.options.ajaxmethod,
			data :d,
			onComplete :this._completeValidaton.bindAsEventListener(this, [ id, origid ])
		}).request();
	},

	_completeValidaton : function(r, id, origid) {
		r = Json.evaluate(r);
		if(!this.runPlugins('onCompleteElementValidation', null)){
			return;
		}
		var el = this.formElements.get(id);
		if ($defined(r.modified[origid])) {
			el.update(r.modified[origid]);
		}
		if ($type(r.errors[origid]) !== false) {
			this._showElementError(r.errors[origid][el.options.repeatCounter], id);
		} else {
			this._showElementError([], id);
		}
	},

	_prepareRepeatsForAjax : function(d) {
		this.getForm();
		//ensure we are dealing with a simple object
		if ($type(d) === 'hash'
				|| ($type(d.obj) === 'object' && this.options.mooversion == 1.1)) {
			d = (this.options.mooversion == 1.1) ? d.obj : d.getClean();
		}
		//data should be key'd on the data stored in the elements name between []'s which is the group id
		this.form.getElements('input[name^=fabrik_repeat_group]').each(
				function(e) {
					var c = e.name.match(/\[(.*)\]/)[1];
					d['fabrik_repeat_group[' + c + ']'] = e.getValue(); // good for mootools 1.1
				}
		);
		return d;
	},

	_showGroupError : function(r, d) {
		var gids = $A(this.options.pages.get(this.currentPage.toInt()));
		var err = false;
		$H(d).each( function(v, k) {
			k = k.replace('[]', '');//for dropdown validations
			if(this.formElements.hasKey(k)){
				var el = this.formElements.get(k);
				if(gids.contains(el.groupid)){
					if (r.errors[k]) {
					// prepare error so that it only triggers for real errors and not sucess
					// msgs
			
						var msg = '';
						if ($type(r.errors[k]) !== false) {
							for ( var i = 0; i < r.errors[k].length; i++) {
								if (r.errors[k][i] != '') {
									msg += r[i] + '<br />';
								}
							}
						}
						if (msg !== '') {
							tmperr = this._showElementError(r.errors[k], k);
							if (err == false) {
								err = tmperr;
							}
						}
					}
					if (r.modified[k]) {
						if (el) {
							el.update(r.modified[k]);
						}
					}
				}
			}
			}.bind(this));
			
			return err;
		},
	
		_showElementError : function(r, id) {
		// r should be the errors for the specific element, down to its repeat group
		// id.
		var msg = '';
		if ($type(r) !== false) {
			for ( var i = 0; i < r.length; i++) {
				if (r[i] != '') {
					msg += r[i] + '<br />';
				}
			}
		}
		var classname = (msg === '') ? 'fabrikSuccess' : 'fabrikError';
		if (msg === '')
			msg = this.lang.success;
		this.formElements.get(id).setErrorMessage(msg, classname);
		return (classname === 'fabrikSuccess') ? false : true;
	},

	updateMainError : function() {
		var mainEr = this.form.getElement('.fabrikMainError');
		mainEr.setHTML(this.options.error);
		var activeValidations = this.form.getElements('.fabrikError').filter(
				function(e, index) {
				return !e.hasClass('fabrikMainError');
				});
		if (activeValidations.length > 0 && mainEr.hasClass('fabrikHide')) {
			mainEr.removeClass('fabrikHide');
			var myfx = new Fx.Style(mainEr, 'opacity', {
				duration :500
			}).start(0, 1);
		}
		if (activeValidations.length === 0) {
			myfx = new Fx.Style(mainEr, 'opacity', {
				duration :500,
				onComplete : function() {
				mainEr.addClass('fabrikHide');
				}
			}).start(1, 0);
		}
	},

	runPlugins : function(func, event) {
		var ret = true;
		this.options.plugins.each( function(plugin) {
		if ($type(plugin[func]) != false) {
			if (plugin[func](event) == false) {
				ret = false;
			}
		}
		});
		return ret;
	},

	watchSubmit : function() {
		if (!$('fabrikSubmit' + this.id)) {
			return;
		}
		$('fabrikSubmit' + this.id).addEvent('click', function(e) {
		var ret = this.runPlugins('onSubmit', e);
		this.elementsBeforeSubmit(e);
		if (ret == false) {
			new Event(e).stop();
			// update global status error
			this.updateMainError();
		}
		if (ret && this.options.postMethod == 'ajax') {
			//do ajax val only if onSubmit val ok
			if (this.form) {
				oPackage.startLoading(this.getBlock());
				this.elementsBeforeSubmit(e);
				// get all values from the form
				var data = $H(this.getFormData());
				data = this._prepareRepeatsForAjax(data);
				data.fabrik_postMethod = 'ajax';
				data.format = 'raw';
					var myajax = new Ajax(this.form.action, {
						'data' :data,
						'method' :this.options.ajaxmethod,
						onComplete : function(json) {
						json = Json.evaluate(json);
						
						if($type(json) === false) {
						// stop spinner
							oPackage.stopLoading(this.getBlock(), 'error in returned json');
							return;
						}
						// process errors if there are some
						var errfound = false;
						if ($defined(json.errors)) {
							// for every element of the form update error message
							$H(json.errors).each(function(errors, key){
								//replace join[id][label] with join___id___label
								key = key.replace(/(\[)|(\]\[)/g, '___').replace(/\]/, '');
								if(this.formElements.hasKey(key) && errors.flatten().length > 0){
									errfound = true;
									this._showElementError(errors, key);
								};
							}.bind(this));
							
							// this.runPlugins('onAjaxSubmitComplete'); don't run it I guess
						}
						// update global status error
						this.updateMainError();
						
						if (errfound === false) {
							// $$$ rob clearForm() was commented out but in module with ajax on this gave appearance that
							// form was not submitted
							this.clearForm();
							oPackage.sendMessage(this.getBlock(), 'updateRows', 'ok', json, this.lang.form_saved);
							this.runPlugins('onAjaxSubmitComplete', e);
						}else{
							// stop spinner
							oPackage.stopLoading(this.getBlock(), this.lang.validation_error);
						}
					}.bind(this)
					}).request();
				}
				new Event(e).stop();
			}
			}.bind(this));
		
		},
	
		elementsBeforeSubmit : function(e) {
			e = new Event(e);
			this.formElements.each( function(el, key) {
				if (!el.onsubmit()) {
					e.stop();
				}
		});
	},

	// used to get the querystring data and
	// for any element overwrite with its own data definition
	// required for empty select lists which return undefined as their value if no
	// items
	// available

	getFormData : function() {
		this.getForm();
		var s = this.form.toQueryString();
		var h = {};
		s = s.split('&');
		var arrayCounters = $H({});
		s.each( function(p) {
			p = p.split('=');
			var k = p[0];
			// $$$ rob deal with checkboxes
			if(k.substring(k.length-2) === '[]'){
				k = k.substring(0, k.length-2);
				if(!arrayCounters.hasKey(k)){
					//rob for ajax validation on repeat element this is required to be set to 0
					//arrayCounters.set(k, 1);
					arrayCounters.set(k, 0);
				}else{
					arrayCounters.set(k, arrayCounters.get(k)+1);
				}
				k = k + '[' + arrayCounters.get(k) + ']';
			}
			h[k] = p[1];
		});
		// $$$rob test commenting out - as this messes up for date from ajax popupform
		// in cal
		/*
		 * this.formElements.each(function(el, key){ var v = el.getValue(); if(v !==
		 * false){ h[key] = v; } }.bind(this));
		 */
		return h;
	},
	
	// $$$ hugh - added this, so far only used by cascading dropdown JS
	// to populate 'data' for the AJAX update, so custom cascade 'where' clauses
	// can use {placeholders}.  Initially tried to use getFormData for this, but because
	// it adds ALL the query string args from the page, the AJAX call from cascade ended
	// up trying to submit the form.  So, this func does what the commented out code in
	// getFormData used to do, and only fecthes actual form element data.
	
	getFormElementData : function() {
		var h = {};
		this.formElements.each(function(el,key) {
			if (el.element) {
				h[key] = el.getValue();
				h[key+'_raw'] = h[key];
			}
		}.bind(this));
		return h;
	},

	watchGroupButtons : function() {
		this.unwatchGroupButtons();
		this.form.getElements('.deleteGroup').each( function(g, i) {
			g.addEvent('click', this.clickDeleteGroup);
		}.bind(this));
		this.form.getElements('.addGroup').each( function(g, i) {
			g.addEvent('click', this.clickDuplicateGroup);
		}.bind(this));
		this.form.getElements('.fabrikSubGroup').each( function(subGroup) {
		var r = subGroup.getElement('.fabrikGroupRepeater');
		if (r) {
			subGroup.addEvent('mouseenter', function(e) {
			r.effect('opacity', {
				wait :true,
				duration :200
			}).start(0.2, 1);
			});
			subGroup.addEvent('mouseleave', function(e) {
			r.effect('opacity', {
				wait :true,
				duration :200
			}).start(1, 0.2);
			});
		}
		});
	},

	unwatchGroupButtons : function() {
		this.form.getElements('.deleteGroup').each( function(g, i) {
			g.removeEvent('click', this.clickDeleteGroup);
		}.bind(this));
		this.form.getElements('.addGroup').each( function(g, i) {
			g.removeEvent('click', this.clickDuplicateGroup);
		}.bind(this));
		this.form.getElements('.fabrikSubGroup').each( function(subGroup) {
			subGroup.removeEvents('mouseenter');
			subGroup.removeEvents('mouseleave');
		});
	},

	addGroupJS : function(groupId, e, js) {
		if (e == 'delete') {
			this.delGroupJS.set(groupId, js);
		} else {
			this.duplicateGroupJS.set(groupId, js);
		}
	},

	deleteGroup : function(event) {
		if(!this.runPlugins('onDeleteGroup', event)){
			return;
		}
		var e = new Event(event).stop();
		var group = $(e.target).findClassUp('fabrikGroup');
		var i = group.id.replace('group', '');
		this.duplicatedGroups.remove(i);
		if($('fabrik_repeat_group_' + i + '_counter').value == '0'){
			return;
		}
		var subgroups = group.getElements('.fabrikSubGroup');
		
		var subGroup = $(e.target).findClassUp('fabrikSubGroup');
		this.subGroups.set(i, subGroup.clone());
		if (subgroups.length <= 1) {
			this.hideLastGroup(i, subGroup);
			
		}else{
			
			var toel = subGroup.getPrevious();
			var js = this.delGroupJS.get(i);
			
			var myFx = new Fx.Style(subGroup, 'opacity', {
				duration :300,
				onComplete : function() {
					if (subgroups.length > 1) {
						subGroup.remove();
					}
					
					this.formElements.each(function(e, k){
						if($type($(e.element.id)) == false){
							e.decloned(i);
							this.formElements.remove(k);
						}
					}.bind(this));
					
					eval(js);
				}.bind(this)
			}).start(1, 0);
			if (toel) {
				this.winScroller.toElement(toel);
			}
		}
	// update the hidden field containing number of repeat groups
		$('fabrik_repeat_group_' + i + '_counter').value = $(
				'fabrik_repeat_group_' + i + '_counter').getValue().toInt() - 1;
	},
	
	hideLastGroup:function(groupid, subGroup){
		var sge = subGroup.getElement('.fabrikSubGroupElements');
		sge.setStyle('display', 'none');
		new Element('div', { 'class' :'fabrikNotice' }).appendText(this.lang.nodata).injectAfter(sge);
	},
	
	isFirstRepeatSubGroup:function(group)
	{
		var subgroups = group.getElements('.fabrikSubGroup');
		return subgroups.length == 1 && subgroups[0].getElement('.fabrikNotice');
	},
	
	getSubGroupToClone:function(groupid)
	{
		var group = $('group' + groupid);
		var subgroup = group.getElement('.fabrikSubGroup');
		if (!subgroup) {
			subgroup = this.subGroups.get(groupid);
		}

		var clone = null;
		var found = false;
		if (this.duplicatedGroups.hasKey(groupid)) {
			found = true;
		}
		if (!found) {
			clone = subgroup.cloneNode(true);
			this.duplicatedGroups.set(groupid, clone);
		} else {
			if (!subgroup) {
				clone = this.duplicatedGroups.get(groupid);
			} else {
				clone = subgroup.cloneNode(true);
			}
		}
		return clone;
	},
	
	repeatGetChecked:function(group)
	{
		///stupid fix for radio buttons loosing their checked value
		var tocheck = [];
		group.getElements('.fabrikinput').each(function(i){
			if(i.type == 'radio' && i.getProperty('checked') ){
				tocheck.push(i);
			}
		});
		return tocheck;
	},

	/* duplicates the groups sub group and places it at the end of the group */

	duplicateGroup : function(event) {
		if(!this.runPlugins('onDuplicateGroup', event)){
			return;
		}
		if (this.options.mooversion == '1.1' && event){
			var event = new Event(event);
		}
		if (event) event.stop();
		var i = $(event.target).findClassUp('fabrikGroup').id.replace('group', '');
		var js = this.duplicateGroupJS.get(i);
		var group = $('group' + i);
		var c = this.repeatGroupMarkers.get(i);

		$('fabrik_repeat_group_' + i + '_counter').value = $(
				'fabrik_repeat_group_' + i + '_counter').getValue().toInt() + 1;
		
		if (this.isFirstRepeatSubGroup(group)) {
			var subgroups = group.getElements('.fabrikSubGroup');
			//user has removed all repeat groups and now wants to add it back in
			//remove the 'no groups' notice
			subgroups[0].getElement('.fabrikNotice').remove();
			subgroups[0].getElement('.fabrikSubGroupElements').setStyle('display', '');
			return;
		}
		var clone = this.getSubGroupToClone(i);
		var tocheck = this.repeatGetChecked(group);
		
		group.appendChild(clone);
		tocheck.each(function(i){
			i.setProperty('checked', true);
		});
		// remove values and increment ids
		var newElementControllers = [];
		this.subelementCounter = 0;
		var hasSubElements = false;
		var inputs = clone.getElements('.fabrikinput');
		var lastinput = null;
		this.formElements.each( function(el) {
			var formElementFound = false;
			subElementContainer = null;
			var subElementCounter = -1;

			inputs.each( function(input) {
	
				hasSubElements = el.hasSubElements();
	
				//for all instances of the call to findClassUp use el.element rather than input (HMM SEE LINE 912 - PERHAPS WE CAN REVERT TO USING INPUT NOW?)
				// var testid = (hasSubElements) ?
				// input.findClassUp('fabrikSubElementContainer').id : input.id
				//var testid = (hasSubElements) ? el.element.findClassUp('fabrikSubElementContainer').id : input.id;
				var testid = (hasSubElements) ? input.findClassUp('fabrikSubElementContainer').id : input.id;
				
				if (el.options.element == testid) {
					lastinput = input;
					formElementFound = true;
	
					if (hasSubElements) {
						subElementCounter++;
						//the line below meant that we updated the orginal groups id @ line 942 - which in turn meant when we cleared the values we were clearing the orignal elements values 
						//not sure how this fits in with comments above which state we should use el.element.findClassUp('fabrikSubElementContainer');
						//REAL ISSUE WAS THAT inputs  CONTAINED ADD OPTIONS (elementmodel->getAddOptionFields) WHICH HAD ELEMENTS WITH THE CLASS fabrikinput THIS CLASS IS RESERVERED FOR ACTUAL DATA ELEMENTS 
						//subElementContainer = el.element.findClassUp('fabrikSubElementContainer');
						
						subElementContainer = input.findClassUp('fabrikSubElementContainer');
						// clone the first inputs event to all subelements
						input.cloneEvents($(testid).getElement('input'));
	
						// id set out side this each() function
					} else {
						input.cloneEvents(el.element);
	
						// update the element id use el.element.id rather than input.id as that may contain _1 at end of id
						input.id = el.element.id + '_' + c;
	
						// update labels for non sub elements 
						var l = input.findClassUp('fabrikElementContainer').getElement('label');
						if (l) {
							l.setProperty('for', input.id);
						}
					}
	
					input.name = input.name.replace('[0]', '[' + (c) + ']');
				}
			}.bind(this));
	
			if (formElementFound) {
				if (hasSubElements && $type(subElementContainer) != false ) {
					// if we are checking subelements set the container id after they have all
					// been processed
					// otherwise if check only works for first subelement and no further
					// events are cloned
					subElementContainer.id = el.options.element + '_' + c;
				}
				var origelid = el.options.element;
				// clone js element controller, set form to be passed by reference and not cloned
				var ignore = el.unclonableProperties();
				var newEl = new CloneObject(el, true, ignore);

				newEl.container = null;
				newEl.options.repeatCounter = c;
				newEl.origId = origelid;
				
				if (hasSubElements && $type(subElementContainer) != false ) {
					newEl.element = $(subElementContainer);
					newEl.options.element = subElementContainer.id;
					newEl._getSubElements();
				} else {
					newEl.element = $(lastinput.id);
					newEl.options.element = lastinput.id;
				}
				newEl.reset();
				newElementControllers.push(newEl);
			}
		}.bind(this));
		
		// add new element controllers to form
		
		this.addElements({i:newElementControllers});
		this.winScroller.toElement(clone);
		var myFx = new Fx.Style(clone, 'opacity', {
			duration :500
		}).set(0);
		newElementControllers.each( function(newEl) {
			newEl.cloned(c);
		});
		c = c + 1;
		myFx.start(1);
		eval(js);
		this.repeatGroupMarkers.set(i, this.repeatGroupMarkers.get(i) + 1);
		this.unwatchGroupButtons();
		this.watchGroupButtons();
	},

	update:function(o){
		if(!this.runPlugins('onUpdate', null)){
			return;
		}
		var leaveEmpties = arguments[1] || false;
		var data = o.data;
		this.getForm();
		if (this.form) { // test for detailed view in module???
			var rowidel = this.form.getElement('input[name=rowid]');
			if (rowidel && data.rowid) {
				rowidel.value = data.rowid;
			}
		}
		this.formElements.each( function(el, key) {
			//if updating from a detailed view with prev/next then data's key is in _ro format
			if ($type(data[key]) === false) {
				if (key.substring(key.length - 3, key.length) == '_ro') {
					key = key.substring(0, key.length - 3);
				}
			}
			// this if stopped the form updating empty fields. Element update() methods
			// should test for null
			// variables and convert to their correct values
			// if (data[key]) {
			if ($type(data[key]) === false) {
				// only update blanks if the form is updating itself
				// leaveEmpties set to true when this form is called from updateRows
				if (o.id == this.id && !leaveEmpties) {
					el.update('');
				}
			} else {
				el.update(data[key]);
			}
		}.bind(this));
	},

	reset : function() {
		if(!this.runPlugins('onReset', null)){
			return;
		}
		this.formElements.each( function(el, key) {
			el.reset();
		}.bind(this));
	},

	showErrors : function(data) {
		var d = null;
		if (data.id == this.id) {
		// show errors
		var errors = new Hash(data.errors);
		if (errors.keys().length > 0) {
			if($type(this.form.getElement('.fabrikMainError')) !== false){
				this.form.getElement('.fabrikMainError').setHTML(this.options.error);
				this.form.getElement('.fabrikMainError').removeClass('fabrikHide');
			}
			errors.each( function(a, key) {
				if ($(key + '_error')) {
					var e = $(key + '_error');
					var msg = new Element('span');
					for ( var x = 0; x < a.length; x++) {
						for ( var y = 0; y < a[x].length; y++) {
							d = new Element('div').appendText(a[x][y]).injectInside(e);
						}
					}
				} else {
					fconsole(key + '_error' + ' not found');
				}
			});
		}
	}
},

	/** add additional data to an element - e.g database join elements */
	appendInfo : function(data) {
		this.formElements.each( function(el, key) {
		if (el.appendInfo) {
			el.appendInfo(data);
		}
		}.bind(this));
	},

	addListenTo : function(blockId) {
	this.listenTo.push(blockId);
	},
	
	clearForm : function() {
		this.getForm();
		if (!this.form) {
			return;
		}
		this.formElements.each( function(el, key) {
			if (key == this.options.primaryKey) {
				this.form.getElement('input[name=rowid]').value = '';
			}
			el.update('');
		}.bind(this));
		// reset errors
		this.form.getElements('.fabrikError').empty();
		this.form.getElements('.fabrikError').addClass('fabrikHide');
	},
		
	receiveMessage : function(senderBlock, task, taskStatus, data) {
		if (this.listenTo.indexOf(senderBlock) != -1) {
			if (task == 'processForm') {
		
			}
			// a row from the table has been loaded
			if (task == 'update') {
				this.update(data);
			}
			if (task == 'clearForm') {
				this.clearForm();
			}
		}
		
		// a form has been submitted which contains data that should be updated in this
		// form
		// currently for updating database join drop downs, data is used just as a
		// test to see if the dd needs
		// updating. If found a new ajax call is made from within the dd to update
		// itself
		// $$$ hugh - moved showErrors() so it only runs if data.errors has content
		if (task == 'updateRows') {
			if ($H(data.errors).keys().length === 0) {
				if ($type(data.data) !== false) {
					this.appendInfo(data);
					this.update(data, true);
				}
			} else {
				this.showErrors(data);
			}
		}
	},
	
	addPlugin : function(plugin) {
			this.options.plugins.push(plugin);
	}

});
