Code Duplication    Length = 3830-3830 lines in 2 locations

public/lib/semantic/semantic.js 1 location

@@ 4368-8197 (lines=3830) @@
4365
 *
4366
 */
4367
4368
;(function ($, window, document, undefined) {
4369
4370
"use strict";
4371
4372
window = (typeof window != 'undefined' && window.Math == Math)
4373
  ? window
4374
  : (typeof self != 'undefined' && self.Math == Math)
4375
    ? self
4376
    : Function('return this')()
4377
;
4378
4379
$.fn.dropdown = function(parameters) {
4380
  var
4381
    $allModules    = $(this),
4382
    $document      = $(document),
4383
4384
    moduleSelector = $allModules.selector || '',
4385
4386
    hasTouch       = ('ontouchstart' in document.documentElement),
4387
    time           = new Date().getTime(),
4388
    performance    = [],
4389
4390
    query          = arguments[0],
4391
    methodInvoked  = (typeof query == 'string'),
4392
    queryArguments = [].slice.call(arguments, 1),
4393
    returnedValue
4394
  ;
4395
4396
  $allModules
4397
    .each(function(elementIndex) {
4398
      var
4399
        settings          = ( $.isPlainObject(parameters) )
4400
          ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
4401
          : $.extend({}, $.fn.dropdown.settings),
4402
4403
        className       = settings.className,
4404
        message         = settings.message,
4405
        fields          = settings.fields,
4406
        keys            = settings.keys,
4407
        metadata        = settings.metadata,
4408
        namespace       = settings.namespace,
4409
        regExp          = settings.regExp,
4410
        selector        = settings.selector,
4411
        error           = settings.error,
4412
        templates       = settings.templates,
4413
4414
        eventNamespace  = '.' + namespace,
4415
        moduleNamespace = 'module-' + namespace,
4416
4417
        $module         = $(this),
4418
        $context        = $(settings.context),
4419
        $text           = $module.find(selector.text),
4420
        $search         = $module.find(selector.search),
4421
        $sizer          = $module.find(selector.sizer),
4422
        $input          = $module.find(selector.input),
4423
        $icon           = $module.find(selector.icon),
4424
4425
        $combo = ($module.prev().find(selector.text).length > 0)
4426
          ? $module.prev().find(selector.text)
4427
          : $module.prev(),
4428
4429
        $menu           = $module.children(selector.menu),
4430
        $item           = $menu.find(selector.item),
4431
4432
        activated       = false,
4433
        itemActivated   = false,
4434
        internalChange  = false,
4435
        element         = this,
4436
        instance        = $module.data(moduleNamespace),
4437
4438
        initialLoad,
4439
        pageLostFocus,
4440
        willRefocus,
4441
        elementNamespace,
4442
        id,
4443
        selectObserver,
4444
        menuObserver,
4445
        module
4446
      ;
4447
4448
      module = {
4449
4450
        initialize: function() {
4451
          module.debug('Initializing dropdown', settings);
4452
4453
          if( module.is.alreadySetup() ) {
4454
            module.setup.reference();
4455
          }
4456
          else {
4457
            module.setup.layout();
4458
            module.refreshData();
4459
4460
            module.save.defaults();
4461
            module.restore.selected();
4462
4463
            module.create.id();
4464
            module.bind.events();
4465
4466
            module.observeChanges();
4467
            module.instantiate();
4468
          }
4469
4470
        },
4471
4472
        instantiate: function() {
4473
          module.verbose('Storing instance of dropdown', module);
4474
          instance = module;
4475
          $module
4476
            .data(moduleNamespace, module)
4477
          ;
4478
        },
4479
4480
        destroy: function() {
4481
          module.verbose('Destroying previous dropdown', $module);
4482
          module.remove.tabbable();
4483
          $module
4484
            .off(eventNamespace)
4485
            .removeData(moduleNamespace)
4486
          ;
4487
          $menu
4488
            .off(eventNamespace)
4489
          ;
4490
          $document
4491
            .off(elementNamespace)
4492
          ;
4493
          module.disconnect.menuObserver();
4494
          module.disconnect.selectObserver();
4495
        },
4496
4497
        observeChanges: function() {
4498
          if('MutationObserver' in window) {
4499
            selectObserver = new MutationObserver(module.event.select.mutation);
4500
            menuObserver   = new MutationObserver(module.event.menu.mutation);
4501
            module.debug('Setting up mutation observer', selectObserver, menuObserver);
4502
            module.observe.select();
4503
            module.observe.menu();
4504
          }
4505
        },
4506
4507
        disconnect: {
4508
          menuObserver: function() {
4509
            if(menuObserver) {
4510
              menuObserver.disconnect();
4511
            }
4512
          },
4513
          selectObserver: function() {
4514
            if(selectObserver) {
4515
              selectObserver.disconnect();
4516
            }
4517
          }
4518
        },
4519
        observe: {
4520
          select: function() {
4521
            if(module.has.input()) {
4522
              selectObserver.observe($input[0], {
4523
                childList : true,
4524
                subtree   : true
4525
              });
4526
            }
4527
          },
4528
          menu: function() {
4529
            if(module.has.menu()) {
4530
              menuObserver.observe($menu[0], {
4531
                childList : true,
4532
                subtree   : true
4533
              });
4534
            }
4535
          }
4536
        },
4537
4538
        create: {
4539
          id: function() {
4540
            id = (Math.random().toString(16) + '000000000').substr(2, 8);
4541
            elementNamespace = '.' + id;
4542
            module.verbose('Creating unique id for element', id);
4543
          },
4544
          userChoice: function(values) {
4545
            var
4546
              $userChoices,
4547
              $userChoice,
4548
              isUserValue,
4549
              html
4550
            ;
4551
            values = values || module.get.userValues();
4552
            if(!values) {
4553
              return false;
4554
            }
4555
            values = $.isArray(values)
4556
              ? values
4557
              : [values]
4558
            ;
4559
            $.each(values, function(index, value) {
4560
              if(module.get.item(value) === false) {
4561
                html         = settings.templates.addition( module.add.variables(message.addResult, value) );
4562
                $userChoice  = $('<div />')
4563
                  .html(html)
4564
                  .attr('data-' + metadata.value, value)
4565
                  .attr('data-' + metadata.text, value)
4566
                  .addClass(className.addition)
4567
                  .addClass(className.item)
4568
                ;
4569
                if(settings.hideAdditions) {
4570
                  $userChoice.addClass(className.hidden);
4571
                }
4572
                $userChoices = ($userChoices === undefined)
4573
                  ? $userChoice
4574
                  : $userChoices.add($userChoice)
4575
                ;
4576
                module.verbose('Creating user choices for value', value, $userChoice);
4577
              }
4578
            });
4579
            return $userChoices;
4580
          },
4581
          userLabels: function(value) {
4582
            var
4583
              userValues = module.get.userValues()
4584
            ;
4585
            if(userValues) {
4586
              module.debug('Adding user labels', userValues);
4587
              $.each(userValues, function(index, value) {
4588
                module.verbose('Adding custom user value');
4589
                module.add.label(value, value);
4590
              });
4591
            }
4592
          },
4593
          menu: function() {
4594
            $menu = $('<div />')
4595
              .addClass(className.menu)
4596
              .appendTo($module)
4597
            ;
4598
          },
4599
          sizer: function() {
4600
            $sizer = $('<span />')
4601
              .addClass(className.sizer)
4602
              .insertAfter($search)
4603
            ;
4604
          }
4605
        },
4606
4607
        search: function(query) {
4608
          query = (query !== undefined)
4609
            ? query
4610
            : module.get.query()
4611
          ;
4612
          module.verbose('Searching for query', query);
4613
          if(module.has.minCharacters(query)) {
4614
            module.filter(query);
4615
          }
4616
          else {
4617
            module.hide();
4618
          }
4619
        },
4620
4621
        select: {
4622
          firstUnfiltered: function() {
4623
            module.verbose('Selecting first non-filtered element');
4624
            module.remove.selectedItem();
4625
            $item
4626
              .not(selector.unselectable)
4627
              .not(selector.addition + selector.hidden)
4628
                .eq(0)
4629
                .addClass(className.selected)
4630
            ;
4631
          },
4632
          nextAvailable: function($selected) {
4633
            $selected = $selected.eq(0);
4634
            var
4635
              $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
4636
              $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
4637
              hasNext        = ($nextAvailable.length > 0)
4638
            ;
4639
            if(hasNext) {
4640
              module.verbose('Moving selection to', $nextAvailable);
4641
              $nextAvailable.addClass(className.selected);
4642
            }
4643
            else {
4644
              module.verbose('Moving selection to', $prevAvailable);
4645
              $prevAvailable.addClass(className.selected);
4646
            }
4647
          }
4648
        },
4649
4650
        setup: {
4651
          api: function() {
4652
            var
4653
              apiSettings = {
4654
                debug   : settings.debug,
4655
                urlData : {
4656
                  value : module.get.value(),
4657
                  query : module.get.query()
4658
                },
4659
                on    : false
4660
              }
4661
            ;
4662
            module.verbose('First request, initializing API');
4663
            $module
4664
              .api(apiSettings)
4665
            ;
4666
          },
4667
          layout: function() {
4668
            if( $module.is('select') ) {
4669
              module.setup.select();
4670
              module.setup.returnedObject();
4671
            }
4672
            if( !module.has.menu() ) {
4673
              module.create.menu();
4674
            }
4675
            if( module.is.search() && !module.has.search() ) {
4676
              module.verbose('Adding search input');
4677
              $search = $('<input />')
4678
                .addClass(className.search)
4679
                .prop('autocomplete', 'off')
4680
                .insertBefore($text)
4681
              ;
4682
            }
4683
            if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
4684
              module.create.sizer();
4685
            }
4686
            if(settings.allowTab) {
4687
              module.set.tabbable();
4688
            }
4689
          },
4690
          select: function() {
4691
            var
4692
              selectValues  = module.get.selectValues()
4693
            ;
4694
            module.debug('Dropdown initialized on a select', selectValues);
4695
            if( $module.is('select') ) {
4696
              $input = $module;
4697
            }
4698
            // see if select is placed correctly already
4699
            if($input.parent(selector.dropdown).length > 0) {
4700
              module.debug('UI dropdown already exists. Creating dropdown menu only');
4701
              $module = $input.closest(selector.dropdown);
4702
              if( !module.has.menu() ) {
4703
                module.create.menu();
4704
              }
4705
              $menu = $module.children(selector.menu);
4706
              module.setup.menu(selectValues);
4707
            }
4708
            else {
4709
              module.debug('Creating entire dropdown from select');
4710
              $module = $('<div />')
4711
                .attr('class', $input.attr('class') )
4712
                .addClass(className.selection)
4713
                .addClass(className.dropdown)
4714
                .html( templates.dropdown(selectValues) )
4715
                .insertBefore($input)
4716
              ;
4717
              if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
4718
                module.error(error.missingMultiple);
4719
                $input.prop('multiple', true);
4720
              }
4721
              if($input.is('[multiple]')) {
4722
                module.set.multiple();
4723
              }
4724
              if ($input.prop('disabled')) {
4725
                module.debug('Disabling dropdown');
4726
                $module.addClass(className.disabled);
4727
              }
4728
              $input
4729
                .removeAttr('class')
4730
                .detach()
4731
                .prependTo($module)
4732
              ;
4733
            }
4734
            module.refresh();
4735
          },
4736
          menu: function(values) {
4737
            $menu.html( templates.menu(values, fields));
4738
            $item = $menu.find(selector.item);
4739
          },
4740
          reference: function() {
4741
            module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
4742
            // replace module reference
4743
            $module = $module.parent(selector.dropdown);
4744
            module.refresh();
4745
            module.setup.returnedObject();
4746
            // invoke method in context of current instance
4747
            if(methodInvoked) {
4748
              instance = module;
4749
              module.invoke(query);
4750
            }
4751
          },
4752
          returnedObject: function() {
4753
            var
4754
              $firstModules = $allModules.slice(0, elementIndex),
4755
              $lastModules = $allModules.slice(elementIndex + 1)
4756
            ;
4757
            // adjust all modules to use correct reference
4758
            $allModules = $firstModules.add($module).add($lastModules);
4759
          }
4760
        },
4761
4762
        refresh: function() {
4763
          module.refreshSelectors();
4764
          module.refreshData();
4765
        },
4766
4767
        refreshItems: function() {
4768
          $item = $menu.find(selector.item);
4769
        },
4770
4771
        refreshSelectors: function() {
4772
          module.verbose('Refreshing selector cache');
4773
          $text   = $module.find(selector.text);
4774
          $search = $module.find(selector.search);
4775
          $input  = $module.find(selector.input);
4776
          $icon   = $module.find(selector.icon);
4777
          $combo  = ($module.prev().find(selector.text).length > 0)
4778
            ? $module.prev().find(selector.text)
4779
            : $module.prev()
4780
          ;
4781
          $menu    = $module.children(selector.menu);
4782
          $item    = $menu.find(selector.item);
4783
        },
4784
4785
        refreshData: function() {
4786
          module.verbose('Refreshing cached metadata');
4787
          $item
4788
            .removeData(metadata.text)
4789
            .removeData(metadata.value)
4790
          ;
4791
        },
4792
4793
        clearData: function() {
4794
          module.verbose('Clearing metadata');
4795
          $item
4796
            .removeData(metadata.text)
4797
            .removeData(metadata.value)
4798
          ;
4799
          $module
4800
            .removeData(metadata.defaultText)
4801
            .removeData(metadata.defaultValue)
4802
            .removeData(metadata.placeholderText)
4803
          ;
4804
        },
4805
4806
        toggle: function() {
4807
          module.verbose('Toggling menu visibility');
4808
          if( !module.is.active() ) {
4809
            module.show();
4810
          }
4811
          else {
4812
            module.hide();
4813
          }
4814
        },
4815
4816
        show: function(callback) {
4817
          callback = $.isFunction(callback)
4818
            ? callback
4819
            : function(){}
4820
          ;
4821
          if(!module.can.show() && module.is.remote()) {
4822
            module.debug('No API results retrieved, searching before show');
4823
            module.queryRemote(module.get.query(), module.show);
4824
          }
4825
          if( module.can.show() && !module.is.active() ) {
4826
            module.debug('Showing dropdown');
4827
            if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
4828
              module.remove.message();
4829
            }
4830
            if(module.is.allFiltered()) {
4831
              return true;
4832
            }
4833
            if(settings.onShow.call(element) !== false) {
4834
              module.animate.show(function() {
4835
                if( module.can.click() ) {
4836
                  module.bind.intent();
4837
                }
4838
                if(module.has.menuSearch()) {
4839
                  module.focusSearch();
4840
                }
4841
                module.set.visible();
4842
                callback.call(element);
4843
              });
4844
            }
4845
          }
4846
        },
4847
4848
        hide: function(callback) {
4849
          callback = $.isFunction(callback)
4850
            ? callback
4851
            : function(){}
4852
          ;
4853
          if( module.is.active() ) {
4854
            module.debug('Hiding dropdown');
4855
            if(settings.onHide.call(element) !== false) {
4856
              module.animate.hide(function() {
4857
                module.remove.visible();
4858
                callback.call(element);
4859
              });
4860
            }
4861
          }
4862
        },
4863
4864
        hideOthers: function() {
4865
          module.verbose('Finding other dropdowns to hide');
4866
          $allModules
4867
            .not($module)
4868
              .has(selector.menu + '.' + className.visible)
4869
                .dropdown('hide')
4870
          ;
4871
        },
4872
4873
        hideMenu: function() {
4874
          module.verbose('Hiding menu  instantaneously');
4875
          module.remove.active();
4876
          module.remove.visible();
4877
          $menu.transition('hide');
4878
        },
4879
4880
        hideSubMenus: function() {
4881
          var
4882
            $subMenus = $menu.children(selector.item).find(selector.menu)
4883
          ;
4884
          module.verbose('Hiding sub menus', $subMenus);
4885
          $subMenus.transition('hide');
4886
        },
4887
4888
        bind: {
4889
          events: function() {
4890
            if(hasTouch) {
4891
              module.bind.touchEvents();
4892
            }
4893
            module.bind.keyboardEvents();
4894
            module.bind.inputEvents();
4895
            module.bind.mouseEvents();
4896
          },
4897
          touchEvents: function() {
4898
            module.debug('Touch device detected binding additional touch events');
4899
            if( module.is.searchSelection() ) {
4900
              // do nothing special yet
4901
            }
4902
            else if( module.is.single() ) {
4903
              $module
4904
                .on('touchstart' + eventNamespace, module.event.test.toggle)
4905
              ;
4906
            }
4907
            $menu
4908
              .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
4909
            ;
4910
          },
4911
          keyboardEvents: function() {
4912
            module.verbose('Binding keyboard events');
4913
            $module
4914
              .on('keydown' + eventNamespace, module.event.keydown)
4915
            ;
4916
            if( module.has.search() ) {
4917
              $module
4918
                .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
4919
              ;
4920
            }
4921
            if( module.is.multiple() ) {
4922
              $document
4923
                .on('keydown' + elementNamespace, module.event.document.keydown)
4924
              ;
4925
            }
4926
          },
4927
          inputEvents: function() {
4928
            module.verbose('Binding input change events');
4929
            $module
4930
              .on('change' + eventNamespace, selector.input, module.event.change)
4931
            ;
4932
          },
4933
          mouseEvents: function() {
4934
            module.verbose('Binding mouse events');
4935
            if(module.is.multiple()) {
4936
              $module
4937
                .on('click'   + eventNamespace, selector.label,  module.event.label.click)
4938
                .on('click'   + eventNamespace, selector.remove, module.event.remove.click)
4939
              ;
4940
            }
4941
            if( module.is.searchSelection() ) {
4942
              $module
4943
                .on('mousedown' + eventNamespace, module.event.mousedown)
4944
                .on('mouseup'   + eventNamespace, module.event.mouseup)
4945
                .on('mousedown' + eventNamespace, selector.menu,   module.event.menu.mousedown)
4946
                .on('mouseup'   + eventNamespace, selector.menu,   module.event.menu.mouseup)
4947
                .on('click'     + eventNamespace, selector.icon,   module.event.icon.click)
4948
                .on('focus'     + eventNamespace, selector.search, module.event.search.focus)
4949
                .on('click'     + eventNamespace, selector.search, module.event.search.focus)
4950
                .on('blur'      + eventNamespace, selector.search, module.event.search.blur)
4951
                .on('click'     + eventNamespace, selector.text,   module.event.text.focus)
4952
              ;
4953
              if(module.is.multiple()) {
4954
                $module
4955
                  .on('click' + eventNamespace, module.event.click)
4956
                ;
4957
              }
4958
            }
4959
            else {
4960
              if(settings.on == 'click') {
4961
                $module
4962
                  .on('click' + eventNamespace, selector.icon, module.event.icon.click)
4963
                  .on('click' + eventNamespace, module.event.test.toggle)
4964
                ;
4965
              }
4966
              else if(settings.on == 'hover') {
4967
                $module
4968
                  .on('mouseenter' + eventNamespace, module.delay.show)
4969
                  .on('mouseleave' + eventNamespace, module.delay.hide)
4970
                ;
4971
              }
4972
              else {
4973
                $module
4974
                  .on(settings.on + eventNamespace, module.toggle)
4975
                ;
4976
              }
4977
              $module
4978
                .on('mousedown' + eventNamespace, module.event.mousedown)
4979
                .on('mouseup'   + eventNamespace, module.event.mouseup)
4980
                .on('focus'     + eventNamespace, module.event.focus)
4981
              ;
4982
              if(module.has.menuSearch() ) {
4983
                $module
4984
                  .on('blur' + eventNamespace, selector.search, module.event.search.blur)
4985
                ;
4986
              }
4987
              else {
4988
                $module
4989
                  .on('blur' + eventNamespace, module.event.blur)
4990
                ;
4991
              }
4992
            }
4993
            $menu
4994
              .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter)
4995
              .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
4996
              .on('click'      + eventNamespace, selector.item, module.event.item.click)
4997
            ;
4998
          },
4999
          intent: function() {
5000
            module.verbose('Binding hide intent event to document');
5001
            if(hasTouch) {
5002
              $document
5003
                .on('touchstart' + elementNamespace, module.event.test.touch)
5004
                .on('touchmove'  + elementNamespace, module.event.test.touch)
5005
              ;
5006
            }
5007
            $document
5008
              .on('click' + elementNamespace, module.event.test.hide)
5009
            ;
5010
          }
5011
        },
5012
5013
        unbind: {
5014
          intent: function() {
5015
            module.verbose('Removing hide intent event from document');
5016
            if(hasTouch) {
5017
              $document
5018
                .off('touchstart' + elementNamespace)
5019
                .off('touchmove' + elementNamespace)
5020
              ;
5021
            }
5022
            $document
5023
              .off('click' + elementNamespace)
5024
            ;
5025
          }
5026
        },
5027
5028
        filter: function(query) {
5029
          var
5030
            searchTerm = (query !== undefined)
5031
              ? query
5032
              : module.get.query(),
5033
            afterFiltered = function() {
5034
              if(module.is.multiple()) {
5035
                module.filterActive();
5036
              }
5037
              if(query || (!query && module.get.activeItem().length == 0)) {
5038
                module.select.firstUnfiltered();
5039
              }
5040
              if( module.has.allResultsFiltered() ) {
5041
                if( settings.onNoResults.call(element, searchTerm) ) {
5042
                  if(settings.allowAdditions) {
5043
                    if(settings.hideAdditions) {
5044
                      module.verbose('User addition with no menu, setting empty style');
5045
                      module.set.empty();
5046
                      module.hideMenu();
5047
                    }
5048
                  }
5049
                  else {
5050
                    module.verbose('All items filtered, showing message', searchTerm);
5051
                    module.add.message(message.noResults);
5052
                  }
5053
                }
5054
                else {
5055
                  module.verbose('All items filtered, hiding dropdown', searchTerm);
5056
                  module.hideMenu();
5057
                }
5058
              }
5059
              else {
5060
                module.remove.empty();
5061
                module.remove.message();
5062
              }
5063
              if(settings.allowAdditions) {
5064
                module.add.userSuggestion(query);
5065
              }
5066
              if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
5067
                module.show();
5068
              }
5069
            }
5070
          ;
5071
          if(settings.useLabels && module.has.maxSelections()) {
5072
            return;
5073
          }
5074
          if(settings.apiSettings) {
5075
            if( module.can.useAPI() ) {
5076
              module.queryRemote(searchTerm, function() {
5077
                if(settings.filterRemoteData) {
5078
                  module.filterItems(searchTerm);
5079
                }
5080
                afterFiltered();
5081
              });
5082
            }
5083
            else {
5084
              module.error(error.noAPI);
5085
            }
5086
          }
5087
          else {
5088
            module.filterItems(searchTerm);
5089
            afterFiltered();
5090
          }
5091
        },
5092
5093
        queryRemote: function(query, callback) {
5094
          var
5095
            apiSettings = {
5096
              errorDuration : false,
5097
              cache         : 'local',
5098
              throttle      : settings.throttle,
5099
              urlData       : {
5100
                query: query
5101
              },
5102
              onError: function() {
5103
                module.add.message(message.serverError);
5104
                callback();
5105
              },
5106
              onFailure: function() {
5107
                module.add.message(message.serverError);
5108
                callback();
5109
              },
5110
              onSuccess : function(response) {
5111
                module.remove.message();
5112
                module.setup.menu({
5113
                  values: response[fields.remoteValues]
5114
                });
5115
                callback();
5116
              }
5117
            }
5118
          ;
5119
          if( !$module.api('get request') ) {
5120
            module.setup.api();
5121
          }
5122
          apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
5123
          $module
5124
            .api('setting', apiSettings)
5125
            .api('query')
5126
          ;
5127
        },
5128
5129
        filterItems: function(query) {
5130
          var
5131
            searchTerm = (query !== undefined)
5132
              ? query
5133
              : module.get.query(),
5134
            results          =  null,
5135
            escapedTerm      = module.escape.string(searchTerm),
5136
            beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm')
5137
          ;
5138
          // avoid loop if we're matching nothing
5139
          if( module.has.query() ) {
5140
            results = [];
5141
5142
            module.verbose('Searching for matching values', searchTerm);
5143
            $item
5144
              .each(function(){
5145
                var
5146
                  $choice = $(this),
5147
                  text,
5148
                  value
5149
                ;
5150
                if(settings.match == 'both' || settings.match == 'text') {
5151
                  text = String(module.get.choiceText($choice, false));
5152
                  if(text.search(beginsWithRegExp) !== -1) {
5153
                    results.push(this);
5154
                    return true;
5155
                  }
5156
                  else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
5157
                    results.push(this);
5158
                    return true;
5159
                  }
5160
                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
5161
                    results.push(this);
5162
                    return true;
5163
                  }
5164
                }
5165
                if(settings.match == 'both' || settings.match == 'value') {
5166
                  value = String(module.get.choiceValue($choice, text));
5167
                  if(value.search(beginsWithRegExp) !== -1) {
5168
                    results.push(this);
5169
                    return true;
5170
                  }
5171
                  else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
5172
                    results.push(this);
5173
                    return true;
5174
                  }
5175
                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
5176
                    results.push(this);
5177
                    return true;
5178
                  }
5179
                }
5180
              })
5181
            ;
5182
          }
5183
          module.debug('Showing only matched items', searchTerm);
5184
          module.remove.filteredItem();
5185
          if(results) {
5186
            $item
5187
              .not(results)
5188
              .addClass(className.filtered)
5189
            ;
5190
          }
5191
        },
5192
5193
        fuzzySearch: function(query, term) {
5194
          var
5195
            termLength  = term.length,
5196
            queryLength = query.length
5197
          ;
5198
          query = query.toLowerCase();
5199
          term  = term.toLowerCase();
5200
          if(queryLength > termLength) {
5201
            return false;
5202
          }
5203
          if(queryLength === termLength) {
5204
            return (query === term);
5205
          }
5206
          search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
5207
            var
5208
              queryCharacter = query.charCodeAt(characterIndex)
5209
            ;
5210
            while(nextCharacterIndex < termLength) {
5211
              if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
5212
                continue search;
5213
              }
5214
            }
5215
            return false;
5216
          }
5217
          return true;
5218
        },
5219
        exactSearch: function (query, term) {
5220
          query = query.toLowerCase();
5221
          term  = term.toLowerCase();
5222
          if(term.indexOf(query) > -1) {
5223
             return true;
5224
          }
5225
          return false;
5226
        },
5227
        filterActive: function() {
5228
          if(settings.useLabels) {
5229
            $item.filter('.' + className.active)
5230
              .addClass(className.filtered)
5231
            ;
5232
          }
5233
        },
5234
5235
        focusSearch: function(skipHandler) {
5236
          if( module.has.search() && !module.is.focusedOnSearch() ) {
5237
            if(skipHandler) {
5238
              $module.off('focus' + eventNamespace, selector.search);
5239
              $search.focus();
5240
              $module.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
5241
            }
5242
            else {
5243
              $search.focus();
5244
            }
5245
          }
5246
        },
5247
5248
        forceSelection: function() {
5249
          var
5250
            $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
5251
            $activeItem        = $item.not(className.filtered).filter('.' + className.active).eq(0),
5252
            $selectedItem      = ($currentlySelected.length > 0)
5253
              ? $currentlySelected
5254
              : $activeItem,
5255
            hasSelected = ($selectedItem.length > 0)
5256
          ;
5257
          if(hasSelected && !module.is.multiple()) {
5258
            module.debug('Forcing partial selection to selected item', $selectedItem);
5259
            module.event.item.click.call($selectedItem, {}, true);
5260
            return;
5261
          }
5262
          else {
5263
            if(settings.allowAdditions) {
5264
              module.set.selected(module.get.query());
5265
              module.remove.searchTerm();
5266
            }
5267
            else {
5268
              module.remove.searchTerm();
5269
            }
5270
          }
5271
        },
5272
5273
        event: {
5274
          change: function() {
5275
            if(!internalChange) {
5276
              module.debug('Input changed, updating selection');
5277
              module.set.selected();
5278
            }
5279
          },
5280
          focus: function() {
5281
            if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
5282
              module.show();
5283
            }
5284
          },
5285
          blur: function(event) {
5286
            pageLostFocus = (document.activeElement === this);
5287
            if(!activated && !pageLostFocus) {
5288
              module.remove.activeLabel();
5289
              module.hide();
5290
            }
5291
          },
5292
          mousedown: function() {
5293
            if(module.is.searchSelection()) {
5294
              // prevent menu hiding on immediate re-focus
5295
              willRefocus = true;
5296
            }
5297
            else {
5298
              // prevents focus callback from occurring on mousedown
5299
              activated = true;
5300
            }
5301
          },
5302
          mouseup: function() {
5303
            if(module.is.searchSelection()) {
5304
              // prevent menu hiding on immediate re-focus
5305
              willRefocus = false;
5306
            }
5307
            else {
5308
              activated = false;
5309
            }
5310
          },
5311
          click: function(event) {
5312
            var
5313
              $target = $(event.target)
5314
            ;
5315
            // focus search
5316
            if($target.is($module)) {
5317
              if(!module.is.focusedOnSearch()) {
5318
                module.focusSearch();
5319
              }
5320
              else {
5321
                module.show();
5322
              }
5323
            }
5324
          },
5325
          search: {
5326
            focus: function() {
5327
              activated = true;
5328
              if(module.is.multiple()) {
5329
                module.remove.activeLabel();
5330
              }
5331
              if(settings.showOnFocus) {
5332
                module.search();
5333
              }
5334
            },
5335
            blur: function(event) {
5336
              pageLostFocus = (document.activeElement === this);
5337
              if(module.is.searchSelection() && !willRefocus) {
5338
                if(!itemActivated && !pageLostFocus) {
5339
                  if(settings.forceSelection) {
5340
                    module.forceSelection();
5341
                  }
5342
                  module.hide();
5343
                }
5344
              }
5345
              willRefocus = false;
5346
            }
5347
          },
5348
          icon: {
5349
            click: function(event) {
5350
              module.toggle();
5351
            }
5352
          },
5353
          text: {
5354
            focus: function(event) {
5355
              activated = true;
5356
              module.focusSearch();
5357
            }
5358
          },
5359
          input: function(event) {
5360
            if(module.is.multiple() || module.is.searchSelection()) {
5361
              module.set.filtered();
5362
            }
5363
            clearTimeout(module.timer);
5364
            module.timer = setTimeout(module.search, settings.delay.search);
5365
          },
5366
          label: {
5367
            click: function(event) {
5368
              var
5369
                $label        = $(this),
5370
                $labels       = $module.find(selector.label),
5371
                $activeLabels = $labels.filter('.' + className.active),
5372
                $nextActive   = $label.nextAll('.' + className.active),
5373
                $prevActive   = $label.prevAll('.' + className.active),
5374
                $range = ($nextActive.length > 0)
5375
                  ? $label.nextUntil($nextActive).add($activeLabels).add($label)
5376
                  : $label.prevUntil($prevActive).add($activeLabels).add($label)
5377
              ;
5378
              if(event.shiftKey) {
5379
                $activeLabels.removeClass(className.active);
5380
                $range.addClass(className.active);
5381
              }
5382
              else if(event.ctrlKey) {
5383
                $label.toggleClass(className.active);
5384
              }
5385
              else {
5386
                $activeLabels.removeClass(className.active);
5387
                $label.addClass(className.active);
5388
              }
5389
              settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
5390
            }
5391
          },
5392
          remove: {
5393
            click: function() {
5394
              var
5395
                $label = $(this).parent()
5396
              ;
5397
              if( $label.hasClass(className.active) ) {
5398
                // remove all selected labels
5399
                module.remove.activeLabels();
5400
              }
5401
              else {
5402
                // remove this label only
5403
                module.remove.activeLabels( $label );
5404
              }
5405
            }
5406
          },
5407
          test: {
5408
            toggle: function(event) {
5409
              var
5410
                toggleBehavior = (module.is.multiple())
5411
                  ? module.show
5412
                  : module.toggle
5413
              ;
5414
              if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
5415
                return;
5416
              }
5417
              if( module.determine.eventOnElement(event, toggleBehavior) ) {
5418
                event.preventDefault();
5419
              }
5420
            },
5421
            touch: function(event) {
5422
              module.determine.eventOnElement(event, function() {
5423
                if(event.type == 'touchstart') {
5424
                  module.timer = setTimeout(function() {
5425
                    module.hide();
5426
                  }, settings.delay.touch);
5427
                }
5428
                else if(event.type == 'touchmove') {
5429
                  clearTimeout(module.timer);
5430
                }
5431
              });
5432
              event.stopPropagation();
5433
            },
5434
            hide: function(event) {
5435
              module.determine.eventInModule(event, module.hide);
5436
            }
5437
          },
5438
          select: {
5439
            mutation: function(mutations) {
5440
              module.debug('<select> modified, recreating menu');
5441
              module.setup.select();
5442
            }
5443
          },
5444
          menu: {
5445
            mutation: function(mutations) {
5446
              var
5447
                mutation   = mutations[0],
5448
                $addedNode = mutation.addedNodes
5449
                  ? $(mutation.addedNodes[0])
5450
                  : $(false),
5451
                $removedNode = mutation.removedNodes
5452
                  ? $(mutation.removedNodes[0])
5453
                  : $(false),
5454
                $changedNodes  = $addedNode.add($removedNode),
5455
                isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
5456
                isMessage      = $changedNodes.is(selector.message)  || $changedNodes.closest(selector.message).length > 0
5457
              ;
5458
              if(isUserAddition || isMessage) {
5459
                module.debug('Updating item selector cache');
5460
                module.refreshItems();
5461
              }
5462
              else {
5463
                module.debug('Menu modified, updating selector cache');
5464
                module.refresh();
5465
              }
5466
            },
5467
            mousedown: function() {
5468
              itemActivated = true;
5469
            },
5470
            mouseup: function() {
5471
              itemActivated = false;
5472
            }
5473
          },
5474
          item: {
5475
            mouseenter: function(event) {
5476
              var
5477
                $target        = $(event.target),
5478
                $item          = $(this),
5479
                $subMenu       = $item.children(selector.menu),
5480
                $otherMenus    = $item.siblings(selector.item).children(selector.menu),
5481
                hasSubMenu     = ($subMenu.length > 0),
5482
                isBubbledEvent = ($subMenu.find($target).length > 0)
5483
              ;
5484
              if( !isBubbledEvent && hasSubMenu ) {
5485
                clearTimeout(module.itemTimer);
5486
                module.itemTimer = setTimeout(function() {
5487
                  module.verbose('Showing sub-menu', $subMenu);
5488
                  $.each($otherMenus, function() {
5489
                    module.animate.hide(false, $(this));
5490
                  });
5491
                  module.animate.show(false, $subMenu);
5492
                }, settings.delay.show);
5493
                event.preventDefault();
5494
              }
5495
            },
5496
            mouseleave: function(event) {
5497
              var
5498
                $subMenu = $(this).children(selector.menu)
5499
              ;
5500
              if($subMenu.length > 0) {
5501
                clearTimeout(module.itemTimer);
5502
                module.itemTimer = setTimeout(function() {
5503
                  module.verbose('Hiding sub-menu', $subMenu);
5504
                  module.animate.hide(false, $subMenu);
5505
                }, settings.delay.hide);
5506
              }
5507
            },
5508
            click: function (event, skipRefocus) {
5509
              var
5510
                $choice        = $(this),
5511
                $target        = (event)
5512
                  ? $(event.target)
5513
                  : $(''),
5514
                $subMenu       = $choice.find(selector.menu),
5515
                text           = module.get.choiceText($choice),
5516
                value          = module.get.choiceValue($choice, text),
5517
                hasSubMenu     = ($subMenu.length > 0),
5518
                isBubbledEvent = ($subMenu.find($target).length > 0)
5519
              ;
5520
              // prevents IE11 bug where menu receives focus even though `tabindex=-1`
5521
              if(module.has.menuSearch()) {
5522
                $(document.activeElement).blur();
5523
              }
5524
              if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
5525
                if(module.is.searchSelection()) {
5526
                  if(settings.allowAdditions) {
5527
                    module.remove.userAddition();
5528
                  }
5529
                  module.remove.searchTerm();
5530
                  if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
5531
                    module.focusSearch(true);
5532
                  }
5533
                }
5534
                if(!settings.useLabels) {
5535
                  module.remove.filteredItem();
5536
                  module.set.scrollPosition($choice);
5537
                }
5538
                module.determine.selectAction.call(this, text, value);
5539
              }
5540
            }
5541
          },
5542
5543
          document: {
5544
            // label selection should occur even when element has no focus
5545
            keydown: function(event) {
5546
              var
5547
                pressedKey    = event.which,
5548
                isShortcutKey = module.is.inObject(pressedKey, keys)
5549
              ;
5550
              if(isShortcutKey) {
5551
                var
5552
                  $label            = $module.find(selector.label),
5553
                  $activeLabel      = $label.filter('.' + className.active),
5554
                  activeValue       = $activeLabel.data(metadata.value),
5555
                  labelIndex        = $label.index($activeLabel),
5556
                  labelCount        = $label.length,
5557
                  hasActiveLabel    = ($activeLabel.length > 0),
5558
                  hasMultipleActive = ($activeLabel.length > 1),
5559
                  isFirstLabel      = (labelIndex === 0),
5560
                  isLastLabel       = (labelIndex + 1 == labelCount),
5561
                  isSearch          = module.is.searchSelection(),
5562
                  isFocusedOnSearch = module.is.focusedOnSearch(),
5563
                  isFocused         = module.is.focused(),
5564
                  caretAtStart      = (isFocusedOnSearch && module.get.caretPosition() === 0),
5565
                  $nextLabel
5566
                ;
5567
                if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
5568
                  return;
5569
                }
5570
5571
                if(pressedKey == keys.leftArrow) {
5572
                  // activate previous label
5573
                  if((isFocused || caretAtStart) && !hasActiveLabel) {
5574
                    module.verbose('Selecting previous label');
5575
                    $label.last().addClass(className.active);
5576
                  }
5577
                  else if(hasActiveLabel) {
5578
                    if(!event.shiftKey) {
5579
                      module.verbose('Selecting previous label');
5580
                      $label.removeClass(className.active);
5581
                    }
5582
                    else {
5583
                      module.verbose('Adding previous label to selection');
5584
                    }
5585
                    if(isFirstLabel && !hasMultipleActive) {
5586
                      $activeLabel.addClass(className.active);
5587
                    }
5588
                    else {
5589
                      $activeLabel.prev(selector.siblingLabel)
5590
                        .addClass(className.active)
5591
                        .end()
5592
                      ;
5593
                    }
5594
                    event.preventDefault();
5595
                  }
5596
                }
5597
                else if(pressedKey == keys.rightArrow) {
5598
                  // activate first label
5599
                  if(isFocused && !hasActiveLabel) {
5600
                    $label.first().addClass(className.active);
5601
                  }
5602
                  // activate next label
5603
                  if(hasActiveLabel) {
5604
                    if(!event.shiftKey) {
5605
                      module.verbose('Selecting next label');
5606
                      $label.removeClass(className.active);
5607
                    }
5608
                    else {
5609
                      module.verbose('Adding next label to selection');
5610
                    }
5611
                    if(isLastLabel) {
5612
                      if(isSearch) {
5613
                        if(!isFocusedOnSearch) {
5614
                          module.focusSearch();
5615
                        }
5616
                        else {
5617
                          $label.removeClass(className.active);
5618
                        }
5619
                      }
5620
                      else if(hasMultipleActive) {
5621
                        $activeLabel.next(selector.siblingLabel).addClass(className.active);
5622
                      }
5623
                      else {
5624
                        $activeLabel.addClass(className.active);
5625
                      }
5626
                    }
5627
                    else {
5628
                      $activeLabel.next(selector.siblingLabel).addClass(className.active);
5629
                    }
5630
                    event.preventDefault();
5631
                  }
5632
                }
5633
                else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
5634
                  if(hasActiveLabel) {
5635
                    module.verbose('Removing active labels');
5636
                    if(isLastLabel) {
5637
                      if(isSearch && !isFocusedOnSearch) {
5638
                        module.focusSearch();
5639
                      }
5640
                    }
5641
                    $activeLabel.last().next(selector.siblingLabel).addClass(className.active);
5642
                    module.remove.activeLabels($activeLabel);
5643
                    event.preventDefault();
5644
                  }
5645
                  else if(caretAtStart && !hasActiveLabel && pressedKey == keys.backspace) {
5646
                    module.verbose('Removing last label on input backspace');
5647
                    $activeLabel = $label.last().addClass(className.active);
5648
                    module.remove.activeLabels($activeLabel);
5649
                  }
5650
                }
5651
                else {
5652
                  $activeLabel.removeClass(className.active);
5653
                }
5654
              }
5655
            }
5656
          },
5657
5658
          keydown: function(event) {
5659
            var
5660
              pressedKey    = event.which,
5661
              isShortcutKey = module.is.inObject(pressedKey, keys)
5662
            ;
5663
            if(isShortcutKey) {
5664
              var
5665
                $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
5666
                $activeItem        = $menu.children('.' + className.active).eq(0),
5667
                $selectedItem      = ($currentlySelected.length > 0)
5668
                  ? $currentlySelected
5669
                  : $activeItem,
5670
                $visibleItems = ($selectedItem.length > 0)
5671
                  ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
5672
                  : $menu.children(':not(.' + className.filtered +')'),
5673
                $subMenu              = $selectedItem.children(selector.menu),
5674
                $parentMenu           = $selectedItem.closest(selector.menu),
5675
                inVisibleMenu         = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0),
5676
                hasSubMenu            = ($subMenu.length> 0),
5677
                hasSelectedItem       = ($selectedItem.length > 0),
5678
                selectedIsSelectable  = ($selectedItem.not(selector.unselectable).length > 0),
5679
                delimiterPressed      = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
5680
                isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
5681
                $nextItem,
5682
                isSubMenuItem,
5683
                newIndex
5684
              ;
5685
              // allow selection with menu closed
5686
              if(isAdditionWithoutMenu) {
5687
                module.verbose('Selecting item from keyboard shortcut', $selectedItem);
5688
                module.event.item.click.call($selectedItem, event);
5689
                if(module.is.searchSelection()) {
5690
                  module.remove.searchTerm();
5691
                }
5692
              }
5693
5694
              // visible menu keyboard shortcuts
5695
              if( module.is.visible() ) {
5696
5697
                // enter (select or open sub-menu)
5698
                if(pressedKey == keys.enter || delimiterPressed) {
5699
                  if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
5700
                    module.verbose('Pressed enter on unselectable category, opening sub menu');
5701
                    pressedKey = keys.rightArrow;
5702
                  }
5703
                  else if(selectedIsSelectable) {
5704
                    module.verbose('Selecting item from keyboard shortcut', $selectedItem);
5705
                    module.event.item.click.call($selectedItem, event);
5706
                    if(module.is.searchSelection()) {
5707
                      module.remove.searchTerm();
5708
                    }
5709
                  }
5710
                  event.preventDefault();
5711
                }
5712
5713
                // sub-menu actions
5714
                if(hasSelectedItem) {
5715
5716
                  if(pressedKey == keys.leftArrow) {
5717
5718
                    isSubMenuItem = ($parentMenu[0] !== $menu[0]);
5719
5720
                    if(isSubMenuItem) {
5721
                      module.verbose('Left key pressed, closing sub-menu');
5722
                      module.animate.hide(false, $parentMenu);
5723
                      $selectedItem
5724
                        .removeClass(className.selected)
5725
                      ;
5726
                      $parentMenu
5727
                        .closest(selector.item)
5728
                          .addClass(className.selected)
5729
                      ;
5730
                      event.preventDefault();
5731
                    }
5732
                  }
5733
5734
                  // right arrow (show sub-menu)
5735
                  if(pressedKey == keys.rightArrow) {
5736
                    if(hasSubMenu) {
5737
                      module.verbose('Right key pressed, opening sub-menu');
5738
                      module.animate.show(false, $subMenu);
5739
                      $selectedItem
5740
                        .removeClass(className.selected)
5741
                      ;
5742
                      $subMenu
5743
                        .find(selector.item).eq(0)
5744
                          .addClass(className.selected)
5745
                      ;
5746
                      event.preventDefault();
5747
                    }
5748
                  }
5749
                }
5750
5751
                // up arrow (traverse menu up)
5752
                if(pressedKey == keys.upArrow) {
5753
                  $nextItem = (hasSelectedItem && inVisibleMenu)
5754
                    ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
5755
                    : $item.eq(0)
5756
                  ;
5757
                  if($visibleItems.index( $nextItem ) < 0) {
5758
                    module.verbose('Up key pressed but reached top of current menu');
5759
                    event.preventDefault();
5760
                    return;
5761
                  }
5762
                  else {
5763
                    module.verbose('Up key pressed, changing active item');
5764
                    $selectedItem
5765
                      .removeClass(className.selected)
5766
                    ;
5767
                    $nextItem
5768
                      .addClass(className.selected)
5769
                    ;
5770
                    module.set.scrollPosition($nextItem);
5771
                    if(settings.selectOnKeydown && module.is.single()) {
5772
                      module.set.selectedItem($nextItem);
5773
                    }
5774
                  }
5775
                  event.preventDefault();
5776
                }
5777
5778
                // down arrow (traverse menu down)
5779
                if(pressedKey == keys.downArrow) {
5780
                  $nextItem = (hasSelectedItem && inVisibleMenu)
5781
                    ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
5782
                    : $item.eq(0)
5783
                  ;
5784
                  if($nextItem.length === 0) {
5785
                    module.verbose('Down key pressed but reached bottom of current menu');
5786
                    event.preventDefault();
5787
                    return;
5788
                  }
5789
                  else {
5790
                    module.verbose('Down key pressed, changing active item');
5791
                    $item
5792
                      .removeClass(className.selected)
5793
                    ;
5794
                    $nextItem
5795
                      .addClass(className.selected)
5796
                    ;
5797
                    module.set.scrollPosition($nextItem);
5798
                    if(settings.selectOnKeydown && module.is.single()) {
5799
                      module.set.selectedItem($nextItem);
5800
                    }
5801
                  }
5802
                  event.preventDefault();
5803
                }
5804
5805
                // page down (show next page)
5806
                if(pressedKey == keys.pageUp) {
5807
                  module.scrollPage('up');
5808
                  event.preventDefault();
5809
                }
5810
                if(pressedKey == keys.pageDown) {
5811
                  module.scrollPage('down');
5812
                  event.preventDefault();
5813
                }
5814
5815
                // escape (close menu)
5816
                if(pressedKey == keys.escape) {
5817
                  module.verbose('Escape key pressed, closing dropdown');
5818
                  module.hide();
5819
                }
5820
5821
              }
5822
              else {
5823
                // delimiter key
5824
                if(delimiterPressed) {
5825
                  event.preventDefault();
5826
                }
5827
                // down arrow (open menu)
5828
                if(pressedKey == keys.downArrow && !module.is.visible()) {
5829
                  module.verbose('Down key pressed, showing dropdown');
5830
                  module.show();
5831
                  event.preventDefault();
5832
                }
5833
              }
5834
            }
5835
            else {
5836
              if( !module.has.search() ) {
5837
                module.set.selectedLetter( String.fromCharCode(pressedKey) );
5838
              }
5839
            }
5840
          }
5841
        },
5842
5843
        trigger: {
5844
          change: function() {
5845
            var
5846
              events       = document.createEvent('HTMLEvents'),
5847
              inputElement = $input[0]
5848
            ;
5849
            if(inputElement) {
5850
              module.verbose('Triggering native change event');
5851
              events.initEvent('change', true, false);
5852
              inputElement.dispatchEvent(events);
5853
            }
5854
          }
5855
        },
5856
5857
        determine: {
5858
          selectAction: function(text, value) {
5859
            module.verbose('Determining action', settings.action);
5860
            if( $.isFunction( module.action[settings.action] ) ) {
5861
              module.verbose('Triggering preset action', settings.action, text, value);
5862
              module.action[ settings.action ].call(element, text, value, this);
5863
            }
5864
            else if( $.isFunction(settings.action) ) {
5865
              module.verbose('Triggering user action', settings.action, text, value);
5866
              settings.action.call(element, text, value, this);
5867
            }
5868
            else {
5869
              module.error(error.action, settings.action);
5870
            }
5871
          },
5872
          eventInModule: function(event, callback) {
5873
            var
5874
              $target    = $(event.target),
5875
              inDocument = ($target.closest(document.documentElement).length > 0),
5876
              inModule   = ($target.closest($module).length > 0)
5877
            ;
5878
            callback = $.isFunction(callback)
5879
              ? callback
5880
              : function(){}
5881
            ;
5882
            if(inDocument && !inModule) {
5883
              module.verbose('Triggering event', callback);
5884
              callback();
5885
              return true;
5886
            }
5887
            else {
5888
              module.verbose('Event occurred in dropdown, canceling callback');
5889
              return false;
5890
            }
5891
          },
5892
          eventOnElement: function(event, callback) {
5893
            var
5894
              $target      = $(event.target),
5895
              $label       = $target.closest(selector.siblingLabel),
5896
              inVisibleDOM = document.body.contains(event.target),
5897
              notOnLabel   = ($module.find($label).length === 0),
5898
              notInMenu    = ($target.closest($menu).length === 0)
5899
            ;
5900
            callback = $.isFunction(callback)
5901
              ? callback
5902
              : function(){}
5903
            ;
5904
            if(inVisibleDOM && notOnLabel && notInMenu) {
5905
              module.verbose('Triggering event', callback);
5906
              callback();
5907
              return true;
5908
            }
5909
            else {
5910
              module.verbose('Event occurred in dropdown menu, canceling callback');
5911
              return false;
5912
            }
5913
          }
5914
        },
5915
5916
        action: {
5917
5918
          nothing: function() {},
5919
5920
          activate: function(text, value, element) {
5921
            value = (value !== undefined)
5922
              ? value
5923
              : text
5924
            ;
5925
            if( module.can.activate( $(element) ) ) {
5926
              module.set.selected(value, $(element));
5927
              if(module.is.multiple() && !module.is.allFiltered()) {
5928
                return;
5929
              }
5930
              else {
5931
                module.hideAndClear();
5932
              }
5933
            }
5934
          },
5935
5936
          select: function(text, value, element) {
5937
            value = (value !== undefined)
5938
              ? value
5939
              : text
5940
            ;
5941
            if( module.can.activate( $(element) ) ) {
5942
              module.set.value(value, $(element));
5943
              if(module.is.multiple() && !module.is.allFiltered()) {
5944
                return;
5945
              }
5946
              else {
5947
                module.hideAndClear();
5948
              }
5949
            }
5950
          },
5951
5952
          combo: function(text, value, element) {
5953
            value = (value !== undefined)
5954
              ? value
5955
              : text
5956
            ;
5957
            module.set.selected(value, $(element));
5958
            module.hideAndClear();
5959
          },
5960
5961
          hide: function(text, value, element) {
5962
            module.set.value(value, text);
5963
            module.hideAndClear();
5964
          }
5965
5966
        },
5967
5968
        get: {
5969
          id: function() {
5970
            return id;
5971
          },
5972
          defaultText: function() {
5973
            return $module.data(metadata.defaultText);
5974
          },
5975
          defaultValue: function() {
5976
            return $module.data(metadata.defaultValue);
5977
          },
5978
          placeholderText: function() {
5979
            return $module.data(metadata.placeholderText) || '';
5980
          },
5981
          text: function() {
5982
            return $text.text();
5983
          },
5984
          query: function() {
5985
            return $.trim($search.val());
5986
          },
5987
          searchWidth: function(value) {
5988
            value = (value !== undefined)
5989
              ? value
5990
              : $search.val()
5991
            ;
5992
            $sizer.text(value);
5993
            // prevent rounding issues
5994
            return Math.ceil( $sizer.width() + 1);
5995
          },
5996
          selectionCount: function() {
5997
            var
5998
              values = module.get.values(),
5999
              count
6000
            ;
6001
            count = ( module.is.multiple() )
6002
              ? $.isArray(values)
6003
                ? values.length
6004
                : 0
6005
              : (module.get.value() !== '')
6006
                ? 1
6007
                : 0
6008
            ;
6009
            return count;
6010
          },
6011
          transition: function($subMenu) {
6012
            return (settings.transition == 'auto')
6013
              ? module.is.upward($subMenu)
6014
                ? 'slide up'
6015
                : 'slide down'
6016
              : settings.transition
6017
            ;
6018
          },
6019
          userValues: function() {
6020
            var
6021
              values = module.get.values()
6022
            ;
6023
            if(!values) {
6024
              return false;
6025
            }
6026
            values = $.isArray(values)
6027
              ? values
6028
              : [values]
6029
            ;
6030
            return $.grep(values, function(value) {
6031
              return (module.get.item(value) === false);
6032
            });
6033
          },
6034
          uniqueArray: function(array) {
6035
            return $.grep(array, function (value, index) {
6036
                return $.inArray(value, array) === index;
6037
            });
6038
          },
6039
          caretPosition: function() {
6040
            var
6041
              input = $search.get(0),
6042
              range,
6043
              rangeLength
6044
            ;
6045
            if('selectionStart' in input) {
6046
              return input.selectionStart;
6047
            }
6048
            else if (document.selection) {
6049
              input.focus();
6050
              range       = document.selection.createRange();
6051
              rangeLength = range.text.length;
6052
              range.moveStart('character', -input.value.length);
6053
              return range.text.length - rangeLength;
6054
            }
6055
          },
6056
          value: function() {
6057
            var
6058
              value = ($input.length > 0)
6059
                ? $input.val()
6060
                : $module.data(metadata.value),
6061
              isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '')
6062
            ;
6063
            // prevents placeholder element from being selected when multiple
6064
            return (value === undefined || isEmptyMultiselect)
6065
              ? ''
6066
              : value
6067
            ;
6068
          },
6069
          values: function() {
6070
            var
6071
              value = module.get.value()
6072
            ;
6073
            if(value === '') {
6074
              return '';
6075
            }
6076
            return ( !module.has.selectInput() && module.is.multiple() )
6077
              ? (typeof value == 'string') // delimited string
6078
                ? value.split(settings.delimiter)
6079
                : ''
6080
              : value
6081
            ;
6082
          },
6083
          remoteValues: function() {
6084
            var
6085
              values = module.get.values(),
6086
              remoteValues = false
6087
            ;
6088
            if(values) {
6089
              if(typeof values == 'string') {
6090
                values = [values];
6091
              }
6092
              $.each(values, function(index, value) {
6093
                var
6094
                  name = module.read.remoteData(value)
6095
                ;
6096
                module.verbose('Restoring value from session data', name, value);
6097
                if(name) {
6098
                  if(!remoteValues) {
6099
                    remoteValues = {};
6100
                  }
6101
                  remoteValues[value] = name;
6102
                }
6103
              });
6104
            }
6105
            return remoteValues;
6106
          },
6107
          choiceText: function($choice, preserveHTML) {
6108
            preserveHTML = (preserveHTML !== undefined)
6109
              ? preserveHTML
6110
              : settings.preserveHTML
6111
            ;
6112
            if($choice) {
6113
              if($choice.find(selector.menu).length > 0) {
6114
                module.verbose('Retrieving text of element with sub-menu');
6115
                $choice = $choice.clone();
6116
                $choice.find(selector.menu).remove();
6117
                $choice.find(selector.menuIcon).remove();
6118
              }
6119
              return ($choice.data(metadata.text) !== undefined)
6120
                ? $choice.data(metadata.text)
6121
                : (preserveHTML)
6122
                  ? $.trim($choice.html())
6123
                  : $.trim($choice.text())
6124
              ;
6125
            }
6126
          },
6127
          choiceValue: function($choice, choiceText) {
6128
            choiceText = choiceText || module.get.choiceText($choice);
6129
            if(!$choice) {
6130
              return false;
6131
            }
6132
            return ($choice.data(metadata.value) !== undefined)
6133
              ? String( $choice.data(metadata.value) )
6134
              : (typeof choiceText === 'string')
6135
                ? $.trim(choiceText.toLowerCase())
6136
                : String(choiceText)
6137
            ;
6138
          },
6139
          inputEvent: function() {
6140
            var
6141
              input = $search[0]
6142
            ;
6143
            if(input) {
6144
              return (input.oninput !== undefined)
6145
                ? 'input'
6146
                : (input.onpropertychange !== undefined)
6147
                  ? 'propertychange'
6148
                  : 'keyup'
6149
              ;
6150
            }
6151
            return false;
6152
          },
6153
          selectValues: function() {
6154
            var
6155
              select = {}
6156
            ;
6157
            select.values = [];
6158
            $module
6159
              .find('option')
6160
                .each(function() {
6161
                  var
6162
                    $option  = $(this),
6163
                    name     = $option.html(),
6164
                    disabled = $option.attr('disabled'),
6165
                    value    = ( $option.attr('value') !== undefined )
6166
                      ? $option.attr('value')
6167
                      : name
6168
                  ;
6169
                  if(settings.placeholder === 'auto' && value === '') {
6170
                    select.placeholder = name;
6171
                  }
6172
                  else {
6173
                    select.values.push({
6174
                      name     : name,
6175
                      value    : value,
6176
                      disabled : disabled
6177
                    });
6178
                  }
6179
                })
6180
            ;
6181
            if(settings.placeholder && settings.placeholder !== 'auto') {
6182
              module.debug('Setting placeholder value to', settings.placeholder);
6183
              select.placeholder = settings.placeholder;
6184
            }
6185
            if(settings.sortSelect) {
6186
              select.values.sort(function(a, b) {
6187
                return (a.name > b.name)
6188
                  ? 1
6189
                  : -1
6190
                ;
6191
              });
6192
              module.debug('Retrieved and sorted values from select', select);
6193
            }
6194
            else {
6195
              module.debug('Retrieved values from select', select);
6196
            }
6197
            return select;
6198
          },
6199
          activeItem: function() {
6200
            return $item.filter('.'  + className.active);
6201
          },
6202
          selectedItem: function() {
6203
            var
6204
              $selectedItem = $item.not(selector.unselectable).filter('.'  + className.selected)
6205
            ;
6206
            return ($selectedItem.length > 0)
6207
              ? $selectedItem
6208
              : $item.eq(0)
6209
            ;
6210
          },
6211
          itemWithAdditions: function(value) {
6212
            var
6213
              $items       = module.get.item(value),
6214
              $userItems   = module.create.userChoice(value),
6215
              hasUserItems = ($userItems && $userItems.length > 0)
6216
            ;
6217
            if(hasUserItems) {
6218
              $items = ($items.length > 0)
6219
                ? $items.add($userItems)
6220
                : $userItems
6221
              ;
6222
            }
6223
            return $items;
6224
          },
6225
          item: function(value, strict) {
6226
            var
6227
              $selectedItem = false,
6228
              shouldSearch,
6229
              isMultiple
6230
            ;
6231
            value = (value !== undefined)
6232
              ? value
6233
              : ( module.get.values() !== undefined)
6234
                ? module.get.values()
6235
                : module.get.text()
6236
            ;
6237
            shouldSearch = (isMultiple)
6238
              ? (value.length > 0)
6239
              : (value !== undefined && value !== null)
6240
            ;
6241
            isMultiple = (module.is.multiple() && $.isArray(value));
6242
            strict     = (value === '' || value === 0)
6243
              ? true
6244
              : strict || false
6245
            ;
6246
            if(shouldSearch) {
6247
              $item
6248
                .each(function() {
6249
                  var
6250
                    $choice       = $(this),
6251
                    optionText    = module.get.choiceText($choice),
6252
                    optionValue   = module.get.choiceValue($choice, optionText)
6253
                  ;
6254
                  // safe early exit
6255
                  if(optionValue === null || optionValue === undefined) {
6256
                    return;
6257
                  }
6258
                  if(isMultiple) {
6259
                    if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) {
6260
                      $selectedItem = ($selectedItem)
6261
                        ? $selectedItem.add($choice)
6262
                        : $choice
6263
                      ;
6264
                    }
6265
                  }
6266
                  else if(strict) {
6267
                    module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
6268
                    if( optionValue === value || optionText === value) {
6269
                      $selectedItem = $choice;
6270
                      return true;
6271
                    }
6272
                  }
6273
                  else {
6274
                    if( String(optionValue) == String(value) || optionText == value) {
6275
                      module.verbose('Found select item by value', optionValue, value);
6276
                      $selectedItem = $choice;
6277
                      return true;
6278
                    }
6279
                  }
6280
                })
6281
              ;
6282
            }
6283
            return $selectedItem;
6284
          }
6285
        },
6286
6287
        check: {
6288
          maxSelections: function(selectionCount) {
6289
            if(settings.maxSelections) {
6290
              selectionCount = (selectionCount !== undefined)
6291
                ? selectionCount
6292
                : module.get.selectionCount()
6293
              ;
6294
              if(selectionCount >= settings.maxSelections) {
6295
                module.debug('Maximum selection count reached');
6296
                if(settings.useLabels) {
6297
                  $item.addClass(className.filtered);
6298
                  module.add.message(message.maxSelections);
6299
                }
6300
                return true;
6301
              }
6302
              else {
6303
                module.verbose('No longer at maximum selection count');
6304
                module.remove.message();
6305
                module.remove.filteredItem();
6306
                if(module.is.searchSelection()) {
6307
                  module.filterItems();
6308
                }
6309
                return false;
6310
              }
6311
            }
6312
            return true;
6313
          }
6314
        },
6315
6316
        restore: {
6317
          defaults: function() {
6318
            module.clear();
6319
            module.restore.defaultText();
6320
            module.restore.defaultValue();
6321
          },
6322
          defaultText: function() {
6323
            var
6324
              defaultText     = module.get.defaultText(),
6325
              placeholderText = module.get.placeholderText
6326
            ;
6327
            if(defaultText === placeholderText) {
6328
              module.debug('Restoring default placeholder text', defaultText);
6329
              module.set.placeholderText(defaultText);
6330
            }
6331
            else {
6332
              module.debug('Restoring default text', defaultText);
6333
              module.set.text(defaultText);
6334
            }
6335
          },
6336
          placeholderText: function() {
6337
            module.set.placeholderText();
6338
          },
6339
          defaultValue: function() {
6340
            var
6341
              defaultValue = module.get.defaultValue()
6342
            ;
6343
            if(defaultValue !== undefined) {
6344
              module.debug('Restoring default value', defaultValue);
6345
              if(defaultValue !== '') {
6346
                module.set.value(defaultValue);
6347
                module.set.selected();
6348
              }
6349
              else {
6350
                module.remove.activeItem();
6351
                module.remove.selectedItem();
6352
              }
6353
            }
6354
          },
6355
          labels: function() {
6356
            if(settings.allowAdditions) {
6357
              if(!settings.useLabels) {
6358
                module.error(error.labels);
6359
                settings.useLabels = true;
6360
              }
6361
              module.debug('Restoring selected values');
6362
              module.create.userLabels();
6363
            }
6364
            module.check.maxSelections();
6365
          },
6366
          selected: function() {
6367
            module.restore.values();
6368
            if(module.is.multiple()) {
6369
              module.debug('Restoring previously selected values and labels');
6370
              module.restore.labels();
6371
            }
6372
            else {
6373
              module.debug('Restoring previously selected values');
6374
            }
6375
          },
6376
          values: function() {
6377
            // prevents callbacks from occurring on initial load
6378
            module.set.initialLoad();
6379
            if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
6380
              module.restore.remoteValues();
6381
            }
6382
            else {
6383
              module.set.selected();
6384
            }
6385
            module.remove.initialLoad();
6386
          },
6387
          remoteValues: function() {
6388
            var
6389
              values = module.get.remoteValues()
6390
            ;
6391
            module.debug('Recreating selected from session data', values);
6392
            if(values) {
6393
              if( module.is.single() ) {
6394
                $.each(values, function(value, name) {
6395
                  module.set.text(name);
6396
                });
6397
              }
6398
              else {
6399
                $.each(values, function(value, name) {
6400
                  module.add.label(value, name);
6401
                });
6402
              }
6403
            }
6404
          }
6405
        },
6406
6407
        read: {
6408
          remoteData: function(value) {
6409
            var
6410
              name
6411
            ;
6412
            if(window.Storage === undefined) {
6413
              module.error(error.noStorage);
6414
              return;
6415
            }
6416
            name = sessionStorage.getItem(value);
6417
            return (name !== undefined)
6418
              ? name
6419
              : false
6420
            ;
6421
          }
6422
        },
6423
6424
        save: {
6425
          defaults: function() {
6426
            module.save.defaultText();
6427
            module.save.placeholderText();
6428
            module.save.defaultValue();
6429
          },
6430
          defaultValue: function() {
6431
            var
6432
              value = module.get.value()
6433
            ;
6434
            module.verbose('Saving default value as', value);
6435
            $module.data(metadata.defaultValue, value);
6436
          },
6437
          defaultText: function() {
6438
            var
6439
              text = module.get.text()
6440
            ;
6441
            module.verbose('Saving default text as', text);
6442
            $module.data(metadata.defaultText, text);
6443
          },
6444
          placeholderText: function() {
6445
            var
6446
              text
6447
            ;
6448
            if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
6449
              text = module.get.text();
6450
              module.verbose('Saving placeholder text as', text);
6451
              $module.data(metadata.placeholderText, text);
6452
            }
6453
          },
6454
          remoteData: function(name, value) {
6455
            if(window.Storage === undefined) {
6456
              module.error(error.noStorage);
6457
              return;
6458
            }
6459
            module.verbose('Saving remote data to session storage', value, name);
6460
            sessionStorage.setItem(value, name);
6461
          }
6462
        },
6463
6464
        clear: function() {
6465
          if(module.is.multiple() && settings.useLabels) {
6466
            module.remove.labels();
6467
          }
6468
          else {
6469
            module.remove.activeItem();
6470
            module.remove.selectedItem();
6471
          }
6472
          module.set.placeholderText();
6473
          module.clearValue();
6474
        },
6475
6476
        clearValue: function() {
6477
          module.set.value('');
6478
        },
6479
6480
        scrollPage: function(direction, $selectedItem) {
6481
          var
6482
            $currentItem  = $selectedItem || module.get.selectedItem(),
6483
            $menu         = $currentItem.closest(selector.menu),
6484
            menuHeight    = $menu.outerHeight(),
6485
            currentScroll = $menu.scrollTop(),
6486
            itemHeight    = $item.eq(0).outerHeight(),
6487
            itemsPerPage  = Math.floor(menuHeight / itemHeight),
6488
            maxScroll     = $menu.prop('scrollHeight'),
6489
            newScroll     = (direction == 'up')
6490
              ? currentScroll - (itemHeight * itemsPerPage)
6491
              : currentScroll + (itemHeight * itemsPerPage),
6492
            $selectableItem = $item.not(selector.unselectable),
6493
            isWithinRange,
6494
            $nextSelectedItem,
6495
            elementIndex
6496
          ;
6497
          elementIndex      = (direction == 'up')
6498
            ? $selectableItem.index($currentItem) - itemsPerPage
6499
            : $selectableItem.index($currentItem) + itemsPerPage
6500
          ;
6501
          isWithinRange = (direction == 'up')
6502
            ? (elementIndex >= 0)
6503
            : (elementIndex < $selectableItem.length)
6504
          ;
6505
          $nextSelectedItem = (isWithinRange)
6506
            ? $selectableItem.eq(elementIndex)
6507
            : (direction == 'up')
6508
              ? $selectableItem.first()
6509
              : $selectableItem.last()
6510
          ;
6511
          if($nextSelectedItem.length > 0) {
6512
            module.debug('Scrolling page', direction, $nextSelectedItem);
6513
            $currentItem
6514
              .removeClass(className.selected)
6515
            ;
6516
            $nextSelectedItem
6517
              .addClass(className.selected)
6518
            ;
6519
            if(settings.selectOnKeydown && module.is.single()) {
6520
              module.set.selectedItem($nextSelectedItem);
6521
            }
6522
            $menu
6523
              .scrollTop(newScroll)
6524
            ;
6525
          }
6526
        },
6527
6528
        set: {
6529
          filtered: function() {
6530
            var
6531
              isMultiple       = module.is.multiple(),
6532
              isSearch         = module.is.searchSelection(),
6533
              isSearchMultiple = (isMultiple && isSearch),
6534
              searchValue      = (isSearch)
6535
                ? module.get.query()
6536
                : '',
6537
              hasSearchValue   = (typeof searchValue === 'string' && searchValue.length > 0),
6538
              searchWidth      = module.get.searchWidth(),
6539
              valueIsSet       = searchValue !== ''
6540
            ;
6541
            if(isMultiple && hasSearchValue) {
6542
              module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
6543
              $search.css('width', searchWidth);
6544
            }
6545
            if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
6546
              module.verbose('Hiding placeholder text');
6547
              $text.addClass(className.filtered);
6548
            }
6549
            else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
6550
              module.verbose('Showing placeholder text');
6551
              $text.removeClass(className.filtered);
6552
            }
6553
          },
6554
          empty: function() {
6555
            $module.addClass(className.empty);
6556
          },
6557
          loading: function() {
6558
            $module.addClass(className.loading);
6559
          },
6560
          placeholderText: function(text) {
6561
            text = text || module.get.placeholderText();
6562
            module.debug('Setting placeholder text', text);
6563
            module.set.text(text);
6564
            $text.addClass(className.placeholder);
6565
          },
6566
          tabbable: function() {
6567
            if( module.is.searchSelection() ) {
6568
              module.debug('Added tabindex to searchable dropdown');
6569
              $search
6570
                .val('')
6571
                .attr('tabindex', 0)
6572
              ;
6573
              $menu
6574
                .attr('tabindex', -1)
6575
              ;
6576
            }
6577
            else {
6578
              module.debug('Added tabindex to dropdown');
6579
              if( $module.attr('tabindex') === undefined) {
6580
                $module
6581
                  .attr('tabindex', 0)
6582
                ;
6583
                $menu
6584
                  .attr('tabindex', -1)
6585
                ;
6586
              }
6587
            }
6588
          },
6589
          initialLoad: function() {
6590
            module.verbose('Setting initial load');
6591
            initialLoad = true;
6592
          },
6593
          activeItem: function($item) {
6594
            if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
6595
              $item.addClass(className.filtered);
6596
            }
6597
            else {
6598
              $item.addClass(className.active);
6599
            }
6600
          },
6601
          partialSearch: function(text) {
6602
            var
6603
              length = module.get.query().length
6604
            ;
6605
            $search.val( text.substr(0 , length));
6606
          },
6607
          scrollPosition: function($item, forceScroll) {
6608
            var
6609
              edgeTolerance = 5,
6610
              $menu,
6611
              hasActive,
6612
              offset,
6613
              itemHeight,
6614
              itemOffset,
6615
              menuOffset,
6616
              menuScroll,
6617
              menuHeight,
6618
              abovePage,
6619
              belowPage
6620
            ;
6621
6622
            $item       = $item || module.get.selectedItem();
6623
            $menu       = $item.closest(selector.menu);
6624
            hasActive   = ($item && $item.length > 0);
6625
            forceScroll = (forceScroll !== undefined)
6626
              ? forceScroll
6627
              : false
6628
            ;
6629
            if($item && $menu.length > 0 && hasActive) {
6630
              itemOffset = $item.position().top;
6631
6632
              $menu.addClass(className.loading);
6633
              menuScroll = $menu.scrollTop();
6634
              menuOffset = $menu.offset().top;
6635
              itemOffset = $item.offset().top;
6636
              offset     = menuScroll - menuOffset + itemOffset;
6637
              if(!forceScroll) {
6638
                menuHeight = $menu.height();
6639
                belowPage  = menuScroll + menuHeight < (offset + edgeTolerance);
6640
                abovePage  = ((offset - edgeTolerance) < menuScroll);
6641
              }
6642
              module.debug('Scrolling to active item', offset);
6643
              if(forceScroll || abovePage || belowPage) {
6644
                $menu.scrollTop(offset);
6645
              }
6646
              $menu.removeClass(className.loading);
6647
            }
6648
          },
6649
          text: function(text) {
6650
            if(settings.action !== 'select') {
6651
              if(settings.action == 'combo') {
6652
                module.debug('Changing combo button text', text, $combo);
6653
                if(settings.preserveHTML) {
6654
                  $combo.html(text);
6655
                }
6656
                else {
6657
                  $combo.text(text);
6658
                }
6659
              }
6660
              else {
6661
                if(text !== module.get.placeholderText()) {
6662
                  $text.removeClass(className.placeholder);
6663
                }
6664
                module.debug('Changing text', text, $text);
6665
                $text
6666
                  .removeClass(className.filtered)
6667
                ;
6668
                if(settings.preserveHTML) {
6669
                  $text.html(text);
6670
                }
6671
                else {
6672
                  $text.text(text);
6673
                }
6674
              }
6675
            }
6676
          },
6677
          selectedItem: function($item) {
6678
            var
6679
              value      = module.get.choiceValue($item),
6680
              searchText = module.get.choiceText($item, false),
6681
              text       = module.get.choiceText($item, true)
6682
            ;
6683
            module.debug('Setting user selection to item', $item);
6684
            module.remove.activeItem();
6685
            module.set.partialSearch(searchText);
6686
            module.set.activeItem($item);
6687
            module.set.selected(value, $item);
6688
            module.set.text(text);
6689
          },
6690
          selectedLetter: function(letter) {
6691
            var
6692
              $selectedItem         = $item.filter('.' + className.selected),
6693
              alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
6694
              $nextValue            = false,
6695
              $nextItem
6696
            ;
6697
            // check next of same letter
6698
            if(alreadySelectedLetter) {
6699
              $nextItem = $selectedItem.nextAll($item).eq(0);
6700
              if( module.has.firstLetter($nextItem, letter) ) {
6701
                $nextValue  = $nextItem;
6702
              }
6703
            }
6704
            // check all values
6705
            if(!$nextValue) {
6706
              $item
6707
                .each(function(){
6708
                  if(module.has.firstLetter($(this), letter)) {
6709
                    $nextValue = $(this);
6710
                    return false;
6711
                  }
6712
                })
6713
              ;
6714
            }
6715
            // set next value
6716
            if($nextValue) {
6717
              module.verbose('Scrolling to next value with letter', letter);
6718
              module.set.scrollPosition($nextValue);
6719
              $selectedItem.removeClass(className.selected);
6720
              $nextValue.addClass(className.selected);
6721
              if(settings.selectOnKeydown && module.is.single()) {
6722
                module.set.selectedItem($nextValue);
6723
              }
6724
            }
6725
          },
6726
          direction: function($menu) {
6727
            if(settings.direction == 'auto') {
6728
              // reset position
6729
              module.remove.upward();
6730
6731
              if(module.can.openDownward($menu)) {
6732
                module.remove.upward($menu);
6733
              }
6734
              else {
6735
                module.set.upward($menu);
6736
              }
6737
              if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
6738
                module.set.leftward($menu);
6739
              }
6740
            }
6741
            else if(settings.direction == 'upward') {
6742
              module.set.upward($menu);
6743
            }
6744
          },
6745
          upward: function($currentMenu) {
6746
            var $element = $currentMenu || $module;
6747
            $element.addClass(className.upward);
6748
          },
6749
          leftward: function($currentMenu) {
6750
            var $element = $currentMenu || $menu;
6751
            $element.addClass(className.leftward);
6752
          },
6753
          value: function(value, text, $selected) {
6754
            var
6755
              escapedValue = module.escape.value(value),
6756
              hasInput     = ($input.length > 0),
6757
              isAddition   = !module.has.value(value),
6758
              currentValue = module.get.values(),
6759
              stringValue  = (value !== undefined)
6760
                ? String(value)
6761
                : value,
6762
              newValue
6763
            ;
6764
            if(hasInput) {
6765
              if(!settings.allowReselection && stringValue == currentValue) {
6766
                module.verbose('Skipping value update already same value', value, currentValue);
6767
                if(!module.is.initialLoad()) {
6768
                  return;
6769
                }
6770
              }
6771
6772
              if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
6773
                module.debug('Adding user option', value);
6774
                module.add.optionValue(value);
6775
              }
6776
              module.debug('Updating input value', escapedValue, currentValue);
6777
              internalChange = true;
6778
              $input
6779
                .val(escapedValue)
6780
              ;
6781
              if(settings.fireOnInit === false && module.is.initialLoad()) {
6782
                module.debug('Input native change event ignored on initial load');
6783
              }
6784
              else {
6785
                module.trigger.change();
6786
              }
6787
              internalChange = false;
6788
            }
6789
            else {
6790
              module.verbose('Storing value in metadata', escapedValue, $input);
6791
              if(escapedValue !== currentValue) {
6792
                $module.data(metadata.value, stringValue);
6793
              }
6794
            }
6795
            if(settings.fireOnInit === false && module.is.initialLoad()) {
6796
              module.verbose('No callback on initial load', settings.onChange);
6797
            }
6798
            else {
6799
              settings.onChange.call(element, value, text, $selected);
6800
            }
6801
          },
6802
          active: function() {
6803
            $module
6804
              .addClass(className.active)
6805
            ;
6806
          },
6807
          multiple: function() {
6808
            $module.addClass(className.multiple);
6809
          },
6810
          visible: function() {
6811
            $module.addClass(className.visible);
6812
          },
6813
          exactly: function(value, $selectedItem) {
6814
            module.debug('Setting selected to exact values');
6815
            module.clear();
6816
            module.set.selected(value, $selectedItem);
6817
          },
6818
          selected: function(value, $selectedItem) {
6819
            var
6820
              isMultiple = module.is.multiple(),
6821
              $userSelectedItem
6822
            ;
6823
            $selectedItem = (settings.allowAdditions)
6824
              ? $selectedItem || module.get.itemWithAdditions(value)
6825
              : $selectedItem || module.get.item(value)
6826
            ;
6827
            if(!$selectedItem) {
6828
              return;
6829
            }
6830
            module.debug('Setting selected menu item to', $selectedItem);
6831
            if(module.is.multiple()) {
6832
              module.remove.searchWidth();
6833
            }
6834
            if(module.is.single()) {
6835
              module.remove.activeItem();
6836
              module.remove.selectedItem();
6837
            }
6838
            else if(settings.useLabels) {
6839
              module.remove.selectedItem();
6840
            }
6841
            // select each item
6842
            $selectedItem
6843
              .each(function() {
6844
                var
6845
                  $selected      = $(this),
6846
                  selectedText   = module.get.choiceText($selected),
6847
                  selectedValue  = module.get.choiceValue($selected, selectedText),
6848
6849
                  isFiltered     = $selected.hasClass(className.filtered),
6850
                  isActive       = $selected.hasClass(className.active),
6851
                  isUserValue    = $selected.hasClass(className.addition),
6852
                  shouldAnimate  = (isMultiple && $selectedItem.length == 1)
6853
                ;
6854
                if(isMultiple) {
6855
                  if(!isActive || isUserValue) {
6856
                    if(settings.apiSettings && settings.saveRemoteData) {
6857
                      module.save.remoteData(selectedText, selectedValue);
6858
                    }
6859
                    if(settings.useLabels) {
6860
                      module.add.value(selectedValue, selectedText, $selected);
6861
                      module.add.label(selectedValue, selectedText, shouldAnimate);
6862
                      module.set.activeItem($selected);
6863
                      module.filterActive();
6864
                      module.select.nextAvailable($selectedItem);
6865
                    }
6866
                    else {
6867
                      module.add.value(selectedValue, selectedText, $selected);
6868
                      module.set.text(module.add.variables(message.count));
6869
                      module.set.activeItem($selected);
6870
                    }
6871
                  }
6872
                  else if(!isFiltered) {
6873
                    module.debug('Selected active value, removing label');
6874
                    module.remove.selected(selectedValue);
6875
                  }
6876
                }
6877
                else {
6878
                  if(settings.apiSettings && settings.saveRemoteData) {
6879
                    module.save.remoteData(selectedText, selectedValue);
6880
                  }
6881
                  module.set.text(selectedText);
6882
                  module.set.value(selectedValue, selectedText, $selected);
6883
                  $selected
6884
                    .addClass(className.active)
6885
                    .addClass(className.selected)
6886
                  ;
6887
                }
6888
              })
6889
            ;
6890
          }
6891
        },
6892
6893
        add: {
6894
          label: function(value, text, shouldAnimate) {
6895
            var
6896
              $next  = module.is.searchSelection()
6897
                ? $search
6898
                : $text,
6899
              escapedValue = module.escape.value(value),
6900
              $label
6901
            ;
6902
            $label =  $('<a />')
6903
              .addClass(className.label)
6904
              .attr('data-' + metadata.value, escapedValue)
6905
              .html(templates.label(escapedValue, text))
6906
            ;
6907
            $label = settings.onLabelCreate.call($label, escapedValue, text);
6908
6909
            if(module.has.label(value)) {
6910
              module.debug('Label already exists, skipping', escapedValue);
6911
              return;
6912
            }
6913
            if(settings.label.variation) {
6914
              $label.addClass(settings.label.variation);
6915
            }
6916
            if(shouldAnimate === true) {
6917
              module.debug('Animating in label', $label);
6918
              $label
6919
                .addClass(className.hidden)
6920
                .insertBefore($next)
6921
                .transition(settings.label.transition, settings.label.duration)
6922
              ;
6923
            }
6924
            else {
6925
              module.debug('Adding selection label', $label);
6926
              $label
6927
                .insertBefore($next)
6928
              ;
6929
            }
6930
          },
6931
          message: function(message) {
6932
            var
6933
              $message = $menu.children(selector.message),
6934
              html     = settings.templates.message(module.add.variables(message))
6935
            ;
6936
            if($message.length > 0) {
6937
              $message
6938
                .html(html)
6939
              ;
6940
            }
6941
            else {
6942
              $message = $('<div/>')
6943
                .html(html)
6944
                .addClass(className.message)
6945
                .appendTo($menu)
6946
              ;
6947
            }
6948
          },
6949
          optionValue: function(value) {
6950
            var
6951
              escapedValue = module.escape.value(value),
6952
              $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
6953
              hasOption    = ($option.length > 0)
6954
            ;
6955
            if(hasOption) {
6956
              return;
6957
            }
6958
            // temporarily disconnect observer
6959
            module.disconnect.selectObserver();
6960
            if( module.is.single() ) {
6961
              module.verbose('Removing previous user addition');
6962
              $input.find('option.' + className.addition).remove();
6963
            }
6964
            $('<option/>')
6965
              .prop('value', escapedValue)
6966
              .addClass(className.addition)
6967
              .html(value)
6968
              .appendTo($input)
6969
            ;
6970
            module.verbose('Adding user addition as an <option>', value);
6971
            module.observe.select();
6972
          },
6973
          userSuggestion: function(value) {
6974
            var
6975
              $addition         = $menu.children(selector.addition),
6976
              $existingItem     = module.get.item(value),
6977
              alreadyHasValue   = $existingItem && $existingItem.not(selector.addition).length,
6978
              hasUserSuggestion = $addition.length > 0,
6979
              html
6980
            ;
6981
            if(settings.useLabels && module.has.maxSelections()) {
6982
              return;
6983
            }
6984
            if(value === '' || alreadyHasValue) {
6985
              $addition.remove();
6986
              return;
6987
            }
6988
            if(hasUserSuggestion) {
6989
              $addition
6990
                .data(metadata.value, value)
6991
                .data(metadata.text, value)
6992
                .attr('data-' + metadata.value, value)
6993
                .attr('data-' + metadata.text, value)
6994
                .removeClass(className.filtered)
6995
              ;
6996
              if(!settings.hideAdditions) {
6997
                html = settings.templates.addition( module.add.variables(message.addResult, value) );
6998
                $addition
6999
                  .html(html)
7000
                ;
7001
              }
7002
              module.verbose('Replacing user suggestion with new value', $addition);
7003
            }
7004
            else {
7005
              $addition = module.create.userChoice(value);
7006
              $addition
7007
                .prependTo($menu)
7008
              ;
7009
              module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
7010
            }
7011
            if(!settings.hideAdditions || module.is.allFiltered()) {
7012
              $addition
7013
                .addClass(className.selected)
7014
                .siblings()
7015
                .removeClass(className.selected)
7016
              ;
7017
            }
7018
            module.refreshItems();
7019
          },
7020
          variables: function(message, term) {
7021
            var
7022
              hasCount    = (message.search('{count}') !== -1),
7023
              hasMaxCount = (message.search('{maxCount}') !== -1),
7024
              hasTerm     = (message.search('{term}') !== -1),
7025
              values,
7026
              count,
7027
              query
7028
            ;
7029
            module.verbose('Adding templated variables to message', message);
7030
            if(hasCount) {
7031
              count  = module.get.selectionCount();
7032
              message = message.replace('{count}', count);
7033
            }
7034
            if(hasMaxCount) {
7035
              count  = module.get.selectionCount();
7036
              message = message.replace('{maxCount}', settings.maxSelections);
7037
            }
7038
            if(hasTerm) {
7039
              query   = term || module.get.query();
7040
              message = message.replace('{term}', query);
7041
            }
7042
            return message;
7043
          },
7044
          value: function(addedValue, addedText, $selectedItem) {
7045
            var
7046
              currentValue = module.get.values(),
7047
              newValue
7048
            ;
7049
            if(addedValue === '') {
7050
              module.debug('Cannot select blank values from multiselect');
7051
              return;
7052
            }
7053
            // extend current array
7054
            if($.isArray(currentValue)) {
7055
              newValue = currentValue.concat([addedValue]);
7056
              newValue = module.get.uniqueArray(newValue);
7057
            }
7058
            else {
7059
              newValue = [addedValue];
7060
            }
7061
            // add values
7062
            if( module.has.selectInput() ) {
7063
              if(module.can.extendSelect()) {
7064
                module.debug('Adding value to select', addedValue, newValue, $input);
7065
                module.add.optionValue(addedValue);
7066
              }
7067
            }
7068
            else {
7069
              newValue = newValue.join(settings.delimiter);
7070
              module.debug('Setting hidden input to delimited value', newValue, $input);
7071
            }
7072
7073
            if(settings.fireOnInit === false && module.is.initialLoad()) {
7074
              module.verbose('Skipping onadd callback on initial load', settings.onAdd);
7075
            }
7076
            else {
7077
              settings.onAdd.call(element, addedValue, addedText, $selectedItem);
7078
            }
7079
            module.set.value(newValue, addedValue, addedText, $selectedItem);
7080
            module.check.maxSelections();
7081
          }
7082
        },
7083
7084
        remove: {
7085
          active: function() {
7086
            $module.removeClass(className.active);
7087
          },
7088
          activeLabel: function() {
7089
            $module.find(selector.label).removeClass(className.active);
7090
          },
7091
          empty: function() {
7092
            $module.removeClass(className.empty);
7093
          },
7094
          loading: function() {
7095
            $module.removeClass(className.loading);
7096
          },
7097
          initialLoad: function() {
7098
            initialLoad = false;
7099
          },
7100
          upward: function($currentMenu) {
7101
            var $element = $currentMenu || $module;
7102
            $element.removeClass(className.upward);
7103
          },
7104
          leftward: function($currentMenu) {
7105
            var $element = $currentMenu || $menu;
7106
            $element.removeClass(className.leftward);
7107
          },
7108
          visible: function() {
7109
            $module.removeClass(className.visible);
7110
          },
7111
          activeItem: function() {
7112
            $item.removeClass(className.active);
7113
          },
7114
          filteredItem: function() {
7115
            if(settings.useLabels && module.has.maxSelections() ) {
7116
              return;
7117
            }
7118
            if(settings.useLabels && module.is.multiple()) {
7119
              $item.not('.' + className.active).removeClass(className.filtered);
7120
            }
7121
            else {
7122
              $item.removeClass(className.filtered);
7123
            }
7124
            module.remove.empty();
7125
          },
7126
          optionValue: function(value) {
7127
            var
7128
              escapedValue = module.escape.value(value),
7129
              $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
7130
              hasOption    = ($option.length > 0)
7131
            ;
7132
            if(!hasOption || !$option.hasClass(className.addition)) {
7133
              return;
7134
            }
7135
            // temporarily disconnect observer
7136
            if(selectObserver) {
7137
              selectObserver.disconnect();
7138
              module.verbose('Temporarily disconnecting mutation observer');
7139
            }
7140
            $option.remove();
7141
            module.verbose('Removing user addition as an <option>', escapedValue);
7142
            if(selectObserver) {
7143
              selectObserver.observe($input[0], {
7144
                childList : true,
7145
                subtree   : true
7146
              });
7147
            }
7148
          },
7149
          message: function() {
7150
            $menu.children(selector.message).remove();
7151
          },
7152
          searchWidth: function() {
7153
            $search.css('width', '');
7154
          },
7155
          searchTerm: function() {
7156
            module.verbose('Cleared search term');
7157
            $search.val('');
7158
            module.set.filtered();
7159
          },
7160
          userAddition: function() {
7161
            $item.filter(selector.addition).remove();
7162
          },
7163
          selected: function(value, $selectedItem) {
7164
            $selectedItem = (settings.allowAdditions)
7165
              ? $selectedItem || module.get.itemWithAdditions(value)
7166
              : $selectedItem || module.get.item(value)
7167
            ;
7168
7169
            if(!$selectedItem) {
7170
              return false;
7171
            }
7172
7173
            $selectedItem
7174
              .each(function() {
7175
                var
7176
                  $selected     = $(this),
7177
                  selectedText  = module.get.choiceText($selected),
7178
                  selectedValue = module.get.choiceValue($selected, selectedText)
7179
                ;
7180
                if(module.is.multiple()) {
7181
                  if(settings.useLabels) {
7182
                    module.remove.value(selectedValue, selectedText, $selected);
7183
                    module.remove.label(selectedValue);
7184
                  }
7185
                  else {
7186
                    module.remove.value(selectedValue, selectedText, $selected);
7187
                    if(module.get.selectionCount() === 0) {
7188
                      module.set.placeholderText();
7189
                    }
7190
                    else {
7191
                      module.set.text(module.add.variables(message.count));
7192
                    }
7193
                  }
7194
                }
7195
                else {
7196
                  module.remove.value(selectedValue, selectedText, $selected);
7197
                }
7198
                $selected
7199
                  .removeClass(className.filtered)
7200
                  .removeClass(className.active)
7201
                ;
7202
                if(settings.useLabels) {
7203
                  $selected.removeClass(className.selected);
7204
                }
7205
              })
7206
            ;
7207
          },
7208
          selectedItem: function() {
7209
            $item.removeClass(className.selected);
7210
          },
7211
          value: function(removedValue, removedText, $removedItem) {
7212
            var
7213
              values = module.get.values(),
7214
              newValue
7215
            ;
7216
            if( module.has.selectInput() ) {
7217
              module.verbose('Input is <select> removing selected option', removedValue);
7218
              newValue = module.remove.arrayValue(removedValue, values);
7219
              module.remove.optionValue(removedValue);
7220
            }
7221
            else {
7222
              module.verbose('Removing from delimited values', removedValue);
7223
              newValue = module.remove.arrayValue(removedValue, values);
7224
              newValue = newValue.join(settings.delimiter);
7225
            }
7226
            if(settings.fireOnInit === false && module.is.initialLoad()) {
7227
              module.verbose('No callback on initial load', settings.onRemove);
7228
            }
7229
            else {
7230
              settings.onRemove.call(element, removedValue, removedText, $removedItem);
7231
            }
7232
            module.set.value(newValue, removedText, $removedItem);
7233
            module.check.maxSelections();
7234
          },
7235
          arrayValue: function(removedValue, values) {
7236
            if( !$.isArray(values) ) {
7237
              values = [values];
7238
            }
7239
            values = $.grep(values, function(value){
7240
              return (removedValue != value);
7241
            });
7242
            module.verbose('Removed value from delimited string', removedValue, values);
7243
            return values;
7244
          },
7245
          label: function(value, shouldAnimate) {
7246
            var
7247
              $labels       = $module.find(selector.label),
7248
              $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]')
7249
            ;
7250
            module.verbose('Removing label', $removedLabel);
7251
            $removedLabel.remove();
7252
          },
7253
          activeLabels: function($activeLabels) {
7254
            $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
7255
            module.verbose('Removing active label selections', $activeLabels);
7256
            module.remove.labels($activeLabels);
7257
          },
7258
          labels: function($labels) {
7259
            $labels = $labels || $module.find(selector.label);
7260
            module.verbose('Removing labels', $labels);
7261
            $labels
7262
              .each(function(){
7263
                var
7264
                  $label      = $(this),
7265
                  value       = $label.data(metadata.value),
7266
                  stringValue = (value !== undefined)
7267
                    ? String(value)
7268
                    : value,
7269
                  isUserValue = module.is.userValue(stringValue)
7270
                ;
7271
                if(settings.onLabelRemove.call($label, value) === false) {
7272
                  module.debug('Label remove callback cancelled removal');
7273
                  return;
7274
                }
7275
                module.remove.message();
7276
                if(isUserValue) {
7277
                  module.remove.value(stringValue);
7278
                  module.remove.label(stringValue);
7279
                }
7280
                else {
7281
                  // selected will also remove label
7282
                  module.remove.selected(stringValue);
7283
                }
7284
              })
7285
            ;
7286
          },
7287
          tabbable: function() {
7288
            if( module.is.searchSelection() ) {
7289
              module.debug('Searchable dropdown initialized');
7290
              $search
7291
                .removeAttr('tabindex')
7292
              ;
7293
              $menu
7294
                .removeAttr('tabindex')
7295
              ;
7296
            }
7297
            else {
7298
              module.debug('Simple selection dropdown initialized');
7299
              $module
7300
                .removeAttr('tabindex')
7301
              ;
7302
              $menu
7303
                .removeAttr('tabindex')
7304
              ;
7305
            }
7306
          }
7307
        },
7308
7309
        has: {
7310
          menuSearch: function() {
7311
            return (module.has.search() && $search.closest($menu).length > 0);
7312
          },
7313
          search: function() {
7314
            return ($search.length > 0);
7315
          },
7316
          sizer: function() {
7317
            return ($sizer.length > 0);
7318
          },
7319
          selectInput: function() {
7320
            return ( $input.is('select') );
7321
          },
7322
          minCharacters: function(searchTerm) {
7323
            if(settings.minCharacters) {
7324
              searchTerm = (searchTerm !== undefined)
7325
                ? String(searchTerm)
7326
                : String(module.get.query())
7327
              ;
7328
              return (searchTerm.length >= settings.minCharacters);
7329
            }
7330
            return true;
7331
          },
7332
          firstLetter: function($item, letter) {
7333
            var
7334
              text,
7335
              firstLetter
7336
            ;
7337
            if(!$item || $item.length === 0 || typeof letter !== 'string') {
7338
              return false;
7339
            }
7340
            text        = module.get.choiceText($item, false);
7341
            letter      = letter.toLowerCase();
7342
            firstLetter = String(text).charAt(0).toLowerCase();
7343
            return (letter == firstLetter);
7344
          },
7345
          input: function() {
7346
            return ($input.length > 0);
7347
          },
7348
          items: function() {
7349
            return ($item.length > 0);
7350
          },
7351
          menu: function() {
7352
            return ($menu.length > 0);
7353
          },
7354
          message: function() {
7355
            return ($menu.children(selector.message).length !== 0);
7356
          },
7357
          label: function(value) {
7358
            var
7359
              escapedValue = module.escape.value(value),
7360
              $labels      = $module.find(selector.label)
7361
            ;
7362
            return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
7363
          },
7364
          maxSelections: function() {
7365
            return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
7366
          },
7367
          allResultsFiltered: function() {
7368
            var
7369
              $normalResults = $item.not(selector.addition)
7370
            ;
7371
            return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
7372
          },
7373
          userSuggestion: function() {
7374
            return ($menu.children(selector.addition).length > 0);
7375
          },
7376
          query: function() {
7377
            return (module.get.query() !== '');
7378
          },
7379
          value: function(value) {
7380
            var
7381
              values   = module.get.values(),
7382
              hasValue = $.isArray(values)
7383
               ? values && ($.inArray(value, values) !== -1)
7384
               : (values == value)
7385
            ;
7386
            return (hasValue)
7387
              ? true
7388
              : false
7389
            ;
7390
          }
7391
        },
7392
7393
        is: {
7394
          active: function() {
7395
            return $module.hasClass(className.active);
7396
          },
7397
          bubbledLabelClick: function(event) {
7398
            return $(event.target).is('select, input') && $module.closest('label').length > 0;
7399
          },
7400
          bubbledIconClick: function(event) {
7401
            return $(event.target).closest($icon).length > 0;
7402
          },
7403
          alreadySetup: function() {
7404
            return ($module.is('select') && $module.parent(selector.dropdown).length > 0  && $module.prev().length === 0);
7405
          },
7406
          animating: function($subMenu) {
7407
            return ($subMenu)
7408
              ? $subMenu.transition && $subMenu.transition('is animating')
7409
              : $menu.transition    && $menu.transition('is animating')
7410
            ;
7411
          },
7412
          leftward: function($subMenu) {
7413
            var $selectedMenu = $subMenu || $menu;
7414
            return $selectedMenu.hasClass(className.leftward);
7415
          },
7416
          disabled: function() {
7417
            return $module.hasClass(className.disabled);
7418
          },
7419
          focused: function() {
7420
            return (document.activeElement === $module[0]);
7421
          },
7422
          focusedOnSearch: function() {
7423
            return (document.activeElement === $search[0]);
7424
          },
7425
          allFiltered: function() {
7426
            return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
7427
          },
7428
          hidden: function($subMenu) {
7429
            return !module.is.visible($subMenu);
7430
          },
7431
          initialLoad: function() {
7432
            return initialLoad;
7433
          },
7434
          inObject: function(needle, object) {
7435
            var
7436
              found = false
7437
            ;
7438
            $.each(object, function(index, property) {
7439
              if(property == needle) {
7440
                found = true;
7441
                return true;
7442
              }
7443
            });
7444
            return found;
7445
          },
7446
          multiple: function() {
7447
            return $module.hasClass(className.multiple);
7448
          },
7449
          remote: function() {
7450
            return settings.apiSettings && module.can.useAPI();
7451
          },
7452
          single: function() {
7453
            return !module.is.multiple();
7454
          },
7455
          selectMutation: function(mutations) {
7456
            var
7457
              selectChanged = false
7458
            ;
7459
            $.each(mutations, function(index, mutation) {
7460
              if(mutation.target && $(mutation.target).is('select')) {
7461
                selectChanged = true;
7462
                return true;
7463
              }
7464
            });
7465
            return selectChanged;
7466
          },
7467
          search: function() {
7468
            return $module.hasClass(className.search);
7469
          },
7470
          searchSelection: function() {
7471
            return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
7472
          },
7473
          selection: function() {
7474
            return $module.hasClass(className.selection);
7475
          },
7476
          userValue: function(value) {
7477
            return ($.inArray(value, module.get.userValues()) !== -1);
7478
          },
7479
          upward: function($menu) {
7480
            var $element = $menu || $module;
7481
            return $element.hasClass(className.upward);
7482
          },
7483
          visible: function($subMenu) {
7484
            return ($subMenu)
7485
              ? $subMenu.hasClass(className.visible)
7486
              : $menu.hasClass(className.visible)
7487
            ;
7488
          },
7489
          verticallyScrollableContext: function() {
7490
            var
7491
              overflowY = ($context.get(0) !== window)
7492
                ? $context.css('overflow-y')
7493
                : false
7494
            ;
7495
            return (overflowY == 'auto' || overflowY == 'scroll');
7496
          },
7497
          horizontallyScrollableContext: function() {
7498
            var
7499
              overflowX = ($context.get(0) !== window)
7500
                ? $context.css('overflow-X')
7501
                : false
7502
            ;
7503
            return (overflowX == 'auto' || overflowX == 'scroll');
7504
          }
7505
        },
7506
7507
        can: {
7508
          activate: function($item) {
7509
            if(settings.useLabels) {
7510
              return true;
7511
            }
7512
            if(!module.has.maxSelections()) {
7513
              return true;
7514
            }
7515
            if(module.has.maxSelections() && $item.hasClass(className.active)) {
7516
              return true;
7517
            }
7518
            return false;
7519
          },
7520
          openDownward: function($subMenu) {
7521
            var
7522
              $currentMenu    = $subMenu || $menu,
7523
              canOpenDownward = true,
7524
              onScreen        = {},
7525
              calculations
7526
            ;
7527
            $currentMenu
7528
              .addClass(className.loading)
7529
            ;
7530
            calculations = {
7531
              context: {
7532
                scrollTop : $context.scrollTop(),
7533
                height    : $context.outerHeight()
7534
              },
7535
              menu : {
7536
                offset: $currentMenu.offset(),
7537
                height: $currentMenu.outerHeight()
7538
              }
7539
            };
7540
            if(module.is.verticallyScrollableContext()) {
7541
              calculations.menu.offset.top += calculations.context.scrollTop;
7542
            }
7543
            onScreen = {
7544
              above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.menu.height,
7545
              below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top + calculations.menu.height
7546
            };
7547
            if(onScreen.below) {
7548
              module.verbose('Dropdown can fit in context downward', onScreen);
7549
              canOpenDownward = true;
7550
            }
7551
            else if(!onScreen.below && !onScreen.above) {
7552
              module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
7553
              canOpenDownward = true;
7554
            }
7555
            else {
7556
              module.verbose('Dropdown cannot fit below, opening upward', onScreen);
7557
              canOpenDownward = false;
7558
            }
7559
            $currentMenu.removeClass(className.loading);
7560
            return canOpenDownward;
7561
          },
7562
          openRightward: function($subMenu) {
7563
            var
7564
              $currentMenu     = $subMenu || $menu,
7565
              canOpenRightward = true,
7566
              isOffscreenRight = false,
7567
              calculations
7568
            ;
7569
            $currentMenu
7570
              .addClass(className.loading)
7571
            ;
7572
            calculations = {
7573
              context: {
7574
                scrollLeft : $context.scrollLeft(),
7575
                width      : $context.outerWidth()
7576
              },
7577
              menu: {
7578
                offset : $currentMenu.offset(),
7579
                width  : $currentMenu.outerWidth()
7580
              }
7581
            };
7582
            if(module.is.horizontallyScrollableContext()) {
7583
              calculations.menu.offset.left += calculations.context.scrollLeft;
7584
            }
7585
            isOffscreenRight = (calculations.menu.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
7586
            if(isOffscreenRight) {
7587
              module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
7588
              canOpenRightward = false;
7589
            }
7590
            $currentMenu.removeClass(className.loading);
7591
            return canOpenRightward;
7592
          },
7593
          click: function() {
7594
            return (hasTouch || settings.on == 'click');
7595
          },
7596
          extendSelect: function() {
7597
            return settings.allowAdditions || settings.apiSettings;
7598
          },
7599
          show: function() {
7600
            return !module.is.disabled() && (module.has.items() || module.has.message());
7601
          },
7602
          useAPI: function() {
7603
            return $.fn.api !== undefined;
7604
          }
7605
        },
7606
7607
        animate: {
7608
          show: function(callback, $subMenu) {
7609
            var
7610
              $currentMenu = $subMenu || $menu,
7611
              start = ($subMenu)
7612
                ? function() {}
7613
                : function() {
7614
                  module.hideSubMenus();
7615
                  module.hideOthers();
7616
                  module.set.active();
7617
                },
7618
              transition
7619
            ;
7620
            callback = $.isFunction(callback)
7621
              ? callback
7622
              : function(){}
7623
            ;
7624
            module.verbose('Doing menu show animation', $currentMenu);
7625
            module.set.direction($subMenu);
7626
            transition = module.get.transition($subMenu);
7627
            if( module.is.selection() ) {
7628
              module.set.scrollPosition(module.get.selectedItem(), true);
7629
            }
7630
            if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
7631
              if(transition == 'none') {
7632
                start();
7633
                $currentMenu.transition('show');
7634
                callback.call(element);
7635
              }
7636
              else if($.fn.transition !== undefined && $module.transition('is supported')) {
7637
                $currentMenu
7638
                  .transition({
7639
                    animation  : transition + ' in',
7640
                    debug      : settings.debug,
7641
                    verbose    : settings.verbose,
7642
                    duration   : settings.duration,
7643
                    queue      : true,
7644
                    onStart    : start,
7645
                    onComplete : function() {
7646
                      callback.call(element);
7647
                    }
7648
                  })
7649
                ;
7650
              }
7651
              else {
7652
                module.error(error.noTransition, transition);
7653
              }
7654
            }
7655
          },
7656
          hide: function(callback, $subMenu) {
7657
            var
7658
              $currentMenu = $subMenu || $menu,
7659
              duration = ($subMenu)
7660
                ? (settings.duration * 0.9)
7661
                : settings.duration,
7662
              start = ($subMenu)
7663
                ? function() {}
7664
                : function() {
7665
                  if( module.can.click() ) {
7666
                    module.unbind.intent();
7667
                  }
7668
                  module.remove.active();
7669
                },
7670
              transition = module.get.transition($subMenu)
7671
            ;
7672
            callback = $.isFunction(callback)
7673
              ? callback
7674
              : function(){}
7675
            ;
7676
            if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
7677
              module.verbose('Doing menu hide animation', $currentMenu);
7678
7679
              if(transition == 'none') {
7680
                start();
7681
                $currentMenu.transition('hide');
7682
                callback.call(element);
7683
              }
7684
              else if($.fn.transition !== undefined && $module.transition('is supported')) {
7685
                $currentMenu
7686
                  .transition({
7687
                    animation  : transition + ' out',
7688
                    duration   : settings.duration,
7689
                    debug      : settings.debug,
7690
                    verbose    : settings.verbose,
7691
                    queue      : true,
7692
                    onStart    : start,
7693
                    onComplete : function() {
7694
                      callback.call(element);
7695
                    }
7696
                  })
7697
                ;
7698
              }
7699
              else {
7700
                module.error(error.transition);
7701
              }
7702
            }
7703
          }
7704
        },
7705
7706
        hideAndClear: function() {
7707
          module.remove.searchTerm();
7708
          if( module.has.maxSelections() ) {
7709
            return;
7710
          }
7711
          if(module.has.search()) {
7712
            module.hide(function() {
7713
              module.remove.filteredItem();
7714
            });
7715
          }
7716
          else {
7717
            module.hide();
7718
          }
7719
        },
7720
7721
        delay: {
7722
          show: function() {
7723
            module.verbose('Delaying show event to ensure user intent');
7724
            clearTimeout(module.timer);
7725
            module.timer = setTimeout(module.show, settings.delay.show);
7726
          },
7727
          hide: function() {
7728
            module.verbose('Delaying hide event to ensure user intent');
7729
            clearTimeout(module.timer);
7730
            module.timer = setTimeout(module.hide, settings.delay.hide);
7731
          }
7732
        },
7733
7734
        escape: {
7735
          value: function(value) {
7736
            var
7737
              multipleValues = $.isArray(value),
7738
              stringValue    = (typeof value === 'string'),
7739
              isUnparsable   = (!stringValue && !multipleValues),
7740
              hasQuotes      = (stringValue && value.search(regExp.quote) !== -1),
7741
              values         = []
7742
            ;
7743
            if(isUnparsable || !hasQuotes) {
7744
              return value;
7745
            }
7746
            module.debug('Encoding quote values for use in select', value);
7747
            if(multipleValues) {
7748
              $.each(value, function(index, value){
7749
                values.push(value.replace(regExp.quote, '&quot;'));
7750
              });
7751
              return values;
7752
            }
7753
            return value.replace(regExp.quote, '&quot;');
7754
          },
7755
          string: function(text) {
7756
            text =  String(text);
7757
            return text.replace(regExp.escape, '\\$&');
7758
          }
7759
        },
7760
7761
        setting: function(name, value) {
7762
          module.debug('Changing setting', name, value);
7763
          if( $.isPlainObject(name) ) {
7764
            $.extend(true, settings, name);
7765
          }
7766
          else if(value !== undefined) {
7767
            if($.isPlainObject(settings[name])) {
7768
              $.extend(true, settings[name], value);
7769
            }
7770
            else {
7771
              settings[name] = value;
7772
            }
7773
          }
7774
          else {
7775
            return settings[name];
7776
          }
7777
        },
7778
        internal: function(name, value) {
7779
          if( $.isPlainObject(name) ) {
7780
            $.extend(true, module, name);
7781
          }
7782
          else if(value !== undefined) {
7783
            module[name] = value;
7784
          }
7785
          else {
7786
            return module[name];
7787
          }
7788
        },
7789
        debug: function() {
7790
          if(!settings.silent && settings.debug) {
7791
            if(settings.performance) {
7792
              module.performance.log(arguments);
7793
            }
7794
            else {
7795
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
7796
              module.debug.apply(console, arguments);
7797
            }
7798
          }
7799
        },
7800
        verbose: function() {
7801
          if(!settings.silent && settings.verbose && settings.debug) {
7802
            if(settings.performance) {
7803
              module.performance.log(arguments);
7804
            }
7805
            else {
7806
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
7807
              module.verbose.apply(console, arguments);
7808
            }
7809
          }
7810
        },
7811
        error: function() {
7812
          if(!settings.silent) {
7813
            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
7814
            module.error.apply(console, arguments);
7815
          }
7816
        },
7817
        performance: {
7818
          log: function(message) {
7819
            var
7820
              currentTime,
7821
              executionTime,
7822
              previousTime
7823
            ;
7824
            if(settings.performance) {
7825
              currentTime   = new Date().getTime();
7826
              previousTime  = time || currentTime;
7827
              executionTime = currentTime - previousTime;
7828
              time          = currentTime;
7829
              performance.push({
7830
                'Name'           : message[0],
7831
                'Arguments'      : [].slice.call(message, 1) || '',
7832
                'Element'        : element,
7833
                'Execution Time' : executionTime
7834
              });
7835
            }
7836
            clearTimeout(module.performance.timer);
7837
            module.performance.timer = setTimeout(module.performance.display, 500);
7838
          },
7839
          display: function() {
7840
            var
7841
              title = settings.name + ':',
7842
              totalTime = 0
7843
            ;
7844
            time = false;
7845
            clearTimeout(module.performance.timer);
7846
            $.each(performance, function(index, data) {
7847
              totalTime += data['Execution Time'];
7848
            });
7849
            title += ' ' + totalTime + 'ms';
7850
            if(moduleSelector) {
7851
              title += ' \'' + moduleSelector + '\'';
7852
            }
7853
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
7854
              console.groupCollapsed(title);
7855
              if(console.table) {
7856
                console.table(performance);
7857
              }
7858
              else {
7859
                $.each(performance, function(index, data) {
7860
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
7861
                });
7862
              }
7863
              console.groupEnd();
7864
            }
7865
            performance = [];
7866
          }
7867
        },
7868
        invoke: function(query, passedArguments, context) {
7869
          var
7870
            object = instance,
7871
            maxDepth,
7872
            found,
7873
            response
7874
          ;
7875
          passedArguments = passedArguments || queryArguments;
7876
          context         = element         || context;
7877
          if(typeof query == 'string' && object !== undefined) {
7878
            query    = query.split(/[\. ]/);
7879
            maxDepth = query.length - 1;
7880
            $.each(query, function(depth, value) {
7881
              var camelCaseValue = (depth != maxDepth)
7882
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
7883
                : query
7884
              ;
7885
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
7886
                object = object[camelCaseValue];
7887
              }
7888
              else if( object[camelCaseValue] !== undefined ) {
7889
                found = object[camelCaseValue];
7890
                return false;
7891
              }
7892
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
7893
                object = object[value];
7894
              }
7895
              else if( object[value] !== undefined ) {
7896
                found = object[value];
7897
                return false;
7898
              }
7899
              else {
7900
                module.error(error.method, query);
7901
                return false;
7902
              }
7903
            });
7904
          }
7905
          if ( $.isFunction( found ) ) {
7906
            response = found.apply(context, passedArguments);
7907
          }
7908
          else if(found !== undefined) {
7909
            response = found;
7910
          }
7911
          if($.isArray(returnedValue)) {
7912
            returnedValue.push(response);
7913
          }
7914
          else if(returnedValue !== undefined) {
7915
            returnedValue = [returnedValue, response];
7916
          }
7917
          else if(response !== undefined) {
7918
            returnedValue = response;
7919
          }
7920
          return found;
7921
        }
7922
      };
7923
7924
      if(methodInvoked) {
7925
        if(instance === undefined) {
7926
          module.initialize();
7927
        }
7928
        module.invoke(query);
7929
      }
7930
      else {
7931
        if(instance !== undefined) {
7932
          instance.invoke('destroy');
7933
        }
7934
        module.initialize();
7935
      }
7936
    })
7937
  ;
7938
  return (returnedValue !== undefined)
7939
    ? returnedValue
7940
    : $allModules
7941
  ;
7942
};
7943
7944
$.fn.dropdown.settings = {
7945
7946
  silent                 : false,
7947
  debug                  : false,
7948
  verbose                : false,
7949
  performance            : true,
7950
7951
  on                     : 'click',    // what event should show menu action on item selection
7952
  action                 : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
7953
7954
7955
  apiSettings            : false,
7956
  selectOnKeydown        : true,       // Whether selection should occur automatically when keyboard shortcuts used
7957
  minCharacters          : 0,          // Minimum characters required to trigger API call
7958
7959
  filterRemoteData       : false,      // Whether API results should be filtered after being returned for query term
7960
  saveRemoteData         : true,       // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
7961
7962
  throttle               : 200,        // How long to wait after last user input to search remotely
7963
7964
  context                : window,     // Context to use when determining if on screen
7965
  direction              : 'auto',     // Whether dropdown should always open in one direction
7966
  keepOnScreen           : true,       // Whether dropdown should check whether it is on screen before showing
7967
7968
  match                  : 'both',     // what to match against with search selection (both, text, or label)
7969
  fullTextSearch         : false,      // search anywhere in value (set to 'exact' to require exact matches)
7970
7971
  placeholder            : 'auto',     // whether to convert blank <select> values to placeholder text
7972
  preserveHTML           : true,       // preserve html when selecting value
7973
  sortSelect             : false,      // sort selection on init
7974
7975
  forceSelection         : true,       // force a choice on blur with search selection
7976
7977
  allowAdditions         : false,      // whether multiple select should allow user added values
7978
  hideAdditions          : true,      // whether or not to hide special message prompting a user they can enter a value
7979
7980
  maxSelections          : false,      // When set to a number limits the number of selections to this count
7981
  useLabels              : true,       // whether multiple select should filter currently active selections from choices
7982
  delimiter              : ',',        // when multiselect uses normal <input> the values will be delimited with this character
7983
7984
  showOnFocus            : true,       // show menu on focus
7985
  allowReselection       : false,      // whether current value should trigger callbacks when reselected
7986
  allowTab               : true,       // add tabindex to element
7987
  allowCategorySelection : false,      // allow elements with sub-menus to be selected
7988
7989
  fireOnInit             : false,      // Whether callbacks should fire when initializing dropdown values
7990
7991
  transition             : 'auto',     // auto transition will slide down or up based on direction
7992
  duration               : 200,        // duration of transition
7993
7994
  glyphWidth             : 1.037,      // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
7995
7996
  // label settings on multi-select
7997
  label: {
7998
    transition : 'scale',
7999
    duration   : 200,
8000
    variation  : false
8001
  },
8002
8003
  // delay before event
8004
  delay : {
8005
    hide   : 300,
8006
    show   : 200,
8007
    search : 20,
8008
    touch  : 50
8009
  },
8010
8011
  /* Callbacks */
8012
  onChange      : function(value, text, $selected){},
8013
  onAdd         : function(value, text, $selected){},
8014
  onRemove      : function(value, text, $selected){},
8015
8016
  onLabelSelect : function($selectedLabels){},
8017
  onLabelCreate : function(value, text) { return $(this); },
8018
  onLabelRemove : function(value) { return true; },
8019
  onNoResults   : function(searchTerm) { return true; },
8020
  onShow        : function(){},
8021
  onHide        : function(){},
8022
8023
  /* Component */
8024
  name           : 'Dropdown',
8025
  namespace      : 'dropdown',
8026
8027
  message: {
8028
    addResult     : 'Add <b>{term}</b>',
8029
    count         : '{count} selected',
8030
    maxSelections : 'Max {maxCount} selections',
8031
    noResults     : 'No results found.',
8032
    serverError   : 'There was an error contacting the server'
8033
  },
8034
8035
  error : {
8036
    action          : 'You called a dropdown action that was not defined',
8037
    alreadySetup    : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
8038
    labels          : 'Allowing user additions currently requires the use of labels.',
8039
    missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
8040
    method          : 'The method you called is not defined.',
8041
    noAPI           : 'The API module is required to load resources remotely',
8042
    noStorage       : 'Saving remote data requires session storage',
8043
    noTransition    : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>'
8044
  },
8045
8046
  regExp : {
8047
    escape   : /[-[\]{}()*+?.,\\^$|#\s]/g,
8048
    quote    : /"/g
8049
  },
8050
8051
  metadata : {
8052
    defaultText     : 'defaultText',
8053
    defaultValue    : 'defaultValue',
8054
    placeholderText : 'placeholder',
8055
    text            : 'text',
8056
    value           : 'value'
8057
  },
8058
8059
  // property names for remote query
8060
  fields: {
8061
    remoteValues : 'results',  // grouping for api results
8062
    values       : 'values',   // grouping for all dropdown values
8063
    disabled     : 'disabled', // whether value should be disabled
8064
    name         : 'name',     // displayed dropdown text
8065
    value        : 'value',    // actual dropdown value
8066
    text         : 'text'      // displayed text when selected
8067
  },
8068
8069
  keys : {
8070
    backspace  : 8,
8071
    delimiter  : 188, // comma
8072
    deleteKey  : 46,
8073
    enter      : 13,
8074
    escape     : 27,
8075
    pageUp     : 33,
8076
    pageDown   : 34,
8077
    leftArrow  : 37,
8078
    upArrow    : 38,
8079
    rightArrow : 39,
8080
    downArrow  : 40
8081
  },
8082
8083
  selector : {
8084
    addition     : '.addition',
8085
    dropdown     : '.ui.dropdown',
8086
    hidden       : '.hidden',
8087
    icon         : '> .dropdown.icon',
8088
    input        : '> input[type="hidden"], > select',
8089
    item         : '.item',
8090
    label        : '> .label',
8091
    remove       : '> .label > .delete.icon',
8092
    siblingLabel : '.label',
8093
    menu         : '.menu',
8094
    message      : '.message',
8095
    menuIcon     : '.dropdown.icon',
8096
    search       : 'input.search, .menu > .search > input, .menu input.search',
8097
    sizer        : '> input.sizer',
8098
    text         : '> .text:not(.icon)',
8099
    unselectable : '.disabled, .filtered'
8100
  },
8101
8102
  className : {
8103
    active      : 'active',
8104
    addition    : 'addition',
8105
    animating   : 'animating',
8106
    disabled    : 'disabled',
8107
    empty       : 'empty',
8108
    dropdown    : 'ui dropdown',
8109
    filtered    : 'filtered',
8110
    hidden      : 'hidden transition',
8111
    item        : 'item',
8112
    label       : 'ui label',
8113
    loading     : 'loading',
8114
    menu        : 'menu',
8115
    message     : 'message',
8116
    multiple    : 'multiple',
8117
    placeholder : 'default',
8118
    sizer       : 'sizer',
8119
    search      : 'search',
8120
    selected    : 'selected',
8121
    selection   : 'selection',
8122
    upward      : 'upward',
8123
    leftward    : 'left',
8124
    visible     : 'visible'
8125
  }
8126
8127
};
8128
8129
/* Templates */
8130
$.fn.dropdown.settings.templates = {
8131
8132
  // generates dropdown from select values
8133
  dropdown: function(select) {
8134
    var
8135
      placeholder = select.placeholder || false,
8136
      values      = select.values || {},
8137
      html        = ''
8138
    ;
8139
    html +=  '<i class="dropdown icon"></i>';
8140
    if(select.placeholder) {
8141
      html += '<div class="default text">' + placeholder + '</div>';
8142
    }
8143
    else {
8144
      html += '<div class="text"></div>';
8145
    }
8146
    html += '<div class="menu">';
8147
    $.each(select.values, function(index, option) {
8148
      html += (option.disabled)
8149
        ? '<div class="disabled item" data-value="' + option.value + '">' + option.name + '</div>'
8150
        : '<div class="item" data-value="' + option.value + '">' + option.name + '</div>'
8151
      ;
8152
    });
8153
    html += '</div>';
8154
    return html;
8155
  },
8156
8157
  // generates just menu from select
8158
  menu: function(response, fields) {
8159
    var
8160
      values = response[fields.values] || {},
8161
      html   = ''
8162
    ;
8163
    $.each(values, function(index, option) {
8164
      var
8165
        maybeText = (option[fields.text])
8166
          ? 'data-text="' + option[fields.text] + '"'
8167
          : '',
8168
        maybeDisabled = (option[fields.disabled])
8169
          ? 'disabled '
8170
          : ''
8171
      ;
8172
      html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>'
8173
      html +=   option[fields.name];
8174
      html += '</div>';
8175
    });
8176
    return html;
8177
  },
8178
8179
  // generates label for multiselect
8180
  label: function(value, text) {
8181
    return text + '<i class="delete icon"></i>';
8182
  },
8183
8184
8185
  // generates messages like "No results"
8186
  message: function(message) {
8187
    return message;
8188
  },
8189
8190
  // generates user addition to selection menu
8191
  addition: function(choice) {
8192
    return choice;
8193
  }
8194
8195
};
8196
8197
})( jQuery, window, document );
8198
8199
/*!
8200
 * # Semantic UI 2.2.11 - Embed

public/lib/semantic/components/dropdown.js 1 location

@@ 11-3840 (lines=3830) @@
8
 *
9
 */
10
11
;(function ($, window, document, undefined) {
12
13
"use strict";
14
15
window = (typeof window != 'undefined' && window.Math == Math)
16
  ? window
17
  : (typeof self != 'undefined' && self.Math == Math)
18
    ? self
19
    : Function('return this')()
20
;
21
22
$.fn.dropdown = function(parameters) {
23
  var
24
    $allModules    = $(this),
25
    $document      = $(document),
26
27
    moduleSelector = $allModules.selector || '',
28
29
    hasTouch       = ('ontouchstart' in document.documentElement),
30
    time           = new Date().getTime(),
31
    performance    = [],
32
33
    query          = arguments[0],
34
    methodInvoked  = (typeof query == 'string'),
35
    queryArguments = [].slice.call(arguments, 1),
36
    returnedValue
37
  ;
38
39
  $allModules
40
    .each(function(elementIndex) {
41
      var
42
        settings          = ( $.isPlainObject(parameters) )
43
          ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
44
          : $.extend({}, $.fn.dropdown.settings),
45
46
        className       = settings.className,
47
        message         = settings.message,
48
        fields          = settings.fields,
49
        keys            = settings.keys,
50
        metadata        = settings.metadata,
51
        namespace       = settings.namespace,
52
        regExp          = settings.regExp,
53
        selector        = settings.selector,
54
        error           = settings.error,
55
        templates       = settings.templates,
56
57
        eventNamespace  = '.' + namespace,
58
        moduleNamespace = 'module-' + namespace,
59
60
        $module         = $(this),
61
        $context        = $(settings.context),
62
        $text           = $module.find(selector.text),
63
        $search         = $module.find(selector.search),
64
        $sizer          = $module.find(selector.sizer),
65
        $input          = $module.find(selector.input),
66
        $icon           = $module.find(selector.icon),
67
68
        $combo = ($module.prev().find(selector.text).length > 0)
69
          ? $module.prev().find(selector.text)
70
          : $module.prev(),
71
72
        $menu           = $module.children(selector.menu),
73
        $item           = $menu.find(selector.item),
74
75
        activated       = false,
76
        itemActivated   = false,
77
        internalChange  = false,
78
        element         = this,
79
        instance        = $module.data(moduleNamespace),
80
81
        initialLoad,
82
        pageLostFocus,
83
        willRefocus,
84
        elementNamespace,
85
        id,
86
        selectObserver,
87
        menuObserver,
88
        module
89
      ;
90
91
      module = {
92
93
        initialize: function() {
94
          module.debug('Initializing dropdown', settings);
95
96
          if( module.is.alreadySetup() ) {
97
            module.setup.reference();
98
          }
99
          else {
100
            module.setup.layout();
101
            module.refreshData();
102
103
            module.save.defaults();
104
            module.restore.selected();
105
106
            module.create.id();
107
            module.bind.events();
108
109
            module.observeChanges();
110
            module.instantiate();
111
          }
112
113
        },
114
115
        instantiate: function() {
116
          module.verbose('Storing instance of dropdown', module);
117
          instance = module;
118
          $module
119
            .data(moduleNamespace, module)
120
          ;
121
        },
122
123
        destroy: function() {
124
          module.verbose('Destroying previous dropdown', $module);
125
          module.remove.tabbable();
126
          $module
127
            .off(eventNamespace)
128
            .removeData(moduleNamespace)
129
          ;
130
          $menu
131
            .off(eventNamespace)
132
          ;
133
          $document
134
            .off(elementNamespace)
135
          ;
136
          module.disconnect.menuObserver();
137
          module.disconnect.selectObserver();
138
        },
139
140
        observeChanges: function() {
141
          if('MutationObserver' in window) {
142
            selectObserver = new MutationObserver(module.event.select.mutation);
143
            menuObserver   = new MutationObserver(module.event.menu.mutation);
144
            module.debug('Setting up mutation observer', selectObserver, menuObserver);
145
            module.observe.select();
146
            module.observe.menu();
147
          }
148
        },
149
150
        disconnect: {
151
          menuObserver: function() {
152
            if(menuObserver) {
153
              menuObserver.disconnect();
154
            }
155
          },
156
          selectObserver: function() {
157
            if(selectObserver) {
158
              selectObserver.disconnect();
159
            }
160
          }
161
        },
162
        observe: {
163
          select: function() {
164
            if(module.has.input()) {
165
              selectObserver.observe($input[0], {
166
                childList : true,
167
                subtree   : true
168
              });
169
            }
170
          },
171
          menu: function() {
172
            if(module.has.menu()) {
173
              menuObserver.observe($menu[0], {
174
                childList : true,
175
                subtree   : true
176
              });
177
            }
178
          }
179
        },
180
181
        create: {
182
          id: function() {
183
            id = (Math.random().toString(16) + '000000000').substr(2, 8);
184
            elementNamespace = '.' + id;
185
            module.verbose('Creating unique id for element', id);
186
          },
187
          userChoice: function(values) {
188
            var
189
              $userChoices,
190
              $userChoice,
191
              isUserValue,
192
              html
193
            ;
194
            values = values || module.get.userValues();
195
            if(!values) {
196
              return false;
197
            }
198
            values = $.isArray(values)
199
              ? values
200
              : [values]
201
            ;
202
            $.each(values, function(index, value) {
203
              if(module.get.item(value) === false) {
204
                html         = settings.templates.addition( module.add.variables(message.addResult, value) );
205
                $userChoice  = $('<div />')
206
                  .html(html)
207
                  .attr('data-' + metadata.value, value)
208
                  .attr('data-' + metadata.text, value)
209
                  .addClass(className.addition)
210
                  .addClass(className.item)
211
                ;
212
                if(settings.hideAdditions) {
213
                  $userChoice.addClass(className.hidden);
214
                }
215
                $userChoices = ($userChoices === undefined)
216
                  ? $userChoice
217
                  : $userChoices.add($userChoice)
218
                ;
219
                module.verbose('Creating user choices for value', value, $userChoice);
220
              }
221
            });
222
            return $userChoices;
223
          },
224
          userLabels: function(value) {
225
            var
226
              userValues = module.get.userValues()
227
            ;
228
            if(userValues) {
229
              module.debug('Adding user labels', userValues);
230
              $.each(userValues, function(index, value) {
231
                module.verbose('Adding custom user value');
232
                module.add.label(value, value);
233
              });
234
            }
235
          },
236
          menu: function() {
237
            $menu = $('<div />')
238
              .addClass(className.menu)
239
              .appendTo($module)
240
            ;
241
          },
242
          sizer: function() {
243
            $sizer = $('<span />')
244
              .addClass(className.sizer)
245
              .insertAfter($search)
246
            ;
247
          }
248
        },
249
250
        search: function(query) {
251
          query = (query !== undefined)
252
            ? query
253
            : module.get.query()
254
          ;
255
          module.verbose('Searching for query', query);
256
          if(module.has.minCharacters(query)) {
257
            module.filter(query);
258
          }
259
          else {
260
            module.hide();
261
          }
262
        },
263
264
        select: {
265
          firstUnfiltered: function() {
266
            module.verbose('Selecting first non-filtered element');
267
            module.remove.selectedItem();
268
            $item
269
              .not(selector.unselectable)
270
              .not(selector.addition + selector.hidden)
271
                .eq(0)
272
                .addClass(className.selected)
273
            ;
274
          },
275
          nextAvailable: function($selected) {
276
            $selected = $selected.eq(0);
277
            var
278
              $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
279
              $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
280
              hasNext        = ($nextAvailable.length > 0)
281
            ;
282
            if(hasNext) {
283
              module.verbose('Moving selection to', $nextAvailable);
284
              $nextAvailable.addClass(className.selected);
285
            }
286
            else {
287
              module.verbose('Moving selection to', $prevAvailable);
288
              $prevAvailable.addClass(className.selected);
289
            }
290
          }
291
        },
292
293
        setup: {
294
          api: function() {
295
            var
296
              apiSettings = {
297
                debug   : settings.debug,
298
                urlData : {
299
                  value : module.get.value(),
300
                  query : module.get.query()
301
                },
302
                on    : false
303
              }
304
            ;
305
            module.verbose('First request, initializing API');
306
            $module
307
              .api(apiSettings)
308
            ;
309
          },
310
          layout: function() {
311
            if( $module.is('select') ) {
312
              module.setup.select();
313
              module.setup.returnedObject();
314
            }
315
            if( !module.has.menu() ) {
316
              module.create.menu();
317
            }
318
            if( module.is.search() && !module.has.search() ) {
319
              module.verbose('Adding search input');
320
              $search = $('<input />')
321
                .addClass(className.search)
322
                .prop('autocomplete', 'off')
323
                .insertBefore($text)
324
              ;
325
            }
326
            if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
327
              module.create.sizer();
328
            }
329
            if(settings.allowTab) {
330
              module.set.tabbable();
331
            }
332
          },
333
          select: function() {
334
            var
335
              selectValues  = module.get.selectValues()
336
            ;
337
            module.debug('Dropdown initialized on a select', selectValues);
338
            if( $module.is('select') ) {
339
              $input = $module;
340
            }
341
            // see if select is placed correctly already
342
            if($input.parent(selector.dropdown).length > 0) {
343
              module.debug('UI dropdown already exists. Creating dropdown menu only');
344
              $module = $input.closest(selector.dropdown);
345
              if( !module.has.menu() ) {
346
                module.create.menu();
347
              }
348
              $menu = $module.children(selector.menu);
349
              module.setup.menu(selectValues);
350
            }
351
            else {
352
              module.debug('Creating entire dropdown from select');
353
              $module = $('<div />')
354
                .attr('class', $input.attr('class') )
355
                .addClass(className.selection)
356
                .addClass(className.dropdown)
357
                .html( templates.dropdown(selectValues) )
358
                .insertBefore($input)
359
              ;
360
              if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
361
                module.error(error.missingMultiple);
362
                $input.prop('multiple', true);
363
              }
364
              if($input.is('[multiple]')) {
365
                module.set.multiple();
366
              }
367
              if ($input.prop('disabled')) {
368
                module.debug('Disabling dropdown');
369
                $module.addClass(className.disabled);
370
              }
371
              $input
372
                .removeAttr('class')
373
                .detach()
374
                .prependTo($module)
375
              ;
376
            }
377
            module.refresh();
378
          },
379
          menu: function(values) {
380
            $menu.html( templates.menu(values, fields));
381
            $item = $menu.find(selector.item);
382
          },
383
          reference: function() {
384
            module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
385
            // replace module reference
386
            $module = $module.parent(selector.dropdown);
387
            module.refresh();
388
            module.setup.returnedObject();
389
            // invoke method in context of current instance
390
            if(methodInvoked) {
391
              instance = module;
392
              module.invoke(query);
393
            }
394
          },
395
          returnedObject: function() {
396
            var
397
              $firstModules = $allModules.slice(0, elementIndex),
398
              $lastModules = $allModules.slice(elementIndex + 1)
399
            ;
400
            // adjust all modules to use correct reference
401
            $allModules = $firstModules.add($module).add($lastModules);
402
          }
403
        },
404
405
        refresh: function() {
406
          module.refreshSelectors();
407
          module.refreshData();
408
        },
409
410
        refreshItems: function() {
411
          $item = $menu.find(selector.item);
412
        },
413
414
        refreshSelectors: function() {
415
          module.verbose('Refreshing selector cache');
416
          $text   = $module.find(selector.text);
417
          $search = $module.find(selector.search);
418
          $input  = $module.find(selector.input);
419
          $icon   = $module.find(selector.icon);
420
          $combo  = ($module.prev().find(selector.text).length > 0)
421
            ? $module.prev().find(selector.text)
422
            : $module.prev()
423
          ;
424
          $menu    = $module.children(selector.menu);
425
          $item    = $menu.find(selector.item);
426
        },
427
428
        refreshData: function() {
429
          module.verbose('Refreshing cached metadata');
430
          $item
431
            .removeData(metadata.text)
432
            .removeData(metadata.value)
433
          ;
434
        },
435
436
        clearData: function() {
437
          module.verbose('Clearing metadata');
438
          $item
439
            .removeData(metadata.text)
440
            .removeData(metadata.value)
441
          ;
442
          $module
443
            .removeData(metadata.defaultText)
444
            .removeData(metadata.defaultValue)
445
            .removeData(metadata.placeholderText)
446
          ;
447
        },
448
449
        toggle: function() {
450
          module.verbose('Toggling menu visibility');
451
          if( !module.is.active() ) {
452
            module.show();
453
          }
454
          else {
455
            module.hide();
456
          }
457
        },
458
459
        show: function(callback) {
460
          callback = $.isFunction(callback)
461
            ? callback
462
            : function(){}
463
          ;
464
          if(!module.can.show() && module.is.remote()) {
465
            module.debug('No API results retrieved, searching before show');
466
            module.queryRemote(module.get.query(), module.show);
467
          }
468
          if( module.can.show() && !module.is.active() ) {
469
            module.debug('Showing dropdown');
470
            if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
471
              module.remove.message();
472
            }
473
            if(module.is.allFiltered()) {
474
              return true;
475
            }
476
            if(settings.onShow.call(element) !== false) {
477
              module.animate.show(function() {
478
                if( module.can.click() ) {
479
                  module.bind.intent();
480
                }
481
                if(module.has.menuSearch()) {
482
                  module.focusSearch();
483
                }
484
                module.set.visible();
485
                callback.call(element);
486
              });
487
            }
488
          }
489
        },
490
491
        hide: function(callback) {
492
          callback = $.isFunction(callback)
493
            ? callback
494
            : function(){}
495
          ;
496
          if( module.is.active() ) {
497
            module.debug('Hiding dropdown');
498
            if(settings.onHide.call(element) !== false) {
499
              module.animate.hide(function() {
500
                module.remove.visible();
501
                callback.call(element);
502
              });
503
            }
504
          }
505
        },
506
507
        hideOthers: function() {
508
          module.verbose('Finding other dropdowns to hide');
509
          $allModules
510
            .not($module)
511
              .has(selector.menu + '.' + className.visible)
512
                .dropdown('hide')
513
          ;
514
        },
515
516
        hideMenu: function() {
517
          module.verbose('Hiding menu  instantaneously');
518
          module.remove.active();
519
          module.remove.visible();
520
          $menu.transition('hide');
521
        },
522
523
        hideSubMenus: function() {
524
          var
525
            $subMenus = $menu.children(selector.item).find(selector.menu)
526
          ;
527
          module.verbose('Hiding sub menus', $subMenus);
528
          $subMenus.transition('hide');
529
        },
530
531
        bind: {
532
          events: function() {
533
            if(hasTouch) {
534
              module.bind.touchEvents();
535
            }
536
            module.bind.keyboardEvents();
537
            module.bind.inputEvents();
538
            module.bind.mouseEvents();
539
          },
540
          touchEvents: function() {
541
            module.debug('Touch device detected binding additional touch events');
542
            if( module.is.searchSelection() ) {
543
              // do nothing special yet
544
            }
545
            else if( module.is.single() ) {
546
              $module
547
                .on('touchstart' + eventNamespace, module.event.test.toggle)
548
              ;
549
            }
550
            $menu
551
              .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter)
552
            ;
553
          },
554
          keyboardEvents: function() {
555
            module.verbose('Binding keyboard events');
556
            $module
557
              .on('keydown' + eventNamespace, module.event.keydown)
558
            ;
559
            if( module.has.search() ) {
560
              $module
561
                .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
562
              ;
563
            }
564
            if( module.is.multiple() ) {
565
              $document
566
                .on('keydown' + elementNamespace, module.event.document.keydown)
567
              ;
568
            }
569
          },
570
          inputEvents: function() {
571
            module.verbose('Binding input change events');
572
            $module
573
              .on('change' + eventNamespace, selector.input, module.event.change)
574
            ;
575
          },
576
          mouseEvents: function() {
577
            module.verbose('Binding mouse events');
578
            if(module.is.multiple()) {
579
              $module
580
                .on('click'   + eventNamespace, selector.label,  module.event.label.click)
581
                .on('click'   + eventNamespace, selector.remove, module.event.remove.click)
582
              ;
583
            }
584
            if( module.is.searchSelection() ) {
585
              $module
586
                .on('mousedown' + eventNamespace, module.event.mousedown)
587
                .on('mouseup'   + eventNamespace, module.event.mouseup)
588
                .on('mousedown' + eventNamespace, selector.menu,   module.event.menu.mousedown)
589
                .on('mouseup'   + eventNamespace, selector.menu,   module.event.menu.mouseup)
590
                .on('click'     + eventNamespace, selector.icon,   module.event.icon.click)
591
                .on('focus'     + eventNamespace, selector.search, module.event.search.focus)
592
                .on('click'     + eventNamespace, selector.search, module.event.search.focus)
593
                .on('blur'      + eventNamespace, selector.search, module.event.search.blur)
594
                .on('click'     + eventNamespace, selector.text,   module.event.text.focus)
595
              ;
596
              if(module.is.multiple()) {
597
                $module
598
                  .on('click' + eventNamespace, module.event.click)
599
                ;
600
              }
601
            }
602
            else {
603
              if(settings.on == 'click') {
604
                $module
605
                  .on('click' + eventNamespace, selector.icon, module.event.icon.click)
606
                  .on('click' + eventNamespace, module.event.test.toggle)
607
                ;
608
              }
609
              else if(settings.on == 'hover') {
610
                $module
611
                  .on('mouseenter' + eventNamespace, module.delay.show)
612
                  .on('mouseleave' + eventNamespace, module.delay.hide)
613
                ;
614
              }
615
              else {
616
                $module
617
                  .on(settings.on + eventNamespace, module.toggle)
618
                ;
619
              }
620
              $module
621
                .on('mousedown' + eventNamespace, module.event.mousedown)
622
                .on('mouseup'   + eventNamespace, module.event.mouseup)
623
                .on('focus'     + eventNamespace, module.event.focus)
624
              ;
625
              if(module.has.menuSearch() ) {
626
                $module
627
                  .on('blur' + eventNamespace, selector.search, module.event.search.blur)
628
                ;
629
              }
630
              else {
631
                $module
632
                  .on('blur' + eventNamespace, module.event.blur)
633
                ;
634
              }
635
            }
636
            $menu
637
              .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter)
638
              .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
639
              .on('click'      + eventNamespace, selector.item, module.event.item.click)
640
            ;
641
          },
642
          intent: function() {
643
            module.verbose('Binding hide intent event to document');
644
            if(hasTouch) {
645
              $document
646
                .on('touchstart' + elementNamespace, module.event.test.touch)
647
                .on('touchmove'  + elementNamespace, module.event.test.touch)
648
              ;
649
            }
650
            $document
651
              .on('click' + elementNamespace, module.event.test.hide)
652
            ;
653
          }
654
        },
655
656
        unbind: {
657
          intent: function() {
658
            module.verbose('Removing hide intent event from document');
659
            if(hasTouch) {
660
              $document
661
                .off('touchstart' + elementNamespace)
662
                .off('touchmove' + elementNamespace)
663
              ;
664
            }
665
            $document
666
              .off('click' + elementNamespace)
667
            ;
668
          }
669
        },
670
671
        filter: function(query) {
672
          var
673
            searchTerm = (query !== undefined)
674
              ? query
675
              : module.get.query(),
676
            afterFiltered = function() {
677
              if(module.is.multiple()) {
678
                module.filterActive();
679
              }
680
              if(query || (!query && module.get.activeItem().length == 0)) {
681
                module.select.firstUnfiltered();
682
              }
683
              if( module.has.allResultsFiltered() ) {
684
                if( settings.onNoResults.call(element, searchTerm) ) {
685
                  if(settings.allowAdditions) {
686
                    if(settings.hideAdditions) {
687
                      module.verbose('User addition with no menu, setting empty style');
688
                      module.set.empty();
689
                      module.hideMenu();
690
                    }
691
                  }
692
                  else {
693
                    module.verbose('All items filtered, showing message', searchTerm);
694
                    module.add.message(message.noResults);
695
                  }
696
                }
697
                else {
698
                  module.verbose('All items filtered, hiding dropdown', searchTerm);
699
                  module.hideMenu();
700
                }
701
              }
702
              else {
703
                module.remove.empty();
704
                module.remove.message();
705
              }
706
              if(settings.allowAdditions) {
707
                module.add.userSuggestion(query);
708
              }
709
              if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
710
                module.show();
711
              }
712
            }
713
          ;
714
          if(settings.useLabels && module.has.maxSelections()) {
715
            return;
716
          }
717
          if(settings.apiSettings) {
718
            if( module.can.useAPI() ) {
719
              module.queryRemote(searchTerm, function() {
720
                if(settings.filterRemoteData) {
721
                  module.filterItems(searchTerm);
722
                }
723
                afterFiltered();
724
              });
725
            }
726
            else {
727
              module.error(error.noAPI);
728
            }
729
          }
730
          else {
731
            module.filterItems(searchTerm);
732
            afterFiltered();
733
          }
734
        },
735
736
        queryRemote: function(query, callback) {
737
          var
738
            apiSettings = {
739
              errorDuration : false,
740
              cache         : 'local',
741
              throttle      : settings.throttle,
742
              urlData       : {
743
                query: query
744
              },
745
              onError: function() {
746
                module.add.message(message.serverError);
747
                callback();
748
              },
749
              onFailure: function() {
750
                module.add.message(message.serverError);
751
                callback();
752
              },
753
              onSuccess : function(response) {
754
                module.remove.message();
755
                module.setup.menu({
756
                  values: response[fields.remoteValues]
757
                });
758
                callback();
759
              }
760
            }
761
          ;
762
          if( !$module.api('get request') ) {
763
            module.setup.api();
764
          }
765
          apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
766
          $module
767
            .api('setting', apiSettings)
768
            .api('query')
769
          ;
770
        },
771
772
        filterItems: function(query) {
773
          var
774
            searchTerm = (query !== undefined)
775
              ? query
776
              : module.get.query(),
777
            results          =  null,
778
            escapedTerm      = module.escape.string(searchTerm),
779
            beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm')
780
          ;
781
          // avoid loop if we're matching nothing
782
          if( module.has.query() ) {
783
            results = [];
784
785
            module.verbose('Searching for matching values', searchTerm);
786
            $item
787
              .each(function(){
788
                var
789
                  $choice = $(this),
790
                  text,
791
                  value
792
                ;
793
                if(settings.match == 'both' || settings.match == 'text') {
794
                  text = String(module.get.choiceText($choice, false));
795
                  if(text.search(beginsWithRegExp) !== -1) {
796
                    results.push(this);
797
                    return true;
798
                  }
799
                  else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
800
                    results.push(this);
801
                    return true;
802
                  }
803
                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
804
                    results.push(this);
805
                    return true;
806
                  }
807
                }
808
                if(settings.match == 'both' || settings.match == 'value') {
809
                  value = String(module.get.choiceValue($choice, text));
810
                  if(value.search(beginsWithRegExp) !== -1) {
811
                    results.push(this);
812
                    return true;
813
                  }
814
                  else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
815
                    results.push(this);
816
                    return true;
817
                  }
818
                  else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
819
                    results.push(this);
820
                    return true;
821
                  }
822
                }
823
              })
824
            ;
825
          }
826
          module.debug('Showing only matched items', searchTerm);
827
          module.remove.filteredItem();
828
          if(results) {
829
            $item
830
              .not(results)
831
              .addClass(className.filtered)
832
            ;
833
          }
834
        },
835
836
        fuzzySearch: function(query, term) {
837
          var
838
            termLength  = term.length,
839
            queryLength = query.length
840
          ;
841
          query = query.toLowerCase();
842
          term  = term.toLowerCase();
843
          if(queryLength > termLength) {
844
            return false;
845
          }
846
          if(queryLength === termLength) {
847
            return (query === term);
848
          }
849
          search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
850
            var
851
              queryCharacter = query.charCodeAt(characterIndex)
852
            ;
853
            while(nextCharacterIndex < termLength) {
854
              if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
855
                continue search;
856
              }
857
            }
858
            return false;
859
          }
860
          return true;
861
        },
862
        exactSearch: function (query, term) {
863
          query = query.toLowerCase();
864
          term  = term.toLowerCase();
865
          if(term.indexOf(query) > -1) {
866
             return true;
867
          }
868
          return false;
869
        },
870
        filterActive: function() {
871
          if(settings.useLabels) {
872
            $item.filter('.' + className.active)
873
              .addClass(className.filtered)
874
            ;
875
          }
876
        },
877
878
        focusSearch: function(skipHandler) {
879
          if( module.has.search() && !module.is.focusedOnSearch() ) {
880
            if(skipHandler) {
881
              $module.off('focus' + eventNamespace, selector.search);
882
              $search.focus();
883
              $module.on('focus'  + eventNamespace, selector.search, module.event.search.focus);
884
            }
885
            else {
886
              $search.focus();
887
            }
888
          }
889
        },
890
891
        forceSelection: function() {
892
          var
893
            $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
894
            $activeItem        = $item.not(className.filtered).filter('.' + className.active).eq(0),
895
            $selectedItem      = ($currentlySelected.length > 0)
896
              ? $currentlySelected
897
              : $activeItem,
898
            hasSelected = ($selectedItem.length > 0)
899
          ;
900
          if(hasSelected && !module.is.multiple()) {
901
            module.debug('Forcing partial selection to selected item', $selectedItem);
902
            module.event.item.click.call($selectedItem, {}, true);
903
            return;
904
          }
905
          else {
906
            if(settings.allowAdditions) {
907
              module.set.selected(module.get.query());
908
              module.remove.searchTerm();
909
            }
910
            else {
911
              module.remove.searchTerm();
912
            }
913
          }
914
        },
915
916
        event: {
917
          change: function() {
918
            if(!internalChange) {
919
              module.debug('Input changed, updating selection');
920
              module.set.selected();
921
            }
922
          },
923
          focus: function() {
924
            if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
925
              module.show();
926
            }
927
          },
928
          blur: function(event) {
929
            pageLostFocus = (document.activeElement === this);
930
            if(!activated && !pageLostFocus) {
931
              module.remove.activeLabel();
932
              module.hide();
933
            }
934
          },
935
          mousedown: function() {
936
            if(module.is.searchSelection()) {
937
              // prevent menu hiding on immediate re-focus
938
              willRefocus = true;
939
            }
940
            else {
941
              // prevents focus callback from occurring on mousedown
942
              activated = true;
943
            }
944
          },
945
          mouseup: function() {
946
            if(module.is.searchSelection()) {
947
              // prevent menu hiding on immediate re-focus
948
              willRefocus = false;
949
            }
950
            else {
951
              activated = false;
952
            }
953
          },
954
          click: function(event) {
955
            var
956
              $target = $(event.target)
957
            ;
958
            // focus search
959
            if($target.is($module)) {
960
              if(!module.is.focusedOnSearch()) {
961
                module.focusSearch();
962
              }
963
              else {
964
                module.show();
965
              }
966
            }
967
          },
968
          search: {
969
            focus: function() {
970
              activated = true;
971
              if(module.is.multiple()) {
972
                module.remove.activeLabel();
973
              }
974
              if(settings.showOnFocus) {
975
                module.search();
976
              }
977
            },
978
            blur: function(event) {
979
              pageLostFocus = (document.activeElement === this);
980
              if(module.is.searchSelection() && !willRefocus) {
981
                if(!itemActivated && !pageLostFocus) {
982
                  if(settings.forceSelection) {
983
                    module.forceSelection();
984
                  }
985
                  module.hide();
986
                }
987
              }
988
              willRefocus = false;
989
            }
990
          },
991
          icon: {
992
            click: function(event) {
993
              module.toggle();
994
            }
995
          },
996
          text: {
997
            focus: function(event) {
998
              activated = true;
999
              module.focusSearch();
1000
            }
1001
          },
1002
          input: function(event) {
1003
            if(module.is.multiple() || module.is.searchSelection()) {
1004
              module.set.filtered();
1005
            }
1006
            clearTimeout(module.timer);
1007
            module.timer = setTimeout(module.search, settings.delay.search);
1008
          },
1009
          label: {
1010
            click: function(event) {
1011
              var
1012
                $label        = $(this),
1013
                $labels       = $module.find(selector.label),
1014
                $activeLabels = $labels.filter('.' + className.active),
1015
                $nextActive   = $label.nextAll('.' + className.active),
1016
                $prevActive   = $label.prevAll('.' + className.active),
1017
                $range = ($nextActive.length > 0)
1018
                  ? $label.nextUntil($nextActive).add($activeLabels).add($label)
1019
                  : $label.prevUntil($prevActive).add($activeLabels).add($label)
1020
              ;
1021
              if(event.shiftKey) {
1022
                $activeLabels.removeClass(className.active);
1023
                $range.addClass(className.active);
1024
              }
1025
              else if(event.ctrlKey) {
1026
                $label.toggleClass(className.active);
1027
              }
1028
              else {
1029
                $activeLabels.removeClass(className.active);
1030
                $label.addClass(className.active);
1031
              }
1032
              settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
1033
            }
1034
          },
1035
          remove: {
1036
            click: function() {
1037
              var
1038
                $label = $(this).parent()
1039
              ;
1040
              if( $label.hasClass(className.active) ) {
1041
                // remove all selected labels
1042
                module.remove.activeLabels();
1043
              }
1044
              else {
1045
                // remove this label only
1046
                module.remove.activeLabels( $label );
1047
              }
1048
            }
1049
          },
1050
          test: {
1051
            toggle: function(event) {
1052
              var
1053
                toggleBehavior = (module.is.multiple())
1054
                  ? module.show
1055
                  : module.toggle
1056
              ;
1057
              if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
1058
                return;
1059
              }
1060
              if( module.determine.eventOnElement(event, toggleBehavior) ) {
1061
                event.preventDefault();
1062
              }
1063
            },
1064
            touch: function(event) {
1065
              module.determine.eventOnElement(event, function() {
1066
                if(event.type == 'touchstart') {
1067
                  module.timer = setTimeout(function() {
1068
                    module.hide();
1069
                  }, settings.delay.touch);
1070
                }
1071
                else if(event.type == 'touchmove') {
1072
                  clearTimeout(module.timer);
1073
                }
1074
              });
1075
              event.stopPropagation();
1076
            },
1077
            hide: function(event) {
1078
              module.determine.eventInModule(event, module.hide);
1079
            }
1080
          },
1081
          select: {
1082
            mutation: function(mutations) {
1083
              module.debug('<select> modified, recreating menu');
1084
              module.setup.select();
1085
            }
1086
          },
1087
          menu: {
1088
            mutation: function(mutations) {
1089
              var
1090
                mutation   = mutations[0],
1091
                $addedNode = mutation.addedNodes
1092
                  ? $(mutation.addedNodes[0])
1093
                  : $(false),
1094
                $removedNode = mutation.removedNodes
1095
                  ? $(mutation.removedNodes[0])
1096
                  : $(false),
1097
                $changedNodes  = $addedNode.add($removedNode),
1098
                isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
1099
                isMessage      = $changedNodes.is(selector.message)  || $changedNodes.closest(selector.message).length > 0
1100
              ;
1101
              if(isUserAddition || isMessage) {
1102
                module.debug('Updating item selector cache');
1103
                module.refreshItems();
1104
              }
1105
              else {
1106
                module.debug('Menu modified, updating selector cache');
1107
                module.refresh();
1108
              }
1109
            },
1110
            mousedown: function() {
1111
              itemActivated = true;
1112
            },
1113
            mouseup: function() {
1114
              itemActivated = false;
1115
            }
1116
          },
1117
          item: {
1118
            mouseenter: function(event) {
1119
              var
1120
                $target        = $(event.target),
1121
                $item          = $(this),
1122
                $subMenu       = $item.children(selector.menu),
1123
                $otherMenus    = $item.siblings(selector.item).children(selector.menu),
1124
                hasSubMenu     = ($subMenu.length > 0),
1125
                isBubbledEvent = ($subMenu.find($target).length > 0)
1126
              ;
1127
              if( !isBubbledEvent && hasSubMenu ) {
1128
                clearTimeout(module.itemTimer);
1129
                module.itemTimer = setTimeout(function() {
1130
                  module.verbose('Showing sub-menu', $subMenu);
1131
                  $.each($otherMenus, function() {
1132
                    module.animate.hide(false, $(this));
1133
                  });
1134
                  module.animate.show(false, $subMenu);
1135
                }, settings.delay.show);
1136
                event.preventDefault();
1137
              }
1138
            },
1139
            mouseleave: function(event) {
1140
              var
1141
                $subMenu = $(this).children(selector.menu)
1142
              ;
1143
              if($subMenu.length > 0) {
1144
                clearTimeout(module.itemTimer);
1145
                module.itemTimer = setTimeout(function() {
1146
                  module.verbose('Hiding sub-menu', $subMenu);
1147
                  module.animate.hide(false, $subMenu);
1148
                }, settings.delay.hide);
1149
              }
1150
            },
1151
            click: function (event, skipRefocus) {
1152
              var
1153
                $choice        = $(this),
1154
                $target        = (event)
1155
                  ? $(event.target)
1156
                  : $(''),
1157
                $subMenu       = $choice.find(selector.menu),
1158
                text           = module.get.choiceText($choice),
1159
                value          = module.get.choiceValue($choice, text),
1160
                hasSubMenu     = ($subMenu.length > 0),
1161
                isBubbledEvent = ($subMenu.find($target).length > 0)
1162
              ;
1163
              // prevents IE11 bug where menu receives focus even though `tabindex=-1`
1164
              if(module.has.menuSearch()) {
1165
                $(document.activeElement).blur();
1166
              }
1167
              if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
1168
                if(module.is.searchSelection()) {
1169
                  if(settings.allowAdditions) {
1170
                    module.remove.userAddition();
1171
                  }
1172
                  module.remove.searchTerm();
1173
                  if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
1174
                    module.focusSearch(true);
1175
                  }
1176
                }
1177
                if(!settings.useLabels) {
1178
                  module.remove.filteredItem();
1179
                  module.set.scrollPosition($choice);
1180
                }
1181
                module.determine.selectAction.call(this, text, value);
1182
              }
1183
            }
1184
          },
1185
1186
          document: {
1187
            // label selection should occur even when element has no focus
1188
            keydown: function(event) {
1189
              var
1190
                pressedKey    = event.which,
1191
                isShortcutKey = module.is.inObject(pressedKey, keys)
1192
              ;
1193
              if(isShortcutKey) {
1194
                var
1195
                  $label            = $module.find(selector.label),
1196
                  $activeLabel      = $label.filter('.' + className.active),
1197
                  activeValue       = $activeLabel.data(metadata.value),
1198
                  labelIndex        = $label.index($activeLabel),
1199
                  labelCount        = $label.length,
1200
                  hasActiveLabel    = ($activeLabel.length > 0),
1201
                  hasMultipleActive = ($activeLabel.length > 1),
1202
                  isFirstLabel      = (labelIndex === 0),
1203
                  isLastLabel       = (labelIndex + 1 == labelCount),
1204
                  isSearch          = module.is.searchSelection(),
1205
                  isFocusedOnSearch = module.is.focusedOnSearch(),
1206
                  isFocused         = module.is.focused(),
1207
                  caretAtStart      = (isFocusedOnSearch && module.get.caretPosition() === 0),
1208
                  $nextLabel
1209
                ;
1210
                if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
1211
                  return;
1212
                }
1213
1214
                if(pressedKey == keys.leftArrow) {
1215
                  // activate previous label
1216
                  if((isFocused || caretAtStart) && !hasActiveLabel) {
1217
                    module.verbose('Selecting previous label');
1218
                    $label.last().addClass(className.active);
1219
                  }
1220
                  else if(hasActiveLabel) {
1221
                    if(!event.shiftKey) {
1222
                      module.verbose('Selecting previous label');
1223
                      $label.removeClass(className.active);
1224
                    }
1225
                    else {
1226
                      module.verbose('Adding previous label to selection');
1227
                    }
1228
                    if(isFirstLabel && !hasMultipleActive) {
1229
                      $activeLabel.addClass(className.active);
1230
                    }
1231
                    else {
1232
                      $activeLabel.prev(selector.siblingLabel)
1233
                        .addClass(className.active)
1234
                        .end()
1235
                      ;
1236
                    }
1237
                    event.preventDefault();
1238
                  }
1239
                }
1240
                else if(pressedKey == keys.rightArrow) {
1241
                  // activate first label
1242
                  if(isFocused && !hasActiveLabel) {
1243
                    $label.first().addClass(className.active);
1244
                  }
1245
                  // activate next label
1246
                  if(hasActiveLabel) {
1247
                    if(!event.shiftKey) {
1248
                      module.verbose('Selecting next label');
1249
                      $label.removeClass(className.active);
1250
                    }
1251
                    else {
1252
                      module.verbose('Adding next label to selection');
1253
                    }
1254
                    if(isLastLabel) {
1255
                      if(isSearch) {
1256
                        if(!isFocusedOnSearch) {
1257
                          module.focusSearch();
1258
                        }
1259
                        else {
1260
                          $label.removeClass(className.active);
1261
                        }
1262
                      }
1263
                      else if(hasMultipleActive) {
1264
                        $activeLabel.next(selector.siblingLabel).addClass(className.active);
1265
                      }
1266
                      else {
1267
                        $activeLabel.addClass(className.active);
1268
                      }
1269
                    }
1270
                    else {
1271
                      $activeLabel.next(selector.siblingLabel).addClass(className.active);
1272
                    }
1273
                    event.preventDefault();
1274
                  }
1275
                }
1276
                else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
1277
                  if(hasActiveLabel) {
1278
                    module.verbose('Removing active labels');
1279
                    if(isLastLabel) {
1280
                      if(isSearch && !isFocusedOnSearch) {
1281
                        module.focusSearch();
1282
                      }
1283
                    }
1284
                    $activeLabel.last().next(selector.siblingLabel).addClass(className.active);
1285
                    module.remove.activeLabels($activeLabel);
1286
                    event.preventDefault();
1287
                  }
1288
                  else if(caretAtStart && !hasActiveLabel && pressedKey == keys.backspace) {
1289
                    module.verbose('Removing last label on input backspace');
1290
                    $activeLabel = $label.last().addClass(className.active);
1291
                    module.remove.activeLabels($activeLabel);
1292
                  }
1293
                }
1294
                else {
1295
                  $activeLabel.removeClass(className.active);
1296
                }
1297
              }
1298
            }
1299
          },
1300
1301
          keydown: function(event) {
1302
            var
1303
              pressedKey    = event.which,
1304
              isShortcutKey = module.is.inObject(pressedKey, keys)
1305
            ;
1306
            if(isShortcutKey) {
1307
              var
1308
                $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
1309
                $activeItem        = $menu.children('.' + className.active).eq(0),
1310
                $selectedItem      = ($currentlySelected.length > 0)
1311
                  ? $currentlySelected
1312
                  : $activeItem,
1313
                $visibleItems = ($selectedItem.length > 0)
1314
                  ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
1315
                  : $menu.children(':not(.' + className.filtered +')'),
1316
                $subMenu              = $selectedItem.children(selector.menu),
1317
                $parentMenu           = $selectedItem.closest(selector.menu),
1318
                inVisibleMenu         = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0),
1319
                hasSubMenu            = ($subMenu.length> 0),
1320
                hasSelectedItem       = ($selectedItem.length > 0),
1321
                selectedIsSelectable  = ($selectedItem.not(selector.unselectable).length > 0),
1322
                delimiterPressed      = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
1323
                isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
1324
                $nextItem,
1325
                isSubMenuItem,
1326
                newIndex
1327
              ;
1328
              // allow selection with menu closed
1329
              if(isAdditionWithoutMenu) {
1330
                module.verbose('Selecting item from keyboard shortcut', $selectedItem);
1331
                module.event.item.click.call($selectedItem, event);
1332
                if(module.is.searchSelection()) {
1333
                  module.remove.searchTerm();
1334
                }
1335
              }
1336
1337
              // visible menu keyboard shortcuts
1338
              if( module.is.visible() ) {
1339
1340
                // enter (select or open sub-menu)
1341
                if(pressedKey == keys.enter || delimiterPressed) {
1342
                  if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
1343
                    module.verbose('Pressed enter on unselectable category, opening sub menu');
1344
                    pressedKey = keys.rightArrow;
1345
                  }
1346
                  else if(selectedIsSelectable) {
1347
                    module.verbose('Selecting item from keyboard shortcut', $selectedItem);
1348
                    module.event.item.click.call($selectedItem, event);
1349
                    if(module.is.searchSelection()) {
1350
                      module.remove.searchTerm();
1351
                    }
1352
                  }
1353
                  event.preventDefault();
1354
                }
1355
1356
                // sub-menu actions
1357
                if(hasSelectedItem) {
1358
1359
                  if(pressedKey == keys.leftArrow) {
1360
1361
                    isSubMenuItem = ($parentMenu[0] !== $menu[0]);
1362
1363
                    if(isSubMenuItem) {
1364
                      module.verbose('Left key pressed, closing sub-menu');
1365
                      module.animate.hide(false, $parentMenu);
1366
                      $selectedItem
1367
                        .removeClass(className.selected)
1368
                      ;
1369
                      $parentMenu
1370
                        .closest(selector.item)
1371
                          .addClass(className.selected)
1372
                      ;
1373
                      event.preventDefault();
1374
                    }
1375
                  }
1376
1377
                  // right arrow (show sub-menu)
1378
                  if(pressedKey == keys.rightArrow) {
1379
                    if(hasSubMenu) {
1380
                      module.verbose('Right key pressed, opening sub-menu');
1381
                      module.animate.show(false, $subMenu);
1382
                      $selectedItem
1383
                        .removeClass(className.selected)
1384
                      ;
1385
                      $subMenu
1386
                        .find(selector.item).eq(0)
1387
                          .addClass(className.selected)
1388
                      ;
1389
                      event.preventDefault();
1390
                    }
1391
                  }
1392
                }
1393
1394
                // up arrow (traverse menu up)
1395
                if(pressedKey == keys.upArrow) {
1396
                  $nextItem = (hasSelectedItem && inVisibleMenu)
1397
                    ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
1398
                    : $item.eq(0)
1399
                  ;
1400
                  if($visibleItems.index( $nextItem ) < 0) {
1401
                    module.verbose('Up key pressed but reached top of current menu');
1402
                    event.preventDefault();
1403
                    return;
1404
                  }
1405
                  else {
1406
                    module.verbose('Up key pressed, changing active item');
1407
                    $selectedItem
1408
                      .removeClass(className.selected)
1409
                    ;
1410
                    $nextItem
1411
                      .addClass(className.selected)
1412
                    ;
1413
                    module.set.scrollPosition($nextItem);
1414
                    if(settings.selectOnKeydown && module.is.single()) {
1415
                      module.set.selectedItem($nextItem);
1416
                    }
1417
                  }
1418
                  event.preventDefault();
1419
                }
1420
1421
                // down arrow (traverse menu down)
1422
                if(pressedKey == keys.downArrow) {
1423
                  $nextItem = (hasSelectedItem && inVisibleMenu)
1424
                    ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
1425
                    : $item.eq(0)
1426
                  ;
1427
                  if($nextItem.length === 0) {
1428
                    module.verbose('Down key pressed but reached bottom of current menu');
1429
                    event.preventDefault();
1430
                    return;
1431
                  }
1432
                  else {
1433
                    module.verbose('Down key pressed, changing active item');
1434
                    $item
1435
                      .removeClass(className.selected)
1436
                    ;
1437
                    $nextItem
1438
                      .addClass(className.selected)
1439
                    ;
1440
                    module.set.scrollPosition($nextItem);
1441
                    if(settings.selectOnKeydown && module.is.single()) {
1442
                      module.set.selectedItem($nextItem);
1443
                    }
1444
                  }
1445
                  event.preventDefault();
1446
                }
1447
1448
                // page down (show next page)
1449
                if(pressedKey == keys.pageUp) {
1450
                  module.scrollPage('up');
1451
                  event.preventDefault();
1452
                }
1453
                if(pressedKey == keys.pageDown) {
1454
                  module.scrollPage('down');
1455
                  event.preventDefault();
1456
                }
1457
1458
                // escape (close menu)
1459
                if(pressedKey == keys.escape) {
1460
                  module.verbose('Escape key pressed, closing dropdown');
1461
                  module.hide();
1462
                }
1463
1464
              }
1465
              else {
1466
                // delimiter key
1467
                if(delimiterPressed) {
1468
                  event.preventDefault();
1469
                }
1470
                // down arrow (open menu)
1471
                if(pressedKey == keys.downArrow && !module.is.visible()) {
1472
                  module.verbose('Down key pressed, showing dropdown');
1473
                  module.show();
1474
                  event.preventDefault();
1475
                }
1476
              }
1477
            }
1478
            else {
1479
              if( !module.has.search() ) {
1480
                module.set.selectedLetter( String.fromCharCode(pressedKey) );
1481
              }
1482
            }
1483
          }
1484
        },
1485
1486
        trigger: {
1487
          change: function() {
1488
            var
1489
              events       = document.createEvent('HTMLEvents'),
1490
              inputElement = $input[0]
1491
            ;
1492
            if(inputElement) {
1493
              module.verbose('Triggering native change event');
1494
              events.initEvent('change', true, false);
1495
              inputElement.dispatchEvent(events);
1496
            }
1497
          }
1498
        },
1499
1500
        determine: {
1501
          selectAction: function(text, value) {
1502
            module.verbose('Determining action', settings.action);
1503
            if( $.isFunction( module.action[settings.action] ) ) {
1504
              module.verbose('Triggering preset action', settings.action, text, value);
1505
              module.action[ settings.action ].call(element, text, value, this);
1506
            }
1507
            else if( $.isFunction(settings.action) ) {
1508
              module.verbose('Triggering user action', settings.action, text, value);
1509
              settings.action.call(element, text, value, this);
1510
            }
1511
            else {
1512
              module.error(error.action, settings.action);
1513
            }
1514
          },
1515
          eventInModule: function(event, callback) {
1516
            var
1517
              $target    = $(event.target),
1518
              inDocument = ($target.closest(document.documentElement).length > 0),
1519
              inModule   = ($target.closest($module).length > 0)
1520
            ;
1521
            callback = $.isFunction(callback)
1522
              ? callback
1523
              : function(){}
1524
            ;
1525
            if(inDocument && !inModule) {
1526
              module.verbose('Triggering event', callback);
1527
              callback();
1528
              return true;
1529
            }
1530
            else {
1531
              module.verbose('Event occurred in dropdown, canceling callback');
1532
              return false;
1533
            }
1534
          },
1535
          eventOnElement: function(event, callback) {
1536
            var
1537
              $target      = $(event.target),
1538
              $label       = $target.closest(selector.siblingLabel),
1539
              inVisibleDOM = document.body.contains(event.target),
1540
              notOnLabel   = ($module.find($label).length === 0),
1541
              notInMenu    = ($target.closest($menu).length === 0)
1542
            ;
1543
            callback = $.isFunction(callback)
1544
              ? callback
1545
              : function(){}
1546
            ;
1547
            if(inVisibleDOM && notOnLabel && notInMenu) {
1548
              module.verbose('Triggering event', callback);
1549
              callback();
1550
              return true;
1551
            }
1552
            else {
1553
              module.verbose('Event occurred in dropdown menu, canceling callback');
1554
              return false;
1555
            }
1556
          }
1557
        },
1558
1559
        action: {
1560
1561
          nothing: function() {},
1562
1563
          activate: function(text, value, element) {
1564
            value = (value !== undefined)
1565
              ? value
1566
              : text
1567
            ;
1568
            if( module.can.activate( $(element) ) ) {
1569
              module.set.selected(value, $(element));
1570
              if(module.is.multiple() && !module.is.allFiltered()) {
1571
                return;
1572
              }
1573
              else {
1574
                module.hideAndClear();
1575
              }
1576
            }
1577
          },
1578
1579
          select: function(text, value, element) {
1580
            value = (value !== undefined)
1581
              ? value
1582
              : text
1583
            ;
1584
            if( module.can.activate( $(element) ) ) {
1585
              module.set.value(value, $(element));
1586
              if(module.is.multiple() && !module.is.allFiltered()) {
1587
                return;
1588
              }
1589
              else {
1590
                module.hideAndClear();
1591
              }
1592
            }
1593
          },
1594
1595
          combo: function(text, value, element) {
1596
            value = (value !== undefined)
1597
              ? value
1598
              : text
1599
            ;
1600
            module.set.selected(value, $(element));
1601
            module.hideAndClear();
1602
          },
1603
1604
          hide: function(text, value, element) {
1605
            module.set.value(value, text);
1606
            module.hideAndClear();
1607
          }
1608
1609
        },
1610
1611
        get: {
1612
          id: function() {
1613
            return id;
1614
          },
1615
          defaultText: function() {
1616
            return $module.data(metadata.defaultText);
1617
          },
1618
          defaultValue: function() {
1619
            return $module.data(metadata.defaultValue);
1620
          },
1621
          placeholderText: function() {
1622
            return $module.data(metadata.placeholderText) || '';
1623
          },
1624
          text: function() {
1625
            return $text.text();
1626
          },
1627
          query: function() {
1628
            return $.trim($search.val());
1629
          },
1630
          searchWidth: function(value) {
1631
            value = (value !== undefined)
1632
              ? value
1633
              : $search.val()
1634
            ;
1635
            $sizer.text(value);
1636
            // prevent rounding issues
1637
            return Math.ceil( $sizer.width() + 1);
1638
          },
1639
          selectionCount: function() {
1640
            var
1641
              values = module.get.values(),
1642
              count
1643
            ;
1644
            count = ( module.is.multiple() )
1645
              ? $.isArray(values)
1646
                ? values.length
1647
                : 0
1648
              : (module.get.value() !== '')
1649
                ? 1
1650
                : 0
1651
            ;
1652
            return count;
1653
          },
1654
          transition: function($subMenu) {
1655
            return (settings.transition == 'auto')
1656
              ? module.is.upward($subMenu)
1657
                ? 'slide up'
1658
                : 'slide down'
1659
              : settings.transition
1660
            ;
1661
          },
1662
          userValues: function() {
1663
            var
1664
              values = module.get.values()
1665
            ;
1666
            if(!values) {
1667
              return false;
1668
            }
1669
            values = $.isArray(values)
1670
              ? values
1671
              : [values]
1672
            ;
1673
            return $.grep(values, function(value) {
1674
              return (module.get.item(value) === false);
1675
            });
1676
          },
1677
          uniqueArray: function(array) {
1678
            return $.grep(array, function (value, index) {
1679
                return $.inArray(value, array) === index;
1680
            });
1681
          },
1682
          caretPosition: function() {
1683
            var
1684
              input = $search.get(0),
1685
              range,
1686
              rangeLength
1687
            ;
1688
            if('selectionStart' in input) {
1689
              return input.selectionStart;
1690
            }
1691
            else if (document.selection) {
1692
              input.focus();
1693
              range       = document.selection.createRange();
1694
              rangeLength = range.text.length;
1695
              range.moveStart('character', -input.value.length);
1696
              return range.text.length - rangeLength;
1697
            }
1698
          },
1699
          value: function() {
1700
            var
1701
              value = ($input.length > 0)
1702
                ? $input.val()
1703
                : $module.data(metadata.value),
1704
              isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '')
1705
            ;
1706
            // prevents placeholder element from being selected when multiple
1707
            return (value === undefined || isEmptyMultiselect)
1708
              ? ''
1709
              : value
1710
            ;
1711
          },
1712
          values: function() {
1713
            var
1714
              value = module.get.value()
1715
            ;
1716
            if(value === '') {
1717
              return '';
1718
            }
1719
            return ( !module.has.selectInput() && module.is.multiple() )
1720
              ? (typeof value == 'string') // delimited string
1721
                ? value.split(settings.delimiter)
1722
                : ''
1723
              : value
1724
            ;
1725
          },
1726
          remoteValues: function() {
1727
            var
1728
              values = module.get.values(),
1729
              remoteValues = false
1730
            ;
1731
            if(values) {
1732
              if(typeof values == 'string') {
1733
                values = [values];
1734
              }
1735
              $.each(values, function(index, value) {
1736
                var
1737
                  name = module.read.remoteData(value)
1738
                ;
1739
                module.verbose('Restoring value from session data', name, value);
1740
                if(name) {
1741
                  if(!remoteValues) {
1742
                    remoteValues = {};
1743
                  }
1744
                  remoteValues[value] = name;
1745
                }
1746
              });
1747
            }
1748
            return remoteValues;
1749
          },
1750
          choiceText: function($choice, preserveHTML) {
1751
            preserveHTML = (preserveHTML !== undefined)
1752
              ? preserveHTML
1753
              : settings.preserveHTML
1754
            ;
1755
            if($choice) {
1756
              if($choice.find(selector.menu).length > 0) {
1757
                module.verbose('Retrieving text of element with sub-menu');
1758
                $choice = $choice.clone();
1759
                $choice.find(selector.menu).remove();
1760
                $choice.find(selector.menuIcon).remove();
1761
              }
1762
              return ($choice.data(metadata.text) !== undefined)
1763
                ? $choice.data(metadata.text)
1764
                : (preserveHTML)
1765
                  ? $.trim($choice.html())
1766
                  : $.trim($choice.text())
1767
              ;
1768
            }
1769
          },
1770
          choiceValue: function($choice, choiceText) {
1771
            choiceText = choiceText || module.get.choiceText($choice);
1772
            if(!$choice) {
1773
              return false;
1774
            }
1775
            return ($choice.data(metadata.value) !== undefined)
1776
              ? String( $choice.data(metadata.value) )
1777
              : (typeof choiceText === 'string')
1778
                ? $.trim(choiceText.toLowerCase())
1779
                : String(choiceText)
1780
            ;
1781
          },
1782
          inputEvent: function() {
1783
            var
1784
              input = $search[0]
1785
            ;
1786
            if(input) {
1787
              return (input.oninput !== undefined)
1788
                ? 'input'
1789
                : (input.onpropertychange !== undefined)
1790
                  ? 'propertychange'
1791
                  : 'keyup'
1792
              ;
1793
            }
1794
            return false;
1795
          },
1796
          selectValues: function() {
1797
            var
1798
              select = {}
1799
            ;
1800
            select.values = [];
1801
            $module
1802
              .find('option')
1803
                .each(function() {
1804
                  var
1805
                    $option  = $(this),
1806
                    name     = $option.html(),
1807
                    disabled = $option.attr('disabled'),
1808
                    value    = ( $option.attr('value') !== undefined )
1809
                      ? $option.attr('value')
1810
                      : name
1811
                  ;
1812
                  if(settings.placeholder === 'auto' && value === '') {
1813
                    select.placeholder = name;
1814
                  }
1815
                  else {
1816
                    select.values.push({
1817
                      name     : name,
1818
                      value    : value,
1819
                      disabled : disabled
1820
                    });
1821
                  }
1822
                })
1823
            ;
1824
            if(settings.placeholder && settings.placeholder !== 'auto') {
1825
              module.debug('Setting placeholder value to', settings.placeholder);
1826
              select.placeholder = settings.placeholder;
1827
            }
1828
            if(settings.sortSelect) {
1829
              select.values.sort(function(a, b) {
1830
                return (a.name > b.name)
1831
                  ? 1
1832
                  : -1
1833
                ;
1834
              });
1835
              module.debug('Retrieved and sorted values from select', select);
1836
            }
1837
            else {
1838
              module.debug('Retrieved values from select', select);
1839
            }
1840
            return select;
1841
          },
1842
          activeItem: function() {
1843
            return $item.filter('.'  + className.active);
1844
          },
1845
          selectedItem: function() {
1846
            var
1847
              $selectedItem = $item.not(selector.unselectable).filter('.'  + className.selected)
1848
            ;
1849
            return ($selectedItem.length > 0)
1850
              ? $selectedItem
1851
              : $item.eq(0)
1852
            ;
1853
          },
1854
          itemWithAdditions: function(value) {
1855
            var
1856
              $items       = module.get.item(value),
1857
              $userItems   = module.create.userChoice(value),
1858
              hasUserItems = ($userItems && $userItems.length > 0)
1859
            ;
1860
            if(hasUserItems) {
1861
              $items = ($items.length > 0)
1862
                ? $items.add($userItems)
1863
                : $userItems
1864
              ;
1865
            }
1866
            return $items;
1867
          },
1868
          item: function(value, strict) {
1869
            var
1870
              $selectedItem = false,
1871
              shouldSearch,
1872
              isMultiple
1873
            ;
1874
            value = (value !== undefined)
1875
              ? value
1876
              : ( module.get.values() !== undefined)
1877
                ? module.get.values()
1878
                : module.get.text()
1879
            ;
1880
            shouldSearch = (isMultiple)
1881
              ? (value.length > 0)
1882
              : (value !== undefined && value !== null)
1883
            ;
1884
            isMultiple = (module.is.multiple() && $.isArray(value));
1885
            strict     = (value === '' || value === 0)
1886
              ? true
1887
              : strict || false
1888
            ;
1889
            if(shouldSearch) {
1890
              $item
1891
                .each(function() {
1892
                  var
1893
                    $choice       = $(this),
1894
                    optionText    = module.get.choiceText($choice),
1895
                    optionValue   = module.get.choiceValue($choice, optionText)
1896
                  ;
1897
                  // safe early exit
1898
                  if(optionValue === null || optionValue === undefined) {
1899
                    return;
1900
                  }
1901
                  if(isMultiple) {
1902
                    if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) {
1903
                      $selectedItem = ($selectedItem)
1904
                        ? $selectedItem.add($choice)
1905
                        : $choice
1906
                      ;
1907
                    }
1908
                  }
1909
                  else if(strict) {
1910
                    module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
1911
                    if( optionValue === value || optionText === value) {
1912
                      $selectedItem = $choice;
1913
                      return true;
1914
                    }
1915
                  }
1916
                  else {
1917
                    if( String(optionValue) == String(value) || optionText == value) {
1918
                      module.verbose('Found select item by value', optionValue, value);
1919
                      $selectedItem = $choice;
1920
                      return true;
1921
                    }
1922
                  }
1923
                })
1924
              ;
1925
            }
1926
            return $selectedItem;
1927
          }
1928
        },
1929
1930
        check: {
1931
          maxSelections: function(selectionCount) {
1932
            if(settings.maxSelections) {
1933
              selectionCount = (selectionCount !== undefined)
1934
                ? selectionCount
1935
                : module.get.selectionCount()
1936
              ;
1937
              if(selectionCount >= settings.maxSelections) {
1938
                module.debug('Maximum selection count reached');
1939
                if(settings.useLabels) {
1940
                  $item.addClass(className.filtered);
1941
                  module.add.message(message.maxSelections);
1942
                }
1943
                return true;
1944
              }
1945
              else {
1946
                module.verbose('No longer at maximum selection count');
1947
                module.remove.message();
1948
                module.remove.filteredItem();
1949
                if(module.is.searchSelection()) {
1950
                  module.filterItems();
1951
                }
1952
                return false;
1953
              }
1954
            }
1955
            return true;
1956
          }
1957
        },
1958
1959
        restore: {
1960
          defaults: function() {
1961
            module.clear();
1962
            module.restore.defaultText();
1963
            module.restore.defaultValue();
1964
          },
1965
          defaultText: function() {
1966
            var
1967
              defaultText     = module.get.defaultText(),
1968
              placeholderText = module.get.placeholderText
1969
            ;
1970
            if(defaultText === placeholderText) {
1971
              module.debug('Restoring default placeholder text', defaultText);
1972
              module.set.placeholderText(defaultText);
1973
            }
1974
            else {
1975
              module.debug('Restoring default text', defaultText);
1976
              module.set.text(defaultText);
1977
            }
1978
          },
1979
          placeholderText: function() {
1980
            module.set.placeholderText();
1981
          },
1982
          defaultValue: function() {
1983
            var
1984
              defaultValue = module.get.defaultValue()
1985
            ;
1986
            if(defaultValue !== undefined) {
1987
              module.debug('Restoring default value', defaultValue);
1988
              if(defaultValue !== '') {
1989
                module.set.value(defaultValue);
1990
                module.set.selected();
1991
              }
1992
              else {
1993
                module.remove.activeItem();
1994
                module.remove.selectedItem();
1995
              }
1996
            }
1997
          },
1998
          labels: function() {
1999
            if(settings.allowAdditions) {
2000
              if(!settings.useLabels) {
2001
                module.error(error.labels);
2002
                settings.useLabels = true;
2003
              }
2004
              module.debug('Restoring selected values');
2005
              module.create.userLabels();
2006
            }
2007
            module.check.maxSelections();
2008
          },
2009
          selected: function() {
2010
            module.restore.values();
2011
            if(module.is.multiple()) {
2012
              module.debug('Restoring previously selected values and labels');
2013
              module.restore.labels();
2014
            }
2015
            else {
2016
              module.debug('Restoring previously selected values');
2017
            }
2018
          },
2019
          values: function() {
2020
            // prevents callbacks from occurring on initial load
2021
            module.set.initialLoad();
2022
            if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
2023
              module.restore.remoteValues();
2024
            }
2025
            else {
2026
              module.set.selected();
2027
            }
2028
            module.remove.initialLoad();
2029
          },
2030
          remoteValues: function() {
2031
            var
2032
              values = module.get.remoteValues()
2033
            ;
2034
            module.debug('Recreating selected from session data', values);
2035
            if(values) {
2036
              if( module.is.single() ) {
2037
                $.each(values, function(value, name) {
2038
                  module.set.text(name);
2039
                });
2040
              }
2041
              else {
2042
                $.each(values, function(value, name) {
2043
                  module.add.label(value, name);
2044
                });
2045
              }
2046
            }
2047
          }
2048
        },
2049
2050
        read: {
2051
          remoteData: function(value) {
2052
            var
2053
              name
2054
            ;
2055
            if(window.Storage === undefined) {
2056
              module.error(error.noStorage);
2057
              return;
2058
            }
2059
            name = sessionStorage.getItem(value);
2060
            return (name !== undefined)
2061
              ? name
2062
              : false
2063
            ;
2064
          }
2065
        },
2066
2067
        save: {
2068
          defaults: function() {
2069
            module.save.defaultText();
2070
            module.save.placeholderText();
2071
            module.save.defaultValue();
2072
          },
2073
          defaultValue: function() {
2074
            var
2075
              value = module.get.value()
2076
            ;
2077
            module.verbose('Saving default value as', value);
2078
            $module.data(metadata.defaultValue, value);
2079
          },
2080
          defaultText: function() {
2081
            var
2082
              text = module.get.text()
2083
            ;
2084
            module.verbose('Saving default text as', text);
2085
            $module.data(metadata.defaultText, text);
2086
          },
2087
          placeholderText: function() {
2088
            var
2089
              text
2090
            ;
2091
            if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
2092
              text = module.get.text();
2093
              module.verbose('Saving placeholder text as', text);
2094
              $module.data(metadata.placeholderText, text);
2095
            }
2096
          },
2097
          remoteData: function(name, value) {
2098
            if(window.Storage === undefined) {
2099
              module.error(error.noStorage);
2100
              return;
2101
            }
2102
            module.verbose('Saving remote data to session storage', value, name);
2103
            sessionStorage.setItem(value, name);
2104
          }
2105
        },
2106
2107
        clear: function() {
2108
          if(module.is.multiple() && settings.useLabels) {
2109
            module.remove.labels();
2110
          }
2111
          else {
2112
            module.remove.activeItem();
2113
            module.remove.selectedItem();
2114
          }
2115
          module.set.placeholderText();
2116
          module.clearValue();
2117
        },
2118
2119
        clearValue: function() {
2120
          module.set.value('');
2121
        },
2122
2123
        scrollPage: function(direction, $selectedItem) {
2124
          var
2125
            $currentItem  = $selectedItem || module.get.selectedItem(),
2126
            $menu         = $currentItem.closest(selector.menu),
2127
            menuHeight    = $menu.outerHeight(),
2128
            currentScroll = $menu.scrollTop(),
2129
            itemHeight    = $item.eq(0).outerHeight(),
2130
            itemsPerPage  = Math.floor(menuHeight / itemHeight),
2131
            maxScroll     = $menu.prop('scrollHeight'),
2132
            newScroll     = (direction == 'up')
2133
              ? currentScroll - (itemHeight * itemsPerPage)
2134
              : currentScroll + (itemHeight * itemsPerPage),
2135
            $selectableItem = $item.not(selector.unselectable),
2136
            isWithinRange,
2137
            $nextSelectedItem,
2138
            elementIndex
2139
          ;
2140
          elementIndex      = (direction == 'up')
2141
            ? $selectableItem.index($currentItem) - itemsPerPage
2142
            : $selectableItem.index($currentItem) + itemsPerPage
2143
          ;
2144
          isWithinRange = (direction == 'up')
2145
            ? (elementIndex >= 0)
2146
            : (elementIndex < $selectableItem.length)
2147
          ;
2148
          $nextSelectedItem = (isWithinRange)
2149
            ? $selectableItem.eq(elementIndex)
2150
            : (direction == 'up')
2151
              ? $selectableItem.first()
2152
              : $selectableItem.last()
2153
          ;
2154
          if($nextSelectedItem.length > 0) {
2155
            module.debug('Scrolling page', direction, $nextSelectedItem);
2156
            $currentItem
2157
              .removeClass(className.selected)
2158
            ;
2159
            $nextSelectedItem
2160
              .addClass(className.selected)
2161
            ;
2162
            if(settings.selectOnKeydown && module.is.single()) {
2163
              module.set.selectedItem($nextSelectedItem);
2164
            }
2165
            $menu
2166
              .scrollTop(newScroll)
2167
            ;
2168
          }
2169
        },
2170
2171
        set: {
2172
          filtered: function() {
2173
            var
2174
              isMultiple       = module.is.multiple(),
2175
              isSearch         = module.is.searchSelection(),
2176
              isSearchMultiple = (isMultiple && isSearch),
2177
              searchValue      = (isSearch)
2178
                ? module.get.query()
2179
                : '',
2180
              hasSearchValue   = (typeof searchValue === 'string' && searchValue.length > 0),
2181
              searchWidth      = module.get.searchWidth(),
2182
              valueIsSet       = searchValue !== ''
2183
            ;
2184
            if(isMultiple && hasSearchValue) {
2185
              module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
2186
              $search.css('width', searchWidth);
2187
            }
2188
            if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
2189
              module.verbose('Hiding placeholder text');
2190
              $text.addClass(className.filtered);
2191
            }
2192
            else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
2193
              module.verbose('Showing placeholder text');
2194
              $text.removeClass(className.filtered);
2195
            }
2196
          },
2197
          empty: function() {
2198
            $module.addClass(className.empty);
2199
          },
2200
          loading: function() {
2201
            $module.addClass(className.loading);
2202
          },
2203
          placeholderText: function(text) {
2204
            text = text || module.get.placeholderText();
2205
            module.debug('Setting placeholder text', text);
2206
            module.set.text(text);
2207
            $text.addClass(className.placeholder);
2208
          },
2209
          tabbable: function() {
2210
            if( module.is.searchSelection() ) {
2211
              module.debug('Added tabindex to searchable dropdown');
2212
              $search
2213
                .val('')
2214
                .attr('tabindex', 0)
2215
              ;
2216
              $menu
2217
                .attr('tabindex', -1)
2218
              ;
2219
            }
2220
            else {
2221
              module.debug('Added tabindex to dropdown');
2222
              if( $module.attr('tabindex') === undefined) {
2223
                $module
2224
                  .attr('tabindex', 0)
2225
                ;
2226
                $menu
2227
                  .attr('tabindex', -1)
2228
                ;
2229
              }
2230
            }
2231
          },
2232
          initialLoad: function() {
2233
            module.verbose('Setting initial load');
2234
            initialLoad = true;
2235
          },
2236
          activeItem: function($item) {
2237
            if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
2238
              $item.addClass(className.filtered);
2239
            }
2240
            else {
2241
              $item.addClass(className.active);
2242
            }
2243
          },
2244
          partialSearch: function(text) {
2245
            var
2246
              length = module.get.query().length
2247
            ;
2248
            $search.val( text.substr(0 , length));
2249
          },
2250
          scrollPosition: function($item, forceScroll) {
2251
            var
2252
              edgeTolerance = 5,
2253
              $menu,
2254
              hasActive,
2255
              offset,
2256
              itemHeight,
2257
              itemOffset,
2258
              menuOffset,
2259
              menuScroll,
2260
              menuHeight,
2261
              abovePage,
2262
              belowPage
2263
            ;
2264
2265
            $item       = $item || module.get.selectedItem();
2266
            $menu       = $item.closest(selector.menu);
2267
            hasActive   = ($item && $item.length > 0);
2268
            forceScroll = (forceScroll !== undefined)
2269
              ? forceScroll
2270
              : false
2271
            ;
2272
            if($item && $menu.length > 0 && hasActive) {
2273
              itemOffset = $item.position().top;
2274
2275
              $menu.addClass(className.loading);
2276
              menuScroll = $menu.scrollTop();
2277
              menuOffset = $menu.offset().top;
2278
              itemOffset = $item.offset().top;
2279
              offset     = menuScroll - menuOffset + itemOffset;
2280
              if(!forceScroll) {
2281
                menuHeight = $menu.height();
2282
                belowPage  = menuScroll + menuHeight < (offset + edgeTolerance);
2283
                abovePage  = ((offset - edgeTolerance) < menuScroll);
2284
              }
2285
              module.debug('Scrolling to active item', offset);
2286
              if(forceScroll || abovePage || belowPage) {
2287
                $menu.scrollTop(offset);
2288
              }
2289
              $menu.removeClass(className.loading);
2290
            }
2291
          },
2292
          text: function(text) {
2293
            if(settings.action !== 'select') {
2294
              if(settings.action == 'combo') {
2295
                module.debug('Changing combo button text', text, $combo);
2296
                if(settings.preserveHTML) {
2297
                  $combo.html(text);
2298
                }
2299
                else {
2300
                  $combo.text(text);
2301
                }
2302
              }
2303
              else {
2304
                if(text !== module.get.placeholderText()) {
2305
                  $text.removeClass(className.placeholder);
2306
                }
2307
                module.debug('Changing text', text, $text);
2308
                $text
2309
                  .removeClass(className.filtered)
2310
                ;
2311
                if(settings.preserveHTML) {
2312
                  $text.html(text);
2313
                }
2314
                else {
2315
                  $text.text(text);
2316
                }
2317
              }
2318
            }
2319
          },
2320
          selectedItem: function($item) {
2321
            var
2322
              value      = module.get.choiceValue($item),
2323
              searchText = module.get.choiceText($item, false),
2324
              text       = module.get.choiceText($item, true)
2325
            ;
2326
            module.debug('Setting user selection to item', $item);
2327
            module.remove.activeItem();
2328
            module.set.partialSearch(searchText);
2329
            module.set.activeItem($item);
2330
            module.set.selected(value, $item);
2331
            module.set.text(text);
2332
          },
2333
          selectedLetter: function(letter) {
2334
            var
2335
              $selectedItem         = $item.filter('.' + className.selected),
2336
              alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
2337
              $nextValue            = false,
2338
              $nextItem
2339
            ;
2340
            // check next of same letter
2341
            if(alreadySelectedLetter) {
2342
              $nextItem = $selectedItem.nextAll($item).eq(0);
2343
              if( module.has.firstLetter($nextItem, letter) ) {
2344
                $nextValue  = $nextItem;
2345
              }
2346
            }
2347
            // check all values
2348
            if(!$nextValue) {
2349
              $item
2350
                .each(function(){
2351
                  if(module.has.firstLetter($(this), letter)) {
2352
                    $nextValue = $(this);
2353
                    return false;
2354
                  }
2355
                })
2356
              ;
2357
            }
2358
            // set next value
2359
            if($nextValue) {
2360
              module.verbose('Scrolling to next value with letter', letter);
2361
              module.set.scrollPosition($nextValue);
2362
              $selectedItem.removeClass(className.selected);
2363
              $nextValue.addClass(className.selected);
2364
              if(settings.selectOnKeydown && module.is.single()) {
2365
                module.set.selectedItem($nextValue);
2366
              }
2367
            }
2368
          },
2369
          direction: function($menu) {
2370
            if(settings.direction == 'auto') {
2371
              // reset position
2372
              module.remove.upward();
2373
2374
              if(module.can.openDownward($menu)) {
2375
                module.remove.upward($menu);
2376
              }
2377
              else {
2378
                module.set.upward($menu);
2379
              }
2380
              if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
2381
                module.set.leftward($menu);
2382
              }
2383
            }
2384
            else if(settings.direction == 'upward') {
2385
              module.set.upward($menu);
2386
            }
2387
          },
2388
          upward: function($currentMenu) {
2389
            var $element = $currentMenu || $module;
2390
            $element.addClass(className.upward);
2391
          },
2392
          leftward: function($currentMenu) {
2393
            var $element = $currentMenu || $menu;
2394
            $element.addClass(className.leftward);
2395
          },
2396
          value: function(value, text, $selected) {
2397
            var
2398
              escapedValue = module.escape.value(value),
2399
              hasInput     = ($input.length > 0),
2400
              isAddition   = !module.has.value(value),
2401
              currentValue = module.get.values(),
2402
              stringValue  = (value !== undefined)
2403
                ? String(value)
2404
                : value,
2405
              newValue
2406
            ;
2407
            if(hasInput) {
2408
              if(!settings.allowReselection && stringValue == currentValue) {
2409
                module.verbose('Skipping value update already same value', value, currentValue);
2410
                if(!module.is.initialLoad()) {
2411
                  return;
2412
                }
2413
              }
2414
2415
              if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
2416
                module.debug('Adding user option', value);
2417
                module.add.optionValue(value);
2418
              }
2419
              module.debug('Updating input value', escapedValue, currentValue);
2420
              internalChange = true;
2421
              $input
2422
                .val(escapedValue)
2423
              ;
2424
              if(settings.fireOnInit === false && module.is.initialLoad()) {
2425
                module.debug('Input native change event ignored on initial load');
2426
              }
2427
              else {
2428
                module.trigger.change();
2429
              }
2430
              internalChange = false;
2431
            }
2432
            else {
2433
              module.verbose('Storing value in metadata', escapedValue, $input);
2434
              if(escapedValue !== currentValue) {
2435
                $module.data(metadata.value, stringValue);
2436
              }
2437
            }
2438
            if(settings.fireOnInit === false && module.is.initialLoad()) {
2439
              module.verbose('No callback on initial load', settings.onChange);
2440
            }
2441
            else {
2442
              settings.onChange.call(element, value, text, $selected);
2443
            }
2444
          },
2445
          active: function() {
2446
            $module
2447
              .addClass(className.active)
2448
            ;
2449
          },
2450
          multiple: function() {
2451
            $module.addClass(className.multiple);
2452
          },
2453
          visible: function() {
2454
            $module.addClass(className.visible);
2455
          },
2456
          exactly: function(value, $selectedItem) {
2457
            module.debug('Setting selected to exact values');
2458
            module.clear();
2459
            module.set.selected(value, $selectedItem);
2460
          },
2461
          selected: function(value, $selectedItem) {
2462
            var
2463
              isMultiple = module.is.multiple(),
2464
              $userSelectedItem
2465
            ;
2466
            $selectedItem = (settings.allowAdditions)
2467
              ? $selectedItem || module.get.itemWithAdditions(value)
2468
              : $selectedItem || module.get.item(value)
2469
            ;
2470
            if(!$selectedItem) {
2471
              return;
2472
            }
2473
            module.debug('Setting selected menu item to', $selectedItem);
2474
            if(module.is.multiple()) {
2475
              module.remove.searchWidth();
2476
            }
2477
            if(module.is.single()) {
2478
              module.remove.activeItem();
2479
              module.remove.selectedItem();
2480
            }
2481
            else if(settings.useLabels) {
2482
              module.remove.selectedItem();
2483
            }
2484
            // select each item
2485
            $selectedItem
2486
              .each(function() {
2487
                var
2488
                  $selected      = $(this),
2489
                  selectedText   = module.get.choiceText($selected),
2490
                  selectedValue  = module.get.choiceValue($selected, selectedText),
2491
2492
                  isFiltered     = $selected.hasClass(className.filtered),
2493
                  isActive       = $selected.hasClass(className.active),
2494
                  isUserValue    = $selected.hasClass(className.addition),
2495
                  shouldAnimate  = (isMultiple && $selectedItem.length == 1)
2496
                ;
2497
                if(isMultiple) {
2498
                  if(!isActive || isUserValue) {
2499
                    if(settings.apiSettings && settings.saveRemoteData) {
2500
                      module.save.remoteData(selectedText, selectedValue);
2501
                    }
2502
                    if(settings.useLabels) {
2503
                      module.add.value(selectedValue, selectedText, $selected);
2504
                      module.add.label(selectedValue, selectedText, shouldAnimate);
2505
                      module.set.activeItem($selected);
2506
                      module.filterActive();
2507
                      module.select.nextAvailable($selectedItem);
2508
                    }
2509
                    else {
2510
                      module.add.value(selectedValue, selectedText, $selected);
2511
                      module.set.text(module.add.variables(message.count));
2512
                      module.set.activeItem($selected);
2513
                    }
2514
                  }
2515
                  else if(!isFiltered) {
2516
                    module.debug('Selected active value, removing label');
2517
                    module.remove.selected(selectedValue);
2518
                  }
2519
                }
2520
                else {
2521
                  if(settings.apiSettings && settings.saveRemoteData) {
2522
                    module.save.remoteData(selectedText, selectedValue);
2523
                  }
2524
                  module.set.text(selectedText);
2525
                  module.set.value(selectedValue, selectedText, $selected);
2526
                  $selected
2527
                    .addClass(className.active)
2528
                    .addClass(className.selected)
2529
                  ;
2530
                }
2531
              })
2532
            ;
2533
          }
2534
        },
2535
2536
        add: {
2537
          label: function(value, text, shouldAnimate) {
2538
            var
2539
              $next  = module.is.searchSelection()
2540
                ? $search
2541
                : $text,
2542
              escapedValue = module.escape.value(value),
2543
              $label
2544
            ;
2545
            $label =  $('<a />')
2546
              .addClass(className.label)
2547
              .attr('data-' + metadata.value, escapedValue)
2548
              .html(templates.label(escapedValue, text))
2549
            ;
2550
            $label = settings.onLabelCreate.call($label, escapedValue, text);
2551
2552
            if(module.has.label(value)) {
2553
              module.debug('Label already exists, skipping', escapedValue);
2554
              return;
2555
            }
2556
            if(settings.label.variation) {
2557
              $label.addClass(settings.label.variation);
2558
            }
2559
            if(shouldAnimate === true) {
2560
              module.debug('Animating in label', $label);
2561
              $label
2562
                .addClass(className.hidden)
2563
                .insertBefore($next)
2564
                .transition(settings.label.transition, settings.label.duration)
2565
              ;
2566
            }
2567
            else {
2568
              module.debug('Adding selection label', $label);
2569
              $label
2570
                .insertBefore($next)
2571
              ;
2572
            }
2573
          },
2574
          message: function(message) {
2575
            var
2576
              $message = $menu.children(selector.message),
2577
              html     = settings.templates.message(module.add.variables(message))
2578
            ;
2579
            if($message.length > 0) {
2580
              $message
2581
                .html(html)
2582
              ;
2583
            }
2584
            else {
2585
              $message = $('<div/>')
2586
                .html(html)
2587
                .addClass(className.message)
2588
                .appendTo($menu)
2589
              ;
2590
            }
2591
          },
2592
          optionValue: function(value) {
2593
            var
2594
              escapedValue = module.escape.value(value),
2595
              $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
2596
              hasOption    = ($option.length > 0)
2597
            ;
2598
            if(hasOption) {
2599
              return;
2600
            }
2601
            // temporarily disconnect observer
2602
            module.disconnect.selectObserver();
2603
            if( module.is.single() ) {
2604
              module.verbose('Removing previous user addition');
2605
              $input.find('option.' + className.addition).remove();
2606
            }
2607
            $('<option/>')
2608
              .prop('value', escapedValue)
2609
              .addClass(className.addition)
2610
              .html(value)
2611
              .appendTo($input)
2612
            ;
2613
            module.verbose('Adding user addition as an <option>', value);
2614
            module.observe.select();
2615
          },
2616
          userSuggestion: function(value) {
2617
            var
2618
              $addition         = $menu.children(selector.addition),
2619
              $existingItem     = module.get.item(value),
2620
              alreadyHasValue   = $existingItem && $existingItem.not(selector.addition).length,
2621
              hasUserSuggestion = $addition.length > 0,
2622
              html
2623
            ;
2624
            if(settings.useLabels && module.has.maxSelections()) {
2625
              return;
2626
            }
2627
            if(value === '' || alreadyHasValue) {
2628
              $addition.remove();
2629
              return;
2630
            }
2631
            if(hasUserSuggestion) {
2632
              $addition
2633
                .data(metadata.value, value)
2634
                .data(metadata.text, value)
2635
                .attr('data-' + metadata.value, value)
2636
                .attr('data-' + metadata.text, value)
2637
                .removeClass(className.filtered)
2638
              ;
2639
              if(!settings.hideAdditions) {
2640
                html = settings.templates.addition( module.add.variables(message.addResult, value) );
2641
                $addition
2642
                  .html(html)
2643
                ;
2644
              }
2645
              module.verbose('Replacing user suggestion with new value', $addition);
2646
            }
2647
            else {
2648
              $addition = module.create.userChoice(value);
2649
              $addition
2650
                .prependTo($menu)
2651
              ;
2652
              module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
2653
            }
2654
            if(!settings.hideAdditions || module.is.allFiltered()) {
2655
              $addition
2656
                .addClass(className.selected)
2657
                .siblings()
2658
                .removeClass(className.selected)
2659
              ;
2660
            }
2661
            module.refreshItems();
2662
          },
2663
          variables: function(message, term) {
2664
            var
2665
              hasCount    = (message.search('{count}') !== -1),
2666
              hasMaxCount = (message.search('{maxCount}') !== -1),
2667
              hasTerm     = (message.search('{term}') !== -1),
2668
              values,
2669
              count,
2670
              query
2671
            ;
2672
            module.verbose('Adding templated variables to message', message);
2673
            if(hasCount) {
2674
              count  = module.get.selectionCount();
2675
              message = message.replace('{count}', count);
2676
            }
2677
            if(hasMaxCount) {
2678
              count  = module.get.selectionCount();
2679
              message = message.replace('{maxCount}', settings.maxSelections);
2680
            }
2681
            if(hasTerm) {
2682
              query   = term || module.get.query();
2683
              message = message.replace('{term}', query);
2684
            }
2685
            return message;
2686
          },
2687
          value: function(addedValue, addedText, $selectedItem) {
2688
            var
2689
              currentValue = module.get.values(),
2690
              newValue
2691
            ;
2692
            if(addedValue === '') {
2693
              module.debug('Cannot select blank values from multiselect');
2694
              return;
2695
            }
2696
            // extend current array
2697
            if($.isArray(currentValue)) {
2698
              newValue = currentValue.concat([addedValue]);
2699
              newValue = module.get.uniqueArray(newValue);
2700
            }
2701
            else {
2702
              newValue = [addedValue];
2703
            }
2704
            // add values
2705
            if( module.has.selectInput() ) {
2706
              if(module.can.extendSelect()) {
2707
                module.debug('Adding value to select', addedValue, newValue, $input);
2708
                module.add.optionValue(addedValue);
2709
              }
2710
            }
2711
            else {
2712
              newValue = newValue.join(settings.delimiter);
2713
              module.debug('Setting hidden input to delimited value', newValue, $input);
2714
            }
2715
2716
            if(settings.fireOnInit === false && module.is.initialLoad()) {
2717
              module.verbose('Skipping onadd callback on initial load', settings.onAdd);
2718
            }
2719
            else {
2720
              settings.onAdd.call(element, addedValue, addedText, $selectedItem);
2721
            }
2722
            module.set.value(newValue, addedValue, addedText, $selectedItem);
2723
            module.check.maxSelections();
2724
          }
2725
        },
2726
2727
        remove: {
2728
          active: function() {
2729
            $module.removeClass(className.active);
2730
          },
2731
          activeLabel: function() {
2732
            $module.find(selector.label).removeClass(className.active);
2733
          },
2734
          empty: function() {
2735
            $module.removeClass(className.empty);
2736
          },
2737
          loading: function() {
2738
            $module.removeClass(className.loading);
2739
          },
2740
          initialLoad: function() {
2741
            initialLoad = false;
2742
          },
2743
          upward: function($currentMenu) {
2744
            var $element = $currentMenu || $module;
2745
            $element.removeClass(className.upward);
2746
          },
2747
          leftward: function($currentMenu) {
2748
            var $element = $currentMenu || $menu;
2749
            $element.removeClass(className.leftward);
2750
          },
2751
          visible: function() {
2752
            $module.removeClass(className.visible);
2753
          },
2754
          activeItem: function() {
2755
            $item.removeClass(className.active);
2756
          },
2757
          filteredItem: function() {
2758
            if(settings.useLabels && module.has.maxSelections() ) {
2759
              return;
2760
            }
2761
            if(settings.useLabels && module.is.multiple()) {
2762
              $item.not('.' + className.active).removeClass(className.filtered);
2763
            }
2764
            else {
2765
              $item.removeClass(className.filtered);
2766
            }
2767
            module.remove.empty();
2768
          },
2769
          optionValue: function(value) {
2770
            var
2771
              escapedValue = module.escape.value(value),
2772
              $option      = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
2773
              hasOption    = ($option.length > 0)
2774
            ;
2775
            if(!hasOption || !$option.hasClass(className.addition)) {
2776
              return;
2777
            }
2778
            // temporarily disconnect observer
2779
            if(selectObserver) {
2780
              selectObserver.disconnect();
2781
              module.verbose('Temporarily disconnecting mutation observer');
2782
            }
2783
            $option.remove();
2784
            module.verbose('Removing user addition as an <option>', escapedValue);
2785
            if(selectObserver) {
2786
              selectObserver.observe($input[0], {
2787
                childList : true,
2788
                subtree   : true
2789
              });
2790
            }
2791
          },
2792
          message: function() {
2793
            $menu.children(selector.message).remove();
2794
          },
2795
          searchWidth: function() {
2796
            $search.css('width', '');
2797
          },
2798
          searchTerm: function() {
2799
            module.verbose('Cleared search term');
2800
            $search.val('');
2801
            module.set.filtered();
2802
          },
2803
          userAddition: function() {
2804
            $item.filter(selector.addition).remove();
2805
          },
2806
          selected: function(value, $selectedItem) {
2807
            $selectedItem = (settings.allowAdditions)
2808
              ? $selectedItem || module.get.itemWithAdditions(value)
2809
              : $selectedItem || module.get.item(value)
2810
            ;
2811
2812
            if(!$selectedItem) {
2813
              return false;
2814
            }
2815
2816
            $selectedItem
2817
              .each(function() {
2818
                var
2819
                  $selected     = $(this),
2820
                  selectedText  = module.get.choiceText($selected),
2821
                  selectedValue = module.get.choiceValue($selected, selectedText)
2822
                ;
2823
                if(module.is.multiple()) {
2824
                  if(settings.useLabels) {
2825
                    module.remove.value(selectedValue, selectedText, $selected);
2826
                    module.remove.label(selectedValue);
2827
                  }
2828
                  else {
2829
                    module.remove.value(selectedValue, selectedText, $selected);
2830
                    if(module.get.selectionCount() === 0) {
2831
                      module.set.placeholderText();
2832
                    }
2833
                    else {
2834
                      module.set.text(module.add.variables(message.count));
2835
                    }
2836
                  }
2837
                }
2838
                else {
2839
                  module.remove.value(selectedValue, selectedText, $selected);
2840
                }
2841
                $selected
2842
                  .removeClass(className.filtered)
2843
                  .removeClass(className.active)
2844
                ;
2845
                if(settings.useLabels) {
2846
                  $selected.removeClass(className.selected);
2847
                }
2848
              })
2849
            ;
2850
          },
2851
          selectedItem: function() {
2852
            $item.removeClass(className.selected);
2853
          },
2854
          value: function(removedValue, removedText, $removedItem) {
2855
            var
2856
              values = module.get.values(),
2857
              newValue
2858
            ;
2859
            if( module.has.selectInput() ) {
2860
              module.verbose('Input is <select> removing selected option', removedValue);
2861
              newValue = module.remove.arrayValue(removedValue, values);
2862
              module.remove.optionValue(removedValue);
2863
            }
2864
            else {
2865
              module.verbose('Removing from delimited values', removedValue);
2866
              newValue = module.remove.arrayValue(removedValue, values);
2867
              newValue = newValue.join(settings.delimiter);
2868
            }
2869
            if(settings.fireOnInit === false && module.is.initialLoad()) {
2870
              module.verbose('No callback on initial load', settings.onRemove);
2871
            }
2872
            else {
2873
              settings.onRemove.call(element, removedValue, removedText, $removedItem);
2874
            }
2875
            module.set.value(newValue, removedText, $removedItem);
2876
            module.check.maxSelections();
2877
          },
2878
          arrayValue: function(removedValue, values) {
2879
            if( !$.isArray(values) ) {
2880
              values = [values];
2881
            }
2882
            values = $.grep(values, function(value){
2883
              return (removedValue != value);
2884
            });
2885
            module.verbose('Removed value from delimited string', removedValue, values);
2886
            return values;
2887
          },
2888
          label: function(value, shouldAnimate) {
2889
            var
2890
              $labels       = $module.find(selector.label),
2891
              $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]')
2892
            ;
2893
            module.verbose('Removing label', $removedLabel);
2894
            $removedLabel.remove();
2895
          },
2896
          activeLabels: function($activeLabels) {
2897
            $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
2898
            module.verbose('Removing active label selections', $activeLabels);
2899
            module.remove.labels($activeLabels);
2900
          },
2901
          labels: function($labels) {
2902
            $labels = $labels || $module.find(selector.label);
2903
            module.verbose('Removing labels', $labels);
2904
            $labels
2905
              .each(function(){
2906
                var
2907
                  $label      = $(this),
2908
                  value       = $label.data(metadata.value),
2909
                  stringValue = (value !== undefined)
2910
                    ? String(value)
2911
                    : value,
2912
                  isUserValue = module.is.userValue(stringValue)
2913
                ;
2914
                if(settings.onLabelRemove.call($label, value) === false) {
2915
                  module.debug('Label remove callback cancelled removal');
2916
                  return;
2917
                }
2918
                module.remove.message();
2919
                if(isUserValue) {
2920
                  module.remove.value(stringValue);
2921
                  module.remove.label(stringValue);
2922
                }
2923
                else {
2924
                  // selected will also remove label
2925
                  module.remove.selected(stringValue);
2926
                }
2927
              })
2928
            ;
2929
          },
2930
          tabbable: function() {
2931
            if( module.is.searchSelection() ) {
2932
              module.debug('Searchable dropdown initialized');
2933
              $search
2934
                .removeAttr('tabindex')
2935
              ;
2936
              $menu
2937
                .removeAttr('tabindex')
2938
              ;
2939
            }
2940
            else {
2941
              module.debug('Simple selection dropdown initialized');
2942
              $module
2943
                .removeAttr('tabindex')
2944
              ;
2945
              $menu
2946
                .removeAttr('tabindex')
2947
              ;
2948
            }
2949
          }
2950
        },
2951
2952
        has: {
2953
          menuSearch: function() {
2954
            return (module.has.search() && $search.closest($menu).length > 0);
2955
          },
2956
          search: function() {
2957
            return ($search.length > 0);
2958
          },
2959
          sizer: function() {
2960
            return ($sizer.length > 0);
2961
          },
2962
          selectInput: function() {
2963
            return ( $input.is('select') );
2964
          },
2965
          minCharacters: function(searchTerm) {
2966
            if(settings.minCharacters) {
2967
              searchTerm = (searchTerm !== undefined)
2968
                ? String(searchTerm)
2969
                : String(module.get.query())
2970
              ;
2971
              return (searchTerm.length >= settings.minCharacters);
2972
            }
2973
            return true;
2974
          },
2975
          firstLetter: function($item, letter) {
2976
            var
2977
              text,
2978
              firstLetter
2979
            ;
2980
            if(!$item || $item.length === 0 || typeof letter !== 'string') {
2981
              return false;
2982
            }
2983
            text        = module.get.choiceText($item, false);
2984
            letter      = letter.toLowerCase();
2985
            firstLetter = String(text).charAt(0).toLowerCase();
2986
            return (letter == firstLetter);
2987
          },
2988
          input: function() {
2989
            return ($input.length > 0);
2990
          },
2991
          items: function() {
2992
            return ($item.length > 0);
2993
          },
2994
          menu: function() {
2995
            return ($menu.length > 0);
2996
          },
2997
          message: function() {
2998
            return ($menu.children(selector.message).length !== 0);
2999
          },
3000
          label: function(value) {
3001
            var
3002
              escapedValue = module.escape.value(value),
3003
              $labels      = $module.find(selector.label)
3004
            ;
3005
            return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
3006
          },
3007
          maxSelections: function() {
3008
            return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
3009
          },
3010
          allResultsFiltered: function() {
3011
            var
3012
              $normalResults = $item.not(selector.addition)
3013
            ;
3014
            return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
3015
          },
3016
          userSuggestion: function() {
3017
            return ($menu.children(selector.addition).length > 0);
3018
          },
3019
          query: function() {
3020
            return (module.get.query() !== '');
3021
          },
3022
          value: function(value) {
3023
            var
3024
              values   = module.get.values(),
3025
              hasValue = $.isArray(values)
3026
               ? values && ($.inArray(value, values) !== -1)
3027
               : (values == value)
3028
            ;
3029
            return (hasValue)
3030
              ? true
3031
              : false
3032
            ;
3033
          }
3034
        },
3035
3036
        is: {
3037
          active: function() {
3038
            return $module.hasClass(className.active);
3039
          },
3040
          bubbledLabelClick: function(event) {
3041
            return $(event.target).is('select, input') && $module.closest('label').length > 0;
3042
          },
3043
          bubbledIconClick: function(event) {
3044
            return $(event.target).closest($icon).length > 0;
3045
          },
3046
          alreadySetup: function() {
3047
            return ($module.is('select') && $module.parent(selector.dropdown).length > 0  && $module.prev().length === 0);
3048
          },
3049
          animating: function($subMenu) {
3050
            return ($subMenu)
3051
              ? $subMenu.transition && $subMenu.transition('is animating')
3052
              : $menu.transition    && $menu.transition('is animating')
3053
            ;
3054
          },
3055
          leftward: function($subMenu) {
3056
            var $selectedMenu = $subMenu || $menu;
3057
            return $selectedMenu.hasClass(className.leftward);
3058
          },
3059
          disabled: function() {
3060
            return $module.hasClass(className.disabled);
3061
          },
3062
          focused: function() {
3063
            return (document.activeElement === $module[0]);
3064
          },
3065
          focusedOnSearch: function() {
3066
            return (document.activeElement === $search[0]);
3067
          },
3068
          allFiltered: function() {
3069
            return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
3070
          },
3071
          hidden: function($subMenu) {
3072
            return !module.is.visible($subMenu);
3073
          },
3074
          initialLoad: function() {
3075
            return initialLoad;
3076
          },
3077
          inObject: function(needle, object) {
3078
            var
3079
              found = false
3080
            ;
3081
            $.each(object, function(index, property) {
3082
              if(property == needle) {
3083
                found = true;
3084
                return true;
3085
              }
3086
            });
3087
            return found;
3088
          },
3089
          multiple: function() {
3090
            return $module.hasClass(className.multiple);
3091
          },
3092
          remote: function() {
3093
            return settings.apiSettings && module.can.useAPI();
3094
          },
3095
          single: function() {
3096
            return !module.is.multiple();
3097
          },
3098
          selectMutation: function(mutations) {
3099
            var
3100
              selectChanged = false
3101
            ;
3102
            $.each(mutations, function(index, mutation) {
3103
              if(mutation.target && $(mutation.target).is('select')) {
3104
                selectChanged = true;
3105
                return true;
3106
              }
3107
            });
3108
            return selectChanged;
3109
          },
3110
          search: function() {
3111
            return $module.hasClass(className.search);
3112
          },
3113
          searchSelection: function() {
3114
            return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
3115
          },
3116
          selection: function() {
3117
            return $module.hasClass(className.selection);
3118
          },
3119
          userValue: function(value) {
3120
            return ($.inArray(value, module.get.userValues()) !== -1);
3121
          },
3122
          upward: function($menu) {
3123
            var $element = $menu || $module;
3124
            return $element.hasClass(className.upward);
3125
          },
3126
          visible: function($subMenu) {
3127
            return ($subMenu)
3128
              ? $subMenu.hasClass(className.visible)
3129
              : $menu.hasClass(className.visible)
3130
            ;
3131
          },
3132
          verticallyScrollableContext: function() {
3133
            var
3134
              overflowY = ($context.get(0) !== window)
3135
                ? $context.css('overflow-y')
3136
                : false
3137
            ;
3138
            return (overflowY == 'auto' || overflowY == 'scroll');
3139
          },
3140
          horizontallyScrollableContext: function() {
3141
            var
3142
              overflowX = ($context.get(0) !== window)
3143
                ? $context.css('overflow-X')
3144
                : false
3145
            ;
3146
            return (overflowX == 'auto' || overflowX == 'scroll');
3147
          }
3148
        },
3149
3150
        can: {
3151
          activate: function($item) {
3152
            if(settings.useLabels) {
3153
              return true;
3154
            }
3155
            if(!module.has.maxSelections()) {
3156
              return true;
3157
            }
3158
            if(module.has.maxSelections() && $item.hasClass(className.active)) {
3159
              return true;
3160
            }
3161
            return false;
3162
          },
3163
          openDownward: function($subMenu) {
3164
            var
3165
              $currentMenu    = $subMenu || $menu,
3166
              canOpenDownward = true,
3167
              onScreen        = {},
3168
              calculations
3169
            ;
3170
            $currentMenu
3171
              .addClass(className.loading)
3172
            ;
3173
            calculations = {
3174
              context: {
3175
                scrollTop : $context.scrollTop(),
3176
                height    : $context.outerHeight()
3177
              },
3178
              menu : {
3179
                offset: $currentMenu.offset(),
3180
                height: $currentMenu.outerHeight()
3181
              }
3182
            };
3183
            if(module.is.verticallyScrollableContext()) {
3184
              calculations.menu.offset.top += calculations.context.scrollTop;
3185
            }
3186
            onScreen = {
3187
              above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.menu.height,
3188
              below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top + calculations.menu.height
3189
            };
3190
            if(onScreen.below) {
3191
              module.verbose('Dropdown can fit in context downward', onScreen);
3192
              canOpenDownward = true;
3193
            }
3194
            else if(!onScreen.below && !onScreen.above) {
3195
              module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
3196
              canOpenDownward = true;
3197
            }
3198
            else {
3199
              module.verbose('Dropdown cannot fit below, opening upward', onScreen);
3200
              canOpenDownward = false;
3201
            }
3202
            $currentMenu.removeClass(className.loading);
3203
            return canOpenDownward;
3204
          },
3205
          openRightward: function($subMenu) {
3206
            var
3207
              $currentMenu     = $subMenu || $menu,
3208
              canOpenRightward = true,
3209
              isOffscreenRight = false,
3210
              calculations
3211
            ;
3212
            $currentMenu
3213
              .addClass(className.loading)
3214
            ;
3215
            calculations = {
3216
              context: {
3217
                scrollLeft : $context.scrollLeft(),
3218
                width      : $context.outerWidth()
3219
              },
3220
              menu: {
3221
                offset : $currentMenu.offset(),
3222
                width  : $currentMenu.outerWidth()
3223
              }
3224
            };
3225
            if(module.is.horizontallyScrollableContext()) {
3226
              calculations.menu.offset.left += calculations.context.scrollLeft;
3227
            }
3228
            isOffscreenRight = (calculations.menu.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
3229
            if(isOffscreenRight) {
3230
              module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
3231
              canOpenRightward = false;
3232
            }
3233
            $currentMenu.removeClass(className.loading);
3234
            return canOpenRightward;
3235
          },
3236
          click: function() {
3237
            return (hasTouch || settings.on == 'click');
3238
          },
3239
          extendSelect: function() {
3240
            return settings.allowAdditions || settings.apiSettings;
3241
          },
3242
          show: function() {
3243
            return !module.is.disabled() && (module.has.items() || module.has.message());
3244
          },
3245
          useAPI: function() {
3246
            return $.fn.api !== undefined;
3247
          }
3248
        },
3249
3250
        animate: {
3251
          show: function(callback, $subMenu) {
3252
            var
3253
              $currentMenu = $subMenu || $menu,
3254
              start = ($subMenu)
3255
                ? function() {}
3256
                : function() {
3257
                  module.hideSubMenus();
3258
                  module.hideOthers();
3259
                  module.set.active();
3260
                },
3261
              transition
3262
            ;
3263
            callback = $.isFunction(callback)
3264
              ? callback
3265
              : function(){}
3266
            ;
3267
            module.verbose('Doing menu show animation', $currentMenu);
3268
            module.set.direction($subMenu);
3269
            transition = module.get.transition($subMenu);
3270
            if( module.is.selection() ) {
3271
              module.set.scrollPosition(module.get.selectedItem(), true);
3272
            }
3273
            if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
3274
              if(transition == 'none') {
3275
                start();
3276
                $currentMenu.transition('show');
3277
                callback.call(element);
3278
              }
3279
              else if($.fn.transition !== undefined && $module.transition('is supported')) {
3280
                $currentMenu
3281
                  .transition({
3282
                    animation  : transition + ' in',
3283
                    debug      : settings.debug,
3284
                    verbose    : settings.verbose,
3285
                    duration   : settings.duration,
3286
                    queue      : true,
3287
                    onStart    : start,
3288
                    onComplete : function() {
3289
                      callback.call(element);
3290
                    }
3291
                  })
3292
                ;
3293
              }
3294
              else {
3295
                module.error(error.noTransition, transition);
3296
              }
3297
            }
3298
          },
3299
          hide: function(callback, $subMenu) {
3300
            var
3301
              $currentMenu = $subMenu || $menu,
3302
              duration = ($subMenu)
3303
                ? (settings.duration * 0.9)
3304
                : settings.duration,
3305
              start = ($subMenu)
3306
                ? function() {}
3307
                : function() {
3308
                  if( module.can.click() ) {
3309
                    module.unbind.intent();
3310
                  }
3311
                  module.remove.active();
3312
                },
3313
              transition = module.get.transition($subMenu)
3314
            ;
3315
            callback = $.isFunction(callback)
3316
              ? callback
3317
              : function(){}
3318
            ;
3319
            if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
3320
              module.verbose('Doing menu hide animation', $currentMenu);
3321
3322
              if(transition == 'none') {
3323
                start();
3324
                $currentMenu.transition('hide');
3325
                callback.call(element);
3326
              }
3327
              else if($.fn.transition !== undefined && $module.transition('is supported')) {
3328
                $currentMenu
3329
                  .transition({
3330
                    animation  : transition + ' out',
3331
                    duration   : settings.duration,
3332
                    debug      : settings.debug,
3333
                    verbose    : settings.verbose,
3334
                    queue      : true,
3335
                    onStart    : start,
3336
                    onComplete : function() {
3337
                      callback.call(element);
3338
                    }
3339
                  })
3340
                ;
3341
              }
3342
              else {
3343
                module.error(error.transition);
3344
              }
3345
            }
3346
          }
3347
        },
3348
3349
        hideAndClear: function() {
3350
          module.remove.searchTerm();
3351
          if( module.has.maxSelections() ) {
3352
            return;
3353
          }
3354
          if(module.has.search()) {
3355
            module.hide(function() {
3356
              module.remove.filteredItem();
3357
            });
3358
          }
3359
          else {
3360
            module.hide();
3361
          }
3362
        },
3363
3364
        delay: {
3365
          show: function() {
3366
            module.verbose('Delaying show event to ensure user intent');
3367
            clearTimeout(module.timer);
3368
            module.timer = setTimeout(module.show, settings.delay.show);
3369
          },
3370
          hide: function() {
3371
            module.verbose('Delaying hide event to ensure user intent');
3372
            clearTimeout(module.timer);
3373
            module.timer = setTimeout(module.hide, settings.delay.hide);
3374
          }
3375
        },
3376
3377
        escape: {
3378
          value: function(value) {
3379
            var
3380
              multipleValues = $.isArray(value),
3381
              stringValue    = (typeof value === 'string'),
3382
              isUnparsable   = (!stringValue && !multipleValues),
3383
              hasQuotes      = (stringValue && value.search(regExp.quote) !== -1),
3384
              values         = []
3385
            ;
3386
            if(isUnparsable || !hasQuotes) {
3387
              return value;
3388
            }
3389
            module.debug('Encoding quote values for use in select', value);
3390
            if(multipleValues) {
3391
              $.each(value, function(index, value){
3392
                values.push(value.replace(regExp.quote, '&quot;'));
3393
              });
3394
              return values;
3395
            }
3396
            return value.replace(regExp.quote, '&quot;');
3397
          },
3398
          string: function(text) {
3399
            text =  String(text);
3400
            return text.replace(regExp.escape, '\\$&');
3401
          }
3402
        },
3403
3404
        setting: function(name, value) {
3405
          module.debug('Changing setting', name, value);
3406
          if( $.isPlainObject(name) ) {
3407
            $.extend(true, settings, name);
3408
          }
3409
          else if(value !== undefined) {
3410
            if($.isPlainObject(settings[name])) {
3411
              $.extend(true, settings[name], value);
3412
            }
3413
            else {
3414
              settings[name] = value;
3415
            }
3416
          }
3417
          else {
3418
            return settings[name];
3419
          }
3420
        },
3421
        internal: function(name, value) {
3422
          if( $.isPlainObject(name) ) {
3423
            $.extend(true, module, name);
3424
          }
3425
          else if(value !== undefined) {
3426
            module[name] = value;
3427
          }
3428
          else {
3429
            return module[name];
3430
          }
3431
        },
3432
        debug: function() {
3433
          if(!settings.silent && settings.debug) {
3434
            if(settings.performance) {
3435
              module.performance.log(arguments);
3436
            }
3437
            else {
3438
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
3439
              module.debug.apply(console, arguments);
3440
            }
3441
          }
3442
        },
3443
        verbose: function() {
3444
          if(!settings.silent && settings.verbose && settings.debug) {
3445
            if(settings.performance) {
3446
              module.performance.log(arguments);
3447
            }
3448
            else {
3449
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
3450
              module.verbose.apply(console, arguments);
3451
            }
3452
          }
3453
        },
3454
        error: function() {
3455
          if(!settings.silent) {
3456
            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
3457
            module.error.apply(console, arguments);
3458
          }
3459
        },
3460
        performance: {
3461
          log: function(message) {
3462
            var
3463
              currentTime,
3464
              executionTime,
3465
              previousTime
3466
            ;
3467
            if(settings.performance) {
3468
              currentTime   = new Date().getTime();
3469
              previousTime  = time || currentTime;
3470
              executionTime = currentTime - previousTime;
3471
              time          = currentTime;
3472
              performance.push({
3473
                'Name'           : message[0],
3474
                'Arguments'      : [].slice.call(message, 1) || '',
3475
                'Element'        : element,
3476
                'Execution Time' : executionTime
3477
              });
3478
            }
3479
            clearTimeout(module.performance.timer);
3480
            module.performance.timer = setTimeout(module.performance.display, 500);
3481
          },
3482
          display: function() {
3483
            var
3484
              title = settings.name + ':',
3485
              totalTime = 0
3486
            ;
3487
            time = false;
3488
            clearTimeout(module.performance.timer);
3489
            $.each(performance, function(index, data) {
3490
              totalTime += data['Execution Time'];
3491
            });
3492
            title += ' ' + totalTime + 'ms';
3493
            if(moduleSelector) {
3494
              title += ' \'' + moduleSelector + '\'';
3495
            }
3496
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
3497
              console.groupCollapsed(title);
3498
              if(console.table) {
3499
                console.table(performance);
3500
              }
3501
              else {
3502
                $.each(performance, function(index, data) {
3503
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
3504
                });
3505
              }
3506
              console.groupEnd();
3507
            }
3508
            performance = [];
3509
          }
3510
        },
3511
        invoke: function(query, passedArguments, context) {
3512
          var
3513
            object = instance,
3514
            maxDepth,
3515
            found,
3516
            response
3517
          ;
3518
          passedArguments = passedArguments || queryArguments;
3519
          context         = element         || context;
3520
          if(typeof query == 'string' && object !== undefined) {
3521
            query    = query.split(/[\. ]/);
3522
            maxDepth = query.length - 1;
3523
            $.each(query, function(depth, value) {
3524
              var camelCaseValue = (depth != maxDepth)
3525
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
3526
                : query
3527
              ;
3528
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
3529
                object = object[camelCaseValue];
3530
              }
3531
              else if( object[camelCaseValue] !== undefined ) {
3532
                found = object[camelCaseValue];
3533
                return false;
3534
              }
3535
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
3536
                object = object[value];
3537
              }
3538
              else if( object[value] !== undefined ) {
3539
                found = object[value];
3540
                return false;
3541
              }
3542
              else {
3543
                module.error(error.method, query);
3544
                return false;
3545
              }
3546
            });
3547
          }
3548
          if ( $.isFunction( found ) ) {
3549
            response = found.apply(context, passedArguments);
3550
          }
3551
          else if(found !== undefined) {
3552
            response = found;
3553
          }
3554
          if($.isArray(returnedValue)) {
3555
            returnedValue.push(response);
3556
          }
3557
          else if(returnedValue !== undefined) {
3558
            returnedValue = [returnedValue, response];
3559
          }
3560
          else if(response !== undefined) {
3561
            returnedValue = response;
3562
          }
3563
          return found;
3564
        }
3565
      };
3566
3567
      if(methodInvoked) {
3568
        if(instance === undefined) {
3569
          module.initialize();
3570
        }
3571
        module.invoke(query);
3572
      }
3573
      else {
3574
        if(instance !== undefined) {
3575
          instance.invoke('destroy');
3576
        }
3577
        module.initialize();
3578
      }
3579
    })
3580
  ;
3581
  return (returnedValue !== undefined)
3582
    ? returnedValue
3583
    : $allModules
3584
  ;
3585
};
3586
3587
$.fn.dropdown.settings = {
3588
3589
  silent                 : false,
3590
  debug                  : false,
3591
  verbose                : false,
3592
  performance            : true,
3593
3594
  on                     : 'click',    // what event should show menu action on item selection
3595
  action                 : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
3596
3597
3598
  apiSettings            : false,
3599
  selectOnKeydown        : true,       // Whether selection should occur automatically when keyboard shortcuts used
3600
  minCharacters          : 0,          // Minimum characters required to trigger API call
3601
3602
  filterRemoteData       : false,      // Whether API results should be filtered after being returned for query term
3603
  saveRemoteData         : true,       // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
3604
3605
  throttle               : 200,        // How long to wait after last user input to search remotely
3606
3607
  context                : window,     // Context to use when determining if on screen
3608
  direction              : 'auto',     // Whether dropdown should always open in one direction
3609
  keepOnScreen           : true,       // Whether dropdown should check whether it is on screen before showing
3610
3611
  match                  : 'both',     // what to match against with search selection (both, text, or label)
3612
  fullTextSearch         : false,      // search anywhere in value (set to 'exact' to require exact matches)
3613
3614
  placeholder            : 'auto',     // whether to convert blank <select> values to placeholder text
3615
  preserveHTML           : true,       // preserve html when selecting value
3616
  sortSelect             : false,      // sort selection on init
3617
3618
  forceSelection         : true,       // force a choice on blur with search selection
3619
3620
  allowAdditions         : false,      // whether multiple select should allow user added values
3621
  hideAdditions          : true,      // whether or not to hide special message prompting a user they can enter a value
3622
3623
  maxSelections          : false,      // When set to a number limits the number of selections to this count
3624
  useLabels              : true,       // whether multiple select should filter currently active selections from choices
3625
  delimiter              : ',',        // when multiselect uses normal <input> the values will be delimited with this character
3626
3627
  showOnFocus            : true,       // show menu on focus
3628
  allowReselection       : false,      // whether current value should trigger callbacks when reselected
3629
  allowTab               : true,       // add tabindex to element
3630
  allowCategorySelection : false,      // allow elements with sub-menus to be selected
3631
3632
  fireOnInit             : false,      // Whether callbacks should fire when initializing dropdown values
3633
3634
  transition             : 'auto',     // auto transition will slide down or up based on direction
3635
  duration               : 200,        // duration of transition
3636
3637
  glyphWidth             : 1.037,      // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
3638
3639
  // label settings on multi-select
3640
  label: {
3641
    transition : 'scale',
3642
    duration   : 200,
3643
    variation  : false
3644
  },
3645
3646
  // delay before event
3647
  delay : {
3648
    hide   : 300,
3649
    show   : 200,
3650
    search : 20,
3651
    touch  : 50
3652
  },
3653
3654
  /* Callbacks */
3655
  onChange      : function(value, text, $selected){},
3656
  onAdd         : function(value, text, $selected){},
3657
  onRemove      : function(value, text, $selected){},
3658
3659
  onLabelSelect : function($selectedLabels){},
3660
  onLabelCreate : function(value, text) { return $(this); },
3661
  onLabelRemove : function(value) { return true; },
3662
  onNoResults   : function(searchTerm) { return true; },
3663
  onShow        : function(){},
3664
  onHide        : function(){},
3665
3666
  /* Component */
3667
  name           : 'Dropdown',
3668
  namespace      : 'dropdown',
3669
3670
  message: {
3671
    addResult     : 'Add <b>{term}</b>',
3672
    count         : '{count} selected',
3673
    maxSelections : 'Max {maxCount} selections',
3674
    noResults     : 'No results found.',
3675
    serverError   : 'There was an error contacting the server'
3676
  },
3677
3678
  error : {
3679
    action          : 'You called a dropdown action that was not defined',
3680
    alreadySetup    : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
3681
    labels          : 'Allowing user additions currently requires the use of labels.',
3682
    missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
3683
    method          : 'The method you called is not defined.',
3684
    noAPI           : 'The API module is required to load resources remotely',
3685
    noStorage       : 'Saving remote data requires session storage',
3686
    noTransition    : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>'
3687
  },
3688
3689
  regExp : {
3690
    escape   : /[-[\]{}()*+?.,\\^$|#\s]/g,
3691
    quote    : /"/g
3692
  },
3693
3694
  metadata : {
3695
    defaultText     : 'defaultText',
3696
    defaultValue    : 'defaultValue',
3697
    placeholderText : 'placeholder',
3698
    text            : 'text',
3699
    value           : 'value'
3700
  },
3701
3702
  // property names for remote query
3703
  fields: {
3704
    remoteValues : 'results',  // grouping for api results
3705
    values       : 'values',   // grouping for all dropdown values
3706
    disabled     : 'disabled', // whether value should be disabled
3707
    name         : 'name',     // displayed dropdown text
3708
    value        : 'value',    // actual dropdown value
3709
    text         : 'text'      // displayed text when selected
3710
  },
3711
3712
  keys : {
3713
    backspace  : 8,
3714
    delimiter  : 188, // comma
3715
    deleteKey  : 46,
3716
    enter      : 13,
3717
    escape     : 27,
3718
    pageUp     : 33,
3719
    pageDown   : 34,
3720
    leftArrow  : 37,
3721
    upArrow    : 38,
3722
    rightArrow : 39,
3723
    downArrow  : 40
3724
  },
3725
3726
  selector : {
3727
    addition     : '.addition',
3728
    dropdown     : '.ui.dropdown',
3729
    hidden       : '.hidden',
3730
    icon         : '> .dropdown.icon',
3731
    input        : '> input[type="hidden"], > select',
3732
    item         : '.item',
3733
    label        : '> .label',
3734
    remove       : '> .label > .delete.icon',
3735
    siblingLabel : '.label',
3736
    menu         : '.menu',
3737
    message      : '.message',
3738
    menuIcon     : '.dropdown.icon',
3739
    search       : 'input.search, .menu > .search > input, .menu input.search',
3740
    sizer        : '> input.sizer',
3741
    text         : '> .text:not(.icon)',
3742
    unselectable : '.disabled, .filtered'
3743
  },
3744
3745
  className : {
3746
    active      : 'active',
3747
    addition    : 'addition',
3748
    animating   : 'animating',
3749
    disabled    : 'disabled',
3750
    empty       : 'empty',
3751
    dropdown    : 'ui dropdown',
3752
    filtered    : 'filtered',
3753
    hidden      : 'hidden transition',
3754
    item        : 'item',
3755
    label       : 'ui label',
3756
    loading     : 'loading',
3757
    menu        : 'menu',
3758
    message     : 'message',
3759
    multiple    : 'multiple',
3760
    placeholder : 'default',
3761
    sizer       : 'sizer',
3762
    search      : 'search',
3763
    selected    : 'selected',
3764
    selection   : 'selection',
3765
    upward      : 'upward',
3766
    leftward    : 'left',
3767
    visible     : 'visible'
3768
  }
3769
3770
};
3771
3772
/* Templates */
3773
$.fn.dropdown.settings.templates = {
3774
3775
  // generates dropdown from select values
3776
  dropdown: function(select) {
3777
    var
3778
      placeholder = select.placeholder || false,
3779
      values      = select.values || {},
3780
      html        = ''
3781
    ;
3782
    html +=  '<i class="dropdown icon"></i>';
3783
    if(select.placeholder) {
3784
      html += '<div class="default text">' + placeholder + '</div>';
3785
    }
3786
    else {
3787
      html += '<div class="text"></div>';
3788
    }
3789
    html += '<div class="menu">';
3790
    $.each(select.values, function(index, option) {
3791
      html += (option.disabled)
3792
        ? '<div class="disabled item" data-value="' + option.value + '">' + option.name + '</div>'
3793
        : '<div class="item" data-value="' + option.value + '">' + option.name + '</div>'
3794
      ;
3795
    });
3796
    html += '</div>';
3797
    return html;
3798
  },
3799
3800
  // generates just menu from select
3801
  menu: function(response, fields) {
3802
    var
3803
      values = response[fields.values] || {},
3804
      html   = ''
3805
    ;
3806
    $.each(values, function(index, option) {
3807
      var
3808
        maybeText = (option[fields.text])
3809
          ? 'data-text="' + option[fields.text] + '"'
3810
          : '',
3811
        maybeDisabled = (option[fields.disabled])
3812
          ? 'disabled '
3813
          : ''
3814
      ;
3815
      html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>'
3816
      html +=   option[fields.name];
3817
      html += '</div>';
3818
    });
3819
    return html;
3820
  },
3821
3822
  // generates label for multiselect
3823
  label: function(value, text) {
3824
    return text + '<i class="delete icon"></i>';
3825
  },
3826
3827
3828
  // generates messages like "No results"
3829
  message: function(message) {
3830
    return message;
3831
  },
3832
3833
  // generates user addition to selection menu
3834
  addition: function(choice) {
3835
    return choice;
3836
  }
3837
3838
};
3839
3840
})( jQuery, window, document );
3841