1/*! 2 * jQuery Pretty Dropdowns Plugin v4.17.0 by T. H. Doan (https://thdoan.github.io/pretty-dropdowns/) 3 * 4 * jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License. 5 * Read a copy of the license in the LICENSE file or at https://choosealicense.com/licenses/mit/ 6 */ 7 8(function($) { 9 $.fn.prettyDropdown = function(oOptions) { 10 11 // Default options 12 oOptions = $.extend({ 13 classic: false, 14 customClass: 'arrow', 15 width: null, 16 height: 50, 17 hoverIntent: 200, 18 multiDelimiter: '; ', 19 multiVerbosity: 99, 20 selectedMarker: '✓', 21 afterLoad: function(){} 22 }, oOptions); 23 24 oOptions.selectedMarker = '<span aria-hidden="true" class="checked"> ' + oOptions.selectedMarker + '</span>'; 25 // Validate options 26 if (isNaN(oOptions.width) && !/^\d+%$/.test(oOptions.width)) oOptions.width = null; 27 if (isNaN(oOptions.height)) oOptions.height = 50; 28 else if (oOptions.height<8) oOptions.height = 8; 29 if (isNaN(oOptions.hoverIntent)) oOptions.hoverIntent = 200; 30 if (isNaN(oOptions.multiVerbosity)) oOptions.multiVerbosity = 99; 31 32 // Translatable strings 33 var MULTI_NONE = 'None selected', 34 MULTI_PREFIX = 'Selected: ', 35 MULTI_POSTFIX = ' selected'; 36 37 // Globals 38 var $current, 39 aKeys = [ 40 '0','1','2','3','4','5','6','7','8','9',,,,,,,,
41 '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' 42 ], 43 nCount, 44 nHoverIndex, 45 nLastIndex, 46 nTimer, 47 nTimestamp, 48 49 // Initiate pretty drop-downs 50 init = function(elSel) { 51 var $select = $(elSel), 52 nSize = elSel.size, 53 sId = elSel.name || elSel.id || '', 54 sLabelId; 55 // Exit if widget has already been initiated 56 if ($select.data('loaded')) return; 57 // Remove 'size' attribute to it doesn't affect vertical alignment 58 $select.data('size', nSize).removeAttr('size'); 59 // Set <select> height to reserve space for <div> container 60 $select.css('visibility', 'hidden').outerHeight(oOptions.height); 61 nTimestamp = performance.now()*100000000000000; 62 // Test whether to add 'aria-labelledby' 63 if (elSel.id) { 64 // Look for <label> 65 var $label = $('label[for=' + elSel.id + ']'); 66 if ($label.length) { 67 // Add 'id' to <label> if necessary 68 if ($label.attr('id') && !/^menu\d{13,}$/.test($label.attr('id'))) sLabelId = $label.attr('id'); 69 else $label.attr('id', (sLabelId = 'menu' + nTimestamp)); 70 } 71 } 72 nCount = 0; 73 var $items = $('optgroup, option', $select), 74 $selected = $items.filter(':selected'), 75 bMultiple = elSel.multiple, 76 // Height - 2px for borders 77 sHtml = '<ul' + (elSel.disabled ? '' : ' tabindex="0"') + ' role="listbox"' 78 + (elSel.title ? ' title="' + elSel.title + '" aria-label="' + elSel.title + '"' : '') 79 + (sLabelId ? ' aria-labelledby="' + sLabelId + '"' : '') 80 + ' aria-activedescendant="item' + nTimestamp + '-1" aria-expanded="false"' 81 + ' style="max-height:' + (oOptions.height-2) + 'px;margin:' 82 // NOTE: $select.css('margin') returns an empty string in Firefox, so we have to get 83 // each margin individually. See https://github.com/jquery/jquery/issues/3383 84 + $select.css('margin-top') + ' ' 85 + $select.css('margin-right') + ' ' 86 + $select.css('margin-bottom') + ' ' 87 + $select.css('margin-left') + ';">'; 88 if (bMultiple) { 89 sHtml += renderItem(null, 'selected'); 90 $items.each(function() { 91 if (this.selected) { 92 sHtml += renderItem(this, '', true) 93 } else { 94 sHtml += renderItem(this); 95 } 96 }); 97 } else { 98 if (oOptions.classic) { 99 $items.each(function() { 100 sHtml += renderItem(this); 101 }); 102 } else { 103 sHtml += renderItem($selected[0], 'selected'); 104 $items.filter(':not(:selected)').each(function() { 105 sHtml += renderItem(this); 106 }); 107 } 108 } 109 sHtml += '</ul>'; 110 $select.wrap('<div ' + (sId ? 'id="prettydropdown-' + sId + '" ' : '') 111 + 'class="prettydropdown ' 112 + (oOptions.classic ? 'classic ' : '') 113 + (elSel.disabled ? 'disabled ' : '') 114 + (bMultiple ? 'multiple ' : '') 115 + oOptions.customClass + ' loading"' 116 // NOTE: For some reason, the container height is larger by 1px if the <select> has the 117 // 'multiple' attribute or 'size' attribute with a value larger than 1. To fix this, we 118 // have to inline the height. 119 + ((bMultiple || nSize>1) ? ' style="height:' + oOptions.height + 'px;"' : '') 120 +'></div>').before(sHtml).data('loaded', true); 121 var $dropdown = $select.parent().children('ul'), 122 nWidth = $dropdown.outerWidth(true), 123 nOuterWidth; 124 $items = $dropdown.children();
125 // Update default selected values for multi-select menu 126 if (bMultiple) updateSelected($dropdown); 127 else if (oOptions.classic) $('[data-value="' + $selected.val() + '"]', $dropdown).addClass('selected').append(oOptions.selectedMarker); 128 // Calculate width if initially hidden 129 if ($dropdown.width()<=0) { 130 var $clone = $dropdown.parent().clone().css({ 131 position: 'absolute', 132 top: '-100%' 133 }); 134 $('body').append($clone); 135 nWidth = $clone.children('ul').outerWidth(true); 136 $('li', $clone).width(nWidth); 137 nOuterWidth = $clone.children('ul').outerWidth(true); 138 $clone.remove(); 139 } 140 // Set dropdown width and event handler 141 // NOTE: Setting width using width(), then css() because width() only can return a float, 142 // which can result in a missing right border when there is a scrollbar. 143 $items.width(nWidth).css('width', $items.css('width')); 144 if (oOptions.width) { 145 $dropdown.parent().css('min-width', $items.css('width')); 146 $dropdown.css('width', '100%'); 147 $items.css('width', '100%'); 148 } 149 $items.click(function() { 150 var $li = $(this), 151 $selected = $dropdown.children('.selected'); 152 // Ignore disabled menu 153 if ($dropdown.parent().hasClass('disabled')) return; 154 // Only update if not disabled, not a label, and a different value selected 155 if ($dropdown.hasClass('active') && !$li.hasClass('disabled') && !$li.hasClass('label') && $li.data('value')!==$selected.data('value')) { 156 // Select highlighted item 157 if (bMultiple) { 158 if ($li.children('span.checked').length) $li.children('span.checked').remove(); 159 else $li.append(oOptions.selectedMarker); 160 // Sync <select> element 161 $dropdown.children(':not(.selected)').each(function(nIndex) { 162 $('optgroup, option', $select).eq(nIndex).prop('selected', $(this).children('span.checked').length>0); 163 }); 164 // Update selected values for multi-select menu 165 updateSelected($dropdown); 166 } else { 167 $selected.removeClass('selected').children('span.checked').remove(); 168 $li.addClass('selected').append(oOptions.selectedMarker); 169 if (!oOptions.classic) $dropdown.prepend($li); 170 $dropdown.removeClass('reverse').attr('aria-activedescendant', $li.attr('id')); 171 if ($selected.data('group') && !oOptions.classic) $dropdown.children('.label').filter(function() { 172 return $(this).text()===$selected.data('group'); 173 }).after($selected); 174 // Sync <select> element 175 $('optgroup, option', $select).filter(function() { 176 // <option>: this.value = this.text, $li.data('value') = $li.contents() 177 // <option value="">: this.value = "", $li.data('value') = undefined 178 return (this.value+'|'+this.text)===($li.data('value')||'')+'|'+$li.contents().filter(function() { 179 // Filter out selected marker 180 return this.nodeType===3; 181 }).text(); 182 }).prop('selected', true); 183 } 184 $select.trigger('change'); 185 } 186 if ($li.hasClass('selected') || !bMultiple) { 187 $dropdown.toggleClass('active'); 188 $dropdown.attr('aria-expanded', $dropdown.hasClass('active')); 189 } 190 // Try to keep drop-down menu within viewport 191 if ($dropdown.hasClass('active')) { 192 // Close any other open menus 193 if ($('.prettydropdown > ul.active').length>1) resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]); 194 var nWinHeight = window.innerHeight, 195 nMaxHeight, 196 nOffsetTop = $dropdown.offset().top, 197 nScrollTop = $(document).scrollTop(), 198 nDropdownHeight = $dropdown.outerHeight(); 199 if (nSize) { 200 nMaxHeight = nSize*(oOptions.height-2); 201 if (nMaxHeight<nDropdownHeight-2) nDropdownHeight = nMaxHeight+2; 202 } 203 var nDropdownBottom = nOffsetTop-nScrollTop+nDropdownHeight; 204 if (nDropdownBottom>nWinHeight) { 205 // Expand to direction that has the most space 206 if (nOffsetTop-nScrollTop>nWinHeight-(nOffsetTop-nScrollTop+oOptions.height)) { 207 $dropdown.addClass('reverse'); 208 if (!oOptions.classic) $dropdown.append($selected); 209 if (nOffsetTop-nScrollTop+oOptions.height<nDropdownHeight) { 210 $dropdown.outerHeight(nOffsetTop-nScrollTop+oOptions.height); 211 // Ensure the selected item is in view 212 $dropdown.scrollTop(nDropdownHeight); 213 } 214 } else { 215 $dropdown.height($dropdown.height()-(nDropdownBottom-nWinHeight)); 216 } 217 } 218 if (nMaxHeight && nMaxHeight<$dropdown.height()) $dropdown.css('height', nMaxHeight + 'px'); 219 // Ensure the selected item is in view 220 if (oOptions.classic) $dropdown.scrollTop($selected.index()*(oOptions.height-2)); 221 } else { 222 $dropdown.data('clicked', true); 223 resetDropdown($dropdown[0]); 224 } 225 }); 226 $dropdown.on({ 227 focusin: function() { 228 // Unregister any existing handlers first to prevent duplicate firings 229 $(window).off('keydown', handleKeypress).on('keydown', handleKeypress); 230 }, 231 focusout: function() { 232 $(window).off('keydown', handleKeypress); 233 }, 234 mouseenter: function() { 235 $dropdown.data('hover', true); 236 }, 237 mouseleave: resetDropdown, 238 mousemove: hoverDropdownItem 239 }); 240 if (oOptions.hoverIntent<0) { 241 $(document).click(function(e) { 242 if ($dropdown.data('hover') && !$dropdown[0].contains(e.target)) resetDropdown($dropdown[0]); 243 }); 244 } 245 // Put focus on menu when user clicks on label 246 if (sLabelId) $('#' + sLabelId).off('click', handleFocus).click(handleFocus); 247 // Done with everything! 248 $dropdown.parent().width(oOptions.width||nOuterWidth||$dropdown.outerWidth(true)).removeClass('loading'); 249 oOptions.afterLoad(); 250 }, 251 252 // Manage widget focusing 253 handleFocus = function(e) { 254 $('ul[aria-labelledby=' + e.target.id + ']').focus(); 255 }, 256
257 // Manage keyboard navigation 258 handleKeypress = function(e) { 259 var $dropdown = $('.prettydropdown > ul.active, .prettydropdown > ul:focus'); 260 if (!$dropdown.length) return; 261 if (e.which===9) { // Tab 262 resetDropdown($dropdown[0]); 263 return; 264 } else { 265 // Intercept non-Tab keys only 266 e.preventDefault(); 267 e.stopPropagation(); 268 } 269 var $items = $dropdown.children(), 270 bOpen = $dropdown.hasClass('active'), 271 nItemsHeight = $dropdown.height()/(oOptions.height-2), 272 nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight), 273 sKey; 274 nHoverIndex = Math.max(0, $dropdown.children('.hover').index()); 275 nLastIndex = $items.length-1; 276 $current = $items.eq(nHoverIndex); 277 $dropdown.data('lastKeypress', +new Date()); 278 switch (e.which) { 279 case 13: // Enter 280 if (!bOpen) { 281 $current = $items.filter('.selected'); 282 toggleHover($current, 1); 283 } 284 $current.click(); 285 break; 286 case 27: // Esc 287 if (bOpen) resetDropdown($dropdown[0]); 288 break; 289 case 32: // Space 290 if (bOpen) { 291 sKey = ' '; 292 } else { 293 $current = $items.filter('.selected'); 294 toggleHover($current, 1); 295 $current.click(); 296 } 297 break; 298 case 33: // Page Up 299 if (bOpen) { 300 toggleHover($current, 0); 301 toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1); 302 } 303 break; 304 case 34: // Page Down 305 if (bOpen) { 306 toggleHover($current, 0); 307 toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1); 308 } 309 break; 310 case 35: // End 311 if (bOpen) { 312 toggleHover($current, 0); 313 toggleHover($items.eq(nLastIndex), 1); 314 } 315 break; 316 case 36: // Home 317 if (bOpen) { 318 toggleHover($current, 0); 319 toggleHover($items.eq(0), 1); 320 } 321 break; 322 case 38: // Up 323 if (bOpen) { 324 toggleHover($current, 0); 325 // If not already key-navigated or first item is selected, cycle to the last item; or 326 // else select the previous item 327 toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1); 328 } 329 break; 330 case 40: // Down 331 if (bOpen) { 332 toggleHover($current, 0); 333 // If last item is selected, cycle to the first item; or else select the next item 334 toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1); 335 } 336 break; 337 default: 338 if (bOpen) sKey = aKeys[e.which-48]; 339 } 340 if (sKey) { // Alphanumeric key pressed 341 clearTimeout(nTimer); 342 $dropdown.data('keysPressed', $dropdown.data('keysPressed')===undefined ? sKey : $dropdown.data('keysPressed') + sKey); 343 nTimer = setTimeout(function() { 344 $dropdown.removeData('keysPressed'); 345 // NOTE: Windows keyboard repeat delay is 250-1000 ms. See
346 // https://technet.microsoft.com/en-us/library/cc978658.aspx 347 }, 300); 348 // Build index of matches 349 var aMatches = [], 350 nCurrentIndex = $current.index(); 351 $items.each(function(nIndex) { 352 if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex); 353 }); 354 if (aMatches.length) { 355 // Cycle through items matching key(s) pressed 356 for (var i=0; i<aMatches.length; ++i) { 357 if (aMatches[i]>nCurrentIndex) { 358 toggleHover($items, 0); 359 toggleHover($items.eq(aMatches[i]), 1); 360 break; 361 } 362 if (i===aMatches.length-1) { 363 toggleHover($items, 0); 364 toggleHover($items.eq(aMatches[0]), 1); 365 } 366 } 367 } 368 } 369 }, 370 371 // Highlight menu item 372 hoverDropdownItem = function(e) { 373 var $dropdown = $(e.currentTarget); 374 if (e.target.nodeName!=='LI' || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return; 375 toggleHover($dropdown.children(), 0, 1); 376 toggleHover($(e.target), 1, 1); 377 }, 378 379 // Construct menu item 380 // elOpt is null for first item in multi-select menus 381 renderItem = function(elOpt, sClass, bSelected) { 382 var sGroup = '', 383 sText = '', 384 sTitle; 385 sClass = sClass || ''; 386 if (elOpt) { 387 switch (elOpt.nodeName) { 388 case 'OPTION': 389 if (elOpt.parentNode.nodeName==='OPTGROUP') sGroup = elOpt.parentNode.getAttribute('label'); 390 sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.text + (elOpt.getAttribute('data-suffix') || ''); 391 break; 392 case 'OPTGROUP': 393 sClass += ' label'; 394 sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.getAttribute('label') + (elOpt.getAttribute('data-suffix') || ''); 395 break; 396 } 397 if (elOpt.disabled || (sGroup && elOpt.parentNode.disabled)) sClass += ' disabled'; 398 sTitle = elOpt.title; 399 if (sGroup && !sTitle) sTitle = elOpt.parentNode.title; 400 } 401 ++nCount; 402 return '<li id="item' + nTimestamp + '-' + nCount + '"' 403 + (sGroup ? ' data-group="' + sGroup + '"' : '') 404 + (elOpt && (elOpt.value||oOptions.classic) ? ' data-value="' + elOpt.value + '"' : '') 405 + (elOpt && elOpt.nodeName==='OPTION' ? ' role="option"' : '') 406 + (sTitle ? ' title="' + sTitle + '" aria-label="' + sTitle + '"' : '') 407 + (sClass ? ' class="' + $.trim(sClass) + '"' : '') 408 + ((oOptions.height!==50) ? ' style="height:' + (oOptions.height-2) 409 + 'px;line-height:' + (oOptions.height-4) + 'px;"' : '') + '>' + sText 410 + ((bSelected || sClass==='selected') ? oOptions.selectedMarker : '') + '</li>'; 411 }, 412 413 // Reset menu state 414 // @param o Event or Element object 415 resetDropdown = function(o) { 416 if (oOptions.hoverIntent<0 && o.type==='mouseleave') return; 417 var $dropdown = $(o.currentTarget||o); 418 $dropdown.data('hover', false); 419 clearTimeout(nTimer); 420 nTimer = setTimeout(function() { 421 if ($dropdown.data('hover')) return; 422 if ($dropdown.hasClass('reverse') && !oOptions.classic) $dropdown.prepend($dropdown.children(':last-child')); 423 $dropdown.removeClass('active reverse').removeData('clicked').attr('aria-expanded', 'false').css('height', ''); 424 $dropdown.children().removeClass('hover nohover'); 425 // Update focus for NVDA screen readers 426 $dropdown.attr('aria-activedescendant', $dropdown.children('.selected').attr('id')); 427 }, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0); 428 }, 429 430 // Set menu item hover state 431 // bNoScroll set on hoverDropdownItem() 432 toggleHover = function($li, bOn, bNoScroll) { 433 if (bOn) { 434 var $dropdown = $li.parent(); 435 $li.removeClass('nohover').addClass('hover'); 436 // Update focus for NVDA screen readers 437 $dropdown.attr('aria-activedescendant', $li.attr('id')); 438 if ($li.length===1 && $current && !bNoScroll) { 439 // Ensure items are always in view 440 var nDropdownHeight = $dropdown.outerHeight(), 441 nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border 442 if ($li.index()===0) { 443 $dropdown.scrollTop(0); 444 } else if ($li.index()===nLastIndex) { 445 $dropdown.scrollTop($dropdown.children().length*oOptions.height); 446 } else { 447 if (nItemOffset+oOptions.height>nDropdownHeight) $dropdown.scrollTop($dropdown.scrollTop()+oOptions.height+nItemOffset-nDropdownHeight); 448 else if (nItemOffset<0) $dropdown.scrollTop($dropdown.scrollTop()+nItemOffset); 449 } 450 } 451 } else { 452 $li.removeClass('hover').addClass('nohover'); 453 } 454 }, 455 456 // Update selected values for multi-select menu 457 updateSelected = function($dropdown) { 458 var $select = $dropdown.parent().children('select'), 459 aSelected = $('option', $select).map(function() { 460 if (this.selected) return this.text; 461 }).get(), 462 sSelected; 463 if (oOptions.multiVerbosity>=aSelected.length) sSelected = aSelected.join(oOptions.multiDelimiter) || MULTI_NONE; 464 else sSelected = aSelected.length + '/' + $('option', $select).length + MULTI_POSTFIX; 465 if (sSelected) { 466 var sTitle = ($select.attr('title') ? $select.attr('title') : '') + (aSelected.length ? '\n' + MULTI_PREFIX + aSelected.join(oOptions.multiDelimiter) : ''); 467 $dropdown.children('.selected').text(sSelected); 468 $dropdown.attr({ 469 'title': sTitle, 470 'aria-label': sTitle 471 }); 472 } else { 473 $dropdown.children('.selected').empty(); 474 $dropdown.attr({ 475 'title': $select.attr('title'), 476 'aria-label': $select.attr('title') 477 }); 478 } 479 }; 480 481 /** 482 * Public Functions 483 */ 484 485 // Resync the menu with <select> to reflect state changes 486 this.refresh = function(oOptions) { 487 return this.each(function() { 488 var $select = $(this); 489 $select.prevAll('ul').remove(); 490 $select.unwrap().data('loaded', false); 491 this.size = $select.data('size'); 492 init(this); 493 }); 494 }; 495 496 return this.each(function() { 497 init(this); 498 }); 499 500 }; 501}(jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.