/* * jQuery Plugin: Tokenizing Autocomplete Text Entry * Version 1.6.0 * * Copyright (c) 2009 James Smith (http://loopj.com) * Licensed jointly under the GPL and MIT licenses, * choose which one suits your project best! * * Licensed under MIT * With modifications * */ (function ($) { // Default settings var DEFAULT_SETTINGS = { // Search settings method: "GET", contentType: "json", queryParam: "q", searchDelay: 300, minChars: 1, propertyToSearch: "name", jsonContainer: null, scrollKeyboard: false, // Display settings hintText: null, noResultsText: null, noResultsHideDropdown: false, searchingText: null, deleteText: "×", animateDropdown: true, emptyInputLength: null, // Tokenization settings tokenLimit: null, tokenDelimiter: ",", preventDuplicates: false, // Output settings tokenValue: "id", // Prepopulation settings prePopulate: null, processPrePopulate: false, // Manipulation settings idPrefix: "token-input-", // Formatters resultsFormatter: function(item) { let listItem = document.createElement("li"); listItem.textContent = item[this.propertyToSearch]; return listItem.outerHTML; }, tokenFormatter: function(item) { let listItem = document.createElement("li"); let p = document.createElement("p"); p.textContent = item[this.propertyToSearch]; listItem.appendChild(p); return listItem.outerHTML; }, // Validations validateItem: null, // Force selections only on mouse click noHoverSelect: false, // Callbacks onResult: null, onAdd: null, onDelete: null, onReady: null }; // Default classes to use when theming var DEFAULT_CLASSES = { 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" }; // Input box position "enum" var POSITION = { BEFORE: 0, AFTER: 1, END: 2 }; // Keys "enum" var KEY = { BACKSPACE: 8, TAB: 9, ENTER: 13, ESCAPE: 27, SPACE: 32, PAGE_UP: 33, PAGE_DOWN: 34, END: 35, HOME: 36, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, NUMPAD_ENTER: 108, COMMA: 188 }; // Additional public (exposed) methods var methods = { init: function(url_or_data_or_function, options) { var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); return this.each(function () { $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); }); }, clear: function() { this.data("tokenInputObject").clear(); return this; }, add: function(item) { this.data("tokenInputObject").add(item); return this; }, remove: function(item) { this.data("tokenInputObject").remove(item); return this; }, get: function() { return this.data("tokenInputObject").getTokens(); } } // Expose the .tokenInput function to jQuery as a plugin $.fn.tokenInput = function (method) { // Method calling and initialization logic if(methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else { return methods.init.apply(this, arguments); } }; // TokenList class for each input $.TokenList = function (input, url_or_data, settings) { // // Initialization // // Configure the data source if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { // Set the url to query against settings.url = url_or_data; // If the URL is a function, evaluate it here to do our initalization work var url = computeURL(); // Make a smart guess about cross-domain if it wasn't explicitly specified if(settings.crossDomain === undefined) { if(url.indexOf("://") === -1) { settings.crossDomain = false; } else { settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); } } } else if(typeof(url_or_data) === "object") { // Set the local data to search through settings.local_data = url_or_data; } // Build class names if(settings.classes) { // Use custom class names settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); } else if(settings.theme) { // Use theme-suffixed default class names settings.classes = {}; $.each(DEFAULT_CLASSES, function(key, value) { settings.classes[key] = value + "-" + settings.theme; }); } else { settings.classes = DEFAULT_CLASSES; } // 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, old vals var timeout; var input_val; function tokenize(){ var item = $(selected_dropdown_item).data("tokeninput"); if(!item && settings.textToData){ item = settings.textToData(input_box.val()); } if(item) { add_token(item); hidden_input.change(); return false; } } // Create a new text input an attach keyup events var input_box = $("") .css({ outline: "none" }) .attr("id", settings.idPrefix + input.id) .focus(function () { if (settings.minChars == 0) { setTimeout(function(){do_search();}, 5); } if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { show_dropdown_hint(); } }) .blur(function () { tokenize(); hide_dropdown(); $(this).val(""); }) .bind("keyup keydown blur update", resize_input) .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 { if (event.keyCode === KEY.UP || event.keyCode === KEY.DOWN) { var dropdown_item = null; if(!selected_dropdown_item && (event.keyCode === KEY.DOWN)) { dropdown_item = $('.token-input-dropdown li').first(); } else if(event.keyCode === KEY.DOWN) { dropdown_item = $(selected_dropdown_item).next(); } else { dropdown_item = $(selected_dropdown_item).prev(); } if(dropdown_item.length) { select_dropdown_item(dropdown_item,true); } else if (!(event.keyCode === KEY.DOWN) && $(selected_dropdown_item).length) { deselect_dropdown_item($(selected_dropdown_item)); } return false; } } break; case KEY.BACKSPACE: previous_token = input_token.prev(); if(!$(this).val().length) { if(selected_token) { delete_token($(selected_token)); hidden_input.change(); } 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();}, 5); } break; case KEY.TAB: case KEY.ENTER: case KEY.NUMPAD_ENTER: case KEY.COMMA: if (event.keyCode != KEY.ENTER && event.keyCode != KEY.NUMPAD_ENTER) { event.preventDefault(); } tokenize(); break; case KEY.ESCAPE: hide_dropdown(); return true; default: if(String.fromCharCode(event.which)) { // set a timeout just long enough to let this function finish. setTimeout(function(){do_search();}, 5); } break; } }); // Keep a reference to the original input box var hidden_input = $(input) .hide() .val("") .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_token_index = 0; var selected_dropdown_item = null; // The list to store the token items in var token_list = $("
"+settings.searchingText+"
"); show_dropdown(); } } function show_dropdown_hint () { if(settings.hintText) { dropdown.html(""+settings.hintText+"
"); show_dropdown(); } } // Highlight the query part of the search term function highlight_term(value, term) { return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); } function find_value_and_highlight_term(template, value, term) { return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); } // Populate the results dropdown with some results function populate_dropdown (query, results) { if(results && results.length) { dropdown.empty(); var dropdown_ul = $(""+settings.noResultsText+"
"); show_dropdown(); } if (settings.noResultsHideDropdown) { hide_dropdown(); } } } // Highlight an item in the results dropdown function select_dropdown_item (item,withkeyboard) { if(item) { if(selected_dropdown_item) { deselect_dropdown_item($(selected_dropdown_item)); } if (settings.scrollKeyboard && withkeyboard) { var list = $('.token-input-dropdown-tag ul'); var listheight = list.height(); var itemheight = item.outerHeight(); var itemtop = item.position().top; if (itemtop > listheight) { var listscroll = list.scrollTop(); list.scrollTop(listscroll + itemheight); } else if (itemtop < 0) { var listscroll = list.scrollTop(); list.scrollTop(listscroll - itemheight); } } 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() { var query = input_box.val().toLowerCase(); if(query && query.length || settings.minChars == 0) { if(selected_token) { deselect_token($(selected_token), POSITION.AFTER); } if(query.length >= settings.minChars) { show_dropdown_searching(); clearTimeout(timeout); timeout = setTimeout(function(){ run_search(query); }, settings.searchDelay); } else { hide_dropdown(); } } } // Do the actual search function run_search(query) { var cache_key = query + computeURL(); var cached_results = cache.get(cache_key); if(cached_results) { populate_dropdown(query, cached_results); } else { // Are we doing an ajax search or local data search? if(settings.url) { var url = computeURL(); // Extract exisiting get params var ajax_params = {}; ajax_params.data = {}; if(url.indexOf("?") > -1) { var parts = url.split("?"); ajax_params.url = parts[0]; var param_array = parts[1].split("&"); $.each(param_array, function (index, value) { var kv = value.split("="); ajax_params.data[kv[0]] = kv[1]; }); } else { ajax_params.url = url; } // Prepare the request ajax_params.data[settings.queryParam] = query; ajax_params.type = settings.method; ajax_params.dataType = settings.contentType; if(settings.crossDomain) { ajax_params.dataType = "jsonp"; } // Attach the success callback ajax_params.success = function(results) { if($.isFunction(settings.onResult)) { results = settings.onResult.call(hidden_input, results); } cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); // only populate the dropdown if the results are associated with the active search query if(input_box.val().toLowerCase() === query) { populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); } }; // Make the request $.ajax(ajax_params); } else if(settings.search_function){ settings.search_function(query, function(results){ cache.add(cache_key, results); populate_dropdown(query, results); }); } else if(settings.local_data) { // Do the search through local data var results = $.grep(settings.local_data, function (row) { return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; }); if($.isFunction(settings.onResult)) { results = settings.onResult.call(hidden_input, results); } cache.add(cache_key, results); populate_dropdown(query, results); } } } // compute the dynamic URL function computeURL() { var url = settings.url; if(typeof settings.url == 'function') { url = settings.url.call(); } return url; } }; // Really basic cache for the results $.TokenList.Cache = function (options) { var settings = $.extend({ max_size: 500 }, 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 += 1; } data[query] = results; }; this.get = function (query) { return data[query]; }; }; }(jQuery));