﻿/// <reference path="jquery.intellisense.js"/>  
/**
 * Coral Elements for jQuery
 * cComboBox handles a flyout-type box with ajax search results, based on the text typed into an input box.
 *
 * These methods are build on the jQuery and interface foundation to provider
 * client functionality to the Coral CMS system.
 *
 * Copyright (c) 2007 Scorpion Design, Inc.
 *
 */
(function($) {

	// Create the cobalt namespace if it has not already been done.
	$.cobalt = $.cobalt || {};

	// Combobox contructor.
	$.cobalt.combobox = function(el,o) { this.init(el,o); };

	// Return a new instance of the cobalt combobox.
	$.fn.combobox = function(o)
	{
		return this.each(function()
			{
				this._combo = new $.cobalt.combobox(this,o);
			});
	};

	// Extend the combobox prototype with these shared properties/methods.
	$.extend($.cobalt.combobox.prototype, {

		// Basic properties.
		flyout : null,			// DOM element of the flyout box.
		active : false,			// Is this combobox active?
		size : 0,				// How many elements in the combobox result set?
		pos : -1,				// The position of the selected element.
		selected : null,		// The JSON object that was selected.
		isover : false,			// Is the mouse hovering over the flyout box?
		ajaxcall : false,		// Is there an ajax call in progress?
		ajaxresults : null,		// Store the ajax results.

		// Ajax methods.
		getResultsUrl : null,

		// Combobox events.
		onsearch : null,
		onnav : null,
		onselect : null,
		onclose : null,

		// Flyout hover events.
		flyoutOver : function(e) { this._combo.isover = true; },
		flyoutOut : function(e) { this._combo.isover = false; },

		// Event for monitoring entries made to the input field.
		onKeyup : function(e)
			{
				// Only handle valid characters for this event.
				var key = e.which;
				if (key!=8 && key!=32 && key<=48) { return; }

				// If we already have a timeout scheduled, kill it.
				if (this._timeout)
				{
					clearTimeout(this._timeout);
					this._timeout = null;
				}

				// Set a 1/20 second timeout to start the ajax call.
				var fn = function(combo){
					return function(){
						combo.search(combo.input.val());
						combo = null;
					};
				}(this._combo);
				this._timeout = setTimeout(fn,50);
			},

		// Based on the current search results, and the current value of the textbox, do we need to do another ajax call or can 
		// we filter in-memory with the current results?
		validSearch : function()
			{
				var text,current;
				if (!this.ajaxresults)
					// If we have no ajax results, we have to do the call.
					return false;
				else if (!(text=this.ajaxresults.text))
					// If the results have no text assigned (which is an error) we'll have to redo the call.
					return false;
				else if (!(current=this.input.val()))
					// If we don't have anything in the textbox, we're good.
					return true;
				else if (current.toLowerCase().indexOf(text.toLowerCase())<0)
					// If the current value cannot be found inside the value of the text used in the ajax call, we'll have to redo it.
					return false;
				else
					// Otherwise, our search is good.
					return true;
			},

		// Search for results via the ajax call
		search : function(text)
			{
				// If we're already in the middle of an ajax call, do nothing.
				if (this.ajaxcall) return;

				// If we don't have a text value or is it less than the minimum, cancel the action.
				if (!text || text.length<this.options.minChars)
				{
					// Close the box if it is active.
					if (this.active) this.close();
					return;
				}

				// If our last search is still a valid set of results, filter them without a new ajax call.
				if (this.validSearch())
				{
					this.activate();
					this.displayResults(text);
					return;
				}

				// Fire any onsearch event.
				if ( $.isFunction(this.options.onsearch) ) {
					if ( this.options.onsearch.apply(this, arguments) === false ) {
						return false;
					}
				}

				// Get the folder list.
				var url = $.getAjaxUrl(this.getResultsUrl,{Terms:$.encode(text)});
				if (url)
				{
					// Activate the flyout.
					this.activate();
					this.flyout.loading();

					// Fire the ajax call.
					var combo = this;
					this.ajaxcall = true;
					var fn = function(combo){
						return function(results){
							// Get the current value of the input box.
							combo.ajaxcall = false;
							combo.flyout.doneLoading();
							var newtext = combo.input.val();
							
							// Ensure the user hasn't changed the value of the textbox to the point
							// Where the search wouldn't be invalidated.
							if (newtext && newtext.toLowerCase().indexOf(text.toLowerCase()) >= 0)
							{
								combo.ajaxresults = {text:text,results:results};
								combo.displayResults(newtext);
								combo=null;
							}
							else
								// Otherwise, do a new search.
								combo.search(newtext);

							// Null out the closure.
							combo=null;
						};
					}(this);
					$.ajax({url:url,dataType:'json',success:fn});
				}
			},

		// Show the combobox.
		activate : function()
			{
				// Check to see if the box is already visible.
				if (this.active) return;
				
				// Fly the box in position.
				this.flyout.relativePos(this.input,this.options.position||$._DIR._LOWER_LEFT,this.options.direction||$._DIR._LOWER_RIGHT);
				this.active = true;
				
				// Activate the events.
				$(document).bind('keydown',{combo:this},this.flyoutNav).bind('mousedown',{combo:this},this.checkClose);
				this.input.bind('mouseover',this.flyoutOver).bind('mouseout.combo',this.flyoutOut);
			},

		// Display the results found from the ajax call.
		displayResults : function(text)
			{
				// Clear the flyout and reset the variables.
				this.flyout.empty();
				this.size = 0;
				this.pos = -1;
				this.selected = null;

				// Iterate through the results and add each to the flyout.
				var results = this.ajaxresults.results;
				for (var i=0;i<results.length;i++)
				{
					// Get the name and ensure it is valid by the current filter criteria
					var name = results[i][this.options.textField];
					if (name && name.toLowerCase().indexOf(text.toLowerCase())>=0)
					{
						// Create the div and add it to the flyout.
						var div = $('<div>'+name+'</div>');
						div.appendTo(this.flyout);
						div.hover(this.itemOver,this.itemOut).click(this.select);
						
						// Assign the div properties.
						div[0]._combo = this;
						div[0].$value = results[i];
						div[0].$pos = this.size++;
					}
				}
			},

		// When one of the elements is moused over.
		itemOver : function(e)
			{
				var combo = this._combo;
				var css = combo.options.cssActive;

				// If there was a previous selected element (such as from a cursor move), then deactivate it.
				if (combo.pos>=0)
				{
					combo.flyout.find('div.'+css).removeClass(css);
					combo.selected = null;
				}

				// Activate the current element.
				if (!this.$self) { this.$self = $(this); }
				this.$self.addClass(css);
				combo.selected = this.$value;
				combo.pos = this.$pos;			
			},
		
		// When one of the elements is moused out.
		itemOut : function(e)
			{
				var combo = this._combo;
				var css = combo.options.cssActive;
				if (!this.$self) { this.$self = $(this); }
				this.$self.removeClass(css);
			},

		// If a mousedown event happens and the cursor is not over the combobox, close it.
		checkClose : function(e)
			{
				if (!e.data.combo.isover) { e.data.combo.close.call(e.data.combo); }
			},

		// Close the combobox flyout and unbind the events.
		close : function()
			{
				if (!this.active) return;
				
				if ($.isFunction(this.onclose)) { this.onclose(this); }
				this.flyout.empty().hide();
				this.active = false;
				
				// Cancel the events.
				$(document).unbind('keydown',this.flyoutNav).unbind('mousedown',this.checkClose);
				this.input.unbind('mouseover',this.flyoutOver).unbind('mouseout',this.flyoutOut);
			},

		// Handle a document keydown to manipulate the entries in the combobox list.
		flyoutNav : function(e)
			{
				var combo = e.data.combo;
				
				switch (e.which)
				{
					case 27:
						// Escape
						combo.close();
						break;
					case 38:
						// Up Arrow
						combo.move(-1);
						break;
					case 40:
						// Down Arrow
						combo.move(1);
						break;
					case 33:
						// Page Up
						combo.move(-10);
						break;
					case 34:
						// Page Down
						combo.move(10);
						break;
					case 13:
						// Enter
						combo.select();
						return false;
				}
				
				return true;
			},

		// Select the next element below the current one.
		move : function(amount)
			{
				if (amount<=0 && this.pos<=0)
					// If we're moving up but we're already at the beginning, do nothing.
					return;
				else if (amount>0 && this.pos+1>=this.size)
					// If we're moving down by we're already at the end, do nothing.
					return;
				else
					// Adjust the position.
					this.pos += amount;

				// If we've moved out of bounds, adjust the position.
				if (this.pos<0) this.pos = 0;
				if (this.pos+1>this.size) this.pos = this.size-1;

				// Unselect the previous element.
				var css = this.options.cssActive;
				if (this.selected)
				{
					this.flyout.find('div.'+css).removeClass(css);
					this.selected = null;
				}

				// Advance to the next position, find the element and activate it.
				var div = this.flyout.find('div:eq('+this.pos+')');
				div.addClass(css);
				this.selected = div[0].$value;
				
				if (amount<0)
				{
					// If we have navigated to an element above the visible scroll area,
					// adjust the scroll position to make the element visible.
					var pos = div[0].offsetTop;
					if (pos<div[0].offsetParent.scrollTop) { div[0].offsetParent.scrollTop = pos }
				}
				else
				{
					// If we have navigated to an element beyond the visible scroll area,
					// adjust the scroll position to make the element visible.
					var pos = div[0].offsetTop+div[0].offsetHeight-div[0].offsetParent.offsetHeight+$.toInt(div.css('borderBottomWidth'))+$.toInt(div.css('marginBottom'));
					if (pos>div[0].offsetParent.scrollTop) { div[0].offsetParent.scrollTop = pos }
				}
			},

		// Select the current element in the combobox.
		select : function(e)
			{
				var combo = (e && e.data && e.data.combo) || this._combo || this;
				var result;
				if (combo.selected)
				{
					// Assign its value to the input box.
					combo.input.val(combo.selected[combo.options.textField]);
					
					// If there is an onselect event, fire it.
					if ($.isFunction(combo.onselect))
						result = combo.onselect.apply(combo, [combo.selected]);
				}
				combo.close();
				return result;
			},

		init : function(el,o)
			{
				// Define the basic options.
				o = jQuery.extend(
					{
						cssClass	: 'comboBox',			// CssClass of the flyout box.
						cssActive	: 'comboActive',		// CssClass of an active child element.
						width		: 300,					// Width of the flyout box.
						height		: 180,					// Height of the flyout box.
						position	: $._DIR._LOWER_LEFT,	// Position relative to the textbox.
						direction	: $._DIR._LOWER_RIGHT,	// Direction relative to the textbox.
						minChars	: 3,					// How many characters before the search is fired.
						textField	: '',					// Which field in the result to display.
						showOnFocus : true					// Show the dialog box on focus
					}, o||{});

				// Assign the basic variables.
				this.options = o;

				// Assign the events.
				this.getResultsUrl = o.getResultsUrl;
				this.onsearch = o.onsearch;
				this.onnav = o.onnav;
				this.onselect = o.onselect;
				this.onclose = o.onclose;

				// Set the combobox to fire on user input.
				this.input = $(el).bind('keyup',this.onKeyup);
				if (this.options.showOnFocus)
					this.input.bind('focus',function(e){if (!this._combo.active) this._combo.search(this.value); });

				// Build the flyout and set its properties.
				this.flyout = $('<div class="'+o.cssClass+'"></div>')
					.css({position:'absolute',display:'none',width:o.width,height:o.height})
					.appendTo(document.body)
					.hover(this.flyoutOver,this.flyoutOut);

				// Record a reference in the flyout to this combobox object.
				this.flyout[0]._combo = this;
			}
	});
	
	
})(jQuery);
