/*
 * jQuery UI Multiselect
 *
 * Authors:
 *  Michael Aufreiter (quasipartikel.at)
 *  Yanick Rochon (yanick.rochon[at]gmail[dot]com)
 * 
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 * 
 * http://www.quasipartikel.at/multiselect/
 *
 * 
 * Depends:
 *	ui.core.js
 *	ui.sortable.js
 *
 * Optional:
 * localization (http://plugins.jquery.com/project/localisation)
 * scrollTo (http://plugins.jquery.com/project/ScrollTo)
 * 
 * Todo:
 *  Make batch actions faster
 *  Implement dynamic insertion through remote calls
 */


 (function($) {

	$.widget("ui.multiselect", {
	  options: {
			sortable: true,
			searchable: true,
			doubleClickable: true,
			animated: 'fast',
			show: 'slideDown',
			hide: 'slideUp',
			dividerLocation: 0.6,
			availableFirst: false,
			nodeComparator: function(node1,node2) {
				var text1 = node1.text(),
					text2 = node2.text();
				return text1 == text2 ? 0 : (text1 < text2 ? -1 : 1);
			}
		},
		_create: function() {
			this.element.hide();
			this.id = this.element.attr("id");
			this.container = $('<div class="ui-multiselect ui-helper-clearfix ui-widget"></div>').insertAfter(this.element);
			this.count = 0; // number of currently selected options
			this.selectedContainer = $('<div class="selected"></div>').appendTo(this.container);
			this.availableContainer = $('<div class="available"></div>')[this.options.availableFirst?'prependTo': 'appendTo'](this.container);
			this.selectedActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><span class="count">0 '+$.ui.multiselect.locale.itemsCount+'</span><a href="#" class="remove-all">'+$.ui.multiselect.locale.removeAll+'</a></div>').appendTo(this.selectedContainer);
			this.availableActions = $('<div class="actions ui-widget-header ui-helper-clearfix"><input type="text" class="search empty ui-widget-content ui-corner-all"/><a href="#" class="add-all">'+$.ui.multiselect.locale.addAll+'</a></div>').appendTo(this.availableContainer);
			this.selectedList = $('<ul class="selected connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.selectedContainer);
			this.availableList = $('<ul class="available connected-list"><li class="ui-helper-hidden-accessible"></li></ul>').bind('selectstart', function(){return false;}).appendTo(this.availableContainer);
			
			var that = this;
	
			// set dimensions
			this.container.width(this.element.width()+1);
			this.selectedContainer.width(Math.floor(this.element.width()*this.options.dividerLocation));
			this.availableContainer.width(Math.floor(this.element.width()*(1-this.options.dividerLocation)));
	
			// fix list height to match <option> depending on their individual header's heights
			this.selectedList.height(Math.max(this.element.height()-this.selectedActions.height(),1));
			this.availableList.height(Math.max(this.element.height()-this.availableActions.height(),1));
			
			if ( !this.options.animated ) {
				this.options.show = 'show';
				this.options.hide = 'hide';
			}
			
			// init lists
			this._populateLists(this.element.find('option'));
			
			// make selection sortable
			if (this.options.sortable) {
				this.selectedList.sortable({
					placeholder: 'ui-state-highlight',
					axis: 'y',
					update: function(event, ui) {
						// apply the new sort order to the original selectbox
						that.selectedList.find('li').each(function() {
							if ($(this).data('optionLink'))
								$(this).data('optionLink').remove().appendTo(that.element);
						});
					},
					receive: function(event, ui) {
						ui.item.data('optionLink').prop('selected', true);
						// increment count
						that.count += 1;
						that._updateCount();
						// workaround, because there's no way to reference 
						// the new element, see http://dev.jqueryui.com/ticket/4303
						that.selectedList.children('.ui-draggable').each(function() {
							$(this).removeClass('ui-draggable');
							$(this).data('optionLink', ui.item.data('optionLink'));
							$(this).data('idx', ui.item.data('idx'));
							that._applyItemState($(this), true);
						});
				
						// workaround according to http://dev.jqueryui.com/ticket/4088
						setTimeout(function() { ui.item.remove(); }, 1);
					}
				});
			}
			
			// set up livesearch
			if (this.options.searchable) {
				this._registerSearchEvents(this.availableContainer.find('input.search'));
			} else {
				$('.search').hide();
			}
			
			// batch actions
			this.container.find(".remove-all").click(function() {
				that._populateLists(that.element.find('option').removeAttr('selected'));
				return false;
			});
			
			this.container.find(".add-all").click(function() {
				var options = that.element.find('option').not(":selected");
				if (that.availableList.children('li:hidden').length > 1) {
					that.availableList.children('li').each(function(i) {
						if ($(this).is(":visible")) $(options[i-1]).attr('selected', 'selected'); 
					});
				} else {
					options.attr('selected', 'selected');
				}
				that._populateLists(that.element.find('option'));
				return false;
			});
		},
		destroy: function() {
			this.element.show();
			this.container.remove();
	
			$.Widget.prototype.destroy.apply(this, arguments);
		},
		_populateLists: function(options) {
			this.selectedList.children('.ui-element').remove();
			this.availableList.children('.ui-element').remove();
			this.count = 0;
	
			var that = this;
			var items = $(options.map(function(i) {
			  var item = that._getOptionNode(this).appendTo(this.selected ? that.selectedList : that.availableList).show();
	
				if (this.selected) that.count += 1;
				that._applyItemState(item, this.selected);
				item.data('idx', i);
				return item[0];
		}));
			
			// update count
			this._updateCount();
			that._filter.apply(this.availableContainer.find('input.search'), [that.availableList]);
	  },
		_updateCount: function() {
			this.element.trigger('change');
			this.selectedContainer.find('span.count').text(this.count+" "+$.ui.multiselect.locale.itemsCount);
		},
		_getOptionNode: function(option) {
			option = $(option);
			var node = $('<li class="ui-state-default ui-element" title="'+option.text()+'" data-selected-value="' + option.val() + '"><span class="ui-icon"></span>'+option.text()+'<a href="#" class="action"><span class="ui-corner-all ui-icon"/></a></li>').hide();
			node.data('optionLink', option);
			return node;
		},
		// clones an item with associated data
		// didn't find a smarter away around this
		_cloneWithData: function(clonee) {
			var clone = clonee.clone(false,false);
			clone.data('optionLink', clonee.data('optionLink'));
			clone.data('idx', clonee.data('idx'));
			return clone;
		},
		_setSelected: function(item, selected) {
			item.data('optionLink').attr('selected', selected);
	
			if (selected) {
				var selectedItem = this._cloneWithData(item);
				item[this.options.hide](this.options.animated, function() { $(this).remove(); });
				selectedItem.appendTo(this.selectedList).hide()[this.options.show](this.options.animated);
				
				this._applyItemState(selectedItem, true);
				return selectedItem;
			} else {
				
				// look for successor based on initial option index
				var items = this.availableList.find('li'), comparator = this.options.nodeComparator;
				var succ = null, i = item.data('idx'), direction = comparator(item, $(items[i]));
	
				// TODO: test needed for dynamic list populating
				if ( direction ) {
					while (i>=0 && i<items.length) {
						direction > 0 ? i++ : i--;
						if ( direction != comparator(item, $(items[i])) ) {
							// going up, go back one item down, otherwise leave as is
							succ = items[direction > 0 ? i : i+1];
							break;
						}
					}
				} else {
					succ = items[i];
				}
				
				var availableItem = this._cloneWithData(item);
				succ ? availableItem.insertBefore($(succ)) : availableItem.appendTo(this.availableList);
				item[this.options.hide](this.options.animated, function() { $(this).remove(); });
				availableItem.hide()[this.options.show](this.options.animated);
				
				this._applyItemState(availableItem, false);
				return availableItem;
			}
		},
		_applyItemState: function(item, selected) {
			if (selected) {
				if (this.options.sortable)
					item.children('span').addClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').addClass('ui-icon');
				else
					item.children('span').removeClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').removeClass('ui-icon');
				item.find('a.action span').addClass('ui-icon-minus').removeClass('ui-icon-plus');
				this._registerRemoveEvents(item.find('a.action'));
				
			} else {
				item.children('span').removeClass('ui-icon-arrowthick-2-n-s').removeClass('ui-helper-hidden').removeClass('ui-icon');
				item.find('a.action span').addClass('ui-icon-plus').removeClass('ui-icon-minus');
				this._registerAddEvents(item.find('a.action'));
			}
			
			this._registerDoubleClickEvents(item);
			this._registerHoverEvents(item);
		},
		// taken from John Resig's liveUpdate script
		_filter: function(list) {
			var input = $(this);
			var rows = list.children('li'),
				cache = rows.map(function(){
					
					return $(this).text().toLowerCase();
				});
			
			var term = $.trim(input.val().toLowerCase()), scores = [];
			
			if (!term) {
				rows.show();
			} else {
				rows.hide();
	
				cache.each(function(i) {
					if (this.indexOf(term)>-1) { scores.push(i); }
				});
	
				$.each(scores, function() {
					$(rows[this]).show();
				});
			}
		},
		_registerDoubleClickEvents: function(elements) {
			if (!this.options.doubleClickable) return;
			elements.dblclick(function(ev) {
				if ($(ev.target).closest('.action').length === 0) {
					// This may be triggered with rapid clicks on actions as well. In that
					// case don't trigger an additional click.
					elements.find('a.action').click();
				}
			});
		},
		_registerHoverEvents: function(elements) {
			elements.removeClass('ui-state-hover');
			elements.mouseover(function() {
				$(this).addClass('ui-state-hover');
			});
			elements.mouseout(function() {
				$(this).removeClass('ui-state-hover');
			});
		},
		_registerAddEvents: function(elements) {
			var that = this;
			elements.click(function() {
				var item = that._setSelected($(elements).parent(), true);
				that.count += 1;
				that._updateCount();
				return false;
			});
			
			// make draggable
			if (this.options.sortable) {
			  elements.each(function() {
				  $(this).parent().draggable({
				connectToSortable: that.selectedList,
					  helper: function() {
						  var selectedItem = that._cloneWithData($(this)).width($(this).width() - 50);
						  selectedItem.width($(this).width());
						  return selectedItem;
					  },
					  appendTo: that.container,
					  containment: that.container,
					  revert: 'invalid'
			  });
			  });		  
			}
		},
		_registerRemoveEvents: function(elements) {
			var that = this;
			elements.click(function() {
				that._setSelected($(elements).parent(), false);
				that.count -= 1;
				that._updateCount();
				return false;
			});
		 },
		_registerSearchEvents: function(input) {
			var that = this;
	
			input.focus(function() {
				$(this).addClass('ui-state-active');
			})
			.blur(function() {
				$(this).removeClass('ui-state-active');
			})
			.keypress(function(e) {
				if (e.keyCode == 13)
					return false;
			})
			.keyup(function() {
				that._filter.apply(this, [that.availableList]);
			});
		}
	});
			
	$.extend($.ui.multiselect, {
		locale: {
			addAll:'Add all',
			removeAll:'Remove all',
			itemsCount:'items selected'
		}
	});
	
	
	})(jQuery);
	
