assets/javascripts/semantic_ui/definitions/modules/dropdown.js in less-rails-semantic_ui-2.1.8.2 vs assets/javascripts/semantic_ui/definitions/modules/dropdown.js in less-rails-semantic_ui-2.2.1.0

- old
+ new

@@ -1,20 +1,26 @@ /*! * # Semantic UI - 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), @@ -53,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) @@ -71,10 +78,11 @@ element = this, instance = $module.data(moduleNamespace), initialLoad, pageLostFocus, + willRefocus, elementNamespace, id, selectObserver, menuObserver, module @@ -123,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(menuObserver) { + menuObserver.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() { @@ -188,10 +207,13 @@ .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); @@ -214,28 +236,40 @@ 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) { @@ -258,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 @@ -283,10 +321,13 @@ .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(); } }, select: function() { @@ -322,12 +363,12 @@ } if($input.is('[multiple]')) { module.set.multiple(); } if ($input.prop('disabled')) { - module.debug('Disabling dropdown') - $module.addClass(className.disabled) + module.debug('Disabling dropdown'); + $module.addClass(className.disabled); } $input .removeAttr('class') .detach() .prependTo($module) @@ -364,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); @@ -384,10 +429,18 @@ 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) ; @@ -408,21 +461,24 @@ ? callback : function(){} ; if( module.can.show() && !module.is.active() ) { module.debug('Showing dropdown'); - if(module.is.multiple() && !module.has.search() && module.is.allFiltered()) { - return true; - } if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) { module.remove.message(); } + if(module.is.allFiltered()) { + return true; + } if(settings.onShow.call(element) !== false) { module.animate.show(function() { if( module.can.click() ) { module.bind.intent(); } + if(module.has.menuSearch()) { + module.focusSearch(); + } module.set.visible(); callback.call(element); }); } } @@ -521,15 +577,17 @@ .on('click' + eventNamespace, selector.remove, module.event.remove.click) ; } if( module.is.searchSelection() ) { $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) .on('click' + eventNamespace, selector.icon, module.event.icon.click) - .on('click' + eventNamespace, selector.search, module.show) .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 @@ -607,21 +665,29 @@ 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); } @@ -650,13 +716,14 @@ }, queryRemote: function(query, callback) { var apiSettings = { - errorDuration : false, - throttle : settings.throttle, - urlData : { + errorDuration : false, + cache : 'local', + throttle : settings.throttle, + urlData : { query: query }, onError: function() { module.add.message(message.serverError); callback(); @@ -709,14 +776,18 @@ text = String(module.get.choiceText($choice, false)); if(text.search(beginsWithRegExp) !== -1) { results.push(this); return true; } - else if(settings.fullTextSearch && module.fuzzySearch(searchTerm, text)) { + 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) { @@ -765,45 +836,62 @@ } 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( module.has.query() ) { - if(hasSelected) { - module.debug('Forcing partial selection to selected item', $selectedItem); - module.event.item.click.call($selectedItem); - return; + if(hasSelected) { + module.debug('Forcing partial selection to selected item', $selectedItem); + module.event.item.click.call($selectedItem, {}, true); + return; + } + else { + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); } else { module.remove.searchTerm(); } } - module.hide(); }, event: { change: function() { if(!internalChange) { @@ -814,63 +902,71 @@ 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) && !module.is.focusedOnSearch()) { - module.focusSearch(); - } - }, blur: function(event) { pageLostFocus = (document.activeElement === this); if(!activated && !pageLostFocus) { module.remove.activeLabel(); module.hide(); } }, - // prevents focus callback from occurring 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.search(); - module.show(); } }, 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.has.query()) { + module.forceSelection(); + } module.hide(); } - else if(settings.forceSelection) { - module.forceSelection(); - } - else { - module.hide(); - } } - else if(pageLostFocus) { - if(settings.forceSelection) { - module.forceSelection(); - } - } + willRefocus = false; } }, icon: { click: function(event) { module.toggle(); @@ -936,10 +1032,13 @@ var toggleBehavior = (module.is.multiple()) ? module.show : module.toggle ; + if(module.is.bubbledLabelClick(event)) { + return; + } if( module.determine.eventOnElement(event, toggleBehavior) ) { event.preventDefault(); } }, touch: function(event) { @@ -957,32 +1056,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) { @@ -991,17 +1122,15 @@ ; 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); } }, - touchend: function() { - }, - click: function (event) { + click: function (event, skipRefocus) { var $choice = $(this), $target = (event) ? $(event.target) : $(''), @@ -1010,13 +1139,21 @@ value = module.get.choiceValue($choice, text), hasSubMenu = ($subMenu.length > 0), isBubbledEvent = ($subMenu.find($target).length > 0) ; if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) { + if(module.is.searchSelection()) { + if(settings.allowAdditions) { + module.remove.userAddition(); + } + module.remove.searchTerm(); + if(!module.is.focusedOnSearch() && !(skipRefocus == true)) { + module.focusSearch(true); + } + } if(!settings.useLabels) { module.remove.filteredItem(); - module.remove.searchTerm(); module.set.scrollPosition($choice); } module.determine.selectAction.call(this, text, value); } } @@ -1148,23 +1285,33 @@ $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), - selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0), - delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()), + $subMenu = $selectedItem.children(selector.menu), + $parentMenu = $selectedItem.closest(selector.menu), + inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0), + hasSubMenu = ($subMenu.length> 0), + hasSelectedItem = ($selectedItem.length > 0), + selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0), + delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()), + isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable), $nextItem, isSubMenuItem, newIndex ; + // allow selection with menu closed + if(isAdditionWithoutMenu) { + module.verbose('Selecting item from keyboard shortcut', $selectedItem); + module.event.item.click.call($selectedItem, event); + if(module.is.searchSelection()) { + module.remove.searchTerm(); + } + } + // visible menu keyboard shortcuts if( module.is.visible() ) { // enter (select or open sub-menu) if(pressedKey == keys.enter || delimiterPressed) { @@ -1180,42 +1327,45 @@ } } 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) { @@ -1235,10 +1385,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) @@ -1259,10 +1412,14 @@ ; $nextItem .addClass(className.selected) ; module.set.scrollPosition($nextItem); + if(settings.selectOnKeydown && module.is.single()) { + module.set.activeItem($nextItem); + module.set.selected(module.get.choiceValue($nextItem), $nextItem); + } } event.preventDefault(); } // page down (show next page) @@ -1286,19 +1443,20 @@ // delimiter key 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) ); } } } }, @@ -1320,15 +1478,15 @@ 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); } }, @@ -1352,20 +1510,21 @@ return false; } }, eventOnElement: function(event, callback) { var - $target = $(event.target), - $label = $target.closest(selector.siblingLabel), - notOnLabel = ($module.find($label).length === 0), - notInMenu = ($target.closest($menu).length === 0) + $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(notOnLabel && notInMenu) { + if(inVisibleDOM && notOnLabel && notInMenu) { module.verbose('Triggering event', callback); callback(); return true; } else { @@ -1377,42 +1536,42 @@ action: { nothing: function() {}, - activate: function(text, value) { + activate: function(text, value, element) { value = (value !== undefined) ? value : text ; - if( module.can.activate( $(this) ) ) { - module.set.selected(value, $(this)); + if( module.can.activate( $(element) ) ) { + module.set.selected(value, $(element)); if(module.is.multiple() && !module.is.allFiltered()) { return; } else { module.hideAndClear(); } } }, - select: function(text, value) { + select: function(text, value, element) { // mimics action.activate but does not select text - module.action.activate.call(this); + module.action.activate.call(element); }, - 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(text, value) { - module.set.value(value); + hide: function(text, value, element) { + module.set.value(value, text); module.hideAndClear(); } }, @@ -1433,12 +1592,18 @@ 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(), count @@ -1500,17 +1665,18 @@ }, value: function() { var value = ($input.length > 0) ? $input.val() - : $module.data(metadata.value) + : $module.data(metadata.value), + isEmptyMultiselect = ($.isArray(value) && value.length === 1 && value[0] === '') ; // prevents placeholder element from being selected when multiple - if($.isArray(value) && value.length === 1 && value[0] === '') { - return ''; - } - return value; + return (value === undefined || isEmptyMultiselect) + ? '' + : value + ; }, values: function() { var value = module.get.value() ; @@ -1531,20 +1697,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) { @@ -1552,11 +1719,11 @@ ? 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) @@ -1633,11 +1800,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); @@ -1774,10 +1941,13 @@ else { module.debug('Restoring default text', defaultText); module.set.text(defaultText); } }, + placeholderText: function() { + module.set.placeholderText(); + }, defaultValue: function() { var defaultValue = module.get.defaultValue() ; if(defaultValue !== undefined) { @@ -1812,19 +1982,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(); @@ -1959,10 +2124,13 @@ .removeClass(className.selected) ; $nextSelectedItem .addClass(className.selected) ; + if(settings.selectOnKeydown && module.is.single()) { + module.set.selectedItem($nextSelectedItem); + } $menu .scrollTop(newScroll) ; } }, @@ -1975,11 +2143,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); @@ -1991,10 +2159,13 @@ else if(!isMultiple || (isSearchMultiple && !valueIsSet)) { module.verbose('Showing placeholder text'); $text.removeClass(className.filtered); } }, + empty: function() { + $module.addClass(className.empty); + }, loading: function() { $module.addClass(className.loading); }, placeholderText: function(text) { text = text || module.get.placeholderText(); @@ -2105,10 +2276,16 @@ $text.text(text); } } } }, + selectedItem: function($item) { + module.debug('Setting user selection to item', $item); + module.remove.activeItem(); + module.set.activeItem($item); + module.set.selected(module.get.choiceValue($item), $item); + }, selectedLetter: function(letter) { var $selectedItem = $item.filter('.' + className.selected), alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter), $nextValue = false, @@ -2136,10 +2313,13 @@ 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)) { @@ -2157,46 +2337,47 @@ var $element = $menu || $module; $element.addClass(className.upward); }, value: function(value, text, $selected) { var + escapedValue = module.escape.value(value), hasInput = ($input.length > 0), isAddition = !module.has.value(value), currentValue = module.get.values(), stringValue = (value !== undefined) ? String(value) : value, newValue ; if(hasInput) { - if(stringValue == currentValue) { + if(!settings.allowReselection && stringValue == currentValue) { module.verbose('Skipping value update already same value', value, currentValue); if(!module.is.initialLoad()) { return; } } if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) { module.debug('Adding user option', value); module.add.optionValue(value); } - module.debug('Updating input value', value, currentValue); + module.debug('Updating input value', escapedValue, currentValue); internalChange = true; $input - .val(value) + .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.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); @@ -2232,10 +2413,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) { @@ -2297,21 +2481,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); } @@ -2348,38 +2533,31 @@ ; } }, 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), $existingItem = module.get.item(value), @@ -2392,32 +2570,41 @@ } if(value === '' || alreadyHasValue) { $addition.remove(); return; } - $item - .removeClass(className.selected) - ; if(hasUserSuggestion) { - html = settings.templates.addition( module.add.variables(message.addResult, 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, term) { var hasCount = (message.search('{count}') !== -1), hasMaxCount = (message.search('{maxCount}') !== -1), @@ -2486,10 +2673,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; @@ -2512,41 +2702,49 @@ $item.not('.' + className.active).removeClass(className.filtered); } else { $item.removeClass(className.filtered); } + module.remove.empty(); }, 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 || !$option.hasClass(className.addition)) { return; } // temporarily disconnect observer if(selectObserver) { selectObserver.disconnect(); - module.verbose('Temporarily disconnecting mutation observer', value); + module.verbose('Temporarily disconnecting mutation observer'); } $option.remove(); - module.verbose('Removing user addition as an <option>', value); + 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) ; @@ -2655,10 +2853,11 @@ ; if(settings.onLabelRemove.call($label, value) === false) { module.debug('Label remove callback cancelled removal'); return; } + module.remove.message(); if(isUserValue) { module.remove.value(stringValue); module.remove.label(stringValue); } else { @@ -2689,16 +2888,32 @@ } } }, 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 ; @@ -2722,20 +2937,27 @@ 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 @@ -2753,10 +2975,13 @@ is: { active: function() { return $module.hasClass(className.active); }, + bubbledLabelClick: function(event) { + return $(event.target).is('select, input') && $module.closest('label').length > 0; + }, alreadySetup: function() { return ($module.is('select') && $module.parent(selector.dropdown).length > 0 && $module.prev().length === 0); }, animating: function($subMenu) { return ($subMenu) @@ -2772,11 +2997,11 @@ }, 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() { @@ -3029,10 +3254,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, '\\$&'); } }, @@ -3041,11 +3286,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]; } }, @@ -3059,34 +3309,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, @@ -3213,50 +3465,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 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, @@ -3306,10 +3564,11 @@ noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>' }, regExp : { escape : /[-[\]{}()*+?.,\\^$|#\s]/g, + quote : /"/g }, metadata : { defaultText : 'defaultText', defaultValue : 'defaultValue', @@ -3318,14 +3577,16 @@ value : 'value' }, // property names for remote query fields: { - remoteValues : 'results', // grouping for api results - values : 'values', // grouping for all dropdown values - name : 'name', // displayed dropdown text - value : 'value' // actual dropdown value + 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 @@ -3341,39 +3602,43 @@ }, 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' @@ -3414,10 +3679,20 @@ var values = response[fields.values] || {}, html = '' ; $.each(values, function(index, option) { - html += '<div class="item" data-value="' + option[fields.value] + '">' + option[fields.name] + '</div>'; + 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