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