1/* 2 * Autocomplete - jQuery plugin 1.1pre 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 5785 2008-07-12 10:37:33Z joern.zaefferer $ 11 * 12 */ 13 14;(function($) { 15 16$.fn.extend({ 17 autocomplete: function(urlOrData, options) { 18 var isUrl = typeof urlOrData == "string"; 19 options = $.extend({}, $.Autocompleter.defaults, { 20 url: isUrl ? urlOrData : null, 21 data: isUrl ? null : urlOrData, 22 delay: isUrl ? $.Autocompleter.defaults.delay : 10, 23 max: options && !options.scroll ? 10 : 150 24 }, options); 25 26 // if highlight is set to false, replace it with a do-nothing function 27 options.highlight = options.highlight || function(value) { return value; }; 28 29 // if the formatMatch option is not specified, then use formatItem for backwards compatibility 30 options.formatMatch = options.formatMatch || options.formatItem; 31 32 return this.each(function() { 33 new $.Autocompleter(this, options); 34 }); 35 }, 36 result: function(handler) { 37 return this.bind("result", handler); 38 }, 39 onSelect: null, 40 search: function(handler) { 41 return this.trigger("search", [handler]); 42 }, 43 flushCache: function() { 44 return this.trigger("flushCache"); 45 }, 46 setOptions: function(options){ 47 return this.trigger("setOptions", [options]); 48 }, 49 unautocomplete: function() { 50 return this.trigger("unautocomplete"); 51 } 52}); 53 54$.Autocompleter = function(input, options) { 55 56 var KEY = { 57 UP: 38, 58 DOWN: 40, 59 DEL: 46, 60 TAB: 9, 61 RETURN: 13, 62 ESC: 27, 63 COMMA: 188, 64 PAGEUP: 33, 65 PAGEDOWN: 34, 66 BACKSPACE: 8 67 }; 68 69 // Create $ object for input element 70 var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); 71 72 var timeout; 73 var previousValue = ""; 74 var cache = $.Autocompleter.Cache(options); 75 var hasFocus = 0; 76 var lastKeyPressCode; 77 var config = { 78 mouseDownOnSelect: false 79 }; 80 var select = $.Autocompleter.Select(options, input, selectCurrent, config); 81 82 var blockSubmit; 83 84 // prevent form submit in opera when selecting with return key 85 // jquery 3.7.1 does not support $.browser object 86 /* $.browser.opera && $(input.form).bind("submit.autocomplete", function() { 87 if (blockSubmit) { 88 blockSubmit = false; 89 return false; 90 } 91 });*/ 92 93 // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all 94 $input.bind("keydown.autocomplete", function(event) { 95 // track last key pressed 96 lastKeyPressCode = event.keyCode; 97 switch(event.keyCode) { 98 99 case KEY.UP: 100 event.preventDefault(); 101 if ( select.visible() ) { 102 select.prev(); 103 } else { 104 onChange(0, true); 105 } 106 break; 107 108 case KEY.DOWN: 109 event.preventDefault(); 110 if ( select.visible() ) { 111 select.next(); 112 } else { 113 onChange(0, true); 114 } 115 break; 116 117 case KEY.PAGEUP: 118 event.preventDefault(); 119 if ( select.visible() ) { 120 select.pageUp(); 121 } else { 122 onChange(0, true); 123 } 124 break; 125 126 case KEY.PAGEDOWN: 127 event.preventDefault(); 128 if ( select.visible() ) { 129 select.pageDown(); 130 } else { 131 onChange(0, true); 132 } 133 break; 134 135 // matches also semicolon 136 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: 137 case KEY.TAB: 138 case KEY.RETURN: 139 if( selectCurrent() ) { 140 // stop default to prevent a form submit, Opera needs special handling 141 event.preventDefault(); 142 blockSubmit = true; 143 return false; 144 } 145 break; 146 147 case KEY.ESC: 148 select.hide(); 149 break; 150 151 default: 152 clearTimeout(timeout); 153 timeout = setTimeout(onChange, options.delay); 154 break; 155 } 156 }).focus(function(){ 157 // track whether the field has focus, we shouldn't process any 158 // results if the field no longer has focus 159 hasFocus++; 160 }).blur(function() { 161 hasFocus = 0; 162 if (!config.mouseDownOnSelect) { 163 hideResults(); 164 } 165 }).click(function() { 166 // show select when clicking in a focused field 167 if ( hasFocus++ > 1 && !select.visible() ) { 168 onChange(0, true); 169 } 170 }).bind("search", function() { 171 // TODO why not just specifying both arguments? 172 var fn = (arguments.length > 1) ? arguments[1] : null; 173 function findValueCallback(q, data) { 174 var result; 175 if( data && data.length ) { 176 for (var i=0; i < data.length; i++) { 177 if( data[i].result.toLowerCase() == q.toLowerCase() ) { 178 result = data[i]; 179 break; 180 } 181 } 182 } 183 if( typeof fn == "function" ) fn(result); 184 else $input.trigger("result", result && [result.data, result.value]); 185 } 186 $.each(trimWords($input.val()), function(i, value) { 187 request(value, findValueCallback, findValueCallback); 188 }); 189 }).bind("flushCache", function() { 190 cache.flush(); 191 }).bind("setOptions", function() { 192 $.extend(options, arguments[1]); 193 // if we've updated the data, repopulate 194 if ( "data" in arguments[1] ) 195 cache.populate(); 196 }).bind("unautocomplete", function() { 197 select.unbind(); 198 $input.unbind(); 199 $(input.form).unbind(".autocomplete"); 200 }); 201 202 203 function selectCurrent() { 204 var selected = select.selected(); 205 if( !selected ) 206 return false;
207 208 var v = selected.result; 209 previousValue = v; 210 211 if ( options.multiple ) { 212 var words = trimWords($input.val()); 213 if ( words.length > 1 ) { 214 v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v; 215 } 216 v += options.multipleSeparator; 217 } 218 219 $input.val(v); 220 hideResultsNow(); 221 $input.trigger("result", [selected.data, selected.value]); 222 if($.isFunction(options.onSelect)) options.onSelect.apply(); 223 return true; 224 } 225 226 function onChange(crap, skipPrevCheck) { 227 if( lastKeyPressCode == KEY.DEL ) { 228 select.hide(); 229 return; 230 } 231 232 var currentValue = $input.val(); 233 234 if ( !skipPrevCheck && currentValue == previousValue ) 235 return; 236 237 previousValue = currentValue; 238 239 currentValue = lastWord(currentValue); 240 if ( currentValue.length >= options.minChars) { 241 $input.addClass(options.loadingClass); 242 if (!options.matchCase) 243 currentValue = currentValue.toLowerCase(); 244 request(currentValue, receiveData, hideResultsNow); 245 } else { 246 stopLoading(); 247 select.hide(); 248 } 249 }; 250 251 function trimWords(value) { 252 if ( !value ) { 253 return [""]; 254 } 255 var words = value.split( options.multipleSeparator ); 256 var result = []; 257 $.each(words, function(i, value) { 258 if ( $.trim(value) ) 259 result[i] = $.trim(value); 260 }); 261 return result; 262 } 263 264 function lastWord(value) { 265 if ( !options.multiple ) 266 return value; 267 var words = trimWords(value); 268 return words[words.length - 1]; 269 } 270 271 // fills in the input box w/the first match (assumed to be the best match) 272 // q: the term entered 273 // sValue: the first matching result 274 function autoFill(q, sValue){ 275 // autofill in the complete box w/the first match as long as the user hasn't entered in more data 276 // if the last user key pressed was backspace, don't autofill 277 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { 278 // fill in the value (keep the case the user has typed) 279 $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); 280 // select the portion of the value not typed by the user (so the next character will erase) 281 $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length); 282 } 283 }; 284 285 function hideResults() { 286 clearTimeout(timeout); 287 timeout = setTimeout(hideResultsNow, 200); 288 }; 289 290 function hideResultsNow() { 291 var wasVisible = select.visible(); 292 select.hide(); 293 clearTimeout(timeout); 294 stopLoading(); 295 if (options.mustMatch) { 296 // call search and run callback 297 $input.search( 298 function (result){ 299 // if no value found, clear the input box 300 if( !result ) { 301 if (options.multiple) { 302 var words = trimWords($input.val()).slice(0, -1); 303 $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); 304 } 305 else 306 $input.val( "" ); 307 } 308 } 309 ); 310 } 311 if (wasVisible) 312 // position cursor at end of input field 313 $.Autocompleter.Selection(input, input.value.length, input.value.length); 314 }; 315 316 function receiveData(q, data) { 317 if ( data && data.length && hasFocus ) { 318 stopLoading(); 319 select.display(data, q); 320 autoFill(q, data[0].value); 321 select.show(); 322 } else { 323 hideResultsNow(); 324 } 325 }; 326 327 function request(term, success, failure) { 328 if (!options.matchCase) 329 term = term.toLowerCase(); 330 //var data = cache.load(term); 331 var data = ""; 332 // recieve the cached data 333 if (data && data.length) { 334 success(term, data); 335 // if an AJAX url has been supplied, try loading the data now 336 } else if( (typeof options.url == "string") && (options.url.length > 0) ){ 337 338 var extraParams = { 339 timestamp: +new Date() 340 }; 341 $.each(options.extraParams, function(key, param) { 342 extraParams[key] = typeof param == "function" ? param() : param; 343 }); 344 345 $.ajax({ 346 // try to leverage ajaxQueue plugin to abort previous requests 347 mode: "abort", 348 // limit abortion to this input 349 port: "autocomplete" + input.name, 350 dataType: options.dataType, 351 url: options.url, 352 data: $.extend({ 353 q: lastWord(term), 354 limit: options.max 355 }, extraParams), 356 success: function(data) { 357 var parsed = options.parse && options.parse(data) || parse(data); 358 cache.add(term, parsed); 359 success(term, parsed); 360 } 361 }); 362 } else { 363 // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match 364 select.emptyList(); 365 failure(term); 366 } 367 }; 368 369 function parse(data) { 370 var parsed = []; 371 var rows = data.split("\n"); 372 for (var i=0; i < rows.length; i++) { 373 var row = $.trim(rows[i]); 374 if (row) { 375 row = row.split("|"); 376 parsed[parsed.length] = { 377 data: row, 378 value: row[0], 379 result: options.formatResult && options.formatResult(row, row[0]) || row[1] 380 }; 381 } 382 } 383 return parsed; 384 }; 385 386 function stopLoading() { 387 $input.removeClass(options.loadingClass); 388 }; 389 390}; 391 392$.Autocompleter.defaults = { 393 inputClass: "ac_input", 394 resultsClass: "ac_results", 395 resultsClassPlus: "", 396 loadingClass: "ac_loading", 397 minChars: 1, 398 delay: 400, 399 matchCase: false, 400 matchSubset: true,
401 matchContains: false, 402 cacheLength: 10, 403 max: 100, 404 mustMatch: false, 405 extraParams: {}, 406 selectFirst: true, 407 formatItem: function(row) { return row[0]; }, 408 formatMatch: null, 409 autoFill: false, 410 width: 0, 411 multiple: false, 412 multipleSeparator: ", ", 413 highlight: function(value, term) { 414 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); 415 }, 416 scroll: true, 417 scrollHeight: 260 418}; 419 420$.Autocompleter.Cache = function(options) { 421 422 var data = {}; 423 var length = 0; 424 425 function matchSubset(s, sub) { 426 if (!options.matchCase) 427 s = s.toLowerCase(); 428 var i = s.indexOf(sub); 429 if (options.matchContains == "word"){ 430 i = s.toLowerCase().search("\\b" + sub.toLowerCase()); 431 } 432 if (i == -1) return false; 433 return i == 0 || options.matchContains; 434 }; 435 436 function add(q, value) { 437 if (length > options.cacheLength){ 438 flush(); 439 } 440 if (!data[q]){ 441 length++; 442 } 443 data[q] = value; 444 } 445 446 function populate(){ 447 if( !options.data ) return false; 448 // track the matches 449 var stMatchSets = {}, 450 nullData = 0; 451 452 // no url was specified, we need to adjust the cache length to make sure it fits the local data store 453 if( !options.url ) options.cacheLength = 1; 454 455 // track all options for minChars = 0 456 stMatchSets[""] = []; 457 458 // loop through the array and create a lookup structure 459 for ( var i = 0, ol = options.data.length; i < ol; i++ ) { 460 var rawValue = options.data[i]; 461 // if rawValue is a string, make an array otherwise just reference the array 462 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; 463 464 var value = options.formatMatch(rawValue, i+1, options.data.length); 465 if ( value === false ) 466 continue; 467 468 var firstChar = value.charAt(0).toLowerCase(); 469 // if no lookup array for this character exists, look it up now 470 if( !stMatchSets[firstChar] ) 471 stMatchSets[firstChar] = []; 472 473 // if the match is a string 474 var row = { 475 value: value, 476 data: rawValue, 477 result: options.formatResult && options.formatResult(rawValue) || value 478 }; 479 480 // push the current match into the set list 481 stMatchSets[firstChar].push(row); 482 483 // keep track of minChars zero items 484 if ( nullData++ < options.max ) { 485 stMatchSets[""].push(row); 486 } 487 }; 488 489 // add the data items to the cache 490 $.each(stMatchSets, function(i, value) { 491 // increase the cache size 492 options.cacheLength++; 493 // add to the cache 494 add(i, value); 495 }); 496 } 497 498 // populate any existing data 499 setTimeout(populate, 25); 500 501 function flush(){ 502 data = {}; 503 length = 0; 504 } 505 506 return { 507 flush: flush, 508 add: add, 509 populate: populate, 510 load: function(q) { 511 if (!options.cacheLength || !length) 512 return null; 513 /* 514 * if dealing w/local data and matchContains than we must make sure 515 * to loop through all the data collections looking for matches 516 */ 517 if( !options.url && options.matchContains ){ 518 // track all matches 519 var csub = []; 520 // loop through all the data grids for matches 521 for( var k in data ){ 522 // don't search through the stMatchSets[""] (minChars: 0) cache 523 // this prevents duplicates 524 if( k.length > 0 ){ 525 var c = data[k]; 526 $.each(c, function(i, x) { 527 // if we've got a match, add it to the array 528 if (matchSubset(x.value, q)) { 529 csub.push(x); 530 } 531 }); 532 } 533 } 534 return csub; 535 } else 536 // if the exact item exists, use it 537 if (data[q]){ 538 return data[q]; 539 } else 540 if (options.matchSubset) { 541 for (var i = q.length - 1; i >= options.minChars; i--) { 542 var c = data[q.substr(0, i)]; 543 if (c) { 544 var csub = []; 545 $.each(c, function(i, x) { 546 if (matchSubset(x.value, q)) { 547 csub[csub.length] = x; 548 } 549 }); 550 return csub; 551 } 552 } 553 } 554 return null; 555 } 556 }; 557}; 558 559$.Autocompleter.Select = function (options, input, select, config) { 560 var CLASSES = { 561 ACTIVE: "ac_over" 562 }; 563 564 var listItems, 565 active = -1, 566 data, 567 term = "", 568 needsInit = true, 569 element, 570 list; 571 572 // Create results
573 function init() { 574 if (!needsInit) 575 return; 576 577 if ($("."+options.resultsClass).length==0) { 578 element = $("<div/>") 579 .hide() 580 .addClass(options.resultsClass) 581 .css("position", "absolute") 582 .appendTo(document.body); 583 } else { 584 element=$("."+options.resultsClass); 585 } 586 if (options.resultsClassPlus!="") element.addClass(options.resultsClassPlus); 587 588 list = $("<ul/>").appendTo(element).mouseover( function(event) { 589 if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { 590 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); 591 $(target(event)).addClass(CLASSES.ACTIVE); 592 } 593 }).click(function(event) { 594 if (event.target.closest('.info-icon') || event.target.closest('.icon--info')) { 595 return false; 596 } 597 $(target(event)).addClass(CLASSES.ACTIVE); 598 select(); 599 // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus 600 input.focus(); 601 return false; 602 }).mousedown(function() { 603 config.mouseDownOnSelect = true; 604 }).mouseup(function() { 605 config.mouseDownOnSelect = false; 606 }); 607 608 if( options.width > 0 ) 609 element.css("width", options.width); 610 611 needsInit = false; 612 } 613 614 function target(event) { 615 var element = event.target; 616 while(element && element.tagName != "LI") 617 element = element.parentNode; 618 // more fun with IE, sometimes event.target is empty, just ignore it then 619 if(!element) 620 return []; 621 return element; 622 } 623 624 function moveSelect(step) { 625 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); 626 movePosition(step); 627 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); 628 if(options.scroll) { 629 var offset = 0; 630 listItems.slice(0, active).each(function() { 631 offset += this.offsetHeight; 632 }); 633 if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { 634 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); 635 } else if(offset < list.scrollTop()) { 636 list.scrollTop(offset); 637 } 638 } 639 } 640 641 function movePosition(step) { 642 active += step; 643 if (active < 0) { 644 active = listItems.length - 1; 645 } else if (active >= listItems.length) { 646 active = 0; 647 } 648 } 649 650 function limitNumberOfItems(available) { 651 return options.max && options.max < available 652 ? options.max 653 : available; 654 } 655 656 function fillList() { 657 list.empty(); 658 var max = limitNumberOfItems(data.length); 659 for (var i=0; i < max; i++) { 660 if (!data[i]) 661 continue; 662 var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); 663 if ( formatted === false ) 664 continue; 665 666 (data[i]["result"].indexOf("unas_category_link") >= 0) ? is_cat=" category_item" : is_cat=""; 667 668 var li = $("<li/>").html( options.highlight(formatted, term) ).attr('role','link').addClass(i%2 == 0 ? "ac_even"+is_cat : "ac_odd"+is_cat).appendTo(list)[0]; 669 $.data(li, "ac_data", data[i]); 670 } 671 listItems = list.find("li"); 672 if ( options.selectFirst ) { 673 listItems.slice(0, 1).addClass(CLASSES.ACTIVE); 674 active = 0; 675 } 676 // apply bgiframe if available 677 if ( $.fn.bgiframe ) 678 list.bgiframe(); 679 } 680 681 return { 682 display: function(d, q) { 683 init(); 684 data = d; 685 term = q; 686 fillList(); 687 $(document).trigger("autoCompleteListDisplayed"); 688 }, 689 next: function() { 690 moveSelect(1); 691 }, 692 prev: function() { 693 moveSelect(-1); 694 }, 695 pageUp: function() { 696 if (active != 0 && active - 8 < 0) { 697 moveSelect( -active ); 698 } else { 699 moveSelect(-8); 700 } 701 }, 702 pageDown: function() { 703 if (active != listItems.length - 1 && active + 8 > listItems.length) { 704 moveSelect( listItems.length - 1 - active ); 705 } else { 706 moveSelect(8); 707 } 708 }, 709 hide: function() { 710 element && element.hide().removeClass('show'); 711 listItems && listItems.removeClass(CLASSES.ACTIVE); 712 active = -1; 713 $(document).trigger("autoCompleteListHide"); 714 }, 715 visible : function() { 716 return element && element.is(":visible"); 717 }, 718 current: function() { 719 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); 720 }, 721 show: function() { 722 var offset = $(input).offset(); 723 //var input_height = $(input).height(); 724 element.css({ 725 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), 726 top: offset.top + input.offsetHeight, 727 left: offset.left 728 //top: input_height, 729 //left:0 730 }).show().addClass('show'); 731 if(options.scroll) { 732 list.scrollTop(0); 733 list.css({ 734 maxHeight: options.scrollHeight, 735 overflow: 'auto' 736 }); 737 738 // remove ie support 739/* if($.browser.msie && typeof document.body.style.maxHeight === "undefined") { 740 var listHeight = 0; 741 listItems.each(function() { 742 listHeight += this.offsetHeight; 743 }); 744 var scrollbarsVisible = listHeight > options.scrollHeight; 745 list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); 746 if (!scrollbarsVisible) { 747 // IE doesn't recalculate width when scrollbar disappears 748 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); 749 } 750 }*/ 751 752 } 753 }, 754 selected: function() { 755 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); 756 return selected && selected.length && $.data(selected[0], "ac_data"); 757 }, 758 emptyList: function (){ 759 list && list.empty(); 760 }, 761 unbind: function() { 762 element && element.remove(); 763 } 764 }; 765}; 766 767$.Autocompleter.Selection = function(field, start, end) { 768 if( field.createTextRange ){ 769 var selRange = field.createTextRange(); 770 selRange.collapse(true); 771 selRange.moveStart("character", start); 772 selRange.moveEnd("character", end); 773 selRange.select(); 774 } else if( field.setSelectionRange ){ 775 field.setSelectionRange(start, end); 776 } else { 777 if( field.selectionStart ){ 778 field.selectionStart = start; 779 field.selectionEnd = end; 780 } 781 } 782 field.focus(); 783}; 784 785})(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.