UNPKG

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