UNPKG

fomantic-ui

Version:

Fomantic empowers designers and developers by creating a shared vocabulary for UI.

1,044 lines (978 loc) • 217 kB
/*! * # Fomantic-UI 2.9.3 - Dropdown * https://github.com/fomantic/Fomantic-UI/ * * * Released under the MIT license * https://opensource.org/licenses/MIT * */ (function ($, window, document) { 'use strict'; function isFunction(obj) { return typeof obj === 'function' && typeof obj.nodeType !== 'number'; } window = window !== undefined && window.Math === Math ? window : globalThis; $.fn.dropdown = function (parameters) { var $allModules = $(this), $document = $(document), time = Date.now(), performance = [], query = arguments[0], methodInvoked = typeof query === 'string', queryArguments = [].slice.call(arguments, 1), contextCheck = function (context, win) { var $context; if ([window, document].indexOf(context) >= 0) { $context = $(context); } else { $context = $(win.document).find(context); if ($context.length === 0) { $context = win.frameElement ? contextCheck(context, win.parent) : window; } } return $context; }, returnedValue ; $allModules.each(function (elementIndex) { var settings = $.isPlainObject(parameters) ? $.extend(true, {}, $.fn.dropdown.settings, parameters) : $.extend({}, $.fn.dropdown.settings), className = settings.className, message = settings.message, fields = settings.fields, keys = settings.keys, metadata = settings.metadata, namespace = settings.namespace, regExp = settings.regExp, selector = settings.selector, error = settings.error, templates = settings.templates, eventNamespace = '.' + namespace, moduleNamespace = 'module-' + namespace, $module = $(this), $context = contextCheck(settings.context, window), $text = $module.find(selector.text), $search = $module.find(selector.search), $sizer = $module.find(selector.sizer), $input = $module.find(selector.input), $icon = $module.find(selector.icon), $clear = $module.find(selector.clearIcon), $combo = $module.prev().find(selector.text).length > 0 ? $module.prev().find(selector.text) : $module.prev(), $menu = $module.children(selector.menu), $item = $menu.find(selector.item), $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(), activated = false, itemActivated = false, internalChange = false, iconClicked = false, element = this, focused = false, instance = $module.data(moduleNamespace), selectActionActive, initialLoad, pageLostFocus, willRefocus, elementNamespace, id, selectObserver, menuObserver, classObserver, module, tempDisableApiCache = false ; module = { initialize: function () { module.debug('Initializing dropdown', settings); if (module.is.alreadySetup()) { module.setup.reference(); } else { if (settings.ignoreDiacritics && !String.prototype.normalize) { settings.ignoreDiacritics = false; module.error(error.noNormalize, element); } module.create.id(); module.setup.layout(); if (settings.values) { module.set.initialLoad(); module.change.values(settings.values); module.remove.initialLoad(); } module.refreshData(); module.save.defaults(); module.restore.selected(); module.bind.events(); module.observeChanges(); module.instantiate(); } }, instantiate: function () { module.verbose('Storing instance of dropdown', module); instance = module; $module .data(moduleNamespace, module) ; }, destroy: function () { module.verbose('Destroying previous dropdown', $module); module.remove.tabbable(); module.remove.active(); $menu.transition('stop all'); $menu.removeClass(className.visible).addClass(className.hidden); $module .off(eventNamespace) .removeData(moduleNamespace) ; $menu .off(eventNamespace) ; $document .off(elementNamespace) ; module.disconnect.menuObserver(); module.disconnect.selectObserver(); module.disconnect.classObserver(); }, observeChanges: function () { if ('MutationObserver' in window) { selectObserver = new MutationObserver(module.event.select.mutation); menuObserver = new MutationObserver(module.event.menu.mutation); classObserver = new MutationObserver(module.event.class.mutation); module.debug('Setting up mutation observer', selectObserver, menuObserver, classObserver); module.observe.select(); module.observe.menu(); module.observe.class(); } }, disconnect: { menuObserver: function () { if (menuObserver) { menuObserver.disconnect(); } }, selectObserver: function () { if (selectObserver) { selectObserver.disconnect(); } }, classObserver: function () { if (classObserver) { classObserver.disconnect(); } }, }, observe: { select: function () { if (module.has.input() && selectObserver) { selectObserver.observe($module[0], { attributes: true, childList: true, subtree: true, }); } }, menu: function () { if (module.has.menu() && menuObserver) { menuObserver.observe($menu[0], { childList: true, subtree: true, }); } }, class: function () { if (module.has.search() && classObserver) { classObserver.observe($module[0], { attributes: true, }); } }, }, create: { id: function () { id = (Math.random().toString(16) + '000000000').slice(2, 10); elementNamespace = '.' + id; module.verbose('Creating unique id for element', id); }, userChoice: function (values) { var $userChoices, $userChoice, html ; values = values || module.get.userValues(); if (!values) { return false; } values = Array.isArray(values) ? values : [values]; $.each(values, function (index, value) { if (module.get.item(value) === false) { html = settings.templates.addition(module.add.variables(message.addResult, value)); $userChoice = $('<div />') .html(html) .attr('data-' + metadata.value, value) .attr('data-' + metadata.text, value) .addClass(className.addition) .addClass(className.item) ; if (settings.hideAdditions) { $userChoice.addClass(className.hidden); } $userChoices = $userChoices === undefined ? $userChoice : $userChoices.add($userChoice); module.verbose('Creating user choices for value', value, $userChoice); } }); return $userChoices; }, userLabels: function (value) { var userValues = module.get.userValues() ; if (userValues) { module.debug('Adding user labels', userValues); $.each(userValues, function (index, value) { module.verbose('Adding custom user value'); module.add.label(value, value); }); } }, menu: function () { $menu = $('<div />') .addClass(className.menu) .appendTo($module) ; }, sizer: function () { $sizer = $('<span />') .addClass(className.sizer) .insertAfter($search) ; }, }, search: function (query) { query = query !== undefined ? query : module.get.query(); module.verbose('Searching for query', query); if (settings.fireOnInit === false && module.is.initialLoad()) { module.verbose('Skipping callback on initial load', settings.onSearch); } else if (module.has.minCharacters(query) && settings.onSearch.call(element, query) !== false) { module.filter(query); } else { module.hide(null, true); } }, select: { firstUnfiltered: function () { module.verbose('Selecting first non-filtered element'); module.remove.selectedItem(); $item .not(selector.unselectable) .not(selector.addition + selector.hidden) .eq(0) .addClass(className.selected) ; }, nextAvailable: function ($selected) { $selected = $selected.eq(0); var $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0), $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0), hasNext = $nextAvailable.length > 0 ; if (hasNext) { module.verbose('Moving selection to', $nextAvailable); $nextAvailable.addClass(className.selected); } else { module.verbose('Moving selection to', $prevAvailable); $prevAvailable.addClass(className.selected); } }, }, setup: { api: function () { var apiSettings = { debug: settings.debug, urlData: { value: module.get.value(), query: module.get.query(), }, on: false, } ; module.verbose('First request, initializing API'); $module .api(apiSettings) ; }, layout: function () { if ($module.is('select')) { module.setup.select(); module.setup.returnedObject(); } if (!module.has.menu()) { module.create.menu(); } if (module.is.clearable() && !module.has.clearItem()) { module.verbose('Adding clear icon'); $clear = $('<i />') .addClass('remove icon') .insertAfter($icon) ; } if (module.is.search() && !module.has.search()) { module.verbose('Adding search input'); var labelNode = $module.prev('label') ; $search = $('<input />') .addClass(className.search) .prop('autocomplete', module.is.chrome() ? 'fomantic-search' : 'off') ; if (labelNode.length > 0) { if (!labelNode.attr('id')) { labelNode.attr('id', '_' + module.get.id() + '_formLabel'); } $search.attr('aria-labelledby', labelNode.attr('id')); } $search.insertBefore($text); } if (module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) { module.create.sizer(); } if (settings.allowTab) { module.set.tabbable(); } }, select: function () { var selectValues = module.get.selectValues() ; module.debug('Dropdown initialized on a select', selectValues); if ($module.is('select')) { $input = $module; } // see if select is placed correctly already if ($input.parent(selector.dropdown).length > 0) { module.debug('UI dropdown already exists. Creating dropdown menu only'); $module = $input.closest(selector.dropdown); if (!module.has.menu()) { module.create.menu(); } $menu = $module.children(selector.menu); module.setup.menu(selectValues); } else { module.debug('Creating entire dropdown from select'); $module = $('<div />') .attr('class', $input.attr('class')) .addClass(className.selection) .addClass(className.dropdown) .html(templates.dropdown(selectValues, fields, settings.preserveHTML, settings.className)) .insertBefore($input) ; if ($input.hasClass(className.multiple) && $input.prop('multiple') === false) { module.error(error.missingMultiple); $input.prop('multiple', true); } if ($input.is('[multiple]')) { module.set.multiple(); } if ($input.prop('disabled')) { module.debug('Disabling dropdown'); $module.addClass(className.disabled); } if ($input.is('[required]')) { settings.forceSelection = true; } if (!settings.allowTab) { $input.removeAttr('tabindex'); } $input .prop('required', false) .removeAttr('class') .detach() .prependTo($module) ; } module.refresh(); }, menu: function (values) { $menu.html(templates.menu(values, fields, settings.preserveHTML, settings.className)); $item = $menu.find(selector.item); $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(); }, reference: function () { module.debug('Dropdown behavior was called on select, replacing with closest dropdown'); // replace module reference $module = $module.parent(selector.dropdown); instance = $module.data(moduleNamespace); element = $module[0]; module.refresh(); module.setup.returnedObject(); }, returnedObject: function () { var $firstModules = $allModules.slice(0, elementIndex), $lastModules = $allModules.slice(elementIndex + 1) ; // adjust all modules to use correct reference $allModules = $firstModules.add($module).add($lastModules); }, }, refresh: function () { module.refreshSelectors(); module.refreshData(); }, refreshItems: function () { $item = $menu.find(selector.item); $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(); }, refreshSelectors: function () { module.verbose('Refreshing selector cache'); $text = $module.find(selector.text); $search = $module.find(selector.search); $input = $module.find(selector.input); $icon = $module.find(selector.icon); $combo = $module.prev().find(selector.text).length > 0 ? $module.prev().find(selector.text) : $module.prev(); $menu = $module.children(selector.menu); $item = $menu.find(selector.item); $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(); }, refreshData: function () { module.verbose('Refreshing cached metadata'); $item .removeData(metadata.text) .removeData(metadata.value) ; }, clearData: function () { module.verbose('Clearing metadata'); $item .removeData(metadata.text) .removeData(metadata.value) ; $module .removeData(metadata.defaultText) .removeData(metadata.defaultValue) .removeData(metadata.placeholderText) ; }, clearItems: function () { $menu.empty(); module.refreshItems(); }, toggle: function () { module.verbose('Toggling menu visibility'); if (!module.is.active()) { module.show(); } else { module.hide(); } }, show: function (callback, preventFocus) { callback = isFunction(callback) ? callback : function () {}; if ((focused || iconClicked) && module.is.remote() && module.is.noApiCache() && !module.has.maxSelections()) { module.clearItems(); } if (!module.can.show() && module.is.remote()) { module.debug('No API results retrieved, searching before show'); module.queryRemote(module.get.query(), module.show, [callback, preventFocus]); } if (module.can.show() && !module.is.active()) { module.debug('Showing dropdown'); if (module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered())) { module.remove.message(); } if (module.is.allFiltered()) { return true; } if (settings.onShow.call(element) !== false) { module.remove.empty(); module.animate.show(function () { module.bind.intent(); if (module.has.search() && !preventFocus) { module.focusSearch(); } module.set.visible(); callback.call(element); }); } } }, hide: function (callback, preventBlur) { callback = isFunction(callback) ? callback : function () {}; if (module.is.active() && !module.is.animatingOutward()) { module.debug('Hiding dropdown'); if (settings.onHide.call(element) !== false) { module.animate.hide(function () { module.remove.visible(); // hiding search focus if (module.is.focusedOnSearch() && preventBlur !== true) { $search.trigger('blur'); } callback.call(element); }); // Hide submenus explicitly. On some browsers (esp. mobile), they will not automatically receive a // mouseleave event var $subMenu = $module.find(selector.menu); if ($subMenu.length > 0) { module.verbose('Hiding sub-menu', $subMenu); $subMenu.each(function () { var $sub = $(this); if (!module.is.animating($sub)) { module.animate.hide(false, $sub); } }); } } } else { module.unbind.intent(); } iconClicked = false; focused = false; }, hideOthers: function () { module.verbose('Finding other dropdowns to hide'); $allModules .not($module) .has(selector.menu + '.' + className.visible) .dropdown('hide') ; }, hideMenu: function () { module.verbose('Hiding menu instantaneously'); module.remove.active(); module.remove.visible(); $menu.transition('destroy').transition('hide'); }, hideSubMenus: function () { var $subMenus = $menu.children(selector.item).find(selector.menu) ; module.verbose('Hiding sub menus', $subMenus); $subMenus.transition('hide'); }, bind: { events: function () { module.bind.keyboardEvents(); module.bind.inputEvents(); module.bind.mouseEvents(); }, keyboardEvents: function () { module.verbose('Binding keyboard events'); $module .on('keydown' + eventNamespace, module.event.keydown) ; if (module.has.search()) { $module .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input) ; } if (module.is.multiple()) { $document .on('keydown' + elementNamespace, module.event.document.keydown) ; } }, inputEvents: function () { module.verbose('Binding input change events'); $module .on('change' + eventNamespace, selector.input, module.event.change) ; if (module.is.multiple() && module.is.searchSelection()) { $module .on('paste' + eventNamespace, selector.search, module.event.paste) ; } }, mouseEvents: function () { module.verbose('Binding mouse events'); if (module.is.multiple()) { $module .on('click' + eventNamespace, selector.label, module.event.label.click) .on('click' + eventNamespace, selector.remove, module.event.remove.click) ; } if (module.is.searchSelection()) { $module .on('mousedown' + eventNamespace, module.event.mousedown) .on('mouseup' + eventNamespace, module.event.mouseup) .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) .on('click' + eventNamespace, selector.icon, module.event.icon.click) .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click) .on('focus' + eventNamespace, selector.search, module.event.search.focus) .on('click' + eventNamespace, selector.search, module.event.search.focus) .on('blur' + eventNamespace, selector.search, module.event.search.blur) .on('click' + eventNamespace, selector.text, module.event.text.focus) ; if (module.is.multiple()) { $module .on('click' + eventNamespace, module.event.click) .on('click' + eventNamespace, module.event.search.focus) ; } } else { if (settings.on === 'click') { $module .on('click' + eventNamespace, selector.icon, module.event.icon.click) .on('click' + eventNamespace, module.event.test.toggle) ; } else if (settings.on === 'hover') { $module .on('mouseenter' + eventNamespace, module.delay.show) .on('mouseleave' + eventNamespace, module.delay.hide) .on('touchstart' + eventNamespace, module.event.test.toggle) .on('touchstart' + eventNamespace, selector.icon, module.event.icon.click) ; } else { $module .on(settings.on + eventNamespace, module.toggle) ; } $module .on('mousedown' + eventNamespace, module.event.mousedown) .on('mouseup' + eventNamespace, module.event.mouseup) .on('focus' + eventNamespace, module.event.focus) .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click) ; if (module.has.menuSearch()) { $module .on('blur' + eventNamespace, selector.search, module.event.search.blur) ; } else { $module .on('blur' + eventNamespace, module.event.blur) ; } } $menu .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter) .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave) .on('click' + eventNamespace, selector.item, module.event.item.click) ; }, intent: function () { module.verbose('Binding hide intent event to document'); $document .on('click' + elementNamespace, module.event.test.hide) ; }, }, unbind: { intent: function () { module.verbose('Removing hide intent event from document'); $document .off('click' + elementNamespace) ; }, }, filter: function (query) { var searchTerm = query !== undefined ? query : module.get.query(), afterFiltered = function () { if (module.is.multiple()) { module.filterActive(); } if (query || (!query && module.get.activeItem().length === 0)) { module.select.firstUnfiltered(); } if (module.has.allResultsFiltered()) { if (settings.onNoResults.call(element, searchTerm)) { if (settings.allowAdditions) { if (settings.hideAdditions) { module.verbose('User addition with no menu, setting empty style'); module.set.empty(); module.hideMenu(); } } else { module.verbose('All items filtered, showing message', searchTerm); module.add.message(message.noResults); } } else { module.verbose('All items filtered, hiding dropdown', searchTerm); module.set.empty(); module.hideMenu(); } } else { module.remove.empty(); module.remove.message(); } if (settings.allowAdditions) { module.add.userSuggestion(module.escape.htmlEntities(query)); } if (module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() && !module.is.empty()) { module.show(); } } ; if (settings.useLabels && module.has.maxSelections()) { module.show(); return; } if (settings.apiSettings) { if (module.can.useAPI()) { module.queryRemote(searchTerm, function () { if (settings.filterRemoteData) { module.filterItems(searchTerm); } var preSelected = $input.val(); if (!Array.isArray(preSelected)) { preSelected = preSelected && preSelected !== '' ? preSelected.split(settings.delimiter) : []; } if (module.is.multiple()) { $.each(preSelected, function (index, value) { $item.filter('[data-value="' + value + '"]') .addClass(className.filtered) ; }); } module.focusSearch(true); afterFiltered(); }); } else { module.error(error.noAPI); } } else { module.filterItems(searchTerm); afterFiltered(); } }, queryRemote: function (query, callback, callbackParameters) { if (!Array.isArray(callbackParameters)) { callbackParameters = [callbackParameters]; } var apiSettings = { errorDuration: false, cache: 'local', throttle: settings.throttle, urlData: { query: query, }, }, apiCallbacks = { onError: function (errorMessage, $module, xhr) { module.add.message(message.serverError); iconClicked = false; focused = false; callback.apply(null, callbackParameters); if (typeof settings.apiSettings.onError === 'function') { settings.apiSettings.onError.call(this, errorMessage, $module, xhr); } }, onFailure: function (response, $module, xhr) { module.add.message(message.serverError); iconClicked = false; focused = false; callback.apply(null, callbackParameters); if (typeof settings.apiSettings.onFailure === 'function') { settings.apiSettings.onFailure.call(this, response, $module, xhr); } }, onSuccess: function (response, $module, xhr) { var values = response[fields.remoteValues] ; if (!Array.isArray(values)) { values = []; } module.remove.message(); var menuConfig = {}; menuConfig[fields.values] = values; module.setup.menu(menuConfig); if (values.length === 0 && !settings.allowAdditions) { module.add.message(message.noResults); } else { var value = module.is.multiple() ? module.get.values() : module.get.value(); if (value !== '') { module.verbose('Value(s) present after click icon, select value(s) in items'); module.set.selected(value, null, true, true); } } iconClicked = false; focused = false; callback.apply(null, callbackParameters); if (typeof settings.apiSettings.onSuccess === 'function') { settings.apiSettings.onSuccess.call(this, response, $module, xhr); } }, } ; if (!$module.api('get request')) { module.setup.api(); } apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings, apiCallbacks, tempDisableApiCache ? { cache: false } : {}); $module .api('setting', apiSettings) .api('query') ; tempDisableApiCache = false; }, filterItems: function (query) { var searchTerm = module.remove.diacritics( query !== undefined ? query : module.get.query() ), results = null, escapedTerm = module.escape.string(searchTerm), regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm', beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags) ; // avoid loop if we're matching nothing if (module.has.query()) { results = []; module.verbose('Searching for matching values', searchTerm); $item .each(function () { var $choice = $(this), text, value ; if ($choice.hasClass(className.unfilterable)) { results.push(this); return true; } if (settings.match === 'both' || settings.match === 'text') { text = module.remove.diacritics(String(module.get.choiceText($choice, false))); if (text.search(beginsWithRegExp) !== -1 || (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) || (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) ) { results.push(this); return true; } } if (settings.match === 'both' || settings.match === 'value') { value = module.remove.diacritics(String(module.get.choiceValue($choice, text))); if (value.search(beginsWithRegExp) !== -1 || (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) || (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) ) { results.push(this); return true; } } }) ; } module.debug('Showing only matched items', searchTerm); module.remove.filteredItem(); if (results) { $item .not(results) .addClass(className.filtered) ; } if (!module.has.query()) { $divider .removeClass(className.hidden) ; } else if (settings.hideDividers === true) { $divider .addClass(className.hidden) ; } else if (settings.hideDividers === 'empty') { $divider .removeClass(className.hidden) .filter(function () { // First find the last divider in this divider group // Dividers which are direct siblings are considered a group var $lastDivider = $(this).nextUntil(selector.item); return ($lastDivider.length > 0 ? $lastDivider : $(this)) // Count all non-filtered items until the next divider (or end of the dropdown) .nextUntil(selector.divider) .filter(selector.item + ':not(.' + className.filtered + ')') // Hide divider if no items are found .length === 0; }) .addClass(className.hidden) ; } }, fuzzySearch: function (query, term) { var termLength = term.length, queryLength = query.length ; query = settings.ignoreSearchCase ? query.toLowerCase() : query; term = settings.ignoreSearchCase ? term.toLowerCase() : term; if (queryLength > termLength) { return false; } if (queryLength === termLength) { return query === term; } for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { var continueSearch = false, queryCharacter = query.charCodeAt(characterIndex) ; while (nextCharacterIndex < termLength) { if (term.charCodeAt(nextCharacterIndex++) === queryCharacter) { continueSearch = true; break; } } if (!continueSearch) { return false; } } return true; }, exactSearch: function (query, term) { query = settings.ignoreSearchCase ? query.toLowerCase() : query; term = settings.ignoreSearchCase ? term.toLowerCase() : term; return term.indexOf(query) > -1; }, filterActive: function () { if (settings.useLabels) { $item.filter('.' + className.active) .addClass(className.filtered) ; } }, focusSearch: function (skipHandler) { if (module.has.search() && !module.is.focusedOnSearch()) { if (skipHandler) { $module.off('focus' + eventNamespace, selector.search); $search.trigger('focus'); $module.on('focus' + eventNamespace, selector.search, module.event.search.focus); } else { $search.trigger('focus'); } } }, blurSearch: function () { if (module.has.search()) { $search.trigger('blur'); } }, forceSelection: function () { var $currentlySelected = $item.not(className.filtered).filter('.' + class