+/*
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
+ * Version 1.1
+ *
+ * Copyright (c) 2009 James Smith (http://loopj.com)
+ * Licensed jointly under the GPL and MIT licenses,
+ * choose which one suits your project best!
+ *
+ */
+
+(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
+ }, options);
+
+ settings.classes = $.extend({
+ tokenList: "token-input-list",
+ token: "token-input-token",
+ 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"
+ }, 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;
+
+ // Create a new text input an attach keyup events
+ var input_box = $("<input type=\"text\">")
+ .css({
+ outline: "none"
+ })
+ .focus(function () {
+ if (settings.tokenLimit == null || settings.tokenLimit != token_count) {
+ show_dropdown_hint();
+ }
+ })
+ .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(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:
+ if(selected_dropdown_item) {
+ add_token($(selected_dropdown_item));
+ 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 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 = $("<ul />")
+ .addClass(settings.classes.tokenList)
+ .insertAfter(hidden_input)
+ .click(function (event) {
+ var li = get_element_from_event(event, "li");
+ if(li && li.get(0) != input_token.get(0)) {
+ toggle_select_token(li);
+ return false;
+ } else {
+ input_box.focus();
+
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+ }
+ })
+ .mouseover(function (event) {
+ var li = get_element_from_event(event, "li");
+ if(li && selected_token !== this) {
+ li.addClass(settings.classes.highlightedToken);
+ }
+ })
+ .mouseout(function (event) {
+ var li = get_element_from_event(event, "li");
+ if(li && selected_token !== this) {
+ li.removeClass(settings.classes.highlightedToken);
+ }
+ })
+ .mousedown(function (event) {
+ // Stop user selecting text on tokens
+ var li = get_element_from_event(event, "li");
+ if(li){
+ return false;
+ }
+ });
+
+
+ // The list to store the dropdown items in
+ var dropdown = $("<div>")
+ .addClass(settings.classes.dropdown)
+ .insertAfter(token_list)
+ .hide();
+
+ // The token holding the input box
+ var input_token = $("<li />")
+ .addClass(settings.classes.inputToken)
+ .appendTo(token_list)
+ .append(input_box);
+
+ init_list();
+
+ //
+ // Functions
+ //
+
+
+ // Pre-populate list if items exist
+ function init_list () {
+ li_data = settings.prePopulate;
+ if(li_data && li_data.length) {
+ for(var i in li_data) {
+ var this_token = $("<li><p>"+li_data[i].name+"</p> </li>")
+ .addClass(settings.classes.token)
+ .insertBefore(input_token);
+
+ $("<span>x</span>")
+ .addClass(settings.classes.tokenDelete)
+ .appendTo(this_token)
+ .click(function () {
+ delete_token($(this).parent());
+ return false;
+ });
+
+ $.data(this_token.get(0), "tokeninput", {"id": li_data[i].id, "name": li_data[i].name});
+
+ // Clear input box and make sure it keeps focus
+ input_box
+ .val("")
+ .focus();
+
+ // Don't show the help dropdown, they've got the idea
+ hide_dropdown();
+
+ // Save this token id
+ var id_string = li_data[i].id + ","
+ hidden_input.val(hidden_input.val() + id_string);
+ }
+ }
+ }
+
+ function is_printable_character(keycode) {
+ if((keycode >= 48 && keycode <= 90) || // 0-1a-z
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
+ (keycode >= 219 && keycode <= 222) // ( \ ) '
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Get an element of a particular type from an event (click/mouseover etc)
+ function get_element_from_event (event, element_type) {
+ var target = $(event.target);
+ var element = null;
+
+ if(target.is(element_type)) {
+ element = target;
+ } else if(target.parent(element_type).length) {
+ element = target.parent(element_type+":first");
+ }
+
+ return element;
+ }
+
+ // Inner function to a token to the list
+ function insert_token(id, value) {
+ var this_token = $("<li><p>"+ value +"</p> </li>")
+ .addClass(settings.classes.token)
+ .insertBefore(input_token);
+
+ // The 'delete token' button
+ $("<span>x</span>")
+ .addClass(settings.classes.tokenDelete)
+ .appendTo(this_token)
+ .click(function () {
+ delete_token($(this).parent());
+ return false;
+ });
+
+ $.data(this_token.get(0), "tokeninput", {"id": id, "name": value});
+
+ return this_token;
+ }
+
+ // Add a token to the token list based on user input
+ function add_token (item) {
+ var li_data = $.data(item.get(0), "tokeninput");
+ var this_token = insert_token(li_data.id, li_data.name);
+
+ // Clear input box and make sure it keeps focus
+ input_box
+ .val("")
+ .focus();
+
+ // Don't show the help dropdown, they've got the idea
+ hide_dropdown();
+
+ // Save this token id
+ var id_string = li_data.id + ","
+ hidden_input.val(hidden_input.val() + id_string);
+
+ token_count++;
+
+ if(settings.tokenLimit != null && settings.tokenLimit >= token_count) {
+ input_box.hide();
+ hide_dropdown();
+ }
+ }
+
+ // Select a token in the token list
+ function select_token (token) {
+ token.addClass(settings.classes.selectedToken);
+ selected_token = token.get(0);
+
+ // Hide input box
+ input_box.val("");
+
+ // Hide dropdown if it is visible (eg if we clicked to select token)
+ hide_dropdown();
+ }
+
+ // Deselect a token in the token list
+ function deselect_token (token, position) {
+ token.removeClass(settings.classes.selectedToken);
+ selected_token = null;
+
+ if(position == POSITION.BEFORE) {
+ input_token.insertBefore(token);
+ } else if(position == POSITION.AFTER) {
+ input_token.insertAfter(token);
+ } else {
+ input_token.appendTo(token_list);
+ }
+
+ // Show the input box and give it focus again
+ input_box.focus();
+ }
+
+ // Toggle selection of a token in the token list
+ function toggle_select_token (token) {
+ if(selected_token == token.get(0)) {
+ deselect_token(token, POSITION.END);
+ } else {
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+ select_token(token);
+ }
+ }
+
+ // Delete a token from the token list
+ function delete_token (token) {
+ // Remove the id from the saved list
+ var token_data = $.data(token.get(0), "tokeninput");
+
+ // Delete the token
+ token.remove();
+ selected_token = null;
+
+ // Show the input box and give it focus again
+ input_box.focus();
+
+ // Delete this token's id from hidden input
+ var str = hidden_input.val()
+ var start = str.indexOf(token_data.id+",");
+ var end = str.indexOf(",", start) + 1;
+
+ if(end >= str.length) {
+ hidden_input.val(str.slice(0, start));
+ } else {
+ hidden_input.val(str.slice(0, start) + str.slice(end, str.length));
+ }
+
+ 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 () {
+ dropdown
+ .html("<p>"+settings.searchingText+"</p>")
+ .show();
+ }
+
+ function show_dropdown_hint () {
+ dropdown
+ .html("<p>"+settings.hintText+"</p>")
+ .show();
+ }
+
+ // Highlight the query part of the search term
+ function highlight_term(value, term) {
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
+ }
+
+ // Populate the results dropdown with some results
+ function populate_dropdown (query, results) {
+ if(results.length) {
+ dropdown.empty();
+ var dropdown_ul = $("<ul>")
+ .appendTo(dropdown)
+ .mouseover(function (event) {
+ select_dropdown_item(get_element_from_event(event, "li"));
+ })
+ .mousedown(function (event) {
+ add_token(get_element_from_event(event, "li"));
+ return false;
+ })
+ .hide();
+
+ for(var i in results) {
+ if (results.hasOwnProperty(i)) {
+ var this_li = $("<li>"+highlight_term(results[i].name, query)+"</li>")
+ .appendTo(dropdown_ul);
+
+ if(i%2) {
+ this_li.addClass(settings.classes.dropdownItem);
+ } else {
+ this_li.addClass(settings.classes.dropdownItem2);
+ }
+
+ if(i == 0) {
+ select_dropdown_item(this_li);
+ }
+
+ $.data(this_li.get(0), "tokeninput", {"id": results[i].id, "name": results[i].name});
+ }
+ }
+
+ dropdown.show();
+ dropdown_ul.slideDown("fast");
+
+ } else {
+ dropdown
+ .html("<p>"+settings.noResultsText+"</p>")
+ .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) {
+ 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) {
+ var cached_results = cache.get(query);
+ if(cached_results) {
+ populate_dropdown(query, cached_results);
+ } else {
+ var queryStringDelimiter = settings.url.indexOf("?") < 0 ? "?" : "&";
+ 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);
+ };
+
+ 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);
+ }
+ }
+ }
+};
+
+// Really basic cache for the results
+$.TokenList.Cache = function (options) {
+ var settings = $.extend({
+ max_size: 50
+ }, options);
+
+ var data = {};
+ var size = 0;
+
+ var flush = function () {
+ data = {};
+ size = 0;
+ };
+
+ this.add = function (query, results) {
+ if(size > settings.max_size) {
+ flush();
+ }
+
+ if(!data[query]) {
+ size++;
+ }
+
+ data[query] = results;
+ };
+
+ this.get = function (query) {
+ return data[query];
+ };
+};
+
+})(jQuery);
\ No newline at end of file