/**
 * jQuery Fi Dropdown Component
 * 
 * Replaces the browser's default select element with a component that can be 
 * skinned with greater control.
 * 
 * @author Kevin Sweeney (Fi)
 * @author Karl Stanton (Fi)
 * @version 0.9
 * @uses 			$.fn.equals		To help compare DOM elements
 * 
 * -----------------------------------------------------------------------------
 * OPTIONS
 * -----------------------------------------------------------------------------
 * 
 * 1. {String}		className		Default class to apply to the component
 * 2. {String}		defaultLabel	Default label to display of no "selected" attribute has been specified
 * 3. {Boolean}		autoSize		Specifies if the width of the component should stretch to fit its longest option
 * 4. {Function}	open			Callback to invoke when dropdown menu is opened
 * 5. {Function}	close			Callback to invoke when dropdown menu is closed
 * 6. {Function}	focus			Callback to invoke when component receives focus
 * 7. {Function}	blur			Callback to invoke when component loses focus
 * 8. {Function}	change			Callback to invoke when component's value changes
 * 
 * -----------------------------------------------------------------------------
 * EXAMPLES
 * -----------------------------------------------------------------------------
 * 
 * HTML:
 * <select>
 * 		<option value="opt1">Option #1</option>
 * 		<option value="opt2">Option #2</option>
 * 		<option value="opt3">Option #3</option>
 * 		<option value="opt4" selected="selected">Option #4</option>
 * 		<option value="opt5">Option #5</option>
 * </select>
 * 
 * Javascript:
 * $('select').dropdown();
 * 
 * $('select.customDropdown').dropdown({
 *     defaultLabel: 'Choose One...',
 *     autoSize: false,
 *     change: function () {
 *     		alert($(this).val());
 *     };
 * });
 * 
 * -----------------------------------------------------------------------------
 * TODO LIST
 * -----------------------------------------------------------------------------
 * 
 * 1. Keyboard and aaccessibility features (focus event on <select> should 
 *    immediately shift focus to replacement control)
 *    
 * 2. Consider implications of using the <optgroup> tag (handle either by using 
 *    nested lists or use the find() method instead of children() when getting 
 *    <option> elements)
 * 
 */



/**
 * Drop down
 * @param {Object} $
 */
(function ($) {
	
	var DROPDOWN_STATE, KEY, BUTTON_ARROW_WIDTH;
	
	DROPDOWN_STATE = {
		CLOSED: 'closed',
		OPEN: 'open'
	};
	
	KEY = {
		BACKSPACE: 8,
		DELETE: 46,
		DOWN: 40,
		END: 35,
		ENTER: 13,
		ESCAPE: 27,
		HOME: 36,
		LEFT: 37,
		PAGE_DOWN: 34,
		PAGE_UP: 33,
		RIGHT: 39,
		TAB: 9,
		UP: 38
	};
	
	BUTTON_ARROW_WIDTH = $.browser.mozilla ? 14 : 16;
	
	/**
	 * @constructor
	 */
	$.fn.dropdown = function (customOptions) {
		
		$.fn.dropdown.defaults = {
			className: 'dropdown',
			defaultLabel: '- Select -',
			autoSize: true,
			open: function () {},
			close: function () {},
			focus: function () {},
			blur: function () {},
			change: function () {}
		};
		
		var options = $.extend({}, $.fn.dropdown.defaults, customOptions);
		
		this.each(function () {
			
			var instance, container, component, button, label, listWrapper, slidingWrapper, list, listItems, selectedOption, state, width;
			
		// Initial Setup _______________________________________________________
			
			/**
			 * Initializes the component, creating the new markup and establishing
			 * references to all subcomponents.
			 */
			function init() {
				
				// Create new HTML
				container		= $('<div><div><div class="dropdown-button"><span class="dropdown-label"><span></span></span></div><div class="dropdown-list"><div class="dropdown-wrapper"><ul></ul></div></div></div></div>');
				
				// Define subcomponents
				component		= container.children('div').addClass(options.className);
				button			= component.children('div.dropdown-button');
				label			= button.children('span.dropdown-label').children('span');
				listWrapper		= component.children('div.dropdown-list').hide();
				slidingWrapper	= listWrapper.children('div.dropdown-wrapper');
				list			= slidingWrapper.children('ul');
				
				// Add options to new list element
				$('option', instance).each(function (i) {
					list.append('<li>' + $(this).text() + '</li>');
				});
				
				// Add helper classes for easier CSS styling
				list.children('li:first').addClass('first');
				list.children('li:last').addClass('last');
				
				listItems = list.children('li');
				
				// Find out the original width, and replace original component
				width = instance.css('width')
				//width = instance.width();
				instance.hide().after(component);
				
			}
			
			/**
			 * Sets the initial state of the dropdown components
			 */
			function setProperties() {
				
				component.data('state', DROPDOWN_STATE.CLOSED);
				
				selectedOption	= instance.find(':selected');				
				
				// If the "select" element has a static width set it as the static width for the component
				if (width !== 'auto' && width !== 'intrinsic') {
					width = parseInt(width.split('px').join(''), 10) + 'px';
					component.css('width', width);
					slidingWrapper.css('width', width);					
				}				
				// Gets the longest item in the list, temporarily sets it as the 
				// label, calculates the new width, and sets that as the static 
				// width for the component
				else if (options.autoSize === true) {
					label.text(getLongestItem());
					component.css('width', component.width() + 'px');
					slidingWrapper.css('width', component.width() + 'px');
				}
				
				// If an item is already selected, set that as the default label
				// Otherwise, use the default label specified
				if (selectedOption.length > 0) {
					label.text(selectedOption.text());
				}
				else {
					label.text(options.defaultLabel);
				}
				
			}
			
			/**
			 * Binds all events used by the component and its associated select element
			 */
			function registerEvents() {
				
				// Original select control
				instance.change(onChange).keyup(onKeyUp);
				
				// Toggle button
				button.click(onButtonClick);
				
				// List item selection
				list.click(onListClick);
				
			}
			
		// Event Handlers ______________________________________________________
			
			/**
			 * Updates the value of the text label and the value of the hidden
			 * select control element
			 * @param {Event} e - Change event object
			 */
			function onChange(e) {
				
				selectedOption = $(':selected', instance);
				label.text(selectedOption.text());
				
				// Trigger callback
				options.change.call(this);
				
			}
			
			/**
			 * Updates the value of the hidden select control element
			 * @param {Event} e - Key event object
			 */
			function onKeyUp(e) {
				instance.change();
			}
			
			/**
			 * Opens the dropdown if it is closed, or closes it if it is open
			 * @param {Event} e - Click event object
			 */
			function onButtonClick(e) {
								
				if (component.data('state') === DROPDOWN_STATE.CLOSED) {
					open();
				}
				else if (component.data('state') === DROPDOWN_STATE.OPEN) {
					close();
				}
				
				return false;
				
			}
			
			/**
			 * Sets the selected item of the actual select element when an 
			 * option from this skinned component is selected
			 * @param {Event} e - Click event object
			 */
			function onListClick(e) {
				
				if (e.target.nodeName.toLowerCase() === 'li') {
					selectItem(e.target);
					close();
					return false;
				}
				
			}
			
		// Utility Methods _____________________________________________________
			
			/**
			 * Returns the list item that has the longest amount of text
			 * @return {ListElement} The element whose text length is the longest
			 * @see setProperties()
			 */
			function getLongestItem() {
				
				var currentItem, longestItem;
				
				// Set initial value (so there's something to check against)
				longestItem = $(listItems.get(0)).text();
				
				// Comparison check
				listItems.each(function (i, val) {
					
					currentItem = $(val).text();
					
					if (currentItem.length > longestItem.length) {
						longestItem = currentItem;
					}
					
				});
				
				return longestItem;
				
			}
			
		// List Positioning ____________________________________________________
			
			/**
			 * Checks the height of the dropdown against the scroll position of 
			 * the page to determine whether to display the list above or below 
			 * the button control. If it will get cut off either way, the list 
			 * will be clipped on the bottom, so that users don't have to scroll 
			 * back up to view the options.
			 */
			function updateListPosition () {
	
				var listPosY, listHeight;
				
				listPosY	= component.offset().top + component.height();
				listHeight	= listWrapper.outerHeight();
				
				// Reset position
				listWrapper.css('top', '');
				
				// Position list above button if window is going to cut it off,
				// but only if there is room to position it above the button
				if ((listWillClipBottom(listPosY, listHeight) === true) && (listWillClipTop(listPosY, listHeight) === false)) {
					listWrapper.css('top', (listHeight * -1) + 'px');
					component.addClass('top');
				} else {
					component.removeClass('top');
				}
				
			}
			
			/**
			 * Simple check to see if list will be cut off by the bottom edge of 
			 * the browser window
			 * @param {Number} yPos - The position of the list on the Y axis
			 * @param {Number} height - The computed height of the list
			 * @return Whether or not bottom edge of list will be clipped by bottom edge of browser
			 */
			function listWillClipBottom (yPos, height) {
				var scrollOffset = $(window).height() + getScrollXY()[1];
				return (yPos + height > scrollOffset);
			}
			
			/**
			 * Simple check to see if list will be cut off by the top edge of 
			 * the browser window
			 * @param {Number} yPos - The position of the list on the Y axis
			 * @param {Number} height - The computed height of the list
			 * @return Whether or not top edge of list will be clipped by top edge of browser
			 */
			function listWillClipTop (yPos, height) {
				return (yPos - height < 0);
			}

			/**
			 * Fixes scroll position bugs across browsers
			 * http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
			 */
			function getScrollXY () {
				var scrOfX = 0, scrOfY = 0;
				if (typeof(window.pageYOffset) === 'number') {
					//Netscape compliant
					scrOfY = window.pageYOffset;
					scrOfX = window.pageXOffset;
				}
				else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) {
					//DOM compliant
					scrOfY = document.body.scrollTop;
					scrOfX = document.body.scrollLeft;
				}
				else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) {
					//IE6 standards compliant mode
					scrOfY = document.documentElement.scrollTop;
					scrOfX = document.documentElement.scrollLeft;
				}
				return [scrOfX, scrOfY];
			}

		// Public Methods ______________________________________________________
			
			/**
			 * Sets the component to its open state
			 */
			function open() {
				
				component.data('state', DROPDOWN_STATE.OPEN);
				
				// Closes all drop downs
				closeAllOther();
				
				$('html').bind('mousedown', function (e) {
					if ($(e.target).parents('.dropdown').length === 0) {
						close();
					}
				});
					
				updateListPosition();
				component.addClass('open');
				listWrapper.show();

				// Trigger callback
				options.open.call(this);
				
			}
			
			/**
			 * Sets the component to its closed state
			 */
			function close() {
				
				component.data('state', DROPDOWN_STATE.CLOSED);
				
				// Close all open drop downs
				$('.' + options.className).each(function () {
					$(this).children('div.dropdown-list').hide();
				}).removeClass('open').removeClass('top');
				//listWrapper.hide();
				
				$('html').unbind('mousedown');
				
				// Trigger callback
				options.close.call(this);
				
			}
			
			/**
			 * Closes all other dropdowns except this one
			 */
			function closeAllOther () {
				
				$('.dropdown').each(function () {
					
					var dropdown = $(this);
					if(!dropdown.equals(component)){
						
						// Close it
						dropdown.each(function () {
							dropdown.children('div.dropdown-list').hide();
						}).removeClass('open').removeClass('top');
						
						dropdown.data('state', DROPDOWN_STATE.CLOSED);
						
					}
					
					
				});
				
			}
			
			/**
			 * Sets the selected item, giving it a custom "selected" class and 
			 * updating the selected index of the select element
			 * @param {HTMLElement} listItem - The LI element that was clicked
			 */
			function selectItem(listItem) {
				
				listItems.removeClass('selected');
				$(listItem).addClass('selected');
				
				instance[0].selectedIndex = listItems.index(listItem);
				instance.change();
			}
			
		// Initialization ______________________________________________________
			
			instance = $(this);
			
			init();
			setProperties();
			registerEvents();
			
		});
		
		return this;
		
	};
	
})(jQuery);





/**
 * Compares two DOM elements. Returns true if they are the same DOM element
 * @param {Object} compareTo - DOM element
 */
$.fn.equals = function(compareTo) {
	if (!compareTo || !compareTo.length || this.length!=compareTo.length) {
		return false;
	}
	for (var i = 0; i < this.length; i++) {
		if (this[i] !== compareTo[i]) {
			return false;
		}
	}
	return true;
} 

