Use Semantic UI for all HTML templates
[cacert-boardvoting.git] / static / semantic.js
index 0a2d432..54fe2a3 100644 (file)
@@ -1,5 +1,5 @@
  /*
- * # Semantic UI - 
+ * # Semantic UI - 2.2.10
  * https://github.com/Semantic-Org/Semantic-UI
  * http://www.semantic-ui.com/
  *
@@ -9,7 +9,7 @@
  *
  */
 /*!
- * # Semantic UI undefined - Site
+ * # Semantic UI 2.2.10 - Site
  * http://github.com/semantic-org/semantic-ui/
  *
  *
@@ -497,7 +497,7 @@ $.extend($.expr[ ":" ], {
 })( jQuery, window, document );
 
 /*!
- * # Semantic UI undefined - Form Validation
+ * # Semantic UI 2.2.10 - Form Validation
  * http://github.com/semantic-org/semantic-ui/
  *
  *
@@ -2104,7 +2104,7 @@ $.fn.form.settings = {
 })( jQuery, window, document );
 
 /*!
- * # Semantic UI undefined - Accordion
+ * # Semantic UI 2.2.10 - Checkbox
  * http://github.com/semantic-org/semantic-ui/
  *
  *
@@ -2124,31 +2124,24 @@ window = (typeof window != 'undefined' && window.Math == Math)
     : Function('return this')()
 ;
 
-$.fn.accordion = function(parameters) {
+$.fn.checkbox = function(parameters) {
   var
-    $allModules     = $(this),
-
-    time            = new Date().getTime(),
-    performance     = [],
-
-    query           = arguments[0],
-    methodInvoked   = (typeof query == 'string'),
-    queryArguments  = [].slice.call(arguments, 1),
+    $allModules    = $(this),
+    moduleSelector = $allModules.selector || '',
 
-    requestAnimationFrame = window.requestAnimationFrame
-      || window.mozRequestAnimationFrame
-      || window.webkitRequestAnimationFrame
-      || window.msRequestAnimationFrame
-      || function(callback) { setTimeout(callback, 0); },
+    time           = new Date().getTime(),
+    performance    = [],
 
+    query          = arguments[0],
+    methodInvoked  = (typeof query == 'string'),
+    queryArguments = [].slice.call(arguments, 1),
     returnedValue
   ;
+
   $allModules
     .each(function() {
       var
-        settings        = ( $.isPlainObject(parameters) )
-          ? $.extend(true, {}, $.fn.accordion.settings, parameters)
-          : $.extend({}, $.fn.accordion.settings),
+        settings        = $.extend(true, {}, $.fn.checkbox.settings, parameters),
 
         className       = settings.className,
         namespace       = settings.namespace,
@@ -2157,30 +2150,39 @@ $.fn.accordion = function(parameters) {
 
         eventNamespace  = '.' + namespace,
         moduleNamespace = 'module-' + namespace,
-        moduleSelector  = $allModules.selector || '',
 
-        $module  = $(this),
-        $title   = $module.find(selector.title),
-        $content = $module.find(selector.content),
+        $module         = $(this),
+        $label          = $(this).children(selector.label),
+        $input          = $(this).children(selector.input),
+        input           = $input[0],
+
+        initialLoad     = false,
+        shortcutPressed = false,
+        instance        = $module.data(moduleNamespace),
 
-        element  = this,
-        instance = $module.data(moduleNamespace),
         observer,
+        element         = this,
         module
       ;
 
-      module = {
+      module      = {
 
         initialize: function() {
-          module.debug('Initializing', $module);
+          module.verbose('Initializing checkbox', settings);
+
+          module.create.label();
           module.bind.events();
-          if(settings.observeChanges) {
-            module.observeChanges();
-          }
+
+          module.set.tabbable();
+          module.hide.input();
+
+          module.observeChanges();
           module.instantiate();
+          module.setup();
         },
 
         instantiate: function() {
+          module.verbose('Storing instance of module', module);
           instance = module;
           $module
             .data(moduleNamespace, module)
@@ -2188,16 +2190,56 @@ $.fn.accordion = function(parameters) {
         },
 
         destroy: function() {
-          module.debug('Destroying previous instance', $module);
-          $module
-            .off(eventNamespace)
-            .removeData(moduleNamespace)
-          ;
+          module.verbose('Destroying module');
+          module.unbind.events();
+          module.show.input();
+          $module.removeData(moduleNamespace);
+        },
+
+        fix: {
+          reference: function() {
+            if( $module.is(selector.input) ) {
+              module.debug('Behavior called on <input> adjusting invoked element');
+              $module = $module.closest(selector.checkbox);
+              module.refresh();
+            }
+          }
+        },
+
+        setup: function() {
+          module.set.initialLoad();
+          if( module.is.indeterminate() ) {
+            module.debug('Initial value is indeterminate');
+            module.indeterminate();
+          }
+          else if( module.is.checked() ) {
+            module.debug('Initial value is checked');
+            module.check();
+          }
+          else {
+            module.debug('Initial value is unchecked');
+            module.uncheck();
+          }
+          module.remove.initialLoad();
         },
 
         refresh: function() {
-          $title   = $module.find(selector.title);
-          $content = $module.find(selector.content);
+          $label = $module.children(selector.label);
+          $input = $module.children(selector.input);
+          input  = $input[0];
+        },
+
+        hide: {
+          input: function() {
+            module.verbose('Modifying <input> z-index to be unselectable');
+            $input.addClass(className.hidden);
+          }
+        },
+        show: {
+          input: function() {
+            module.verbose('Modifying <input> z-index to be selectable');
+            $input.removeClass(className.hidden);
+          }
         },
 
         observeChanges: function() {
@@ -2214,1322 +2256,669 @@ $.fn.accordion = function(parameters) {
           }
         },
 
-        bind: {
-          events: function() {
-            module.debug('Binding delegated events');
-            $module
-              .on(settings.on + eventNamespace, selector.trigger, module.event.click)
+        attachEvents: function(selector, event) {
+          var
+            $element = $(selector)
+          ;
+          event = $.isFunction(module[event])
+            ? module[event]
+            : module.toggle
+          ;
+          if($element.length > 0) {
+            module.debug('Attaching checkbox events to element', selector, event);
+            $element
+              .on('click' + eventNamespace, event)
             ;
           }
-        },
-
-        event: {
-          click: function() {
-            module.toggle.call(this);
+          else {
+            module.error(error.notFound);
           }
         },
 
-        toggle: function(query) {
-          var
-            $activeTitle = (query !== undefined)
-              ? (typeof query === 'number')
-                ? $title.eq(query)
-                : $(query).closest(selector.title)
-              : $(this).closest(selector.title),
-            $activeContent = $activeTitle.next($content),
-            isAnimating = $activeContent.hasClass(className.animating),
-            isActive    = $activeContent.hasClass(className.active),
-            isOpen      = (isActive && !isAnimating),
-            isOpening   = (!isActive && isAnimating)
-          ;
-          module.debug('Toggling visibility of content', $activeTitle);
-          if(isOpen || isOpening) {
-            if(settings.collapsible) {
-              module.close.call($activeTitle);
+        event: {
+          click: function(event) {
+            var
+              $target = $(event.target)
+            ;
+            if( $target.is(selector.input) ) {
+              module.verbose('Using default check action on initialized checkbox');
+              return;
+            }
+            if( $target.is(selector.link) ) {
+              module.debug('Clicking link inside checkbox, skipping toggle');
+              return;
+            }
+            module.toggle();
+            $input.focus();
+            event.preventDefault();
+          },
+          keydown: function(event) {
+            var
+              key     = event.which,
+              keyCode = {
+                enter  : 13,
+                space  : 32,
+                escape : 27
+              }
+            ;
+            if(key == keyCode.escape) {
+              module.verbose('Escape key pressed blurring field');
+              $input.blur();
+              shortcutPressed = true;
+            }
+            else if(!event.ctrlKey && ( key == keyCode.space || key == keyCode.enter) ) {
+              module.verbose('Enter/space key pressed, toggling checkbox');
+              module.toggle();
+              shortcutPressed = true;
             }
             else {
-              module.debug('Cannot close accordion content collapsing is disabled');
+              shortcutPressed = false;
+            }
+          },
+          keyup: function(event) {
+            if(shortcutPressed) {
+              event.preventDefault();
             }
-          }
-          else {
-            module.open.call($activeTitle);
           }
         },
 
-        open: function(query) {
-          var
-            $activeTitle = (query !== undefined)
-              ? (typeof query === 'number')
-                ? $title.eq(query)
-                : $(query).closest(selector.title)
-              : $(this).closest(selector.title),
-            $activeContent = $activeTitle.next($content),
-            isAnimating = $activeContent.hasClass(className.animating),
-            isActive    = $activeContent.hasClass(className.active),
-            isOpen      = (isActive || isAnimating)
-          ;
-          if(isOpen) {
-            module.debug('Accordion already open, skipping', $activeContent);
+        check: function() {
+          if( !module.should.allowCheck() ) {
             return;
           }
-          module.debug('Opening accordion content', $activeTitle);
-          settings.onOpening.call($activeContent);
-          if(settings.exclusive) {
-            module.closeOthers.call($activeTitle);
-          }
-          $activeTitle
-            .addClass(className.active)
-          ;
-          $activeContent
-            .stop(true, true)
-            .addClass(className.animating)
-          ;
-          if(settings.animateChildren) {
-            if($.fn.transition !== undefined && $module.transition('is supported')) {
-              $activeContent
-                .children()
-                  .transition({
-                    animation   : 'fade in',
-                    queue       : false,
-                    useFailSafe : true,
-                    debug       : settings.debug,
-                    verbose     : settings.verbose,
-                    duration    : settings.duration
-                  })
-              ;
-            }
-            else {
-              $activeContent
-                .children()
-                  .stop(true, true)
-                  .animate({
-                    opacity: 1
-                  }, settings.duration, module.resetOpacity)
-              ;
-            }
+          module.debug('Checking checkbox', $input);
+          module.set.checked();
+          if( !module.should.ignoreCallbacks() ) {
+            settings.onChecked.call(input);
+            settings.onChange.call(input);
           }
-          $activeContent
-            .slideDown(settings.duration, settings.easing, function() {
-              $activeContent
-                .removeClass(className.animating)
-                .addClass(className.active)
-              ;
-              module.reset.display.call(this);
-              settings.onOpen.call(this);
-              settings.onChange.call(this);
-            })
-          ;
         },
 
-        close: function(query) {
-          var
-            $activeTitle = (query !== undefined)
-              ? (typeof query === 'number')
-                ? $title.eq(query)
-                : $(query).closest(selector.title)
-              : $(this).closest(selector.title),
-            $activeContent = $activeTitle.next($content),
-            isAnimating    = $activeContent.hasClass(className.animating),
-            isActive       = $activeContent.hasClass(className.active),
-            isOpening      = (!isActive && isAnimating),
-            isClosing      = (isActive && isAnimating)
-          ;
-          if((isActive || isOpening) && !isClosing) {
-            module.debug('Closing accordion content', $activeContent);
-            settings.onClosing.call($activeContent);
-            $activeTitle
-              .removeClass(className.active)
-            ;
-            $activeContent
-              .stop(true, true)
-              .addClass(className.animating)
-            ;
-            if(settings.animateChildren) {
-              if($.fn.transition !== undefined && $module.transition('is supported')) {
-                $activeContent
-                  .children()
-                    .transition({
-                      animation   : 'fade out',
-                      queue       : false,
-                      useFailSafe : true,
-                      debug       : settings.debug,
-                      verbose     : settings.verbose,
-                      duration    : settings.duration
-                    })
-                ;
-              }
-              else {
-                $activeContent
-                  .children()
-                    .stop(true, true)
-                    .animate({
-                      opacity: 0
-                    }, settings.duration, module.resetOpacity)
-                ;
-              }
-            }
-            $activeContent
-              .slideUp(settings.duration, settings.easing, function() {
-                $activeContent
-                  .removeClass(className.animating)
-                  .removeClass(className.active)
-                ;
-                module.reset.display.call(this);
-                settings.onClose.call(this);
-                settings.onChange.call(this);
-              })
-            ;
+        uncheck: function() {
+          if( !module.should.allowUncheck() ) {
+            return;
+          }
+          module.debug('Unchecking checkbox');
+          module.set.unchecked();
+          if( !module.should.ignoreCallbacks() ) {
+            settings.onUnchecked.call(input);
+            settings.onChange.call(input);
           }
         },
 
-        closeOthers: function(index) {
-          var
-            $activeTitle = (index !== undefined)
-              ? $title.eq(index)
-              : $(this).closest(selector.title),
-            $parentTitles    = $activeTitle.parents(selector.content).prev(selector.title),
-            $activeAccordion = $activeTitle.closest(selector.accordion),
-            activeSelector   = selector.title + '.' + className.active + ':visible',
-            activeContent    = selector.content + '.' + className.active + ':visible',
-            $openTitles,
-            $nestedTitles,
-            $openContents
-          ;
-          if(settings.closeNested) {
-            $openTitles   = $activeAccordion.find(activeSelector).not($parentTitles);
-            $openContents = $openTitles.next($content);
+        indeterminate: function() {
+          if( module.should.allowIndeterminate() ) {
+            module.debug('Checkbox is already indeterminate');
+            return;
           }
-          else {
-            $openTitles   = $activeAccordion.find(activeSelector).not($parentTitles);
-            $nestedTitles = $activeAccordion.find(activeContent).find(activeSelector).not($parentTitles);
-            $openTitles   = $openTitles.not($nestedTitles);
-            $openContents = $openTitles.next($content);
-          }
-          if( ($openTitles.length > 0) ) {
-            module.debug('Exclusive enabled, closing other content', $openTitles);
-            $openTitles
-              .removeClass(className.active)
-            ;
-            $openContents
-              .removeClass(className.animating)
-              .stop(true, true)
-            ;
-            if(settings.animateChildren) {
-              if($.fn.transition !== undefined && $module.transition('is supported')) {
-                $openContents
-                  .children()
-                    .transition({
-                      animation   : 'fade out',
-                      useFailSafe : true,
-                      debug       : settings.debug,
-                      verbose     : settings.verbose,
-                      duration    : settings.duration
-                    })
-                ;
-              }
-              else {
-                $openContents
-                  .children()
-                    .stop(true, true)
-                    .animate({
-                      opacity: 0
-                    }, settings.duration, module.resetOpacity)
-                ;
-              }
-            }
-            $openContents
-              .slideUp(settings.duration , settings.easing, function() {
-                $(this).removeClass(className.active);
-                module.reset.display.call(this);
-              })
-            ;
+          module.debug('Making checkbox indeterminate');
+          module.set.indeterminate();
+          if( !module.should.ignoreCallbacks() ) {
+            settings.onIndeterminate.call(input);
+            settings.onChange.call(input);
           }
         },
 
-        reset: {
-
-          display: function() {
-            module.verbose('Removing inline display from element', this);
-            $(this).css('display', '');
-            if( $(this).attr('style') === '') {
-              $(this)
-                .attr('style', '')
-                .removeAttr('style')
-              ;
-            }
-          },
-
-          opacity: function() {
-            module.verbose('Removing inline opacity from element', this);
-            $(this).css('opacity', '');
-            if( $(this).attr('style') === '') {
-              $(this)
-                .attr('style', '')
-                .removeAttr('style')
-              ;
-            }
-          },
+        determinate: function() {
+          if( module.should.allowDeterminate() ) {
+            module.debug('Checkbox is already determinate');
+            return;
+          }
+          module.debug('Making checkbox determinate');
+          module.set.determinate();
+          if( !module.should.ignoreCallbacks() ) {
+            settings.onDeterminate.call(input);
+            settings.onChange.call(input);
+          }
+        },
 
+        enable: function() {
+          if( module.is.enabled() ) {
+            module.debug('Checkbox is already enabled');
+            return;
+          }
+          module.debug('Enabling checkbox');
+          module.set.enabled();
+          settings.onEnable.call(input);
+          // preserve legacy callbacks
+          settings.onEnabled.call(input);
         },
 
-        setting: function(name, value) {
-          module.debug('Changing setting', name, value);
-          if( $.isPlainObject(name) ) {
-            $.extend(true, settings, name);
+        disable: function() {
+          if( module.is.disabled() ) {
+            module.debug('Checkbox is already disabled');
+            return;
           }
-          else if(value !== undefined) {
-            if($.isPlainObject(settings[name])) {
-              $.extend(true, settings[name], value);
-            }
-            else {
-              settings[name] = value;
-            }
+          module.debug('Disabling checkbox');
+          module.set.disabled();
+          settings.onDisable.call(input);
+          // preserve legacy callbacks
+          settings.onDisabled.call(input);
+        },
+
+        get: {
+          radios: function() {
+            var
+              name = module.get.name()
+            ;
+            return $('input[name="' + name + '"]').closest(selector.checkbox);
+          },
+          otherRadios: function() {
+            return module.get.radios().not($module);
+          },
+          name: function() {
+            return $input.attr('name');
           }
-          else {
-            return settings[name];
+        },
+
+        is: {
+          initialLoad: function() {
+            return initialLoad;
+          },
+          radio: function() {
+            return ($input.hasClass(className.radio) || $input.attr('type') == 'radio');
+          },
+          indeterminate: function() {
+            return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate');
+          },
+          checked: function() {
+            return $input.prop('checked') !== undefined && $input.prop('checked');
+          },
+          disabled: function() {
+            return $input.prop('disabled') !== undefined && $input.prop('disabled');
+          },
+          enabled: function() {
+            return !module.is.disabled();
+          },
+          determinate: function() {
+            return !module.is.indeterminate();
+          },
+          unchecked: function() {
+            return !module.is.checked();
           }
         },
-        internal: function(name, value) {
-          module.debug('Changing internal', name, value);
-          if(value !== undefined) {
-            if( $.isPlainObject(name) ) {
-              $.extend(true, module, name);
+
+        should: {
+          allowCheck: function() {
+            if(module.is.determinate() && module.is.checked() && !module.should.forceCallbacks() ) {
+              module.debug('Should not allow check, checkbox is already checked');
+              return false;
             }
-            else {
-              module[name] = value;
+            if(settings.beforeChecked.apply(input) === false) {
+              module.debug('Should not allow check, beforeChecked cancelled');
+              return false;
             }
-          }
-          else {
-            return module[name];
-          }
-        },
-        debug: function() {
-          if(!settings.silent && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
+            return true;
+          },
+          allowUncheck: function() {
+            if(module.is.determinate() && module.is.unchecked() && !module.should.forceCallbacks() ) {
+              module.debug('Should not allow uncheck, checkbox is already unchecked');
+              return false;
             }
-            else {
-              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.debug.apply(console, arguments);
+            if(settings.beforeUnchecked.apply(input) === false) {
+              module.debug('Should not allow uncheck, beforeUnchecked cancelled');
+              return false;
             }
-          }
-        },
-        verbose: function() {
-          if(!settings.silent && settings.verbose && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
+            return true;
+          },
+          allowIndeterminate: function() {
+            if(module.is.indeterminate() && !module.should.forceCallbacks() ) {
+              module.debug('Should not allow indeterminate, checkbox is already indeterminate');
+              return false;
             }
-            else {
-              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.verbose.apply(console, arguments);
+            if(settings.beforeIndeterminate.apply(input) === false) {
+              module.debug('Should not allow indeterminate, beforeIndeterminate cancelled');
+              return false;
+            }
+            return true;
+          },
+          allowDeterminate: function() {
+            if(module.is.determinate() && !module.should.forceCallbacks() ) {
+              module.debug('Should not allow determinate, checkbox is already determinate');
+              return false;
+            }
+            if(settings.beforeDeterminate.apply(input) === false) {
+              module.debug('Should not allow determinate, beforeDeterminate cancelled');
+              return false;
             }
+            return true;
+          },
+          forceCallbacks: function() {
+            return (module.is.initialLoad() && settings.fireOnInit);
+          },
+          ignoreCallbacks: function() {
+            return (initialLoad && !settings.fireOnInit);
           }
         },
-        error: function() {
-          if(!settings.silent) {
-            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-            module.error.apply(console, arguments);
+
+        can: {
+          change: function() {
+            return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') );
+          },
+          uncheck: function() {
+            return (typeof settings.uncheckable === 'boolean')
+              ? settings.uncheckable
+              : !module.is.radio()
+            ;
           }
         },
-        performance: {
-          log: function(message) {
-            var
-              currentTime,
-              executionTime,
-              previousTime
+
+        set: {
+          initialLoad: function() {
+            initialLoad = true;
+          },
+          checked: function() {
+            module.verbose('Setting class to checked');
+            $module
+              .removeClass(className.indeterminate)
+              .addClass(className.checked)
             ;
-            if(settings.performance) {
-              currentTime   = new Date().getTime();
-              previousTime  = time || currentTime;
-              executionTime = currentTime - previousTime;
-              time          = currentTime;
-              performance.push({
-                'Name'           : message[0],
-                'Arguments'      : [].slice.call(message, 1) || '',
-                'Element'        : element,
-                'Execution Time' : executionTime
-              });
+            if( module.is.radio() ) {
+              module.uncheckOthers();
             }
-            clearTimeout(module.performance.timer);
-            module.performance.timer = setTimeout(module.performance.display, 500);
+            if(!module.is.indeterminate() && module.is.checked()) {
+              module.debug('Input is already checked, skipping input property change');
+              return;
+            }
+            module.verbose('Setting state to checked', input);
+            $input
+              .prop('indeterminate', false)
+              .prop('checked', true)
+            ;
+            module.trigger.change();
           },
-          display: function() {
-            var
-              title = settings.name + ':',
-              totalTime = 0
+          unchecked: function() {
+            module.verbose('Removing checked class');
+            $module
+              .removeClass(className.indeterminate)
+              .removeClass(className.checked)
             ;
-            time = false;
-            clearTimeout(module.performance.timer);
-            $.each(performance, function(index, data) {
-              totalTime += data['Execution Time'];
-            });
-            title += ' ' + totalTime + 'ms';
-            if(moduleSelector) {
-              title += ' \'' + moduleSelector + '\'';
+            if(!module.is.indeterminate() &&  module.is.unchecked() ) {
+              module.debug('Input is already unchecked');
+              return;
             }
-            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-              console.groupCollapsed(title);
-              if(console.table) {
-                console.table(performance);
-              }
-              else {
-                $.each(performance, function(index, data) {
-                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-                });
-              }
-              console.groupEnd();
+            module.debug('Setting state to unchecked');
+            $input
+              .prop('indeterminate', false)
+              .prop('checked', false)
+            ;
+            module.trigger.change();
+          },
+          indeterminate: function() {
+            module.verbose('Setting class to indeterminate');
+            $module
+              .addClass(className.indeterminate)
+            ;
+            if( module.is.indeterminate() ) {
+              module.debug('Input is already indeterminate, skipping input property change');
+              return;
+            }
+            module.debug('Setting state to indeterminate');
+            $input
+              .prop('indeterminate', true)
+            ;
+            module.trigger.change();
+          },
+          determinate: function() {
+            module.verbose('Removing indeterminate class');
+            $module
+              .removeClass(className.indeterminate)
+            ;
+            if( module.is.determinate() ) {
+              module.debug('Input is already determinate, skipping input property change');
+              return;
+            }
+            module.debug('Setting state to determinate');
+            $input
+              .prop('indeterminate', false)
+            ;
+          },
+          disabled: function() {
+            module.verbose('Setting class to disabled');
+            $module
+              .addClass(className.disabled)
+            ;
+            if( module.is.disabled() ) {
+              module.debug('Input is already disabled, skipping input property change');
+              return;
+            }
+            module.debug('Setting state to disabled');
+            $input
+              .prop('disabled', 'disabled')
+            ;
+            module.trigger.change();
+          },
+          enabled: function() {
+            module.verbose('Removing disabled class');
+            $module.removeClass(className.disabled);
+            if( module.is.enabled() ) {
+              module.debug('Input is already enabled, skipping input property change');
+              return;
+            }
+            module.debug('Setting state to enabled');
+            $input
+              .prop('disabled', false)
+            ;
+            module.trigger.change();
+          },
+          tabbable: function() {
+            module.verbose('Adding tabindex to checkbox');
+            if( $input.attr('tabindex') === undefined) {
+              $input.attr('tabindex', 0);
             }
-            performance = [];
-          }
-        },
-        invoke: function(query, passedArguments, context) {
-          var
-            object = instance,
-            maxDepth,
-            found,
-            response
-          ;
-          passedArguments = passedArguments || queryArguments;
-          context         = element         || context;
-          if(typeof query == 'string' && object !== undefined) {
-            query    = query.split(/[\. ]/);
-            maxDepth = query.length - 1;
-            $.each(query, function(depth, value) {
-              var camelCaseValue = (depth != maxDepth)
-                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-                : query
-              ;
-              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-                object = object[camelCaseValue];
-              }
-              else if( object[camelCaseValue] !== undefined ) {
-                found = object[camelCaseValue];
-                return false;
-              }
-              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-                object = object[value];
-              }
-              else if( object[value] !== undefined ) {
-                found = object[value];
-                return false;
-              }
-              else {
-                module.error(error.method, query);
-                return false;
-              }
-            });
-          }
-          if ( $.isFunction( found ) ) {
-            response = found.apply(context, passedArguments);
-          }
-          else if(found !== undefined) {
-            response = found;
-          }
-          if($.isArray(returnedValue)) {
-            returnedValue.push(response);
-          }
-          else if(returnedValue !== undefined) {
-            returnedValue = [returnedValue, response];
-          }
-          else if(response !== undefined) {
-            returnedValue = response;
           }
-          return found;
-        }
-      };
-      if(methodInvoked) {
-        if(instance === undefined) {
-          module.initialize();
-        }
-        module.invoke(query);
-      }
-      else {
-        if(instance !== undefined) {
-          instance.invoke('destroy');
-        }
-        module.initialize();
-      }
-    })
-  ;
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.fn.accordion.settings = {
-
-  name            : 'Accordion',
-  namespace       : 'accordion',
-
-  silent          : false,
-  debug           : false,
-  verbose         : false,
-  performance     : true,
-
-  on              : 'click', // event on title that opens accordion
-
-  observeChanges  : true,  // whether accordion should automatically refresh on DOM insertion
-
-  exclusive       : true,  // whether a single accordion content panel should be open at once
-  collapsible     : true,  // whether accordion content can be closed
-  closeNested     : false, // whether nested content should be closed when a panel is closed
-  animateChildren : true,  // whether children opacity should be animated
-
-  duration        : 350, // duration of animation
-  easing          : 'easeOutQuad', // easing equation for animation
-
-
-  onOpening       : function(){}, // callback before open animation
-  onOpen          : function(){}, // callback after open animation
-  onClosing       : function(){}, // callback before closing animation
-  onClose         : function(){}, // callback after closing animation
-  onChange        : function(){}, // callback after closing or opening animation
-
-  error: {
-    method : 'The method you called is not defined'
-  },
-
-  className   : {
-    active    : 'active',
-    animating : 'animating'
-  },
-
-  selector    : {
-    accordion : '.accordion',
-    title     : '.title',
-    trigger   : '.title',
-    content   : '.content'
-  }
-
-};
-
-// Adds easing
-$.extend( $.easing, {
-  easeOutQuad: function (x, t, b, c, d) {
-    return -c *(t/=d)*(t-2) + b;
-  }
-});
-
-})( jQuery, window, document );
-
-
-/*!
- * # Semantic UI undefined - Checkbox
- * http://github.com/semantic-org/semantic-ui/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-"use strict";
-
-window = (typeof window != 'undefined' && window.Math == Math)
-  ? window
-  : (typeof self != 'undefined' && self.Math == Math)
-    ? self
-    : Function('return this')()
-;
+        },
 
-$.fn.checkbox = function(parameters) {
-  var
-    $allModules    = $(this),
-    moduleSelector = $allModules.selector || '',
+        remove: {
+          initialLoad: function() {
+            initialLoad = false;
+          }
+        },
 
-    time           = new Date().getTime(),
-    performance    = [],
+        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);
+            }
+          }
+        },
 
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-    returnedValue
-  ;
 
-  $allModules
-    .each(function() {
-      var
-        settings        = $.extend(true, {}, $.fn.checkbox.settings, parameters),
+        create: {
+          label: function() {
+            if($input.prevAll(selector.label).length > 0) {
+              $input.prev(selector.label).detach().insertAfter($input);
+              module.debug('Moving existing label', $label);
+            }
+            else if( !module.has.label() ) {
+              $label = $('<label>').insertAfter($input);
+              module.debug('Creating label', $label);
+            }
+          }
+        },
 
-        className       = settings.className,
-        namespace       = settings.namespace,
-        selector        = settings.selector,
-        error           = settings.error,
+        has: {
+          label: function() {
+            return ($label.length > 0);
+          }
+        },
 
-        eventNamespace  = '.' + namespace,
-        moduleNamespace = 'module-' + namespace,
+        bind: {
+          events: function() {
+            module.verbose('Attaching checkbox events');
+            $module
+              .on('click'   + eventNamespace, module.event.click)
+              .on('keydown' + eventNamespace, selector.input, module.event.keydown)
+              .on('keyup'   + eventNamespace, selector.input, module.event.keyup)
+            ;
+          }
+        },
 
-        $module         = $(this),
-        $label          = $(this).children(selector.label),
-        $input          = $(this).children(selector.input),
-        input           = $input[0],
+        unbind: {
+          events: function() {
+            module.debug('Removing events');
+            $module
+              .off(eventNamespace)
+            ;
+          }
+        },
 
-        initialLoad     = false,
-        shortcutPressed = false,
-        instance        = $module.data(moduleNamespace),
+        uncheckOthers: function() {
+          var
+            $radios = module.get.otherRadios()
+          ;
+          module.debug('Unchecking other radios', $radios);
+          $radios.removeClass(className.checked);
+        },
 
-        observer,
-        element         = this,
-        module
-      ;
-
-      module      = {
-
-        initialize: function() {
-          module.verbose('Initializing checkbox', settings);
-
-          module.create.label();
-          module.bind.events();
-
-          module.set.tabbable();
-          module.hide.input();
-
-          module.observeChanges();
-          module.instantiate();
-          module.setup();
-        },
-
-        instantiate: function() {
-          module.verbose('Storing instance of module', module);
-          instance = module;
-          $module
-            .data(moduleNamespace, module)
-          ;
-        },
-
-        destroy: function() {
-          module.verbose('Destroying module');
-          module.unbind.events();
-          module.show.input();
-          $module.removeData(moduleNamespace);
-        },
-
-        fix: {
-          reference: function() {
-            if( $module.is(selector.input) ) {
-              module.debug('Behavior called on <input> adjusting invoked element');
-              $module = $module.closest(selector.checkbox);
-              module.refresh();
+        toggle: function() {
+          if( !module.can.change() ) {
+            if(!module.is.radio()) {
+              module.debug('Checkbox is read-only or disabled, ignoring toggle');
             }
+            return;
           }
-        },
-
-        setup: function() {
-          module.set.initialLoad();
-          if( module.is.indeterminate() ) {
-            module.debug('Initial value is indeterminate');
-            module.indeterminate();
-          }
-          else if( module.is.checked() ) {
-            module.debug('Initial value is checked');
+          if( module.is.indeterminate() || module.is.unchecked() ) {
+            module.debug('Currently unchecked');
             module.check();
           }
-          else {
-            module.debug('Initial value is unchecked');
+          else if( module.is.checked() && module.can.uncheck() ) {
+            module.debug('Currently checked');
             module.uncheck();
           }
-          module.remove.initialLoad();
-        },
-
-        refresh: function() {
-          $label = $module.children(selector.label);
-          $input = $module.children(selector.input);
-          input  = $input[0];
         },
-
-        hide: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be unselectable');
-            $input.addClass(className.hidden);
+        setting: function(name, value) {
+          module.debug('Changing setting', name, value);
+          if( $.isPlainObject(name) ) {
+            $.extend(true, settings, name);
           }
-        },
-        show: {
-          input: function() {
-            module.verbose('Modifying <input> z-index to be selectable');
-            $input.removeClass(className.hidden);
+          else if(value !== undefined) {
+            if($.isPlainObject(settings[name])) {
+              $.extend(true, settings[name], value);
+            }
+            else {
+              settings[name] = value;
+            }
           }
-        },
-
-        observeChanges: function() {
-          if('MutationObserver' in window) {
-            observer = new MutationObserver(function(mutations) {
-              module.debug('DOM tree modified, updating selector cache');
-              module.refresh();
-            });
-            observer.observe(element, {
-              childList : true,
-              subtree   : true
-            });
-            module.debug('Setting up mutation observer', observer);
+          else {
+            return settings[name];
           }
         },
-
-        attachEvents: function(selector, event) {
-          var
-            $element = $(selector)
-          ;
-          event = $.isFunction(module[event])
-            ? module[event]
-            : module.toggle
-          ;
-          if($element.length > 0) {
-            module.debug('Attaching checkbox events to element', selector, event);
-            $element
-              .on('click' + eventNamespace, event)
-            ;
+        internal: function(name, value) {
+          if( $.isPlainObject(name) ) {
+            $.extend(true, module, name);
+          }
+          else if(value !== undefined) {
+            module[name] = value;
           }
           else {
-            module.error(error.notFound);
+            return module[name];
           }
         },
-
-        event: {
-          click: function(event) {
-            var
-              $target = $(event.target)
-            ;
-            if( $target.is(selector.input) ) {
-              module.verbose('Using default check action on initialized checkbox');
-              return;
-            }
-            if( $target.is(selector.link) ) {
-              module.debug('Clicking link inside checkbox, skipping toggle');
-              return;
+        debug: function() {
+          if(!settings.silent && settings.debug) {
+            if(settings.performance) {
+              module.performance.log(arguments);
             }
-            module.toggle();
-            $input.focus();
-            event.preventDefault();
-          },
-          keydown: function(event) {
-            var
-              key     = event.which,
-              keyCode = {
-                enter  : 13,
-                space  : 32,
-                escape : 27
-              }
-            ;
-            if(key == keyCode.escape) {
-              module.verbose('Escape key pressed blurring field');
-              $input.blur();
-              shortcutPressed = true;
+            else {
+              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+              module.debug.apply(console, arguments);
             }
-            else if(!event.ctrlKey && ( key == keyCode.space || key == keyCode.enter) ) {
-              module.verbose('Enter/space key pressed, toggling checkbox');
-              module.toggle();
-              shortcutPressed = true;
+          }
+        },
+        verbose: function() {
+          if(!settings.silent && settings.verbose && settings.debug) {
+            if(settings.performance) {
+              module.performance.log(arguments);
             }
             else {
-              shortcutPressed = false;
-            }
-          },
-          keyup: function(event) {
-            if(shortcutPressed) {
-              event.preventDefault();
+              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+              module.verbose.apply(console, arguments);
             }
           }
         },
-
-        check: function() {
-          if( !module.should.allowCheck() ) {
-            return;
-          }
-          module.debug('Checking checkbox', $input);
-          module.set.checked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onChecked.call(input);
-            settings.onChange.call(input);
+        error: function() {
+          if(!settings.silent) {
+            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+            module.error.apply(console, arguments);
           }
         },
-
-        uncheck: function() {
-          if( !module.should.allowUncheck() ) {
-            return;
-          }
-          module.debug('Unchecking checkbox');
-          module.set.unchecked();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onUnchecked.call(input);
-            settings.onChange.call(input);
+        performance: {
+          log: function(message) {
+            var
+              currentTime,
+              executionTime,
+              previousTime
+            ;
+            if(settings.performance) {
+              currentTime   = new Date().getTime();
+              previousTime  = time || currentTime;
+              executionTime = currentTime - previousTime;
+              time          = currentTime;
+              performance.push({
+                'Name'           : message[0],
+                'Arguments'      : [].slice.call(message, 1) || '',
+                'Element'        : element,
+                'Execution Time' : executionTime
+              });
+            }
+            clearTimeout(module.performance.timer);
+            module.performance.timer = setTimeout(module.performance.display, 500);
+          },
+          display: function() {
+            var
+              title = settings.name + ':',
+              totalTime = 0
+            ;
+            time = false;
+            clearTimeout(module.performance.timer);
+            $.each(performance, function(index, data) {
+              totalTime += data['Execution Time'];
+            });
+            title += ' ' + totalTime + 'ms';
+            if(moduleSelector) {
+              title += ' \'' + moduleSelector + '\'';
+            }
+            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+              console.groupCollapsed(title);
+              if(console.table) {
+                console.table(performance);
+              }
+              else {
+                $.each(performance, function(index, data) {
+                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+                });
+              }
+              console.groupEnd();
+            }
+            performance = [];
           }
         },
-
-        indeterminate: function() {
-          if( module.should.allowIndeterminate() ) {
-            module.debug('Checkbox is already indeterminate');
-            return;
+        invoke: function(query, passedArguments, context) {
+          var
+            object = instance,
+            maxDepth,
+            found,
+            response
+          ;
+          passedArguments = passedArguments || queryArguments;
+          context         = element         || context;
+          if(typeof query == 'string' && object !== undefined) {
+            query    = query.split(/[\. ]/);
+            maxDepth = query.length - 1;
+            $.each(query, function(depth, value) {
+              var camelCaseValue = (depth != maxDepth)
+                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+                : query
+              ;
+              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+                object = object[camelCaseValue];
+              }
+              else if( object[camelCaseValue] !== undefined ) {
+                found = object[camelCaseValue];
+                return false;
+              }
+              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+                object = object[value];
+              }
+              else if( object[value] !== undefined ) {
+                found = object[value];
+                return false;
+              }
+              else {
+                module.error(error.method, query);
+                return false;
+              }
+            });
           }
-          module.debug('Making checkbox indeterminate');
-          module.set.indeterminate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onIndeterminate.call(input);
-            settings.onChange.call(input);
+          if ( $.isFunction( found ) ) {
+            response = found.apply(context, passedArguments);
           }
-        },
-
-        determinate: function() {
-          if( module.should.allowDeterminate() ) {
-            module.debug('Checkbox is already determinate');
-            return;
+          else if(found !== undefined) {
+            response = found;
           }
-          module.debug('Making checkbox determinate');
-          module.set.determinate();
-          if( !module.should.ignoreCallbacks() ) {
-            settings.onDeterminate.call(input);
-            settings.onChange.call(input);
+          if($.isArray(returnedValue)) {
+            returnedValue.push(response);
           }
-        },
-
-        enable: function() {
-          if( module.is.enabled() ) {
-            module.debug('Checkbox is already enabled');
-            return;
+          else if(returnedValue !== undefined) {
+            returnedValue = [returnedValue, response];
           }
-          module.debug('Enabling checkbox');
-          module.set.enabled();
-          settings.onEnable.call(input);
-          // preserve legacy callbacks
-          settings.onEnabled.call(input);
-        },
-
-        disable: function() {
-          if( module.is.disabled() ) {
-            module.debug('Checkbox is already disabled');
-            return;
+          else if(response !== undefined) {
+            returnedValue = response;
           }
-          module.debug('Disabling checkbox');
-          module.set.disabled();
-          settings.onDisable.call(input);
-          // preserve legacy callbacks
-          settings.onDisabled.call(input);
-        },
+          return found;
+        }
+      };
 
-        get: {
-          radios: function() {
-            var
-              name = module.get.name()
-            ;
-            return $('input[name="' + name + '"]').closest(selector.checkbox);
-          },
-          otherRadios: function() {
-            return module.get.radios().not($module);
-          },
-          name: function() {
-            return $input.attr('name');
-          }
-        },
+      if(methodInvoked) {
+        if(instance === undefined) {
+          module.initialize();
+        }
+        module.invoke(query);
+      }
+      else {
+        if(instance !== undefined) {
+          instance.invoke('destroy');
+        }
+        module.initialize();
+      }
+    })
+  ;
 
-        is: {
-          initialLoad: function() {
-            return initialLoad;
-          },
-          radio: function() {
-            return ($input.hasClass(className.radio) || $input.attr('type') == 'radio');
-          },
-          indeterminate: function() {
-            return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate');
-          },
-          checked: function() {
-            return $input.prop('checked') !== undefined && $input.prop('checked');
-          },
-          disabled: function() {
-            return $input.prop('disabled') !== undefined && $input.prop('disabled');
-          },
-          enabled: function() {
-            return !module.is.disabled();
-          },
-          determinate: function() {
-            return !module.is.indeterminate();
-          },
-          unchecked: function() {
-            return !module.is.checked();
-          }
-        },
+  return (returnedValue !== undefined)
+    ? returnedValue
+    : this
+  ;
+};
 
-        should: {
-          allowCheck: function() {
-            if(module.is.determinate() && module.is.checked() && !module.should.forceCallbacks() ) {
-              module.debug('Should not allow check, checkbox is already checked');
-              return false;
-            }
-            if(settings.beforeChecked.apply(input) === false) {
-              module.debug('Should not allow check, beforeChecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowUncheck: function() {
-            if(module.is.determinate() && module.is.unchecked() && !module.should.forceCallbacks() ) {
-              module.debug('Should not allow uncheck, checkbox is already unchecked');
-              return false;
-            }
-            if(settings.beforeUnchecked.apply(input) === false) {
-              module.debug('Should not allow uncheck, beforeUnchecked cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowIndeterminate: function() {
-            if(module.is.indeterminate() && !module.should.forceCallbacks() ) {
-              module.debug('Should not allow indeterminate, checkbox is already indeterminate');
-              return false;
-            }
-            if(settings.beforeIndeterminate.apply(input) === false) {
-              module.debug('Should not allow indeterminate, beforeIndeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          allowDeterminate: function() {
-            if(module.is.determinate() && !module.should.forceCallbacks() ) {
-              module.debug('Should not allow determinate, checkbox is already determinate');
-              return false;
-            }
-            if(settings.beforeDeterminate.apply(input) === false) {
-              module.debug('Should not allow determinate, beforeDeterminate cancelled');
-              return false;
-            }
-            return true;
-          },
-          forceCallbacks: function() {
-            return (module.is.initialLoad() && settings.fireOnInit);
-          },
-          ignoreCallbacks: function() {
-            return (initialLoad && !settings.fireOnInit);
-          }
-        },
+$.fn.checkbox.settings = {
 
-        can: {
-          change: function() {
-            return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') );
-          },
-          uncheck: function() {
-            return (typeof settings.uncheckable === 'boolean')
-              ? settings.uncheckable
-              : !module.is.radio()
-            ;
-          }
-        },
+  name                : 'Checkbox',
+  namespace           : 'checkbox',
 
-        set: {
-          initialLoad: function() {
-            initialLoad = true;
-          },
-          checked: function() {
-            module.verbose('Setting class to checked');
-            $module
-              .removeClass(className.indeterminate)
-              .addClass(className.checked)
-            ;
-            if( module.is.radio() ) {
-              module.uncheckOthers();
-            }
-            if(!module.is.indeterminate() && module.is.checked()) {
-              module.debug('Input is already checked, skipping input property change');
-              return;
-            }
-            module.verbose('Setting state to checked', input);
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', true)
-            ;
-            module.trigger.change();
-          },
-          unchecked: function() {
-            module.verbose('Removing checked class');
-            $module
-              .removeClass(className.indeterminate)
-              .removeClass(className.checked)
-            ;
-            if(!module.is.indeterminate() &&  module.is.unchecked() ) {
-              module.debug('Input is already unchecked');
-              return;
-            }
-            module.debug('Setting state to unchecked');
-            $input
-              .prop('indeterminate', false)
-              .prop('checked', false)
-            ;
-            module.trigger.change();
-          },
-          indeterminate: function() {
-            module.verbose('Setting class to indeterminate');
-            $module
-              .addClass(className.indeterminate)
-            ;
-            if( module.is.indeterminate() ) {
-              module.debug('Input is already indeterminate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to indeterminate');
-            $input
-              .prop('indeterminate', true)
-            ;
-            module.trigger.change();
-          },
-          determinate: function() {
-            module.verbose('Removing indeterminate class');
-            $module
-              .removeClass(className.indeterminate)
-            ;
-            if( module.is.determinate() ) {
-              module.debug('Input is already determinate, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to determinate');
-            $input
-              .prop('indeterminate', false)
-            ;
-          },
-          disabled: function() {
-            module.verbose('Setting class to disabled');
-            $module
-              .addClass(className.disabled)
-            ;
-            if( module.is.disabled() ) {
-              module.debug('Input is already disabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to disabled');
-            $input
-              .prop('disabled', 'disabled')
-            ;
-            module.trigger.change();
-          },
-          enabled: function() {
-            module.verbose('Removing disabled class');
-            $module.removeClass(className.disabled);
-            if( module.is.enabled() ) {
-              module.debug('Input is already enabled, skipping input property change');
-              return;
-            }
-            module.debug('Setting state to enabled');
-            $input
-              .prop('disabled', false)
-            ;
-            module.trigger.change();
-          },
-          tabbable: function() {
-            module.verbose('Adding tabindex to checkbox');
-            if( $input.attr('tabindex') === undefined) {
-              $input.attr('tabindex', 0);
-            }
-          }
-        },
+  silent              : false,
+  debug               : false,
+  verbose             : true,
+  performance         : true,
 
-        remove: {
-          initialLoad: function() {
-            initialLoad = false;
-          }
-        },
+  // delegated event context
+  uncheckable         : 'auto',
+  fireOnInit          : false,
 
-        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);
-            }
-          }
-        },
+  onChange            : function(){},
 
+  beforeChecked       : function(){},
+  beforeUnchecked     : function(){},
+  beforeDeterminate   : function(){},
+  beforeIndeterminate : function(){},
 
-        create: {
-          label: function() {
-            if($input.prevAll(selector.label).length > 0) {
-              $input.prev(selector.label).detach().insertAfter($input);
-              module.debug('Moving existing label', $label);
-            }
-            else if( !module.has.label() ) {
-              $label = $('<label>').insertAfter($input);
-              module.debug('Creating label', $label);
-            }
-          }
-        },
+  onChecked           : function(){},
+  onUnchecked         : function(){},
 
-        has: {
-          label: function() {
-            return ($label.length > 0);
-          }
-        },
+  onDeterminate       : function() {},
+  onIndeterminate     : function() {},
 
-        bind: {
-          events: function() {
-            module.verbose('Attaching checkbox events');
-            $module
-              .on('click'   + eventNamespace, module.event.click)
-              .on('keydown' + eventNamespace, selector.input, module.event.keydown)
-              .on('keyup'   + eventNamespace, selector.input, module.event.keyup)
-            ;
-          }
-        },
+  onEnable            : function(){},
+  onDisable           : function(){},
 
-        unbind: {
-          events: function() {
-            module.debug('Removing events');
-            $module
-              .off(eventNamespace)
-            ;
-          }
-        },
+  // preserve misspelled callbacks (will be removed in 3.0)
+  onEnabled           : function(){},
+  onDisabled          : function(){},
 
-        uncheckOthers: function() {
-          var
-            $radios = module.get.otherRadios()
-          ;
-          module.debug('Unchecking other radios', $radios);
-          $radios.removeClass(className.checked);
-        },
-
-        toggle: function() {
-          if( !module.can.change() ) {
-            if(!module.is.radio()) {
-              module.debug('Checkbox is read-only or disabled, ignoring toggle');
-            }
-            return;
-          }
-          if( module.is.indeterminate() || module.is.unchecked() ) {
-            module.debug('Currently unchecked');
-            module.check();
-          }
-          else if( module.is.checked() && module.can.uncheck() ) {
-            module.debug('Currently checked');
-            module.uncheck();
-          }
-        },
-        setting: function(name, value) {
-          module.debug('Changing setting', name, value);
-          if( $.isPlainObject(name) ) {
-            $.extend(true, settings, name);
-          }
-          else if(value !== undefined) {
-            if($.isPlainObject(settings[name])) {
-              $.extend(true, settings[name], value);
-            }
-            else {
-              settings[name] = value;
-            }
-          }
-          else {
-            return settings[name];
-          }
-        },
-        internal: function(name, value) {
-          if( $.isPlainObject(name) ) {
-            $.extend(true, module, name);
-          }
-          else if(value !== undefined) {
-            module[name] = value;
-          }
-          else {
-            return module[name];
-          }
-        },
-        debug: function() {
-          if(!settings.silent && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.debug.apply(console, arguments);
-            }
-          }
-        },
-        verbose: function() {
-          if(!settings.silent && settings.verbose && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.verbose.apply(console, arguments);
-            }
-          }
-        },
-        error: function() {
-          if(!settings.silent) {
-            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-            module.error.apply(console, arguments);
-          }
-        },
-        performance: {
-          log: function(message) {
-            var
-              currentTime,
-              executionTime,
-              previousTime
-            ;
-            if(settings.performance) {
-              currentTime   = new Date().getTime();
-              previousTime  = time || currentTime;
-              executionTime = currentTime - previousTime;
-              time          = currentTime;
-              performance.push({
-                'Name'           : message[0],
-                'Arguments'      : [].slice.call(message, 1) || '',
-                'Element'        : element,
-                'Execution Time' : executionTime
-              });
-            }
-            clearTimeout(module.performance.timer);
-            module.performance.timer = setTimeout(module.performance.display, 500);
-          },
-          display: function() {
-            var
-              title = settings.name + ':',
-              totalTime = 0
-            ;
-            time = false;
-            clearTimeout(module.performance.timer);
-            $.each(performance, function(index, data) {
-              totalTime += data['Execution Time'];
-            });
-            title += ' ' + totalTime + 'ms';
-            if(moduleSelector) {
-              title += ' \'' + moduleSelector + '\'';
-            }
-            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-              console.groupCollapsed(title);
-              if(console.table) {
-                console.table(performance);
-              }
-              else {
-                $.each(performance, function(index, data) {
-                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-                });
-              }
-              console.groupEnd();
-            }
-            performance = [];
-          }
-        },
-        invoke: function(query, passedArguments, context) {
-          var
-            object = instance,
-            maxDepth,
-            found,
-            response
-          ;
-          passedArguments = passedArguments || queryArguments;
-          context         = element         || context;
-          if(typeof query == 'string' && object !== undefined) {
-            query    = query.split(/[\. ]/);
-            maxDepth = query.length - 1;
-            $.each(query, function(depth, value) {
-              var camelCaseValue = (depth != maxDepth)
-                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-                : query
-              ;
-              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-                object = object[camelCaseValue];
-              }
-              else if( object[camelCaseValue] !== undefined ) {
-                found = object[camelCaseValue];
-                return false;
-              }
-              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-                object = object[value];
-              }
-              else if( object[value] !== undefined ) {
-                found = object[value];
-                return false;
-              }
-              else {
-                module.error(error.method, query);
-                return false;
-              }
-            });
-          }
-          if ( $.isFunction( found ) ) {
-            response = found.apply(context, passedArguments);
-          }
-          else if(found !== undefined) {
-            response = found;
-          }
-          if($.isArray(returnedValue)) {
-            returnedValue.push(response);
-          }
-          else if(returnedValue !== undefined) {
-            returnedValue = [returnedValue, response];
-          }
-          else if(response !== undefined) {
-            returnedValue = response;
-          }
-          return found;
-        }
-      };
-
-      if(methodInvoked) {
-        if(instance === undefined) {
-          module.initialize();
-        }
-        module.invoke(query);
-      }
-      else {
-        if(instance !== undefined) {
-          instance.invoke('destroy');
-        }
-        module.initialize();
-      }
-    })
-  ;
-
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.fn.checkbox.settings = {
-
-  name                : 'Checkbox',
-  namespace           : 'checkbox',
-
-  silent              : false,
-  debug               : false,
-  verbose             : true,
-  performance         : true,
-
-  // delegated event context
-  uncheckable         : 'auto',
-  fireOnInit          : false,
-
-  onChange            : function(){},
-
-  beforeChecked       : function(){},
-  beforeUnchecked     : function(){},
-  beforeDeterminate   : function(){},
-  beforeIndeterminate : function(){},
-
-  onChecked           : function(){},
-  onUnchecked         : function(){},
-
-  onDeterminate       : function() {},
-  onIndeterminate     : function() {},
-
-  onEnable            : function(){},
-  onDisable           : function(){},
-
-  // preserve misspelled callbacks (will be removed in 3.0)
-  onEnabled           : function(){},
-  onDisabled          : function(){},
-
-  className       : {
-    checked       : 'checked',
-    indeterminate : 'indeterminate',
-    disabled      : 'disabled',
-    hidden        : 'hidden',
-    radio         : 'radio',
-    readOnly      : 'read-only'
-  },
+  className       : {
+    checked       : 'checked',
+    indeterminate : 'indeterminate',
+    disabled      : 'disabled',
+    hidden        : 'hidden',
+    radio         : 'radio',
+    readOnly      : 'read-only'
+  },
 
   error     : {
     method       : 'The method you called is not defined'
@@ -3547,7 +2936,7 @@ $.fn.checkbox.settings = {
 })( jQuery, window, document );
 
 /*!
- * # Semantic UI undefined - Dimmer
+ * # Semantic UI 2.2.10 - Dropdown
  * http://github.com/semantic-org/semantic-ui/
  *
  *
@@ -3567,12651 +2956,3328 @@ window = (typeof window != 'undefined' && window.Math == Math)
     : Function('return this')()
 ;
 
-$.fn.dimmer = function(parameters) {
+$.fn.dropdown = function(parameters) {
   var
-    $allModules     = $(this),
+    $allModules    = $(this),
+    $document      = $(document),
 
-    time            = new Date().getTime(),
-    performance     = [],
+    moduleSelector = $allModules.selector || '',
 
-    query           = arguments[0],
-    methodInvoked   = (typeof query == 'string'),
-    queryArguments  = [].slice.call(arguments, 1),
+    hasTouch       = ('ontouchstart' in document.documentElement),
+    time           = new Date().getTime(),
+    performance    = [],
 
+    query          = arguments[0],
+    methodInvoked  = (typeof query == 'string'),
+    queryArguments = [].slice.call(arguments, 1),
     returnedValue
   ;
 
   $allModules
-    .each(function() {
+    .each(function(elementIndex) {
       var
-        settings        = ( $.isPlainObject(parameters) )
-          ? $.extend(true, {}, $.fn.dimmer.settings, parameters)
-          : $.extend({}, $.fn.dimmer.settings),
+        settings          = ( $.isPlainObject(parameters) )
+          ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
+          : $.extend({}, $.fn.dropdown.settings),
 
-        selector        = settings.selector,
-        namespace       = settings.namespace,
         className       = settings.className,
+        message         = settings.message,
+        fields          = settings.fields,
+        keys            = settings.keys,
+        metadata        = settings.metadata,
+        namespace       = settings.namespace,
+        regExp          = settings.regExp,
+        selector        = settings.selector,
         error           = settings.error,
+        templates       = settings.templates,
 
         eventNamespace  = '.' + namespace,
         moduleNamespace = 'module-' + namespace,
-        moduleSelector  = $allModules.selector || '',
 
-        clickEvent      = ('ontouchstart' in document.documentElement)
-          ? 'touchstart'
-          : 'click',
+        $module         = $(this),
+        $context        = $(settings.context),
+        $text           = $module.find(selector.text),
+        $search         = $module.find(selector.search),
+        $sizer          = $module.find(selector.sizer),
+        $input          = $module.find(selector.input),
+        $icon           = $module.find(selector.icon),
+
+        $combo = ($module.prev().find(selector.text).length > 0)
+          ? $module.prev().find(selector.text)
+          : $module.prev(),
+
+        $menu           = $module.children(selector.menu),
+        $item           = $menu.find(selector.item),
 
-        $module = $(this),
-        $dimmer,
-        $dimmable,
+        activated       = false,
+        itemActivated   = false,
+        internalChange  = false,
+        element         = this,
+        instance        = $module.data(moduleNamespace),
 
-        element   = this,
-        instance  = $module.data(moduleNamespace),
+        initialLoad,
+        pageLostFocus,
+        willRefocus,
+        elementNamespace,
+        id,
+        selectObserver,
+        menuObserver,
         module
       ;
 
       module = {
 
-        preinitialize: function() {
-          if( module.is.dimmer() ) {
+        initialize: function() {
+          module.debug('Initializing dropdown', settings);
 
-            $dimmable = $module.parent();
-            $dimmer   = $module;
+          if( module.is.alreadySetup() ) {
+            module.setup.reference();
           }
           else {
-            $dimmable = $module;
-            if( module.has.dimmer() ) {
-              if(settings.dimmerName) {
-                $dimmer = $dimmable.find(selector.dimmer).filter('.' + settings.dimmerName);
-              }
-              else {
-                $dimmer = $dimmable.find(selector.dimmer);
-              }
-            }
-            else {
-              $dimmer = module.create();
-            }
-            module.set.variation();
-          }
-        },
+            module.setup.layout();
+            module.refreshData();
 
-        initialize: function() {
-          module.debug('Initializing dimmer', settings);
+            module.save.defaults();
+            module.restore.selected();
+
+            module.create.id();
+            module.bind.events();
+
+            module.observeChanges();
+            module.instantiate();
+          }
 
-          module.bind.events();
-          module.set.dimmable();
-          module.instantiate();
         },
 
         instantiate: function() {
-          module.verbose('Storing instance of module', module);
+          module.verbose('Storing instance of dropdown', module);
           instance = module;
           $module
-            .data(moduleNamespace, instance)
+            .data(moduleNamespace, module)
           ;
         },
 
         destroy: function() {
-          module.verbose('Destroying previous module', $dimmer);
-          module.unbind.events();
-          module.remove.variation();
-          $dimmable
+          module.verbose('Destroying previous dropdown', $module);
+          module.remove.tabbable();
+          $module
+            .off(eventNamespace)
+            .removeData(moduleNamespace)
+          ;
+          $menu
             .off(eventNamespace)
           ;
+          $document
+            .off(elementNamespace)
+          ;
+          module.disconnect.menuObserver();
+          module.disconnect.selectObserver();
         },
 
-        bind: {
-          events: function() {
-            if(settings.on == 'hover') {
-              $dimmable
-                .on('mouseenter' + eventNamespace, module.show)
-                .on('mouseleave' + eventNamespace, module.hide)
-              ;
-            }
-            else if(settings.on == 'click') {
-              $dimmable
-                .on(clickEvent + eventNamespace, module.toggle)
-              ;
-            }
-            if( module.is.page() ) {
-              module.debug('Setting as a page dimmer', $dimmable);
-              module.set.pageDimmer();
-            }
+        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();
+          }
+        },
 
-            if( module.is.closable() ) {
-              module.verbose('Adding dimmer close event', $dimmer);
-              $dimmable
-                .on(clickEvent + eventNamespace, selector.dimmer, module.event.click)
-              ;
+        disconnect: {
+          menuObserver: function() {
+            if(menuObserver) {
+              menuObserver.disconnect();
             }
-          }
-        },
-
-        unbind: {
-          events: function() {
-            $module
-              .removeData(moduleNamespace)
-            ;
-            $dimmable
-              .off(eventNamespace)
-            ;
-          }
-        },
-
-        event: {
-          click: function(event) {
-            module.verbose('Determining if event occured on dimmer', event);
-            if( $dimmer.find(event.target).length === 0 || $(event.target).is(selector.content) ) {
-              module.hide();
-              event.stopImmediatePropagation();
+          },
+          selectObserver: function() {
+            if(selectObserver) {
+              selectObserver.disconnect();
             }
           }
         },
-
-        addContent: function(element) {
-          var
-            $content = $(element)
-          ;
-          module.debug('Add content to dimmer', $content);
-          if($content.parent()[0] !== $dimmer[0]) {
-            $content.detach().appendTo($dimmer);
-          }
-        },
-
-        create: function() {
-          var
-            $element = $( settings.template.dimmer() )
-          ;
-          if(settings.dimmerName) {
-            module.debug('Creating named dimmer', settings.dimmerName);
-            $element.addClass(settings.dimmerName);
+        observe: {
+          select: function() {
+            if(module.has.input()) {
+              selectObserver.observe($input[0], {
+                childList : true,
+                subtree   : true
+              });
+            }
+          },
+          menu: function() {
+            if(module.has.menu()) {
+              menuObserver.observe($menu[0], {
+                childList : true,
+                subtree   : true
+              });
+            }
           }
-          $element
-            .appendTo($dimmable)
-          ;
-          return $element;
         },
 
-        show: function(callback) {
-          callback = $.isFunction(callback)
-            ? callback
-            : function(){}
-          ;
-          module.debug('Showing dimmer', $dimmer, settings);
-          if( (!module.is.dimmed() || module.is.animating()) && module.is.enabled() ) {
-            module.animate.show(callback);
-            settings.onShow.call(element);
-            settings.onChange.call(element);
-          }
-          else {
-            module.debug('Dimmer is already shown or disabled');
+        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)
+                  .addClass(className.addition)
+                  .addClass(className.item)
+                ;
+                if(settings.hideAdditions) {
+                  $userChoice.addClass(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 />')
+              .addClass(className.menu)
+              .appendTo($module)
+            ;
+          },
+          sizer: function() {
+            $sizer = $('<span />')
+              .addClass(className.sizer)
+              .insertAfter($search)
+            ;
           }
         },
 
-        hide: function(callback) {
-          callback = $.isFunction(callback)
-            ? callback
-            : function(){}
+        search: function(query) {
+          query = (query !== undefined)
+            ? query
+            : module.get.query()
           ;
-          if( module.is.dimmed() || module.is.animating() ) {
-            module.debug('Hiding dimmer', $dimmer);
-            module.animate.hide(callback);
-            settings.onHide.call(element);
-            settings.onChange.call(element);
-          }
-          else {
-            module.debug('Dimmer is not visible');
-          }
-        },
-
-        toggle: function() {
-          module.verbose('Toggling dimmer visibility', $dimmer);
-          if( !module.is.dimmed() ) {
-            module.show();
+          module.verbose('Searching for query', query);
+          if(module.has.minCharacters(query)) {
+            module.filter(query);
           }
           else {
             module.hide();
           }
         },
 
-        animate: {
-          show: function(callback) {
-            callback = $.isFunction(callback)
-              ? callback
-              : function(){}
+        select: {
+          firstUnfiltered: function() {
+            module.verbose('Selecting first non-filtered element');
+            module.remove.selectedItem();
+            $item
+              .not(selector.unselectable)
+              .not(selector.addition + selector.hidden)
+                .eq(0)
+                .addClass(className.selected)
             ;
-            if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) {
-              if(settings.opacity !== 'auto') {
-                module.set.opacity();
-              }
-              $dimmer
-                .transition({
-                  animation   : settings.transition + ' in',
-                  queue       : false,
-                  duration    : module.get.duration(),
-                  useFailSafe : true,
-                  onStart     : function() {
-                    module.set.dimmed();
-                  },
-                  onComplete  : function() {
-                    module.set.active();
-                    callback();
-                  }
-                })
-              ;
-            }
-            else {
-              module.verbose('Showing dimmer animation with javascript');
-              module.set.dimmed();
-              if(settings.opacity == 'auto') {
-                settings.opacity = 0.8;
-              }
-              $dimmer
-                .stop()
-                .css({
-                  opacity : 0,
-                  width   : '100%',
-                  height  : '100%'
-                })
-                .fadeTo(module.get.duration(), settings.opacity, function() {
-                  $dimmer.removeAttr('style');
-                  module.set.active();
-                  callback();
-                })
-              ;
-            }
           },
-          hide: function(callback) {
-            callback = $.isFunction(callback)
-              ? callback
-              : function(){}
+          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(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) {
-              module.verbose('Hiding dimmer with css');
-              $dimmer
-                .transition({
-                  animation   : settings.transition + ' out',
-                  queue       : false,
-                  duration    : module.get.duration(),
-                  useFailSafe : true,
-                  onStart     : function() {
-                    module.remove.dimmed();
-                  },
-                  onComplete  : function() {
-                    module.remove.active();
-                    callback();
-                  }
-                })
-              ;
+            if(hasNext) {
+              module.verbose('Moving selection to', $nextAvailable);
+              $nextAvailable.addClass(className.selected);
             }
             else {
-              module.verbose('Hiding dimmer with javascript');
-              module.remove.dimmed();
-              $dimmer
-                .stop()
-                .fadeOut(module.get.duration(), function() {
-                  module.remove.active();
-                  $dimmer.removeAttr('style');
-                  callback();
-                })
-              ;
+              module.verbose('Moving selection to', $prevAvailable);
+              $prevAvailable.addClass(className.selected);
             }
           }
         },
 
-        get: {
-          dimmer: function() {
-            return $dimmer;
-          },
-          duration: function() {
-            if(typeof settings.duration == 'object') {
-              if( module.is.active() ) {
-                return settings.duration.hide;
-              }
-              else {
-                return settings.duration.show;
+        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');
+            $module
+              .api(apiSettings)
+            ;
+          },
+          layout: function() {
+            if( $module.is('select') ) {
+              module.setup.select();
+              module.setup.returnedObject();
             }
-            return settings.duration;
-          }
-        },
-
-        has: {
-          dimmer: function() {
-            if(settings.dimmerName) {
-              return ($module.find(selector.dimmer).filter('.' + settings.dimmerName).length > 0);
+            if( !module.has.menu() ) {
+              module.create.menu();
             }
-            else {
-              return ( $module.find(selector.dimmer).length > 0 );
+            if( module.is.search() && !module.has.search() ) {
+              module.verbose('Adding search input');
+              $search = $('<input />')
+                .addClass(className.search)
+                .prop('autocomplete', 'off')
+                .insertBefore($text)
+              ;
             }
-          }
-        },
-
-        is: {
-          active: function() {
-            return $dimmer.hasClass(className.active);
-          },
-          animating: function() {
-            return ( $dimmer.is(':animated') || $dimmer.hasClass(className.animating) );
-          },
-          closable: function() {
-            if(settings.closable == 'auto') {
-              if(settings.on == 'hover') {
-                return false;
-              }
-              return true;
+            if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
+              module.create.sizer();
+            }
+            if(settings.allowTab) {
+              module.set.tabbable();
             }
-            return settings.closable;
-          },
-          dimmer: function() {
-            return $module.hasClass(className.dimmer);
-          },
-          dimmable: function() {
-            return $module.hasClass(className.dimmable);
-          },
-          dimmed: function() {
-            return $dimmable.hasClass(className.dimmed);
-          },
-          disabled: function() {
-            return $dimmable.hasClass(className.disabled);
-          },
-          enabled: function() {
-            return !module.is.disabled();
-          },
-          page: function () {
-            return $dimmable.is('body');
           },
-          pageDimmer: function() {
-            return $dimmer.hasClass(className.pageDimmer);
-          }
-        },
-
-        can: {
-          show: function() {
-            return !$dimmer.hasClass(className.disabled);
-          }
-        },
-
-        set: {
-          opacity: function(opacity) {
+          select: function() {
             var
-              color      = $dimmer.css('background-color'),
-              colorArray = color.split(','),
-              isRGB      = (colorArray && colorArray.length == 3),
-              isRGBA     = (colorArray && colorArray.length == 4)
+              selectValues  = module.get.selectValues()
             ;
-            opacity    = settings.opacity === 0 ? 0 : settings.opacity || opacity;
-            if(isRGB || isRGBA) {
-              colorArray[3] = opacity + ')';
-              color         = colorArray.join(',');
+            module.debug('Dropdown initialized on a select', selectValues);
+            if( $module.is('select') ) {
+              $input = $module;
+            }
+            // see if select is placed correctly already
+            if($input.parent(selector.dropdown).length > 0) {
+              module.debug('UI dropdown already exists. Creating dropdown menu only');
+              $module = $input.closest(selector.dropdown);
+              if( !module.has.menu() ) {
+                module.create.menu();
+              }
+              $menu = $module.children(selector.menu);
+              module.setup.menu(selectValues);
             }
             else {
-              color = 'rgba(0, 0, 0, ' + opacity + ')';
+              module.debug('Creating entire dropdown from select');
+              $module = $('<div />')
+                .attr('class', $input.attr('class') )
+                .addClass(className.selection)
+                .addClass(className.dropdown)
+                .html( templates.dropdown(selectValues) )
+                .insertBefore($input)
+              ;
+              if($input.hasClass(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');
+                $module.addClass(className.disabled);
+              }
+              $input
+                .removeAttr('class')
+                .detach()
+                .prependTo($module)
+              ;
             }
-            module.debug('Setting opacity to', opacity);
-            $dimmer.css('background-color', color);
-          },
-          active: function() {
-            $dimmer.addClass(className.active);
-          },
-          dimmable: function() {
-            $dimmable.addClass(className.dimmable);
-          },
-          dimmed: function() {
-            $dimmable.addClass(className.dimmed);
-          },
-          pageDimmer: function() {
-            $dimmer.addClass(className.pageDimmer);
+            module.refresh();
           },
-          disabled: function() {
-            $dimmer.addClass(className.disabled);
+          menu: function(values) {
+            $menu.html( templates.menu(values, fields));
+            $item = $menu.find(selector.item);
           },
-          variation: function(variation) {
-            variation = variation || settings.variation;
-            if(variation) {
-              $dimmer.addClass(variation);
+          reference: function() {
+            module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
+            // replace module reference
+            $module = $module.parent(selector.dropdown);
+            module.refresh();
+            module.setup.returnedObject();
+            // invoke method in context of current instance
+            if(methodInvoked) {
+              instance = module;
+              module.invoke(query);
             }
+          },
+          returnedObject: function() {
+            var
+              $firstModules = $allModules.slice(0, elementIndex),
+              $lastModules = $allModules.slice(elementIndex + 1)
+            ;
+            // adjust all modules to use correct reference
+            $allModules = $firstModules.add($module).add($lastModules);
           }
         },
 
-        remove: {
-          active: function() {
-            $dimmer
-              .removeClass(className.active)
-            ;
-          },
-          dimmed: function() {
-            $dimmable.removeClass(className.dimmed);
-          },
-          disabled: function() {
-            $dimmer.removeClass(className.disabled);
-          },
-          variation: function(variation) {
-            variation = variation || settings.variation;
-            if(variation) {
-              $dimmer.removeClass(variation);
-            }
-          }
+        refresh: function() {
+          module.refreshSelectors();
+          module.refreshData();
         },
 
-        setting: function(name, value) {
-          module.debug('Changing setting', name, value);
-          if( $.isPlainObject(name) ) {
-            $.extend(true, settings, name);
-          }
-          else if(value !== undefined) {
-            if($.isPlainObject(settings[name])) {
-              $.extend(true, settings[name], value);
-            }
-            else {
-              settings[name] = value;
-            }
-          }
-          else {
-            return settings[name];
-          }
+        refreshItems: function() {
+          $item = $menu.find(selector.item);
         },
-        internal: function(name, value) {
-          if( $.isPlainObject(name) ) {
-            $.extend(true, module, name);
-          }
-          else if(value !== undefined) {
-            module[name] = value;
+
+        refreshSelectors: function() {
+          module.verbose('Refreshing selector cache');
+          $text   = $module.find(selector.text);
+          $search = $module.find(selector.search);
+          $input  = $module.find(selector.input);
+          $icon   = $module.find(selector.icon);
+          $combo  = ($module.prev().find(selector.text).length > 0)
+            ? $module.prev().find(selector.text)
+            : $module.prev()
+          ;
+          $menu    = $module.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)
+          ;
+          $module
+            .removeData(metadata.defaultText)
+            .removeData(metadata.defaultValue)
+            .removeData(metadata.placeholderText)
+          ;
+        },
+
+        toggle: function() {
+          module.verbose('Toggling menu visibility');
+          if( !module.is.active() ) {
+            module.show();
           }
           else {
-            return module[name];
+            module.hide();
           }
         },
-        debug: function() {
-          if(!settings.silent && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
+
+        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();
             }
-            else {
-              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.debug.apply(console, arguments);
+            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);
+              });
             }
           }
         },
-        verbose: function() {
-          if(!settings.silent && settings.verbose && settings.debug) {
-            if(settings.performance) {
-              module.performance.log(arguments);
-            }
-            else {
-              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
-              module.verbose.apply(console, arguments);
+
+        hide: function(callback) {
+          callback = $.isFunction(callback)
+            ? callback
+            : function(){}
+          ;
+          if( module.is.active() ) {
+            module.debug('Hiding dropdown');
+            if(settings.onHide.call(element) !== false) {
+              module.animate.hide(function() {
+                module.remove.visible();
+                callback.call(element);
+              });
             }
           }
         },
-        error: function() {
-          if(!settings.silent) {
-            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
-            module.error.apply(console, arguments);
-          }
+
+        hideOthers: function() {
+          module.verbose('Finding other dropdowns to hide');
+          $allModules
+            .not($module)
+              .has(selector.menu + '.' + className.visible)
+                .dropdown('hide')
+          ;
         },
-        performance: {
-          log: function(message) {
-            var
-              currentTime,
-              executionTime,
-              previousTime
-            ;
-            if(settings.performance) {
-              currentTime   = new Date().getTime();
-              previousTime  = time || currentTime;
-              executionTime = currentTime - previousTime;
-              time          = currentTime;
-              performance.push({
-                'Name'           : message[0],
-                'Arguments'      : [].slice.call(message, 1) || '',
-                'Element'        : element,
-                'Execution Time' : executionTime
-              });
-            }
-            clearTimeout(module.performance.timer);
-            module.performance.timer = setTimeout(module.performance.display, 500);
-          },
-          display: function() {
-            var
-              title = settings.name + ':',
-              totalTime = 0
-            ;
-            time = false;
-            clearTimeout(module.performance.timer);
-            $.each(performance, function(index, data) {
-              totalTime += data['Execution Time'];
-            });
-            title += ' ' + totalTime + 'ms';
-            if(moduleSelector) {
-              title += ' \'' + moduleSelector + '\'';
+
+        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();
             }
-            if($allModules.length > 1) {
-              title += ' ' + '(' + $allModules.length + ')';
+            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
             }
-            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
-              console.groupCollapsed(title);
-              if(console.table) {
-                console.table(performance);
+            else if( module.is.single() ) {
+              $module
+                .on('touchstart' + eventNamespace, module.event.test.toggle)
+              ;
+            }
+            $menu
+              .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
+            ;
+          },
+          keyboardEvents: function() {
+            module.verbose('Binding keyboard events');
+            $module
+              .on('keydown' + eventNamespace, module.event.keydown)
+            ;
+            if( module.has.search() ) {
+              $module
+                .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');
+            $module
+              .on('change' + eventNamespace, selector.input, module.event.change)
+            ;
+          },
+          mouseEvents: function() {
+            module.verbose('Binding mouse events');
+            if(module.is.multiple()) {
+              $module
+                .on('click'   + eventNamespace, selector.label,  module.event.label.click)
+                .on('click'   + eventNamespace, selector.remove, module.event.remove.click)
+              ;
+            }
+            if( module.is.searchSelection() ) {
+              $module
+                .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()) {
+                $module
+                  .on('click' + eventNamespace, module.event.click)
+                ;
+              }
+            }
+            else {
+              if(settings.on == 'click') {
+                $module
+                  .on('click' + eventNamespace, selector.icon, module.event.icon.click)
+                  .on('click' + eventNamespace, module.event.test.toggle)
+                ;
+              }
+              else if(settings.on == 'hover') {
+                $module
+                  .on('mouseenter' + eventNamespace, module.delay.show)
+                  .on('mouseleave' + eventNamespace, module.delay.hide)
+                ;
               }
               else {
-                $.each(performance, function(index, data) {
-                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
-                });
+                $module
+                  .on(settings.on + eventNamespace, module.toggle)
+                ;
+              }
+              $module
+                .on('mousedown' + eventNamespace, module.event.mousedown)
+                .on('mouseup'   + eventNamespace, module.event.mouseup)
+                .on('focus'     + eventNamespace, module.event.focus)
+              ;
+              if(module.has.menuSearch() ) {
+                $module
+                  .on('blur' + eventNamespace, selector.search, module.event.search.blur)
+                ;
+              }
+              else {
+                $module
+                  .on('blur' + eventNamespace, module.event.blur)
+                ;
               }
-              console.groupEnd();
             }
-            performance = [];
+            $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)
+            ;
           }
         },
-        invoke: function(query, passedArguments, context) {
-          var
-            object = instance,
-            maxDepth,
-            found,
-            response
-          ;
-          passedArguments = passedArguments || queryArguments;
-          context         = element         || context;
-          if(typeof query == 'string' && object !== undefined) {
-            query    = query.split(/[\. ]/);
-            maxDepth = query.length - 1;
-            $.each(query, function(depth, value) {
-              var camelCaseValue = (depth != maxDepth)
-                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
-                : query
+
+        unbind: {
+          intent: function() {
+            module.verbose('Removing hide intent event from document');
+            if(hasTouch) {
+              $document
+                .off('touchstart' + elementNamespace)
+                .off('touchmove' + elementNamespace)
               ;
-              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
-                object = object[camelCaseValue];
+            }
+            $document
+              .off('click' + elementNamespace)
+            ;
+          }
+        },
+
+        filter: function(query) {
+          var
+            searchTerm = (query !== undefined)
+              ? query
+              : module.get.query(),
+            afterFiltered = function() {
+              if(module.is.multiple()) {
+                module.filterActive();
               }
-              else if( object[camelCaseValue] !== undefined ) {
-                found = object[camelCaseValue];
-                return false;
+              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 if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
-                object = object[value];
+              else {
+                module.remove.empty();
+                module.remove.message();
               }
-              else if( object[value] !== undefined ) {
-                found = object[value];
-                return false;
+              if(settings.allowAdditions) {
+                module.add.userSuggestion(query);
               }
-              else {
-                module.error(error.method, query);
-                return false;
+              if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
+                module.show();
               }
-            });
-          }
-          if ( $.isFunction( found ) ) {
-            response = found.apply(context, passedArguments);
-          }
-          else if(found !== undefined) {
-            response = found;
-          }
-          if($.isArray(returnedValue)) {
-            returnedValue.push(response);
+            }
+          ;
+          if(settings.useLabels && module.has.maxSelections()) {
+            return;
           }
-          else if(returnedValue !== undefined) {
-            returnedValue = [returnedValue, response];
+          if(settings.apiSettings) {
+            if( module.can.useAPI() ) {
+              module.queryRemote(searchTerm, function() {
+                if(settings.filterRemoteData) {
+                  module.filterItems(searchTerm);
+                }
+                afterFiltered();
+              });
+            }
+            else {
+              module.error(error.noAPI);
+            }
           }
-          else if(response !== undefined) {
-            returnedValue = response;
+          else {
+            module.filterItems(searchTerm);
+            afterFiltered();
           }
-          return found;
-        }
-      };
-
-      module.preinitialize();
-
-      if(methodInvoked) {
-        if(instance === undefined) {
-          module.initialize();
-        }
-        module.invoke(query);
-      }
-      else {
-        if(instance !== undefined) {
-          instance.invoke('destroy');
-        }
-        module.initialize();
-      }
-    })
-  ;
-
-  return (returnedValue !== undefined)
-    ? returnedValue
-    : this
-  ;
-};
-
-$.fn.dimmer.settings = {
-
-  name        : 'Dimmer',
-  namespace   : 'dimmer',
-
-  silent      : false,
-  debug       : false,
-  verbose     : false,
-  performance : true,
-
-  // name to distinguish between multiple dimmers in context
-  dimmerName  : false,
-
-  // whether to add a variation type
-  variation   : false,
-
-  // whether to bind close events
-  closable    : 'auto',
-
-  // whether to use css animations
-  useCSS      : true,
-
-  // css animation to use
-  transition  : 'fade',
-
-  // event to bind to
-  on          : false,
-
-  // overriding opacity value
-  opacity     : 'auto',
-
-  // transition durations
-  duration    : {
-    show : 500,
-    hide : 500
-  },
-
-  onChange    : function(){},
-  onShow      : function(){},
-  onHide      : function(){},
-
-  error   : {
-    method   : 'The method you called is not defined.'
-  },
-
-  className : {
-    active     : 'active',
-    animating  : 'animating',
-    dimmable   : 'dimmable',
-    dimmed     : 'dimmed',
-    dimmer     : 'dimmer',
-    disabled   : 'disabled',
-    hide       : 'hide',
-    pageDimmer : 'page',
-    show       : 'show'
-  },
-
-  selector: {
-    dimmer   : '> .ui.dimmer',
-    content  : '.ui.dimmer > .content, .ui.dimmer > .content > .center'
-  },
-
-  template: {
-    dimmer: function() {
-     return $('<div />').attr('class', 'ui dimmer');
-    }
-  }
-
-};
-
-})( jQuery, window, document );
-
-/*!
- * # Semantic UI undefined - Dropdown
- * http://github.com/semantic-org/semantic-ui/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-"use strict";
-
-window = (typeof window != 'undefined' && window.Math == Math)
-  ? window
-  : (typeof self != 'undefined' && self.Math == Math)
-    ? self
-    : Function('return this')()
-;
-
-$.fn.dropdown = function(parameters) {
-  var
-    $allModules    = $(this),
-    $document      = $(document),
-
-    moduleSelector = $allModules.selector || '',
-
-    hasTouch       = ('ontouchstart' in document.documentElement),
-    time           = new Date().getTime(),
-    performance    = [],
-
-    query          = arguments[0],
-    methodInvoked  = (typeof query == 'string'),
-    queryArguments = [].slice.call(arguments, 1),
-    returnedValue
-  ;
-
-  $allModules
-    .each(function(elementIndex) {
-      var
-        settings          = ( $.isPlainObject(parameters) )
-          ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
-          : $.extend({}, $.fn.dropdown.settings),
-
-        className       = settings.className,
-        message         = settings.message,
-        fields          = settings.fields,
-        keys            = settings.keys,
-        metadata        = settings.metadata,
-        namespace       = settings.namespace,
-        regExp          = settings.regExp,
-        selector        = settings.selector,
-        error           = settings.error,
-        templates       = settings.templates,
-
-        eventNamespace  = '.' + namespace,
-        moduleNamespace = 'module-' + namespace,
-
-        $module         = $(this),
-        $context        = $(settings.context),
-        $text           = $module.find(selector.text),
-        $search         = $module.find(selector.search),
-        $sizer          = $module.find(selector.sizer),
-        $input          = $module.find(selector.input),
-        $icon           = $module.find(selector.icon),
-
-        $combo = ($module.prev().find(selector.text).length > 0)
-          ? $module.prev().find(selector.text)
-          : $module.prev(),
-
-        $menu           = $module.children(selector.menu),
-        $item           = $menu.find(selector.item),
-
-        activated       = false,
-        itemActivated   = false,
-        internalChange  = false,
-        element         = this,
-        instance        = $module.data(moduleNamespace),
-
-        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();
-            module.refreshData();
-
-            module.save.defaults();
-            module.restore.selected();
-
-            module.create.id();
-            module.bind.events();
-
-            module.observeChanges();
-            module.instantiate();
-          }
-
         },
 
-        instantiate: function() {
-          module.verbose('Storing instance of dropdown', module);
-          instance = module;
-          $module
-            .data(moduleNamespace, module)
+        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();
+              }
+            }
           ;
-        },
-
-        destroy: function() {
-          module.verbose('Destroying previous dropdown', $module);
-          module.remove.tabbable();
+          if( !$module.api('get request') ) {
+            module.setup.api();
+          }
+          apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
           $module
-            .off(eventNamespace)
-            .removeData(moduleNamespace)
-          ;
-          $menu
-            .off(eventNamespace)
-          ;
-          $document
-            .off(elementNamespace)
+            .api('setting', apiSettings)
+            .api('query')
           ;
-          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();
-          }
-        },
+        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 = [];
 
-        disconnect: {
-          menuObserver: function() {
-            if(menuObserver) {
-              menuObserver.disconnect();
-            }
-          },
-          selectObserver: function() {
-            if(selectObserver) {
-              selectObserver.disconnect();
-            }
-          }
-        },
-        observe: {
-          select: function() {
-            if(module.has.input()) {
-              selectObserver.observe($input[0], {
-                childList : true,
-                subtree   : true
-              });
-            }
-          },
-          menu: function() {
-            if(module.has.menu()) {
-              menuObserver.observe($menu[0], {
-                childList : true,
-                subtree   : true
-              });
-            }
+            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)
+              .addClass(className.filtered)
+            ;
           }
         },
 
-        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) {
+        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
-              $userChoices,
-              $userChoice,
-              isUserValue,
-              html
-            ;
-            values = values || module.get.userValues();
-            if(!values) {
-              return false;
-            }
-            values = $.isArray(values)
-              ? values
-              : [values]
+              queryCharacter = query.charCodeAt(characterIndex)
             ;
-            $.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)
-                  .addClass(className.addition)
-                  .addClass(className.item)
-                ;
-                if(settings.hideAdditions) {
-                  $userChoice.addClass(className.hidden);
-                }
-                $userChoices = ($userChoices === undefined)
-                  ? $userChoice
-                  : $userChoices.add($userChoice)
-                ;
-                module.verbose('Creating user choices for value', value, $userChoice);
+            while(nextCharacterIndex < termLength) {
+              if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
+                continue search;
               }
-            });
-            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 />')
-              .addClass(className.menu)
-              .appendTo($module)
-            ;
-          },
-          sizer: function() {
-            $sizer = $('<span />')
-              .addClass(className.sizer)
-              .insertAfter($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)
+              .addClass(className.filtered)
             ;
           }
         },
 
-        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();
+        focusSearch: function(skipHandler) {
+          if( module.has.search() && !module.is.focusedOnSearch() ) {
+            if(skipHandler) {
+              $module.off('focus' + eventNamespace, selector.search);
+              $search.focus();
+              $module.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
+            }
+            else {
+              $search.focus();
+            }
           }
         },
 
-        select: {
-          firstUnfiltered: function() {
-            module.verbose('Selecting first non-filtered element');
-            module.remove.selectedItem();
-            $item
-              .not(selector.unselectable)
-              .not(selector.addition + selector.hidden)
-                .eq(0)
-                .addClass(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.addClass(className.selected);
+        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.verbose('Moving selection to', $prevAvailable);
-              $prevAvailable.addClass(className.selected);
+              module.remove.searchTerm();
             }
           }
         },
 
-        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');
-            $module
-              .api(apiSettings)
-            ;
+        event: {
+          change: function() {
+            if(!internalChange) {
+              module.debug('Input changed, updating selection');
+              module.set.selected();
+            }
           },
-          layout: function() {
-            if( $module.is('select') ) {
-              module.setup.select();
-              module.setup.returnedObject();
+          focus: function() {
+            if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
+              module.show();
             }
-            if( !module.has.menu() ) {
-              module.create.menu();
+          },
+          blur: function(event) {
+            pageLostFocus = (document.activeElement === this);
+            if(!activated && !pageLostFocus) {
+              module.remove.activeLabel();
+              module.hide();
             }
-            if( module.is.search() && !module.has.search() ) {
-              module.verbose('Adding search input');
-              $search = $('<input />')
-                .addClass(className.search)
-                .prop('autocomplete', 'off')
-                .insertBefore($text)
-              ;
+          },
+          mousedown: function() {
+            if(module.is.searchSelection()) {
+              // prevent menu hiding on immediate re-focus
+              willRefocus = true;
             }
-            if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
-              module.create.sizer();
+            else {
+              // prevents focus callback from occurring on mousedown
+              activated = true;
             }
-            if(settings.allowTab) {
-              module.set.tabbable();
+          },
+          mouseup: function() {
+            if(module.is.searchSelection()) {
+              // prevent menu hiding on immediate re-focus
+              willRefocus = false;
+            }
+            else {
+              activated = false;
             }
           },
-          select: function() {
+          click: function(event) {
             var
-              selectValues  = module.get.selectValues()
+              $target = $(event.target)
             ;
-            module.debug('Dropdown initialized on a select', selectValues);
-            if( $module.is('select') ) {
-              $input = $module;
-            }
-            // see if select is placed correctly already
-            if($input.parent(selector.dropdown).length > 0) {
-              module.debug('UI dropdown already exists. Creating dropdown menu only');
-              $module = $input.closest(selector.dropdown);
-              if( !module.has.menu() ) {
-                module.create.menu();
+            // focus search
+            if($target.is($module)) {
+              if(!module.is.focusedOnSearch()) {
+                module.focusSearch();
+              }
+              else {
+                module.show();
               }
-              $menu = $module.children(selector.menu);
-              module.setup.menu(selectValues);
             }
-            else {
-              module.debug('Creating entire dropdown from select');
-              $module = $('<div />')
-                .attr('class', $input.attr('class') )
-                .addClass(className.selection)
-                .addClass(className.dropdown)
-                .html( templates.dropdown(selectValues) )
-                .insertBefore($input)
-              ;
-              if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
-                module.error(error.missingMultiple);
-                $input.prop('multiple', true);
+          },
+          search: {
+            focus: function() {
+              activated = true;
+              if(module.is.multiple()) {
+                module.remove.activeLabel();
               }
-              if($input.is('[multiple]')) {
-                module.set.multiple();
+              if(settings.showOnFocus) {
+                module.search();
               }
-              if ($input.prop('disabled')) {
-                module.debug('Disabling dropdown');
-                $module.addClass(className.disabled);
+            },
+            blur: function(event) {
+              pageLostFocus = (document.activeElement === this);
+              if(module.is.searchSelection() && !willRefocus) {
+                if(!itemActivated && !pageLostFocus) {
+                  if(settings.forceSelection) {
+                    module.forceSelection();
+                  }
+                  module.hide();
+                }
               }
-              $input
-                .removeAttr('class')
-                .detach()
-                .prependTo($module)
-              ;
+              willRefocus = false;
             }
-            module.refresh();
           },
-          menu: function(values) {
-            $menu.html( templates.menu(values, fields));
-            $item = $menu.find(selector.item);
+          icon: {
+            click: function(event) {
+              module.toggle();
+            }
           },
-          reference: function() {
-            module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
-            // replace module reference
-            $module = $module.parent(selector.dropdown);
-            module.refresh();
-            module.setup.returnedObject();
-            // invoke method in context of current instance
-            if(methodInvoked) {
-              instance = module;
-              module.invoke(query);
+          text: {
+            focus: function(event) {
+              activated = true;
+              module.focusSearch();
             }
           },
-          returnedObject: function() {
-            var
-              $firstModules = $allModules.slice(0, elementIndex),
-              $lastModules = $allModules.slice(elementIndex + 1)
-            ;
-            // adjust all modules to use correct reference
-            $allModules = $firstModules.add($module).add($lastModules);
-          }
-        },
-
-        refresh: function() {
-          module.refreshSelectors();
-          module.refreshData();
-        },
-
-        refreshItems: function() {
-          $item = $menu.find(selector.item);
-        },
-
-        refreshSelectors: function() {
-          module.verbose('Refreshing selector cache');
-          $text   = $module.find(selector.text);
-          $search = $module.find(selector.search);
-          $input  = $module.find(selector.input);
-          $icon   = $module.find(selector.icon);
-          $combo  = ($module.prev().find(selector.text).length > 0)
-            ? $module.prev().find(selector.text)
-            : $module.prev()
-          ;
-          $menu    = $module.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)
-          ;
-          $module
-            .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();
+          input: function(event) {
+            if(module.is.multiple() || module.is.searchSelection()) {
+              module.set.filtered();
             }
-            if(module.is.allFiltered()) {
-              return true;
+            clearTimeout(module.timer);
+            module.timer = setTimeout(module.search, settings.delay.search);
+          },
+          label: {
+            click: function(event) {
+              var
+                $label        = $(this),
+                $labels       = $module.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.removeClass(className.active);
+                $range.addClass(className.active);
+              }
+              else if(event.ctrlKey) {
+                $label.toggleClass(className.active);
+              }
+              else {
+                $activeLabels.removeClass(className.active);
+                $label.addClass(className.active);
+              }
+              settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
             }
-            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);
-              });
+          },
+          remove: {
+            click: function() {
+              var
+                $label = $(this).parent()
+              ;
+              if( $label.hasClass(className.active) ) {
+                // remove all selected labels
+                module.remove.activeLabels();
+              }
+              else {
+                // remove this label only
+                module.remove.activeLabels( $label );
+              }
             }
-          }
-        },
-
-        hide: function(callback) {
-          callback = $.isFunction(callback)
-            ? callback
-            : function(){}
-          ;
-          if( module.is.active() ) {
-            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($module)
-              .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() ) {
-              $module
-                .on('touchstart' + eventNamespace, module.event.test.toggle)
+          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);
             }
-            $menu
-              .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
-            ;
           },
-          keyboardEvents: function() {
-            module.verbose('Binding keyboard events');
-            $module
-              .on('keydown' + eventNamespace, module.event.keydown)
-            ;
-            if( module.has.search() ) {
-              $module
-                .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
-              ;
-            }
-            if( module.is.multiple() ) {
-              $document
-                .on('keydown' + elementNamespace, module.event.document.keydown)
-              ;
+          select: {
+            mutation: function(mutations) {
+              module.debug('<select> modified, recreating menu');
+              module.setup.select();
             }
           },
-          inputEvents: function() {
-            module.verbose('Binding input change events');
-            $module
-              .on('change' + eventNamespace, selector.input, module.event.change)
-            ;
-          },
-          mouseEvents: function() {
-            module.verbose('Binding mouse events');
-            if(module.is.multiple()) {
-              $module
-                .on('click'   + eventNamespace, selector.label,  module.event.label.click)
-                .on('click'   + eventNamespace, selector.remove, module.event.remove.click)
-              ;
-            }
-            if( module.is.searchSelection() ) {
-              $module
-                .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)
+          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(module.is.multiple()) {
-                $module
-                  .on('click' + eventNamespace, module.event.click)
-                ;
+              if(isUserAddition || isMessage) {
+                module.debug('Updating item selector cache');
+                module.refreshItems();
               }
-            }
-            else {
-              if(settings.on == 'click') {
-                $module
-                  .on('click' + eventNamespace, selector.icon, module.event.icon.click)
-                  .on('click' + eventNamespace, module.event.test.toggle)
-                ;
+              else {
+                module.debug('Menu modified, updating selector cache');
+                module.refresh();
               }
-              else if(settings.on == 'hover') {
-                $module
-                  .on('mouseenter' + eventNamespace, module.delay.show)
-                  .on('mouseleave' + eventNamespace, module.delay.hide)
-                ;
+            },
+            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();
               }
-              else {
-                $module
-                  .on(settings.on + eventNamespace, module.toggle)
-                ;
+            },
+            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);
               }
-              $module
-                .on('mousedown' + eventNamespace, module.event.mousedown)
-                .on('mouseup'   + eventNamespace, module.event.mouseup)
-                .on('focus'     + eventNamespace, module.event.focus)
+            },
+            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)
               ;
-              if(module.has.menuSearch() ) {
-                $module
-                  .on('blur' + eventNamespace, selector.search, module.event.search.blur)
-                ;
+              // prevents IE11 bug where menu receives focus even though `tabindex=-1`
+              if(module.has.menuSearch()) {
+                $(document.activeElement).blur();
               }
-              else {
-                $module
-                  .on('blur' + eventNamespace, module.event.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);
               }
             }
-            $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: {
+            // label selection should occur even when element has no focus
+            keydown: function(event) {
+              var
+                pressedKey    = event.which,
+                isShortcutKey = module.is.inObject(pressedKey, keys)
               ;
-            }
-            $document
-              .off('click' + elementNamespace)
-            ;
-          }
-        },
+              if(isShortcutKey) {
+                var
+                  $label            = $module.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;
+                }
 
-        filter: function(query) {
-          var
-            searchTerm = (query !== undefined)
-              ? query
-              : module.get.query(),
-            afterFiltered = function() {
-              if(module.is.multiple()) {
-                module.filterActive();
-              }
-              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();
+                if(pressedKey == keys.leftArrow) {
+                  // activate previous label
+                  if((isFocused || caretAtStart) && !hasActiveLabel) {
+                    module.verbose('Selecting previous label');
+                    $label.last().addClass(className.active);
+                  }
+                  else if(hasActiveLabel) {
+                    if(!event.shiftKey) {
+                      module.verbose('Selecting previous label');
+                      $label.removeClass(className.active);
+                    }
+                    else {
+                      module.verbose('Adding previous label to selection');
+                    }
+                    if(isFirstLabel && !hasMultipleActive) {
+                      $activeLabel.addClass(className.active);
+                    }
+                    else {
+                      $activeLabel.prev(selector.siblingLabel)
+                        .addClass(className.active)
+                        .end()
+                      ;
                     }
+                    event.preventDefault();
                   }
-                  else {
-                    module.verbose('All items filtered, showing message', searchTerm);
-                    module.add.message(message.noResults);
+                }
+                else if(pressedKey == keys.rightArrow) {
+                  // activate first label
+                  if(isFocused && !hasActiveLabel) {
+                    $label.first().addClass(className.active);
+                  }
+                  // activate next label
+                  if(hasActiveLabel) {
+                    if(!event.shiftKey) {
+                      module.verbose('Selecting next label');
+                      $label.removeClass(className.active);
+                    }
+                    else {
+                      module.verbose('Adding next label to selection');
+                    }
+                    if(isLastLabel) {
+                      if(isSearch) {
+                        if(!isFocusedOnSearch) {
+                          module.focusSearch();
+                        }
+                        else {
+                          $label.removeClass(className.active);
+                        }
+                      }
+                      else if(hasMultipleActive) {
+                        $activeLabel.next(selector.siblingLabel).addClass(className.active);
+                      }
+                      else {
+                        $activeLabel.addClass(className.active);
+                      }
+                    }
+                    else {
+                      $activeLabel.next(selector.siblingLabel).addClass(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).addClass(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().addClass(className.active);
+                    module.remove.activeLabels($activeLabel);
                   }
                 }
                 else {
-                  module.verbose('All items filtered, hiding dropdown', searchTerm);
-                  module.hideMenu();
+                  $activeLabel.removeClass(className.active);
                 }
               }
-              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();
+          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.hasClass(className.visible) || $parentMenu.hasClass(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();
+                }
               }
-            }
-          ;
-          if( !$module.api('get request') ) {
-            module.setup.api();
-          }
-          apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
-          $module
-            .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 = [];
+              // visible menu keyboard shortcuts
+              if( module.is.visible() ) {
 
-            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;
+                // 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 (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
-                    results.push(this);
-                    return true;
+                  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();
+                    }
                   }
-                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
-                    results.push(this);
-                    return true;
+                  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
+                        .removeClass(className.selected)
+                      ;
+                      $parentMenu
+                        .closest(selector.item)
+                          .addClass(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
+                        .removeClass(className.selected)
+                      ;
+                      $subMenu
+                        .find(selector.item).eq(0)
+                          .addClass(className.selected)
+                      ;
+                      event.preventDefault();
+                    }
                   }
                 }
-                if(settings.match == 'both' || settings.match == 'value') {
-                  value = String(module.get.choiceValue($choice, text));
-                  if(value.search(beginsWithRegExp) !== -1) {
-                    results.push(this);
-                    return true;
+
+                // 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 if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
-                    results.push(this);
-                    return true;
+                  else {
+                    module.verbose('Up key pressed, changing active item');
+                    $selectedItem
+                      .removeClass(className.selected)
+                    ;
+                    $nextItem
+                      .addClass(className.selected)
+                    ;
+                    module.set.scrollPosition($nextItem);
+                    if(settings.selectOnKeydown && module.is.single()) {
+                      module.set.selectedItem($nextItem);
+                    }
                   }
-                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
-                    results.push(this);
-                    return true;
+                  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
+                      .removeClass(className.selected)
+                    ;
+                    $nextItem
+                      .addClass(className.selected)
+                    ;
+                    module.set.scrollPosition($nextItem);
+                    if(settings.selectOnKeydown && module.is.single()) {
+                      module.set.selectedItem($nextItem);
+                    }
                   }
+                  event.preventDefault();
                 }
-              })
-            ;
-          }
-          module.debug('Showing only matched items', searchTerm);
-          module.remove.filteredItem();
-          if(results) {
-            $item
-              .not(results)
-              .addClass(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)
-              .addClass(className.filtered)
-            ;
-          }
-        },
+                // page down (show next page)
+                if(pressedKey == keys.pageUp) {
+                  module.scrollPage('up');
+                  event.preventDefault();
+                }
+                if(pressedKey == keys.pageDown) {
+                  module.scrollPage('down');
+                  event.preventDefault();
+                }
 
-        focusSearch: function(skipHandler) {
-          if( module.has.search() && !module.is.focusedOnSearch() ) {
-            if(skipHandler) {
-              $module.off('focus' + eventNamespace, selector.search);
-              $search.focus();
-              $module.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
+                // 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.select.firstUnfiltered();
+                  module.show();
+                  event.preventDefault();
+                }
+              }
             }
             else {
-              $search.focus();
+              if( !module.has.search() ) {
+                module.set.selectedLetter( String.fromCharCode(pressedKey) );
+              }
             }
           }
         },
 
-        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();
+        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);
             }
           }
         },
 
-        event: {
-          change: function() {
-            if(!internalChange) {
-              module.debug('Input changed, updating selection');
-              module.set.selected();
+        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);
             }
-          },
-          focus: function() {
-            if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
-              module.show();
+            else if( $.isFunction(settings.action) ) {
+              module.verbose('Triggering user action', settings.action, text, value);
+              settings.action.call(element, text, value, this);
             }
-          },
-          blur: function(event) {
-            pageLostFocus = (document.activeElement === this);
-            if(!activated && !pageLostFocus) {
-              module.remove.activeLabel();
-              module.hide();
+            else {
+              module.error(error.action, settings.action);
             }
           },
-          mousedown: function() {
-            if(module.is.searchSelection()) {
-              // prevent menu hiding on immediate re-focus
-              willRefocus = true;
+          eventInModule: function(event, callback) {
+            var
+              $target    = $(event.target),
+              inDocument = ($target.closest(document.documentElement).length > 0),
+              inModule   = ($target.closest($module).length > 0)
+            ;
+            callback = $.isFunction(callback)
+              ? callback
+              : function(){}
+            ;
+            if(inDocument && !inModule) {
+              module.verbose('Triggering event', callback);
+              callback();
+              return true;
             }
             else {
-              // prevents focus callback from occurring on mousedown
-              activated = true;
+              module.verbose('Event occurred in dropdown, canceling callback');
+              return false;
             }
           },
-          mouseup: function() {
-            if(module.is.searchSelection()) {
-              // prevent menu hiding on immediate re-focus
-              willRefocus = false;
+          eventOnElement: function(event, callback) {
+            var
+              $target      = $(event.target),
+              $label       = $target.closest(selector.siblingLabel),
+              inVisibleDOM = document.body.contains(event.target),
+              notOnLabel   = ($module.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 {
-              activated = false;
+              module.verbose('Event occurred in dropdown menu, canceling callback');
+              return false;
             }
-          },
-          click: function(event) {
-            var
-              $target = $(event.target)
+          }
+        },
+
+        action: {
+
+          nothing: function() {},
+
+          activate: function(text, value, element) {
+            value = (value !== undefined)
+              ? value
+              : text
             ;
-            // focus search
-            if($target.is($module)) {
-              if(!module.is.focusedOnSearch()) {
-                module.focusSearch();
+            if( module.can.activate( $(element) ) ) {
+              module.set.selected(value, $(element));
+              if(module.is.multiple() && !module.is.allFiltered()) {
+                return;
               }
               else {
-                module.show();
+                module.hideAndClear();
               }
             }
           },
-          search: {
-            focus: function() {
-              activated = true;
-              if(module.is.multiple()) {
-                module.remove.activeLabel();
-              }
-              if(settings.showOnFocus) {
-                module.search();
+
+          select: function(text, value, element) {
+            value = (value !== undefined)
+              ? value
+              : text
+            ;
+            if( module.can.activate( $(element) ) ) {
+              module.set.value(value, $(element));
+              if(module.is.multiple() && !module.is.allFiltered()) {
+                return;
               }
-            },
-            blur: function(event) {
-              pageLostFocus = (document.activeElement === this);
-              if(module.is.searchSelection() && !willRefocus) {
-                if(!itemActivated && !pageLostFocus) {
-                  if(settings.forceSelection) {
-                    module.forceSelection();
-                  }
-                  module.hide();
-                }
+              else {
+                module.hideAndClear();
               }
-              willRefocus = false;
             }
           },
-          icon: {
-            click: function(event) {
-              module.toggle();
-            }
+
+          combo: function(text, value, element) {
+            value = (value !== undefined)
+              ? value
+              : text
+            ;
+            module.set.selected(value, $(element));
+            module.hideAndClear();
           },
-          text: {
-            focus: function(event) {
-              activated = true;
-              module.focusSearch();
-            }
+
+          hide: function(text, value, element) {
+            module.set.value(value, text);
+            module.hideAndClear();
+          }
+
+        },
+
+        get: {
+          id: function() {
+            return id;
           },
-          input: function(event) {
-            if(module.is.multiple() || module.is.searchSelection()) {
-              module.set.filtered();
-            }
-            clearTimeout(module.timer);
-            module.timer = setTimeout(module.search, settings.delay.search);
+          defaultText: function() {
+            return $module.data(metadata.defaultText);
           },
-          label: {
-            click: function(event) {
-              var
-                $label        = $(this),
-                $labels       = $module.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.removeClass(className.active);
-                $range.addClass(className.active);
-              }
-              else if(event.ctrlKey) {
-                $label.toggleClass(className.active);
-              }
-              else {
-                $activeLabels.removeClass(className.active);
-                $label.addClass(className.active);
-              }
-              settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
-            }
+          defaultValue: function() {
+            return $module.data(metadata.defaultValue);
           },
-          remove: {
-            click: function() {
-              var
-                $label = $(this).parent()
-              ;
-              if( $label.hasClass(className.active) ) {
-                // remove all selected labels
-                module.remove.activeLabels();
-              }
-              else {
-                // remove this label only
-                module.remove.activeLabels( $label );
-              }
-            }
+          placeholderText: function() {
+            return $module.data(metadata.placeholderText) || '';
           },
-          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);
-            }
+          text: function() {
+            return $text.text();
           },
-          select: {
-            mutation: function(mutations) {
-              module.debug('<select> modified, recreating menu');
-              module.setup.select();
-            }
+          query: function() {
+            return $.trim($search.val());
           },
-          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;
+          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);
+            });
           },
-          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();
+          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()
+                : $module.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];
               }
-              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);
+              $.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;
                 }
-                if(!settings.useLabels) {
-                  module.remove.filteredItem();
-                  module.set.scrollPosition($choice);
-                }
-                module.determine.selectAction.call(this, text, value);
+              });
+            }
+            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())
+              ;
             }
           },
-
-          document: {
-            // label selection should occur even when element has no focus
-            keydown: function(event) {
-              var
-                pressedKey    = event.which,
-                isShortcutKey = module.is.inObject(pressedKey, keys)
+          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'
               ;
-              if(isShortcutKey) {
-                var
-                  $label            = $module.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().addClass(className.active);
+            }
+            return false;
+          },
+          selectValues: function() {
+            var
+              select = {}
+            ;
+            select.values = [];
+            $module
+              .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 if(hasActiveLabel) {
-                    if(!event.shiftKey) {
-                      module.verbose('Selecting previous label');
-                      $label.removeClass(className.active);
-                    }
-                    else {
-                      module.verbose('Adding previous label to selection');
-                    }
-                    if(isFirstLabel && !hasMultipleActive) {
-                      $activeLabel.addClass(className.active);
-                    }
-                    else {
-                      $activeLabel.prev(selector.siblingLabel)
-                        .addClass(className.active)
-                        .end()
-                      ;
-                    }
-                    event.preventDefault();
+                  else {
+                    select.values.push({
+                      name     : name,
+                      value    : value,
+                      disabled : disabled
+                    });
                   }
-                }
-                else if(pressedKey == keys.rightArrow) {
-                  // activate first label
-                  if(isFocused && !hasActiveLabel) {
-                    $label.first().addClass(className.active);
+                })
+            ;
+            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;
                   }
-                  // activate next label
-                  if(hasActiveLabel) {
-                    if(!event.shiftKey) {
-                      module.verbose('Selecting next label');
-                      $label.removeClass(className.active);
-                    }
-                    else {
-                      module.verbose('Adding next label to selection');
-                    }
-                    if(isLastLabel) {
-                      if(isSearch) {
-                        if(!isFocusedOnSearch) {
-                          module.focusSearch();
-                        }
-                        else {
-                          $label.removeClass(className.active);
-                        }
-                      }
-                      else if(hasMultipleActive) {
-                        $activeLabel.next(selector.siblingLabel).addClass(className.active);
-                      }
-                      else {
-                        $activeLabel.addClass(className.active);
-                      }
-                    }
-                    else {
-                      $activeLabel.next(selector.siblingLabel).addClass(className.active);
+                  if(isMultiple) {
+                    if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) {
+                      $selectedItem = ($selectedItem)
+                        ? $selectedItem.add($choice)
+                        : $choice
+                      ;
                     }
-                    event.preventDefault();
                   }
-                }
-                else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
-                  if(hasActiveLabel) {
-                    module.verbose('Removing active labels');
-                    if(isLastLabel) {
-                      if(isSearch && !isFocusedOnSearch) {
-                        module.focusSearch();
-                      }
+                  else if(strict) {
+                    module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
+                    if( optionValue === value || optionText === value) {
+                      $selectedItem = $choice;
+                      return true;
                     }
-                    $activeLabel.last().next(selector.siblingLabel).addClass(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().addClass(className.active);
-                    module.remove.activeLabels($activeLabel);
+                  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.addClass(className.filtered);
+                  module.add.message(message.maxSelections);
                 }
-                else {
-                  $activeLabel.removeClass(className.active);
+                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;
+          }
+        },
 
-          keydown: function(event) {
+        restore: {
+          defaults: function() {
+            module.clear();
+            module.restore.defaultText();
+            module.restore.defaultValue();
+          },
+          defaultText: function() {
             var
-              pressedKey    = event.which,
-              isShortcutKey = module.is.inObject(pressedKey, keys)
+              defaultText     = module.get.defaultText(),
+              placeholderText = module.get.placeholderText
             ;
-            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.hasClass(className.visible) || $parentMenu.hasClass(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
-                        .removeClass(className.selected)
-                      ;
-                      $parentMenu
-                        .closest(selector.item)
-                          .addClass(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
-                        .removeClass(className.selected)
-                      ;
-                      $subMenu
-                        .find(selector.item).eq(0)
-                          .addClass(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
-                      .removeClass(className.selected)
-                    ;
-                    $nextItem
-                      .addClass(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
-                      .removeClass(className.selected)
-                    ;
-                    $nextItem
-                      .addClass(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.select.firstUnfiltered();
-                  module.show();
-                  event.preventDefault();
-                }
-              }
+            if(defaultText === placeholderText) {
+              module.debug('Restoring default placeholder text', defaultText);
+              module.set.placeholderText(defaultText);
             }
             else {
-              if( !module.has.search() ) {
-                module.set.selectedLetter( String.fromCharCode(pressedKey) );
-              }
+              module.debug('Restoring default text', defaultText);
+              module.set.text(defaultText);
             }
-          }
-        },
-
-        trigger: {
-          change: function() {
+          },
+          placeholderText: function() {
+            module.set.placeholderText();
+          },
+          defaultValue: function() {
             var
-              events       = document.createEvent('HTMLEvents'),
-              inputElement = $input[0]
+              defaultValue = module.get.defaultValue()
             ;
-            if(inputElement) {
-              module.verbose('Triggering native change event');
-              events.initEvent('change', true, false);
-              inputElement.dispatchEvent(events);
+            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();
+              }
             }
-          }
-        },
-
-        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);
+          },
+          labels: function() {
+            if(settings.allowAdditions) {
+              if(!settings.useLabels) {
+                module.error(error.labels);
+                settings.useLabels = true;
+              }
+              module.debug('Restoring selected values');
+              module.create.userLabels();
             }
-            else if( $.isFunction(settings.action) ) {
-              module.verbose('Triggering user action', settings.action, text, value);
-              settings.action.call(element, text, value, this);
+            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.error(error.action, settings.action);
+              module.debug('Restoring previously selected values');
             }
           },
-          eventInModule: function(event, callback) {
-            var
-              $target    = $(event.target),
-              inDocument = ($target.closest(document.documentElement).length > 0),
-              inModule   = ($target.closest($module).length > 0)
-            ;
-            callback = $.isFunction(callback)
-              ? callback
-              : function(){}
-            ;
-            if(inDocument && !inModule) {
-              module.verbose('Triggering event', callback);
-              callback();
-              return true;
+          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.verbose('Event occurred in dropdown, canceling callback');
-              return false;
+              module.set.selected();
             }
+            module.remove.initialLoad();
           },
-          eventOnElement: function(event, callback) {
+          remoteValues: function() {
             var
-              $target      = $(event.target),
-              $label       = $target.closest(selector.siblingLabel),
-              inVisibleDOM = document.body.contains(event.target),
-              notOnLabel   = ($module.find($label).length === 0),
-              notInMenu    = ($target.closest($menu).length === 0)
+              values = module.get.remoteValues()
             ;
-            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;
+            module.debug('Recreating selected from session data', values);
+            if(values) {
+              if( module.is.single() ) {
+                $.each(values, function(value, name) {
+                  module.set.text(name);
+                });
               }
               else {
-                module.hideAndClear();
+                $.each(values, function(value, name) {
+                  module.add.label(value, name);
+                });
               }
             }
-          },
+          }
+        },
 
-          select: function(text, value, element) {
-            value = (value !== undefined)
-              ? value
-              : text
+        read: {
+          remoteData: function(value) {
+            var
+              name
             ;
-            if( module.can.activate( $(element) ) ) {
-              module.set.value(value, $(element));
-              if(module.is.multiple() && !module.is.allFiltered()) {
-                return;
-              }
-              else {
-                module.hideAndClear();
-              }
+            if(window.Storage === undefined) {
+              module.error(error.noStorage);
+              return;
             }
-          },
-
-          combo: function(text, value, element) {
-            value = (value !== undefined)
-              ? value
-              : text
+            name = sessionStorage.getItem(value);
+            return (name !== undefined)
+              ? name
+              : false
             ;
-            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 $module.data(metadata.defaultText);
+        save: {
+          defaults: function() {
+            module.save.defaultText();
+            module.save.placeholderText();
+            module.save.defaultValue();
           },
           defaultValue: function() {
-            return $module.data(metadata.defaultValue);
-          },
-          placeholderText: function() {
-            return $module.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
+              value = module.get.value()
             ;
+            module.verbose('Saving default value as', value);
+            $module.data(metadata.defaultValue, value);
           },
-          userValues: function() {
+          defaultText: function() {
             var
-              values = module.get.values()
-            ;
-            if(!values) {
-              return false;
-            }
-            values = $.isArray(values)
-              ? values
-              : [values]
+              text = module.get.text()
             ;
-            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;
-            });
+            module.verbose('Saving default text as', text);
+            $module.data(metadata.defaultText, text);
           },
-          caretPosition: function() {
+          placeholderText: function() {
             var
-              input = $search.get(0),
-              range,
-              rangeLength
+              text
             ;
-            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;
+            if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
+              text = module.get.text();
+              module.verbose('Saving placeholder text as', text);
+              $module.data(metadata.placeholderText, text);
             }
           },
-          value: function() {
-            var
-              value = ($input.length > 0)
-                ? $input.val()
-                : $module.data(metadata.value),
-              isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '')
-            ;
-            // prevents placeholder element from being selected when multiple
-            return (value === undefined || isEmptyMultiselect)
-              ? ''
-              : value
+          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
+              .removeClass(className.selected)
             ;
-          },
-          values: function() {
-            var
-              value = module.get.value()
+            $nextSelectedItem
+              .addClass(className.selected)
             ;
-            if(value === '') {
-              return '';
+            if(settings.selectOnKeydown && module.is.single()) {
+              module.set.selectedItem($nextSelectedItem);
             }
-            return ( !module.has.selectInput() && module.is.multiple() )
-              ? (typeof value == 'string') // delimited string
-                ? value.split(settings.delimiter)
-                : ''
-              : value
+            $menu
+              .scrollTop(newScroll)
             ;
-          },
-          remoteValues: function() {
+          }
+        },
+
+        set: {
+          filtered: function() {
             var
-              values = module.get.values(),
-              remoteValues = false
+              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(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;
-                }
-              });
+            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.addClass(className.filtered);
+            }
+            else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
+              module.verbose('Showing placeholder text');
+              $text.removeClass(className.filtered);
             }
-            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())
+          empty: function() {
+            $module.addClass(className.empty);
+          },
+          loading: function() {
+            $module.addClass(className.loading);
+          },
+          placeholderText: function(text) {
+            text = text || module.get.placeholderText();
+            module.debug('Setting placeholder text', text);
+            module.set.text(text);
+            $text.addClass(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( $module.attr('tabindex') === undefined) {
+                $module
+                  .attr('tabindex', 0)
+                ;
+                $menu
+                  .attr('tabindex', -1)
+                ;
+              }
+            }
           },
-          choiceValue: function($choice, choiceText) {
-            choiceText = choiceText || module.get.choiceText($choice);
-            if(!$choice) {
-              return false;
+          initialLoad: function() {
+            module.verbose('Setting initial load');
+            initialLoad = true;
+          },
+          activeItem: function($item) {
+            if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
+              $item.addClass(className.filtered);
+            }
+            else {
+              $item.addClass(className.active);
             }
-            return ($choice.data(metadata.value) !== undefined)
-              ? String( $choice.data(metadata.value) )
-              : (typeof choiceText === 'string')
-                ? $.trim(choiceText.toLowerCase())
-                : String(choiceText)
-            ;
           },
-          inputEvent: function() {
+          partialSearch: function(text) {
             var
-              input = $search[0]
+              length = module.get.query().length
             ;
-            if(input) {
-              return (input.oninput !== undefined)
-                ? 'input'
-                : (input.onpropertychange !== undefined)
-                  ? 'propertychange'
-                  : 'keyup'
-              ;
-            }
-            return false;
+            $search.val( text.substr(0 , length));
           },
-          selectValues: function() {
+          scrollPosition: function($item, forceScroll) {
             var
-              select = {}
+              edgeTolerance = 5,
+              $menu,
+              hasActive,
+              offset,
+              itemHeight,
+              itemOffset,
+              menuOffset,
+              menuScroll,
+              menuHeight,
+              abovePage,
+              belowPage
             ;
-            select.values = [];
-            $module
-              .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
-                    });
-                  }
-                })
+
+            $item       = $item || module.get.selectedItem();
+            $menu       = $item.closest(selector.menu);
+            hasActive   = ($item && $item.length > 0);
+            forceScroll = (forceScroll !== undefined)
+              ? forceScroll
+              : false
             ;
-            if(settings.placeholder && settings.placeholder !== 'auto') {
-              module.debug('Setting placeholder value to', settings.placeholder);
-              select.placeholder = settings.placeholder;
+            if($item && $menu.length > 0 && hasActive) {
+              itemOffset = $item.position().top;
+
+              $menu.addClass(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.removeClass(className.loading);
             }
-            if(settings.sortSelect) {
-              select.values.sort(function(a, b) {
-                return (a.name > b.name)
-                  ? 1
-                  : -1
+          },
+          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.removeClass(className.placeholder);
+                }
+                module.debug('Changing text', text, $text);
+                $text
+                  .removeClass(className.filtered)
                 ;
-              });
-              module.debug('Retrieved and sorted values from select', select);
-            }
-            else {
-              module.debug('Retrieved values from select', select);
+                if(settings.preserveHTML) {
+                  $text.html(text);
+                }
+                else {
+                  $text.text(text);
+                }
+              }
             }
-            return select;
-          },
-          activeItem: function() {
-            return $item.filter('.'  + className.active);
           },
-          selectedItem: function() {
+          selectedItem: function($item) {
             var
-              $selectedItem = $item.not(selector.unselectable).filter('.'  + className.selected)
-            ;
-            return ($selectedItem.length > 0)
-              ? $selectedItem
-              : $item.eq(0)
+              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);
           },
-          itemWithAdditions: function(value) {
+          selectedLetter: function(letter) {
             var
-              $items       = module.get.item(value),
-              $userItems   = module.create.userChoice(value),
-              hasUserItems = ($userItems && $userItems.length > 0)
+              $selectedItem         = $item.filter('.' + className.selected),
+              alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
+              $nextValue            = false,
+              $nextItem
             ;
-            if(hasUserItems) {
-              $items = ($items.length > 0)
-                ? $items.add($userItems)
-                : $userItems
-              ;
+            // check next of same letter
+            if(alreadySelectedLetter) {
+              $nextItem = $selectedItem.nextAll($item).eq(0);
+              if( module.has.firstLetter($nextItem, letter) ) {
+                $nextValue  = $nextItem;
+              }
             }
-            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) {
+            // check all values
+            if(!$nextValue) {
               $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;
-                    }
+                .each(function(){
+                  if(module.has.firstLetter($(this), letter)) {
+                    $nextValue = $(this);
+                    return false;
                   }
                 })
               ;
             }
-            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.addClass(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;
+            // set next value
+            if($nextValue) {
+              module.verbose('Scrolling to next value with letter', letter);
+              module.set.scrollPosition($nextValue);
+              $selectedItem.removeClass(className.selected);
+              $nextValue.addClass(className.selected);
+              if(settings.selectOnKeydown && module.is.single()) {
+                module.set.selectedItem($nextValue);
               }
             }
-            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);
+          direction: function($menu) {
+            if(settings.direction == 'auto') {
+              if(module.is.onScreen($menu)) {
+                module.remove.upward($menu);
+              }
+              else {
+                module.set.upward($menu);
+              }
             }
-            else {
-              module.debug('Restoring default text', defaultText);
-              module.set.text(defaultText);
+            else if(settings.direction == 'upward') {
+              module.set.upward($menu);
             }
           },
-          placeholderText: function() {
-            module.set.placeholderText();
+          upward: function($menu) {
+            var $element = $menu || $module;
+            $element.addClass(className.upward);
           },
-          defaultValue: function() {
+          value: function(value, text, $selected) {
             var
-              defaultValue = module.get.defaultValue()
+              escapedValue = module.escape.value(value),
+              hasInput     = ($input.length > 0),
+              isAddition   = !module.has.value(value),
+              currentValue = module.get.values(),
+              stringValue  = (value !== undefined)
+                ? String(value)
+                : value,
+              newValue
             ;
-            if(defaultValue !== undefined) {
-              module.debug('Restoring default value', defaultValue);
-              if(defaultValue !== '') {
-                module.set.value(defaultValue);
-                module.set.selected();
+            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.remove.activeItem();
-                module.remove.selectedItem();
+                module.trigger.change();
               }
+              internalChange = false;
             }
-          },
-          labels: function() {
-            if(settings.allowAdditions) {
-              if(!settings.useLabels) {
-                module.error(error.labels);
-                settings.useLabels = true;
+            else {
+              module.verbose('Storing value in metadata', escapedValue, $input);
+              if(escapedValue !== currentValue) {
+                $module.data(metadata.value, stringValue);
               }
-              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();
+            if(settings.fireOnInit === false && module.is.initialLoad()) {
+              module.verbose('No callback on initial load', settings.onChange);
             }
             else {
-              module.debug('Restoring previously selected values');
+              settings.onChange.call(element, value, text, $selected);
             }
           },
-          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();
+          active: function() {
+            $module
+              .addClass(className.active)
+            ;
           },
-          remoteValues: function() {
+          multiple: function() {
+            $module.addClass(className.multiple);
+          },
+          visible: function() {
+            $module.addClass(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
-              values = module.get.remoteValues()
+              isMultiple = module.is.multiple(),
+              $userSelectedItem
             ;
-            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
+            $selectedItem = (settings.allowAdditions)
+              ? $selectedItem || module.get.itemWithAdditions(value)
+              : $selectedItem || module.get.item(value)
             ;
-            if(window.Storage === undefined) {
-              module.error(error.noStorage);
+            if(!$selectedItem) {
               return;
             }
-            name = sessionStorage.getItem(value);
-            return (name !== undefined)
-              ? name
-              : false
+            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.hasClass(className.filtered),
+                  isActive       = $selected.hasClass(className.active),
+                  isUserValue    = $selected.hasClass(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.value(selectedValue, selectedText, $selected);
+                      module.add.label(selectedValue, selectedText, shouldAnimate);
+                      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
+                    .addClass(className.active)
+                    .addClass(className.selected)
+                  ;
+                }
+              })
             ;
           }
         },
 
-        save: {
-          defaults: function() {
-            module.save.defaultText();
-            module.save.placeholderText();
-            module.save.defaultValue();
-          },
-          defaultValue: function() {
+        add: {
+          label: function(value, text, shouldAnimate) {
             var
-              value = module.get.value()
+              $next  = module.is.searchSelection()
+                ? $search
+                : $text,
+              escapedValue = module.escape.value(value),
+              $label
             ;
-            module.verbose('Saving default value as', value);
-            $module.data(metadata.defaultValue, value);
-          },
-          defaultText: function() {
-            var
-              text = module.get.text()
+            $label =  $('<a />')
+              .addClass(className.label)
+              .attr('data-' + metadata.value, escapedValue)
+              .html(templates.label(escapedValue, text))
             ;
-            module.verbose('Saving default text as', text);
-            $module.data(metadata.defaultText, text);
+            $label = settings.onLabelCreate.call($label, escapedValue, text);
+
+            if(module.has.label(value)) {
+              module.debug('Label already exists, skipping', escapedValue);
+              return;
+            }
+            if(settings.label.variation) {
+              $label.addClass(settings.label.variation);
+            }
+            if(shouldAnimate === true) {
+              module.debug('Animating in label', $label);
+              $label
+                .addClass(className.hidden)
+                .insertBefore($next)
+                .transition(settings.label.transition, settings.label.duration)
+              ;
+            }
+            else {
+              module.debug('Adding selection label', $label);
+              $label
+                .insertBefore($next)
+              ;
+            }
           },
-          placeholderText: function() {
+          message: function(message) {
             var
-              text
+              $message = $menu.children(selector.message),
+              html     = settings.templates.message(module.add.variables(message))
             ;
-            if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
-              text = module.get.text();
-              module.verbose('Saving placeholder text as', text);
-              $module.data(metadata.placeholderText, text);
+            if($message.length > 0) {
+              $message
+                .html(html)
+              ;
+            }
+            else {
+              $message = $('<div/>')
+                .html(html)
+                .addClass(className.message)
+                .appendTo($menu)
+              ;
             }
           },
-          remoteData: function(name, value) {
-            if(window.Storage === undefined) {
-              module.error(error.noStorage);
+          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;
             }
-            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
-              .removeClass(className.selected)
-            ;
-            $nextSelectedItem
-              .addClass(className.selected)
-            ;
-            if(settings.selectOnKeydown && module.is.single()) {
-              module.set.selectedItem($nextSelectedItem);
+            // temporarily disconnect observer
+            module.disconnect.selectObserver();
+            if( module.is.single() ) {
+              module.verbose('Removing previous user addition');
+              $input.find('option.' + className.addition).remove();
             }
-            $menu
-              .scrollTop(newScroll)
+            $('<option/>')
+              .prop('value', escapedValue)
+              .addClass(className.addition)
+              .html(value)
+              .appendTo($input)
             ;
-          }
-        },
-
-        set: {
-          filtered: function() {
+            module.verbose('Adding user addition as an <option>', value);
+            module.observe.select();
+          },
+          userSuggestion: function(value) {
             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 !== ''
+              $addition         = $menu.children(selector.addition),
+              $existingItem     = module.get.item(value),
+              alreadyHasValue   = $existingItem && $existingItem.not(selector.addition).length,
+              hasUserSuggestion = $addition.length > 0,
+              html
             ;
-            if(isMultiple && hasSearchValue) {
-              module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
-              $search.css('width', searchWidth);
+            if(settings.useLabels && module.has.maxSelections()) {
+              return;
             }
-            if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
-              module.verbose('Hiding placeholder text');
-              $text.addClass(className.filtered);
-            }
-            else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
-              module.verbose('Showing placeholder text');
-              $text.removeClass(className.filtered);
+            if(value === '' || alreadyHasValue) {
+              $addition.remove();
+              return;
             }
-          },
-          empty: function() {
-            $module.addClass(className.empty);
-          },
-          loading: function() {
-            $module.addClass(className.loading);
-          },
-          placeholderText: function(text) {
-            text = text || module.get.placeholderText();
-            module.debug('Setting placeholder text', text);
-            module.set.text(text);
-            $text.addClass(className.placeholder);
-          },
-          tabbable: function() {
-            if( module.is.searchSelection() ) {
-              module.debug('Added tabindex to searchable dropdown');
-              $search
-                .val('')
-                .attr('tabindex', 0)
-              ;
-              $menu
-                .attr('tabindex', -1)
+            if(hasUserSuggestion) {
+              $addition
+                .data(metadata.value, value)
+                .data(metadata.text, value)
+                .attr('data-' + metadata.value, value)
+                .attr('data-' + metadata.text, value)
+                .removeClass(className.filtered)
               ;
-            }
-            else {
-              module.debug('Added tabindex to dropdown');
-              if( $module.attr('tabindex') === undefined) {
-                $module
-                  .attr('tabindex', 0)
-                ;
-                $menu
-                  .attr('tabindex', -1)
+              if(!settings.hideAdditions) {
+                html = settings.templates.addition( module.add.variables(message.addResult, value) );
+                $addition
+                  .html(html)
                 ;
               }
-            }
-          },
-          initialLoad: function() {
-            module.verbose('Setting initial load');
-            initialLoad = true;
-          },
-          activeItem: function($item) {
-            if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
-              $item.addClass(className.filtered);
+              module.verbose('Replacing user suggestion with new value', $addition);
             }
             else {
-              $item.addClass(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.addClass(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.removeClass(className.loading);
+              $addition = module.create.userChoice(value);
+              $addition
+                .prependTo($menu)
+              ;
+              module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
             }
-          },
-          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.removeClass(className.placeholder);
-                }
-                module.debug('Changing text', text, $text);
-                $text
-                  .removeClass(className.filtered)
-                ;
-                if(settings.preserveHTML) {
-                  $text.html(text);
-                }
-                else {
-                  $text.text(text);
-                }
-              }
+            if(!settings.hideAdditions || module.is.allFiltered()) {
+              $addition
+                .addClass(className.selected)
+                .siblings()
+                .removeClass(className.selected)
+              ;
             }
+            module.refreshItems();
           },
-          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) {
+          variables: function(message, term) {
             var
-              $selectedItem         = $item.filter('.' + className.selected),
-              alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
-              $nextValue            = false,
-              $nextItem
+              hasCount    = (message.search('{count}') !== -1),
+              hasMaxCount = (message.search('{maxCount}') !== -1),
+              hasTerm     = (message.search('{term}') !== -1),
+              values,
+              count,
+              query
             ;
-            // 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.removeClass(className.selected);
-              $nextValue.addClass(className.selected);
-              if(settings.selectOnKeydown && module.is.single()) {
-                module.set.selectedItem($nextValue);
-              }
+            module.verbose('Adding templated variables to message', message);
+            if(hasCount) {
+              count  = module.get.selectionCount();
+              message = message.replace('{count}', count);
             }
-          },
-          direction: function($menu) {
-            if(settings.direction == 'auto') {
-              if(module.is.onScreen($menu)) {
-                module.remove.upward($menu);
-              }
-              else {
-                module.set.upward($menu);
-              }
+            if(hasMaxCount) {
+              count  = module.get.selectionCount();
+              message = message.replace('{maxCount}', settings.maxSelections);
             }
-            else if(settings.direction == 'upward') {
-              module.set.upward($menu);
+            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(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);
             }
-          },
-          upward: function($menu) {
-            var $element = $menu || $module;
-            $element.addClass(className.upward);
-          },
-          value: function(value, text, $selected) {
-            var
-              escapedValue = module.escape.value(value),
-              hasInput     = ($input.length > 0),
-              isAddition   = !module.has.value(value),
-              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) {
-                $module.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() {
-            $module
-              .addClass(className.active)
-            ;
-          },
-          multiple: function() {
-            $module.addClass(className.multiple);
-          },
-          visible: function() {
-            $module.addClass(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.hasClass(className.filtered),
-                  isActive       = $selected.hasClass(className.active),
-                  isUserValue    = $selected.hasClass(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.value(selectedValue, selectedText, $selected);
-                      module.add.label(selectedValue, selectedText, shouldAnimate);
-                      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);
-      &nb