1/* 2 * Autocomplete - jQuery plugin 1.0.2 3 * 4 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer 5 * 6 * Dual licensed under the MIT and GPL licenses: 7 * http://www.opensource.org/licenses/mit-license.php 8 * http://www.gnu.org/licenses/gpl.html 9 * 10 * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $ 11 * 12 * Modified by Tim Trinidad 10/04/2010 13 * to be compatible with Drupal (changed 'q' param to 'jq' 14 * and to take a "defaultText" parameter which shows on first click 15 * if the search term is empty 16 * 17 * SGY-3779 - added an anchorTo option to allow the autocomplete hint box to align with an element 18 * other than the associated input. 19 */ 20 21;(function($) { 22 23delete $.fn.Autocompleter; 24delete $.fn.autocomplete; 25 26$.fn.extend({ 27 autocomplete: function(urlOrData, options) { 28 var isUrl = typeof urlOrData == "string"; 29 options = $.extend({}, $.Autocompleter.defaults, { 30 url: isUrl ? urlOrData : null, 31 data: isUrl ? null : urlOrData, 32 delay: isUrl ? $.Autocompleter.defaults.delay : 10, 33 max: options && !options.scroll ? 10 : 150 34 }, options); 35 36 // if highlight is set to false, replace it with a do-nothing function 37 options.highlight = options.highlight || function(value) { return value; }; 38 39 // if the formatMatch option is not specified, then use formatItem for backwards compatibility 40 options.formatMatch = options.formatMatch || options.formatItem; 41 42 return this.each(function() { 43 new $.Autocompleter(this, options); 44 }); 45 }, 46 result: function(handler) { 47 return this.bind("result", handler); 48 }, 49 search: function(handler) { 50 return this.trigger("search", [handler]); 51 }, 52 flushCache: function() { 53 return this.trigger("flushCache"); 54 }, 55 setOptions: function(options){ 56 return this.trigger("setOptions", [options]); 57 }, 58 unautocomplete: function() { 59 return this.trigger("unautocomplete"); 60 } 61}); 62 63$.Autocompleter = function(input, options) { 64 65 var KEY = { 66 UP: 38, 67 DOWN: 40, 68 DEL: 46, 69 TAB: 9, 70 RETURN: 13, 71 ESC: 27, 72 COMMA: 188, 73 PAGEUP: 33, 74 PAGEDOWN: 34, 75 BACKSPACE: 8 76 }; 77 78 // Create $ object for input element 79 var $input = $(input).prop("autocomplete", "off").addClass(options.inputClass).attr({ 80 'role': 'combobox', 81 'aria-autocomplete': 'list', 82 'aria-expanded': 'false', 83 'aria-haspopup': 'listbox' 84 }); 85 86 var timeout; 87 var previousValue = ""; 88 var cache = $.Autocompleter.Cache(options); 89 var hasFocus = 0; 90 var lastKeyPressCode; 91 var config = { 92 mouseDownOnSelect: false, 93 // When true, suppress opening results on the subsequent click event. 94 suppressNextClickOpen: false 95 }; 96 97 // Assign a unique ID for the results element and connect it to the input via aria-controls 98 $.Autocompleter._idCounter = ($.Autocompleter._idCounter || 0) + 1; 99 config.resultsId = 'ac_results_' + $.Autocompleter._idCounter; 100 $input.attr('aria-controls', config.resultsId); 101 // The listbox is appended to document.body, so aria-owns helps some ATs (notably 102 // VoiceOver/Safari) associate the popup with the combobox even when it's not a 103 // DOM descendant of the input. 104 $input.attr('aria-owns', config.resultsId); 105 106 // Use a single shared live region for all autocomplete instances. 107 // Screen readers (especially JAWS) require live regions to be present in the 108 // DOM before the user interacts with the field. A shared regi
108on created on 109 // first use persists across popup open/close cycles, ensuring it is 110 // registered by the AT before any announcements are made. 111 if (!$('#' + $.Autocompleter.ANNOUNCER_ID).length) { 112 $('<div/>') 113 .attr({ 114 'id': $.Autocompleter.ANNOUNCER_ID, 115 'role': 'status', 116 'aria-live': 'polite', 117 'aria-atomic': 'true' 118 }) 119 .addClass('visually-hidden') 120 .appendTo(document.body); 121 } 122 // Separate live region for announcing the active option while navigating. 123 // VoiceOver in Chrome is inconsistent about announcing aria-activedescendant 124 // changes for editable comboboxes; a dedicated region avoids that gap while 125 // limiting behavior to macOS. 126 if (!$('#' + $.Autocompleter.ACTIVE_ANNOUNCER_ID).length) { 127 $('<div/>') 128 .attr({ 129 'id': $.Autocompleter.ACTIVE_ANNOUNCER_ID, 130 // "alert" is more reliably announced by VoiceOver (Chrome) than "status". 131 'role': 'alert', 132 'aria-live': 'assertive', 133 'aria-relevant': 'text', 134 'aria-atomic': 'true' 135 }) 136 .addClass('visually-hidden') 137 .appendTo(document.body); 138 } 139 140 var $countStatus = $('#' + $.Autocompleter.ANNOUNCER_ID); 141 var $activeStatus = $('#' + $.Autocompleter.ACTIVE_ANNOUNCER_ID); 142 config.$countStatus = $countStatus; 143 config.$activeStatus = $activeStatus; 144 145 var select = $.Autocompleter.Select(options, input, selectCurrent, config); 146 147 var blockSubmit; 148 149 var documentMouseDownNamespace = '.autocomplete.' + config.resultsId; 150 function bindDocumentMouseDownToClose() { 151 // Unbind first to ensure at most one handler is bound at a time. 152 unbindDocumentMouseDownToClose(); 153 $(document).bind('mousedown' + documentMouseDownNamespace, function(event) { 154 if (!select || !select.visible || !select.visible()) { 155 return; 156 } 157 var targetEl = event && event.target ? event.target : null; 158 if (!targetEl) { 159 return; 160 } 161 // Ignore clicks inside the results container so option selection works. 162 if (typeof select.contains === 'function' && select.contains(targetEl)) { 163 return; 164 } 165 hideResultsNow(); 166 }); 167 } 168 169 function unbindDocumentMouseDownToClose() { 170 $(document).unbind(documentMouseDownNamespace); 171 } 172 173 // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all 174 $input.bind(("keydown") + ".autocomplete", function(event) { 175 // track last key pressed 176 lastKeyPressCode = event.keyCode; 177 switch(event.keyCode) { 178 179 case KEY.UP: 180 event.preventDefault(); 181 if ( select.visible() ) { 182 if (options.a11yFocusListboxOnArrow && typeof select.focusResults === 'function') { 183 select.focusResults(); 184 } 185 select.prev(); 186 } else { 187 onChange(0, true); 188 } 189 break; 190 191 case KEY.DOWN: 192 event.preventDefault(); 193 if ( select.visible() ) { 194 if (options.a11yFocusListboxOnArrow && typeof select.focusResults === 'function') { 195 select.focusResults(); 196 } 197 select.next(); 198 } else { 199 onChange(0, true); 200 } 201 break; 202 203 case KEY.PAGEUP: 204 event.preventDefault(); 205 if ( select.visible() ) { 206 if (options.a11yFocusListboxOnArrow && typeof select.focusResults === 'function') { 207 select.focusResults(); 208 } 209 select.pageUp(); 210 } else { 211 onChange(0, true); 212 } 213 break; 214 215 case KEY.PAGEDOWN: 216 event.preventDefault(); 217 if ( select.visible() ) { 218 if (options.a11yFocusListboxOnArrow && typeof select.focusResults === 'function') { 219 select.focusResults(); 220 } 221 select.pageDown(); 222 } else { 223 onChange(0, true); 224 } 225 break; 226 227 // matches also semicolon 228 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: 229 case KEY.TAB: 230 case KEY.RETURN: 231 if( selectCurrent() ) { 232 // stop default to prevent a form submit, Opera needs special handling 233 event.preventDefault(); 234 blockSubmit = true; 235 return false; 236 } 237 break; 238 239 case KEY.ESC: 240 select.hide(); 241 break; 242 243 default: 244 clearTimeout(timeout); 245 timeout = setTimeout(onChange, options.delay); 246 break; 247 } 248 }).focus(function(){ 249 // track whether the field has focus, we shouldn't process any 250 // results if the field no longer has focus 251 hasFocus++; 252 }).blur(function() { 253 // When focus moves into the results listbox (e.g. Firefox+VoiceOver moves 254 // DOM focus automatically, or when a11yFocusListboxOnArrow moves it 255 // explicitly), the input will blur but we must keep the popup open while 256 // focus remains within the listbox/options. 257 setTimeout(function() { 258 var focusInResults = false;
259 try { 260 focusInResults = select 261 && typeof select.isFocusInResults === 'function' 262 && select.isFocusInResults(); 263 } catch (e) { 264 focusInResults = false; 265 } 266 267 hasFocus = focusInResults ? 1 : 0; 268 if (!focusInResults && !config.mouseDownOnSelect) { 269 hideResults(); 270 } 271 }, 0); 272 }).click(function() { 273 if (config.suppressNextClickOpen) { 274 config.suppressNextClickOpen = false; 275 return; 276 } 277 // show select when clicking in a focused field 278 if ( hasFocus++ > 0 && !select.visible() ) { 279 onChange(0, true); 280 } 281 }).mousedown(function() { 282 // Match existing behavior: clicking the input while results are open should 283 // close the results, not keep them visible. 284 if (select.visible()) { 285 config.suppressNextClickOpen = true; 286 hideResultsNow(); 287 } 288 }).bind("search", function() { 289 // TODO why not just specifying both arguments? 290 var fn = (arguments.length > 1) ? arguments[1] : null; 291 function findValueCallback(q, data) { 292 var result; 293 if( data && data.length ) { 294 for (var i=0; i < data.length; i++) { 295 if( data[i].result.toLowerCase() == q.toLowerCase() ) { 296 result = data[i]; 297 break; 298 } 299 } 300 } 301 if( typeof fn == "function" ) fn(result); 302 else $input.trigger("result", result && [result.data, result.value]); 303 } 304 $.each(trimWords($input.val()), function(i, value) { 305 request(value, findValueCallback, findValueCallback); 306 }); 307 }).bind("flushCache", function() { 308 cache.flush(); 309 }).bind("setOptions", function() { 310 $.extend(options, arguments[1]); 311 // if we've updated the data, repopulate 312 if ( "data" in arguments[1] ) 313 cache.populate(); 314 }).bind("unautocomplete", function() { 315 select.unbind(); 316 $input.unbind(); 317 $(input.form).unbind(".autocomplete"); 318 unbindDocumentMouseDownToClose(); 319 // Don't remove the shared live region - it may be used by other instances 320 }); 321 322 323 function selectCurrent() { 324 var selected = select.selected(); 325 if( !selected ) 326 return false; 327 328 var v = selected.result; 329 previousValue = v; 330 331 if ( options.multiple ) { 332 var words = trimWords($input.val()); 333 if ( words.length > 1 ) { 334 v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v; 335 } 336 v += options.multipleSeparator; 337 } 338 339 $input.val(v); 340 hideResultsNow(); 341 $input.trigger("result", [selected.data, selected.value]); 342 return true; 343 } 344 345 function onChange(crap, skipPrevCheck) { 346 if( lastKeyPressCode == KEY.DEL ) { 347 select.hide(); 348 // return; 349 } 350 351 var currentValue = $input.val(); 352 353 if ( !skipPrevCheck && currentValue == previousValue ) 354 return; 355 356 previousValue = currentValue; 357 358 currentValue = lastWord(currentValue); 359 if ( currentValue.length >= options.minChars) { 360 $input.addClass(options.loadingClass); 361 if (!options.matchCase) 362 currentValue = currentValue.toLowerCase(); 363 request(currentValue, receiveData, hideResultsNow); 364 } else { 365 stopLoading(); 366 select.hide(); 367 } 368 }; 369 370 function trimWords(value) { 371 if ( !value ) { 372 return [""]; 373 } 374 var words = value.split( options.multipleSeparator ); 375 var result = []; 376 $.each(words, function(i, value) { 377 if ( $.trim(value) ) 378 result[i] = $.trim(value); 379 }); 380 return result; 381 } 382 383 function lastWord(value) { 384 if ( !options.multiple ) 385 return value; 386 var words = trimWords(value); 387 return words[words.length - 1]; 388 } 389 390 // fills in the input box w/the first match (assumed to be the best match) 391 // q: the term entered 392 // sValue: the first matching result 393 function autoFill(q, sValue){ 394 // autofill in the complete box w/the first match as long as the user hasn't entered in more data 395 // if the last user key pressed was backspace, don't autofill 396 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { 397 // fill in the value (keep the case the user has typed) 398 $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); 399 // select the portion of the value not typed by the user (so the next character will erase) 400 $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length); 401 } 402 }; 403 404 function hideResults() { 405 clearTimeout(timeout); 406 timeout = setTimeout(hideResultsNow, 200); 407 }; 408 409 function hideResultsNow() { 410 var wasVisible = select.visible(); 411 select.hide(); 412 unbindDocumentMouseDownToClose(); 413 clearTimeout(timeout); 414 stopLoading(); 415 $countStatus.text(''); 416 $activeStatus.text(''); 417 if (options.mustMatch) { 418 // call search and run callback 419 $input.search( 420 function (result){ 421 // if no value found, clear the input box 422 if( !result ) { 423 if (options.multiple) { 424 var words = trimWords($input.val()).slice(0, -1); 425 $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); 426 } 427 else 428 $input.val( "" ); 429 } 430 } 431 ); 432 } 433 if (wasVisible) 434 // position cursor at end of input field 435 $.Autocompleter.Selection(input, input.value.length, input.value.length); 436 }; 437 438 function receiveData(q, data) { 439 if ( data && data.length && hasFocus ) { 440 stopLoading(); 441 // When a11yFocusListboxOnArrow is active (Chrome+VoiceOver forms), set 442 // aria-expanded="true" before filling the list. This places the state 443 // change and the aria-activedescendant assignment from setActiveItemAria 444 // in the same DOM-mutation batch so VoiceOver processes them together 445 // ("expanded" + active item text) instead of only seeing "expanded". 446 // Only do this on the initial open (when the combobox is still collapsed) 447 // to avoid redundant no-op mutations on subsequent results updates. 448 if (options.a11yFocusListboxOnArrow && $input.attr('aria-expanded') === 'false') { 449 $input.attr('aria-expanded', 'true'); 450 } 451 select.display(data, q); 452 autoFill(q, data[0].value); 453 select.show(); 454 bindDocumentMouseDownToClose(); 455 // Derive count from rendered list items after display() so the announcement 456 // matches what is actually visible (avoids overstating when defaultText is shown). 457 var $listEl = config.resultsId ? $('#' + config.resultsId) : $(); 458 var domCount = $listEl.length ? $listEl.find('li').length : null; 459 var count = domCount !== null 460 ? domCount 461 : (options.max && options.max < data.length ? options.max : data.length); 462 // When only the defaultText placeholder row is shown for an empty query, 463 // skip announcing "results available" to avoid misleading screen-reader users. 464 if (count === 1 && !q && options.defaultText !== false) { 465 $countStatus.text(''); 466 } else { 467 var statusText = (typeof Drupal !== 'undefined' && typeof Drupal.formatPlural === 'function') 468 ? Drupal.formatPlural(count, '1 result available', '@count results available') 469 : count + (count === 1 ? ' result available' : ' results available'); 470 $countStatus.text(statusText); 471 } 472 } else { 473 hideResultsNow(); 474 } 475 }; 476 477 function request(term, success, failure) { 478 if (!options.matchCase) 479 term = term.toLowerCase(); 480 var data = cache.load(term); 481 482 if( options.injectMatch && typeof options.injectMatch == 'function' ){ 483 var injectedMatch = options.injectMatch( data ); 484 if(typeof injectedMatch == 'object') 485 data = data.concat( injectedMatch ); 486 } 487 488 // recieve the cached data 489 if (data && data.length) { 490 success(term, data); 491 // if an AJAX url has been supplied, try loading the data now 492 } else if( (typeof options.url == "string") && (options.url.length > 0) ){ 493 494 var extraParams = { 495 timestamp: +new Date() 496 }; 497 $.each(options.extraParams, function(key, param) { 498 extraParams[key] = typeof param == "function" ? param() : param; 499 }); 500 501 $.ajax({
502 // try to leverage ajaxQueue plugin to abort previous requests 503 mode: "abort", 504 // limit abortion to this input 505 port: "autocomplete" + input.name, 506 dataType: options.dataType, 507 url: options.url, 508 beforeSend: options.beforeSend, 509 data: $.extend({ 510 jq: lastWord(term), 511 limit: options.max 512 }, extraParams), 513 success: function(data) { 514 var parsed = options.parse && options.parse(data) || parse(data); 515 cache.add(term, parsed); 516 success(term, parsed); 517 } 518 }); 519 } else { 520 // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match 521 select.emptyList(); 522 failure(term); 523 } 524 }; 525 526 function parse(data) { 527 var parsed = []; 528 var rows = data.split("\n"); 529 for (var i=0; i < rows.length; i++) { 530 var row = $.trim(rows[i]); 531 if (row) { 532 row = row.split("|"); 533 parsed[parsed.length] = { 534 data: row, 535 value: row[0], 536 result: options.formatResult && options.formatResult(row, row[0]) || row[0] 537 }; 538 } 539 } 540 return parsed; 541 }; 542 543 function stopLoading() { 544 $input.removeClass(options.loadingClass); 545 }; 546 547}; 548 549// ID of the single shared live region used by all autocomplete instances to 550// announce result counts to screen readers. 551$.Autocompleter.ANNOUNCER_ID = 'jquery-ac-announcer'; 552// ID of the shared live region used to announce the currently active option 553// while navigating results. 554$.Autocompleter.ACTIVE_ANNOUNCER_ID = 'jquery-ac-active-announcer'; 555 556$.Autocompleter.defaults = { 557 inputClass: "ac_input", 558 resultsClass: "ac_results", 559 loadingClass: "ac_loading", 560 minChars: 1, 561 delay: 400, 562 defaultText: false, 563 matchCase: false, 564 matchSubset: true, 565 matchContains: false, 566 cacheLength: 10, 567 max: 100, 568 mustMatch: false, 569 extraParams: {}, 570 selectFirst: true, 571 formatItem: function(row) { return row[0]; }, 572 formatMatch: null, 573 autoFill: false, 574 width: 0, 575 multiple: false, 576 multipleSeparator: ", ", 577 highlight: function(value, term) { 578 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); 579 }, 580 scroll: true, 581 scrollHeight: 180, 582 beforeSend: function(){}, 583 injectMatch: function(){}, 584 // When true, pressing arrow keys while the popup is open will move focus to 585 // the listbox and handle navigation there. This is a fallback for 586 // VoiceOver+Chrome, which can fail to announce aria-activedescendant changes 587 // while focus remains in the input. 588 a11yFocusListboxOnArrow: false 589}; 590 591$.Autocompleter.Cache = function(options) { 592 593 var data = {}; 594 var length = 0; 595 596 function matchSubset(s, sub) { if(s == '%') return true; // added to allow an "always on" option 597 if (!options.matchCase) 598 s = s.toLowerCase(); 599 var i = s.indexOf(sub); 600 if (i == -1) return false; 601 return i == 0 || options.matchContains; 602 }; 603 604 function add(q, value) { 605 if (length > options.cacheLength){ 606 flush(); 607 } 608 if (!data[q]){ 609 length++; 610 } 611 data[q] = value; 612 } 613 614 function populate(){ 615 if( !options.data ) return false; 616 // track the matches 617 var stMatchSets = {}, 618 nullData = 0; 619 620 // no url was specified, we need to adjust the cache length to make sure it fits the local data store 621 if( !options.url ) options.cacheLength = 1; 622 623 // track all options for minChars = 0 624 stMatchSets[""] = []; 625 626 // loop through the array and create a lookup structure 627 for ( var i = 0, ol = options.data.length; i < ol; i++ ) { 628 var rawValue = options.data[i]; 629 // if rawValue is a string, make an array otherwise just reference the array 630 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; 631 632 var value = options.formatMatch(rawValue, i+1, options.data.length); 633 if ( value === false ) 634 continue; 635 636 var firstChar = value.charAt(0).toLowerCase(); 637 // if no lookup array for this character exists, look it up now 638 if( !stMatchSets[firstChar] ) 639 stMatchSets[firstChar] = []; 640 641 // if the match is a string 642 var row = { 643 value: value, 644 data: rawValue, 645 result: options.formatResult && options.formatResult(rawValue) || value 646 }; 647 648 // push the current match into the set list 649 stMatchSets[firstChar].push(row); 650 651 // keep track of minChars zero items 652 if ( nullData++ < options.max ) { 653 stMatchSets[""].push(row); 654 } 655 }; 656 657 // add the data items to the cache 658 $.each(stMatchSets, function(i, value) { 659 // increase the cache size 660 options.cacheLength++; 661 // add to the cache 662 add(i, value); 663 });
664 } 665 666 // populate any existing data 667 setTimeout(populate, 25); 668 669 function flush(){ 670 data = {}; 671 length = 0; 672 } 673 674 return { 675 flush: flush, 676 add: add, 677 populate: populate, 678 load: function(q) { 679 if (!options.cacheLength || !length) 680 return null; 681 /* 682 * if dealing w/local data and matchContains than we must make sure 683 * to loop through all the data collections looking for matches 684 */ 685 if( !options.url && options.matchContains ){ 686 // track all matches 687 var csub = []; 688 // loop through all the data grids for matches 689 for( var k in data ){ 690 // don't search through the stMatchSets[""] (minChars: 0) cache 691 // this prevents duplicates 692 if( k.length > 0 ){ 693 var c = data[k]; 694 $.each(c, function(i, x) { 695 // if we've got a match, add it to the array 696 if (matchSubset(x.value, q)) { 697 csub.push(x); 698 } 699 }); 700 } 701 } 702 return csub; 703 } else 704 // if the exact item exists, use it 705 if (data[q]){ 706 return data[q]; 707 } else 708 if (options.matchSubset) { 709 for (var i = q.length - 1; i >= options.minChars; i--) { 710 var c = data[q.substr(0, i)]; 711 if (c) { 712 var csub = []; 713 $.each(c, function(i, x) { 714 if (matchSubset(x.value, q)) { 715 csub[csub.length] = x; 716 } 717 }); 718 return csub; 719 } 720 } 721 } 722 return null; 723 } 724 }; 725}; 726 727$.Autocompleter.Select = function (options, input, select, config) { 728 var CLASSES = { 729 ACTIVE: "ac_over", 730 EVEN: "ac_even", 731 ODD: "ac_odd" 732 }; 733 734 // Number of items to skip per PageUp/PageDown keystroke. 735 var PAGE_SIZE = 8; 736 737 var KEY = { 738 UP: 38, 739 DOWN: 40, 740 PAGEUP: 33, 741 PAGEDOWN: 34, 742 RETURN: 13, 743 TAB: 9, 744 ESC: 27 745 }; 746 747 var listItems, 748 active = -1, 749 data, 750 term = "", 751 needsInit = true, 752 element, 753 list; 754 755 function shouldAnnounceActiveOptionOnThisPlatform() { 756 try { 757 var platform = (navigator && navigator.platform) ? navigator.platform : ''; 758 var ua = (navigator && navigator.userAgent) ? navigator.userAgent : ''; 759 return /Mac/i.test(platform) || /Macintosh/i.test(ua); 760 } catch (e) { 761 return false; 762 } 763 } 764 765 function announceActiveOptionViaStatus($activeItem) { 766 if (!config || !config.$activeStatus || !shouldAnnounceActiveOptionOnThisPlatform()) { 767 return; 768 } 769 if (!$activeItem || !$activeItem.length) { 770 return; 771 } 772 var label = $.trim($activeItem.text()); 773 if (!label) { 774 return; 775 } 776 777 // Including position helps prevent repeated identical text from being ignored. 778 var position = (active >= 0 ? (active + 1) : 0); 779 var total = (listItems && listItems.length) ? listItems.length : 0; 780 var message = (position && total) ? (label + ', ' + position + ' of ' + total) : label; 781 // Some AT/browser combos won't announce if the update happens synchronously 782 // during keydown or if the text is "too similar". Clearing and updating on 783 // the next tick improves reliability. 784 config._activeAnnounceToken = (config._activeAnnounceToken || 0) + 1; 785 var token = config._activeAnnounceToken; 786 config.$activeStatus.text(''); 787 setTimeout(function() { 788 if (config._activeAnnounceToken !== token) { 789 return; 790 } 791 config.$activeStatus.text(message); 792 }, 0); 793 } 794 795 function setActiveItemAria($activeItem, shouldAnnounce) { 796 if (!listItems || !listItems.length) { 797 $(input).removeAttr('aria-activedescendant'); 798 list && list.removeAttr('aria-activedescendant'); 799 return; 800 } 801 802 listItems.attr('aria-selected', 'false'); 803 804 if (!$activeItem || !$activeItem.length) { 805 $(input).removeAttr('aria-activedescendant'); 806 return; 807 } 808 809 $activeItem.attr('aria-selected', 'true'); 810 var activeId = $activeItem.attr('id'); 811 if (activeId) { 812 $(input).attr('aria-activedescendant', activeId); 813 list && list.attr('aria-activedescendant', activeId); 814 } else { 815 $(input).removeAttr('aria-activedescendant'); 816 list && list.removeAttr('aria-activedescendant'); 817 } 818 819 if (shouldAnnounce) { 820 announceActiveOptionViaStatus($activeItem); 821 } 822 } 823 824 function clearActiveItemAria() { 825 $(input).removeAttr('aria-activedescendant'); 826 list && list.removeAttr('aria-activedescendant'); 827 listItems && listItems.attr('aria-selected', 'false'); 828 } 829 830 // Create results
831 function init() { 832 if (!needsInit) 833 return; 834 element = $("<div/>") 835 .hide() 836 .addClass(options.resultsClass) 837 .css("position", "absolute") 838 .appendTo(document.body); 839 840 list = $("<ul/>").attr({ 841 'role': 'listbox', 842 'id': config.resultsId, 843 'tabindex': '-1' 844 }).appendTo(element).mouseover( function(event) { 845 if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { 846 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); 847 var $activeItem = $(target(event)).addClass(CLASSES.ACTIVE); 848 setActiveItemAria($activeItem, false); 849 } 850 }).click(function(event) { 851 $(target(event)).addClass(CLASSES.ACTIVE); 852 select(); 853 // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus 854 try{input.focus();}catch(e){} 855 return false; 856 }).keydown(function(event) { 857 switch(event.keyCode) { 858 case KEY.UP: 859 event.preventDefault(); 860 moveSelect(-1); 861 return false; 862 case KEY.DOWN: 863 event.preventDefault(); 864 moveSelect(1); 865 return false; 866 case KEY.PAGEUP: 867 event.preventDefault(); 868 if (active != 0 && active - PAGE_SIZE < 0) { 869 moveSelect(-active); 870 } else { 871 moveSelect(-PAGE_SIZE); 872 } 873 return false; 874 case KEY.PAGEDOWN: 875 event.preventDefault(); 876 if (active != listItems.length - 1 && active + PAGE_SIZE > listItems.length) { 877 moveSelect(listItems.length - 1 - active); 878 } else { 879 moveSelect(PAGE_SIZE); 880 } 881 return false; 882 case KEY.RETURN: 883 case KEY.TAB: 884 if (select()) { 885 event.preventDefault(); 886 return false; 887 } 888 try{input.focus();}catch(e){} 889 return true; 890 case KEY.ESC: 891 event.preventDefault(); 892 // Delegate ESC handling to the input so it follows the input's 893 // own close logic for this key. 894 var escEvent = $.Event("keydown"); 895 escEvent.keyCode = KEY.ESC; 896 $(input).trigger(escEvent); 897 try{input.focus();}catch(e){} 898 return false; 899 default: 900 try{input.focus();}catch(e){} 901 return true; 902 } 903 }).mousedown(function() { 904 config.mouseDownOnSelect = true; 905 }).mouseup(function() { 906 config.mouseDownOnSelect = false; 907 }); 908 909 if( options.width > 0 ) 910 element.css("width", options.width); 911 912 needsInit = false; 913 } 914 915 function target(event) { 916 var element = event.target; 917 while(element && element.tagName != "LI") 918 element = element.parentNode; 919 // more fun with IE, sometimes event.target is empty, just ignore it then 920 if(!element) 921 return []; 922 return element; 923 } 924 925 function moveSelect(step) { 926 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); 927 movePosition(step); 928 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); 929 setActiveItemAria(activeItem, true); 930 try { 931 // If focus has been moved into the results listbox/options, keep focus 932 // on the active option so VoiceOver reads it. 933 if (document && document.activeElement && list && list.length) { 934 var $activeEl = $(document.activeElement); 935 var focusIsInResults = (document.activeElement === list[0]) || $activeEl.closest(list).length > 0; 936 if (focusIsInResults && activeItem && activeItem.length) { 937 activeItem.focus(); 938 } 939 } 940 } catch (e) {} 941 if(options.scroll) { 942 var offset = 0; 943 listItems.slice(0, active).each(function() { 944 offset += this.offsetHeight; 945 }); 946 if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { 947 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHe
947ight()); 948 } else if(offset < list.scrollTop()) { 949 list.scrollTop(offset); 950 } 951 } 952 }; 953 954 function movePosition(step) { 955 active += step; 956 if (active < 0) { 957 active = listItems.length - 1; 958 } else if (active >= listItems.length) { 959 active = 0; 960 } 961 } 962 963 function limitNumberOfItems(available) { 964 return options.max && options.max < available 965 ? options.max 966 : available; 967 } 968 969 function fillList() { 970 list.empty(); 971 var max = limitNumberOfItems(data.length); 972 973 if(term.length == 0 && options.defaultText !== false){ 974 var i = 0; 975 var li = $("<li/>") 976 .html(options.defaultText) 977 .addClass(i%2 == 0 ? CLASSES.EVEN : CLASSES.ODD) 978 .attr({ 979 'role': 'option', 980 'id': config.resultsId + '_option_' + i, 981 'tabindex': '-1', 982 'aria-selected': 'false' 983 }) 984 .appendTo(list)[0]; 985 $.data(li, "ac_data", data[i]); 986 } 987 else { 988 for (var i=0; i < max; i++) { 989 if (!data[i]) 990 continue; 991 var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); 992 if ( formatted === false ) 993 continue; 994 var li = $("<li/>") 995 .html(options.highlight(formatted, term)) 996 .addClass(i%2 == 0 ? CLASSES.EVEN : CLASSES.ODD) 997 .attr({ 998 'role': 'option', 999 'id': config.resultsId + '_option_' + i, 1000 'tabindex': '-1', 1001 'aria-selected': 'false' 1002 }) 1003 .appendTo(list)[0]; 1004 $.data(li, "ac_data", data[i]); 1005 } 1006 } 1007 1008 listItems = list.find("li"); 1009 clearActiveItemAria(); 1010 if ( options.selectFirst ) { 1011 listItems.slice(0, 1).addClass(CLASSES.ACTIVE); 1012 active = 0; 1013 // Announce the initially active option when the popup opens so VoiceOver 1014 // users get immediate feedback that results are available. 1015 setActiveItemAria(listItems.slice(0, 1), true); 1016 } 1017 // apply bgiframe if available 1018 if ( $.fn.bgiframe ) 1019 list.bgiframe(); 1020 } 1021 1022 return { 1023 display: function(d, q) { 1024 init(); 1025 data = d; 1026 term = q; 1027 fillList(); 1028 }, 1029 next: function() { 1030 moveSelect(1); 1031 }, 1032 prev: function() { 1033 moveSelect(-1); 1034 }, 1035 pageUp: function() { 1036 if (active != 0 && active - PAGE_SIZE < 0) { 1037 moveSelect( -active ); 1038 } else { 1039 moveSelect(-PAGE_SIZE); 1040 } 1041 }, 1042 pageDown: function() { 1043 if (active != listItems.length - 1 && active + PAGE_SIZE > listItems.length) { 1044 moveSelect( listItems.length - 1 - active ); 1045 } else { 1046 moveSelect(PAGE_SIZE); 1047 } 1048 }, 1049 hide: function() { 1050 element && element.hide(); 1051 listItems && listItems.removeClass(CLASSES.ACTIVE); 1052 active = -1; 1053 $(input).attr('aria-expanded', 'false'); 1054 clearActiveItemAria(); 1055 if (config) { 1056 if (config.$countStatus) config.$countStatus.text(''); 1057 if (config.$activeStatus) config.$activeStatus.text(''); 1058 } 1059 try { 1060 if ( 1061 document && 1062 document.activeElement && 1063 list && 1064 list.length && 1065 (document.activeElement === list[0] || $(document.activeElement).closest(list).length > 0) 1066 ) { 1067 input.focus(); 1068 } 1069 } catch (e) {} 1070 }, 1071 visible : function() { 1072 return element && element.is(":visible"); 1073 }, 1074 contains: function(targetEl) { 1075 try { 1076 if (!element || !element.length || !targetEl) { 1077 return false; 1078 } 1079 return targetEl === element[0] || $(targetEl).closest(element).length > 0; 1080 } catch (e) { 1081 return false; 1082 } 1083 }, 1084 isFocusInResults: function() { 1085 try { 1086 if (!element || !element.is(":visible") || !list || !list.length) { 1087 return false; 1088 } 1089 var activeElement = document && document.activeElement ? document.activeElement : null; 1090 if (!activeElement) { 1091 return false; 1092 } 1093 if (activeElement === list[0]) { 1094 return true; 1095 } 1096 return $(activeElement).closest(list).length > 0;
1097 } catch (e) { 1098 return false; 1099 } 1100 }, 1101 current: function() { 1102 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); 1103 }, 1104 show: function() { 1105 var anchorObj = typeof options.anchorTo != 'undefined' ? $(options.anchorTo) : $(input); 1106 var offset = anchorObj.offset(); 1107 var newWidth = typeof options.width == "string" || options.width > 0 ? options.width : (anchorObj.outerWidth() - parseInt(anchorObj.css('border-left-width')) - parseInt(anchorObj.css('border-right-width'))); 1108 element.css({ 1109 width: newWidth, 1110 top: offset.top + anchorObj.get(0).offsetHeight, 1111 left: offset.left 1112 }).show(); 1113 if(options.scroll) { 1114 list.scrollTop(0); 1115 list.css({ 1116 maxHeight: options.scrollHeight, 1117 overflow: 'auto' 1118 }); 1119 } 1120 $(input).attr('aria-expanded', 'true'); 1121 }, 1122 focusResults: function() { 1123 init(); 1124 if (!list) { 1125 return; 1126 } 1127 try{ 1128 if (listItems && active >= 0) { 1129 var $activeItem = listItems.slice(active, active + 1); 1130 if ($activeItem && $activeItem.length) { 1131 $activeItem.focus(); 1132 return; 1133 } 1134 } 1135 list.focus(); 1136 }catch(e){} 1137 }, 1138 selected: function() { 1139 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); 1140 return selected && selected.length && $.data(selected[0], "ac_data"); 1141 }, 1142 emptyList: function (){ 1143 list && list.empty(); 1144 }, 1145 unbind: function() { 1146 element && element.remove(); 1147 } 1148 }; 1149}; 1150 1151$.Autocompleter.Selection = function(field, start, end) { 1152 if( field.createTextRange ){ 1153 var selRange = field.createTextRange(); 1154 selRange.collapse(true); 1155 selRange.moveStart("character", start); 1156 selRange.moveEnd("character", end); 1157 selRange.select(); 1158 } else if( field.setSelectionRange ){ 1159 field.setSelectionRange(start, end); 1160 } else { 1161 if( field.selectionStart ){ 1162 field.selectionStart = start; 1163 field.selectionEnd = end; 1164 } 1165 } 1166 field.focus(); 1167}; 1168 1169})(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.