frontend/app/assets/javascripts/jquery.tokeninput.js
/*
* 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!
*
*/
(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,
theme: null,
zindex: 999,
resultsLimit: null,
enableHTML: true,
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,
// 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,
formatQueryParam: function (q, ajax_params) {
return q;
},
caching: true,
};
// 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: null, // 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">')
.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);
var $combobox = token_list.closest('div.controls');
$combobox.attr('aria-expanded', true);
})
.blur(function () {
hide_dropdown();
token_list
.closest('div.controls')
.find("input[role='searchbox']")
.removeAttr('aria-controls');
$(this).val('');
token_list.removeClass($(input).data('settings').classes.focused);
if ($(input).data('settings').allowFreeTagging) {
add_freetagging_tokens();
} else {
$(this).val('');
}
token_list.removeClass($(input).data('settings').classes.focused);
token_list.closest('div.controls').attr('aria-expanded', false);
})
.bind('keyup keydown blur update', resize_input)
.keydown(function (event) {
var previous_token;
var next_token;
switch (event.keyCode) {
case KEY.LEFT:
return true;
case KEY.RIGHT:
return true;
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;
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:
return true;
case KEY.ENTER:
if (selected_dropdown_item) {
add_token($(selected_dropdown_item).data('tokeninput'));
hidden_input.change();
} else {
$(input).trigger('tokeninput.enter');
}
event.stopPropagation();
event.preventDefault();
break;
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) {
add_freetagging_tokens();
}
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 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
/**
* ANW-897: The plugin appends dropdown to <body>, preventing
* scrolling when at the bottom of the viewport.
* Let's override this unmaintained plugin by
* appending the dropdown to a relative parent.
* Let's also adjust dropdown styles (see show_dropdown() below).
*/
var dropdown_parent = $('<section>')
.insertAfter(token_list)
.css({ position: 'relative' });
var dropdown = $('<div>')
.addClass($(input).data('settings').classes.dropdown)
.appendTo(dropdown_parent)
.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();
});
}
// 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);
};
//
// 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;
}
// Enter new content into resizer and resize input accordingly
input_resizer.html(_escapeHTML(input_val));
input_box.width(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.id === item.id) {
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;
}
}
// Insert the new tokens
if (
$(input).data('settings').tokenLimit === null ||
token_count < $(input).data('settings').tokenLimit
) {
insert_token(item);
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 (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() {
/**
* ANW-897: The plugin appends dropdown to <body>, preventing
* scrolling when at the bottom of the viewport.
* Let's override this unmaintained plugin by
* appending the dropdown to a relative parent
* (see dropdown_parent above). Let's also adjust dropdown
* styles.
*/
dropdown
.css({
position: 'absolute',
top: $(token_list).outerHeight(),
left: 0,
width: $(token_list).outerWidth(),
'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_label = dropdown_parent[0].nextSibling.id + '_label';
var ul_id = dropdown_parent[0].nextSibling.id + '_listbox';
var dropdown_ul = $(
'<ul aria-labelledby=' +
dropdown_label +
" role='listbox' id=" +
ul_id +
'>'
)
.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();
dropdown_ul
.closest('div.controls')
.find("input[role='searchbox']")
.attr('aria-controls', dropdown_ul.attr('id'));
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)
.attr('aria-selected', true);
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)
.removeAttr('aria-selected');
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] = $(input)
.data('settings')
.formatQueryParam(query, ajax_params);
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) {
if ($(input).data('settings').caching) {
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);