PageSourceSearch

https://learn.mcquaid.org/sites/all/misc/jquery-autocomplete/jquery.autocomplete.js?qrstuv

js mcquaid.org collected 2026-10-02 07:53:12 UTC 37,658 bytes, 1,169 lines download raw bytes

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.