/* * jQuery Plugin: Tokenizing Autocomplete Text Entry * Version 1.6.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 ($) { // Default settings var DEFAULT_SETTINGS = { // Search settings method: "GET", queryParam: "q", searchDelay: 300, minChars: 1, propertyToSearch: "name", jsonContainer: null, contentType: "json", // Prepopulation settings prePopulate: null, processPrePopulate: false, // Display settings hintText: "Type in a search term", noResultsText: "No results", searchingText: "Searching...", deleteText: "×", animateDropdown: true, placeholder: null, theme: null, zindex: 999, resultsLimit: null, enableHTML: false, resultsFormatter: function(item) { var string = item[this.propertyToSearch]; return "<li>" + (this.enableHTML ? string : _escapeHTML(string)) + "</li>"; }, tokenFormatter: function(item) { var string = item[this.propertyToSearch]; return "<li><p>" + (this.enableHTML ? string : _escapeHTML(string)) + "</p></li>"; }, // Tokenization settings tokenLimit: null, tokenDelimiter: ",", preventDuplicates: false, tokenValue: "id", // Behavioral settings allowFreeTagging: false, allowTabOut: false, // Callbacks onResult: null, onCachedResult: null, onAdd: null, onFreeTaggingAdd: null, onDelete: null, onReady: null, // Other settings idPrefix: "token-input-", // Keep track if the input is currently in disabled mode disabled: false }; // Default classes to use when theming var DEFAULT_CLASSES = { tokenList: "token-input-list", token: "token-input-token", tokenReadOnly: "token-input-token-readonly", 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", focused: "token-input-focused", disabled: "token-input-disabled" }; // 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 }; var HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' }; var HTML_ESCAPE_CHARS = /[&<>"'\/]/g; function coerceToString(val) { return String((val === null || val === undefined) ? '' : val); } function _escapeHTML(text) { return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) { return HTML_ESCAPES[match]; }); } // 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("settings", settings); $(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(); }, toggleDisabled: function(disable) { this.data("tokenInputObject").toggleDisabled(disable); return this; }, setOptions: function(options){ $(this).data("settings", $.extend({}, $(this).data("settings"), options || {})); return this; } }; // 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 $(input).data("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($(input).data("settings").crossDomain === undefined && typeof url === "string") { if(url.indexOf("://") === -1) { $(input).data("settings").crossDomain = false; } else { $(input).data("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 $(input).data("settings").local_data = url_or_data; } // Build class names if($(input).data("settings").classes) { // Use custom class names $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes); } else if($(input).data("settings").theme) { // Use theme-suffixed default class names $(input).data("settings").classes = {}; $.each(DEFAULT_CLASSES, function(key, value) { $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme; }); } else { $(input).data("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; // Create a new text input an attach keyup events var input_box = $("<input type=\"text\" autocomplete=\"off\" autocapitalize=\"off\">") .css({ outline: "none" }) .attr("id", $(input).data("settings").idPrefix + input.id) .focus(function () { if ($(input).data("settings").disabled) { return false; } else if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) { show_dropdown_hint(); } token_list.addClass($(input).data("settings").classes.focused); }) .blur(function () { hide_dropdown(); if ($(input).data("settings").allowFreeTagging) { add_freetagging_tokens(); } $(this).val(""); token_list.removeClass($(input).data("settings").classes.focused); }) .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 { 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)); 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(selected_dropdown_item) { add_token($(selected_dropdown_item).data("tokeninput")); hidden_input.change(); } else { if ($(input).data("settings").allowFreeTagging) { if($(input).data("settings").allowTabOut && $(this).val() === "") { return true; } else { add_freetagging_tokens(); } } else { $(this).val(""); if($(input).data("settings").allowTabOut) { return true; } } event.stopPropagation(); event.preventDefault(); } return false; 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 reference for placeholder if (settings.placeholder) input_box.attr("placeholder", settings.placeholder) // Keep a reference to the original input box var hidden_input = $(input) .hide() .val("") .focus(function () { focus_with_timeout(input_box); }) .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 = $("<ul />") .addClass($(input).data("settings").classes.tokenList) .click(function (event) { var li = $(event.target).closest("li"); if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { toggle_select_token(li); } else { // Deselect selected token if(selected_token) { deselect_token($(selected_token), POSITION.END); } // Focus input box focus_with_timeout(input_box); } }) .mouseover(function (event) { var li = $(event.target).closest("li"); if(li && selected_token !== this) { li.addClass($(input).data("settings").classes.highlightedToken); } }) .mouseout(function (event) { var li = $(event.target).closest("li"); if(li && selected_token !== this) { li.removeClass($(input).data("settings").classes.highlightedToken); } }) .insertBefore(hidden_input); // The token holding the input box var input_token = $("<li />") .addClass($(input).data("settings").classes.inputToken) .appendTo(token_list) .append(input_box); // The list to store the dropdown items in var dropdown = $("<div>") .addClass($(input).data("settings").classes.dropdown) .appendTo("body") .hide(); // Magic element to help us resize the text input var input_resizer = $("<tester/>") .insertAfter(input_box) .css({ position: "absolute", top: -9999, left: -9999, width: "auto", fontSize: input_box.css("fontSize"), fontFamily: input_box.css("fontFamily"), fontWeight: input_box.css("fontWeight"), letterSpacing: input_box.css("letterSpacing"), whiteSpace: "nowrap" }); // Pre-populate list if items exist hidden_input.val(""); var li_data = $(input).data("settings").prePopulate || hidden_input.data("pre"); if($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) { li_data = $(input).data("settings").onResult.call(hidden_input, li_data); } if(li_data && li_data.length) { $.each(li_data, function (index, value) { insert_token(value); checkTokenLimit(); input_box.attr("placeholder", null) }); } // Check if widget should initialize as disabled if ($(input).data("settings").disabled) { toggleDisabled(true); } // Initialization is done if($.isFunction($(input).data("settings").onReady)) { $(input).data("settings").onReady.call(); } // // Public functions // this.clear = function() { token_list.children("li").each(function() { if ($(this).children("input").length === 0) { delete_token($(this)); } }); }; this.add = function(item) { add_token(item); }; this.remove = function(item) { token_list.children("li").each(function() { if ($(this).children("input").length === 0) { var currToken = $(this).data("tokeninput"); var match = true; for (var prop in item) { if (item[prop] !== currToken[prop]) { match = false; break; } } if (match) { delete_token($(this)); } } }); }; this.getTokens = function() { return saved_tokens; }; this.toggleDisabled = function(disable) { toggleDisabled(disable); }; // Resize input to maximum width so the placeholder can be seen resize_input(); // // Private functions // function escapeHTML(text) { return $(input).data("settings").enableHTML ? text : _escapeHTML(text); } // Toggles the widget between enabled and disabled state, or according // to the [disable] parameter. function toggleDisabled(disable) { if (typeof disable === 'boolean') { $(input).data("settings").disabled = disable } else { $(input).data("settings").disabled = !$(input).data("settings").disabled; } input_box.attr('disabled', $(input).data("settings").disabled); token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled); // if there is any token selected we deselect it if(selected_token) { deselect_token($(selected_token), POSITION.END); } hidden_input.attr('disabled', $(input).data("settings").disabled); } function checkTokenLimit() { if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { input_box.hide(); hide_dropdown(); return; } } function resize_input() { if(input_val === (input_val = input_box.val())) {return;} // Get width left on the current line var width_left = token_list.width() - input_box.offset().left - token_list.offset().left; // Enter new content into resizer and resize input accordingly input_resizer.html(_escapeHTML(input_val)); // Get maximum width, minimum the size of input and maximum the widget's width input_box.width(Math.min(token_list.width(), Math.max(width_left, input_resizer.width() + 30))); } function is_printable_character(keycode) { return ((keycode >= 48 && keycode <= 90) || // 0-1a-z (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ (keycode >= 219 && keycode <= 222)); // ( \ ) ' } function add_freetagging_tokens() { var value = $.trim(input_box.val()); var tokens = value.split($(input).data("settings").tokenDelimiter); $.each(tokens, function(i, token) { if (!token) { return; } if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) { token = $(input).data("settings").onFreeTaggingAdd.call(hidden_input, token); } var object = {}; object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token; add_token(object); }); } // Inner function to a token to the list function insert_token(item) { var $this_token = $($(input).data("settings").tokenFormatter(item)); var readonly = item.readonly === true ? true : false; if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly); $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token); // The 'delete token' button if(!readonly) { $("<span>" + $(input).data("settings").deleteText + "</span>") .addClass($(input).data("settings").classes.tokenDelete) .appendTo($this_token) .click(function () { if (!$(input).data("settings").disabled) { delete_token($(this).parent()); hidden_input.change(); return false; } }); } // Store data on the token var token_data = item; $.data($this_token.get(0), "tokeninput", item); // Save this token for duplicate checking saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); selected_token_index++; // Update the hidden input update_hidden_input(saved_tokens, hidden_input); token_count += 1; // Check the token limit if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) { input_box.hide(); hide_dropdown(); } return $this_token; } // Add a token to the token list based on user input function add_token (item) { var callback = $(input).data("settings").onAdd; // See if the token already exists and select it if we don't want duplicates if(token_count > 0 && $(input).data("settings").preventDuplicates) { var found_existing_token = null; token_list.children().each(function () { var existing_token = $(this); var existing_data = $.data(existing_token.get(0), "tokeninput"); if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) { found_existing_token = existing_token; return false; } }); if(found_existing_token) { select_token(found_existing_token); input_token.insertAfter(found_existing_token); focus_with_timeout(input_box); return; } } // Squeeze input_box so we force no unnecessary line break input_box.width(0); // Insert the new tokens if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) { insert_token(item); // Remove the placeholder so it's not seen after you've added a token input_box.attr("placeholder", null) checkTokenLimit(); } // Clear input box input_box.val(""); // Don't show the help dropdown, they've got the idea hide_dropdown(); // Execute the onAdd callback if defined if($.isFunction(callback)) { callback.call(hidden_input,item); } } // Select a token in the token list function select_token (token) { if (!$(input).data("settings").disabled) { token.addClass($(input).data("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($(input).data("settings").classes.selectedToken); selected_token = null; if(position === POSITION.BEFORE) { input_token.insertBefore(token); selected_token_index--; } else if(position === POSITION.AFTER) { input_token.insertAfter(token); selected_token_index++; } else { input_token.appendTo(token_list); selected_token_index = token_count; } // Show the input box and give it focus again focus_with_timeout(input_box); } // Toggle selection of a token in the token list function toggle_select_token(token) { var previous_selected_token = selected_token; if(selected_token) { deselect_token($(selected_token), POSITION.END); } if(previous_selected_token === token.get(0)) { deselect_token(token, POSITION.END); } else { 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"); var callback = $(input).data("settings").onDelete; var index = token.prevAll().length; if(index > selected_token_index) index--; // Delete the token token.remove(); selected_token = null; // Show the input box and give it focus again focus_with_timeout(input_box); // Remove this token from the saved list saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); if (saved_tokens.length == 0) { input_box.attr("placeholder", settings.placeholder) } if(index < selected_token_index) selected_token_index--; // Update the hidden input update_hidden_input(saved_tokens, hidden_input); token_count -= 1; if($(input).data("settings").tokenLimit !== null) { input_box .show() .val(""); focus_with_timeout(input_box); } // Execute the onDelete callback if defined if($.isFunction(callback)) { callback.call(hidden_input,token_data); } } // Update the hidden input box value function update_hidden_input(saved_tokens, hidden_input) { var token_values = $.map(saved_tokens, function (el) { if(typeof $(input).data("settings").tokenValue == 'function') return $(input).data("settings").tokenValue.call(this, el); return el[$(input).data("settings").tokenValue]; }); hidden_input.val(token_values.join($(input).data("settings").tokenDelimiter)); } // Hide and clear the results dropdown function hide_dropdown () { dropdown.hide().empty(); selected_dropdown_item = null; } function show_dropdown() { dropdown .css({ position: "absolute", top: $(token_list).offset().top + $(token_list).height(), left: $(token_list).offset().left, width: $(token_list).width(), 'z-index': $(input).data("settings").zindex }) .show(); } function show_dropdown_searching () { if($(input).data("settings").searchingText) { dropdown.html("<p>" + escapeHTML($(input).data("settings").searchingText) + "</p>"); show_dropdown(); } } function show_dropdown_hint () { if($(input).data("settings").hintText) { dropdown.html("<p>" + escapeHTML($(input).data("settings").hintText) + "</p>"); show_dropdown(); } } var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'); function regexp_escape(term) { return term.replace(regexp_special_chars, '\\$&'); } // Highlight the query part of the search term function highlight_term(value, term) { return value.replace( new RegExp( "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)", "gi" ), function(match, p1) { return "<b>" + escapeHTML(p1) + "</b>"; } ); } function find_value_and_highlight_term(template, value, term) { return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(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 = $("<ul>") .appendTo(dropdown) .mouseover(function (event) { select_dropdown_item($(event.target).closest("li")); }) .mousedown(function (event) { add_token($(event.target).closest("li").data("tokeninput")); hidden_input.change(); return false; }) .hide(); if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) { results = results.slice(0, $(input).data("settings").resultsLimit); } $.each(results, function(index, value) { var this_li = $(input).data("settings").resultsFormatter(value); this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query); this_li = $(this_li).appendTo(dropdown_ul); if(index % 2) { this_li.addClass($(input).data("settings").classes.dropdownItem); } else { this_li.addClass($(input).data("settings").classes.dropdownItem2); } if(index === 0) { select_dropdown_item(this_li); } $.data(this_li.get(0), "tokeninput", value); }); show_dropdown(); if($(input).data("settings").animateDropdown) { dropdown_ul.slideDown("fast"); } else { dropdown_ul.show(); } } else { if($(input).data("settings").noResultsText) { dropdown.html("<p>" + escapeHTML($(input).data("settings").noResultsText) + "</p>"); show_dropdown(); } } } // 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($(input).data("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($(input).data("settings").classes.selectedDropdownItem); selected_dropdown_item = null; } // Do a search and show the "searching" dropdown if the input is longer // than $(input).data("settings").minChars function do_search() { var query = input_box.val(); if(query && query.length) { if(selected_token) { deselect_token($(selected_token), POSITION.AFTER); } if(query.length >= $(input).data("settings").minChars) { show_dropdown_searching(); clearTimeout(timeout); timeout = setTimeout(function(){ run_search(query); }, $(input).data("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) { if ($.isFunction($(input).data("settings").onCachedResult)) { cached_results = $(input).data("settings").onCachedResult.call(hidden_input, cached_results); } populate_dropdown(query, cached_results); } else { // Are we doing an ajax search or local data search? if($(input).data("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[$(input).data("settings").queryParam] = query; ajax_params.type = $(input).data("settings").method; ajax_params.dataType = $(input).data("settings").contentType; if($(input).data("settings").crossDomain) { ajax_params.dataType = "jsonp"; } // Attach the success callback ajax_params.success = function(results) { cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); if($.isFunction($(input).data("settings").onResult)) { results = $(input).data("settings").onResult.call(hidden_input, results); } // only populate the dropdown if the results are associated with the active search query if(input_box.val() === query) { populate_dropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results); } }; // Make the request $.ajax(ajax_params); } else if($(input).data("settings").local_data) { // Do the search through local data var results = $.grep($(input).data("settings").local_data, function (row) { return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; }); cache.add(cache_key, results); if($.isFunction($(input).data("settings").onResult)) { results = $(input).data("settings").onResult.call(hidden_input, results); } populate_dropdown(query, results); } } } // compute the dynamic URL function computeURL() { var url = $(input).data("settings").url; if(typeof $(input).data("settings").url == 'function') { url = $(input).data("settings").url.call($(input).data("settings")); } return url; } // Bring browser focus to the specified object. // Use of setTimeout is to get around an IE bug. // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie) // // obj: a jQuery object to focus() function focus_with_timeout(obj) { setTimeout(function() { obj.focus(); }, 50); } }; // 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));