diff --git a/src/jquery.tokeninput.js b/src/jquery.tokeninput.js index e75888f0..5445233c 100644 --- a/src/jquery.tokeninput.js +++ b/src/jquery.tokeninput.js @@ -5,474 +5,476 @@ * Copyright (c) 2009 James Smith (http://loopj.com) * Licensed jointly under the GPL and MIT licenses, * choose which one suits your project best! - * - * TH - 2010-08-23 - Added ability to have arbitary tags that don't require a match from the list. - * Added requiresMatch options to suppor this. Defaults to original Tokenizing Autocomplete functionality. + * + * TH - 2010-08-23 - Added ability to have arbitary tags that don't require a match from the list. + * Added requiresMatch options to support this. Defaults to original Tokenizing Autocomplete functionality. * Also added focusHint so it doesn't always show hint when focusing the input. Again, defaults to orignal functionality. */ -(function($) { +;(function($) { $.fn.tokenInput = function (url, options) { - var settings = $.extend({ - url: url, - hintText: "Type in a search term", - noResultsText: "No results", - searchingText: "Searching...", - searchDelay: 300, - minChars: 1, - tokenLimit: null, - jsonContainer: null, - method: "GET", - contentType: "json", - queryParam: "q", - onResult: null, - focusHint: true, //Added TH - determines if drop-down hint should be shown on input focus. - requireMatch: true, //Added TH - determines if a user should be able to add new tags or must match a selection. - animateDropdown: true, - suggestedTagsText: "Suggested tags:", - defaultSuggestTagSize: 14, - defaultSuggestTagSizeUnit: 'px', - afterAdd: function() {}, - useClientSideSearch: false - }, options); - - settings.classes = $.extend({ - tokenList: "token-input-list", - token: "token-input-token", - label: "token-input-label", - tokenDelete: "token-input-delete-token", - selectedToken: "token-input-selected-token", - highlightedToken: "token-input-highlighted-token", - dropdown: "token-input-dropdown", - dropdownItem: "token-input-dropdown-item", - dropdownItem2: "token-input-dropdown-item2", - selectedDropdownItem: "token-input-selected-dropdown-item", - inputToken: "token-input-input-token", - suggestedTags: "token-input-suggested-tags", - suggestedTag: "token-input-suggested-tag" - }, options.classes); - - return this.each(function () { - var list = new $.TokenList(this, settings); - }); + if (!options) options = {}; + var settings = $.extend({ + url: $.isFunction(url) ? url : function(query){ + // N.B. options.url overrides this, so should not be used + var queryStringDelimiter = url.indexOf("?") < 0 ? "?" : "&" + return url + queryStringDelimiter + settings.queryParam + "=" + query; + }, + hintText: "Type in a search term", + noResultsText: "No results", + searchingText: "Searching...", + searchDelay: 300, + minChars: 1, + tokenLimit: null, + jsonContainer: null, + method: "GET", + contentType: "json", + queryParam: "q", + onResult: null, + focusHint: true, //Added TH - determines if drop-down hint should be shown on input focus. + requireMatch: true, //Added TH - determines if a user should be able to add new tags or must match a selection. + animateDropdown: true, + suggestedTagsText: "Suggested tags:", + defaultSuggestTagSize: 14, + defaultSuggestTagSizeUnit: 'px', + afterAdd: function() {}, + useClientSideSearch: false + }, options); + + settings.classes = $.extend({ + tokenList: "token-input-list", + token: "token-input-token", + label: "token-input-label", + tokenDelete: "token-input-delete-token", + selectedToken: "token-input-selected-token", + highlightedToken: "token-input-highlighted-token", + dropdown: "token-input-dropdown", + dropdownItem: "token-input-dropdown-item", + dropdownItem2: "token-input-dropdown-item2", + selectedDropdownItem: "token-input-selected-dropdown-item", + inputToken: "token-input-input-token", + suggestedTags: "token-input-suggested-tags", + suggestedTag: "token-input-suggested-tag" + }, options.classes); + + return this.each(function () { + var list = new $.TokenList(this, settings); + }); }; $.TokenList = function (input, settings) { - // - // Variables - // - - // Input box position "enum" - var POSITION = { - BEFORE: 0, - AFTER: 1, - END: 2 - }; - - // Keys "enum" - var KEY = { - BACKSPACE: 8, - TAB: 9, - RETURN: 13, - ESC: 27, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - COMMA: 188 - }; - - // Save the tokens - var saved_tokens = []; - - // Keep track of the number of tokens in the list - var token_count = 0; - - // Basic cache to save on db hits - var cache = new $.TokenList.Cache(); - - // Keep track of the timeout - var timeout; - - var client_side_data; - - // Create a new text input an attach keyup events - var input_box = $("") - .attr('id', $(input).attr('id')+'Dynamic') - .attr('name', $(input).attr('id')+'Dynamic') - .css({ - outline: "none" - }) - .focus(function () { - if (settings.focusHint && (settings.tokenLimit == null || settings.tokenLimit != token_count)) { - show_dropdown_hint(); - } - - if (settings.useClientSideSearch && !client_side_data) { - client_side_data = []; - var http_method = settings.method.toLowerCase(); - $[http_method](settings.url, {}, prepare_client_side_data, settings.contentType); - } - }) - .blur(function () { - hide_dropdown(); - }) - .keydown(function (event) { - var previous_token; - var next_token; - - switch(event.keyCode) { - case KEY.LEFT: - case KEY.RIGHT: - case KEY.UP: - case KEY.DOWN: - if(!$(this).val()) { - previous_token = input_token.prev(); - next_token = input_token.next(); - - if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { - // Check if there is a previous/next token and it is selected - if(event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) { - deselect_token($(selected_token), POSITION.BEFORE); - } else { - deselect_token($(selected_token), POSITION.AFTER); - } - } else if((event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) && previous_token.length) { - // We are moving left, select the previous token if it exists - select_token($(previous_token.get(0))); - } else if((event.keyCode == KEY.RIGHT || event.keyCode == KEY.DOWN) && next_token.length) { - // We are moving right, select the next token if it exists - select_token($(next_token.get(0))); - } - } else { - var dropdown_item = null; - - if(!selected_dropdown_item) { - dropdown_item = $('li:first', dropdown); - } else { - if(event.keyCode == KEY.DOWN || event.keyCode == KEY.RIGHT) { - dropdown_item = $(selected_dropdown_item).next(); - } else { - dropdown_item = $(selected_dropdown_item).prev(); - } - } - - - if(dropdown_item.length) { - select_dropdown_item(dropdown_item); - } - return false; - } - break; - - case KEY.BACKSPACE: - previous_token = input_token.prev(); - - if(!$(this).val().length) { - if(selected_token) { - delete_token($(selected_token)); - } else if(previous_token.length) { - select_token($(previous_token.get(0))); - } - - return false; - } else if($(this).val().length == 1) { - hide_dropdown(); - } else { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search(false);}, 5); - } - break; - - case KEY.TAB: - case KEY.RETURN: - case KEY.COMMA: - - // Submit form if user hits return a second time - if(event.keyCode == KEY.RETURN && $(this).val() == "") { - parentForm[0].submit(); - return false; - } - + // + // Variables + // + + // Input box position "enum" + var POSITION = { + BEFORE: 0, + AFTER: 1, + END: 2 + }; + + // Keys "enum" + var KEY = { + BACKSPACE: 8, + TAB: 9, + RETURN: 13, + ESC: 27, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + COMMA: 188 + }; + + // Save the tokens + var saved_tokens = []; + + // Keep track of the number of tokens in the list + var token_count = 0; + + // Basic cache to save on db hits + var cache = new $.TokenList.Cache(); + + // Keep track of the timeout + var timeout; + + var client_side_data; + + // Create a new text input an attach keyup events + var input_box = $("") + .attr('id', $(input).attr('id')+'Dynamic') + .attr('name', $(input).attr('id')+'Dynamic') + .css({ + outline: "none" + }) + .focus(function () { + if (settings.focusHint && (settings.tokenLimit == null || settings.tokenLimit != token_count)) { + show_dropdown_hint(); + } + if (settings.useClientSideSearch && !client_side_data) { + client_side_data = []; + $[settings.method.toLowerCase()](settings.url(), {}, prepare_client_side_data, settings.contentType); + } + }) + .blur(function () { + hide_dropdown(); + }) + .keydown(function (event) { + var previous_token; + var next_token; + + switch(event.keyCode) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + if(!$(this).val()) { + previous_token = input_token.prev(); + next_token = input_token.next(); + + if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { + // Check if there is a previous/next token and it is selected + if(event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) { + deselect_token($(selected_token), POSITION.BEFORE); + } else { + deselect_token($(selected_token), POSITION.AFTER); + } + } else if((event.keyCode == KEY.LEFT || event.keyCode == KEY.UP) && previous_token.length) { + // We are moving left, select the previous token if it exists + select_token($(previous_token.get(0))); + } else if((event.keyCode == KEY.RIGHT || event.keyCode == KEY.DOWN) && next_token.length) { + // We are moving right, select the next token if it exists + select_token($(next_token.get(0))); + } + } else { + var dropdown_item = null; + + if(!selected_dropdown_item) { + dropdown_item = $('li:first', dropdown); + } else { + if(event.keyCode == KEY.DOWN || event.keyCode == KEY.RIGHT) { + dropdown_item = $(selected_dropdown_item).next(); + } else { + dropdown_item = $(selected_dropdown_item).prev(); + } + } + + + if(dropdown_item.length) { + select_dropdown_item(dropdown_item); + } + return false; + } + break; + + case KEY.BACKSPACE: + previous_token = input_token.prev(); + + if(!$(this).val().length) { + if(selected_token) { + delete_token($(selected_token)); + } else if(previous_token.length) { + select_token($(previous_token.get(0))); + } + + return false; + } else if($(this).val().length == 1) { + hide_dropdown(); + } else { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search(false);}, 5); + } + break; + + case KEY.TAB: + case KEY.RETURN: + case KEY.COMMA: + + // Submit form if user hits return a second time + if(event.keyCode == KEY.RETURN && $(this).val() == "") { + parentForm[0].submit(); + return false; + } + if(selected_dropdown_item) { add_existing_token($(selected_dropdown_item)); } else if(!settings.requireMatch) { add_new_token($(this).val()); } - + return false; break; - case KEY.ESC: - hide_dropdown(); - return true; - - default: - if(is_printable_character(event.keyCode)) { - // set a timeout just long enough to let this function finish. - setTimeout(function(){do_search(false);}, 5); - } - break; - } - }); - - // Keep a reference to the original input box - var hidden_input = $(input) - .hide() - .focus(function () { - input_box.focus(); - }) - .blur(function () { - input_box.blur(); - }); - - // Keep a reference to the parent form - // Collect the stray arbitrary tags before the parent form submits - var parentForm = hidden_input.parents('form') - .submit(function(){ - if(!settings.requireMatch && input_box.val()!=$('label[for=' + input_box.attr('id') + ']').text()) { - add_new_token(input_box.val()); - } - }); - - // Keep a reference to the selected token and dropdown item - var selected_token = null; - var selected_dropdown_item = null; - - // The list to store the token items in - var token_list = $("
"+settings.searchingText+"
") - .show(); - } - - function show_dropdown_hint () { - dropdown - .html(""+settings.hintText+"
") - .show(); - } - - // Highlight the query part of the search term + token_count--; + + if (settings.tokenLimit != null) { + input_box + .show() + .val("") + .focus(); + } + } + + // Hide and clear the results dropdown + function hide_dropdown () { + dropdown.hide().empty(); + selected_dropdown_item = null; + } + + function show_dropdown_searching () { + hide_dropdown(); + dropdown + .html(""+settings.searchingText+"
") + .show(); + } + + function show_dropdown_hint () { + dropdown + .html(""+settings.hintText+"
") + .show(); + } + + // Highlight the query part of the search term function highlight_term(value, term) { return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); } - // Populate the results dropdown with some results - function populate_dropdown (query, results) { - if(results.length) { - dropdown.empty(); - var dropdown_ul = $(""+settings.noResultsText+"
").show(); - } - } - - // Highlight an item in the results dropdown - function select_dropdown_item (item) { - if(item) { - if(selected_dropdown_item) { - deselect_dropdown_item($(selected_dropdown_item)); - } - - item.addClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = item.get(0); - } - } - - // Remove highlighting from an item in the results dropdown - function deselect_dropdown_item (item) { - item.removeClass(settings.classes.selectedDropdownItem); - selected_dropdown_item = null; - } - - // Do a search and show the "searching" dropdown if the input is longer - // than settings.minChars - function do_search(immediate) { - var query = input_box.val().toLowerCase(); - - if (query && query.length) { - if(selected_token) { - deselect_token($(selected_token), POSITION.AFTER); - } - - if (query.length >= settings.minChars) { - if (settings.searchingText) - show_dropdown_searching(); - if (immediate) { - run_search(query); - } else { - clearTimeout(timeout); - timeout = setTimeout(function(){run_search(query);}, settings.searchDelay); - } - } else { - hide_dropdown(); - } - } - } - - // Do the actual search - function run_search(query) { - - if(query=='') { - hide_dropdown(); - return false; - } - - var cached_results = cache.get(query); - if(cached_results) { - populate_dropdown(query, cached_results); - } else { - var queryStringDelimiter = settings.url.indexOf("?") < 0 ? "?" : "&"; + // Populate the results dropdown with some results + function populate_dropdown (query, results) { + if(results.length) { + dropdown.empty(); + var dropdown_ul = $(""+settings.noResultsText+"
").show(); + } + } + + // Highlight an item in the results dropdown + function select_dropdown_item (item) { + if(item) { + if(selected_dropdown_item) { + deselect_dropdown_item($(selected_dropdown_item)); + } + + item.addClass(settings.classes.selectedDropdownItem); + selected_dropdown_item = item.get(0); + } + } + + // Remove highlighting from an item in the results dropdown + function deselect_dropdown_item (item) { + item.removeClass(settings.classes.selectedDropdownItem); + selected_dropdown_item = null; + } + + // Do a search and show the "searching" dropdown if the input is longer + // than settings.minChars + function do_search(immediate) { + var query = input_box.val().toLowerCase(); + + if (query && query.length) { + if(selected_token) { + deselect_token($(selected_token), POSITION.AFTER); + } + + if (query.length >= settings.minChars) { + if (settings.searchingText) + show_dropdown_searching(); + if (immediate) { + run_search(query); + } else { + clearTimeout(timeout); + timeout = setTimeout(function(){run_search(query);}, settings.searchDelay); + } + } else { + hide_dropdown(); + } + } + } + + // Do the actual search + function run_search(query) { + + if(query=='') { + hide_dropdown(); + return false; + } + + var cached_results = cache.get(query); + if(cached_results) { + populate_dropdown(query, cached_results); + } else { var callback = function(results) { - if($.isFunction(settings.onResult)) { - results = settings.onResult.call(this, results); - } - cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results); - populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); - - //TH - added to make sure we don't show results if there was no query. This can happen due to a race condition inserting tockens. - if($.trim(input_box.val()) == '') { - hide_dropdown(); - } - + if($.isFunction(settings.onResult)) { + results = settings.onResult.call(this, results); + } + cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results); + populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); + + //TH - added to make sure we don't show results if there was no query. This can happen due to a race condition inserting tockens. + if($.trim(input_box.val()) == '') { + hide_dropdown(); + } }; - - if(settings.useClientSideSearch) { - callback(search_client_side_data(query)); - } else if( settings.method == "POST" ) { - $.post(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType); - } else { - $.get(settings.url + queryStringDelimiter + settings.queryParam + "=" + query, {}, callback, settings.contentType); - } - } - } - - function prepare_client_side_data(results) { - client_side_data = []; - $.each(results, function(i,res){ - res.searchable_string = (res.name + "--" + res.id).toLowerCase(); - client_side_data.push(res); - }); - } - - function search_client_side_data(query) { - var lowerQuery = query.toLowerCase(); - var results = []; - $.each(client_side_data, function(i,data) { - if(data.searchable_string.indexOf(query) != -1) { - results.push(data); - } - }); - return results; - } + + if(settings.useClientSideSearch) { + callback(search_client_side_data(query)); + } else { + $[settings.method.toLowerCase()](settings.url(query), {}, callback, settings.contentType); + } + } + } + + function prepare_client_side_data(results) { + client_side_data = []; + $.each(results, function(i,res){ + res.searchable_string = (res.name + "--" + res.id).toLowerCase(); + client_side_data.push(res); + }); + } + + function search_client_side_data(query) { + var lowerQuery = query.toLowerCase(); + var results = []; + $.each(client_side_data, function(i,data) { + if(data.searchable_string.indexOf(query) != -1) { + results.push(data); + } + }); + return results; + } }; // Really basic cache for the results $.TokenList.Cache = function (options) { - var settings = $.extend({ - max_size: 50 - }, options); + var settings = $.extend({ + max_size: 50 + }, options); - var data = {}; - var size = 0; + var data = {}; + var size = 0; - var flush = function () { - data = {}; - size = 0; - }; + var flush = function () { + data = {}; + size = 0; + }; - this.add = function (query, results) { - if(size > settings.max_size) { - flush(); - } + this.add = function (query, results) { + if(size > settings.max_size) { + flush(); + } - if(!data[query]) { - size++; - } + if(!data[query]) { + size++; + } - data[query] = results; - }; + data[query] = results; + }; - this.get = function (query) { - return data[query]; - }; + this.get = function (query) { + return data[query]; + }; }; -})(jQuery); \ No newline at end of file +})(jQuery);