/*! * jQuery Pretty Dropdowns Plugin v4.11.0 by T. H. Doan (http://thdoan.github.io/pretty-dropdowns/) * * jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License. * Read a copy of the license in the LICENSE file or at * http://choosealicense.com/licenses/mit */ (function($) { $.fn.prettyDropdown = function(oOptions) { // Default options oOptions = $.extend({ classic: false, customClass: 'arrow', height: 50, hoverIntent: 200, multiDelimiter: '; ', multiVerbosity: 99, selectedMarker: '✓', reverse: false, afterLoad: function(){} }, oOptions); oOptions.selectedMarker = ' ' + oOptions.selectedMarker + ''; // Validate options if (isNaN(oOptions.height) || oOptions.height<8) oOptions.height = 8; if (isNaN(oOptions.hoverIntent) || oOptions.hoverIntent<0) oOptions.hoverIntent = 200; if (isNaN(oOptions.multiVerbosity)) oOptions.multiVerbosity = 99; // Translatable strings var MULTI_NONE = 'None selected', MULTI_PREFIX = 'Selected: ', MULTI_POSTFIX = ' selected'; // Globals var $current, aKeys = [ '0','1','2','3','4','5','6','7','8','9',,,,,,,, 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z' ], nCount, nHoverIndex, nLastIndex, nTimer, nTimestamp, // Initiate pretty drop-downs init = function(elSel) { var $select = $(elSel), nSize = elSel.size, sId = elSel.name || elSel.id || '', sLabelId; // Exit if widget has already been initiated if ($select.data('loaded')) return; // Remove 'size' attribute to it doesn't affect vertical alignment $select.data('size', nSize).removeAttr('size'); // Set height to reserve space for container $select.css('visibility', 'hidden').outerHeight(oOptions.height); nTimestamp = +new Date(); // Test whether to add 'aria-labelledby' if (elSel.id) { // Look for var $label = $('label[for=' + elSel.id + ']'); if ($label.length) { // Add 'id' to if necessary if ($label.attr('id') && !/^menu\d{13,}$/.test($label.attr('id'))) sLabelId = $label.attr('id'); else $label.attr('id', (sLabelId = 'menu' + nTimestamp)); } } nCount = 0; var $items = $('optgroup, option', $select), $selected = $items.filter(':selected'), bMultiple = elSel.multiple, // Height - 2px for borders sHtml = ''; if (bMultiple) { sHtml += renderItem(null, 'selected'); $items.each(function() { if (this.selected) { sHtml += renderItem(this, '', true) } else { sHtml += renderItem(this); } }); } else { if (oOptions.classic) { $items.each(function() { sHtml += renderItem(this); }); } else { sHtml += renderItem($selected[0], 'selected'); $items.filter(':not(:selected)').each(function() { sHtml += renderItem(this); }); } } sHtml += ''; $select.wrap(' has the // 'multiple' attribute or 'size' attribute with a value larger than 1. To fix this, we // have to inline the height. + ((bMultiple || nSize>1) ? ' style="height:' + oOptions.height + 'px;"' : '') +'>').before(sHtml).data('loaded', true); var $dropdown = $select.parent().children('ul'), nWidth = $dropdown.outerWidth(true), nOuterWidth; $items = $dropdown.children(); // Update default selected values for multi-select menu if (bMultiple) updateSelected($dropdown); else if (oOptions.classic) $('[data-value="' + $selected.val() + '"]', $dropdown).addClass('selected').append(oOptions.selectedMarker); // Calculate width if initially hidden if ($dropdown.width()<=0) { var $clone = $dropdown.parent().clone().css({ position: 'absolute', top: '-100%' }); $('body').append($clone); nWidth = $clone.children('ul').outerWidth(true); $('li', $clone).width(nWidth); nOuterWidth = $clone.children('ul').outerWidth(true); $clone.remove(); } // Set dropdown width and event handler // NOTE: Setting width using width(), then css() because width() only can return a float, // which can result in a missing right border when there is a scrollbar. $items.width(nWidth).css('width', $items.css('width')).click(function() { var $li = $(this), $selected = $dropdown.children('.selected'); // Ignore disabled menu if ($dropdown.parent().hasClass('disabled')) return; // Only update if not disabled, not a label, and a different value selected if ($dropdown.hasClass('active') && !$li.hasClass('disabled') && !$li.hasClass('label') && $li.data('value')!==$selected.data('value')) { // Select highlighted item if (bMultiple) { if ($li.children('span.checked').length) $li.children('span.checked').remove(); else $li.append(oOptions.selectedMarker); // Sync element $dropdown.children(':not(.selected)').each(function(nIndex) { $('optgroup, option', $select).eq(nIndex).prop('selected', $(this).children('span.checked').length>0); }); // Update selected values for multi-select menu updateSelected($dropdown); } else { $selected.removeClass('selected').children('span.checked').remove(); $li.addClass('selected').append(oOptions.selectedMarker); if (!oOptions.classic) $dropdown.prepend($li); $dropdown.removeClass('reverse').attr('aria-activedescendant', $li.attr('id')); if ($selected.data('group') && !oOptions.classic) $dropdown.children('.label').filter(function() { return $(this).text()===$selected.data('group'); }).after($selected); // Sync element $('optgroup, option', $select).filter(function() { // NOTE: .data('value') can return numeric, so using == comparison instead. return this.value==$li.data('value') || this.text===$li.contents().filter(function() { // Filter out selected marker return this.nodeType===3; }).text(); }).prop('selected', true); } $select.trigger('change'); } if ($li.hasClass('selected') || !bMultiple) { $dropdown.toggleClass('active'); $dropdown.attr('aria-expanded', $dropdown.hasClass('active')); } // Try to keep drop-down menu within viewport if ($dropdown.hasClass('active')) { // Close any other open menus if ($('.prettydropdown > ul.active').length>1) resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]); var nWinHeight = window.innerHeight, nMaxHeight, nOffsetTop = $dropdown.offset().top, nScrollTop = document.body.scrollTop, nDropdownHeight = $dropdown.outerHeight(); if (nSize) { nMaxHeight = nSize*(oOptions.height-2); if (nMaxHeight nWinHeight || oOptions.reverse) { // Expand to direction that has the most space if (nOffsetTop - nScrollTop > nWinHeight - (nOffsetTop - nScrollTop + oOptions.height) || oOptions.reverse) { $dropdown.addClass('reverse'); if (!oOptions.classic) $dropdown.append($selected); if (nOffsetTop-nScrollTop+oOptions.height ul.active, .prettydropdown > ul:focus'); if (!$dropdown.length) return; if (e.which===9) { // Tab resetDropdown($dropdown[0]); return; } else { // Intercept non-Tab keys only e.preventDefault(); e.stopPropagation(); } var $items = $dropdown.children(), bOpen = $dropdown.hasClass('active'), nItemsHeight = $dropdown.height()/(oOptions.height-2), nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight), sKey; nHoverIndex = Math.max(0, $dropdown.children('.hover').index()); nLastIndex = $items.length-1; $current = $items.eq(nHoverIndex); $dropdown.data('lastKeypress', +new Date()); switch (e.which) { case 13: // Enter if (!bOpen) { $current = $items.filter('.selected'); toggleHover($current, 1); } $current.click(); break; case 27: // Esc if (bOpen) resetDropdown($dropdown[0]); break; case 32: // Space if (bOpen) { sKey = ' '; } else { $current = $items.filter('.selected'); toggleHover($current, 1); $current.click(); } break; case 33: // Page Up if (bOpen) { toggleHover($current, 0); toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1); } break; case 34: // Page Down if (bOpen) { toggleHover($current, 0); toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1); } break; case 35: // End if (bOpen) { toggleHover($current, 0); toggleHover($items.eq(nLastIndex), 1); } break; case 36: // Home if (bOpen) { toggleHover($current, 0); toggleHover($items.eq(0), 1); } break; case 38: // Up if (bOpen) { toggleHover($current, 0); // If not already key-navigated or first item is selected, cycle to the last item; or // else select the previous item toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1); } break; case 40: // Down if (bOpen) { toggleHover($current, 0); // If last item is selected, cycle to the first item; or else select the next item toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1); } break; default: if (bOpen) sKey = aKeys[e.which-48]; } if (sKey) { // Alphanumeric key pressed clearTimeout(nTimer); $dropdown.data('keysPressed', $dropdown.data('keysPressed')===undefined ? sKey : $dropdown.data('keysPressed') + sKey); nTimer = setTimeout(function() { $dropdown.removeData('keysPressed'); // NOTE: Windows keyboard repeat delay is 250-1000 ms. See // https://technet.microsoft.com/en-us/library/cc978658.aspx }, 300); // Build index of matches var aMatches = [], nCurrentIndex = $current.index(); $items.each(function(nIndex) { if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex); }); if (aMatches.length) { // Cycle through items matching key(s) pressed for (var i=0; inCurrentIndex) { toggleHover($items, 0); toggleHover($items.eq(aMatches[i]), 1); break; } if (i===aMatches.length-1) { toggleHover($items, 0); toggleHover($items.eq(aMatches[0]), 1); } } } } }, // Highlight menu item hoverDropdownItem = function(e) { var $dropdown = $(e.currentTarget); if (e.target.nodeName!=='LI' || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return; toggleHover($dropdown.children(), 0, 1); toggleHover($(e.target), 1, 1); }, // Construct menu item // elOpt is null for first item in multi-select menus renderItem = function(elOpt, sClass, bSelected) { var sGroup = '', sText = '', sTitle; sClass = sClass || ''; if (elOpt) { switch (elOpt.nodeName) { case 'OPTION': if (elOpt.parentNode.nodeName==='OPTGROUP') sGroup = elOpt.parentNode.getAttribute('label'); sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.text + (elOpt.getAttribute('data-suffix') || ''); break; case 'OPTGROUP': sClass += ' label'; sText = elOpt.getAttribute('label'); break; } if (elOpt.disabled || (sGroup && elOpt.parentNode.disabled)) sClass += ' disabled'; sTitle = elOpt.title; if (sGroup && !sTitle) sTitle = elOpt.parentNode.title; } ++nCount; return '' + sText + ((bSelected || sClass==='selected') ? oOptions.selectedMarker : '') + ''; }, // Reset menu state // @param o Event or Element object resetDropdown = function(o) { var $dropdown = $(o.currentTarget||o); // NOTE: Sometimes it's possible for $dropdown to point to the wrong element when you // quickly hover over another menu. To prevent this, we need to check for .active as a // backup and manually reassign $dropdown. This also requires that it's not clicked on // because in rare cases the reassignment fails and the reverse menu will not get reset. if (o.type==='mouseleave' && !$dropdown.hasClass('active') && !$dropdown.data('clicked')) $dropdown = $('.prettydropdown > ul.active'); $dropdown.data('hover', false); clearTimeout(nTimer); nTimer = setTimeout(function() { if ($dropdown.data('hover')) return; if ($dropdown.hasClass('reverse') && !oOptions.classic) $dropdown.prepend($dropdown.children(':last-child')); $dropdown.removeClass('active reverse').removeData('clicked').attr('aria-expanded', 'false').css('height', ''); $dropdown.children().removeClass('hover nohover'); }, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0); }, // Set menu item hover state // bNoScroll set on hoverDropdownItem() toggleHover = function($li, bOn, bNoScroll) { if (bOn) { $li.removeClass('nohover').addClass('hover'); if ($li.length===1 && $current && !bNoScroll) { // Ensure items are always in view var $dropdown = $li.parent(), nDropdownHeight = $dropdown.outerHeight(), nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border if ($li.index()===0) { $dropdown.scrollTop(0); } else if ($li.index()===nLastIndex) { $dropdown.scrollTop($dropdown.children().length*oOptions.height); } else { if (nItemOffset+oOptions.height>nDropdownHeight) $dropdown.scrollTop($dropdown.scrollTop()+oOptions.height+nItemOffset-nDropdownHeight); else if (nItemOffset<0) $dropdown.scrollTop($dropdown.scrollTop()+nItemOffset); } } } else { $li.removeClass('hover').addClass('nohover'); } }, // Update selected values for multi-select menu updateSelected = function($dropdown) { var $select = $dropdown.parent().children('select'), aSelected = $('option', $select).map(function() { if (this.selected) return this.text; }).get(), sSelected; if (oOptions.multiVerbosity>=aSelected.length) sSelected = aSelected.join(oOptions.multiDelimiter) || MULTI_NONE; else sSelected = aSelected.length + '/' + $('option', $select).length + MULTI_POSTFIX; if (sSelected) { var sTitle = ($select.attr('title') ? $select.attr('title') : '') + (aSelected.length ? '\n' + MULTI_PREFIX + aSelected.join(oOptions.multiDelimiter) : ''); $dropdown.children('.selected').text(sSelected); $dropdown.attr({ 'title': sTitle, 'aria-label': sTitle }); } else { $dropdown.children('.selected').empty(); $dropdown.attr({ 'title': $select.attr('title'), 'aria-label': $select.attr('title') }); } }; /** * Public Functions */ // Resync the menu with to reflect state changes this.refresh = function(oOptions) { return this.each(function() { var $select = $(this); $select.prevAll('ul').remove(); $select.unwrap().data('loaded', false); this.size = $select.data('size'); init(this); }); }; return this.each(function() { init(this); }); }; }(jQuery));