1/* 2 * jQuery Plugin: Tokenizing Autocomplete Text Entry 3 * Version 1.6.1 4 * 5 * Copyright (c) 2009 James Smith (http://loopj.com) 6 * Licensed jointly under the GPL and MIT licenses, 7 * choose which one suits your project best! 8 * 9 */ 10 11(function ($) { 12// Default settings 13var DEFAULT_SETTINGS = { 14 // Search settings 15 method: "GET", 16 queryParam: "q", 17 searchDelay: 300, 18 minChars: 1, 19 propertyToSearch: "name", 20 jsonContainer: null, 21 contentType: "json", 22 23 // Prepopulation settings 24 prePopulate: null, 25 processPrePopulate: false, 26 27 // Display settings 28 hintText: "Type in a search term", 29 noResultsText: "No results", 30 searchingText: "Searching...", 31 deleteText: "×", 32 animateDropdown: true, 33 placeholder: null, 34 theme: null, 35 zindex: 9999, 36 resultsLimit: null, 37 38 enableHTML: false, 39 40 resultsFormatter: function(item) { 41 var string = item[this.propertyToSearch]; 42 return "<li>" + (this.enableHTML ? string : _escapeHTML(string)) + "</li>"; 43 }, 44 45 tokenFormatter: function(item) { 46 var string = item[this.propertyToSearch]; 47 return "<li><p>" + (this.enableHTML ? string : _escapeHTML(string)) + "</p></li>"; 48 }, 49 50 // Tokenization settings 51 tokenLimit: null, 52 tokenDelimiter: ",", 53 preventDuplicates: false, 54 tokenValue: "id", 55 56 // Behavioral settings 57 allowFreeTagging: false, 58 allowTabOut: false, 59 60 // Callbacks 61 onResult: null, 62 onCachedResult: null, 63 onAdd: null, 64 onFreeTaggingAdd: null, 65 onDelete: null, 66 onReady: null, 67 68 // Other settings 69 idPrefix: "token-input-", 70 71 // Keep track if the input is currently in disabled mode 72 disabled: false 73}; 74 75// Default classes to use when theming 76var DEFAULT_CLASSES = { 77 tokenList: "token-input-list", 78 token: "token-input-token", 79 tokenReadOnly: "token-input-token-readonly", 80 tokenDelete: "token-input-delete-token", 81 selectedToken: "token-input-selected-token", 82 highlightedToken: "token-input-highlighted-token", 83 dropdown: "token-input-dropdown", 84 dropdownItem: "token-input-dropdown-item", 85 dropdownItem2: "token-input-dropdown-item2", 86 selectedDropdownItem: "token-input-selected-dropdown-item", 87 inputToken: "token-input-input-token", 88 focused: "token-input-focused", 89 disabled: "token-input-disabled" 90}; 91 92// Input box position "enum" 93var POSITION = { 94 BEFORE: 0, 95 AFTER: 1, 96 END: 2 97}; 98 99// Keys "enum" 100var KEY = { 101 BACKSPACE: 8, 102 TAB: 9, 103 ENTER: 13, 104 ESCAPE: 27, 105 SPACE: 32, 106 PAGE_UP: 33, 107 PAGE_DOWN: 34, 108 END: 35, 109 HOME: 36, 110 LEFT: 37, 111 UP: 38, 112 RIGHT: 39, 113 DOWN: 40, 114 NUMPAD_ENTER: 108, 115 COMMA: 188 116}; 117 118var HTML_ESCAPES = { 119 '&': '&', 120 '<': '<', 121 '>': '>', 122 '"': '"', 123 "'": ''', 124 '/': '/' 125}; 126 127var HTML_ESCAPE_CHARS = /[&<>"'\/]/g; 128 129function coerceToString(val) { 130 return String((val === null || val === undefined) ? '' : val); 131} 132 133function _escapeHTML(text) { 134 return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) { 135 return HTML_ESCAPES[match]; 136 }); 137} 138 139// Additional public (exposed) methods 140var methods = { 141 init: function(url_or_data_or_function, options) { 142 var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); 143 144 return this.each(function () { 145 $(this).data("settings", settings); 146 $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); 147 }); 148 }, 149 clear: function() { 150 this.data("tokenInputObject").clear(); 151 return this; 152 }, 153 add: function(item) { 154 this.data("tokenInputObject").add(item); 155 return this; 156 }, 157 remove: function(item) { 158 this.data("tokenInputObject").remove(item); 159 return this; 160 }, 161 get: function() { 162 return this.data("tokenInputObject").getTokens(); 163 }, 164 toggleDisabled: function(disable) { 165 this.data("tokenInputObject").toggleDisabled(disable); 166 return this; 167 }, 168 setOptions: function(options){ 169 $(this).data("settings", $.extend({}, $(this).data("settings"), options || {})); 170 return this; 171 }, 172 destroy: function () { 173 if(this.data("tokenInputObject")){ 174 this.data("tokenInputObject").clear(); 175 var tmpInput = this; 176 var closest = this.parent(); 177 closest.empty(); 178 tmpInput.show(); 179 closest.append(tmpInput); 180 return tmpInput; 181 } 182 } 183}; 184 185// Expose the .tokenInput function to jQuery as a plugin 186$.fn.tokenInput = function (method) { 187 // Method calling and initialization logic 188 if(methods[method]) { 189 return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 190 } else { 191 return methods.init.apply(this, arguments); 192 } 193}; 194 195// TokenList class for each input 196$.TokenList = function (input, url_or_data, settings) { 197 // 198 // Initialization 199 // 200 201 // Configure the data source 202 if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { 203 // Set the url to query against 204 $(input).data("settings").url = url_or_data; 205 206 // If the URL is a function, evaluate it here to do our initalization work 207 var url = computeURL(); 208 209 // Make a smart guess about cross-domain if it wasn't explicitly specified 210 if($(input).data("settings").crossDomain === undefined && typeof url === "string") { 211 if(url.indexOf("://") === -1) { 212 $(input).data("settings").crossDomain = false;
213 } else { 214 $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); 215 } 216 } 217 } else if(typeof(url_or_data) === "object") { 218 // Set the local data to search through 219 $(input).data("settings").local_data = url_or_data; 220 } 221 222 // Build class names 223 if($(input).data("settings").classes) { 224 // Use custom class names 225 $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes); 226 } else if($(input).data("settings").theme) { 227 // Use theme-suffixed default class names 228 $(input).data("settings").classes = {}; 229 $.each(DEFAULT_CLASSES, function(key, value) { 230 $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme; 231 }); 232 } else { 233 $(input).data("settings").classes = DEFAULT_CLASSES; 234 } 235 236 237 // Save the tokens 238 var saved_tokens = []; 239 240 // Keep track of the number of tokens in the list 241 var token_count = 0; 242 243 // Basic cache to save on db hits 244 var cache = new $.TokenList.Cache(); 245 246 // Keep track of the timeout, old vals 247 var timeout; 248 var input_val; 249 250 // Create a new text input an attach keyup events 251 var input_box = $("<input type=\"text\" autocomplete=\"off\" autocapitalize=\"off\">") 252 .css({ 253 outline: "none" 254 }) 255 .attr("id", $(input).data("settings").idPrefix + input.id) 256 .focus(function () { 257 if ($(input).data("settings").disabled) { 258 return false; 259 } else 260 if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) { 261 show_dropdown_hint(); 262 } 263 token_list.addClass($(input).data("settings").classes.focused); 264 }) 265 .blur(function () { 266 hide_dropdown(); 267 268 if ($(input).data("settings").allowFreeTagging) { 269 add_freetagging_tokens(); 270 } 271 272 $(this).val(""); 273 token_list.removeClass($(input).data("settings").classes.focused); 274 }) 275 .bind("keyup keydown blur update", resize_input) 276 .keydown(function (event) { 277 var previous_token; 278 var next_token; 279 280 switch(event.keyCode) { 281 case KEY.LEFT: 282 case KEY.RIGHT: 283 case KEY.UP: 284 case KEY.DOWN: 285 if(!$(this).val()) { 286 previous_token = input_token.prev(); 287 next_token = input_token.next(); 288 289 if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { 290 // Check if there is a previous/next token and it is selected 291 if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { 292 deselect_token($(selected_token), POSITION.BEFORE); 293 } else { 294 deselect_token($(selected_token), POSITION.AFTER); 295 } 296 } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { 297 // We are moving left, select the previous token if it exists 298 select_token($(previous_token.get(0))); 299 } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { 300 // We are moving right, select the next token if it exists 301 select_token($(next_token.get(0))); 302 } 303 } else { 304 var dropdown_item = null; 305 306 if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { 307 dropdown_item = $(selected_dropdown_item).next(); 308 } else { 309 dropdown_item = $(selected_dropdown_item).prev(); 310 } 311 312 if(dropdown_item.length) { 313 select_dropdown_item(dropdown_item); 314 } 315 } 316 return false;
317 break; 318 319 case KEY.BACKSPACE: 320 previous_token = input_token.prev(); 321 322 if(!$(this).val().length) { 323 if(selected_token) { 324 delete_token($(selected_token)); 325 hidden_input.change(); 326 } else if(previous_token.length) { 327 select_token($(previous_token.get(0))); 328 } 329 330 return false; 331 } else if($(this).val().length === 1) { 332 hide_dropdown(); 333 } else { 334 // set a timeout just long enough to let this function finish. 335 setTimeout(function(){do_search();}, 5); 336 } 337 break; 338 339 case KEY.TAB: 340 case KEY.ENTER: 341 case KEY.NUMPAD_ENTER: 342 case KEY.COMMA: 343 if(selected_dropdown_item) { 344 add_token($(selected_dropdown_item).data("tokeninput")); 345 hidden_input.change(); 346 } else { 347 if ($(input).data("settings").allowFreeTagging) { 348 if($(input).data("settings").allowTabOut && $(this).val() === "") { 349 return true; 350 } else { 351 add_freetagging_tokens(); 352 } 353 } else { 354 $(this).val(""); 355 if($(input).data("settings").allowTabOut) { 356 return true; 357 } 358 } 359 event.stopPropagation(); 360 event.preventDefault(); 361 } 362 return false; 363 364 case KEY.ESCAPE: 365 hide_dropdown(); 366 return true; 367 368 default: 369 if(String.fromCharCode(event.which)) { 370 // set a timeout just long enough to let this function finish. 371 setTimeout(function(){do_search();}, 5); 372 } 373 break; 374 } 375 }); 376 377 // Keep reference for placeholder 378 if (settings.placeholder) 379 input_box.attr("placeholder", settings.placeholder) 380 381 // Keep a reference to the original input box 382 var hidden_input = $(input) 383 .hide() 384 .val("") 385 .focus(function () { 386 focus_with_timeout(input_box); 387 }) 388 .blur(function () { 389 input_box.blur(); 390 //return the object to this can be referenced in the callback functions. 391 return hidden_input; 392 }); 393 394 // Keep a reference to the selected token and dropdown item 395 var selected_token = null; 396 var selected_token_index = 0; 397 var selected_dropdown_item = null; 398 399 // The list to store the token items in 400 var token_list = $("<ul />") 401 .addClass($(input).data("settings").classes.tokenList) 402 .click(function (event) { 403 var li = $(event.target).closest("li"); 404 if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { 405 toggle_select_token(li); 406 } else { 407 // Deselect selected token 408 if(selected_token) { 409 deselect_token($(selected_token), POSITION.END); 410 } 411 412 // Focus input box 413 focus_with_timeout(input_box); 414 } 415 }) 416 .mouseover(function (event) { 417 var li = $(event.target).closest("li"); 418 if(li && selected_token !== this) { 419 li.addClass($(input).data("settings").classes.highlightedToken); 420 } 421 }) 422 .mouseout(function (event) { 423 var li = $(event.target).closest("li"); 424 if(li && selected_token !== this) { 425 li.removeClass($(input).data("settings").classes.highlightedToken); 426 } 427 }) 428 .insertBefore(hidden_input); 429 430 // The token holding the input box 431 var input_token = $("<li />") 432 .addClass($(input).data("settings").classes.inputToken) 433 .appendTo(token_list) 434 .append(input_box); 435 436 // The list to store the dropdown items in 437 var dropdown = $("<div>") 438 .addClass($(input).data("settings").classes.dropdown) 439 .appendTo("body") 440 .hide(); 441 442 // Magic element to help us resize the text input 443 var input_resizer = $("<tester/>") 444 .insertAfter(input_box) 445 .css({ 446 position: "absolute", 447 top: -9999, 448 left: -9999, 449 width: "auto", 450 fontSize: input_box.css("fontSize"), 451 fontFamily: input_box.css("fontFamily"), 452 fontWeight: input_box.css("fontWeight"), 453 letterSpacing: input_box.css("letterSpacing"), 454 whiteSpace: "nowrap" 455 }); 456 457 // Pre-populate list if items exist 458 hidden_input.val(""); 459 var li_data = $(input).data("settings").prePopulate || hidden_input.data("pre"); 460 if($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) { 461 li_data = $(input).data("settings").onResult.call(hidden_input, li_data); 462 } 463 if(li_data && li_data.length) { 464 $.each(li_data, function (index, value) { 465 insert_token(value); 466 checkTokenLimit(); 467 input_box.attr("placeholder", null) 468 }); 469 } 470 471 // Check if widget should initialize as disabled 472 if ($(input).data("settings").disabled) { 473 toggleDisabled(true); 474 } 475 476 // Initialization is done 477 if($.isFunction($(input).data("settings").onReady)) { 478 $(input).data("settings").onReady.call(); 479 } 480 481 // 482 // Public functions 483 // 484 485 this.clear = function() { 486 token_list.children("li").each(function() { 487 if ($(this).children("input").length === 0) { 488 delete_token($(this)); 489 } 490 }); 491 }; 492 493 this.add = function(item) { 494 add_token(item); 495 }; 496 497 this.remove = function(item) { 498 token_list.children("li").each(function() { 499 if ($(this).children("input").length === 0) { 500 var currToken = $(this).data("tokeninput"); 501 var match = true; 502 for (var prop in item) { 503 if (item[prop] !== currToken[prop]) { 504 match = false;
505 break; 506 } 507 } 508 if (match) { 509 delete_token($(this)); 510 } 511 } 512 }); 513 }; 514 515 this.getTokens = function() { 516 return saved_tokens; 517 }; 518 519 this.toggleDisabled = function(disable) { 520 toggleDisabled(disable); 521 }; 522 523 // Resize input to maximum width so the placeholder can be seen 524 resize_input(); 525 526 // 527 // Private functions 528 // 529 530 function escapeHTML(text) { 531 return $(input).data("settings").enableHTML ? text : _escapeHTML(text); 532 } 533 534 // Toggles the widget between enabled and disabled state, or according 535 // to the [disable] parameter. 536 function toggleDisabled(disable) { 537 if (typeof disable === 'boolean') { 538 $(input).data("settings").disabled = disable 539 } else { 540 $(input).data("settings").disabled = !$(input).data("settings").disabled; 541 } 542 input_box.attr('disabled', $(input).data("settings").disabled); 543 token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled); 544 // if there is any token selected we deselect it 545 if(selected_token) { 546 deselect_token($(selected_token), POSITION.END); 547 } 548 hidden_input.attr('disabled', $(input).data("settings").disabled); 549 } 550 551 function checkTokenLimit() { 552 if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { 553 input_box.hide(); 554 hide_dropdown(); 555 return; 556 } 557 } 558 559 function resize_input() { 560 if(input_val === (input_val = input_box.val())) {return;} 561 562 // Get width left on the current line 563 var width_left = token_list.width() - input_box.offset().left - token_list.offset().left; 564 // Enter new content into resizer and resize input accordingly 565 input_resizer.html(_escapeHTML(input_val)); 566 // Get maximum width, minimum the size of input and maximum the widget's width 567 input_box.width(Math.min(token_list.width(), 568 Math.max(width_left, input_resizer.width() + 30))); 569 } 570 571 function is_printable_character(keycode) { 572 return ((keycode >= 48 && keycode <= 90) || // 0-1a-z 573 (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . 574 (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ 575 (keycode >= 219 && keycode <= 222)); // ( \ ) ' 576 } 577 578 function add_freetagging_tokens() { 579 var value = $.trim(input_box.val()); 580 var tokens = value.split($(input).data("settings").tokenDelimiter); 581 $.each(tokens, function(i, token) { 582 if (!token) { 583 return; 584 } 585 586 if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) { 587 token = $(input).data("settings").onFreeTaggingAdd.call(hidden_input, token); 588 } 589 var object = {}; 590 object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token; 591 add_token(object); 592 }); 593 } 594 595 // Inner function to a token to the list 596 function insert_token(item) { 597 var $this_token = $($(input).data("settings").tokenFormatter(item)); 598 var readonly = item.readonly === true ? true : false; 599 600 if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly); 601 602 $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token); 603 604 // The 'delete token' button 605 if(!readonly) { 606 $("<span>" + $(input).data("settings").deleteText + "</span>") 607 .addClass($(input).data("settings").classes.tokenDelete) 608 .appendTo($this_token) 609 .click(function () { 610 if (!$(input).data("settings").disabled) { 611 delete_token($(this).parent()); 612 hidden_input.change(); 613 return false; 614 } 615 }); 616 } 617 618 // Store data on the token 619 var token_data = item; 620 $.data($this_token.get(0), "tokeninput", item); 621 622 // Save this token for duplicate checking 623 saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); 624 selected_token_index++; 625 626 // Update the hidden input 627 update_hidden_input(saved_tokens, hidden_input); 628 629 token_count += 1; 630 631 // Check the token limit 632 if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { 633 input_box.hide(); 634 hide_dropdown(); 635 } 636 637 return $this_token; 638 } 639 640 // Add a token to the token list based on user input 641 function add_token (item) { 642 var callback = $(input).data("settings").onAdd; 643 644 // See if the token already exists and select it if we don't want duplicates 645 if(token_count > 0 && $(input).data("settings").preventDuplicates) { 646 var found_existing_token = null; 647 token_list.children().each(function () { 648 var existing_token = $(this); 649 var existing_data = $.data(existing_token.get(0), "tokeninput"); 650 if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) { 651 found_existing_token = existing_token; 652 return false;
653 } 654 }); 655 656 if(found_existing_token) { 657 select_token(found_existing_token); 658 input_token.insertAfter(found_existing_token); 659 focus_with_timeout(input_box); 660 return; 661 } 662 } 663 664 // Squeeze input_box so we force no unnecessary line break 665 input_box.width(0); 666 667 // Insert the new tokens 668 if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) { 669 insert_token(item); 670 // Remove the placeholder so it's not seen after you've added a token 671 input_box.attr("placeholder", null) 672 checkTokenLimit(); 673 } 674 675 // Clear input box 676 input_box.val(""); 677 678 // Don't show the help dropdown, they've got the idea 679 hide_dropdown(); 680 681 // Execute the onAdd callback if defined 682 if($.isFunction(callback)) { 683 callback.call(hidden_input,item); 684 } 685 } 686 687 // Select a token in the token list 688 function select_token (token) { 689 if (!$(input).data("settings").disabled) { 690 token.addClass($(input).data("settings").classes.selectedToken); 691 selected_token = token.get(0); 692 693 // Hide input box 694 input_box.val(""); 695 696 // Hide dropdown if it is visible (eg if we clicked to select token) 697 hide_dropdown(); 698 } 699 } 700 701 // Deselect a token in the token list 702 function deselect_token (token, position) { 703 token.removeClass($(input).data("settings").classes.selectedToken); 704 selected_token = null; 705 706 if(position === POSITION.BEFORE) { 707 input_token.insertBefore(token); 708 selected_token_index--; 709 } else if(position === POSITION.AFTER) { 710 input_token.insertAfter(token); 711 selected_token_index++; 712 } else { 713 input_token.appendTo(token_list); 714 selected_token_index = token_count; 715 } 716 717 // Show the input box and give it focus again 718 focus_with_timeout(input_box); 719 } 720 721 // Toggle selection of a token in the token list 722 function toggle_select_token(token) { 723 var previous_selected_token = selected_token; 724 725 if(selected_token) { 726 deselect_token($(selected_token), POSITION.END); 727 } 728 729 if(previous_selected_token === token.get(0)) { 730 deselect_token(token, POSITION.END); 731 } else { 732 select_token(token); 733 } 734 } 735 736 // Delete a token from the token list 737 function delete_token (token) { 738 // Remove the id from the saved list 739 var token_data = $.data(token.get(0), "tokeninput"); 740 var callback = $(input).data("settings").onDelete; 741 742 var index = token.prevAll().length; 743 if(index > selected_token_index) index--; 744 745 // Delete the token 746 token.remove(); 747 selected_token = null; 748 749 // Show the input box and give it focus again 750 focus_with_timeout(input_box); 751 752 // Remove this token from the saved list 753 saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); 754 if (saved_tokens.length == 0) { 755 input_box.attr("placeholder", settings.placeholder) 756 } 757 if(index < selected_token_index) selected_token_index--; 758 759 // Update the hidden input 760 update_hidden_input(saved_tokens, hidden_input); 761 762 token_count -= 1; 763 764 if($(input).data("settings").tokenLimit !== null) { 765 input_box 766 .show() 767 .val(""); 768 focus_with_timeout(input_box); 769 } 770 771 // Execute the onDelete callback if defined 772 if($.isFunction(callback)) { 773 callback.call(hidden_input,token_data); 774 } 775 } 776 777 // Update the hidden input box value 778 function update_hidden_input(saved_tokens, hidden_input) { 779 var token_values = $.map(saved_tokens, function (el) { 780 if(typeof $(input).data("settings").tokenValue == 'function') 781 return $(input).data("settings").tokenValue.call(this, el); 782 783 return el[$(input).data("settings").tokenValue]; 784 }); 785 hidden_input.val(token_values.join($(input).data("settings").tokenDelimiter)); 786 787 } 788 789 // Hide and clear the results dropdown 790 function hide_dropdown () { 791 dropdown.hide().empty(); 792 selected_dropdown_item = null; 793 } 794 795 function show_dropdown() { 796 dropdown 797 .css({ 798 position: "absolute", 799 top: token_list.offset().top + token_list[0].getBoundingClientRect().height, 800 left: token_list.offset().left, 801 width: token_list.width(), 802 'z-index': $(input).data("settings").zindex 803 }) 804 .show(); 805 } 806 807 function show_dropdown_searching () { 808 if($(input).data("settings").searchingText) { 809 dropdown.html("<p>" + escapeHTML($(input).data("settings").searchingText) + "</p>"); 810 show_dropdown(); 811 } 812 } 813 814 function show_dropdown_hint () { 815 if($(input).data("settings").hintText) { 816 dropdown.html("<p>" + escapeHTML($(input).data("settings").hintText) + "</p>"); 817 show_dropdown(); 818 } 819 } 820 821 var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'); 822 function regexp_escape(term) { 823 return term.replace(regexp_special_chars, '\\$&'); 824 } 825 826 // Highlight the query part of the search term 827 function highlight_term(value, term) { 828 return value.replace( 829 new RegExp( 830 "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)", 831 "gi" 832 ), function(match, p1) { 833 return "<b>" + escapeHTML(p1) + "</b>"; 834 } 835 ); 836 } 837 838 function find_value_and_highlight_term(template, value, term) { 839 return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); 840 } 841 842 // Populate the results dropdown with some results 843 function populate_dropdown (query, results) { 844 if(results && results.length) { 845 dropdown.empty(); 846 var dropdown_ul = $("<ul>") 847 .appendTo(dropdown) 848 .mouseover(function (event) { 849 select_dropdown_item($(event.target).closest("li")); 850 }) 851 .mousedown(function (event) { 852 add_token($(event.target).closest("li").data("tokeninput")); 853 hidden_input.change(); 854 return false;
855 }) 856 .hide(); 857 858 if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) { 859 results = results.slice(0, $(input).data("settings").resultsLimit); 860 } 861 862 $.each(results, function(index, value) { 863 var this_li = $(input).data("settings").resultsFormatter(value); 864 865 this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query); 866 867 this_li = $(this_li).appendTo(dropdown_ul); 868 869 if(index % 2) { 870 this_li.addClass($(input).data("settings").classes.dropdownItem); 871 } else { 872 this_li.addClass($(input).data("settings").classes.dropdownItem2); 873 } 874 875 if(index === 0) { 876 select_dropdown_item(this_li); 877 } 878 879 $.data(this_li.get(0), "tokeninput", value); 880 }); 881 882 show_dropdown(); 883 884 if($(input).data("settings").animateDropdown) { 885 dropdown_ul.slideDown("fast"); 886 } else { 887 dropdown_ul.show(); 888 } 889 } else { 890 if($(input).data("settings").noResultsText) { 891 dropdown.html("<p>" + escapeHTML($(input).data("settings").noResultsText) + "</p>"); 892 show_dropdown(); 893 } 894 } 895 } 896 897 // Highlight an item in the results dropdown 898 function select_dropdown_item (item) { 899 if(item) { 900 if(selected_dropdown_item) { 901 deselect_dropdown_item($(selected_dropdown_item)); 902 } 903 904 item.addClass($(input).data("settings").classes.selectedDropdownItem); 905 selected_dropdown_item = item.get(0); 906 } 907 } 908 909 // Remove highlighting from an item in the results dropdown 910 function deselect_dropdown_item (item) { 911 item.removeClass($(input).data("settings").classes.selectedDropdownItem); 912 selected_dropdown_item = null; 913 } 914 915 // Do a search and show the "searching" dropdown if the input is longer 916 // than $(input).data("settings").minChars 917 function do_search() { 918 var query = input_box.val(); 919 920 if(query && query.length) { 921 if(selected_token) { 922 deselect_token($(selected_token), POSITION.AFTER); 923 } 924 925 if(query.length >= $(input).data("settings").minChars) { 926 show_dropdown_searching(); 927 clearTimeout(timeout); 928 929 timeout = setTimeout(function(){ 930 run_search(query); 931 }, $(input).data("settings").searchDelay); 932 } else { 933 hide_dropdown(); 934 } 935 } 936 } 937 938 // Do the actual search 939 function run_search(query) { 940 var cache_key = query + computeURL(); 941 var cached_results = cache.get(cache_key); 942 if(cached_results) { 943 if ($.isFunction($(input).data("settings").onCachedResult)) { 944 cached_results = $(input).data("settings").onCachedResult.call(hidden_input, cached_results); 945 } 946 populate_dropdown(query, cached_results); 947 } else { 948 // Are we doing an ajax search or local data search? 949 if($(input).data("settings").url) { 950 var url = computeURL(); 951 // Extract exisiting get params 952 var ajax_params = {}; 953 ajax_params.data = {}; 954 if(url.indexOf("?") > -1) { 955 var parts = url.split("?"); 956 ajax_params.url = parts[0]; 957 958 var param_array = parts[1].split("&"); 959 $.each(param_array, function (index, value) { 960 var kv = value.split("="); 961 ajax_params.data[kv[0]] = kv[1]; 962 }); 963 } else { 964 ajax_params.url = url; 965 } 966 967 // Prepare the request 968 ajax_params.data[$(input).data("settings").queryParam] = query; 969 ajax_params.type = $(input).data("settings").method; 970 ajax_params.dataType = $(input).data("settings").contentType; 971 if($(input).data("settings").crossDomain) { 972 ajax_params.dataType = "jsonp"; 973 } 974 975 // Attach the success callback 976 ajax_params.success = function(results) { 977 cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); 978 if($.isFunction($(input).data("settings").onResult)) { 979 results = $(input).data("settings").onResult.call(hidden_input, results); 980 } 981 982 // only populate the dropdown if the results are associated with the active search query 983 if(input_box.val() === query) { 984 populate_dropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); 985 } 986 }; 987 988 // Make the request 989 $.ajax(ajax_params); 990 } else if($(input).data("settings").local_data) { 991 // Do the search through local data 992 var results = $.grep($(input).data("settings").local_data, function (row) { 993 return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; 994 }); 995 996 cache.add(cache_key, results); 997 if($.isFunction($(input).data("settings").onResult)) { 998 results = $(input).data("settings").onResult.call(hidden_input, results); 999 } 1000 populate_dropdown(query, results); 1001 } 1002 } 1003 } 1004 1005 // compute the dynamic URL 1006 function computeURL() { 1007 var url = $(input).data("settings").url; 1008 if(typeof $(input).data("settings").url == 'function') { 1009 url = $(input).data("settings").url.call($(input).data("settings")); 1010 } 1011 return url; 1012 } 1013 1014 // Bring browser focus to the specified object. 1015 // Use of setTimeout is to get around an IE bug. 1016 // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie) 1017 // 1018 // obj: a jQuery object to focus() 1019 function focus_with_timeout(obj) { 1020 setTimeout(function() { obj.focus(); }, 50); 1021 } 1022 1023}; 1024 1025// Really basic cache for the results 1026$.TokenList.Cache = function (options) { 1027 var settings = $.extend({ 1028 max_size: 500 1029 }, options); 1030 1031 var data = {}; 1032 var size = 0; 1033 1034 var flush = function () { 1035 data = {}; 1036 size = 0; 1037 }; 1038 1039 this.add = function (query, results) { 1040 if(size > settings.max_size) { 1041 flush(); 1042 } 1043 1044 if(!data[query]) { 1045 size += 1; 1046 } 1047 1048 data[query] = results; 1049 }; 1050 1051 this.get = function (query) { 1052 return data[query]; 1053 }; 1054}; 1055}(jQuery)); 1056
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.