2 * jQuery Plugin: Tokenizing Autocomplete Text Entry
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!
13 $.fn.tokenInput = function (url, options) {
14 var settings = $.extend({
16 hintText: "Type in a search term",
17 noResultsText: "No results",
18 searchingText: "Searching...",
29 settings.classes = $.extend({
30 tokenList: "token-input-list",
31 token: "token-input-token",
32 tokenDelete: "token-input-delete-token",
33 selectedToken: "token-input-selected-token",
34 highlightedToken: "token-input-highlighted-token",
35 dropdown: "token-input-dropdown",
36 dropdownItem: "token-input-dropdown-item",
37 dropdownItem2: "token-input-dropdown-item2",
38 selectedDropdownItem: "token-input-selected-dropdown-item",
39 inputToken: "token-input-input-token"
42 return this.each(function () {
43 var list = new $.TokenList(this, settings);
47 $.TokenList = function (input, settings) {
52 // Input box position "enum"
73 var saved_tokens = [];
75 // Keep track of the number of tokens in the list
78 // Basic cache to save on db hits
79 var cache = new $.TokenList.Cache();
81 // Keep track of the timeout
84 // Create a new text input an attach keyup events
85 var input_box = $("<input type=\"text\">")
90 if (settings.tokenLimit == null || settings.tokenLimit != token_count) {
97 .keydown(function (event) {
101 switch(event.keyCode) {
107 previous_token = input_token.prev();
108 next_token = input_token.next();
110 if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
111 // Check if there is a previous/next token and it is selected
112 if(event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) {
113 deselect_token($(selected_token), POSITION.BEFORE);
115 deselect_token($(selected_token), POSITION.AFTER);
117 } else if((event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) && previous_token.length) {
118 // We are moving left, select the previous token if it exists
119 select_token($(previous_token.get(0)));
120 } else if((event.keyCode == KEY.RIGHT || event.keyCode == KEY.DOWN) && next_token.length) {
121 // We are moving right, select the next token if it exists
122 select_token($(next_token.get(0)));
125 var dropdown_item = null;
127 if(event.keyCode == KEY.DOWN || event.keyCode == KEY.RIGHT) {
128 dropdown_item = $(selected_dropdown_item).next();
130 dropdown_item = $(selected_dropdown_item).prev();
133 if(dropdown_item.length) {
134 select_dropdown_item(dropdown_item);
141 previous_token = input_token.prev();
143 if(!$(this).val().length) {
145 delete_token($(selected_token));
146 } else if(previous_token.length) {
147 select_token($(previous_token.get(0)));
151 } else if($(this).val().length == 1) {
154 // set a timeout just long enough to let this function finish.
155 setTimeout(function(){do_search(false);}, 5);
162 if(selected_dropdown_item) {
163 add_token($(selected_dropdown_item));
173 if(is_printable_character(event.keyCode)) {
174 // set a timeout just long enough to let this function finish.
175 setTimeout(function(){do_search(false);}, 5);
181 // Keep a reference to the original input box
182 var hidden_input = $(input)
191 // Keep a reference to the selected token and dropdown item
192 var selected_token = null;
193 var selected_dropdown_item = null;
195 // The list to store the token items in
196 var token_list = $("<ul />")
197 .addClass(settings.classes.tokenList)
198 .insertAfter(hidden_input)
199 .click(function (event) {
200 var li = get_element_from_event(event, "li");
201 if(li && li.get(0) != input_token.get(0)) {
202 toggle_select_token(li);
208 deselect_token($(selected_token), POSITION.END);
212 .mouseover(function (event) {
213 var li = get_element_from_event(event, "li");
214 if(li && selected_token !== this) {
215 li.addClass(settings.classes.highlightedToken);
218 .mouseout(function (event) {
219 var li = get_element_from_event(event, "li");
220 if(li && selected_token !== this) {
221 li.removeClass(settings.classes.highlightedToken);
224 .mousedown(function (event) {
225 // Stop user selecting text on tokens
226 var li = get_element_from_event(event, "li");
233 // The list to store the dropdown items in
234 var dropdown = $("<div>")
235 .addClass(settings.classes.dropdown)
236 .insertAfter(token_list)
239 // The token holding the input box
240 var input_token = $("<li />")
241 .addClass(settings.classes.inputToken)
242 .appendTo(token_list)
252 // Pre-populate list if items exist
253 function init_list () {
254 li_data = settings.prePopulate;
255 if(li_data && li_data.length) {
256 for(var i in li_data) {
257 var this_token = $("<li><p>"+li_data[i].name+"</p> </li>")
258 .addClass(settings.classes.token)
259 .insertBefore(input_token);
262 .addClass(settings.classes.tokenDelete)
263 .appendTo(this_token)
265 delete_token($(this).parent());
269 $.data(this_token.get(0), "tokeninput", {"id": li_data[i].id, "name": li_data[i].name});
271 // Clear input box and make sure it keeps focus
276 // Don't show the help dropdown, they've got the idea
279 // Save this token id
280 var id_string = li_data[i].id + ","
281 hidden_input.val(hidden_input.val() + id_string);
286 function is_printable_character(keycode) {
287 if((keycode >= 48 && keycode <= 90) || // 0-1a-z
288 (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
289 (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
290 (keycode >= 219 && keycode <= 222) // ( \ ) '
298 // Get an element of a particular type from an event (click/mouseover etc)
299 function get_element_from_event (event, element_type) {
300 var target = $(event.target);
303 if(target.is(element_type)) {
305 } else if(target.parent(element_type).length) {
306 element = target.parent(element_type+":first");
312 // Inner function to a token to the list
313 function insert_token(id, value) {
314 var this_token = $("<li><p>"+ value +"</p> </li>")
315 .addClass(settings.classes.token)
316 .insertBefore(input_token);
318 // The 'delete token' button
320 .addClass(settings.classes.tokenDelete)
321 .appendTo(this_token)
323 delete_token($(this).parent());
327 $.data(this_token.get(0), "tokeninput", {"id": id, "name": value});
332 // Add a token to the token list based on user input
333 function add_token (item) {
334 var li_data = $.data(item.get(0), "tokeninput");
335 var this_token = insert_token(li_data.id, li_data.name);
337 // Clear input box and make sure it keeps focus
342 // Don't show the help dropdown, they've got the idea
345 // Save this token id
346 var id_string = li_data.id + ","
347 hidden_input.val(hidden_input.val() + id_string);
351 if(settings.tokenLimit != null && settings.tokenLimit >= token_count) {
357 // Select a token in the token list
358 function select_token (token) {
359 token.addClass(settings.classes.selectedToken);
360 selected_token = token.get(0);
365 // Hide dropdown if it is visible (eg if we clicked to select token)
369 // Deselect a token in the token list
370 function deselect_token (token, position) {
371 token.removeClass(settings.classes.selectedToken);
372 selected_token = null;
374 if(position == POSITION.BEFORE) {
375 input_token.insertBefore(token);
376 } else if(position == POSITION.AFTER) {
377 input_token.insertAfter(token);
379 input_token.appendTo(token_list);
382 // Show the input box and give it focus again
386 // Toggle selection of a token in the token list
387 function toggle_select_token (token) {
388 if(selected_token == token.get(0)) {
389 deselect_token(token, POSITION.END);
392 deselect_token($(selected_token), POSITION.END);
398 // Delete a token from the token list
399 function delete_token (token) {
400 // Remove the id from the saved list
401 var token_data = $.data(token.get(0), "tokeninput");
405 selected_token = null;
407 // Show the input box and give it focus again
410 // Delete this token's id from hidden input
411 var str = hidden_input.val()
412 var start = str.indexOf(token_data.id+",");
413 var end = str.indexOf(",", start) + 1;
415 if(end >= str.length) {
416 hidden_input.val(str.slice(0, start));
418 hidden_input.val(str.slice(0, start) + str.slice(end, str.length));
423 if (settings.tokenLimit != null) {
431 // Hide and clear the results dropdown
432 function hide_dropdown () {
433 dropdown.hide().empty();
434 selected_dropdown_item = null;
437 function show_dropdown_searching () {
439 .html("<p>"+settings.searchingText+"</p>")
443 function show_dropdown_hint () {
445 .html("<p>"+settings.hintText+"</p>")
449 // Highlight the query part of the search term
450 function highlight_term(value, term) {
451 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
454 // Populate the results dropdown with some results
455 function populate_dropdown (query, results) {
458 var dropdown_ul = $("<ul>")
460 .mouseover(function (event) {
461 select_dropdown_item(get_element_from_event(event, "li"));
463 .mousedown(function (event) {
464 add_token(get_element_from_event(event, "li"));
469 for(var i in results) {
470 if (results.hasOwnProperty(i)) {
471 var this_li = $("<li>"+highlight_term(results[i].name, query)+"</li>")
472 .appendTo(dropdown_ul);
475 this_li.addClass(settings.classes.dropdownItem);
477 this_li.addClass(settings.classes.dropdownItem2);
481 select_dropdown_item(this_li);
484 $.data(this_li.get(0), "tokeninput", {"id": results[i].id, "name": results[i].name});
489 dropdown_ul.slideDown("fast");
493 .html("<p>"+settings.noResultsText+"</p>")
498 // Highlight an item in the results dropdown
499 function select_dropdown_item (item) {
501 if(selected_dropdown_item) {
502 deselect_dropdown_item($(selected_dropdown_item));
505 item.addClass(settings.classes.selectedDropdownItem);
506 selected_dropdown_item = item.get(0);
510 // Remove highlighting from an item in the results dropdown
511 function deselect_dropdown_item (item) {
512 item.removeClass(settings.classes.selectedDropdownItem);
513 selected_dropdown_item = null;
516 // Do a search and show the "searching" dropdown if the input is longer
517 // than settings.minChars
518 function do_search(immediate) {
519 var query = input_box.val().toLowerCase();
521 if (query && query.length) {
523 deselect_token($(selected_token), POSITION.AFTER);
525 if (query.length >= settings.minChars) {
526 show_dropdown_searching();
530 clearTimeout(timeout);
531 timeout = setTimeout(function(){run_search(query);}, settings.searchDelay);
539 // Do the actual search
540 function run_search(query) {
541 var cached_results = cache.get(query);
543 populate_dropdown(query, cached_results);
545 var queryStringDelimiter = settings.url.indexOf("?") < 0 ? "?" : "&";
546 var callback = function(results) {
547 if($.isFunction(settings.onResult)) {
548 results = settings.onResult.call(this, results);
550 cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
551 populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
554 if(settings.method == "POST") {
555 $.post(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType);
557 $.get(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType);
563 // Really basic cache for the results
564 $.TokenList.Cache = function (options) {
565 var settings = $.extend({
572 var flush = function () {
577 this.add = function (query, results) {
578 if(size > settings.max_size) {
586 data[query] = results;
589 this.get = function (query) {