/********************************************************/
// Author: Vladimir Prieto
// Web: http://vladimirprieto.blogspot.com
// Requirements: mootools 1.0.svn.r464+ (http://www.mootools.net/)
// version: 1.0
/*
changelog : 

1.6 (2008-04-16) :
	- add default values for operators
1.5 (2008-04-14) :
	- add onJsonComplete.  it fires on any response of json.
	- add onSearcherChange.  it fires when it change value from the one it was search (json ajax call).
	- add search method for any input.  now it can be called from user at any time.
	- add ajax response to onJsonSuccess param.
v1.3 (2007-10-10) :
	- fix clon creation, clon is displayed after onAfterClone event.
v1.3 (2007-10-10) :
	- add onAfterClone, wich fires after add_clone call.
	- add onJsonSuccess, wich fires after complete and success json call.
	- clean some code.
v1.2 (2007-07-02) :
	- add onJsonError and onJsonEmpty events.
	- fixed syntax error on IE
v1.1 (2007-07-01) :
	- add calOnKeyUp option.
	- add sumOnCalculate option
	- add onSum event.
	- remove the need to fit add_clone arguments with elements clone_id.  now they can be any length.
	- various code optimizations
*/
/********************************************************/
/*
options :
	containerId - string, id of the element that contains the cloned ones
	cloneId - string, id of the element to be cloned
	readOnlyNoFocus - boolean (default=false), if the elements with readonly property has focus or not
						note : this behaviour does not work well.  let's say is a beta :D
	numberingClass - string, class of the elements that will have numbers (must be inputs)
	emptyShowClass - string, class that will be applied to elements that are empty.
	emptyRequiredClass - string, element's class of the not empty ones
	searchData :
		searcherClass - string, element's class that will be use to search into the jsonDataFile (key)
		destinationsNames - array of strings, names of the elements that will be update when onsuccess of json
							note : order and lenght of array, must fit with the ones sended by json file and with the inputs inside of cloneId
		jsonDataFile - string, json data file
		ajaxWaitClass - string, class to be applied when calling json file
		successFocusElement - string, element's name where focus must go after success json calling
	calItems :
		digits - int (default=0), decimals to round values
		operatorsClasses - array of strings, element's classes that will used on formula calculating
		operatorsDefaultValues - array, element's default values that will be used in formula in case if element got nothing
		formula - string, formula to apply to operatorsClasses
				  example : '(opertor1+operator2)/operator3)', when operatorsClasses = ['operator1','operator2','operator3']
		resultClass - string, element's class where formula results will be.
		calOnKeyUp - boolean (default=false), true for call calculate() on every onKeyUp element event.  Just for element that are operators (operatorsClasses)
		sumOnCalculate - boolean  (default=false), true for call sum_results() after making calculates().
events:
	onSum - arguments(total : total of the results sum).  fires when making sum.
	onJsonError - arguments(element : object from the event is triggered, response : response from json file).
					fires when response length from json file is not equal to destinationsNames length
	onJsonEmpty - arguments(element : object from the event is triggered).  fires when response from json file is null
	onJsonComplete - arguments(element : object from the event is triggered, response: response from json).  if json file returns something.
	onJsonSuccess - rguments(element : object from the event is triggered, response: response from json).  if json file returns something and values matches destinations.

functions :
	add_clone - array of string [optional]. creates a clone of cloneId and inserts it on containerId
	calculates - calculate the operations estiputaled on calItems
	sum_results - returns sum from all resultClass values
	show_empty - change class of emptyRequiredClass to emptyShowClass.
				 returns boolean, true if it founds empty values, false if it didn't found empty values on emptyRequiredClass elements
*/
var mooItems = new Class({
	
	options: {
		containerId: null,
		cloneId: null,
		readOnlyNoFocus: false,
		numberingClass: null,
		emptyShowClass: null,
		emptyRequiredClass: null,
		searchData: {
			searcherClass: null,
			destinationsNames: null,
			jsonDataFile: null,
			ajaxWaitClass: '',
			successFocusElementName: null
		},
		calItems: {
			digits: 0,
			operatorsClasses: null,
			operatorsDefaultValues: [],
			formula: '',
			resultClass: null,
			calOnKeyUp: false,
			sumOnCalculate: false
		},
		onSum: Class.empty, //send parameters : total
		onJsonError:  Class.empty, //send parameters : seacher element,response
		onJsonEmpty:  Class.empty, //send parameters : seacher element
		onJsonSuccess:  Class.empty, //send parameters : seacher element,response
		onJsonComplete:  Class.empty, //send parameters : seacher element,response
		onAfterClone: Class.empty,  //send parameters : new or created
		onSearcherChange:  Class.empty //send parameters : seacher element
	},
	
	initialize: function(options){
		this.setOptions(options);

		this.row_number = 0;
	},
	
	_search: function(searcher,destinations,successFocusElementName,jsonData,ajaxWaitClass) {
		
		if ( (searcher.value != '') && (searcher.value != $(searcher).$tmp.old_value) ){  //if there's something to search
			var row_destinations = new Array();
			
			//search for destinations
			if (destinations){
				brothers = $(searcher).getParent().getChildren();
				for(j=0; j < destinations.length ;j++){
					for(i=0; i < brothers.length ;i++){
						if (brothers[i].name == destinations[j]){
							row_destinations[j] = brothers[i];
						}
						if (successFocusElementName != null)
							if (brothers[i].name == successFocusElementName)
								next_focus = brothers[i];
					}
				}
			}
	
			var bringData = function(req){
				
				//response of json must fit in length with destinations
				//and must be orderer in the same way
				var response = Json.evaluate(req);
				if (response != null) { //if json sends something
					this.fireEvent('onJsonComplete', [searcher,response]);
					if (response.length == row_destinations.length) { //if json sends less data
						for(i=0 ; i < response.length; i++)
							$(row_destinations[i]).value = response[i];
	
						this.fireEvent('onJsonSuccess', [searcher,response]);
	
						//if must move focus
						if ($chk(successFocusElementName)) {
							$(next_focus).focus();
							$(next_focus).select();
						}
					} else
						this.fireEvent('onJsonError', [searcher,response]);
				} else
					this.fireEvent('onJsonEmpty', [searcher]);
			}
			
			//send input_searcher for 'search'
			//results are back on bringData through response array
			var jsonString = Json.toString({input_searcher: $(searcher).value});
			var jSonRequest = new Ajax(jsonData, {
													 data: jsonString,
													 onComplete: bringData.bind(this),
													 onRequest: function (){ $(searcher).addClass(ajaxWaitClass); },
													 onSuccess: function (){ $(searcher).removeClass(ajaxWaitClass); }
													});
			jSonRequest.request();
			$(searcher).$tmp.old_value = $(searcher).value;
		}
	},
	
	//almost just a parser
	search: function(searcher){
		this._search(searcher,this.options.searchData.destinationsNames,this.options.searchData.successFocusElementName,this.options.searchData.jsonDataFile,this.options.searchData.ajaxWaitClass);
	},
	
	add_clone: function(values){
		/***********************************/
		/* the on key up event for searcher*/
		/***********************************/
			var on_keyup = function(event,searcher){
				var event = new Event(event);
				//just for when [ENTER] is pressed
				if (event.key == 'enter') {
					this.search(searcher);
				}
			}
		/***********************************/
		/* the on blur event  for searcher */
		/***********************************/
			var on_blur = function(event,searcher, destinations) {

				if ($(searcher).$tmp.old_value != $(searcher).value) {
					if (destinations){
						//search for destinations to make them blank
						brothers = $(searcher).getParent().getChildren();
						for(j=0; j < destinations.length ;j++){
							for(i=0; i < destinations.length ;i++){
								if (brothers[i].name == destinations[j]){
									brothers[i].value = '';
								}
							}
						}
					}
					this.fireEvent('onSearcherChange', [searcher]);
					//save the old value to compare it later
					//this is usefull when searcher value change
					$(searcher).$tmp.old_value = $(searcher).value;
				}
			}
		/***********************************/
		/***********************************/
		/***********************************/
		/* onfocus event for readonly objects */
		/***********************************/
			var on_focus = function(next_cell) {
				if ($(next_cell).focus)
					$(next_cell).focus();
			}
		/***********************************/
		
		this.row_number++;
		var new_row = $(this.options.cloneId).clone(true);
		new_row.id = this.options.containerId + "_row_" + this.row_number;  //make unique row
		new_row.injectInside(this.options.containerId);
		//just inputs
		//TODO: fit for any selector, not just input tag
		cells = $ES('input',new_row.id); //new_row.getElements('input');

		for (r=0; r < cells.length ;r++) {
			//search readonly objects
			if (this.options.readOnlyNoFocus && cells[r].readOnly && (r < (cells.length-1))) {
				//guide/explanation on http://forum.mootools.net/viewtopic.php?id=871
				//this causes that last object on array doesn't pass the focus
				//TODO : pass focus to all next elements
				cells[r].addEvent('focus', on_focus.pass(cells[r+1],cells[r]) );
			}
			//the searcher element
			if (cells[r].hasClass(this.options.searchData.searcherClass)) {
				//guide/explanation on http://forum.mootools.net/viewtopic.php?id=871
				cells[r].addEvent('keyup', on_keyup.create({
													'arguments': [cells[r]],
													'bind': this,
													'event': Event
											})
								);
				cells[r].addEvent('blur', on_blur.create({
													'arguments': [cells[r],this.options.searchData.destinationsNames],
													'bind': this,
													'event': Event
											})
								);
			}

			//if values passed
			if ((values != null) && (r < values.length)) {
				cells[r].value = values[r];
				//to omit future change value for searcher
				//(see searcher's on blur event)
				if (cells[r].hasClass(this.options.searchData.searcherClass))
					cells[r].$tmp.old_value = values[r];
			}
			//if it is a numbering cell
			if (cells[r].hasClass(this.options.numberingClass))
				cells[r].value = this.row_number;

			//onkeyup event for operators
			if ($chk(this.options.calItems.operatorsClasses))
				for(i=0; i < this.options.calItems.operatorsClasses.length ; i++)
					
					if ((this.options.calItems.calOnKeyUp) && (cells[r].hasClass(this.options.calItems.operatorsClasses[i]))) {
						cells[r].addEvent('keyup', this.calculate.bind(this));
						
						//don't want to add more events to the same cell
						i=this.options.calItems.operatorsClasses.length;
					}
			
		}
		this.fireEvent('onAfterClone', [new_row]);
		new_row.setStyle('display','');  //trick.  works on FF and IE (don't know others)
	},
	delete_clone: function(object){
		//object must be row identifier object
		//delete row
		$(object.id).remove();
		--this.row_number;
		//numebering correction
		if ($chk(this.options.numberingClass)) {
			cells = $(this.options.containerId).getElements('.'+this.options.numberingClass);
			for (i=0; i < cells.length ;i++) { //for all cells
				cells[i].value = i+1;
			}
		}
	},
	sum_results: function(){
		if ($chk(this.options.calItems.resultClass)) {
			var sum  = 0;
			
			$(this.options.containerId)
				.getElements('.'+this.options.calItems.resultClass)
				.each(
					(function(input){
						//is number ? sum += input value;
						sum += ((isNaN(input.value)) || (input.value.length == 0)) ? 0 : input.value.toFloat().round(this.options.calItems.digits);
					}).bind(this) 
				);
			var total = sum.round(this.options.calItems.digits);

			this.fireEvent('onSum', [total]);
			
			return total;
		} else return 'NaN';
	},
	calculate: function(){
		
		if ( (this.options.calItems.resultClass != null) &&
			 (this.options.calItems.operatorsClasses != null) &&
			 (this.options.calItems.formula != '')  )
		{
			//getting all operators to match row by row
			//var operators = $(this.options.containerId).getElementsBySelector('.' + this.options.calItems.operatorsClasses.join(', .'));
			operators = new Array();
			for(i=0; i < this.options.calItems.operatorsClasses.length ;i++)
				operators[i] = $(this.options.containerId).getElements('.'+this.options.calItems.operatorsClasses[i]);
			
			result = $(this.options.containerId).getElements('.'+this.options.calItems.resultClass);
			//result.length must be equal to row count
			for(i=0; i < result.length ;i++){
				formula = this.options.calItems.formula;
				//to all operators
				for(j=0; j < operators.length ;j++){
					//if value of operator is not a number -> formula can't be calculated
					//else calculate
					isEmpty = (isNaN(operators[j][i].value)) || (operators[j][i].value.length == 0);
					defaultNaN = isNaN(this.options.calItems.operatorsDefaultValues[j]);  //default value is number?
					if ( isEmpty && defaultNaN ) {
						formula = 0;
						j = operators.length;  //get out of the loop
					} else {
						//choose the correct value for operator
						var operatorValue = (isEmpty) ? this.options.calItems.operatorsDefaultValues[j] : operators[j][i].value;
						eval("formula = formula.replace(/"+this.options.calItems.operatorsClasses[j]+"/g, operatorValue)");
					}
				}
				result[i].value = eval(formula).toFloat().round(this.options.calItems.digits);
				
				if (result[i].value == '0') result[i].value = '';
			}
		}

		if (this.options.calItems.sumOnCalculate)
			this.sum_results();
	},
	show_empty: function(){
		//TODO: make selection to fit any tag, not only input tag
		if ((this.options.emptyShowClass != null) && (this.options.emptyRequiredClass != null)) {
			empty_exists = false;
			empties = $(this.options.containerId).getElements('.' + this.options.emptyRequiredClass);
			dont_show_empty = false;
			for(i=0; i < empties.length ;i++){
				//check for the searcher
				is_key = empties[i].hasClass(this.options.searchData.searcherClass);

				if (is_key)
					//if the seacher key is empty, empty can't be shown for that row
					dont_show_empty = (empties[i].value == '');
				
				if (!dont_show_empty) {
					if (($(empties[i]).value == '0') || ($(empties[i]).value == '') || ($(empties[i]).value == ' ') || ($(empties[i]).value == 'NaN')){
						empty_exists = true;
						$(empties[i]).addClass(this.options.emptyShowClass)
					} else
						$(empties[i]).removeClass(this.options.emptyShowClass);
				} else
					//if there's not searcher value on the row
					$(empties[i]).removeClass(this.options.emptyShowClass);
			}

		} else alert('can\'t use this feature.  There\'s no emptyShowClass or emptyRequiredClass defined');
		return empty_exists;
	}
});

mooItems.implement(new Events, new Options);

