/**
* Smart Auto Complete plugin
*
* Copyright (c) 2011 Lakshan Perera (laktek.com)
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) licenses.
*
*/
/*
Requirements: jQuery 1.5 or above
Usage:
$(target).smartAutoComplete({options})
Options:
minCharLimit: (integer) minimum characters user have to type before invoking the autocomplete (default: 1)
maxCharLimit: (integer) maximum characters user can type while invoking the autocomplete (default: null (unlimited))
maxResults: (integer) maximum number of results to return (default: null (unlimited))
delay: (integer) delay before autocomplete starts (default: 0)
disabled: (boolean) whether autocomplete disabled on the field (default: false)
forceSelect: (boolean) If set to true, field will be always filled with best matching result, without leaving the custom input.
Better to enable this option, if you want autocomplete field to behave similar to a HTML select field. (Check Example 2 in the demo)
(default: false)
typeAhead: (boolean) If set to true, it will offer the best matching result in grey within the field; that can be auto-completed by pressing the right arrow-key or enter.
This is similar to behaviour in Google Instant Search's query field (Check Example 3 in the demo)
(default: false)
source: (string/array) you can supply an array with items or a string containing a URL to fetch items for the source
this is optional if you prefer to have your own filter method
filter: (function) define a custom function that would return matching items to the entered text (this will override the default filtering algorithm)
should return an array or a Deferred object (ajax call)
parameters available: term, source
resultFormatter: (function) the function you supply here will be called to format the output of an individual result.
should return a string
parameters available: result
resultsContainer: (selector) to which element(s) the result should be appended.
resultElement: (selector) references to the result elements collection (e.g. li, div.result)
Events:
keyIn: fires when user types into the field (parameters: query)
resultsReady: fires when the filter function returns (parameters: results)
showResults: fires when results are shown (parameters: results)
noResults: fires when filter returns an empty array
itemSelect: fires when user selects an item from the result list (paramters: item)
itemFocus: fires when user highlights an item with mouse or arrow keys (paramters: item)
itemUnfocus: fires when user moves out from an highlighted item (paramters: item)
lostFocus: fires when autocomplete field loses focus by user clicking outside of the field or focusing on another field. Also, this event is fired when a value is selected
})
*/
(function($){
$.fn.smartAutoComplete = function(){
if(arguments.length < 1){
// get the smart autocomplete object of the first element and return
var first_element = this[0];
return $(first_element).data("smart-autocomplete")
}
var default_filter_matcher = function(term, source, context){
var matcher = new RegExp(term.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), "i" );
return $.grep(source, function(value) {
return matcher.test( value );
});
}
var default_options = {
minCharLimit: 1,
maxCharLimit: null,
maxResults: null,
delay: 0,
disabled: false,
forceSelect: false,
typeAhead: false,
resultElement: "li",
resultFormatter: function(r){ return ("
" + r + "
"); },
filter: function(term, source){
var context = this;
var options = $(context).data('smart-autocomplete');
//when source is an array
if($.type(source) === "array") {
// directly map
var results = default_filter_matcher(term, source, context);
return results;
}
//when source is a string
else if($.type(source) === "string"){
// treat the string as a URL endpoint
// pass the query as 'term'
return $.Deferred(function(dfd){
$.ajax({
url: source,
data: {"term": term},
dataType: "json"
}).success( function(data){
dfd.resolve( default_filter_matcher(term, data, context) );
});
}).promise();
}
},
alignResultsContainer: false,
clearResults: function(){
//remove type ahead field
var type_ahead_field = $(this.context).prev(".smart_autocomplete_type_ahead_field");
$(this.context).css({ background: type_ahead_field.css("background") });
type_ahead_field.remove();
//clear results div
$(this.resultsContainer).html("");
},
setCurrentSelectionToContext: function(){
if(this.rawResults.length > 0 && this.currentSelection >= 0)
$(this.context).val(this.rawResults[(this.currentSelection)]);
},
setItemSelected: function(val){
this.itemSelected = val;
},
autocompleteFocused: false,
setAutocompleteFocused: function(val){
this.autocompleteFocused = val;
}
};
//define the default events
$.event.special.keyIn = {
setup: function(){ return false; },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
var source = options.source || null;
var filter = options.filter;
var maxChars = (options.maxCharLimit > 0 ? options.maxCharLimit : Number.POSITIVE_INFINITY)
//event specific data
var query = ev.smartAutocompleteData.query;
if(options.disabled || (query.length > maxChars)){
return false;
}
//set item selected property
options.setItemSelected(false);
//set autocomplete focused
options.setAutocompleteFocused(true);
//call the filter function with delay
setTimeout(function(){
$.when( filter.apply(options, [query, options.source]) ).done(function( results ){
//do the trimming
var trimmed_results = (options.maxResults > 0 ? results.splice(0, options.maxResults) : results);
$(context).trigger('resultsReady', [trimmed_results]);
});
}, options.delay);
}
};
$.event.special.resultsReady = {
setup: function(){ return false },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
//event specific data
var results = ev.smartAutocompleteData.results;
//exit if smart complete is disabled
if(options.disabled)
return false;
//clear all previous results
$(context).smartAutoComplete().clearResults();
//save the raw results
options.rawResults = results;
//fire the no match event and exit if no matching results
if(results.length < 1){
$(context).trigger('noResults');
return false
}
//call the results formatter function
var formatted_results = $.map(results, function(result){
return options.resultFormatter.apply(options, [result]);
});
var formatted_results_html = formatted_results.join("");
//append the results to the container
if(options.resultsContainer)
$(options.resultsContainer).append(formatted_results_html);
//trigger results ready event
$(context).trigger('showResults', [results]);
}
};
$.event.special.showResults = {
setup: function(){ return false },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
var results_container = $(options.resultsContainer);
//event specific data
var raw_results = ev.smartAutocompleteData.results;
//type ahead
if(options.typeAhead && (raw_results[0].substr(0, $(context).val().length) == $(context).val()) ){
var suggestion = raw_results[0]; //options.typeAheadExtractor($(context).val(), raw_results[0]);
//add new typeAhead field
$(context).before("");
$(context).css({
position: "relative",
zIndex: 2,
background: 'transparent'
});
var typeAheadField = $(context).prev("input");
typeAheadField.css({
position: "absolute",
zIndex: 1,
overflow: 'hidden',
background: $(context).css("background"),
borderColor: 'transparent',
width: $(context).width(),
color: 'silver'
});
//trigger item over for first item
options.currentSelection = 0;
if(results_container)
$(context).trigger('itemFocus', results_container.children()[options.currentSelection]);
}
//show the results container after aligning it with the field
if(results_container){
if(options.alignResultsContainer){
results_container.css({
position: "absolute",
top: function(){ return $(context).offset().top + $(context).height(); },
left: function(){ return $(context).offset().left; },
width: function(){ return $(context).width(); },
zIndex: 1000
})
}
results_container.show();
}
}
};
$.event.special.noResults = {
setup: function(){ return false },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
var result_container = $(options.resultsContainer);
if(result_container){
//clear previous results
options.clearResults();
}
}
};
$.event.special.itemSelect = {
setup: function(){ return false },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
//event specific data
var selected_item = ev.smartAutocompleteData.item;
//get the text from selected item
var selected_value = $(selected_item).text() || $(selected_item).val();
//set it as the value of the autocomplete field
$(context).val(selected_value);
//set item selected property
options.setItemSelected(true);
//set number of current chars in field
options.originalCharCount = $(context).val().length;
//trigger lost focus
$(context).trigger('lostFocus');
}
};
$.event.special.itemFocus = {
setup: function(){ return false },
_default: function(ev){
//event specific data
var item = ev.smartAutocompleteData.item;
$(item).addClass("smart_autocomplete_highlight");
}
};
$.event.special.itemUnfocus = {
setup: function(){ return false },
_default: function(ev){
//event specific data
var item = ev.smartAutocompleteData.item;
$(item).removeClass("smart_autocomplete_highlight");
}
}
$.event.special.lostFocus = {
setup: function(){ return false },
_default: function(ev){
var context = ev.target;
var options = $(context).data("smart-autocomplete");
//if force select is selected and no item is selected, clear currently entered text
if(options.forceSelect && !options.itemSelected)
$(options.context).val("");
//unset autocomplete focused
options.setAutocompleteFocused(false);
//clear results
options.clearResults();
//hide the results container
if(options.resultsContainer)
$(options.resultsContainer).hide();
//set current selection to null
options.currentSelection = null;
}
};
var passed_options = arguments[0];
return this.each(function(i) {
//set the options
var options = $.extend(default_options, $(this).data("smart-autocomplete"), passed_options);
//set the context
options['context'] = this;
//if a result container is not defined
if($.type(options.resultsContainer) === 'undefined' ){
//define the default result container if it is already not defined
var default_container = $("
");
default_container.appendTo("body");
options.resultsContainer = default_container;
options.alignResultsContainer = true;
}
$(this).data("smart-autocomplete", options);
// bind user events
$(this).keyup(function(ev){
//get the options
var options = $(this).data("smart-autocomplete");
//up arrow
if(ev.keyCode == '38'){
if(options.resultsContainer){
var current_selection = options.currentSelection || 0;
var result_suggestions = $(options.resultsContainer).children();
if(current_selection >= 0)
$(options.context).trigger('itemUnfocus', result_suggestions[current_selection] );
if(--current_selection <= 0)
current_selection = 0;
options['currentSelection'] = current_selection;
$(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] );
}
}
//down arrow
else if(ev.keyCode == '40'){
if(options.resultsContainer && options.resultsContainer.is(':visible')){
var current_selection = options.currentSelection;
var result_suggestions = $(options.resultsContainer).children();
if(current_selection >= 0)
$(options.context).trigger('itemUnfocus', result_suggestions[current_selection] );
if(isNaN(current_selection) || null == current_selection || (++current_selection >= result_suggestions.length) )
current_selection = 0;
options['currentSelection'] = current_selection;
$(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] );
}
//trigger keyIn event on down key
else {
$(options.context).trigger('keyIn', [$(this).val()]);
}
}
//right arrow & enter key
else if(ev.keyCode == '39' || ev.keyCode == '13'){
var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field');
if(options.resultsContainer && $(options.resultsContainer).is(':visible')){
var current_selection = options.currentSelection;
var result_suggestions = $(options.resultsContainer).children();
$(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] );
}
else if(options.typeAhead && type_ahead_field.is(':visible'))
$(options.context).trigger('itemSelect', [ type_ahead_field ] );
return false;
}
else {
var current_char_count = $(options.context).val().length;
//check whether the string has modified
if(options.originalCharCount == current_char_count)
return;
//check minimum and maximum number of characters are typed
if(current_char_count >= options.minCharLimit){
$(options.context).trigger('keyIn', [$(this).val()]);
}
else{
if(options.autocompleteFocused){
options.currentSelection = null;
$(options.context).trigger('lostFocus');
}
}
}
});
$(this).focus(function(){
//if the field is in a form capture the return key event
$(this).closest("form").bind("keydown.block_for_smart_autocomplete", function(ev){
var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field');
if(ev.keyCode == '13'){
if(options.resultsContainer && $(options.resultsContainer).is(':visible')){
var current_selection = options.currentSelection;
var result_suggestions = $(options.resultsContainer).children();
$(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] );
return false;
}
else if(options.typeAhead && type_ahead_field.is(':visible') ){
$(options.context).trigger('itemSelect', [ type_ahead_field ] );
return false;
}
}
});
if(options.forceSelect){
$(this).select();
}
});
//check for loosing focus on smart complete field and results container
//$(this).blur(function(ev){
$(document).bind("focusin click", function(ev){
if(options.autocompleteFocused){
var elemIsParent = $.contains(options.resultsContainer[0], ev.target);
if(ev.target == options.resultsContainer[0] || ev.target == options.context || elemIsParent) return
$(options.context).closest("form").unbind("keydown.block_for_smart_autocomplete");
$(options.context).trigger('lostFocus');
}
});
//bind events to results container
$(options.resultsContainer).delegate(options.resultElement, 'mouseenter.smart_autocomplete', function(){
var old_selection = options.currentSelection || 0;
var result_suggestions = $(options.resultsContainer).children();
options['currentSelection'] = $(this).prevAll().length;
if (old_selection != options.currentSelection) {
$(options.context).trigger('itemUnfocus', result_suggestions[old_selection]);
}
$(options.context).trigger('itemFocus', [this] );
});
$(options.resultsContainer).delegate(options.resultElement, 'mouseleave.smart_autocomplete', function(){
$(options.context).trigger('itemUnfocus', [this] );
});
$(options.resultsContainer).delegate(options.resultElement, 'click.smart_autocomplete', function(){
$(options.context).trigger('itemSelect', [this]);
return false
});
//bind plugin specific events
$(this).bind({
keyIn: function(ev, query){ ev.smartAutocompleteData = {'query': query }; },
resultsReady: function(ev, results){ ev.smartAutocompleteData = {'results': results }; },
showResults: function(ev, results){ ev.smartAutocompleteData = {'results': results } },
noResults: function(){},
lostFocus: function(){},
itemSelect: function(ev, item){ ev.smartAutocompleteData = {'item': item }; },
itemFocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; },
itemUnfocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; }
});
});
}
})(jQuery);