1/* 2 * jQuery Autocomplete plugin 1.1 3 * 4 * Copyright (c) 2009 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 15 2009-08-22 10:30:27Z joern.zaefferer $ 11 */; 12$.browser = navigator.userAgent; 13(function($) { 14 $.fn.extend({ 15 autocomplete: function(urlOrData, options) { 16 var isUrl = typeof urlOrData == "string"; 17 options = $.extend({}, $.Autocompleter.defaults, { 18 url: isUrl ? urlOrData : null, 19 data: isUrl ? null : urlOrData, 20 delay: isUrl ? $.Autocompleter.defaults.delay : 10, 21 max: options && !options.scroll ? 10 : 150 22 }, options); 23 options.highlight = options.highlight || 24 function(value) { 25 return value; 26 }; 27 options.formatMatch = options.formatMatch || options.formatItem; 28 return this.each(function() { 29 new $.Autocompleter(this, options); 30 }); 31 }, 32 result: function(handler) { 33 return this.bind("result", handler); 34 }, 35 search: function(handler) { 36 return this.trigger("search", [handler]); 37 }, 38 flushCache: function() { 39 return this.trigger("flushCache"); 40 }, 41 setOptions: function(options) { 42 return this.trigger("setOptions", [options]); 43 }, 44 unautocomplete: function() { 45 return this.trigger("unautocomplete"); 46 } 47 }); 48 $.Autocompleter = function(input, options) { 49 var KEY = { 50 UP: 38, 51 DOWN: 40, 52 DEL: 46, 53 TAB: 9, 54 RETURN: 13, 55 ESC: 27, 56 COMMA: 188, 57 PAGEUP: 33, 58 PAGEDOWN: 34, 59 BACKSPACE: 8 60 }; 61 var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); 62 var timeout; 63 var previousValue = ""; 64 var cache = $.Autocompleter.Cache(options); 65 var hasFocus = 0; 66 var lastKeyPressCode; 67 var config = { 68 mouseDownOnSelect: false 69 }; 70 var select = $.Autocompleter.Select(options, input, selectCurrent, config); 71 var blockSubmit; 72 $.browser.opera && $(input.form).bind("submit.autocomplete", function() { 73 if (blockSubmit) { 74 blockSubmit = false; 75 return false; 76 } 77 }); 78 $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { 79 hasFocus = 1; 80 lastKeyPressCode = event.keyCode; 81 switch (event.keyCode) { 82 case KEY.UP: 83 event.preventDefault(); 84 if (select.visible()) { 85 select.prev(); 86 } else { 87 onChange(0, true); 88 } 89 break; 90 case KEY.DOWN: 91 event.preventDefault(); 92 if (select.visible()) { 93 select.next(); 94 } else { 95 onChange(0, true); 96 } 97 break; 98 case KEY.PAGEUP: 99 event.preventDefault(); 100 if (select.visible()) { 101 select.pageUp(); 102 } else { 103 onChange(0, true); 104 } 105 break; 106 case KEY.PAGEDOWN: 107 event.preventDefault(); 108 if (select.visible()) { 109 select.pageDown(); 110 } else { 111 onChange(0, true); 112 } 113 break; 114 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: 115 116 case KEY.TAB: 117 if( selectCurrent() ) { 118 // stop default to prevent a form submit, Opera needs special handling 119 event.preventDefault(); 120 blockSubmit = true; 121 return false;
122 } 123 break; 124 case KEY.RETURN: 125 clearTimeout(timeout); 126 timeout = setTimeout(onChange, options.delay); 127 break; 128 case KEY.ESC: 129 select.hide(); 130 break; 131 default: 132 clearTimeout(timeout); 133 timeout = setTimeout(onChange, options.delay); 134 break; 135 } 136 }).focus(function() { 137 hasFocus++; 138 }).blur(function() { 139 hasFocus = 0; 140 if (!config.mouseDownOnSelect) { 141 hideResults(); 142 } 143 }).click(function() { 144 if (hasFocus++ > 1 && !select.visible()) { 145 onChange(0, true); 146 } 147 }).bind("search", function() { 148 var fn = (arguments.length > 1) ? arguments[1] : null; 149 150 function findValueCallback(q, data) { 151 var result; 152 if (data && data.length) { 153 for (var i = 0; i < data.length; i++) { 154 if (data[i].result.toLowerCase() == q.toLowerCase()) { 155 result = data[i]; 156 break; 157 } 158 } 159 } 160 if (typeof fn == "function") fn(result); 161 else $input.trigger("result", result && [result.data, result.value]); 162 } 163 $.each(trimWords($input.val()), function(i, value) { 164 request(value, findValueCallback, findValueCallback); 165 }); 166 }).bind("flushCache", function() { 167 cache.flush(); 168 }).bind("setOptions", function() { 169 $.extend(options, arguments[1]); 170 if ("data" in arguments[1]) cache.populate(); 171 }).bind("unautocomplete", function() { 172 select.unbind(); 173 $input.unbind(); 174 $(input.form).unbind(".autocomplete"); 175 }); 176 177 function selectCurrent() { 178 var selected = select.selected(); 179 if (!selected) return false; 180 var v = selected.result; 181 previousValue = v; 182 if (options.multiple) { 183 var words = trimWords($input.val()); 184 if (words.length > 1) { 185 var seperator = options.multipleSeparator.length; 186 var cursorAt = $(input).selection().start; 187 var wordAt, progress = 0; 188 $.each(words, function(i, word) { 189 progress += word.length; 190 if (cursorAt <= progress) { 191 wordAt = i; 192 return false; 193 } 194 progress += seperator; 195 }); 196 words[wordAt] = v; 197 v = words.join(options.multipleSeparator); 198 } 199 v += options.multipleSeparator; 200 } 201 $input.val(v); 202 hideResultsNow(); 203 $input.trigger("result", [selected.data, selected.value]); 204 return true; 205 } 206 function onChange(crap, skipPrevCheck) { 207 if (lastKeyPressCode == KEY.DEL) { 208 select.hide(); 209 return; 210 } 211 var currentValue = $input.val(); 212 if (!skipPrevCheck && currentValue == previousValue) return; 213 previousValue = currentValue; 214 currentValue = lastWord(currentValue); 215 if (currentValue.length >= options.minChars) { 216 $input.addClass(options.loadingClass); 217 if (!options.matchCase) currentValue = currentValue.toLowerCase(); 218 request(currentValue, receiveData, hideResultsNow); 219 } else { 220 stopLoading(); 221 select.hide(); 222 } 223 }; 224 225 function trimWords(value) { 226 if (!value) return [""]; 227 if (!options.multiple) return [$.trim(value)]; 228 return $.map(value.split(options.multipleSeparator), function(word) { 229 return $.trim(value).length ? $.trim(word) : null; 230 }); 231 } 232 function lastWord(value) { 233 if (!options.multiple) return value; 234 var words = trimWords(value); 235 if (words.length == 1) return words[0]; 236 var cursorAt = $(input).selection().start; 237 if (cursorAt == value.length) { 238 words = trimWords(value) 239 } else { 240 words = trimWords(value.replace(value.substring(cursorAt), "")); 241 } 242 return words[words.length - 1]; 243 } 244 function autoFill(q, sValue) { 245 if (options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE) { 246 $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); 247 $(input).selection(previousValue.length, previousValue.length + sValue.length); 248 } 249 }; 250 251 function hideResults() { 252 clearTimeout(timeout); 253 timeout = setTimeout(hideResultsNow, 200); 254 }; 255 256 function hideResultsNow() { 257 var wasVisible = select.visible(); 258 select.hide(); 259 clearTimeout(timeout); 260 stopLoading(); 261 if (options.mustMatch) { 262 $input.search(function(result) { 263 if (!result) { 264 if (options.multiple) { 265 var words = trimWords($input.val()).slice(0, -1); 266 $input.val(words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "")); 267 } else { 268 $input.val(""); 269 $input.trigger("result", null); 270 } 271 } 272 }); 273 } 274 }; 275 276 function receiveData(q, data) { 277 if (data && data.length && hasFocus) { 278 stopLoading(); 279 select.display(data, q); 280 autoFill(q, data[0].value); 281 select.show(); 282 } else { 283 hideResultsNow(); 284 } 285 }; 286 287 function request(term, success, failure) { 288 if (!options.matchCase) term = term.toLowerCase(); 289 var data = cache.load(term); 290 if (data && data.length) { 291 success(term, data); 292 } else if ((typeof options.url == "string") && (options.url.length > 0)) { 293 var extraParams = { 294 timestamp: +new Date() 295 }; 296 $.each(options.extraParams, function(key, param) { 297 extraParams[key] = typeof param == "function" ? param() : param; 298 }); 299 $.ajax({ 300 mode: "abort", 301 port: "autocomplete" + input.name, 302 dataType: options.dataType, 303 url: options.url, 304 data: $.extend({ 305 wd: lastWord(term), 306 limit: options.max 307 }, extraParams), 308 success: function(data) { 309 var parsed = options.parse && options.parse(data) || parse(data); 310 cache.add(term, parsed); 311 success(term, parsed); 312 } 313 }); 314 } else { 315 select.emptyList(); 316 failure(term); 317 } 318 }; 319 320 function parse(data) { 321 var parsed = []; 322 var rows = data.split("\n"); 323 for (var i = 0; i < rows.length; i++) { 324 var row = $.trim(rows[i]); 325 if (row) { 326 row = row.split("|"); 327 parsed[parsed.length] = { 328 data: row, 329 value: row[0], 330 result: options.formatResult && options.formatResult(row, row[0]) || row[0] 331 }; 332 } 333 } 334 return parsed; 335 }; 336 337 function stopLoading() { 338 $input.removeClass(options.loadingClass); 339 }; 340 }; 341 $.Autocompleter.defaults = { 342 inputClass: "ac_input", 343 resultsClass: "ac_results", 344 loadingClass: "ac_loading", 345 minChars: 1, 346 delay: 400, 347 matchCase: false, 348 matchSubset: true,
349 matchContains: false, 350 cacheLength: 10, 351 max: 100, 352 mustMatch: false, 353 extraParams: {}, 354 selectFirst: true, 355 formatItem: function(row) { 356 return row[0]; 357 }, 358 formatMatch: null, 359 autoFill: false, 360 width: 0, 361 multiple: false, 362 multipleSeparator: ", ", 363 highlight: function(value, term) { 364 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); 365 }, 366 scroll: true, 367 scrollHeight: 180 368 }; 369 $.Autocompleter.Cache = function(options) { 370 var data = {}; 371 var length = 0; 372 373 function matchSubset(s, sub) { 374 if (!options.matchCase) s = s.toLowerCase(); 375 var i = s.indexOf(sub); 376 if (options.matchContains == "word") { 377 i = s.toLowerCase().search("\\b" + sub.toLowerCase()); 378 } 379 if (i == -1) return false; 380 return i == 0 || options.matchContains; 381 }; 382 383 function add(q, value) { 384 if (length > options.cacheLength) { 385 flush(); 386 } 387 if (!data[q]) { 388 length++; 389 } 390 data[q] = value; 391 } 392 function populate() { 393 if (!options.data) return false; 394 var stMatchSets = {}, 395 nullData = 0; 396 if (!options.url) options.cacheLength = 1; 397 stMatchSets[""] = []; 398 for (var i = 0, ol = options.data.length; i < ol; i++) { 399 var rawValue = options.data[i]; 400 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; 401 var value = options.formatMatch(rawValue, i + 1, options.data.length); 402 if (value === false) continue; 403 var firstChar = value.charAt(0).toLowerCase(); 404 if (!stMatchSets[firstChar]) stMatchSets[firstChar] = []; 405 var row = { 406 value: value, 407 data: rawValue, 408 result: options.formatResult && options.formatResult(rawValue) || value 409 }; 410 stMatchSets[firstChar].push(row); 411 if (nullData++ < options.max) { 412 stMatchSets[""].push(row); 413 } 414 }; 415 $.each(stMatchSets, function(i, value) { 416 options.cacheLength++; 417 add(i, value); 418 }); 419 } 420 setTimeout(populate, 25); 421 422 function flush() { 423 data = {}; 424 length = 0; 425 } 426 return { 427 flush: flush, 428 add: add, 429 populate: populate, 430 load: function(q) { 431 if (!options.cacheLength || !length) return null; 432 if (!options.url && options.matchContains) { 433 var csub = []; 434 for (var k in data) { 435 if (k.length > 0) { 436 var c = data[k]; 437 $.each(c, function(i, x) { 438 if (matchSubset(x.value, q)) { 439 csub.push(x); 440 } 441 }); 442 } 443 } 444 return csub; 445 } else if (data[q]) { 446 return data[q]; 447 } else if (options.matchSubset) { 448 for (var i = q.length - 1; i >= options.minChars; i--) { 449 var c = data[q.substr(0, i)]; 450 if (c) { 451 var csub = []; 452 $.each(c, function(i, x) { 453 if (matchSubset(x.value, q)) { 454 csub[csub.length] = x; 455 } 456 }); 457 return csub; 458 } 459 } 460 } 461 return null; 462 } 463 }; 464 }; 465 $.Autocompleter.Select = function(options, input, select, config) { 466 var CLASSES = { 467 ACTIVE: "ac_over" 468 }; 469 var listItems, active = -1, 470 data, term = "", 471 needsInit = true, 472 element, list; 473
474 function init() { 475 if (!needsInit) return; 476 element = $("<div/>").hide().addClass(options.resultsClass).css("position", "absolute").appendTo(document.body); 477 list = $("<ul/>").appendTo(element).mouseover(function(event) { 478 if (target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { 479 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); 480 $(target(event)).addClass(CLASSES.ACTIVE); 481 } 482 }).click(function(event) { 483 $(target(event)).addClass(CLASSES.ACTIVE); 484 select(); 485 input.focus(); 486 return false; 487 }).mousedown(function() { 488 config.mouseDownOnSelect = true; 489 }).mouseup(function() { 490 config.mouseDownOnSelect = false; 491 }); 492 if (options.width > 0) element.css("width", options.width); 493 needsInit = false; 494 } 495 function target(event) { 496 var element = event.target; 497 while (element && element.tagName != "LI") element = element.parentNode; 498 if (!element) return []; 499 return element; 500 } 501 function moveSelect(step) { 502 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); 503 movePosition(step); 504 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); 505 if (options.scroll) { 506 var offset = 0; 507 listItems.slice(0, active).each(function() { 508 offset += this.offsetHeight; 509 }); 510 if ((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { 511 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); 512 } else if (offset < list.scrollTop()) { 513 list.scrollTop(offset); 514 } 515 } 516 }; 517 518 function movePosition(step) { 519 active += step; 520 if (active < 0) { 521 active = listItems.size() - 1; 522 } else if (active >= listItems.size()) { 523 active = 0; 524 } 525 } 526 function limitNumberOfItems(available) { 527 return options.max && options.max < available ? options.max : available; 528 } 529 function fillList() { 530 list.empty(); 531 var max = limitNumberOfItems(data.length); 532 for (var i = 0; i < max; i++) { 533 if (!data[i]) continue; 534 var formatted = options.formatItem(data[i].data, i + 1, max, data[i].value, term); 535 if (formatted === false) continue; 536 var li = $("<li/>").html(options.highlight(formatted, term)).addClass(i % 2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; 537 $.data(li, "ac_data", data[i]); 538 } 539 listItems = list.find("li"); 540 if (options.selectFirst) { 541 listItems.slice(0, 1).addClass(CLASSES.ACTIVE); 542 active = 0; 543 } 544 if ($.fn.bgiframe) list.bgiframe(); 545 } 546 return { 547 display: function(d, q) { 548 init(); 549 data = d; 550 term = q; 551 fillList(); 552 }, 553 next: function() { 554 moveSelect(1); 555 }, 556 prev: function() { 557 moveSelect(-1); 558 }, 559 pageUp: function() { 560 if (active != 0 && active - 8 < 0) { 561 moveSelect(-active); 562 } else { 563 moveSelect(-8); 564 } 565 }, 566 pageDown: function() { 567 if (active != listItems.size() - 1 && active + 8 > listItems.size()) { 568 moveSelect(listItems.size() - 1 - active); 569 } else { 570 moveSelect(8); 571 } 572 }, 573 hide: function() { 574 element && element.hide(); 575 listItems && listItems.removeClass(CLASSES.ACTIVE); 576 active = -1; 577 }, 578 visible: function() { 579 return element && element.is(":visible"); 580 }, 581 current: function() { 582 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); 583 }, 584 show: function() { 585 var offset = $(input).offset(); 586 element.css({ 587 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), 588 top: offset.top + input.offsetHeight, 589 left: offset.left 590 }).show(); 591 if (options.scroll) { 592 list.scrollTop(0); 593 list.css({ 594 maxHeight: options.scrollHeight, 595 overflow: 'auto' 596 }); 597 if ($.browser.msie && typeof document.body.style.maxHeight === "undefined") { 598 var listHeight = 0; 599 listItems.each(function() { 600 listHeight += this.offsetHeight; 601 }); 602 var scrollbarsVisible = listHeight > options.scrollHeight; 603 list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight); 604 if (!scrollbarsVisible) {
605 listItems.width(list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right"))); 606 } 607 } 608 } 609 }, 610 selected: function() { 611 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); 612 return selected && selected.length && $.data(selected[0], "ac_data"); 613 }, 614 emptyList: function() { 615 list && list.empty(); 616 }, 617 unbind: function() { 618 element && element.remove(); 619 } 620 }; 621 }; 622 $.fn.selection = function(start, end) { 623 if (start !== undefined) { 624 return this.each(function() { 625 if (this.createTextRange) { 626 var selRange = this.createTextRange(); 627 if (end === undefined || start == end) { 628 selRange.move("character", start); 629 selRange.select(); 630 } else { 631 selRange.collapse(true); 632 selRange.moveStart("character", start); 633 selRange.moveEnd("character", end); 634 selRange.select(); 635 } 636 } else if (this.setSelectionRange) { 637 this.setSelectionRange(start, end); 638 } else if (this.selectionStart) { 639 this.selectionStart = start; 640 this.selectionEnd = end; 641 } 642 }); 643 } 644 var field = this[0]; 645 if (field.createTextRange) { 646 var range = document.selection.createRange(), 647 orig = field.value, 648 teststring = "<->", 649 textLength = range.text.length; 650 range.text = teststring; 651 var caretAt = field.value.indexOf(teststring); 652 field.value = orig; 653 this.selection(caretAt, caretAt + textLength); 654 return { 655 start: caretAt, 656 end: caretAt + textLength 657 } 658 } else if (field.selectionStart !== undefined) { 659 return { 660 start: field.selectionStart, 661 end: field.selectionEnd 662 } 663 } 664 }; 665})(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.