text: /*!
   * # Vanilla Semantic UI - Dropown
   * http://github.com/morganbarrett/vanilla-semantic-ui/
   *
   * Released under the MIT license
   * http://opensource.org/licenses/MIT
   *
   */
  
  ui.dropdown = function(element, settings){
  	var className = settings.className,
  		message = settings.message,
  		fields = settings.fields,
  		keys = settings.keys,
  		metadata = settings.metadata,
  		regExp = settings.regExp,
  		selector = settings.selector,
  		error = settings.error,
  		templates = settings.templates,
  		/*$context        = $(settings.context),
  		$text           = element.find(selector.text),
  		$search         = element.find(selector.search),
  		$sizer          = element.find(selector.sizer),
  		$input          = element.find(selector.input),
  		$icon           = element.find(selector.icon),
  		$combo = (element.prev().find(selector.text).length > 0)
  			? element.prev().find(selector.text)
  			: element.prev(),
  		$menu           = element.children(selector.menu),
  		$item           = $menu.find(selector.item),*/
  		activated = false,
  		itemActivated = false,
  		internalChange = false,
  		initialLoad,
  		pageLostFocus,
  		willRefocus,
  		elementNamespace,
  		id,
  		selectObserver,
  		menuObserver,
  		module;
  
  	/*module = {
  
  		initialize: function() {
  			module.debug('Initializing dropdown', settings);
  
  			if( module.is.alreadySetup() ) {
  				module.setup.reference();
  			}
  			else {
  
  				module.setup.layout();
  
  				if(settings.values) {
  					module.change.values(settings.values);
  				}
  
  				module.refreshData();
  
  				module.save.defaults();
  				module.restore.selected();
  
  				module.create.id();
  				module.bind.events();
  
  				module.observeChanges();
  				module.instantiate();
  			}
  
  		},
  
  		destroy: function() {
  			module.verbose('Destroying previous dropdown', element);
  			module.remove.tabbable();
  			element
  				.off(eventNamespace)
  				.removeData(moduleNamespace)
  			;
  			$menu
  				.off(eventNamespace)
  			;
  			$document
  				.off(elementNamespace)
  			;
  			module.disconnect.menuObserver();
  			module.disconnect.selectObserver();
  		},
  
  		observeChanges: function() {
  			if('MutationObserver' in window) {
  				selectObserver = new MutationObserver(module.event.select.mutation);
  				menuObserver   = new MutationObserver(module.event.menu.mutation);
  				module.debug('Setting up mutation observer', selectObserver, menuObserver);
  				module.observe.select();
  				module.observe.menu();
  			}
  		},
  
  		disconnect: {
  			menuObserver: function() {
  				if(menuObserver) {
  					menuObserver.disconnect();
  				}
  			},
  			selectObserver: function() {
  				if(selectObserver) {
  					selectObserver.disconnect();
  				}
  			}
  		},
  		observe: {
  			select: function() {
  				if(module.has.input()) {
  					selectObserver.observe(element[0], {
  						childList : true,
  						subtree   : true
  					});
  				}
  			},
  			menu: function() {
  				if(module.has.menu()) {
  					menuObserver.observe($menu[0], {
  						childList : true,
  						subtree   : true
  					});
  				}
  			}
  		},
  
  		create: {
  			id: function() {
  				id = (Math.random().toString(16) + '000000000').substr(2, 8);
  				elementNamespace = '.' + id;
  				module.verbose('Creating unique id for element', id);
  			},
  			userChoice: function(values) {
  				var
  					$userChoices,
  					$userChoice,
  					isUserValue,
  					html
  				;
  				values = values || module.get.userValues();
  				if(!values) {
  					return false;
  				}
  				values = $.isArray(values)
  					? values
  					: [values]
  				;
  				$.each(values, function(index, value) {
  					if(module.get.item(value) === false) {
  						html         = settings.templates.addition( module.add.variables(message.addResult, value) );
  						$userChoice  = $('<div />')
  							.html(html)
  							.attr('data-' + metadata.value, value)
  							.attr('data-' + metadata.text, value)
  							.classList.add(className.addition)
  							.classList.add(className.item)
  						;
  						if(settings.hideAdditions) {
  							$userChoice.classList.add(className.hidden);
  						}
  						$userChoices = ($userChoices === undefined)
  							? $userChoice
  							: $userChoices.add($userChoice)
  						;
  						module.verbose('Creating user choices for value', value, $userChoice);
  					}
  				});
  				return $userChoices;
  			},
  			userLabels: function(value) {
  				var
  					userValues = module.get.userValues()
  				;
  				if(userValues) {
  					module.debug('Adding user labels', userValues);
  					$.each(userValues, function(index, value) {
  						module.verbose('Adding custom user value');
  						module.add.label(value, value);
  					});
  				}
  			},
  			menu: function() {
  				$menu = $('<div />')
  					.classList.add(className.menu)
  					.appendTo(element)
  				;
  			},
  			sizer: function() {
  				$sizer = $('<span />')
  					.classList.add(className.sizer)
  					.insertAfter($search)
  				;
  			}
  		},
  
  		search: function(query) {
  			query = (query !== undefined)
  				? query
  				: module.get.query()
  			;
  			module.verbose('Searching for query', query);
  			if(module.has.minCharacters(query)) {
  				module.filter(query);
  			}
  			else {
  				module.hide();
  			}
  		},
  
  		select: {
  			firstUnfiltered: function() {
  				module.verbose('Selecting first non-filtered element');
  				module.remove.selectedItem();
  				$item
  					.not(selector.unselectable)
  					.not(selector.addition + selector.hidden)
  						.eq(0)
  						.classList.add(className.selected)
  				;
  			},
  			nextAvailable: function($selected) {
  				$selected = $selected.eq(0);
  				var
  					$nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
  					$prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
  					hasNext        = ($nextAvailable.length > 0)
  				;
  				if(hasNext) {
  					module.verbose('Moving selection to', $nextAvailable);
  					$nextAvailable.classList.add(className.selected);
  				}
  				else {
  					module.verbose('Moving selection to', $prevAvailable);
  					$prevAvailable.classList.add(className.selected);
  				}
  			}
  		},
  
  		setup: {
  			api: function() {
  				var
  					apiSettings = {
  						debug   : settings.debug,
  						urlData : {
  							value : module.get.value(),
  							query : module.get.query()
  						},
  						on    : false
  					}
  				;
  				module.verbose('First request, initializing API');
  				element
  					.api(apiSettings)
  				;
  			},
  			layout: function() {
  				if( element.is('select') ) {
  					module.setup.select();
  					module.setup.returnedObject();
  				}
  				if( !module.has.menu() ) {
  					module.create.menu();
  				}
  				if( module.is.search() && !module.has.search() ) {
  					module.verbose('Adding search input');
  					$search = $('<input />')
  						.classList.add(className.search)
  						.prop('autocomplete', 'off')
  						.insertBefore($text)
  					;
  				}
  				if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
  					module.create.sizer();
  				}
  				if(settings.allowTab) {
  					module.set.tabbable();
  				}
  			},
  			select: function() {
  				var
  					selectValues  = module.get.selectValues()
  				;
  				module.debug('Dropdown initialized on a select', selectValues);
  				if( element.is('select') ) {
  					$input = element;
  				}
  				// see if select is placed correctly already
  				if($input.parent(selector.dropdown).length > 0) {
  					module.debug('UI dropdown already exists. Creating dropdown menu only');
  					element = $input.closest(selector.dropdown);
  					if( !module.has.menu() ) {
  						module.create.menu();
  					}
  					$menu = element.children(selector.menu);
  					module.setup.menu(selectValues);
  				}
  				else {
  					module.debug('Creating entire dropdown from select');
  					element = $('<div />')
  						.attr('class', $input.attr('class') )
  						.classList.add(className.selection)
  						.classList.add(className.dropdown)
  						.html( templates.dropdown(selectValues) )
  						.insertBefore($input)
  					;
  					if($input.classList.contains(className.multiple) && $input.prop('multiple') === false) {
  						module.error(error.missingMultiple);
  						$input.prop('multiple', true);
  					}
  					if($input.is('[multiple]')) {
  						module.set.multiple();
  					}
  					if ($input.prop('disabled')) {
  						module.debug('Disabling dropdown');
  						element.classList.add(className.disabled);
  					}
  					$input
  						.removeAttr('class')
  						.detach()
  						.prependTo(element)
  					;
  				}
  				module.refresh();
  			},
  			menu: function(values) {
  				$menu.html( templates.menu(values, fields));
  				$item = $menu.find(selector.item);
  			},
  			reference: function() {
  				module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
  				// replace module reference
  				element  = element.parent(selector.dropdown);
  				instance = element.data(moduleNamespace);
  				element  = element.get(0);
  				module.refresh();
  				module.setup.returnedObject();
  			},
  			returnedObject: function() {
  				var
  					$firstModules = $allModules.slice(0, elementIndex),
  					$lastModules  = $allModules.slice(elementIndex + 1)
  				;
  				// adjust all modules to use correct reference
  				$allModules = $firstModules.add(element).add($lastModules);
  			}
  		},
  
  		refresh: function() {
  			module.refreshSelectors();
  			module.refreshData();
  		},
  
  		refreshItems: function() {
  			$item = $menu.find(selector.item);
  		},
  
  		refreshSelectors: function() {
  			module.verbose('Refreshing selector cache');
  			$text   = element.find(selector.text);
  			$search = element.find(selector.search);
  			$input  = element.find(selector.input);
  			$icon   = element.find(selector.icon);
  			$combo  = (element.prev().find(selector.text).length > 0)
  				? element.prev().find(selector.text)
  				: element.prev()
  			;
  			$menu    = element.children(selector.menu);
  			$item    = $menu.find(selector.item);
  		},
  
  		refreshData: function() {
  			module.verbose('Refreshing cached metadata');
  			$item
  				.removeData(metadata.text)
  				.removeData(metadata.value)
  			;
  		},
  
  		clearData: function() {
  			module.verbose('Clearing metadata');
  			$item
  				.removeData(metadata.text)
  				.removeData(metadata.value)
  			;
  			element
  				.removeData(metadata.defaultText)
  				.removeData(metadata.defaultValue)
  				.removeData(metadata.placeholderText)
  			;
  		},
  
  		toggle: function() {
  			module.verbose('Toggling menu visibility');
  			if( !module.is.active() ) {
  				module.show();
  			}
  			else {
  				module.hide();
  			}
  		},
  
  		show: function(callback) {
  			callback = $.isFunction(callback)
  				? callback
  				: function(){}
  			;
  			if(!module.can.show() && module.is.remote()) {
  				module.debug('No API results retrieved, searching before show');
  				module.queryRemote(module.get.query(), module.show);
  			}
  			if( module.can.show() && !module.is.active() ) {
  				module.debug('Showing dropdown');
  				if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
  					module.remove.message();
  				}
  				if(module.is.allFiltered()) {
  					return true;
  				}
  				if(settings.onShow.call(element) !== false) {
  					module.animate.show(function() {
  						if( module.can.click() ) {
  							module.bind.intent();
  						}
  						if(module.has.menuSearch()) {
  							module.focusSearch();
  						}
  						module.set.visible();
  						callback.call(element);
  					});
  				}
  			}
  		},
  
  		hide: function(callback) {
  			callback = $.isFunction(callback)
  				? callback
  				: function(){}
  			;
  			if( module.is.active() && !module.is.animatingOutward() ) {
  				module.debug('Hiding dropdown');
  				if(settings.onHide.call(element) !== false) {
  					module.animate.hide(function() {
  						module.remove.visible();
  						callback.call(element);
  					});
  				}
  			}
  		},
  
  		hideOthers: function() {
  			module.verbose('Finding other dropdowns to hide');
  			$allModules
  				.not(element)
  					.has(selector.menu + '.' + className.visible)
  						.dropdown('hide')
  			;
  		},
  
  		hideMenu: function() {
  			module.verbose('Hiding menu  instantaneously');
  			module.remove.active();
  			module.remove.visible();
  			$menu.transition('hide');
  		},
  
  		hideSubMenus: function() {
  			var
  				$subMenus = $menu.children(selector.item).find(selector.menu)
  			;
  			module.verbose('Hiding sub menus', $subMenus);
  			$subMenus.transition('hide');
  		},
  
  		bind: {
  			events: function() {
  				if(hasTouch) {
  					module.bind.touchEvents();
  				}
  				module.bind.keyboardEvents();
  				module.bind.inputEvents();
  				module.bind.mouseEvents();
  			},
  			touchEvents: function() {
  				module.debug('Touch device detected binding additional touch events');
  				if( module.is.searchSelection() ) {
  					// do nothing special yet
  				}
  				else if( module.is.single() ) {
  					element
  						.on('touchstart' + eventNamespace, module.event.test.toggle)
  					;
  				}
  				$menu
  					.on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
  				;
  			},
  			keyboardEvents: function() {
  				module.verbose('Binding keyboard events');
  				element
  					.on('keydown' + eventNamespace, module.event.keydown)
  				;
  				if( module.has.search() ) {
  					element
  						.on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
  					;
  				}
  				if( module.is.multiple() ) {
  					$document
  						.on('keydown' + elementNamespace, module.event.document.keydown)
  					;
  				}
  			},
  			inputEvents: function() {
  				module.verbose('Binding input change events');
  				element
  					.on('change' + eventNamespace, selector.input, module.event.change)
  				;
  			},
  			mouseEvents: function() {
  				module.verbose('Binding mouse events');
  				if(module.is.multiple()) {
  					element
  						.on('click'   + eventNamespace, selector.label,  module.event.label.click)
  						.on('click'   + eventNamespace, selector.remove, module.event.remove.click)
  					;
  				}
  				if( module.is.searchSelection() ) {
  					element
  						.on('mousedown' + eventNamespace, module.event.mousedown)
  						.on('mouseup'   + eventNamespace, module.event.mouseup)
  						.on('mousedown' + eventNamespace, selector.menu,   module.event.menu.mousedown)
  						.on('mouseup'   + eventNamespace, selector.menu,   module.event.menu.mouseup)
  						.on('click'     + eventNamespace, selector.icon,   module.event.icon.click)
  						.on('focus'     + eventNamespace, selector.search, module.event.search.focus)
  						.on('click'     + eventNamespace, selector.search, module.event.search.focus)
  						.on('blur'      + eventNamespace, selector.search, module.event.search.blur)
  						.on('click'     + eventNamespace, selector.text,   module.event.text.focus)
  					;
  					if(module.is.multiple()) {
  						element
  							.on('click' + eventNamespace, module.event.click)
  						;
  					}
  				}
  				else {
  					if(settings.on == 'click') {
  						element
  							.on('click' + eventNamespace, selector.icon, module.event.icon.click)
  							.on('click' + eventNamespace, module.event.test.toggle)
  						;
  					}
  					else if(settings.on == 'hover') {
  						element
  							.on('mouseenter' + eventNamespace, module.delay.show)
  							.on('mouseleave' + eventNamespace, module.delay.hide)
  						;
  					}
  					else {
  						element
  							.on(settings.on + eventNamespace, module.toggle)
  						;
  					}
  					element
  						.on('mousedown' + eventNamespace, module.event.mousedown)
  						.on('mouseup'   + eventNamespace, module.event.mouseup)
  						.on('focus'     + eventNamespace, module.event.focus)
  					;
  					if(module.has.menuSearch() ) {
  						element
  							.on('blur' + eventNamespace, selector.search, module.event.search.blur)
  						;
  					}
  					else {
  						element
  							.on('blur' + eventNamespace, module.event.blur)
  						;
  					}
  				}
  				$menu
  					.on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter)
  					.on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
  					.on('click'      + eventNamespace, selector.item, module.event.item.click)
  				;
  			},
  			intent: function() {
  				module.verbose('Binding hide intent event to document');
  				if(hasTouch) {
  					$document
  						.on('touchstart' + elementNamespace, module.event.test.touch)
  						.on('touchmove'  + elementNamespace, module.event.test.touch)
  					;
  				}
  				$document
  					.on('click' + elementNamespace, module.event.test.hide)
  				;
  			}
  		},
  
  		unbind: {
  			intent: function() {
  				module.verbose('Removing hide intent event from document');
  				if(hasTouch) {
  					$document
  						.off('touchstart' + elementNamespace)
  						.off('touchmove' + elementNamespace)
  					;
  				}
  				$document
  					.off('click' + elementNamespace)
  				;
  			}
  		},
  
  		filter: function(query) {
  			var
  				searchTerm = (query !== undefined)
  					? query
  					: module.get.query(),
  				afterFiltered = function() {
  					if(module.is.multiple()) {
  						module.filterActive();
  					}
  					if(query || (!query && module.get.activeItem().length == 0)) {
  						module.select.firstUnfiltered();
  					}
  					if( module.has.allResultsFiltered() ) {
  						if( settings.onNoResults.call(element, searchTerm) ) {
  							if(settings.allowAdditions) {
  								if(settings.hideAdditions) {
  									module.verbose('User addition with no menu, setting empty style');
  									module.set.empty();
  									module.hideMenu();
  								}
  							}
  							else {
  								module.verbose('All items filtered, showing message', searchTerm);
  								module.add.message(message.noResults);
  							}
  						}
  						else {
  							module.verbose('All items filtered, hiding dropdown', searchTerm);
  							module.hideMenu();
  						}
  					}
  					else {
  						module.remove.empty();
  						module.remove.message();
  					}
  					if(settings.allowAdditions) {
  						module.add.userSuggestion(query);
  					}
  					if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
  						module.show();
  					}
  				}
  			;
  			if(settings.useLabels && module.has.maxSelections()) {
  				return;
  			}
  			if(settings.apiSettings) {
  				if( module.can.useAPI() ) {
  					module.queryRemote(searchTerm, function() {
  						if(settings.filterRemoteData) {
  							module.filterItems(searchTerm);
  						}
  						afterFiltered();
  					});
  				}
  				else {
  					module.error(error.noAPI);
  				}
  			}
  			else {
  				module.filterItems(searchTerm);
  				afterFiltered();
  			}
  		},
  
  		queryRemote: function(query, callback) {
  			var
  				apiSettings = {
  					errorDuration : false,
  					cache         : 'local',
  					throttle      : settings.throttle,
  					urlData       : {
  						query: query
  					},
  					onError: function() {
  						module.add.message(message.serverError);
  						callback();
  					},
  					onFailure: function() {
  						module.add.message(message.serverError);
  						callback();
  					},
  					onSuccess : function(response) {
  						module.remove.message();
  						module.setup.menu({
  							values: response[fields.remoteValues]
  						});
  						callback();
  					}
  				}
  			;
  			if( !element.api('get request') ) {
  				module.setup.api();
  			}
  			apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
  			element
  				.api('setting', apiSettings)
  				.api('query')
  			;
  		},
  
  		filterItems: function(query) {
  			var
  				searchTerm = (query !== undefined)
  					? query
  					: module.get.query(),
  				results          =  null,
  				escapedTerm      = module.escape.string(searchTerm),
  				beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm')
  			;
  			// avoid loop if we're matching nothing
  			if( module.has.query() ) {
  				results = [];
  
  				module.verbose('Searching for matching values', searchTerm);
  				$item
  					.each(function(){
  						var
  							$choice = $(this),
  							text,
  							value
  						;
  						if(settings.match == 'both' || settings.match == 'text') {
  							text = String(module.get.choiceText($choice, false));
  							if(text.search(beginsWithRegExp) !== -1) {
  								results.push(this);
  								return true;
  							}
  							else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
  								results.push(this);
  								return true;
  							}
  							else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
  								results.push(this);
  								return true;
  							}
  						}
  						if(settings.match == 'both' || settings.match == 'value') {
  							value = String(module.get.choiceValue($choice, text));
  							if(value.search(beginsWithRegExp) !== -1) {
  								results.push(this);
  								return true;
  							}
  							else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
  								results.push(this);
  								return true;
  							}
  							else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
  								results.push(this);
  								return true;
  							}
  						}
  					})
  				;
  			}
  			module.debug('Showing only matched items', searchTerm);
  			module.remove.filteredItem();
  			if(results) {
  				$item
  					.not(results)
  					.classList.add(className.filtered)
  				;
  			}
  		},
  
  		fuzzySearch: function(query, term) {
  			var
  				termLength  = term.length,
  				queryLength = query.length
  			;
  			query = query.toLowerCase();
  			term  = term.toLowerCase();
  			if(queryLength > termLength) {
  				return false;
  			}
  			if(queryLength === termLength) {
  				return (query === term);
  			}
  			search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
  				var
  					queryCharacter = query.charCodeAt(characterIndex)
  				;
  				while(nextCharacterIndex < termLength) {
  					if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
  						continue search;
  					}
  				}
  				return false;
  			}
  			return true;
  		},
  		exactSearch: function (query, term) {
  			query = query.toLowerCase();
  			term  = term.toLowerCase();
  			if(term.indexOf(query) > -1) {
  				 return true;
  			}
  			return false;
  		},
  		filterActive: function() {
  			if(settings.useLabels) {
  				$item.filter('.' + className.active)
  					.classList.add(className.filtered)
  				;
  			}
  		},
  
  		focusSearch: function(skipHandler) {
  			if( module.has.search() && !module.is.focusedOnSearch() ) {
  				if(skipHandler) {
  					element.off('focus' + eventNamespace, selector.search);
  					$search.focus();
  					element.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
  				}
  				else {
  					$search.focus();
  				}
  			}
  		},
  
  		forceSelection: function() {
  			var
  				$currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
  				$activeItem        = $item.not(className.filtered).filter('.' + className.active).eq(0),
  				$selectedItem      = ($currentlySelected.length > 0)
  					? $currentlySelected
  					: $activeItem,
  				hasSelected = ($selectedItem.length > 0)
  			;
  			if(hasSelected && !module.is.multiple()) {
  				module.debug('Forcing partial selection to selected item', $selectedItem);
  				module.event.item.click.call($selectedItem, {}, true);
  				return;
  			}
  			else {
  				if(settings.allowAdditions) {
  					module.set.selected(module.get.query());
  					module.remove.searchTerm();
  				}
  				else {
  					module.remove.searchTerm();
  				}
  			}
  		},
  
  		change: {
  			values: function(values) {
  				if(!settings.allowAdditions) {
  					module.clear();
  				}
  				module.debug('Creating dropdown with specified values', values);
  				module.setup.menu({values: values});
  				$.each(values, function(index, item) {
  					if(item.selected == true) {
  						module.debug('Setting initial selection to', item.value);
  						module.set.selected(item.value);
  						return true;
  					}
  				});
  			}
  		},
  
  		event: {
  			change: function() {
  				if(!internalChange) {
  					module.debug('Input changed, updating selection');
  					module.set.selected();
  				}
  			},
  			focus: function() {
  				if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
  					module.show();
  				}
  			},
  			blur: function(event) {
  				pageLostFocus = (document.activeElement === this);
  				if(!activated && !pageLostFocus) {
  					module.remove.activeLabel();
  					module.hide();
  				}
  			},
  			mousedown: function() {
  				if(module.is.searchSelection()) {
  					// prevent menu hiding on immediate re-focus
  					willRefocus = true;
  				}
  				else {
  					// prevents focus callback from occurring on mousedown
  					activated = true;
  				}
  			},
  			mouseup: function() {
  				if(module.is.searchSelection()) {
  					// prevent menu hiding on immediate re-focus
  					willRefocus = false;
  				}
  				else {
  					activated = false;
  				}
  			},
  			click: function(event) {
  				var
  					$target = $(event.target)
  				;
  				// focus search
  				if($target.is(element)) {
  					if(!module.is.focusedOnSearch()) {
  						module.focusSearch();
  					}
  					else {
  						module.show();
  					}
  				}
  			},
  			search: {
  				focus: function() {
  					activated = true;
  					if(module.is.multiple()) {
  						module.remove.activeLabel();
  					}
  					if(settings.showOnFocus) {
  						module.search();
  					}
  				},
  				blur: function(event) {
  					pageLostFocus = (document.activeElement === this);
  					if(module.is.searchSelection() && !willRefocus) {
  						if(!itemActivated && !pageLostFocus) {
  							if(settings.forceSelection) {
  								module.forceSelection();
  							}
  							module.hide();
  						}
  					}
  					willRefocus = false;
  				}
  			},
  			icon: {
  				click: function(event) {
  					module.toggle();
  				}
  			},
  			text: {
  				focus: function(event) {
  					activated = true;
  					module.focusSearch();
  				}
  			},
  			input: function(event) {
  				if(module.is.multiple() || module.is.searchSelection()) {
  					module.set.filtered();
  				}
  				clearTimeout(module.timer);
  				module.timer = setTimeout(module.search, settings.delay.search);
  			},
  			label: {
  				click: function(event) {
  					var
  						$label        = $(this),
  						$labels       = element.find(selector.label),
  						$activeLabels = $labels.filter('.' + className.active),
  						$nextActive   = $label.nextAll('.' + className.active),
  						$prevActive   = $label.prevAll('.' + className.active),
  						$range = ($nextActive.length > 0)
  							? $label.nextUntil($nextActive).add($activeLabels).add($label)
  							: $label.prevUntil($prevActive).add($activeLabels).add($label)
  					;
  					if(event.shiftKey) {
  						$activeLabels.classList.remove(className.active);
  						$range.classList.add(className.active);
  					}
  					else if(event.ctrlKey) {
  						$label.toggleClass(className.active);
  					}
  					else {
  						$activeLabels.classList.remove(className.active);
  						$label.classList.add(className.active);
  					}
  					settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
  				}
  			},
  			remove: {
  				click: function() {
  					var
  						$label = $(this).parent()
  					;
  					if( $label.classList.contains(className.active) ) {
  						// remove all selected labels
  						module.remove.activeLabels();
  					}
  					else {
  						// remove this label only
  						module.remove.activeLabels( $label );
  					}
  				}
  			},
  			test: {
  				toggle: function(event) {
  					var
  						toggleBehavior = (module.is.multiple())
  							? module.show
  							: module.toggle
  					;
  					if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
  						return;
  					}
  					if( module.determine.eventOnElement(event, toggleBehavior) ) {
  						event.preventDefault();
  					}
  				},
  				touch: function(event) {
  					module.determine.eventOnElement(event, function() {
  						if(event.type == 'touchstart') {
  							module.timer = setTimeout(function() {
  								module.hide();
  							}, settings.delay.touch);
  						}
  						else if(event.type == 'touchmove') {
  							clearTimeout(module.timer);
  						}
  					});
  					event.stopPropagation();
  				},
  				hide: function(event) {
  					module.determine.eventInModule(event, module.hide);
  				}
  			},
  			select: {
  				mutation: function(mutations) {
  					module.debug('<select> modified, recreating menu');
  					var
  						isSelectMutation = false
  					;
  					$.each(mutations, function(index, mutation) {
  						if($(mutation.target).is('select') || $(mutation.addedNodes).is('select')) {
  							isSelectMutation = true;
  							return true;
  						}
  					});
  					if(isSelectMutation) {
  						module.disconnect.selectObserver();
  						module.refresh();
  						module.setup.select();
  						module.set.selected();
  						module.observe.select();
  					}
  				}
  			},
  			menu: {
  				mutation: function(mutations) {
  					var
  						mutation   = mutations[0],
  						$addedNode = mutation.addedNodes
  							? $(mutation.addedNodes[0])
  							: $(false),
  						$removedNode = mutation.removedNodes
  							? $(mutation.removedNodes[0])
  							: $(false),
  						$changedNodes  = $addedNode.add($removedNode),
  						isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
  						isMessage      = $changedNodes.is(selector.message)  || $changedNodes.closest(selector.message).length > 0
  					;
  					if(isUserAddition || isMessage) {
  						module.debug('Updating item selector cache');
  						module.refreshItems();
  					}
  					else {
  						module.debug('Menu modified, updating selector cache');
  						module.refresh();
  					}
  				},
  				mousedown: function() {
  					itemActivated = true;
  				},
  				mouseup: function() {
  					itemActivated = false;
  				}
  			},
  			item: {
  				mouseenter: function(event) {
  					var
  						$target        = $(event.target),
  						$item          = $(this),
  						$subMenu       = $item.children(selector.menu),
  						$otherMenus    = $item.siblings(selector.item).children(selector.menu),
  						hasSubMenu     = ($subMenu.length > 0),
  						isBubbledEvent = ($subMenu.find($target).length > 0)
  					;
  					if( !isBubbledEvent && hasSubMenu ) {
  						clearTimeout(module.itemTimer);
  						module.itemTimer = setTimeout(function() {
  							module.verbose('Showing sub-menu', $subMenu);
  							$.each($otherMenus, function() {
  								module.animate.hide(false, $(this));
  							});
  							module.animate.show(false, $subMenu);
  						}, settings.delay.show);
  						event.preventDefault();
  					}
  				},
  				mouseleave: function(event) {
  					var
  						$subMenu = $(this).children(selector.menu)
  					;
  					if($subMenu.length > 0) {
  						clearTimeout(module.itemTimer);
  						module.itemTimer = setTimeout(function() {
  							module.verbose('Hiding sub-menu', $subMenu);
  							module.animate.hide(false, $subMenu);
  						}, settings.delay.hide);
  					}
  				},
  				click: function (event, skipRefocus) {
  					var
  						$choice        = $(this),
  						$target        = (event)
  							? $(event.target)
  							: $(''),
  						$subMenu       = $choice.find(selector.menu),
  						text           = module.get.choiceText($choice),
  						value          = module.get.choiceValue($choice, text),
  						hasSubMenu     = ($subMenu.length > 0),
  						isBubbledEvent = ($subMenu.find($target).length > 0)
  					;
  					// prevents IE11 bug where menu receives focus even though `tabindex=-1`
  					if(module.has.menuSearch()) {
  						$(document.activeElement).blur();
  					}
  					if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
  						if(module.is.searchSelection()) {
  							if(settings.allowAdditions) {
  								module.remove.userAddition();
  							}
  							module.remove.searchTerm();
  							if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
  								module.focusSearch(true);
  							}
  						}
  						if(!settings.useLabels) {
  							module.remove.filteredItem();
  							module.set.scrollPosition($choice);
  						}
  						module.determine.selectAction.call(this, text, value);
  					}
  				}
  			},
  
  			document: {
  				// label selection should occur even when element has no focus
  				keydown: function(event) {
  					var
  						pressedKey    = event.which,
  						isShortcutKey = module.is.inObject(pressedKey, keys)
  					;
  					if(isShortcutKey) {
  						var
  							$label            = element.find(selector.label),
  							$activeLabel      = $label.filter('.' + className.active),
  							activeValue       = $activeLabel.data(metadata.value),
  							labelIndex        = $label.index($activeLabel),
  							labelCount        = $label.length,
  							hasActiveLabel    = ($activeLabel.length > 0),
  							hasMultipleActive = ($activeLabel.length > 1),
  							isFirstLabel      = (labelIndex === 0),
  							isLastLabel       = (labelIndex + 1 == labelCount),
  							isSearch          = module.is.searchSelection(),
  							isFocusedOnSearch = module.is.focusedOnSearch(),
  							isFocused         = module.is.focused(),
  							caretAtStart      = (isFocusedOnSearch && module.get.caretPosition() === 0),
  							$nextLabel
  						;
  						if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
  							return;
  						}
  
  						if(pressedKey == keys.leftArrow) {
  							// activate previous label
  							if((isFocused || caretAtStart) && !hasActiveLabel) {
  								module.verbose('Selecting previous label');
  								$label.last().classList.add(className.active);
  							}
  							else if(hasActiveLabel) {
  								if(!event.shiftKey) {
  									module.verbose('Selecting previous label');
  									$label.classList.remove(className.active);
  								}
  								else {
  									module.verbose('Adding previous label to selection');
  								}
  								if(isFirstLabel && !hasMultipleActive) {
  									$activeLabel.classList.add(className.active);
  								}
  								else {
  									$activeLabel.prev(selector.siblingLabel)
  										.classList.add(className.active)
  										.end()
  									;
  								}
  								event.preventDefault();
  							}
  						}
  						else if(pressedKey == keys.rightArrow) {
  							// activate first label
  							if(isFocused && !hasActiveLabel) {
  								$label.first().classList.add(className.active);
  							}
  							// activate next label
  							if(hasActiveLabel) {
  								if(!event.shiftKey) {
  									module.verbose('Selecting next label');
  									$label.classList.remove(className.active);
  								}
  								else {
  									module.verbose('Adding next label to selection');
  								}
  								if(isLastLabel) {
  									if(isSearch) {
  										if(!isFocusedOnSearch) {
  											module.focusSearch();
  										}
  										else {
  											$label.classList.remove(className.active);
  										}
  									}
  									else if(hasMultipleActive) {
  										$activeLabel.next(selector.siblingLabel).classList.add(className.active);
  									}
  									else {
  										$activeLabel.classList.add(className.active);
  									}
  								}
  								else {
  									$activeLabel.next(selector.siblingLabel).classList.add(className.active);
  								}
  								event.preventDefault();
  							}
  						}
  						else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
  							if(hasActiveLabel) {
  								module.verbose('Removing active labels');
  								if(isLastLabel) {
  									if(isSearch && !isFocusedOnSearch) {
  										module.focusSearch();
  									}
  								}
  								$activeLabel.last().next(selector.siblingLabel).classList.add(className.active);
  								module.remove.activeLabels($activeLabel);
  								event.preventDefault();
  							}
  							else if(caretAtStart && !hasActiveLabel && pressedKey == keys.backspace) {
  								module.verbose('Removing last label on input backspace');
  								$activeLabel = $label.last().classList.add(className.active);
  								module.remove.activeLabels($activeLabel);
  							}
  						}
  						else {
  							$activeLabel.classList.remove(className.active);
  						}
  					}
  				}
  			},
  
  			keydown: function(event) {
  				var
  					pressedKey    = event.which,
  					isShortcutKey = module.is.inObject(pressedKey, keys)
  				;
  				if(isShortcutKey) {
  					var
  						$currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
  						$activeItem        = $menu.children('.' + className.active).eq(0),
  						$selectedItem      = ($currentlySelected.length > 0)
  							? $currentlySelected
  							: $activeItem,
  						$visibleItems = ($selectedItem.length > 0)
  							? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
  							: $menu.children(':not(.' + className.filtered +')'),
  						$subMenu              = $selectedItem.children(selector.menu),
  						$parentMenu           = $selectedItem.closest(selector.menu),
  						inVisibleMenu         = ($parentMenu.classList.contains(className.visible) || $parentMenu.classList.contains(className.animating) || $parentMenu.parent(selector.menu).length > 0),
  						hasSubMenu            = ($subMenu.length> 0),
  						hasSelectedItem       = ($selectedItem.length > 0),
  						selectedIsSelectable  = ($selectedItem.not(selector.unselectable).length > 0),
  						delimiterPressed      = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
  						isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
  						$nextItem,
  						isSubMenuItem,
  						newIndex
  					;
  					// allow selection with menu closed
  					if(isAdditionWithoutMenu) {
  						module.verbose('Selecting item from keyboard shortcut', $selectedItem);
  						module.event.item.click.call($selectedItem, event);
  						if(module.is.searchSelection()) {
  							module.remove.searchTerm();
  						}
  					}
  
  					// visible menu keyboard shortcuts
  					if( module.is.visible() ) {
  
  						// enter (select or open sub-menu)
  						if(pressedKey == keys.enter || delimiterPressed) {
  							if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
  								module.verbose('Pressed enter on unselectable category, opening sub menu');
  								pressedKey = keys.rightArrow;
  							}
  							else if(selectedIsSelectable) {
  								module.verbose('Selecting item from keyboard shortcut', $selectedItem);
  								module.event.item.click.call($selectedItem, event);
  								if(module.is.searchSelection()) {
  									module.remove.searchTerm();
  								}
  							}
  							event.preventDefault();
  						}
  
  						// sub-menu actions
  						if(hasSelectedItem) {
  
  							if(pressedKey == keys.leftArrow) {
  
  								isSubMenuItem = ($parentMenu[0] !== $menu[0]);
  
  								if(isSubMenuItem) {
  									module.verbose('Left key pressed, closing sub-menu');
  									module.animate.hide(false, $parentMenu);
  									$selectedItem
  										.classList.remove(className.selected)
  									;
  									$parentMenu
  										.closest(selector.item)
  											.classList.add(className.selected)
  									;
  									event.preventDefault();
  								}
  							}
  
  							// right arrow (show sub-menu)
  							if(pressedKey == keys.rightArrow) {
  								if(hasSubMenu) {
  									module.verbose('Right key pressed, opening sub-menu');
  									module.animate.show(false, $subMenu);
  									$selectedItem
  										.classList.remove(className.selected)
  									;
  									$subMenu
  										.find(selector.item).eq(0)
  											.classList.add(className.selected)
  									;
  									event.preventDefault();
  								}
  							}
  						}
  
  						// up arrow (traverse menu up)
  						if(pressedKey == keys.upArrow) {
  							$nextItem = (hasSelectedItem && inVisibleMenu)
  								? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
  								: $item.eq(0)
  							;
  							if($visibleItems.index( $nextItem ) < 0) {
  								module.verbose('Up key pressed but reached top of current menu');
  								event.preventDefault();
  								return;
  							}
  							else {
  								module.verbose('Up key pressed, changing active item');
  								$selectedItem
  									.classList.remove(className.selected)
  								;
  								$nextItem
  									.classList.add(className.selected)
  								;
  								module.set.scrollPosition($nextItem);
  								if(settings.selectOnKeydown && module.is.single()) {
  									module.set.selectedItem($nextItem);
  								}
  							}
  							event.preventDefault();
  						}
  
  						// down arrow (traverse menu down)
  						if(pressedKey == keys.downArrow) {
  							$nextItem = (hasSelectedItem && inVisibleMenu)
  								? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
  								: $item.eq(0)
  							;
  							if($nextItem.length === 0) {
  								module.verbose('Down key pressed but reached bottom of current menu');
  								event.preventDefault();
  								return;
  							}
  							else {
  								module.verbose('Down key pressed, changing active item');
  								$item
  									.classList.remove(className.selected)
  								;
  								$nextItem
  									.classList.add(className.selected)
  								;
  								module.set.scrollPosition($nextItem);
  								if(settings.selectOnKeydown && module.is.single()) {
  									module.set.selectedItem($nextItem);
  								}
  							}
  							event.preventDefault();
  						}
  
  						// page down (show next page)
  						if(pressedKey == keys.pageUp) {
  							module.scrollPage('up');
  							event.preventDefault();
  						}
  						if(pressedKey == keys.pageDown) {
  							module.scrollPage('down');
  							event.preventDefault();
  						}
  
  						// escape (close menu)
  						if(pressedKey == keys.escape) {
  							module.verbose('Escape key pressed, closing dropdown');
  							module.hide();
  						}
  
  					}
  					else {
  						// delimiter key
  						if(delimiterPressed) {
  							event.preventDefault();
  						}
  						// down arrow (open menu)
  						if(pressedKey == keys.downArrow && !module.is.visible()) {
  							module.verbose('Down key pressed, showing dropdown');
  							module.show();
  							event.preventDefault();
  						}
  					}
  				}
  				else {
  					if( !module.has.search() ) {
  						module.set.selectedLetter( String.fromCharCode(pressedKey) );
  					}
  				}
  			}
  		},
  
  		trigger: {
  			change: function() {
  				var
  					events       = document.createEvent('HTMLEvents'),
  					inputElement = $input[0]
  				;
  				if(inputElement) {
  					module.verbose('Triggering native change event');
  					events.initEvent('change', true, false);
  					inputElement.dispatchEvent(events);
  				}
  			}
  		},
  
  		determine: {
  			selectAction: function(text, value) {
  				module.verbose('Determining action', settings.action);
  				if( $.isFunction( module.action[settings.action] ) ) {
  					module.verbose('Triggering preset action', settings.action, text, value);
  					module.action[ settings.action ].call(element, text, value, this);
  				}
  				else if( $.isFunction(settings.action) ) {
  					module.verbose('Triggering user action', settings.action, text, value);
  					settings.action.call(element, text, value, this);
  				}
  				else {
  					module.error(error.action, settings.action);
  				}
  			},
  			eventInModule: function(event, callback) {
  				var
  					$target    = $(event.target),
  					inDocument = ($target.closest(document.documentElement).length > 0),
  					inModule   = ($target.closest(element).length > 0)
  				;
  				callback = $.isFunction(callback)
  					? callback
  					: function(){}
  				;
  				if(inDocument && !inModule) {
  					module.verbose('Triggering event', callback);
  					callback();
  					return true;
  				}
  				else {
  					module.verbose('Event occurred in dropdown, canceling callback');
  					return false;
  				}
  			},
  			eventOnElement: function(event, callback) {
  				var
  					$target      = $(event.target),
  					$label       = $target.closest(selector.siblingLabel),
  					inVisibleDOM = document.body.contains(event.target),
  					notOnLabel   = (element.find($label).length === 0),
  					notInMenu    = ($target.closest($menu).length === 0)
  				;
  				callback = $.isFunction(callback)
  					? callback
  					: function(){}
  				;
  				if(inVisibleDOM && notOnLabel && notInMenu) {
  					module.verbose('Triggering event', callback);
  					callback();
  					return true;
  				}
  				else {
  					module.verbose('Event occurred in dropdown menu, canceling callback');
  					return false;
  				}
  			}
  		},
  
  		action: {
  
  			nothing: function() {},
  
  			activate: function(text, value, element) {
  				value = (value !== undefined)
  					? value
  					: text
  				;
  				if( module.can.activate( $(element) ) ) {
  					module.set.selected(value, $(element));
  					if(module.is.multiple() && !module.is.allFiltered()) {
  						return;
  					}
  					else {
  						module.hideAndClear();
  					}
  				}
  			},
  
  			select: function(text, value, element) {
  				value = (value !== undefined)
  					? value
  					: text
  				;
  				if( module.can.activate( $(element) ) ) {
  					module.set.value(value, text, $(element));
  					if(module.is.multiple() && !module.is.allFiltered()) {
  						return;
  					}
  					else {
  						module.hideAndClear();
  					}
  				}
  			},
  
  			combo: function(text, value, element) {
  				value = (value !== undefined)
  					? value
  					: text
  				;
  				module.set.selected(value, $(element));
  				module.hideAndClear();
  			},
  
  			hide: function(text, value, element) {
  				module.set.value(value, text);
  				module.hideAndClear();
  			}
  
  		},
  
  		get: {
  			id: function() {
  				return id;
  			},
  			defaultText: function() {
  				return element.data(metadata.defaultText);
  			},
  			defaultValue: function() {
  				return element.data(metadata.defaultValue);
  			},
  			placeholderText: function() {
  				if(settings.placeholder != 'auto' && typeof settings.placeholder == 'string') {
  					return settings.placeholder;
  				}
  				return element.data(metadata.placeholderText) || '';
  			},
  			text: function() {
  				return $text.text();
  			},
  			query: function() {
  				return $.trim($search.val());
  			},
  			searchWidth: function(value) {
  				value = (value !== undefined)
  					? value
  					: $search.val()
  				;
  				$sizer.text(value);
  				// prevent rounding issues
  				return Math.ceil( $sizer.width() + 1);
  			},
  			selectionCount: function() {
  				var
  					values = module.get.values(),
  					count
  				;
  				count = ( module.is.multiple() )
  					? $.isArray(values)
  						? values.length
  						: 0
  					: (module.get.value() !== '')
  						? 1
  						: 0
  				;
  				return count;
  			},
  			transition: function($subMenu) {
  				return (settings.transition == 'auto')
  					? module.is.upward($subMenu)
  						? 'slide up'
  						: 'slide down'
  					: settings.transition
  				;
  			},
  			userValues: function() {
  				var
  					values = module.get.values()
  				;
  				if(!values) {
  					return false;
  				}
  				values = $.isArray(values)
  					? values
  					: [values]
  				;
  				return $.grep(values, function(value) {
  					return (module.get.item(value) === false);
  				});
  			},
  			uniqueArray: function(array) {
  				return $.grep(array, function (value, index) {
  						return $.inArray(value, array) === index;
  				});
  			},
  			caretPosition: function() {
  				var
  					input = $search.get(0),
  					range,
  					rangeLength
  				;
  				if('selectionStart' in input) {
  					return input.selectionStart;
  				}
  				else if (document.selection) {
  					input.focus();
  					range       = document.selection.createRange();
  					rangeLength = range.text.length;
  					range.moveStart('character', -input.value.length);
  					return range.text.length - rangeLength;
  				}
  			},
  			value: function() {
  				var
  					value = ($input.length > 0)
  						? $input.val()
  						: element.data(metadata.value),
  					isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '')
  				;
  				// prevents placeholder element from being selected when multiple
  				return (value === undefined || isEmptyMultiselect)
  					? ''
  					: value
  				;
  			},
  			values: function() {
  				var
  					value = module.get.value()
  				;
  				if(value === '') {
  					return '';
  				}
  				return ( !module.has.selectInput() && module.is.multiple() )
  					? (typeof value == 'string') // delimited string
  						? value.split(settings.delimiter)
  						: ''
  					: value
  				;
  			},
  			remoteValues: function() {
  				var
  					values = module.get.values(),
  					remoteValues = false
  				;
  				if(values) {
  					if(typeof values == 'string') {
  						values = [values];
  					}
  					$.each(values, function(index, value) {
  						var
  							name = module.read.remoteData(value)
  						;
  						module.verbose('Restoring value from session data', name, value);
  						if(name) {
  							if(!remoteValues) {
  								remoteValues = {};
  							}
  							remoteValues[value] = name;
  						}
  					});
  				}
  				return remoteValues;
  			},
  			choiceText: function($choice, preserveHTML) {
  				preserveHTML = (preserveHTML !== undefined)
  					? preserveHTML
  					: settings.preserveHTML
  				;
  				if($choice) {
  					if($choice.find(selector.menu).length > 0) {
  						module.verbose('Retrieving text of element with sub-menu');
  						$choice = $choice.clone();
  						$choice.find(selector.menu).remove();
  						$choice.find(selector.menuIcon).remove();
  					}
  					return ($choice.data(metadata.text) !== undefined)
  						? $choice.data(metadata.text)
  						: (preserveHTML)
  							? $.trim($choice.html())
  							: $.trim($choice.text())
  					;
  				}
  			},
  			choiceValue: function($choice, choiceText) {
  				choiceText = choiceText || module.get.choiceText($choice);
  				if(!$choice) {
  					return false;
  				}
  				return ($choice.data(metadata.value) !== undefined)
  					? String( $choice.data(metadata.value) )
  					: (typeof choiceText === 'string')
  						? $.trim(choiceText.toLowerCase())
  						: String(choiceText)
  				;
  			},
  			inputEvent: function() {
  				var
  					input = $search[0]
  				;
  				if(input) {
  					return (input.oninput !== undefined)
  						? 'input'
  						: (input.onpropertychange !== undefined)
  							? 'propertychange'
  							: 'keyup'
  					;
  				}
  				return false;
  			},
  			selectValues: function() {
  				var
  					select = {}
  				;
  				select.values = [];
  				element
  					.find('option')
  						.each(function() {
  							var
  								$option  = $(this),
  								name     = $option.html(),
  								disabled = $option.attr('disabled'),
  								value    = ( $option.attr('value') !== undefined )
  									? $option.attr('value')
  									: name
  							;
  							if(settings.placeholder === 'auto' && value === '') {
  								select.placeholder = name;
  							}
  							else {
  								select.values.push({
  									name     : name,
  									value    : value,
  									disabled : disabled
  								});
  							}
  						})
  				;
  				if(settings.placeholder && settings.placeholder !== 'auto') {
  					module.debug('Setting placeholder value to', settings.placeholder);
  					select.placeholder = settings.placeholder;
  				}
  				if(settings.sortSelect) {
  					select.values.sort(function(a, b) {
  						return (a.name > b.name)
  							? 1
  							: -1
  						;
  					});
  					module.debug('Retrieved and sorted values from select', select);
  				}
  				else {
  					module.debug('Retrieved values from select', select);
  				}
  				return select;
  			},
  			activeItem: function() {
  				return $item.filter('.'  + className.active);
  			},
  			selectedItem: function() {
  				var
  					$selectedItem = $item.not(selector.unselectable).filter('.'  + className.selected)
  				;
  				return ($selectedItem.length > 0)
  					? $selectedItem
  					: $item.eq(0)
  				;
  			},
  			itemWithAdditions: function(value) {
  				var
  					$items       = module.get.item(value),
  					$userItems   = module.create.userChoice(value),
  					hasUserItems = ($userItems && $userItems.length > 0)
  				;
  				if(hasUserItems) {
  					$items = ($items.length > 0)
  						? $items.add($userItems)
  						: $userItems
  					;
  				}
  				return $items;
  			},
  			item: function(value, strict) {
  				var
  					$selectedItem = false,
  					shouldSearch,
  					isMultiple
  				;
  				value = (value !== undefined)
  					? value
  					: ( module.get.values() !== undefined)
  						? module.get.values()
  						: module.get.text()
  				;
  				shouldSearch = (isMultiple)
  					? (value.length > 0)
  					: (value !== undefined && value !== null)
  				;
  				isMultiple = (module.is.multiple() && $.isArray(value));
  				strict     = (value === '' || value === 0)
  					? true
  					: strict || false
  				;
  				if(shouldSearch) {
  					$item
  						.each(function() {
  							var
  								$choice       = $(this),
  								optionText    = module.get.choiceText($choice),
  								optionValue   = module.get.choiceValue($choice, optionText)
  							;
  							// safe early exit
  							if(optionValue === null || optionValue === undefined) {
  								return;
  							}
  							if(isMultiple) {
  								if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) {
  									$selectedItem = ($selectedItem)
  										? $selectedItem.add($choice)
  										: $choice
  									;
  								}
  							}
  							else if(strict) {
  								module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
  								if( optionValue === value || optionText === value) {
  									$selectedItem = $choice;
  									return true;
  								}
  							}
  							else {
  								if( String(optionValue) == String(value) || optionText == value) {
  									module.verbose('Found select item by value', optionValue, value);
  									$selectedItem = $choice;
  									return true;
  								}
  							}
  						})
  					;
  				}
  				return $selectedItem;
  			}
  		},
  
  		check: {
  			maxSelections: function(selectionCount) {
  				if(settings.maxSelections) {
  					selectionCount = (selectionCount !== undefined)
  						? selectionCount
  						: module.get.selectionCount()
  					;
  					if(selectionCount >= settings.maxSelections) {
  						module.debug('Maximum selection count reached');
  						if(settings.useLabels) {
  							$item.classList.add(className.filtered);
  							module.add.message(message.maxSelections);
  						}
  						return true;
  					}
  					else {
  						module.verbose('No longer at maximum selection count');
  						module.remove.message();
  						module.remove.filteredItem();
  						if(module.is.searchSelection()) {
  							module.filterItems();
  						}
  						return false;
  					}
  				}
  				return true;
  			}
  		},
  
  		restore: {
  			defaults: function() {
  				module.clear();
  				module.restore.defaultText();
  				module.restore.defaultValue();
  			},
  			defaultText: function() {
  				var
  					defaultText     = module.get.defaultText(),
  					placeholderText = module.get.placeholderText
  				;
  				if(defaultText === placeholderText) {
  					module.debug('Restoring default placeholder text', defaultText);
  					module.set.placeholderText(defaultText);
  				}
  				else {
  					module.debug('Restoring default text', defaultText);
  					module.set.text(defaultText);
  				}
  			},
  			placeholderText: function() {
  				module.set.placeholderText();
  			},
  			defaultValue: function() {
  				var
  					defaultValue = module.get.defaultValue()
  				;
  				if(defaultValue !== undefined) {
  					module.debug('Restoring default value', defaultValue);
  					if(defaultValue !== '') {
  						module.set.value(defaultValue);
  						module.set.selected();
  					}
  					else {
  						module.remove.activeItem();
  						module.remove.selectedItem();
  					}
  				}
  			},
  			labels: function() {
  				if(settings.allowAdditions) {
  					if(!settings.useLabels) {
  						module.error(error.labels);
  						settings.useLabels = true;
  					}
  					module.debug('Restoring selected values');
  					module.create.userLabels();
  				}
  				module.check.maxSelections();
  			},
  			selected: function() {
  				module.restore.values();
  				if(module.is.multiple()) {
  					module.debug('Restoring previously selected values and labels');
  					module.restore.labels();
  				}
  				else {
  					module.debug('Restoring previously selected values');
  				}
  			},
  			values: function() {
  				// prevents callbacks from occurring on initial load
  				module.set.initialLoad();
  				if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
  					module.restore.remoteValues();
  				}
  				else {
  					module.set.selected();
  				}
  				module.remove.initialLoad();
  			},
  			remoteValues: function() {
  				var
  					values = module.get.remoteValues()
  				;
  				module.debug('Recreating selected from session data', values);
  				if(values) {
  					if( module.is.single() ) {
  						$.each(values, function(value, name) {
  							module.set.text(name);
  						});
  					}
  					else {
  						$.each(values, function(value, name) {
  							module.add.label(value, name);
  						});
  					}
  				}
  			}
  		},
  
  		read: {
  			remoteData: function(value) {
  				var
  					name
  				;
  				if(window.Storage === undefined) {
  					module.error(error.noStorage);
  					return;
  				}
  				name = sessionStorage.getItem(value);
  				return (name !== undefined)
  					? name
  					: false
  				;
  			}
  		},
  
  		save: {
  			defaults: function() {
  				module.save.defaultText();
  				module.save.placeholderText();
  				module.save.defaultValue();
  			},
  			defaultValue: function() {
  				var
  					value = module.get.value()
  				;
  				module.verbose('Saving default value as', value);
  				element.data(metadata.defaultValue, value);
  			},
  			defaultText: function() {
  				var
  					text = module.get.text()
  				;
  				module.verbose('Saving default text as', text);
  				element.data(metadata.defaultText, text);
  			},
  			placeholderText: function() {
  				var
  					text
  				;
  				if(settings.placeholder !== false && $text.classList.contains(className.placeholder)) {
  					text = module.get.text();
  					module.verbose('Saving placeholder text as', text);
  					element.data(metadata.placeholderText, text);
  				}
  			},
  			remoteData: function(name, value) {
  				if(window.Storage === undefined) {
  					module.error(error.noStorage);
  					return;
  				}
  				module.verbose('Saving remote data to session storage', value, name);
  				sessionStorage.setItem(value, name);
  			}
  		},
  
  		clear: function() {
  			if(module.is.multiple() && settings.useLabels) {
  				module.remove.labels();
  			}
  			else {
  				module.remove.activeItem();
  				module.remove.selectedItem();
  			}
  			module.set.placeholderText();
  			module.clearValue();
  		},
  
  		clearValue: function() {
  			module.set.value('');
  		},
  
  		scrollPage: function(direction, $selectedItem) {
  			var
  				$currentItem  = $selectedItem || module.get.selectedItem(),
  				$menu         = $currentItem.closest(selector.menu),
  				menuHeight    = $menu.outerHeight(),
  				currentScroll = $menu.scrollTop(),
  				itemHeight    = $item.eq(0).outerHeight(),
  				itemsPerPage  = Math.floor(menuHeight / itemHeight),
  				maxScroll     = $menu.prop('scrollHeight'),
  				newScroll     = (direction == 'up')
  					? currentScroll - (itemHeight * itemsPerPage)
  					: currentScroll + (itemHeight * itemsPerPage),
  				$selectableItem = $item.not(selector.unselectable),
  				isWithinRange,
  				$nextSelectedItem,
  				elementIndex
  			;
  			elementIndex      = (direction == 'up')
  				? $selectableItem.index($currentItem) - itemsPerPage
  				: $selectableItem.index($currentItem) + itemsPerPage
  			;
  			isWithinRange = (direction == 'up')
  				? (elementIndex >= 0)
  				: (elementIndex < $selectableItem.length)
  			;
  			$nextSelectedItem = (isWithinRange)
  				? $selectableItem.eq(elementIndex)
  				: (direction == 'up')
  					? $selectableItem.first()
  					: $selectableItem.last()
  			;
  			if($nextSelectedItem.length > 0) {
  				module.debug('Scrolling page', direction, $nextSelectedItem);
  				$currentItem
  					.classList.remove(className.selected)
  				;
  				$nextSelectedItem
  					.classList.add(className.selected)
  				;
  				if(settings.selectOnKeydown && module.is.single()) {
  					module.set.selectedItem($nextSelectedItem);
  				}
  				$menu
  					.scrollTop(newScroll)
  				;
  			}
  		},
  
  		set: {
  			filtered: function() {
  				var
  					isMultiple       = module.is.multiple(),
  					isSearch         = module.is.searchSelection(),
  					isSearchMultiple = (isMultiple && isSearch),
  					searchValue      = (isSearch)
  						? module.get.query()
  						: '',
  					hasSearchValue   = (typeof searchValue === 'string' && searchValue.length > 0),
  					searchWidth      = module.get.searchWidth(),
  					valueIsSet       = searchValue !== ''
  				;
  				if(isMultiple && hasSearchValue) {
  					module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
  					$search.css('width', searchWidth);
  				}
  				if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
  					module.verbose('Hiding placeholder text');
  					$text.classList.add(className.filtered);
  				}
  				else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
  					module.verbose('Showing placeholder text');
  					$text.classList.remove(className.filtered);
  				}
  			},
  			empty: function() {
  				element.classList.add(className.empty);
  			},
  			loading: function() {
  				element.classList.add(className.loading);
  			},
  			placeholderText: function(text) {
  				text = text || module.get.placeholderText();
  				module.debug('Setting placeholder text', text);
  				module.set.text(text);
  				$text.classList.add(className.placeholder);
  			},
  			tabbable: function() {
  				if( module.is.searchSelection() ) {
  					module.debug('Added tabindex to searchable dropdown');
  					$search
  						.val('')
  						.attr('tabindex', 0)
  					;
  					$menu
  						.attr('tabindex', -1)
  					;
  				}
  				else {
  					module.debug('Added tabindex to dropdown');
  					if( element.attr('tabindex') === undefined) {
  						element
  							.attr('tabindex', 0)
  						;
  						$menu
  							.attr('tabindex', -1)
  						;
  					}
  				}
  			},
  			initialLoad: function() {
  				module.verbose('Setting initial load');
  				initialLoad = true;
  			},
  			activeItem: function($item) {
  				if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
  					$item.classList.add(className.filtered);
  				}
  				else {
  					$item.classList.add(className.active);
  				}
  			},
  			partialSearch: function(text) {
  				var
  					length = module.get.query().length
  				;
  				$search.val( text.substr(0, length));
  			},
  			scrollPosition: function($item, forceScroll) {
  				var
  					edgeTolerance = 5,
  					$menu,
  					hasActive,
  					offset,
  					itemHeight,
  					itemOffset,
  					menuOffset,
  					menuScroll,
  					menuHeight,
  					abovePage,
  					belowPage
  				;
  
  				$item       = $item || module.get.selectedItem();
  				$menu       = $item.closest(selector.menu);
  				hasActive   = ($item && $item.length > 0);
  				forceScroll = (forceScroll !== undefined)
  					? forceScroll
  					: false
  				;
  				if($item && $menu.length > 0 && hasActive) {
  					itemOffset = $item.position().top;
  
  					$menu.classList.add(className.loading);
  					menuScroll = $menu.scrollTop();
  					menuOffset = $menu.offset().top;
  					itemOffset = $item.offset().top;
  					offset     = menuScroll - menuOffset + itemOffset;
  					if(!forceScroll) {
  						menuHeight = $menu.height();
  						belowPage  = menuScroll + menuHeight < (offset + edgeTolerance);
  						abovePage  = ((offset - edgeTolerance) < menuScroll);
  					}
  					module.debug('Scrolling to active item', offset);
  					if(forceScroll || abovePage || belowPage) {
  						$menu.scrollTop(offset);
  					}
  					$menu.classList.remove(className.loading);
  				}
  			},
  			text: function(text) {
  				if(settings.action !== 'select') {
  					if(settings.action == 'combo') {
  						module.debug('Changing combo button text', text, $combo);
  						if(settings.preserveHTML) {
  							$combo.html(text);
  						}
  						else {
  							$combo.text(text);
  						}
  					}
  					else {
  						if(text !== module.get.placeholderText()) {
  							$text.classList.remove(className.placeholder);
  						}
  						module.debug('Changing text', text, $text);
  						$text
  							.classList.remove(className.filtered)
  						;
  						if(settings.preserveHTML) {
  							$text.html(text);
  						}
  						else {
  							$text.text(text);
  						}
  					}
  				}
  			},
  			selectedItem: function($item) {
  				var
  					value      = module.get.choiceValue($item),
  					searchText = module.get.choiceText($item, false),
  					text       = module.get.choiceText($item, true)
  				;
  				module.debug('Setting user selection to item', $item);
  				module.remove.activeItem();
  				module.set.partialSearch(searchText);
  				module.set.activeItem($item);
  				module.set.selected(value, $item);
  				module.set.text(text);
  			},
  			selectedLetter: function(letter) {
  				var
  					$selectedItem         = $item.filter('.' + className.selected),
  					alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
  					$nextValue            = false,
  					$nextItem
  				;
  				// check next of same letter
  				if(alreadySelectedLetter) {
  					$nextItem = $selectedItem.nextAll($item).eq(0);
  					if( module.has.firstLetter($nextItem, letter) ) {
  						$nextValue  = $nextItem;
  					}
  				}
  				// check all values
  				if(!$nextValue) {
  					$item
  						.each(function(){
  							if(module.has.firstLetter($(this), letter)) {
  								$nextValue = $(this);
  								return false;
  							}
  						})
  					;
  				}
  				// set next value
  				if($nextValue) {
  					module.verbose('Scrolling to next value with letter', letter);
  					module.set.scrollPosition($nextValue);
  					$selectedItem.classList.remove(className.selected);
  					$nextValue.classList.add(className.selected);
  					if(settings.selectOnKeydown && module.is.single()) {
  						module.set.selectedItem($nextValue);
  					}
  				}
  			},
  			direction: function($menu) {
  				if(settings.direction == 'auto') {
  					// reset position
  					module.remove.upward();
  
  					if(module.can.openDownward($menu)) {
  						module.remove.upward($menu);
  					}
  					else {
  						module.set.upward($menu);
  					}
  					if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
  						module.set.leftward($menu);
  					}
  				}
  				else if(settings.direction == 'upward') {
  					module.set.upward($menu);
  				}
  			},
  			upward: function($currentMenu) {
  				var $element = $currentMenu || element;
  				$element.classList.add(className.upward);
  			},
  			leftward: function($currentMenu) {
  				var $element = $currentMenu || $menu;
  				$element.classList.add(className.leftward);
  			},
  			value: function(value, text, $selected) {
  				var
  					escapedValue = module.escape.value(value),
  					hasInput     = ($input.length > 0),
  					currentValue = module.get.values(),
  					stringValue  = (value !== undefined)
  						? String(value)
  						: value,
  					newValue
  				;
  				if(hasInput) {
  					if(!settings.allowReselection && stringValue == currentValue) {
  						module.verbose('Skipping value update already same value', value, currentValue);
  						if(!module.is.initialLoad()) {
  							return;
  						}
  					}
  
  					if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
  						module.debug('Adding user option', value);
  						module.add.optionValue(value);
  					}
  					module.debug('Updating input value', escapedValue, currentValue);
  					internalChange = true;
  					$input
  						.val(escapedValue)
  					;
  					if(settings.fireOnInit === false && module.is.initialLoad()) {
  						module.debug('Input native change event ignored on initial load');
  					}
  					else {
  						module.trigger.change();
  					}
  					internalChange = false;
  				}
  				else {
  					module.verbose('Storing value in metadata', escapedValue, $input);
  					if(escapedValue !== currentValue) {
  						element.data(metadata.value, stringValue);
  					}
  				}
  				if(settings.fireOnInit === false && module.is.initialLoad()) {
  					module.verbose('No callback on initial load', settings.onChange);
  				}
  				else {
  					settings.onChange.call(element, value, text, $selected);
  				}
  			},
  			active: function() {
  				element
  					.classList.add(className.active)
  				;
  			},
  			multiple: function() {
  				element.classList.add(className.multiple);
  			},
  			visible: function() {
  				element.classList.add(className.visible);
  			},
  			exactly: function(value, $selectedItem) {
  				module.debug('Setting selected to exact values');
  				module.clear();
  				module.set.selected(value, $selectedItem);
  			},
  			selected: function(value, $selectedItem) {
  				var
  					isMultiple = module.is.multiple(),
  					$userSelectedItem
  				;
  				$selectedItem = (settings.allowAdditions)
  					? $selectedItem || module.get.itemWithAdditions(value)
  					: $selectedItem || module.get.item(value)
  				;
  				if(!$selectedItem) {
  					return;
  				}
  				module.debug('Setting selected menu item to', $selectedItem);
  				if(module.is.multiple()) {
  					module.remove.searchWidth();
  				}
  				if(module.is.single()) {
  					module.remove.activeItem();
  					module.remove.selectedItem();
  				}
  				else if(settings.useLabels) {
  					module.remove.selectedItem();
  				}
  				// select each item
  				$selectedItem
  					.each(function() {
  						var
  							$selected      = $(this),
  							selectedText   = module.get.choiceText($selected),
  							selectedValue  = module.get.choiceValue($selected, selectedText),
  
  							isFiltered     = $selected.classList.contains(className.filtered),
  							isActive       = $selected.classList.contains(className.active),
  							isUserValue    = $selected.classList.contains(className.addition),
  							shouldAnimate  = (isMultiple && $selectedItem.length == 1)
  						;
  						if(isMultiple) {
  							if(!isActive || isUserValue) {
  								if(settings.apiSettings && settings.saveRemoteData) {
  									module.save.remoteData(selectedText, selectedValue);
  								}
  								if(settings.useLabels) {
  									module.add.label(selectedValue, selectedText, shouldAnimate);
  									module.add.value(selectedValue, selectedText, $selected);
  									module.set.activeItem($selected);
  									module.filterActive();
  									module.select.nextAvailable($selectedItem);
  								}
  								else {
  									module.add.value(selectedValue, selectedText, $selected);
  									module.set.text(module.add.variables(message.count));
  									module.set.activeItem($selected);
  								}
  							}
  							else if(!isFiltered) {
  								module.debug('Selected active value, removing label');
  								module.remove.selected(selectedValue);
  							}
  						}
  						else {
  							if(settings.apiSettings && settings.saveRemoteData) {
  								module.save.remoteData(selectedText, selectedValue);
  							}
  							module.set.text(selectedText);
  							module.set.value(selectedValue, selectedText, $selected);
  							$selected
  								.classList.add(className.active)
  								.classList.add(className.selected)
  							;
  						}
  					})
  				;
  			}
  		},
  
  		add: {
  			label: function(value, text, shouldAnimate) {
  				var
  					$next  = module.is.searchSelection()
  						? $search
  						: $text,
  					escapedValue = module.escape.value(value),
  					$label
  				;
  				if(settings.ignoreCase) {
  					escapedValue = escapedValue.toLowerCase();
  				}
  				$label =  $('<a />')
  					.classList.add(className.label)
  					.attr('data-' + metadata.value, escapedValue)
  					.html(templates.label(escapedValue, text))
  				;
  				$label = settings.onLabelCreate.call($label, escapedValue, text);
  
  				if(module.has.label(value)) {
  					module.debug('User selection already exists, skipping', escapedValue);
  					return;
  				}
  				if(settings.label.variation) {
  					$label.classList.add(settings.label.variation);
  				}
  				if(shouldAnimate === true) {
  					module.debug('Animating in label', $label);
  					$label
  						.classList.add(className.hidden)
  						.insertBefore($next)
  						.transition(settings.label.transition, settings.label.duration)
  					;
  				}
  				else {
  					module.debug('Adding selection label', $label);
  					$label
  						.insertBefore($next)
  					;
  				}
  			},
  			message: function(message) {
  				var
  					$message = $menu.children(selector.message),
  					html     = settings.templates.message(module.add.variables(message))
  				;
  				if($message.length > 0) {
  					$message
  						.html(html)
  					;
  				}
  				else {
  					$message = $('<div/>')
  						.html(html)
  						.classList.add(className.message)
  						.appendTo($menu)
  					;
  				}
  			},
  			optionValue: function(value) {
  				var
  					escapedValue = module.escape.value(value),
  					$option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
  					hasOption    = ($option.length > 0)
  				;
  				if(hasOption) {
  					return;
  				}
  				// temporarily disconnect observer
  				module.disconnect.selectObserver();
  				if( module.is.single() ) {
  					module.verbose('Removing previous user addition');
  					$input.find('option.' + className.addition).remove();
  				}
  				$('<option/>')
  					.prop('value', escapedValue)
  					.classList.add(className.addition)
  					.html(value)
  					.appendTo($input)
  				;
  				module.verbose('Adding user addition as an <option>', value);
  				module.observe.select();
  			},
  			userSuggestion: function(value) {
  				var
  					$addition         = $menu.children(selector.addition),
  					$existingItem     = module.get.item(value),
  					alreadyHasValue   = $existingItem && $existingItem.not(selector.addition).length,
  					hasUserSuggestion = $addition.length > 0,
  					html
  				;
  				if(settings.useLabels && module.has.maxSelections()) {
  					return;
  				}
  				if(value === '' || alreadyHasValue) {
  					$addition.remove();
  					return;
  				}
  				if(hasUserSuggestion) {
  					$addition
  						.data(metadata.value, value)
  						.data(metadata.text, value)
  						.attr('data-' + metadata.value, value)
  						.attr('data-' + metadata.text, value)
  						.classList.remove(className.filtered)
  					;
  					if(!settings.hideAdditions) {
  						html = settings.templates.addition( module.add.variables(message.addResult, value) );
  						$addition
  							.html(html)
  						;
  					}
  					module.verbose('Replacing user suggestion with new value', $addition);
  				}
  				else {
  					$addition = module.create.userChoice(value);
  					$addition
  						.prependTo($menu)
  					;
  					module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
  				}
  				if(!settings.hideAdditions || module.is.allFiltered()) {
  					$addition
  						.classList.add(className.selected)
  						.siblings()
  						.classList.remove(className.selected)
  					;
  				}
  				module.refreshItems();
  			},
  			variables: function(message, term) {
  				var
  					hasCount    = (message.search('{count}') !== -1),
  					hasMaxCount = (message.search('{maxCount}') !== -1),
  					hasTerm     = (message.search('{term}') !== -1),
  					values,
  					count,
  					query
  				;
  				module.verbose('Adding templated variables to message', message);
  				if(hasCount) {
  					count  = module.get.selectionCount();
  					message = message.replace('{count}', count);
  				}
  				if(hasMaxCount) {
  					count  = module.get.selectionCount();
  					message = message.replace('{maxCount}', settings.maxSelections);
  				}
  				if(hasTerm) {
  					query   = term || module.get.query();
  					message = message.replace('{term}', query);
  				}
  				return message;
  			},
  			value: function(addedValue, addedText, $selectedItem) {
  				var
  					currentValue = module.get.values(),
  					newValue
  				;
  				if(module.has.value(addedValue)) {
  					module.debug('Value already selected');
  					return;
  				}
  				if(addedValue === '') {
  					module.debug('Cannot select blank values from multiselect');
  					return;
  				}
  				// extend current array
  				if($.isArray(currentValue)) {
  					newValue = currentValue.concat([addedValue]);
  					newValue = module.get.uniqueArray(newValue);
  				}
  				else {
  					newValue = [addedValue];
  				}
  				// add values
  				if( module.has.selectInput() ) {
  					if(module.can.extendSelect()) {
  						module.debug('Adding value to select', addedValue, newValue, $input);
  						module.add.optionValue(addedValue);
  					}
  				}
  				else {
  					newValue = newValue.join(settings.delimiter);
  					module.debug('Setting hidden input to delimited value', newValue, $input);
  				}
  
  				if(settings.fireOnInit === false && module.is.initialLoad()) {
  					module.verbose('Skipping onadd callback on initial load', settings.onAdd);
  				}
  				else {
  					settings.onAdd.call(element, addedValue, addedText, $selectedItem);
  				}
  				module.set.value(newValue, addedValue, addedText, $selectedItem);
  				module.check.maxSelections();
  			}
  		},
  
  		remove: {
  			active: function() {
  				element.classList.remove(className.active);
  			},
  			activeLabel: function() {
  				element.find(selector.label).classList.remove(className.active);
  			},
  			empty: function() {
  				element.classList.remove(className.empty);
  			},
  			loading: function() {
  				element.classList.remove(className.loading);
  			},
  			initialLoad: function() {
  				initialLoad = false;
  			},
  			upward: function($currentMenu) {
  				var $element = $currentMenu || element;
  				$element.classList.remove(className.upward);
  			},
  			leftward: function($currentMenu) {
  				var $element = $currentMenu || $menu;
  				$element.classList.remove(className.leftward);
  			},
  			visible: function() {
  				element.classList.remove(className.visible);
  			},
  			activeItem: function() {
  				$item.classList.remove(className.active);
  			},
  			filteredItem: function() {
  				if(settings.useLabels && module.has.maxSelections() ) {
  					return;
  				}
  				if(settings.useLabels && module.is.multiple()) {
  					$item.not('.' + className.active).classList.remove(className.filtered);
  				}
  				else {
  					$item.classList.remove(className.filtered);
  				}
  				module.remove.empty();
  			},
  			optionValue: function(value) {
  				var
  					escapedValue = module.escape.value(value),
  					$option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
  					hasOption    = ($option.length > 0)
  				;
  				if(!hasOption || !$option.classList.contains(className.addition)) {
  					return;
  				}
  				// temporarily disconnect observer
  				if(selectObserver) {
  					selectObserver.disconnect();
  					module.verbose('Temporarily disconnecting mutation observer');
  				}
  				$option.remove();
  				module.verbose('Removing user addition as an <option>', escapedValue);
  				if(selectObserver) {
  					selectObserver.observe($input[0], {
  						childList : true,
  						subtree   : true
  					});
  				}
  			},
  			message: function() {
  				$menu.children(selector.message).remove();
  			},
  			searchWidth: function() {
  				$search.css('width', '');
  			},
  			searchTerm: function() {
  				module.verbose('Cleared search term');
  				$search.val('');
  				module.set.filtered();
  			},
  			userAddition: function() {
  				$item.filter(selector.addition).remove();
  			},
  			selected: function(value, $selectedItem) {
  				$selectedItem = (settings.allowAdditions)
  					? $selectedItem || module.get.itemWithAdditions(value)
  					: $selectedItem || module.get.item(value)
  				;
  
  				if(!$selectedItem) {
  					return false;
  				}
  
  				$selectedItem
  					.each(function() {
  						var
  							$selected     = $(this),
  							selectedText  = module.get.choiceText($selected),
  							selectedValue = module.get.choiceValue($selected, selectedText)
  						;
  						if(module.is.multiple()) {
  							if(settings.useLabels) {
  								module.remove.value(selectedValue, selectedText, $selected);
  								module.remove.label(selectedValue);
  							}
  							else {
  								module.remove.value(selectedValue, selectedText, $selected);
  								if(module.get.selectionCount() === 0) {
  									module.set.placeholderText();
  								}
  								else {
  									module.set.text(module.add.variables(message.count));
  								}
  							}
  						}
  						else {
  							module.remove.value(selectedValue, selectedText, $selected);
  						}
  						$selected
  							.classList.remove(className.filtered)
  							.classList.remove(className.active)
  						;
  						if(settings.useLabels) {
  							$selected.classList.remove(className.selected);
  						}
  					})
  				;
  			},
  			selectedItem: function() {
  				$item.classList.remove(className.selected);
  			},
  			value: function(removedValue, removedText, $removedItem) {
  				var
  					values = module.get.values(),
  					newValue
  				;
  				if( module.has.selectInput() ) {
  					module.verbose('Input is <select> removing selected option', removedValue);
  					newValue = module.remove.arrayValue(removedValue, values);
  					module.remove.optionValue(removedValue);
  				}
  				else {
  					module.verbose('Removing from delimited values', removedValue);
  					newValue = module.remove.arrayValue(removedValue, values);
  					newValue = newValue.join(settings.delimiter);
  				}
  				if(settings.fireOnInit === false && module.is.initialLoad()) {
  					module.verbose('No callback on initial load', settings.onRemove);
  				}
  				else {
  					settings.onRemove.call(element, removedValue, removedText, $removedItem);
  				}
  				module.set.value(newValue, removedText, $removedItem);
  				module.check.maxSelections();
  			},
  			arrayValue: function(removedValue, values) {
  				if( !$.isArray(values) ) {
  					values = [values];
  				}
  				values = $.grep(values, function(value){
  					return (removedValue != value);
  				});
  				module.verbose('Removed value from delimited string', removedValue, values);
  				return values;
  			},
  			label: function(value, shouldAnimate) {
  				var
  					$labels       = element.find(selector.label),
  					$removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]')
  				;
  				module.verbose('Removing label', $removedLabel);
  				$removedLabel.remove();
  			},
  			activeLabels: function($activeLabels) {
  				$activeLabels = $activeLabels || element.find(selector.label).filter('.' + className.active);
  				module.verbose('Removing active label selections', $activeLabels);
  				module.remove.labels($activeLabels);
  			},
  			labels: function($labels) {
  				$labels = $labels || element.find(selector.label);
  				module.verbose('Removing labels', $labels);
  				$labels
  					.each(function(){
  						var
  							$label      = $(this),
  							value       = $label.data(metadata.value),
  							stringValue = (value !== undefined)
  								? String(value)
  								: value,
  							isUserValue = module.is.userValue(stringValue)
  						;
  						if(settings.onLabelRemove.call($label, value) === false) {
  							module.debug('Label remove callback cancelled removal');
  							return;
  						}
  						module.remove.message();
  						if(isUserValue) {
  							module.remove.value(stringValue);
  							module.remove.label(stringValue);
  						}
  						else {
  							// selected will also remove label
  							module.remove.selected(stringValue);
  						}
  					})
  				;
  			},
  			tabbable: function() {
  				if( module.is.searchSelection() ) {
  					module.debug('Searchable dropdown initialized');
  					$search
  						.removeAttr('tabindex')
  					;
  					$menu
  						.removeAttr('tabindex')
  					;
  				}
  				else {
  					module.debug('Simple selection dropdown initialized');
  					element
  						.removeAttr('tabindex')
  					;
  					$menu
  						.removeAttr('tabindex')
  					;
  				}
  			}
  		},
  
  		has: {
  			menuSearch: function() {
  				return (module.has.search() && $search.closest($menu).length > 0);
  			},
  			search: function() {
  				return ($search.length > 0);
  			},
  			sizer: function() {
  				return ($sizer.length > 0);
  			},
  			selectInput: function() {
  				return ( $input.is('select') );
  			},
  			minCharacters: function(searchTerm) {
  				if(settings.minCharacters) {
  					searchTerm = (searchTerm !== undefined)
  						? String(searchTerm)
  						: String(module.get.query())
  					;
  					return (searchTerm.length >= settings.minCharacters);
  				}
  				return true;
  			},
  			firstLetter: function($item, letter) {
  				var
  					text,
  					firstLetter
  				;
  				if(!$item || $item.length === 0 || typeof letter !== 'string') {
  					return false;
  				}
  				text        = module.get.choiceText($item, false);
  				letter      = letter.toLowerCase();
  				firstLetter = String(text).charAt(0).toLowerCase();
  				return (letter == firstLetter);
  			},
  			input: function() {
  				return ($input.length > 0);
  			},
  			items: function() {
  				return ($item.length > 0);
  			},
  			menu: function() {
  				return ($menu.length > 0);
  			},
  			message: function() {
  				return ($menu.children(selector.message).length !== 0);
  			},
  			label: function(value) {
  				var
  					escapedValue = module.escape.value(value),
  					$labels      = element.find(selector.label)
  				;
  				if(settings.ignoreCase) {
  					escapedValue = escapedValue.toLowerCase();
  				}
  				return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
  			},
  			maxSelections: function() {
  				return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
  			},
  			allResultsFiltered: function() {
  				var
  					$normalResults = $item.not(selector.addition)
  				;
  				return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
  			},
  			userSuggestion: function() {
  				return ($menu.children(selector.addition).length > 0);
  			},
  			query: function() {
  				return (module.get.query() !== '');
  			},
  			value: function(value) {
  				return (settings.ignoreCase)
  					? module.has.valueIgnoringCase(value)
  					: module.has.valueMatchingCase(value)
  				;
  			},
  			valueMatchingCase: function(value) {
  				var
  					values   = module.get.values(),
  					hasValue = $.isArray(values)
  					 ? values && ($.inArray(value, values) !== -1)
  					 : (values == value)
  				;
  				return (hasValue)
  					? true
  					: false
  				;
  			},
  			valueIgnoringCase: function(value) {
  				var
  					values   = module.get.values(),
  					hasValue = false
  				;
  				if(!$.isArray(values)) {
  					values = [values];
  				}
  				$.each(values, function(index, existingValue) {
  					if(String(value).toLowerCase() == String(existingValue).toLowerCase()) {
  						hasValue = true;
  						return false;
  					}
  				});
  				return hasValue;
  			}
  		},
  
  		is: {
  			active: function() {
  				return element.classList.contains(className.active);
  			},
  			animatingInward: function() {
  				return $menu.transition('is inward');
  			},
  			animatingOutward: function() {
  				return $menu.transition('is outward');
  			},
  			bubbledLabelClick: function(event) {
  				return $(event.target).is('select, input') && element.closest('label').length > 0;
  			},
  			bubbledIconClick: function(event) {
  				return $(event.target).closest($icon).length > 0;
  			},
  			alreadySetup: function() {
  				return (element.is('select') && element.parent(selector.dropdown).data(moduleNamespace) !== undefined && element.prev().length === 0);
  			},
  			animating: function($subMenu) {
  				return ($subMenu)
  					? $subMenu.transition && $subMenu.transition('is animating')
  					: $menu.transition    && $menu.transition('is animating')
  				;
  			},
  			leftward: function($subMenu) {
  				var $selectedMenu = $subMenu || $menu;
  				return $selectedMenu.classList.contains(className.leftward);
  			},
  			disabled: function() {
  				return element.classList.contains(className.disabled);
  			},
  			focused: function() {
  				return (document.activeElement === element[0]);
  			},
  			focusedOnSearch: function() {
  				return (document.activeElement === $search[0]);
  			},
  			allFiltered: function() {
  				return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
  			},
  			hidden: function($subMenu) {
  				return !module.is.visible($subMenu);
  			},
  			initialLoad: function() {
  				return initialLoad;
  			},
  			inObject: function(needle, object) {
  				var
  					found = false
  				;
  				$.each(object, function(index, property) {
  					if(property == needle) {
  						found = true;
  						return true;
  					}
  				});
  				return found;
  			},
  			multiple: function() {
  				return element.classList.contains(className.multiple);
  			},
  			remote: function() {
  				return settings.apiSettings && module.can.useAPI();
  			},
  			single: function() {
  				return !module.is.multiple();
  			},
  			selectMutation: function(mutations) {
  				var
  					selectChanged = false
  				;
  				$.each(mutations, function(index, mutation) {
  					if(mutation.target && $(mutation.target).is('select')) {
  						selectChanged = true;
  						return true;
  					}
  				});
  				return selectChanged;
  			},
  			search: function() {
  				return element.classList.contains(className.search);
  			},
  			searchSelection: function() {
  				return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
  			},
  			selection: function() {
  				return element.classList.contains(className.selection);
  			},
  			userValue: function(value) {
  				return ($.inArray(value, module.get.userValues()) !== -1);
  			},
  			upward: function($menu) {
  				var $element = $menu || element;
  				return $element.classList.contains(className.upward);
  			},
  			visible: function($subMenu) {
  				return ($subMenu)
  					? $subMenu.classList.contains(className.visible)
  					: $menu.classList.contains(className.visible)
  				;
  			},
  			verticallyScrollableContext: function() {
  				var
  					overflowY = ($context.get(0) !== window)
  						? $context.css('overflow-y')
  						: false
  				;
  				return (overflowY == 'auto' || overflowY == 'scroll');
  			},
  			horizontallyScrollableContext: function() {
  				var
  					overflowX = ($context.get(0) !== window)
  						? $context.css('overflow-X')
  						: false
  				;
  				return (overflowX == 'auto' || overflowX == 'scroll');
  			}
  		},
  
  		can: {
  			activate: function($item) {
  				if(settings.useLabels) {
  					return true;
  				}
  				if(!module.has.maxSelections()) {
  					return true;
  				}
  				if(module.has.maxSelections() && $item.classList.contains(className.active)) {
  					return true;
  				}
  				return false;
  			},
  			openDownward: function($subMenu) {
  				var
  					$currentMenu    = $subMenu || $menu,
  					canOpenDownward = true,
  					onScreen        = {},
  					calculations
  				;
  				$currentMenu
  					.classList.add(className.loading)
  				;
  				calculations = {
  					context: {
  						offset    : ($context.get(0) === window)
  							? { top: 0, left: 0}
  							: $context.offset(),
  						scrollTop : $context.scrollTop(),
  						height    : $context.outerHeight()
  					},
  					menu : {
  						offset: $currentMenu.offset(),
  						height: $currentMenu.outerHeight()
  					}
  				};
  				if(module.is.verticallyScrollableContext()) {
  					calculations.menu.offset.top += calculations.context.scrollTop;
  				}
  				onScreen = {
  					above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height,
  					below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height
  				};
  				if(onScreen.below) {
  					module.verbose('Dropdown can fit in context downward', onScreen);
  					canOpenDownward = true;
  				}
  				else if(!onScreen.below && !onScreen.above) {
  					module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
  					canOpenDownward = true;
  				}
  				else {
  					module.verbose('Dropdown cannot fit below, opening upward', onScreen);
  					canOpenDownward = false;
  				}
  				$currentMenu.classList.remove(className.loading);
  				return canOpenDownward;
  			},
  			openRightward: function($subMenu) {
  				var
  					$currentMenu     = $subMenu || $menu,
  					canOpenRightward = true,
  					isOffscreenRight = false,
  					calculations
  				;
  				$currentMenu
  					.classList.add(className.loading)
  				;
  				calculations = {
  					context: {
  						offset     : ($context.get(0) === window)
  							? { top: 0, left: 0}
  							: $context.offset(),
  						scrollLeft : $context.scrollLeft(),
  						width      : $context.outerWidth()
  					},
  					menu: {
  						offset : $currentMenu.offset(),
  						width  : $currentMenu.outerWidth()
  					}
  				};
  				if(module.is.horizontallyScrollableContext()) {
  					calculations.menu.offset.left += calculations.context.scrollLeft;
  				}
  				isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
  				if(isOffscreenRight) {
  					module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
  					canOpenRightward = false;
  				}
  				$currentMenu.classList.remove(className.loading);
  				return canOpenRightward;
  			},
  			click: function() {
  				return (hasTouch || settings.on == 'click');
  			},
  			extendSelect: function() {
  				return settings.allowAdditions || settings.apiSettings;
  			},
  			show: function() {
  				return !module.is.disabled() && (module.has.items() || module.has.message());
  			},
  			useAPI: function() {
  				return $.fn.api !== undefined;
  			}
  		},
  
  		animate: {
  			show: function(callback, $subMenu) {
  				var
  					$currentMenu = $subMenu || $menu,
  					start = ($subMenu)
  						? function() {}
  						: function() {
  							module.hideSubMenus();
  							module.hideOthers();
  							module.set.active();
  						},
  					transition
  				;
  				callback = $.isFunction(callback)
  					? callback
  					: function(){}
  				;
  				module.verbose('Doing menu show animation', $currentMenu);
  				module.set.direction($subMenu);
  				transition = module.get.transition($subMenu);
  				if( module.is.selection() ) {
  					module.set.scrollPosition(module.get.selectedItem(), true);
  				}
  				if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
  					if(transition == 'none') {
  						start();
  						$currentMenu.transition('show');
  						callback.call(element);
  					}
  					else if($.fn.transition !== undefined && element.transition('is supported')) {
  						$currentMenu
  							.transition({
  								animation  : transition + ' in',
  								debug      : settings.debug,
  								verbose    : settings.verbose,
  								duration   : settings.duration,
  								queue      : true,
  								onStart    : start,
  								onComplete : function() {
  									callback.call(element);
  								}
  							})
  						;
  					}
  					else {
  						module.error(error.noTransition, transition);
  					}
  				}
  			},
  			hide: function(callback, $subMenu) {
  				var
  					$currentMenu = $subMenu || $menu,
  					duration = ($subMenu)
  						? (settings.duration * 0.9)
  						: settings.duration,
  					start = ($subMenu)
  						? function() {}
  						: function() {
  							if( module.can.click() ) {
  								module.unbind.intent();
  							}
  							module.remove.active();
  						},
  					transition = module.get.transition($subMenu)
  				;
  				callback = $.isFunction(callback)
  					? callback
  					: function(){}
  				;
  				if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
  					module.verbose('Doing menu hide animation', $currentMenu);
  
  					if(transition == 'none') {
  						start();
  						$currentMenu.transition('hide');
  						callback.call(element);
  					}
  					else if($.fn.transition !== undefined && element.transition('is supported')) {
  						$currentMenu
  							.transition({
  								animation  : transition + ' out',
  								duration   : settings.duration,
  								debug      : settings.debug,
  								verbose    : settings.verbose,
  								queue      : false,
  								onStart    : start,
  								onComplete : function() {
  									callback.call(element);
  								}
  							})
  						;
  					}
  					else {
  						module.error(error.transition);
  					}
  				}
  			}
  		},
  
  		hideAndClear: function() {
  			module.remove.searchTerm();
  			if( module.has.maxSelections() ) {
  				return;
  			}
  			if(module.has.search()) {
  				module.hide(function() {
  					module.remove.filteredItem();
  				});
  			}
  			else {
  				module.hide();
  			}
  		},
  
  		delay: {
  			show: function() {
  				module.verbose('Delaying show event to ensure user intent');
  				clearTimeout(module.timer);
  				module.timer = setTimeout(module.show, settings.delay.show);
  			},
  			hide: function() {
  				module.verbose('Delaying hide event to ensure user intent');
  				clearTimeout(module.timer);
  				module.timer = setTimeout(module.hide, settings.delay.hide);
  			}
  		},
  
  		escape: {
  			value: function(value) {
  				var
  					multipleValues = $.isArray(value),
  					stringValue    = (typeof value === 'string'),
  					isUnparsable   = (!stringValue && !multipleValues),
  					hasQuotes      = (stringValue && value.search(regExp.quote) !== -1),
  					values         = []
  				;
  				if(isUnparsable || !hasQuotes) {
  					return value;
  				}
  				module.debug('Encoding quote values for use in select', value);
  				if(multipleValues) {
  					$.each(value, function(index, value){
  						values.push(value.replace(regExp.quote, '&quot;'));
  					});
  					return values;
  				}
  				return value.replace(regExp.quote, '&quot;');
  			},
  			string: function(text) {
  				text =  String(text);
  				return text.replace(regExp.escape, '\\$&');
  			}
  		}
  	};*/
  
  	module = {};
  
  	return module;
  };
  
  ui.dropdown.settings = {
  
  	silent                 : false,
  	debug                  : false,
  	verbose                : false,
  	performance            : true,
  
  	on                     : 'click',    // what event should show menu action on item selection
  	action                 : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
  
  	values                 : false,      // specify values to use for dropdown
  
  	apiSettings            : false,
  	selectOnKeydown        : true,       // Whether selection should occur automatically when keyboard shortcuts used
  	minCharacters          : 0,          // Minimum characters required to trigger API call
  
  	filterRemoteData       : false,      // Whether API results should be filtered after being returned for query term
  	saveRemoteData         : true,       // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
  
  	throttle               : 200,        // How long to wait after last user input to search remotely
  
  	context                : window,     // Context to use when determining if on screen
  	direction              : 'auto',     // Whether dropdown should always open in one direction
  	keepOnScreen           : true,       // Whether dropdown should check whether it is on screen before showing
  
  	match                  : 'both',     // what to match against with search selection (both, text, or label)
  	fullTextSearch         : false,      // search anywhere in value (set to 'exact' to require exact matches)
  
  	placeholder            : 'auto',     // whether to convert blank <select> values to placeholder text
  	preserveHTML           : true,       // preserve html when selecting value
  	sortSelect             : false,      // sort selection on init
  
  	forceSelection         : true,       // force a choice on blur with search selection
  
  	allowAdditions         : false,      // whether multiple select should allow user added values
  	ignoreCase             : false,       // whether to consider values not matching in case to be the same
  	hideAdditions          : true,       // whether or not to hide special message prompting a user they can enter a value
  
  	maxSelections          : false,      // When set to a number limits the number of selections to this count
  	useLabels              : true,       // whether multiple select should filter currently active selections from choices
  	delimiter              : ',',        // when multiselect uses normal <input> the values will be delimited with this character
  
  	showOnFocus            : true,       // show menu on focus
  	allowReselection       : false,      // whether current value should trigger callbacks when reselected
  	allowTab               : true,       // add tabindex to element
  	allowCategorySelection : false,      // allow elements with sub-menus to be selected
  
  	fireOnInit             : false,      // Whether callbacks should fire when initializing dropdown values
  
  	transition             : 'auto',     // auto transition will slide down or up based on direction
  	duration               : 200,        // duration of transition
  
  	glyphWidth             : 1.037,      // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
  
  	// label settings on multi-select
  	label: {
  		transition : 'scale',
  		duration   : 200,
  		variation  : false
  	},
  
  	// delay before event
  	delay : {
  		hide   : 300,
  		show   : 200,
  		search : 20,
  		touch  : 50
  	},
  
  	/* Callbacks */
  	onChange      : function(value, text, $selected){},
  	onAdd         : function(value, text, $selected){},
  	onRemove      : function(value, text, $selected){},
  
  	onLabelSelect : function($selectedLabels){},
  	onLabelCreate : function(value, text) { return $(this); },
  	onLabelRemove : function(value) { return true; },
  	onNoResults   : function(searchTerm) { return true; },
  	onShow        : function(){},
  	onHide        : function(){},
  
  	/* Component */
  	name           : 'Dropdown',
  	namespace      : 'dropdown',
  
  	message: {
  		addResult     : 'Add <b>{term}</b>',
  		count         : '{count} selected',
  		maxSelections : 'Max {maxCount} selections',
  		noResults     : 'No results found.',
  		serverError   : 'There was an error contacting the server'
  	},
  
  	error : {
  		action          : 'You called a dropdown action that was not defined',
  		alreadySetup    : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
  		labels          : 'Allowing user additions currently requires the use of labels.',
  		missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
  		method          : 'The method you called is not defined.',
  		noAPI           : 'The API module is required to load resources remotely',
  		noStorage       : 'Saving remote data requires session storage',
  		noTransition    : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>'
  	},
  
  	regExp : {
  		escape   : /[-[\]{}()*+?.,\\^$|#\s]/g,
  		quote    : /"/g
  	},
  
  	metadata : {
  		defaultText     : 'defaultText',
  		defaultValue    : 'defaultValue',
  		placeholderText : 'placeholder',
  		text            : 'text',
  		value           : 'value'
  	},
  
  	// property names for remote query
  	fields: {
  		remoteValues : 'results',  // grouping for api results
  		values       : 'values',   // grouping for all dropdown values
  		disabled     : 'disabled', // whether value should be disabled
  		name         : 'name',     // displayed dropdown text
  		value        : 'value',    // actual dropdown value
  		text         : 'text'      // displayed text when selected
  	},
  
  	keys : {
  		backspace  : 8,
  		delimiter  : 188, // comma
  		deleteKey  : 46,
  		enter      : 13,
  		escape     : 27,
  		pageUp     : 33,
  		pageDown   : 34,
  		leftArrow  : 37,
  		upArrow    : 38,
  		rightArrow : 39,
  		downArrow  : 40
  	},
  
  	selector : {
  		addition     : '.addition',
  		dropdown     : '.ui.dropdown',
  		hidden       : '.hidden',
  		icon         : '> .dropdown.icon',
  		input        : '> input[type="hidden"], > select',
  		item         : '.item',
  		label        : '> .label',
  		remove       : '> .label > .delete.icon',
  		siblingLabel : '.label',
  		menu         : '.menu',
  		message      : '.message',
  		menuIcon     : '.dropdown.icon',
  		search       : 'input.search, .menu > .search > input, .menu input.search',
  		sizer        : '> input.sizer',
  		text         : '> .text:not(.icon)',
  		unselectable : '.disabled, .filtered'
  	},
  
  	className : {
  		active      : 'active',
  		addition    : 'addition',
  		animating   : 'animating',
  		disabled    : 'disabled',
  		empty       : 'empty',
  		dropdown    : 'ui dropdown',
  		filtered    : 'filtered',
  		hidden      : 'hidden transition',
  		item        : 'item',
  		label       : 'ui label',
  		loading     : 'loading',
  		menu        : 'menu',
  		message     : 'message',
  		multiple    : 'multiple',
  		placeholder : 'default',
  		sizer       : 'sizer',
  		search      : 'search',
  		selected    : 'selected',
  		selection   : 'selection',
  		upward      : 'upward',
  		leftward    : 'left',
  		visible     : 'visible'
  	}
  };
  
  ui.dropdown.settings.templates = {
  
  	// generates dropdown from select values
  	dropdown: function(select) {
  		var
  			placeholder = select.placeholder || false,
  			values      = select.values || {},
  			html        = ''
  		;
  		html +=  '<i class="dropdown icon"></i>';
  		if(select.placeholder) {
  			html += '<div class="default text">' + placeholder + '</div>';
  		}
  		else {
  			html += '<div class="text"></div>';
  		}
  		html += '<div class="menu">';
  		$.each(select.values, function(index, option) {
  			html += (option.disabled)
  				? '<div class="disabled item" data-value="' + option.value + '">' + option.name + '</div>'
  				: '<div class="item" data-value="' + option.value + '">' + option.name + '</div>'
  			;
  		});
  		html += '</div>';
  		return html;
  	},
  
  	// generates just menu from select
  	menu: function(response, fields) {
  		var
  			values = response[fields.values] || {},
  			html   = ''
  		;
  		$.each(values, function(index, option) {
  			var
  				maybeText = (option[fields.text])
  					? 'data-text="' + option[fields.text] + '"'
  					: '',
  				maybeDisabled = (option[fields.disabled])
  					? 'disabled '
  					: ''
  			;
  			html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>';
  			html +=   option[fields.name];
  			html += '</div>';
  		});
  		return html;
  	},
  
  	// generates label for multiselect
  	label: function(value, text) {
  		return text + '<i class="delete icon"></i>';
  	},
  
  
  	// generates messages like "No results"
  	message: function(message) {
  		return message;
  	},
  
  	// generates user addition to selection menu
  	addition: function(choice) {
  		return choice;
  	}
  };
  