share/views/public/plugins/semantic-ui/dist/components/dropdown.js in rbbt-rest-1.8.42 vs share/views/public/plugins/semantic-ui/dist/components/dropdown.js in rbbt-rest-1.8.43

- old
+ new

@@ -1,20 +1,26 @@ /*! - * # Semantic UI 2.0.0 - Dropdown + * # Semantic UI 2.2.6 - Dropdown * http://github.com/semantic-org/semantic-ui/ * * - * Copyright 2015 Contributors * Released under the MIT license * http://opensource.org/licenses/MIT * */ -;(function ( $, window, document, undefined ) { +;(function ($, window, document, undefined) { "use strict"; +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + $.fn.dropdown = function(parameters) { var $allModules = $(this), $document = $(document), @@ -37,10 +43,12 @@ ? $.extend(true, {}, $.fn.dropdown.settings, parameters) : $.extend({}, $.fn.dropdown.settings), className = settings.className, message = settings.message, + fields = settings.fields, + keys = settings.keys, metadata = settings.metadata, namespace = settings.namespace, regExp = settings.regExp, selector = settings.selector, error = settings.error, @@ -51,10 +59,11 @@ $module = $(this), $context = $(settings.context), $text = $module.find(selector.text), $search = $module.find(selector.search), + $sizer = $module.find(selector.sizer), $input = $module.find(selector.input), $icon = $module.find(selector.icon), $combo = ($module.prev().find(selector.text).length > 0) ? $module.prev().find(selector.text) @@ -63,15 +72,17 @@ $menu = $module.children(selector.menu), $item = $menu.find(selector.item), activated = false, itemActivated = false, + internalChange = false, element = this, instance = $module.data(moduleNamespace), initialLoad, pageLostFocus, + willRefocus, elementNamespace, id, selectObserver, menuObserver, module @@ -91,15 +102,11 @@ module.save.defaults(); module.restore.selected(); module.create.id(); - if(hasTouch) { - module.bind.touchEvents(); - } - module.bind.mouseEvents(); - module.bind.keyboardEvents(); + module.bind.events(); module.observeChanges(); module.instantiate(); } @@ -124,41 +131,52 @@ .off(eventNamespace) ; $document .off(elementNamespace) ; - if(selectObserver) { - selectObserver.disconnect(); - } - if(menuObserver) { - menuObserver.disconnect(); - } + module.disconnect.menuObserver(); + module.disconnect.selectObserver(); }, observeChanges: function() { if('MutationObserver' in window) { - selectObserver = new MutationObserver(function(mutations) { - module.debug('<select> modified, recreating menu'); - module.setup.select(); - }); - menuObserver = new MutationObserver(function(mutations) { - module.debug('Menu modified, updating selector cache'); - module.refresh(); - }); + selectObserver = new MutationObserver(module.event.select.mutation); + menuObserver = new MutationObserver(module.event.menu.mutation); + module.debug('Setting up mutation observer', selectObserver, menuObserver); + module.observe.select(); + module.observe.menu(); + } + }, + + disconnect: { + menuObserver: function() { + if(menuObserver) { + menuObserver.disconnect(); + } + }, + selectObserver: function() { + if(selectObserver) { + selectObserver.disconnect(); + } + } + }, + observe: { + select: function() { if(module.has.input()) { selectObserver.observe($input[0], { childList : true, subtree : true }); } + }, + menu: function() { if(module.has.menu()) { menuObserver.observe($menu[0], { childList : true, subtree : true }); } - module.debug('Setting up mutation observer', selectObserver, menuObserver); } }, create: { id: function() { @@ -181,17 +199,21 @@ ? values : [values] ; $.each(values, function(index, value) { if(module.get.item(value) === false) { - html = settings.templates.addition(value); + html = settings.templates.addition( module.add.variables(message.addResult, value) ); $userChoice = $('<div />') .html(html) - .data(metadata.value, value) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) .addClass(className.addition) .addClass(className.item) ; + if(settings.hideAdditions) { + $userChoice.addClass(className.hidden); + } $userChoices = ($userChoices === undefined) ? $userChoice : $userChoices.add($userChoice) ; module.verbose('Creating user choices for value', value, $userChoice); @@ -209,27 +231,45 @@ module.verbose('Adding custom user value'); module.add.label(value, value); }); } }, + menu: function() { + $menu = $('<div />') + .addClass(className.menu) + .appendTo($module) + ; + }, + sizer: function() { + $sizer = $('<span />') + .addClass(className.sizer) + .insertAfter($search) + ; + } }, search: function(query) { query = (query !== undefined) ? query : module.get.query() ; module.verbose('Searching for query', query); - module.filter(query); + if(module.has.minCharacters(query)) { + module.filter(query); + } + else { + module.hide(); + } }, select: { firstUnfiltered: function() { module.verbose('Selecting first non-filtered element'); module.remove.selectedItem(); $item .not(selector.unselectable) + .not(selector.addition + selector.hidden) .eq(0) .addClass(className.selected) ; }, nextAvailable: function($selected) { @@ -252,11 +292,15 @@ setup: { api: function() { var apiSettings = { - debug : settings.debug, + debug : settings.debug, + urlData : { + value : module.get.value(), + query : module.get.query() + }, on : false } ; module.verbose('First request, initializing API'); $module @@ -265,28 +309,28 @@ }, layout: function() { if( $module.is('select') ) { module.setup.select(); module.setup.returnedObject(); - console.log($module); } + if( !module.has.menu() ) { + module.create.menu(); + } if( module.is.search() && !module.has.search() ) { module.verbose('Adding search input'); $search = $('<input />') .addClass(className.search) + .prop('autocomplete', 'off') .insertBefore($text) ; } + if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) { + module.create.sizer(); + } if(settings.allowTab) { module.set.tabbable(); } - if($menu.length === 0) { - $menu = $('<div />') - .addClass(className.menu) - .appendTo($module) - ; - } }, select: function() { var selectValues = module.get.selectValues() ; @@ -296,11 +340,14 @@ } // see if select is placed correctly already if($input.parent(selector.dropdown).length > 0) { module.debug('UI dropdown already exists. Creating dropdown menu only'); $module = $input.closest(selector.dropdown); - $menu = $module.children(selector.menu); + if( !module.has.menu() ) { + module.create.menu(); + } + $menu = $module.children(selector.menu); module.setup.menu(selectValues); } else { module.debug('Creating entire dropdown from select'); $module = $('<div />') @@ -308,24 +355,31 @@ .addClass(className.selection) .addClass(className.dropdown) .html( templates.dropdown(selectValues) ) .insertBefore($input) ; + if($input.hasClass(className.multiple) && $input.prop('multiple') === false) { + module.error(error.missingMultiple); + $input.prop('multiple', true); + } + if($input.is('[multiple]')) { + module.set.multiple(); + } + if ($input.prop('disabled')) { + module.debug('Disabling dropdown'); + $module.addClass(className.disabled); + } $input .removeAttr('class') .detach() .prependTo($module) ; - console.log($module); } - if($input.is('[multiple]')) { - module.set.multiple(); - } module.refresh(); }, menu: function(values) { - $menu.html( templates.menu( values )); + $menu.html( templates.menu(values, fields)); $item = $menu.find(selector.item); }, reference: function() { module.debug('Dropdown behavior was called on select, replacing with closest dropdown'); // replace module reference @@ -351,10 +405,14 @@ refresh: function() { module.refreshSelectors(); module.refreshData(); }, + refreshItems: function() { + $item = $menu.find(selector.item); + }, + refreshSelectors: function() { module.verbose('Refreshing selector cache'); $text = $module.find(selector.text); $search = $module.find(selector.search); $input = $module.find(selector.input); @@ -371,18 +429,25 @@ module.verbose('Refreshing cached metadata'); $item .removeData(metadata.text) .removeData(metadata.value) ; + }, + + clearData: function() { + module.verbose('Clearing metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; $module .removeData(metadata.defaultText) .removeData(metadata.defaultValue) .removeData(metadata.placeholderText) ; }, - toggle: function() { module.verbose('Toggling menu visibility'); if( !module.is.active() ) { module.show(); } @@ -396,38 +461,44 @@ ? callback : function(){} ; if( module.can.show() && !module.is.active() ) { module.debug('Showing dropdown'); - if(module.is.multiple()) { - if(!module.has.search() && module.is.allFiltered()) { - return true; - } + if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) { + module.remove.message(); } - module.animate.show(function() { - if( module.can.click() ) { - module.bind.intent(); - } - module.set.visible(); - callback.call(element); - }); - settings.onShow.call(element); + if(module.is.allFiltered()) { + return true; + } + if(settings.onShow.call(element) !== false) { + module.animate.show(function() { + if( module.can.click() ) { + module.bind.intent(); + } + if(module.has.menuSearch()) { + module.focusSearch(); + } + module.set.visible(); + callback.call(element); + }); + } } }, hide: function(callback) { callback = $.isFunction(callback) ? callback : function(){} ; if( module.is.active() ) { module.debug('Hiding dropdown'); - module.animate.hide(function() { - module.remove.visible(); - callback.call(element); - }); - settings.onHide.call(element); + if(settings.onHide.call(element) !== false) { + module.animate.hide(function() { + module.remove.visible(); + callback.call(element); + }); + } } }, hideOthers: function() { module.verbose('Finding other dropdowns to hide'); @@ -452,12 +523,34 @@ module.verbose('Hiding sub menus', $subMenus); $subMenus.transition('hide'); }, bind: { + events: function() { + if(hasTouch) { + module.bind.touchEvents(); + } + module.bind.keyboardEvents(); + module.bind.inputEvents(); + module.bind.mouseEvents(); + }, + touchEvents: function() { + module.debug('Touch device detected binding additional touch events'); + if( module.is.searchSelection() ) { + // do nothing special yet + } + else if( module.is.single() ) { + $module + .on('touchstart' + eventNamespace, module.event.test.toggle) + ; + } + $menu + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + ; + }, keyboardEvents: function() { - module.debug('Binding keyboard events'); + module.verbose('Binding keyboard events'); $module .on('keydown' + eventNamespace, module.event.keydown) ; if( module.has.search() ) { $module @@ -468,50 +561,46 @@ $document .on('keydown' + elementNamespace, module.event.document.keydown) ; } }, - touchEvents: function() { - module.debug('Touch device detected binding additional touch events'); - if( module.is.searchSelection() ) { - // do nothing special yet - } - else { - $module - .on('touchstart' + eventNamespace, module.event.test.toggle) - ; - } - $menu - .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + inputEvents: function() { + module.verbose('Binding input change events'); + $module + .on('change' + eventNamespace, selector.input, module.event.change) ; }, mouseEvents: function() { - module.debug('Mouse detected binding mouse events'); + module.verbose('Binding mouse events'); if(module.is.multiple()) { $module - .on('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + eventNamespace, selector.label, module.event.label.click) .on('click' + eventNamespace, selector.remove, module.event.remove.click) ; } if( module.is.searchSelection() ) { $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) - .on('click' + eventNamespace, selector.search, module.show) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) .on('focus' + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) .on('blur' + eventNamespace, selector.search, module.event.search.blur) .on('click' + eventNamespace, selector.text, module.event.text.focus) ; if(module.is.multiple()) { $module - .on('click' + eventNamespace, module.event.click) + .on('click' + eventNamespace, module.event.click) ; } } else { if(settings.on == 'click') { $module + .on('click' + eventNamespace, selector.icon, module.event.icon.click) .on('click' + eventNamespace, module.event.test.toggle) ; } else if(settings.on == 'hover') { $module @@ -576,32 +665,40 @@ module.filterActive(); } module.select.firstUnfiltered(); if( module.has.allResultsFiltered() ) { if( settings.onNoResults.call(element, searchTerm) ) { - if(!settings.allowAdditions) { + if(settings.allowAdditions) { + if(settings.hideAdditions) { + module.verbose('User addition with no menu, setting empty style'); + module.set.empty(); + module.hideMenu(); + } + } + else { module.verbose('All items filtered, showing message', searchTerm); module.add.message(message.noResults); } } else { module.verbose('All items filtered, hiding dropdown', searchTerm); module.hideMenu(); } } else { + module.remove.empty(); module.remove.message(); } if(settings.allowAdditions) { module.add.userSuggestion(query); } if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { module.show(); } } ; - if(module.has.maxSelections()) { + if(settings.useLabels && module.has.maxSelections()) { return; } if(settings.apiSettings) { if( module.can.useAPI() ) { module.queryRemote(searchTerm, function() { @@ -619,14 +716,14 @@ }, queryRemote: function(query, callback) { var apiSettings = { - errorDuration : false, - throttle : settings.throttle, - cache : 'local', - urlData : { + errorDuration : false, + cache : 'local', + throttle : settings.throttle, + urlData : { query: query }, onError: function() { module.add.message(message.serverError); callback(); @@ -636,11 +733,11 @@ callback(); }, onSuccess : function(response) { module.remove.message(); module.setup.menu({ - values: response.results + values: response[fields.remoteValues] }); callback(); } } ; @@ -657,19 +754,18 @@ filterItems: function(query) { var searchTerm = (query !== undefined) ? query : module.get.query(), - $results = $(), + results = null, escapedTerm = module.escape.regExp(searchTerm), beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm') ; // avoid loop if we're matching nothing - if(searchTerm === '') { - $results = $item; - } - else { + if( module.has.query() ) { + results = []; + module.verbose('Searching for matching values', searchTerm); $item .each(function(){ var $choice = $(this), @@ -677,40 +773,45 @@ value ; if(settings.match == 'both' || settings.match == 'text') { text = String(module.get.choiceText($choice, false)); if(text.search(beginsWithRegExp) !== -1) { - $results = $results.add($choice); + results.push(this); return true; } - else if(settings.fullTextSearch && module.fuzzySearch(searchTerm, text)) { - $results = $results.add($choice); + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) { + results.push(this); return true; } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) { + results.push(this); + return true; + } } if(settings.match == 'both' || settings.match == 'value') { value = String(module.get.choiceValue($choice, text)); if(value.search(beginsWithRegExp) !== -1) { - $results = $results.add($choice); + results.push(this); return true; } else if(settings.fullTextSearch && module.fuzzySearch(searchTerm, value)) { - $results = $results.add($choice); + results.push(this); return true; } } }) ; } - module.debug('Showing only matched items', searchTerm); module.remove.filteredItem(); - $item - .not($results) - .addClass(className.filtered) - ; + if(results) { + $item + .not(results) + .addClass(className.filtered) + ; + } }, fuzzySearch: function(query, term) { var termLength = term.length, @@ -735,98 +836,144 @@ } return false; } return true; }, - + exactSearch: function (query, term) { + query = query.toLowerCase(); + term = term.toLowerCase(); + if(term.indexOf(query) > -1) { + return true; + } + return false; + }, filterActive: function() { if(settings.useLabels) { $item.filter('.' + className.active) .addClass(className.filtered) ; } }, - focusSearch: function() { - if( module.is.search() && !module.is.focusedOnSearch() ) { - $search[0].focus(); + focusSearch: function(skipHandler) { + if( module.has.search() && !module.is.focusedOnSearch() ) { + if(skipHandler) { + $module.off('focus' + eventNamespace, selector.search); + $search.focus(); + $module.on('focus' + eventNamespace, selector.search, module.event.search.focus); + } + else { + $search.focus(); + } } }, forceSelection: function() { var $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0), $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0), $selectedItem = ($currentlySelected.length > 0) ? $currentlySelected : $activeItem, - hasSelected = ($selectedItem.size() > 0) + hasSelected = ($selectedItem.length > 0) ; if(hasSelected) { module.debug('Forcing partial selection to selected item', $selectedItem); - module.event.item.click.call($selectedItem); + module.event.item.click.call($selectedItem, {}, true); + return; } else { - module.hide(); + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); + } + else { + module.remove.searchTerm(); + } } }, event: { + change: function() { + if(!internalChange) { + module.debug('Input changed, updating selection'); + module.set.selected(); + } + }, focus: function() { if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) { module.show(); } }, - click: function(event) { - var - $target = $(event.target) - ; - // focus search - if(($target.is($module) || $target.is($icon)) && !module.is.focusedOnSearch()) { - module.focusSearch(); - } - }, blur: function(event) { pageLostFocus = (document.activeElement === this); if(!activated && !pageLostFocus) { module.remove.activeLabel(); module.hide(); } }, - // prevents focus callback from occuring on mousedown mousedown: function() { - activated = true; + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = true; + } + else { + // prevents focus callback from occurring on mousedown + activated = true; + } }, mouseup: function() { - activated = false; + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = false; + } + else { + activated = false; + } }, + click: function(event) { + var + $target = $(event.target) + ; + // focus search + if($target.is($module)) { + if(!module.is.focusedOnSearch()) { + module.focusSearch(); + } + else { + module.show(); + } + } + }, search: { focus: function() { activated = true; if(module.is.multiple()) { module.remove.activeLabel(); } if(settings.showOnFocus) { - module.show(); + module.search(); } }, blur: function(event) { pageLostFocus = (document.activeElement === this); - if(!itemActivated && !pageLostFocus) { - if(module.is.multiple()) { - module.remove.activeLabel(); + if(!willRefocus) { + if(!itemActivated && !pageLostFocus) { + if(settings.forceSelection) { + module.forceSelection(); + } module.hide(); } - else if(settings.forceSelection) { - module.forceSelection(); - } - else { - module.hide(); - } } + willRefocus = false; } }, + icon: { + click: function(event) { + module.toggle(); + } + }, text: { focus: function(event) { activated = true; module.focusSearch(); } @@ -883,19 +1030,24 @@ toggle: function(event) { var toggleBehavior = (module.is.multiple()) ? module.show : module.toggle - ; + ; + if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) { + return; + } if( module.determine.eventOnElement(event, toggleBehavior) ) { event.preventDefault(); } }, touch: function(event) { module.determine.eventOnElement(event, function() { if(event.type == 'touchstart') { - module.timer = setTimeout(module.hide, settings.delay.touch); + module.timer = setTimeout(function() { + module.hide(); + }, settings.delay.touch); } else if(event.type == 'touchmove') { clearTimeout(module.timer); } }); @@ -903,32 +1055,64 @@ }, hide: function(event) { module.determine.eventInModule(event, module.hide); } }, + select: { + mutation: function(mutations) { + module.debug('<select> modified, recreating menu'); + module.setup.select(); + } + }, menu: { + mutation: function(mutations) { + var + mutation = mutations[0], + $addedNode = mutation.addedNodes + ? $(mutation.addedNodes[0]) + : $(false), + $removedNode = mutation.removedNodes + ? $(mutation.removedNodes[0]) + : $(false), + $changedNodes = $addedNode.add($removedNode), + isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0, + isMessage = $changedNodes.is(selector.message) || $changedNodes.closest(selector.message).length > 0 + ; + if(isUserAddition || isMessage) { + module.debug('Updating item selector cache'); + module.refreshItems(); + } + else { + module.debug('Menu modified, updating selector cache'); + module.refresh(); + } + }, mousedown: function() { itemActivated = true; }, mouseup: function() { itemActivated = false; } }, item: { mouseenter: function(event) { var - $subMenu = $(this).children(selector.menu), - $otherMenus = $(this).siblings(selector.item).children(selector.menu) + $target = $(event.target), + $item = $(this), + $subMenu = $item.children(selector.menu), + $otherMenus = $item.siblings(selector.item).children(selector.menu), + hasSubMenu = ($subMenu.length > 0), + isBubbledEvent = ($subMenu.find($target).length > 0) ; - if( $subMenu.length > 0 ) { + if( !isBubbledEvent && hasSubMenu ) { clearTimeout(module.itemTimer); module.itemTimer = setTimeout(function() { module.verbose('Showing sub-menu', $subMenu); $.each($otherMenus, function() { module.animate.hide(false, $(this)); }); - module.animate.show(false, $subMenu); + module.animate.show(false, $subMenu); }, settings.delay.show); event.preventDefault(); } }, mouseleave: function(event) { @@ -937,48 +1121,57 @@ ; if($subMenu.length > 0) { clearTimeout(module.itemTimer); module.itemTimer = setTimeout(function() { module.verbose('Hiding sub-menu', $subMenu); - module.animate.hide(false, $subMenu); + module.animate.hide(false, $subMenu); }, settings.delay.hide); } }, - click: function (event) { + click: function (event, skipRefocus) { var - $choice = $(this), - $target = (event) + $choice = $(this), + $target = (event) ? $(event.target) : $(''), - $subMenu = $choice.find(selector.menu), - text = module.get.choiceText($choice), - value = module.get.choiceValue($choice, text), + $subMenu = $choice.find(selector.menu), + text = module.get.choiceText($choice), + value = module.get.choiceValue($choice, text), hasSubMenu = ($subMenu.length > 0), isBubbledEvent = ($subMenu.find($target).length > 0) ; if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) { - if(!settings.useLabels) { + if(module.is.searchSelection()) { + if(settings.allowAdditions) { + module.remove.userAddition(); + } module.remove.searchTerm(); + if(!module.is.focusedOnSearch() && !(skipRefocus == true)) { + module.focusSearch(true); + } } + if(!settings.useLabels) { + module.remove.filteredItem(); + module.set.scrollPosition($choice); + } module.determine.selectAction.call(this, text, value); } } }, document: { // label selection should occur even when element has no focus keydown: function(event) { var pressedKey = event.which, - keys = module.get.shortcutKeys(), isShortcutKey = module.is.inObject(pressedKey, keys) ; if(isShortcutKey) { var $label = $module.find(selector.label), $activeLabel = $label.filter('.' + className.active), - activeValue = $activeLabel.data('value'), + activeValue = $activeLabel.data(metadata.value), labelIndex = $label.index($activeLabel), labelCount = $label.length, hasActiveLabel = ($activeLabel.length > 0), hasMultipleActive = ($activeLabel.length > 1), isFirstLabel = (labelIndex === 0), @@ -1081,89 +1274,97 @@ }, keydown: function(event) { var pressedKey = event.which, - keys = module.get.shortcutKeys(), isShortcutKey = module.is.inObject(pressedKey, keys) ; if(isShortcutKey) { var $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0), $activeItem = $menu.children('.' + className.active).eq(0), $selectedItem = ($currentlySelected.length > 0) ? $currentlySelected : $activeItem, $visibleItems = ($selectedItem.length > 0) - ? $selectedItem.siblings(':not(.' + className.filtered +')').andSelf() + ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack() : $menu.children(':not(.' + className.filtered +')'), - $subMenu = $selectedItem.children(selector.menu), - $parentMenu = $selectedItem.closest(selector.menu), - inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0), - hasSubMenu = ($subMenu.length> 0), - hasSelectedItem = ($selectedItem.length > 0), - selectedIsVisible = ($selectedItem.not(selector.unselectable).length > 0), + $subMenu = $selectedItem.children(selector.menu), + $parentMenu = $selectedItem.closest(selector.menu), + inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0), + hasSubMenu = ($subMenu.length> 0), + hasSelectedItem = ($selectedItem.length > 0), + selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0), + delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()), + isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable), $nextItem, isSubMenuItem, newIndex ; + // allow selection with menu closed + if(isAdditionWithoutMenu) { + module.verbose('Selecting item from keyboard shortcut', $selectedItem); + module.event.item.click.call($selectedItem, event); + if(module.is.searchSelection()) { + module.remove.searchTerm(); + } + } // visible menu keyboard shortcuts if( module.is.visible() ) { // enter (select or open sub-menu) - if(pressedKey == keys.enter || pressedKey == keys.delimiter) { - + if(pressedKey == keys.enter || delimiterPressed) { if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) { module.verbose('Pressed enter on unselectable category, opening sub menu'); pressedKey = keys.rightArrow; } - else if(selectedIsVisible) { + else if(selectedIsSelectable) { module.verbose('Selecting item from keyboard shortcut', $selectedItem); module.event.item.click.call($selectedItem, event); - if(settings.useLabels && module.is.searchSelection()) { - module.hideAndClear(); - } - else { + if(module.is.searchSelection()) { module.remove.searchTerm(); } } event.preventDefault(); } - // left arrow (hide sub-menu) - if(pressedKey == keys.leftArrow) { + // sub-menu actions + if(hasSelectedItem) { - isSubMenuItem = ($parentMenu[0] !== $menu[0]); + if(pressedKey == keys.leftArrow) { - if(isSubMenuItem) { - module.verbose('Left key pressed, closing sub-menu'); - module.animate.hide(false, $parentMenu); - $selectedItem - .removeClass(className.selected) - ; - $parentMenu - .closest(selector.item) - .addClass(className.selected) - ; - event.preventDefault(); + isSubMenuItem = ($parentMenu[0] !== $menu[0]); + + if(isSubMenuItem) { + module.verbose('Left key pressed, closing sub-menu'); + module.animate.hide(false, $parentMenu); + $selectedItem + .removeClass(className.selected) + ; + $parentMenu + .closest(selector.item) + .addClass(className.selected) + ; + event.preventDefault(); + } } - } - // right arrow (show sub-menu) - if(pressedKey == keys.rightArrow) { - if(hasSubMenu) { - module.verbose('Right key pressed, opening sub-menu'); - module.animate.show(false, $subMenu); - $selectedItem - .removeClass(className.selected) - ; - $subMenu - .find(selector.item).eq(0) - .addClass(className.selected) - ; - event.preventDefault(); + // right arrow (show sub-menu) + if(pressedKey == keys.rightArrow) { + if(hasSubMenu) { + module.verbose('Right key pressed, opening sub-menu'); + module.animate.show(false, $subMenu); + $selectedItem + .removeClass(className.selected) + ; + $subMenu + .find(selector.item).eq(0) + .addClass(className.selected) + ; + event.preventDefault(); + } } } // up arrow (traverse menu up) if(pressedKey == keys.upArrow) { @@ -1183,10 +1384,13 @@ ; $nextItem .addClass(className.selected) ; module.set.scrollPosition($nextItem); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextItem); + } } event.preventDefault(); } // down arrow (traverse menu down) @@ -1207,10 +1411,13 @@ ; $nextItem .addClass(className.selected) ; module.set.scrollPosition($nextItem); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextItem); + } } event.preventDefault(); } // page down (show next page) @@ -1230,50 +1437,70 @@ } } else { // delimiter key - if(pressedKey == keys.delimiter) { + if(delimiterPressed) { event.preventDefault(); } // down arrow (open menu) - if(pressedKey == keys.downArrow) { + if(pressedKey == keys.downArrow && !module.is.visible()) { module.verbose('Down key pressed, showing dropdown'); + module.select.firstUnfiltered(); module.show(); event.preventDefault(); } } } else { - if( module.is.selection() && !module.is.search() ) { + if( !module.has.search() ) { module.set.selectedLetter( String.fromCharCode(pressedKey) ); } } } }, + trigger: { + change: function() { + var + events = document.createEvent('HTMLEvents'), + inputElement = $input[0] + ; + if(inputElement) { + module.verbose('Triggering native change event'); + events.initEvent('change', true, false); + inputElement.dispatchEvent(events); + } + } + }, + determine: { selectAction: function(text, value) { module.verbose('Determining action', settings.action); if( $.isFunction( module.action[settings.action] ) ) { module.verbose('Triggering preset action', settings.action, text, value); - module.action[ settings.action ].call(this, text, value); + module.action[ settings.action ].call(element, text, value, this); } else if( $.isFunction(settings.action) ) { module.verbose('Triggering user action', settings.action, text, value); - settings.action.call(this, text, value); + settings.action.call(element, text, value, this); } else { module.error(error.action, settings.action); } }, eventInModule: function(event, callback) { + var + $target = $(event.target), + inDocument = ($target.closest(document.documentElement).length > 0), + inModule = ($target.closest($module).length > 0) + ; callback = $.isFunction(callback) ? callback : function(){} ; - if( $(event.target).closest($module).length === 0 ) { + if(inDocument && !inModule) { module.verbose('Triggering event', callback); callback(); return true; } else { @@ -1281,17 +1508,21 @@ return false; } }, eventOnElement: function(event, callback) { var - $target = $(event.target) + $target = $(event.target), + $label = $target.closest(selector.siblingLabel), + inVisibleDOM = document.body.contains(event.target), + notOnLabel = ($module.find($label).length === 0), + notInMenu = ($target.closest($menu).length === 0) ; callback = $.isFunction(callback) ? callback : function(){} ; - if($target.closest($menu).length === 0) { + if(inVisibleDOM && notOnLabel && notInMenu) { module.verbose('Triggering event', callback); callback(); return true; } else { @@ -1303,69 +1534,100 @@ action: { nothing: function() {}, - activate: function(text, value) { + activate: function(text, value, element) { value = (value !== undefined) ? value : text ; - module.set.selected(value, $(this)); - if(module.is.multiple() && !module.is.allFiltered()) { - return; + if( module.can.activate( $(element) ) ) { + module.set.selected(value, $(element)); + if(module.is.multiple() && !module.is.allFiltered()) { + return; + } + else { + module.hideAndClear(); + } } - else { - module.hideAndClear(); - } }, - select: function(text, value) { - // mimics action.activate but does not select text - module.action.activate.call(this); + select: function(text, value, element) { + value = (value !== undefined) + ? value + : text + ; + if( module.can.activate( $(element) ) ) { + module.set.value(value, $(element)); + if(module.is.multiple() && !module.is.allFiltered()) { + return; + } + else { + module.hideAndClear(); + } + } }, - combo: function(text, value) { + combo: function(text, value, element) { value = (value !== undefined) ? value : text ; - module.set.selected(value, $(this)); + module.set.selected(value, $(element)); module.hideAndClear(); }, - hide: function() { + hide: function(text, value, element) { + module.set.value(value, text); module.hideAndClear(); } }, get: { id: function() { return id; }, + defaultText: function() { + return $module.data(metadata.defaultText); + }, + defaultValue: function() { + return $module.data(metadata.defaultValue); + }, + placeholderText: function() { + return $module.data(metadata.placeholderText) || ''; + }, text: function() { return $text.text(); }, query: function() { return $.trim($search.val()); }, - searchWidth: function(characterCount) { - return (characterCount * settings.glyphWidth) + 'em'; + searchWidth: function(value) { + value = (value !== undefined) + ? value + : $search.val() + ; + $sizer.text(value); + // prevent rounding issues + return Math.ceil( $sizer.width() + 1); }, selectionCount: function() { var - values = module.get.values() + values = module.get.values(), + count ; - return ( module.is.multiple() ) + count = ( module.is.multiple() ) ? $.isArray(values) ? values.length : 0 : (module.get.value() !== '') ? 1 : 0 ; + return count; }, transition: function($subMenu) { return (settings.transition == 'auto') ? module.is.upward($subMenu) ? 'slide up' @@ -1408,40 +1670,32 @@ rangeLength = range.text.length; range.moveStart('character', -input.value.length); return range.text.length - rangeLength; } }, - shortcutKeys: function() { - return { - backspace : 8, - delimiter : 188, // comma - deleteKey : 46, - enter : 13, - escape : 27, - pageUp : 33, - pageDown : 34, - leftArrow : 37, - upArrow : 38, - rightArrow : 39, - downArrow : 40 - }; - }, value: function() { - return ($input.length > 0) - ? $input.val() - : $module.data(metadata.value) + var + value = ($input.length > 0) + ? $input.val() + : $module.data(metadata.value), + isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '') ; + // prevents placeholder element from being selected when multiple + return (value === undefined || isEmptyMultiselect) + ? '' + : value + ; }, values: function() { var value = module.get.value() ; if(value === '') { return ''; } - return (!$input.is('select') && module.is.multiple()) - ? typeof value == 'string' + return ( !module.has.selectInput() && module.is.multiple() ) + ? (typeof value == 'string') // delimited string ? value.split(settings.delimiter) : '' : value ; }, @@ -1452,20 +1706,21 @@ ; if(values) { if(typeof values == 'string') { values = [values]; } - remoteValues = {}; $.each(values, function(index, value) { var name = module.read.remoteData(value) ; module.verbose('Restoring value from session data', name, value); - remoteValues[value] = (name) - ? name - : value - ; + if(name) { + if(!remoteValues) { + remoteValues = {}; + } + remoteValues[value] = name; + } }); } return remoteValues; }, choiceText: function($choice, preserveHTML) { @@ -1473,33 +1728,33 @@ ? preserveHTML : settings.preserveHTML ; if($choice) { if($choice.find(selector.menu).length > 0) { - module.verbose('Retreiving text of element with sub-menu'); + module.verbose('Retrieving text of element with sub-menu'); $choice = $choice.clone(); $choice.find(selector.menu).remove(); $choice.find(selector.menuIcon).remove(); } return ($choice.data(metadata.text) !== undefined) ? $choice.data(metadata.text) : (preserveHTML) - ? $choice.html().trim() - : $choice.text().trim() + ? $.trim($choice.html()) + : $.trim($choice.text()) ; } }, choiceValue: function($choice, choiceText) { choiceText = choiceText || module.get.choiceText($choice); if(!$choice) { return false; } return ($choice.data(metadata.value) !== undefined) - ? $choice.data(metadata.value) + ? String( $choice.data(metadata.value) ) : (typeof choiceText === 'string') - ? choiceText.toLowerCase().trim() - : choiceText + ? $.trim(choiceText.toLowerCase()) + : String(choiceText) ; }, inputEvent: function() { var input = $search[0] @@ -1554,11 +1809,11 @@ ; }); module.debug('Retrieved and sorted values from select', select); } else { - module.debug('Retreived values from select', select); + module.debug('Retrieved values from select', select); } return select; }, activeItem: function() { return $item.filter('.' + className.active); @@ -1598,11 +1853,11 @@ ? module.get.values() : module.get.text() ; shouldSearch = (isMultiple) ? (value.length > 0) - : (value !== undefined && value !== '' && value !== null) + : (value !== undefined && value !== null) ; isMultiple = (module.is.multiple() && $.isArray(value)); strict = (value === '' || value === 0) ? true : strict || false @@ -1618,11 +1873,11 @@ // safe early exit if(optionValue === null || optionValue === undefined) { return; } if(isMultiple) { - if($.inArray(optionValue.toString(), value) !== -1 || $.inArray(optionText, value) !== -1) { + if($.inArray( String(optionValue), value) !== -1 || $.inArray(optionText, value) !== -1) { $selectedItem = ($selectedItem) ? $selectedItem.add($choice) : $choice ; } @@ -1633,11 +1888,11 @@ $selectedItem = $choice; return true; } } else { - if( optionValue.toString() == value.toString() || optionText == value) { + if( String(optionValue) == String(value) || optionText == value) { module.verbose('Found select item by value', optionValue, value); $selectedItem = $choice; return true; } } @@ -1655,12 +1910,14 @@ ? selectionCount : module.get.selectionCount() ; if(selectionCount >= settings.maxSelections) { module.debug('Maximum selection count reached'); - $item.addClass(className.filtered); - module.add.message(message.maxSelections); + if(settings.useLabels) { + $item.addClass(className.filtered); + module.add.message(message.maxSelections); + } return true; } else { module.verbose('No longer at maximum selection count'); module.remove.message(); @@ -1675,24 +1932,34 @@ } }, restore: { defaults: function() { + module.clear(); module.restore.defaultText(); module.restore.defaultValue(); }, defaultText: function() { var - defaultText = $module.data(metadata.defaultText) + defaultText = module.get.defaultText(), + placeholderText = module.get.placeholderText ; - module.debug('Restoring default text', defaultText); - module.set.text(defaultText); - $text.addClass(className.placeholder); + if(defaultText === placeholderText) { + module.debug('Restoring default placeholder text', defaultText); + module.set.placeholderText(defaultText); + } + else { + module.debug('Restoring default text', defaultText); + module.set.text(defaultText); + } }, + placeholderText: function() { + module.set.placeholderText(); + }, defaultValue: function() { var - defaultValue = $module.data(metadata.defaultValue) + defaultValue = module.get.defaultValue() ; if(defaultValue !== undefined) { module.debug('Restoring default value', defaultValue); if(defaultValue !== '') { module.set.value(defaultValue); @@ -1724,19 +1991,14 @@ else { module.debug('Restoring previously selected values'); } }, values: function() { - // prevents callbacks from occuring on initial load + // prevents callbacks from occurring on initial load module.set.initialLoad(); - if(settings.apiSettings) { - if(settings.saveRemoteData) { - module.restore.remoteValues(); - } - else { - module.clearValue(); - } + if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) { + module.restore.remoteValues(); } else { module.set.selected(); } module.remove.initialLoad(); @@ -1800,11 +2062,11 @@ }, placeholderText: function() { var text ; - if($text.hasClass(className.placeholder)) { + if(settings.placeholder !== false && $text.hasClass(className.placeholder)) { text = module.get.text(); module.verbose('Saving placeholder text as', text); $module.data(metadata.placeholderText, text); } }, @@ -1817,11 +2079,11 @@ sessionStorage.setItem(value, name); } }, clear: function() { - if(module.is.multiple()) { + if(module.is.multiple() && settings.useLabels) { module.remove.labels(); } else { module.remove.activeItem(); module.remove.selectedItem(); @@ -1834,12 +2096,12 @@ module.set.value(''); }, scrollPage: function(direction, $selectedItem) { var - $selectedItem = $selectedItem || module.get.selectedItem(), - $menu = $selectedItem.closest(selector.menu), + $currentItem = $selectedItem || module.get.selectedItem(), + $menu = $currentItem.closest(selector.menu), menuHeight = $menu.outerHeight(), currentScroll = $menu.scrollTop(), itemHeight = $item.eq(0).outerHeight(), itemsPerPage = Math.floor(menuHeight / itemHeight), maxScroll = $menu.prop('scrollHeight'), @@ -1850,12 +2112,12 @@ isWithinRange, $nextSelectedItem, elementIndex ; elementIndex = (direction == 'up') - ? $selectableItem.index($selectedItem) - itemsPerPage - : $selectableItem.index($selectedItem) + itemsPerPage + ? $selectableItem.index($currentItem) - itemsPerPage + : $selectableItem.index($currentItem) + itemsPerPage ; isWithinRange = (direction == 'up') ? (elementIndex >= 0) : (elementIndex < $selectableItem.length) ; @@ -1865,16 +2127,19 @@ ? $selectableItem.first() : $selectableItem.last() ; if($nextSelectedItem.length > 0) { module.debug('Scrolling page', direction, $nextSelectedItem); - $selectedItem + $currentItem .removeClass(className.selected) ; $nextSelectedItem .addClass(className.selected) ; + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextSelectedItem); + } $menu .scrollTop(newScroll) ; } }, @@ -1887,11 +2152,11 @@ isSearchMultiple = (isMultiple && isSearch), searchValue = (isSearch) ? module.get.query() : '', hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0), - searchWidth = module.get.searchWidth(searchValue.length), + searchWidth = module.get.searchWidth(), valueIsSet = searchValue !== '' ; if(isMultiple && hasSearchValue) { module.verbose('Adjusting input width', searchWidth, settings.glyphWidth); $search.css('width', searchWidth); @@ -1903,20 +2168,21 @@ else if(!isMultiple || (isSearchMultiple && !valueIsSet)) { module.verbose('Showing placeholder text'); $text.removeClass(className.filtered); } }, + empty: function() { + $module.addClass(className.empty); + }, loading: function() { $module.addClass(className.loading); }, placeholderText: function(text) { - text = text || $module.data(metadata.placeholderText); - if(text) { - module.debug('Restoring placeholder text'); - module.set.text(text); - $text.addClass(className.placeholder); - } + text = text || module.get.placeholderText(); + module.debug('Setting placeholder text', text); + module.set.text(text); + $text.addClass(className.placeholder); }, tabbable: function() { if( module.has.search() ) { module.debug('Added tabindex to searchable dropdown'); $search @@ -1927,11 +2193,11 @@ .attr('tabindex', -1) ; } else { module.debug('Added tabindex to dropdown'); - if(!$module.attr('tabindex') ) { + if( $module.attr('tabindex') === undefined) { $module .attr('tabindex', 0) ; $menu .attr('tabindex', -1) @@ -1941,10 +2207,24 @@ }, initialLoad: function() { module.verbose('Setting initial load'); initialLoad = true; }, + activeItem: function($item) { + if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) { + $item.addClass(className.filtered); + } + else { + $item.addClass(className.active); + } + }, + partialSearch: function(text) { + var + length = module.get.query().length + ; + $search.val( text.substr(0 , length)); + }, scrollPosition: function($item, forceScroll) { var edgeTolerance = 5, $menu, hasActive, @@ -1995,48 +2275,72 @@ else { $combo.text(text); } } else { + if(text !== module.get.placeholderText()) { + $text.removeClass(className.placeholder); + } module.debug('Changing text', text, $text); $text .removeClass(className.filtered) - .removeClass(className.placeholder) ; if(settings.preserveHTML) { $text.html(text); } else { $text.text(text); } } } }, + selectedItem: function($item) { + var + value = module.get.choiceValue($item), + text = module.get.choiceText($item, false) + ; + module.debug('Setting user selection to item', $item); + module.remove.activeItem(); + module.set.partialSearch(text); + module.set.activeItem($item); + module.set.selected(value, $item); + module.set.text(text); + }, selectedLetter: function(letter) { var - $selectedItem = $item.filter('.' + className.selected), - $nextValue = false + $selectedItem = $item.filter('.' + className.selected), + alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter), + $nextValue = false, + $nextItem ; - $item - .each(function(){ - var - $choice = $(this), - text = module.get.choiceText($choice, false), - firstLetter = String(text).charAt(0).toLowerCase(), - matchedLetter = letter.toLowerCase() - ; - if(firstLetter == matchedLetter) { - $nextValue = $choice; - return false; - } - }) - ; + // check next of same letter + if(alreadySelectedLetter) { + $nextItem = $selectedItem.nextAll($item).eq(0); + if( module.has.firstLetter($nextItem, letter) ) { + $nextValue = $nextItem; + } + } + // check all values + if(!$nextValue) { + $item + .each(function(){ + if(module.has.firstLetter($(this), letter)) { + $nextValue = $(this); + return false; + } + }) + ; + } + // set next value if($nextValue) { module.verbose('Scrolling to next value with letter', letter); module.set.scrollPosition($nextValue); $selectedItem.removeClass(className.selected); $nextValue.addClass(className.selected); + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextValue); + } } }, direction: function($menu) { if(settings.direction == 'auto') { if(module.is.onScreen($menu)) { @@ -2054,35 +2358,48 @@ var $element = $menu || $module; $element.addClass(className.upward); }, value: function(value, text, $selected) { var + escapedValue = module.escape.value(value), hasInput = ($input.length > 0), isAddition = !module.has.value(value), currentValue = module.get.values(), - stringValue = (typeof value == 'number') - ? value.toString() + stringValue = (value !== undefined) + ? String(value) : value, newValue ; if(hasInput) { - if(stringValue == currentValue) { + if(!settings.allowReselection && stringValue == currentValue) { module.verbose('Skipping value update already same value', value, currentValue); if(!module.is.initialLoad()) { return; } } - module.debug('Updating input value', value, currentValue); + + if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) { + module.debug('Adding user option', value); + module.add.optionValue(value); + } + module.debug('Updating input value', escapedValue, currentValue); + internalChange = true; $input - .val(value) - .trigger('change') + .val(escapedValue) ; + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.debug('Input native change event ignored on initial load'); + } + else { + module.trigger.change(); + } + internalChange = false; } else { - module.verbose('Storing value in metadata', value, $input); - if(value !== currentValue) { - $module.data(metadata.value, value); + module.verbose('Storing value in metadata', escapedValue, $input); + if(escapedValue !== currentValue) { + $module.data(metadata.value, stringValue); } } if(settings.fireOnInit === false && module.is.initialLoad()) { module.verbose('No callback on initial load', settings.onChange); } @@ -2099,10 +2416,15 @@ $module.addClass(className.multiple); }, visible: function() { $module.addClass(className.visible); }, + exactly: function(value, $selectedItem) { + module.debug('Setting selected to exact values'); + module.clear(); + module.set.selected(value, $selectedItem); + }, selected: function(value, $selectedItem) { var isMultiple = module.is.multiple(), $userSelectedItem ; @@ -2112,10 +2434,13 @@ ; if(!$selectedItem) { return; } module.debug('Setting selected menu item to', $selectedItem); + if(module.is.multiple()) { + module.remove.searchWidth(); + } if(module.is.single()) { module.remove.activeItem(); module.remove.selectedItem(); } else if(settings.useLabels) { @@ -2140,18 +2465,18 @@ module.save.remoteData(selectedText, selectedValue); } if(settings.useLabels) { module.add.value(selectedValue, selectedText, $selected); module.add.label(selectedValue, selectedText, shouldAnimate); - $selected.addClass(className.active); + module.set.activeItem($selected); module.filterActive(); module.select.nextAvailable($selectedItem); } else { module.add.value(selectedValue, selectedText, $selected); module.set.text(module.add.variables(message.count)); - $selected.addClass(className.active); + module.set.activeItem($selected); } } else if(!isFiltered) { module.debug('Selected active value, removing label'); module.remove.selected(selectedValue); @@ -2159,12 +2484,12 @@ } else { if(settings.apiSettings && settings.saveRemoteData) { module.save.remoteData(selectedText, selectedValue); } - module.set.value(selectedValue, selectedText, $selected); module.set.text(selectedText); + module.set.value(selectedValue, selectedText, $selected); $selected .addClass(className.active) .addClass(className.selected) ; } @@ -2177,21 +2502,22 @@ label: function(value, text, shouldAnimate) { var $next = module.is.searchSelection() ? $search : $text, + escapedValue = module.escape.value(value), $label ; $label = $('<a />') .addClass(className.label) - .attr('data-value', value) - .html(templates.label(value, text)) + .attr('data-value', escapedValue) + .html(templates.label(escapedValue, text)) ; - $label = settings.onLabelCreate.call($label, value, text); + $label = settings.onLabelCreate.call($label, escapedValue, text); if(module.has.label(value)) { - module.debug('Label already exists, skipping', value); + module.debug('Label already exists, skipping', escapedValue); return; } if(settings.label.variation) { $label.addClass(settings.label.variation); } @@ -2228,71 +2554,80 @@ ; } }, optionValue: function(value) { var - $option = $input.find('option[value="' + value + '"]'), - hasOption = ($option.length > 0) + escapedValue = module.escape.value(value), + $option = $input.find('option[value="' + escapedValue + '"]'), + hasOption = ($option.length > 0) ; if(hasOption) { return; } // temporarily disconnect observer - if(selectObserver) { - selectObserver.disconnect(); - module.verbose('Temporarily disconnecting mutation observer', value); + module.disconnect.selectObserver(); + if( module.is.single() ) { + module.verbose('Removing previous user addition'); + $input.find('option.' + className.addition).remove(); } $('<option/>') - .prop('value', value) + .prop('value', escapedValue) + .addClass(className.addition) .html(value) .appendTo($input) ; module.verbose('Adding user addition as an <option>', value); - if(selectObserver) { - selectObserver.observe($input[0], { - childList : true, - subtree : true - }); - } + module.observe.select(); }, userSuggestion: function(value) { var $addition = $menu.children(selector.addition), - alreadyHasValue = module.get.item(value), + $existingItem = module.get.item(value), + alreadyHasValue = $existingItem && $existingItem.not(selector.addition).length, hasUserSuggestion = $addition.length > 0, html ; - if(module.has.maxSelections()) { + if(settings.useLabels && module.has.maxSelections()) { return; } if(value === '' || alreadyHasValue) { $addition.remove(); return; } - $item - .removeClass(className.selected) - ; if(hasUserSuggestion) { - html = settings.templates.addition(value); $addition - .html(html) .data(metadata.value, value) + .data(metadata.text, value) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) .removeClass(className.filtered) - .addClass(className.selected) ; + if(!settings.hideAdditions) { + html = settings.templates.addition( module.add.variables(message.addResult, value) ); + $addition + .html(html) + ; + } module.verbose('Replacing user suggestion with new value', $addition); } else { $addition = module.create.userChoice(value); $addition .prependTo($menu) - .addClass(className.selected) ; module.verbose('Adding item choice to menu corresponding with user choice addition', $addition); } + if(!settings.hideAdditions || module.is.allFiltered()) { + $addition + .addClass(className.selected) + .siblings() + .removeClass(className.selected) + ; + } + module.refreshItems(); }, - variables: function(message) { + variables: function(message, term) { var hasCount = (message.search('{count}') !== -1), hasMaxCount = (message.search('{maxCount}') !== -1), hasTerm = (message.search('{term}') !== -1), values, @@ -2307,11 +2642,11 @@ if(hasMaxCount) { count = module.get.selectionCount(); message = message.replace('{maxCount}', settings.maxSelections); } if(hasTerm) { - query = module.get.query(); + query = term || module.get.query(); message = message.replace('{term}', query); } return message; }, value: function(addedValue, addedText, $selectedItem) { @@ -2321,32 +2656,32 @@ ; if(addedValue === '') { module.debug('Cannot select blank values from multiselect'); return; } - // extend currently array + // extend current array if($.isArray(currentValue)) { newValue = currentValue.concat([addedValue]); newValue = module.get.uniqueArray(newValue); } else { newValue = [addedValue]; } // add values - if( $input.is('select')) { - if(settings.allowAdditions) { - module.add.optionValue(addedValue); + if( module.has.selectInput() ) { + if(module.can.extendSelect()) { module.debug('Adding value to select', addedValue, newValue, $input); + module.add.optionValue(addedValue); } } else { newValue = newValue.join(settings.delimiter); module.debug('Setting hidden input to delimited value', newValue, $input); } if(settings.fireOnInit === false && module.is.initialLoad()) { - module.verbose('No callback on initial load', settings.onAdd); + module.verbose('Skipping onadd callback on initial load', settings.onAdd); } else { settings.onAdd.call(element, addedValue, addedText, $selectedItem); } module.set.value(newValue, addedValue, addedText, $selectedItem); @@ -2359,10 +2694,13 @@ $module.removeClass(className.active); }, activeLabel: function() { $module.find(selector.label).removeClass(className.active); }, + empty: function() { + $module.removeClass(className.empty); + }, loading: function() { $module.removeClass(className.loading); }, initialLoad: function() { initialLoad = false; @@ -2376,28 +2714,58 @@ }, activeItem: function() { $item.removeClass(className.active); }, filteredItem: function() { - if( module.has.maxSelections() ) { + if(settings.useLabels && module.has.maxSelections() ) { return; } - if(settings.useLabels) { + if(settings.useLabels && module.is.multiple()) { $item.not('.' + className.active).removeClass(className.filtered); } else { $item.removeClass(className.filtered); } + module.remove.empty(); }, + optionValue: function(value) { + var + escapedValue = module.escape.value(value), + $option = $input.find('option[value="' + escapedValue + '"]'), + hasOption = ($option.length > 0) + ; + if(!hasOption || !$option.hasClass(className.addition)) { + return; + } + // temporarily disconnect observer + if(selectObserver) { + selectObserver.disconnect(); + module.verbose('Temporarily disconnecting mutation observer'); + } + $option.remove(); + module.verbose('Removing user addition as an <option>', escapedValue); + if(selectObserver) { + selectObserver.observe($input[0], { + childList : true, + subtree : true + }); + } + }, message: function() { $menu.children(selector.message).remove(); }, + searchWidth: function() { + $search.css('width', ''); + }, searchTerm: function() { module.verbose('Cleared search term'); $search.val(''); module.set.filtered(); }, + userAddition: function() { + $item.filter(selector.addition).remove(); + }, selected: function(value, $selectedItem) { $selectedItem = (settings.allowAdditions) ? $selectedItem || module.get.itemWithAdditions(value) : $selectedItem || module.get.item(value) ; @@ -2418,11 +2786,16 @@ module.remove.value(selectedValue, selectedText, $selected); module.remove.label(selectedValue); } else { module.remove.value(selectedValue, selectedText, $selected); - module.set.text(module.add.variables(message.count)); + if(module.get.selectionCount() === 0) { + module.set.placeholderText(); + } + else { + module.set.text(module.add.variables(message.count)); + } } } else { module.remove.value(selectedValue, selectedText, $selected); } @@ -2439,20 +2812,20 @@ selectedItem: function() { $item.removeClass(className.selected); }, value: function(removedValue, removedText, $removedItem) { var - values = $input.val(), + values = module.get.values(), newValue ; - if( $input.is('select') ) { + if( module.has.selectInput() ) { module.verbose('Input is <select> removing selected option', removedValue); newValue = module.remove.arrayValue(removedValue, values); + module.remove.optionValue(removedValue); } else { module.verbose('Removing from delimited values', removedValue); - values = values.split(settings.delimiter); newValue = module.remove.arrayValue(removedValue, values); newValue = newValue.join(settings.delimiter); } if(settings.fireOnInit === false && module.is.initialLoad()) { module.verbose('No callback on initial load', settings.onRemove); @@ -2462,36 +2835,26 @@ } module.set.value(newValue, removedText, $removedItem); module.check.maxSelections(); }, arrayValue: function(removedValue, values) { + if( !$.isArray(values) ) { + values = [values]; + } values = $.grep(values, function(value){ return (removedValue != value); }); module.verbose('Removed value from delimited string', removedValue, values); return values; }, - label: function(value) { + label: function(value, shouldAnimate) { var $labels = $module.find(selector.label), - $removedLabel = $labels.filter('[data-value="' + value +'"]'), - labelCount = $labels.length, - isLastLabel = ($labels.index($removedLabel) + 1 == labelCount), - shouldAnimate = ( (!module.is.searchSelection() || !module.is.focusedOnSearch()) && isLastLabel) + $removedLabel = $labels.filter('[data-value="' + value +'"]') ; - if(shouldAnimate) { - module.verbose('Animating and removing label', $removedLabel); - $removedLabel - .transition(settings.label.transition, settings.label.duration, function() { - $removedLabel.remove(); - }) - ; - } - else { - module.verbose('Removing label', $removedLabel); - $removedLabel.remove(); - } + module.verbose('Removing label', $removedLabel); + $removedLabel.remove(); }, activeLabels: function($activeLabels) { $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active); module.verbose('Removing active label selections', $activeLabels); module.remove.labels($activeLabels); @@ -2500,71 +2863,125 @@ $labels = $labels || $module.find(selector.label); module.verbose('Removing labels', $labels); $labels .each(function(){ var - value = $(this).data('value'), - isUserValue = module.is.userValue(value) + $label = $(this), + value = $label.data(metadata.value), + stringValue = (value !== undefined) + ? String(value) + : value, + isUserValue = module.is.userValue(stringValue) ; + if(settings.onLabelRemove.call($label, value) === false) { + module.debug('Label remove callback cancelled removal'); + return; + } + module.remove.message(); if(isUserValue) { - module.remove.value(value); - module.remove.label(value); + module.remove.value(stringValue); + module.remove.label(stringValue); } else { // selected will also remove label - module.remove.selected(value); + module.remove.selected(stringValue); } }) ; }, tabbable: function() { if( module.has.search() ) { module.debug('Searchable dropdown initialized'); $search - .attr('tabindex', '-1') + .removeAttr('tabindex') ; $menu - .attr('tabindex', '-1') + .removeAttr('tabindex') ; } else { module.debug('Simple selection dropdown initialized'); $module - .attr('tabindex', '-1') + .removeAttr('tabindex') ; $menu - .attr('tabindex', '-1') + .removeAttr('tabindex') ; } } }, has: { + menuSearch: function() { + return (module.has.search() && $search.closest($menu).length > 0); + }, search: function() { return ($search.length > 0); }, + sizer: function() { + return ($sizer.length > 0); + }, + selectInput: function() { + return ( $input.is('select') ); + }, + minCharacters: function(searchTerm) { + if(settings.minCharacters) { + searchTerm = (searchTerm !== undefined) + ? String(searchTerm) + : String(module.get.query()) + ; + return (searchTerm.length >= settings.minCharacters); + } + return true; + }, + firstLetter: function($item, letter) { + var + text, + firstLetter + ; + if(!$item || $item.length === 0 || typeof letter !== 'string') { + return false; + } + text = module.get.choiceText($item, false); + letter = letter.toLowerCase(); + firstLetter = String(text).charAt(0).toLowerCase(); + return (letter == firstLetter); + }, input: function() { return ($input.length > 0); }, + items: function() { + return ($item.length > 0); + }, menu: function() { return ($menu.length > 0); }, message: function() { return ($menu.children(selector.message).length !== 0); }, label: function(value) { var - $labels = $module.find(selector.label) + escapedValue = module.escape.value(value), + $labels = $module.find(selector.label) ; - return ($labels.filter('[data-value="' + value +'"]').length > 0); + return ($labels.filter('[data-value="' + escapedValue +'"]').length > 0); }, maxSelections: function() { return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections); }, allResultsFiltered: function() { - return ($item.filter(selector.unselectable).length === $item.length); + var + $normalResults = $item.not(selector.addition) + ; + return ($normalResults.filter(selector.unselectable).length === $normalResults.length); }, + userSuggestion: function() { + return ($menu.children(selector.addition).length > 0); + }, + query: function() { + return (module.get.query() !== ''); + }, value: function(value) { var values = module.get.values(), hasValue = $.isArray(values) ? values && ($.inArray(value, values) !== -1) @@ -2579,27 +2996,36 @@ is: { active: function() { return $module.hasClass(className.active); }, + bubbledLabelClick: function(event) { + return $(event.target).is('select, input') && $module.closest('label').length > 0; + }, + bubbledIconClick: function(event) { + return $(event.target).closest($icon).length > 0; + }, alreadySetup: function() { return ($module.is('select') && $module.parent(selector.dropdown).length > 0 && $module.prev().length === 0); }, animating: function($subMenu) { return ($subMenu) ? $subMenu.transition && $subMenu.transition('is animating') : $menu.transition && $menu.transition('is animating') ; }, + disabled: function() { + return $module.hasClass(className.disabled); + }, focused: function() { return (document.activeElement === $module[0]); }, focusedOnSearch: function() { return (document.activeElement === $search[0]); }, allFiltered: function() { - return( (module.is.multiple() || module.has.search()) && !module.has.message() && module.has.allResultsFiltered() ); + return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() ); }, hidden: function($subMenu) { return !module.is.visible($subMenu); }, initialLoad: function() { @@ -2674,11 +3100,11 @@ }, search: function() { return $module.hasClass(className.search); }, searchSelection: function() { - return ( module.has.search() && $search.closest(selector.menu).length === 0 ); + return ( module.has.search() && $search.parent(selector.dropdown).length === 1 ); }, selection: function() { return $module.hasClass(className.selection); }, userValue: function(value) { @@ -2695,15 +3121,30 @@ ; } }, can: { + activate: function($item) { + if(settings.useLabels) { + return true; + } + if(!module.has.maxSelections()) { + return true; + } + if(module.has.maxSelections() && $item.hasClass(className.active)) { + return true; + } + return false; + }, click: function() { return (hasTouch || settings.on == 'click'); }, + extendSelect: function() { + return settings.allowAdditions || settings.apiSettings; + }, show: function() { - return !$module.hasClass(className.disabled) && $item.length > 0; + return !module.is.disabled() && (module.has.items() || module.has.message()); }, useAPI: function() { return $.fn.api !== undefined; } }, @@ -2809,12 +3250,15 @@ } } }, hideAndClear: function() { + module.remove.searchTerm(); + if( module.has.maxSelections() ) { + return; + } if(module.has.search()) { - module.remove.searchTerm(); module.hide(function() { module.remove.filteredItem(); }); } else { @@ -2834,10 +3278,30 @@ module.timer = setTimeout(module.hide, settings.delay.hide); } }, escape: { + value: function(value) { + var + multipleValues = $.isArray(value), + stringValue = (typeof value === 'string'), + isUnparsable = (!stringValue && !multipleValues), + hasQuotes = (stringValue && value.search(regExp.quote) !== -1), + values = [] + ; + if(!module.has.selectInput() || isUnparsable || !hasQuotes) { + return value; + } + module.debug('Encoding quote values for use in select', value); + if(multipleValues) { + $.each(value, function(index, value){ + values.push(value.replace(regExp.quote, '&quot;')); + }); + return values; + } + return value.replace(regExp.quote, '&quot;'); + }, regExp: function(text) { text = String(text); return text.replace(regExp.escape, '\\$&'); } }, @@ -2846,11 +3310,16 @@ module.debug('Changing setting', name, value); if( $.isPlainObject(name) ) { $.extend(true, settings, name); } else if(value !== undefined) { - settings[name] = value; + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } } else { return settings[name]; } }, @@ -2864,34 +3333,36 @@ else { return module[name]; } }, debug: function() { - if(settings.debug) { + if(!settings.silent && settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.debug.apply(console, arguments); } } }, verbose: function() { - if(settings.verbose && settings.debug) { + if(!settings.silent && settings.verbose && settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); module.verbose.apply(console, arguments); } } }, error: function() { - module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); - module.error.apply(console, arguments); + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } }, performance: { log: function(message) { var currentTime, @@ -3018,50 +3489,56 @@ ; }; $.fn.dropdown.settings = { + silent : false, debug : false, verbose : false, performance : true, on : 'click', // what event should show menu action on item selection action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){}) apiSettings : false, - saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh - throttle : 200, // How long to wait after last user input to search remotely + selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used + minCharacters : 0, // Minimum characters required to trigger API call + saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh + throttle : 200, // How long to wait after last user input to search remotely - context : window, // Context to use when determining if on screen + context : window, // Context to use when determining if on screen direction : 'auto', // Whether dropdown should always open in one direction keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing match : 'both', // what to match against with search selection (both, text, or label) - fullTextSearch : false, // search anywhere in value + fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches) placeholder : 'auto', // whether to convert blank <select> values to placeholder text preserveHTML : true, // preserve html when selecting value sortSelect : false, // sort selection on init forceSelection : true, // force a choice on blur with search selection + allowAdditions : false, // whether multiple select should allow user added values + hideAdditions : true, // whether or not to hide special message prompting a user they can enter a value maxSelections : false, // When set to a number limits the number of selections to this count useLabels : true, // whether multiple select should filter currently active selections from choices - delimiter : ',', // when multiselect uses normal <input> the values will be delmited with this character + delimiter : ',', // when multiselect uses normal <input> the values will be delimited with this character showOnFocus : true, // show menu on focus + allowReselection : false, // whether current value should trigger callbacks when reselected allowTab : true, // add tabindex to element allowCategorySelection : false, // allow elements with sub-menus to be selected fireOnInit : false, // Whether callbacks should fire when initializing dropdown values transition : 'auto', // auto transition will slide down or up based on direction duration : 200, // duration of transition - glyphWidth : 1.0714, // widest glyph width in em (W is 1.0714 em) used to calculate multiselect input width + glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width // label settings on multi-select label: { transition : 'scale', duration : 200, @@ -3081,10 +3558,11 @@ onAdd : function(value, text, $selected){}, onRemove : function(value, text, $selected){}, onLabelSelect : function($selectedLabels){}, onLabelCreate : function(value, text) { return $(this); }, + onLabelRemove : function(value) { return true; }, onNoResults : function(searchTerm) { return true; }, onShow : function(){}, onHide : function(){}, /* Component */ @@ -3098,63 +3576,93 @@ noResults : 'No results found.', serverError : 'There was an error contacting the server' }, error : { - action : 'You called a dropdown action that was not defined', - alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown', - labels : 'Allowing user additions currently requires the use of labels.', - method : 'The method you called is not defined.', - noAPI : 'The API module is required to load resources remotely', - noStorage : 'Saving remote data requires session storage', - noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>' + action : 'You called a dropdown action that was not defined', + alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown', + labels : 'Allowing user additions currently requires the use of labels.', + missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values', + method : 'The method you called is not defined.', + noAPI : 'The API module is required to load resources remotely', + noStorage : 'Saving remote data requires session storage', + noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>' }, regExp : { escape : /[-[\]{}()*+?.,\\^$|#\s]/g, + quote : /"/g }, metadata : { defaultText : 'defaultText', defaultValue : 'defaultValue', placeholderText : 'placeholder', text : 'text', value : 'value' }, + // property names for remote query + fields: { + remoteValues : 'results', // grouping for api results + values : 'values', // grouping for all dropdown values + disabled : 'disabled', // whether value should be disabled + name : 'name', // displayed dropdown text + value : 'value', // actual dropdown value + text : 'text' // displayed text when selected + }, + + keys : { + backspace : 8, + delimiter : 188, // comma + deleteKey : 46, + enter : 13, + escape : 27, + pageUp : 33, + pageDown : 34, + leftArrow : 37, + upArrow : 38, + rightArrow : 39, + downArrow : 40 + }, + selector : { addition : '.addition', dropdown : '.ui.dropdown', + hidden : '.hidden', icon : '> .dropdown.icon', input : '> input[type="hidden"], > select', item : '.item', label : '> .label', remove : '> .label > .delete.icon', siblingLabel : '.label', menu : '.menu', message : '.message', menuIcon : '.dropdown.icon', - search : 'input.search, .menu > .search > input', + search : 'input.search, .menu > .search > input, .menu input.search', + sizer : '> input.sizer', text : '> .text:not(.icon)', unselectable : '.disabled, .filtered' }, className : { active : 'active', addition : 'addition', animating : 'animating', disabled : 'disabled', + empty : 'empty', dropdown : 'ui dropdown', filtered : 'filtered', hidden : 'hidden transition', item : 'item', label : 'ui label', loading : 'loading', menu : 'menu', message : 'message', multiple : 'multiple', placeholder : 'default', + sizer : 'sizer', search : 'search', selected : 'selected', selection : 'selection', upward : 'upward', visible : 'visible' @@ -3189,17 +3697,27 @@ html += '</div>'; return html; }, // generates just menu from select - menu: function(response) { + menu: function(response, fields) { var - values = response.values || {}, + values = response[fields.values] || {}, html = '' ; - $.each(response.values, function(index, option) { - html += '<div class="item" data-value="' + option.value + '">' + option.name + '</div>'; + $.each(values, function(index, option) { + var + maybeText = (option[fields.text]) + ? 'data-text="' + option[fields.text] + '"' + : '', + maybeDisabled = (option[fields.disabled]) + ? 'disabled ' + : '' + ; + html += '<div class="'+ maybeDisabled +'item" data-value="' + option[fields.value] + '"' + maybeText + '>' + html += option[fields.name]; + html += '</div>'; }); return html; }, // generates label for multiselect @@ -3218,6 +3736,6 @@ return choice; } }; -})( jQuery, window , document ); +})( jQuery, window, document );