﻿/**
 * Dropdown class
 * Creates customly styleable dropdown from a <select>-tag
 *
 * @author Klaas Dieleman, <klaas[AT]efocus.nl>
 * @since 19 may 2010
 * @version 1.1
 * @copyright eFocus
 *
 * @uses jQuery 1.4.2, <http://www.jquery.com>
 *
 * @param object <select>-element to customize
 * @param object options for customizing the layout and behaviour of the dropdown
 *
 */
function Dropdown(elSelect, options){
	
	this.cleanupOrphans();
	
	if(typeof(elSelect) == 'object' && elSelect.tagName == 'SELECT' && jQuery.data(elSelect).hasDropDown != true) {
	
		jQuery.data(elSelect, 'hasDropDown', true);
		
		this.options = {
			itemsWithoutScroll	: 20,			// max items to show without vert. scrollbar
			dropdownEffect		: '',			// optional effect for opening dropdown, 'slide' or 'fade'
			effectDuration		: 300,			// duration of optional effect, in ms
			cssClassPrefix		: 'customdd'	// optional alternative prefix for css class, handy if you need different styles in one site
		};
		
		jQuery.extend(this.options, options);
		
		// declare variables
		this.blnListOpen = false;
		
		// declare 'constants'
		this.EL_SELECT = jQuery(elSelect);
		this.EL_DROPDOWN = jQuery('<ul class="' + this.options.cssClassPrefix + '"></ul>');
		this.ARR_LIST_ITEMS = [];
		this.INT_LIST_HEIGHT = 0;
		this.EL_TOGGLER = jQuery('<div class="' + this.options.cssClassPrefix + '_toggler"></div>');
		
		this.initialize();
		
	} else {
		
		return false;
	
	}
	
};

Dropdown.prototype = {
	
	/**
	 * The init/controller function
	 */
	initialize: function() {
		
		this.EL_SELECT.hide();
		this.createDropdown();
		this.setStyles();
		this.addEvents();
		
		// this is executed already in case no 'load' event is triggered, for example with AJAX-calls
		this.positionList();
		this.INT_LIST_HEIGHT = this.getListHeight();
		
		jQuery(window).load(jQuery.proxy(function(){
		
			this.positionList();
			this.INT_LIST_HEIGHT = this.getListHeight();
		
		}, this));
		
	},
	
	/**
	 * Creates and injects new DOM elements, based on given <select> with its <option>s
	 *
	 * HTML structure is a <div> which is the toggler, this replaces the <select> on screen and
	 * a <ul> which gets injected at the bottom of the <body> for z-index/stack-order reasons
	 */
	createDropdown: function() {
	
		this.EL_SELECT.children('option').each(jQuery.proxy(function(index, elOption){
				this.ARR_LIST_ITEMS[index] = jQuery('<li>' + jQuery(elOption).text() + '</li>');
			this.ARR_LIST_ITEMS[index].attr('val', jQuery(elOption).val());
			this.EL_DROPDOWN.append(this.ARR_LIST_ITEMS[index]);
		}, this));
		
		this.EL_SELECT.after(this.EL_TOGGLER);		
		jQuery('body').append(this.EL_DROPDOWN);
		
		this.EL_TOGGLER.text(this.EL_SELECT.children('option:selected').text());
		
		jQuery.data(this.EL_DROPDOWN.get(0), 'isCustomDropDown', true);
		jQuery.data(this.EL_DROPDOWN.get(0), 'matchingSelect', this.EL_SELECT);
		jQuery.data(this.EL_DROPDOWN.get(0), 'matchingToggler', this.EL_TOGGLER);
	
	},
	
	/**
	 * Sets necessary styles on dropdown list
	 */
	setStyles: function() {
	
		this.EL_DROPDOWN.css({
			'overflow'	: 'hidden',
			'height'	: 0,
			'visibility': 'hidden'
		});
		
		if(this.options.dropdownEffect == 'fade') {
			this.EL_DROPDOWN.css({
				'opacity'	: 0
			});
		}
	
	},
	
	/**
	 * Positions dropdown list under dropdown toggler element
	 */
	positionList: function() {
	
		this.EL_DROPDOWN.css({
			'position'	: 'absolute',
			'left'		: this.EL_TOGGLER.offset().left,
			'top'		: this.EL_TOGGLER.offset().top + this.EL_TOGGLER.outerHeight()
		});

	},
	
	/**
	 * Gets height of dropdown list
	 * @return int height of list, but only the visible area within vert. scrollbar
	 */
	getListHeight: function() {
	
		if(this.options.itemsWithoutScroll > this.ARR_LIST_ITEMS.length) {
			this.options.itemsWithoutScroll = this.ARR_LIST_ITEMS.length;
		}
		
		return this.ARR_LIST_ITEMS[0].outerHeight() * this.options.itemsWithoutScroll;
	
	},
	
	
	/**
	 * Binds event listeners to dropdown toggler, listitems, document and window
	 * for the behaviour of the dropdown
	 */
	addEvents: function() {
	
		// hide&show dropdown list
		this.EL_TOGGLER.click(jQuery.proxy(function(event){
		
			event.preventDefault();
			event.stopPropagation();
			
			if(this.blnListOpen) {
				this.hideList();
			} else {
				this.showList();
			}
			
		}, this));
		
		// hide dropdown list when clicking anywhere but on the toggler
		jQuery(document).click(jQuery.proxy(function(){

			if(this.blnListOpen) this.hideList();
			
		}, this));
		
		// reposition dropdown list when resizing browser window
		jQuery(window).resize(jQuery.proxy(function(){
		
			this.positionList();
			
		}, this));
		
		// change dropdown and original <select> when clicking on an option
		jQuery(this.ARR_LIST_ITEMS).each(jQuery.proxy(function(index, elListItem){
		
			elListItem.click(jQuery.proxy(function(event){

				if(this.EL_SELECT.val() != jQuery(event.target).attr('val')) {
				
					this.EL_SELECT.val(jQuery(event.target).attr('val'));
					this.EL_TOGGLER.text(jQuery(event.target).text());
					this.hideList();
					
					this.EL_SELECT.trigger('change');
					
				}
				
			}, this));
			
		}, this));
	
	},
	
	/**
	 * Shows dropdown list, optionally with effect
	 */
	showList: function() {
		
		this.positionList();
		this.EL_DROPDOWN.clearQueue();
		this.EL_DROPDOWN.css('visibility', 'visible');
		
		switch(this.options.dropdownEffect) {
		
			case 'slide':
				this.EL_DROPDOWN.animate({'height' : this.INT_LIST_HEIGHT}, this.options.effectDuration, jQuery.proxy(function(){
					this.EL_DROPDOWN.css('overflow-y', 'auto');
					this.blnListOpen = true;
					this.EL_TOGGLER.addClass(this.options.cssClassPrefix + '_toggler_open');
				}, this));
				break;
				
			case 'fade':
				this.EL_DROPDOWN.css('height', this.INT_LIST_HEIGHT);
				this.EL_DROPDOWN.animate({'opacity' : 1}, this.options.effectDuration, jQuery.proxy(function(){
					this.EL_DROPDOWN.css('overflow-y', 'auto');
					this.blnListOpen = true;
					this.EL_TOGGLER.addClass(this.options.cssClassPrefix + '_toggler_open');
				}, this));
				break;
				
			default:
				this.EL_DROPDOWN.css({
					'height'	: this.INT_LIST_HEIGHT,
					'overflow'	: 'auto'
				});
				this.blnListOpen = true;
				this.EL_TOGGLER.addClass(this.options.cssClassPrefix + '_toggler_open');
	
		}
	
	},
	
	/**
	 * Hides dropdown list, optionally with effect
	 */
	hideList: function() {

		this.EL_DROPDOWN.clearQueue();
		this.EL_DROPDOWN.css('overflow', 'hidden');
		
		switch(this.options.dropdownEffect) {
		
			case 'slide':
				this.EL_DROPDOWN.animate({'height' : 0}, this.options.effectDuration, jQuery.proxy(function(){
					this.blnListOpen = false;
					this.EL_TOGGLER.removeClass(this.options.cssClassPrefix + '_toggler_open');
					this.EL_DROPDOWN.css('visibility', 'hidden');
				}, this));
				break;
				
			case 'fade':
				this.EL_DROPDOWN.animate({'opacity' : 0}, this.options.effectDuration, jQuery.proxy(function(){
					this.EL_DROPDOWN.css('height', 0);
					this.blnListOpen = false;
					this.EL_TOGGLER.removeClass(this.options.cssClassPrefix + '_toggler_open');
					this.EL_DROPDOWN.css('visibility', 'hidden');
				}, this));
				break;
				
			default:
				this.EL_DROPDOWN.css({
					'height'	: 0
				});
				this.blnListOpen = false;
				this.EL_TOGGLER.removeClass(this.options.cssClassPrefix + '_toggler_open');
				this.EL_DROPDOWN.css('visibility', 'hidden');
	
		}
		
	},
	
	/**
	 * Removes 'orphan' Custom Dropdown elements without matching <select>-element,
	 * for example those removed with AJAX
	 */
	cleanupOrphans: function() {
		
		jQuery('ul').each(function(index, item){
			
			if(jQuery.data(item).isCustomDropDown) {
				
				if(jQuery.inArray(document.body, jQuery.data(item).matchingSelect.parents()) == -1) {
					jQuery(item).remove();
					if(jQuery.data(item).matchingToggler) {
						jQuery.data(item).matchingToggler.remove();
					}
				}
			}
			
		});
		
	}
}