
/**
 * AutoSuggestion plugin for Centrik
 * @link http://customr.net
 * @copyright Copyright (c) 2009 James Ellis http://customr.net
 * @license Licensed under the MIT license
 * @since 0.1 (17.02.2009)
 * @depends jquery >= 1.3.1
 */
 
(function($) {

	/**
	 * centrik_suggestor()
	 * @note call like $('#input').centrik_suggestor(settings);
	 */
	$.fn.centrik_suggestor = function() {
		//return an instance of the validate handler
		return new $.centrik_suggest(this);
	};
	
	$.centrik_suggest = function(element) {
	
		//configuration
		this.stayOpen = true;//a flag that can keep the box open even if removeresults is called()
		this.stayOpenTimer = null;
		this.timer = null;
		this.initAfterTimer = null;
		this.resultsBox = false;
		this.searching = false;
		this.inputFocussed = false;
		this.input = $(element);
		$(this.input).attr('autocomplete','off');
		this.cache = {};//init cache object
		this.args = {};//args to be sent to the server
		this.args['centrik:format'] = 'json';
		this.key = '_o';
		this.records = [];//current record listing
		
		//default settings
		this.settings = {
			minchars : 3,
			url : false,//endpoint url
			resultBoxClass : 'cas',
			removeAfter : 5000,//ms
			initAfter : 500,//ms
			debug : true,//debug mode
			itemClassCurrent : 'current',
			itemClassNormal : 'normal',
			conflictelements : [],
		};
	};
		
	$.extend($.centrik_suggest, {
		prototype : {
			/**
			* suggest() set handlers and variable for the currently focused form element
			*/
			suggest : function(settings) {
				this.settings = $.extend({}, this.settings, settings);
				
				//assign an ajax error handler
				$(this.input).ajaxError(
					function(event, request, settings) {
						$(this).css('cursor','text');
						//do something else ?
					}
				);
				
				try {
					this.createresults();
					//this.debug('handle');
					this.handle();
				} catch (e) {
					this.debug(e);
					return false;
				}
			},
			createresults : function() {
				//create a results box
				this.resultsBox = document.createElement('div');
				$(this.resultsBox).css('position','absolute');
				$(this.resultsBox).css('left','-500px');
				$(this.resultsBox).css('top','0px');
				$(this.resultsBox).attr('id', this.getboxid());
				$(this.resultsBox).attr('class', this.settings.resultBoxClass);
				$('body').append(this.resultsBox);
				$(this.resultsBox).hide(0);
				//end create
			},
			debug : function(msg) {
				if(this.settings.debug) {
					console.log(msg);
				}
			},
			getboxid : function() {
				return this.settings.resultBoxClass + '_' + $(this.input).attr('id');//box to hold results
			},
			getsetting : function(key) {
				if(typeof this.settings.key != 'undefined') {
					return this.settings.key;
				}
				throw 'Could not get setting: ' + key;
			},
			/**
			* getform() returns form container of input element
			**/
			getform : function () {
				return (typeof this.input.form != 'undefined' ? this.input.form : false);
			},
			/**
			 * handle() handle a suggestion request
			 */
			handle : function() {
				var t = this;
				$(this.input).bind('keyup', function(e) { t.getsuggestions(e);},false);
				$(this.input).bind('keydown',function(e) { t.handlecontrollerkeys(e);},false);
				$(this.input).bind('focus', function() { t.inputFocussed = true;},false);
				$(this.input).bind('blur', function() { t.inputFocussed = false;},false);
				$(this.input).bind('click', function() { this.value = ''; },false);
			},
			/**
			* getsuggestions() send the request to the endpoint
			* @param event the event that has occurred to call getsuggestions
			*/
			getsuggestions : function(event) {
				//this.debug('get suggestions');
				this.stayOpen = true;
				//unhandled event
				if(typeof event.keyCode == 'undefined') {
					this.debug('cannot handle event');
					return false;
				}
				//capture keystrokes
				switch(event.keyCode) {
					case 9:case 13:case 27:	case 16:case 17:case 18:
					case 20:case 33:case 34:case 35:case 36:case 37:case 39:
						//various unwanted key codes
						break;
					case 38:case 40:
						//up arrow + down arrow = highlight record
						this.highlightrecord(event.keyCode);
						break;
					case 8:case 46://every other key plus backspace and delete when over a certain length
					default:
						if($(this.input).attr('value').length >= this.settings.minchars) {
							//this.debug('init request for min ' + this.settings.minchars);
							//send a request (probably cached anyway) - initrequest handles this
							this.initrequest();
						} else {
							//this.debug('removing results');
							this.removeresults(true);//force remove previous results
						}
						break;
				}
				return false;
			},
			handlecontrollerkeys : function(event) {
				if(typeof event.keyCode == 'undefined') {
					return false;
				}
				switch(event.keyCode) {
					case 13:
						//enter / return
						event.preventDefault();
					case 9:case 27:
						//tab + escape
						this.stayOpen = false;
						if($(this.resultsBox).length > 0) {
							this.setfieldvalue();
							//focus the next form element, invokes blur() on input
							this.focusto(event.keyCode);
						}
						break;
					case 8:case 46:
						//backspace + delete
						if($(this.input).attr('value').length <= this.settings.minchars) {
							//remove the box if it exists
							this.removeresults(true);
						} else {
							//send a request (probably cached anyway) - initrequest handles this
							this.initrequest();
						}
						break;
					default:
						return false;
						break;
				}
				return false;
			},
			/**
			* initrequest()
			* @note prepare and send a request, or use a cached result
			*/
			initrequest : function () {
				if(this.settings.url) {
					//set element value
					this.args[this.key] = $(this.input).attr('value');
					
					$(this.input).css('cursor','wait');
					
					if(this.initAfterTimer != null) {
						clearTimeout(this.initAfterTimer);
					}
					var records = this.cacheget($(this.input).attr('value'));
					if(records.length > 0) {
						//cached
						this.populate(records);
						return true;
					} else if($(this.input).attr('value').length == this.minchars) {
						//uncached, first hit - do straight away
						this.send();
					} else {
						//uncached fallback
						var t = this;
						//add a timer to init the request after a pause in typing
						this.initAfterTimer = setTimeout(function() { t.send();return true;},this.initAfter);
					}
				}
				return false;
			},
			/**
			* focusto() move focus to the next form input or select element
			* @param kc the keycode pressed
			*/
			focusto : function(kc) {
				if(kc == 9) return false;//don't do this if a tab keycode passed in
				var f = this.getform();
				if(f) {
					var i;
					var can = false;
					var fl;
					for(i=0;f.elements.length;i++) {
						fl = f.elements[i].nodeName.toLowerCase();
						if(can && (fl == 'input' || fl == 'select')
							&& f.elements[i].getAttribute('type') != 'hidden'
							&& !f.elements[i].readOnly && !f.elements[i].disabled) 
							{
							f.elements[i].focus();
							return true;
						}
						if(f.elements[i].getAttribute('id') == $(this.input).getAttribute('id')) {
							can = true;
						}
					}
				}
				return false;
			},
			/**
			* send() sends an HTTP POST request off to the server.
			*/
			send : function () {
				var t = this;
				this.searching = true;
				$.getJSON(this.settings.url, this.args, function(json, status) { t.response(json, this);});
				return true;
			},
			/**
			* removeresults() remove results in box by emptying it
			* @note use force to force removal. If this.stayOpen is marked as true then the box will not close e.g see blur event handler
			*/
			removeresults : function(force) {
			
				//reverse cascade - should the box stayOpen
				var closeit = false;
				//called but something wants the box to remain open
				if(!this.stayOpen) {
					closeit = true;
				}
				//never close if input focussed
				if(this.inputFocussed) {
					closeit = false;
				}
				//force can override everything
				if(force) {
					closeit = true;
				}
				
				if(closeit) {
					$(this.resultsBox).slideUp(130,
						function() {
							$(this).empty();
						}
					);
					this.stayOpen = false;
				}
				//remove timer
				this.clearstayopentimer();
			},
			response : function(json, response) {
				$(this.input).css('cursor','text');
				try {
					if(typeof json[this.key] == 'undefined') {
						throw 'Could not find key ' + this.key + '  in JSON response';
					}
					this.populate(json[this.key]);
				} catch (error) {
					this.debug(error);
					this.removeresults(true);
				}
			},
			/**
			 * populate() populate record box
			 */
			populate : function(records) {
				this.records = records;
				//this.debug('Records has length ' + records.length);
				if(this.records.length == 0) return false;//do nothing, do not remove previous box
				if(!this.inputFocussed) return false;//input field is no longer focussed, stops ghosts
				if($(this.input).attr('value').length < this.minchars) return false;//not enough characters
				
				var t = this;
				var i, li, record, inner, liTextNode;
				var ol = document.createElement('ol');
				
				$(this.resultsBox).empty();
				$(this.resultsBox).append(ol);
				
				for(i=0;i<this.records.length;i++) {
					li = document.createElement('li');
					if(i==0) li.className = this.settings.itemClassCurrent;
					record = this.emphasise(this.records[i].v);
					
					inner = document.createElement('span');
					$(inner).attr('class','inner');
					$(inner).html(record);
					$(li).append(inner);
					$(ol).append(li);
					
					//attach events
					$(li).bind('click', function(event) { t.setfieldvalue();event.preventDefault(); return false});
					$(li).bind('mouseover',
						function(event) {
							t.stayOpen = true;
							var hl = t.getcurrenthighlight();
							if(hl) {
								$(hl.element).attr('class', t.settings.itemClassNormal);
								$(this).attr('class', t.settings.itemClassCurrent);
							}
							event.preventDefault();
							return true;
						}
					);
					$(li).bind('mouseout',
						function(event) {
							t.stayOpen = true;
							$(this).attr('class', t.settings.itemClassNormal);
							event.preventDefault();
							return true;
						}
					);
				}
				//add the records found to the cache
				this.cacheadd($(this.input).attr('value'), this.records);
				
				//make the box stay open when mouse over and out
				$(this.resultsBox).bind('mouseover', function(event) { t.stayOpen = true; });
				$(this.resultsBox).bind('mouseout', function(event) { t.stayOpen = true;});
				
				//when the field loses focus, close it
				$(this.input).bind('blur', function() { t.setfieldvalue(record);t.removeresults(true);return true;});
				
				//finally , show the results box next to the element
				this.attachresults();
			},
			attachresults : function() {
				var os = $(this.input).offset();
				var h = $(this.input).outerHeight();
				$(this.resultsBox).css('left', os.left + 'px');
				$(this.resultsBox).css('top', os.top + h + 'px');
				$(this.resultsBox).slideDown(130);
			},
			addstayopentimer : function () {
				this.clearstayopentimer();
				var t = this;
				//auto close box after a time, but only if not open and active
				this.stayOpenTimer = setTimeout(function() { t.removeresults(false);},this.removeAfter);
				return true;
			},
			clearstayopentimer : function () {
				//auto close box after a time, but only if not open and active
				if(this.stayOpenTimer != null) {
					clearTimeout(this.stayOpenTimer);
					this.stayOpenTimer = null;
				}
				return true;
			},
			/**
			* highlightrecord() based on a keycode
			* @param kc an event keycode
			*/
			highlightrecord : function(kc) {
				if(this.resultsBox.childNodes.length == 0) return false;
				this.stayOpen = true;//stay open while highlighting
				var curr = this.getcurrenthighlight();
				var hl;
				if(!curr) return false;
				var hl = curr.element;
				switch(kc) {
					case 38:
						//up
						if(!hl.previousSibling) {
							//nothing highlighted, highlight last record
							if(hl) hl.className = this.settings.itemClassNormal;
							this.resultsBox.firstChild.lastChild.className = this.settings.itemClassCurrent;
						} else {
							//hl previous record
							hl.className = this.settings.itemClassNormal;
							hl.previousSibling.className = this.settings.itemClassCurrent;
						}
						break;
					case 40:
						//down
						if(!hl.nextSibling) {
							//nothing highlighted, highlight first record
							if(hl) hl.className = this.settings.itemClassNormal;
							this.resultsBox.firstChild.firstChild.className = this.settings.itemClassCurrent;
						} else {
							//hl next record
							hl.className = this.settings.itemClassNormal;
							hl.nextSibling.className = this.settings.itemClassCurrent;
						}
						break;
					default:
						return false;
						break;
				}
				return true;
			},
			/**
			* getcurrenthighlight() returns the element currently highlighed, or false if nothing highlighted
			*/
			getcurrenthighlight : function () {
				var hl = { element : false, record : false };
				if(this.resultsBox.childNodes.length == 1) {
					var ol = this.resultsBox.childNodes[0];
					if(ol.childNodes.length == 1) {
						//only one item (highlighted or not)
						hl = {
							record : this.records[0],
							element : ol.childNodes[0]
						};
					} else {
						var i;
						for(i=0;i<ol.childNodes.length;i++) {
							if(ol.childNodes[i].className == this.settings.itemClassCurrent) {
								hl = {
									record : this.records[i],
									element : ol.childNodes[i]
								};
							}
						}
					}
				}
				//nothing available or highlighted
				return hl;
			},
			/**
			* getrecord()
			* @note get nth record in the list, if available. Records start at 0.
			* @param idx the index to get
			* @param astext whether to return as plain text (true) or the element itself (false)
			*/
			getrecord : function(idx, astext) {
				if(this.resultsBox.childNodes.length == 1) {
					var ol = this.resultsBox.childNodes[0];
					if(typeof ol.childNodes[idx] != 'undefined') {
						if(astext) {
							return this.gettextcontent(ol.childNodes[idx]);
						} else {
							return ol.childNodes[idx];
						}
					}
				}
				//does not exist
				return false;
			},
			/**
			* gettextcontent()
			* @note get the text content of the element to be sent to the list
			*/
			gettextcontent : function(ele) {
				if(typeof ele.textContent != 'undefined') {
					return ele.textContent;
				} else if (typeof ele.innerText != 'undefined') {
					return ele.innerText;
				} else {
					return '';
				}
			},
			/**
			* emphasise()
			* @note emphasise what the user has entered. Use a stylesheet to present these elements
			*/
			emphasise : function(recordValue) {
				//make the first letter of the inputvalue
				var inputValue = $(this.input).attr('value');
				//make the first letter of each word uppercase
				var parts = inputValue.split(' ');
				var i,f,r;
				var emp = [];
				for(i=0;i<parts.length;i++) {
					f = parts[i].substring(0,1).toUpperCase();
					r = parts[i].substring(1, (inputValue.length + 1));
					emp[i] = f + r;
				}
				var rxp = new RegExp('^' + inputValue, 'i');
				var result = recordValue.replace(rxp, '<em class="em">' + emp.join(' ') + '</em><span class="rem">') + '</span>';
				return result;
			},
			setfieldvalue : function() {
				//find the index of the currently hightlighted record
				//or the first item if none is set
				var hl = this.getcurrenthighlight();
				if(hl.record) {
					$(this.input).attr('value', hl.record.v);
				}
				//update the helper element with the unique identifier for this record
				this.sethelpervalue(hl.record.u);
				this.removeresults(true);
				return true;
			},
			/**
			 * sethelpervalue() assigns the unique identifier for the chosen suggested record to a hidden input next to the  suggestion source
			 * @param uniq a JSON encoded string containing the uniq data
			 */
			sethelpervalue : function(uniq) {
				var hlp = $(this.input).next();
				if($(hlp).attr('type') == 'hidden') {
					$(hlp).attr('value', uniq);
					return true;
				}
				return false;
			},
			cacheget : function(id, test) {
				if(typeof this.cache[id] != 'undefined') {
					if(test) {
						return true;
					} else {
						return this.cache[id];
					}
				}
				return false;
			},
			cacheadd : function(id,value) {
				if(typeof this.cache[id] == 'undefined') {
					this.cache[id] = value;
					return true;
				}
				return false;
			},
			cacheclear : function () {
				if(typeof this.cache[id] == 'undefined') {
					this.cache[id] = value;
					return true;
				}
				return true;
			},
			cachedelete : function () {}
		}
	});
//end
})(jQuery);