/**
*		PLUGIN NAME 	: 	jSelectBox
*
*		VERSION			: 	0.1
*
*		AUTHOR			: 	bogdan gradinariu (bogdan.gradinariu [at] gmail [dot] com)
*
*		LICENCE			:	buy me a beer and well talk ;)
*
*		DEPENDENCIES	:	well, just jQuery (tested 1.4+)
*
*		DESC			: 	a plugin that allows you to cross-browser customise <select> elements 
*
*		USAGE 			: 	$('select').jSelectBox(settings); 
*							settings is an object literal that may overwrite the $.fn.jSelectBox.defaults described below
*
*		CHANGELIST		:	well, it's the first version, so.. nothing :P
*
*  		TO DO			: 	- simulate real focus/blur events
*							- add tabindex navigation
*							- add keyhandlers (such as arrows) for interaction with the selectbox
**/
(function($,window,undefined){
	$.fn.jSelectBox = function(o){
		// some globals
		var options = $.extend(true,{},$.fn.jSelectBox.defaults,o);
		
		
		
		// constructor
		function jSelectBox(el,o){
			// initialise
			this.initialise.apply(this,arguments);
			$.fn.jSelectBox.classPrefixes.push(o.classPrefix);
		}
		// prototype
		jSelectBox.prototype = new function(){
			var api,
				settings,
				originalEl,
				$originalEl,
				wrapperEl,
				optionListEl,
				triggerEl,
				valueEl;
				
			this.el = wrapperEl;	
				
			//internal api
			function _initialise(el,options){
				api = this;
				settings = options;
				originalEl = el;
				$originalEl = $(originalEl);
				_initComponents();
				
				// copy existing attributes from the old on the new select box 
				wrapperEl.attr('class',$.trim(($originalEl.attr('class') || '') + ' jselectbox ' + settings.classPrefix +'-wrapper'));
				wrapperEl.attr('style',$originalEl.attr('style') || '');
				wrapperEl.attr('id',$originalEl.attr('id') || '');
				
				// attach the api to the wrapper element
				wrapperEl.data('jSelectBoxApi',this);
				
				// init references to elements
				triggerEl = wrapperEl.find('.'+settings.classPrefix +'-trigger');
				valueEl = wrapperEl.find('.'+settings.classPrefix +'-value');
				optionListEl = wrapperEl.find('.'+settings.classPrefix +'-option-list');
				
				// init the options
				$originalEl.find('option').each(function(i,option){
					var $option = $(option);
					var $valueNode;
					optionListEl.append(
						$('<li class="'+ settings.classPrefix +'-option"/>').append(
							$valueNode = $('<span class="'+ settings.classPrefix +'-option-value"/>').text($option.attr('value') != null ? $option.attr('value') : $option.text()),
							$('<span class="'+ settings.classPrefix +'-option-text"/>').text($option.text())
						)
					);

					// copy all "data-" attributes to the value node
					$.each(option.attributes, function (i, attribute) {
						if (attribute.name.match(/^data-/)) {
							$valueNode.attr(attribute.name, attribute.value);
						}
					});
				});
				
				_insertNewSelectBox();
				_bindEvents();
				_triggerEvent('init');
				_setValue($originalEl.val());
			}
			
			// construct the actual html marcup for the new selectBox
			function _initComponents(){
				wrapperEl = $('<div class="'+ settings.classPrefix +'-wrapper"/>')
					.append(
						$('<div class="'+ settings.classPrefix +'-trigger"/>')
							.append(
								$('<div class="'+ settings.classPrefix +'-trigger-inner"/>')
							),
						$('<div class="'+ settings.classPrefix +'-value"/>'),
						$('<ul class="'+ settings.classPrefix +'-option-list"/>')
					);
			}
			
			
			
			function _insertNewSelectBox(){
				wrapperEl.insertAfter($originalEl);
				$originalEl.detach();
			}
			
			function _restoreOriginalElement(){
				$originalEl.insertBefore(wrapperEl);
				wrapperEl.detach();
			}
			
			function _destroy(){
				_restoreOriginalElement();
				
				// remove this objects classPrefix from the global prefixes array
				$.fn.jSelectBox.classPrefixes.splice($.inArray(settings.classPrefix,$.fn.jSelectBox.classPrefixes),1);
			}
			
			
			
			
			
			// MANIPULATION FUNCTIONS
			
			// function that returns the current VALUE of the selectBox
			function _getValue(){
				return optionListEl.find('li.selected span.'+ settings.classPrefix +'-option-value').text() || "";
			}
			
			// function that returns the current TEXT of the selectBox
			function _getText(){
				return optionListEl.find('li.selected span.'+ settings.classPrefix +'-option-text').text() || "";
			}
			
			// function that returns the index of the current "option"
			function _getIndex(){
				return optionListEl.find('li').index(optionListEl.find('li.selected')) || -1;
			}
			
			// function that sets the value of the selectBox -> to be used programaticly
			function _setValue(string){
				_selectOption(
					optionListEl
						.find('li span.'+ settings.classPrefix +'-option-value')
						.filter(function(){
						  return $(this).text() == string;
						})
						.first()
						.closest('li')
				);
			}
			
			function _setIndex(index){
				_selectOption(optionListEl.find('li').eq(index));
			}
			
			// function that "selects" the option provided as arguments
			function _selectOption(option){
				var wrapper = option.parents('.'+ settings.classPrefix +'-wrapper:first'),
					oldVal = _getValue(),
					newVal = option.find('span.'+ settings.classPrefix +'-option-value').text();
				option
					.siblings('.selected')
						.removeClass('selected')
					.end()
					.addClass('selected');
				wrapper.find('.' + settings.classPrefix +'-value').text(option.find('span.'+ settings.classPrefix +'-option-text').text());					
				_triggerEvent('change',oldVal,newVal);
			}
			
			function _expandList(el,options){
				var o = $.extend({},settings.expandOptions,options);
				(el || optionListEl)
					.stop()
					.slideDown(o.duration,o.easing,o.callback);
				_triggerEvent('expand');
			}
			
			function _collapseList(el,options){
				var o = $.extend({},settings.collapseOptions,options);
				(el || optionListEl)
					.stop()
					.slideUp(o.duration,o.easing,o.callback);
				_triggerEvent('collapse');
			}
			
			function _toggleListDisplay(el,options){
				var element = el || optionListEl;
				return element.is(':visible') 
					? _collapseList(element,options) 
					: _expandList(element,options);
			}
			
			
			
			
			// EVENT BINDING
			// it uses some live for binding events in order to provide the same functionality for dynamically added components
			
			// if the current dom element is not one created by this certain constructor return false, in order to not proceed with this event handler
			function _isCorrectElement(el){
				return el.parents('div.'+ settings.classPrefix +'-wrapper:first').get(0) == wrapperEl.get(0);
			}
			
			function _triggerEvent(name){
				var args = Array.prototype.slice.call(arguments,1,arguments.length);
				wrapperEl.trigger(name,args);
				wrapperEl.trigger(name + '.' + settings.eventNamespace,args);
			}
			
			function _bindEvents(){
				if(settings.copyEvents)
					$.fn.jSelectBox._copyEvents($originalEl,wrapperEl);
				wrapperEl
					.bind('click',function(e){
						var ul = $(this).find('ul');
						if(!ul.is(':animated'))
							_toggleListDisplay(ul);
					});
			
				// the "options" li
			//	$('.'+ settings.classPrefix +'-option')
				optionListEl.find('li')
					// click  event handler
					.bind('click',function(e){
						_selectOption($(this));
					});
				
				$(document).click(function(e){
					// if the target is not and not inside our wrapper, release the event
					if(!settings.collapseOnBlur || e.target == wrapperEl.get(0) || $(e.target).closest('.jselectbox').get(0) == wrapperEl.get(0))
						return;
					_collapseList();
				});
			}
			
			
			
			
			
			
			
			
			// external api	
			this.initialise = function(){
				_initialise.apply(this,arguments);
				return this;
			};
			this.destroy = function(){
				_destroy.apply(this,arguments);
				return this;
			};
			this.reinitialise = function(){
				return
					this
						.destroy()
						.initialise.apply(this,arguments);
			};
			this.getSelectedValue = _getValue;
			this.getSelectedText = _getText;
			this.getSelectedIndex = _getIndex;
			this.selectValue = function(v){
				_setValue(v);
				return this;
			};
			this.selectIndex = function(i){
				_setIndex(i);
				return this;
			};
			this.expandList = _expandList;
			this.collapseList = _collapseList;
			this.toggleList = _toggleListDisplay;
			
		}
		
		return this.each(function(){
			var that = $(this),
				_api = that.data('jSelectBoxApi');
			if(_api instanceof jSelectBox)
				_api.reinitialise(that,options);
			else
				var jsb = new jSelectBox(this,options);
				window.jsb = jsb;
		});
	}
	
	// the default options
	$.fn.jSelectBox.defaults = {
		classPrefix : 'jselectbox',			// the class prefix that all of the components will have -> nice feature for quick access & applying different themes to different selectboxes
		eventNamespace : 'jselectbox',		// the namespace that the events will have : event.namepsace
		expandOptions : {					// options for the expand animation 
			easing : 'swing',
			duration : 300,
			callback : null
		},
		collapseOptions : {					// options for the collapse animation 
			easing : 'swing',
			duration : 300,
			callback : null
		},
		collapseOnBlur : true,				// if true, when the selectbox loses focus, it will collapse (if expanded)
		copyEvents	: false					// if true, the new selectBox will copy the old one's eventhandlers
	}
	
	// version
	$.fn.jSelectBox.version = 0.1;
	
	// the original  ".val" jquery method
	$.fn.jSelectBox._originalValFn = $.fn.val;
	
	// restore the original  ".val" jquery method
	$.fn.jSelectBox.restoreApi = function(){
		$.fn.val = $.fn.jSelectBox._originalValFn;
	};
	
	$.fn.jSelectBox.classPrefixes = [];
	
	$.fn.jSelectBox.shouldUseOriginalValMethod = function(el){
		var pref = $.unique($.fn.jSelectBox.classPrefixes);
		for(var i=0,l=pref.length;i<l;i++)
			if(el.hasClass(pref[i]))
				return false;
		return true;
	}
	
	$.fn.jSelectBox._copyEvents = function(from,to){
		var fromEl = from.get(0),
			events = fromEl && ($.data && $.data(fromEl, 'events') || fromEl.$events || fromEl.events) || {};
		to.each(function(i,el){
			for (var type in events)
				{
					// in jQuery 1.4.2+ event handlers are stored in an array, previous versions it is an object
					$.each(events[type], function(index, handler) {
						// in jQuery 1.4.2+ handler is an object with a handle property
						// in previous versions it is the actual handler with the properties tacked on
						var namespaces = handler.namespace !== undefined && handler.namespace || handler.type || '';
						namespaces = namespaces.length ? (namespaces.indexOf('.') === 0 ? '' : '.')+namespaces : '';
						$.event.add(el, type + namespaces, handler.handler || handler, handler.data);
					});
				}
		});
	}
	
	// overwrite the default 'val' functionality
	$.fn.val = function(){
		var _api = this.data('jSelectBoxApi');
		if(!_api)
			return $.fn.jSelectBox._originalValFn.apply(this,arguments);
		if(arguments[0] === undefined)
			return _api.getSelectedValue();
		if(typeof arguments[0] != 'function')
			_api.selectValue(arguments[0]);
		return this;
	}

})(jQuery,window);

