How to implement custom autocomplete search component
From MDC
The XUL textbox element supports an autocomplete mechanism that is used to create a textbox with a popup containing a list of possible completions for what the user has started to type. There are actually two autocomplete mechanisms:
- An older mechanism that is part of the "xpfe" codebase and is used in applications such as Thunderbird and SeaMonkey.
- A newer and slightly simplified mechanism that is used in "toolkit" applications such as Firefox and XULRunner.
This article only focuses on the newer, toolkit mechanism. Note that Thunderbird and SeaMonkey are moving to the toolkit codebase and support the newer autocomplete mechanism as of Thunderbird 2.0.0.0 or SeaMonkey 1.1.0 or later.
To use autocomplete with a XUL textbox, all you need to do is set some attributes:
<textbox type="autocomplete" autocompletesearch="history"/>
The type="autocomplete" turns on the autocomplete mechanism and the autocompletesearch="history" sets the source for the autocomplete. There are more options and features, see XUL:textbox_(Firefox_autocomplete) for more details. The toolkit mechanism has built-in support for several autocomplete sources, including:
-
history: Search the browser's URL history -
form-history: Search the values that the user has entered into form fields. -
file: Search file names (GTK builds only). The component uses theautocompletesearchparamattribute orsearchParamproperty to allow the developer to define the default directory otherwise only paths beginning with/or~/will be autocompleted.
Sometimes, you may want to build your own autocomplete source. To build a source you need the following:
- Create an XPCOM component that implements the
nsIAutoCompleteSearchinterface. - Make sure the contract ID of your XPCOM component follows the following form:
"@mozilla.org/autocomplete/search;1?name=xxx", where"xxx"is the name of your autocomplete source. - Implement the list of matched strings using the
nsIAutoCompleteResultinterface.
The simplest way to make an XPCOM component is to use JavaScript. How to Build an XPCOM Component in Javascript will step you through the process. Creating an XPCOM component in order to build a custom autocomplete source can be enough to discourage some developers. Below is an example JavaScript XPCOM component called "simple-autocomplete" that implements the necessary interfaces.
The "simple-autocomplete" autocomplete source was built to show how to create an XPCOM component from scratch, but it can also be used "as-is" for simple autocomplete lists. The component uses the autocompletesearchparam attribute or searchParam property to allow the developer to define a set of data to use for autocompletion. Use "simple-autocomplete" with a textbox like this:
<textbox id="text1" type="autocomplete" autocompletesearch="simple-autocomplete" showcommentcolumn="true"
autocompletesearchparam="[{value:'mark',comment:'cool dude'},{value:'mary',comment:'nice lady'},{value:'jimmy',comment:'very uncool guy'},{value:'jimbo',comment:null}]"/>
Note the format of the JSON. The comment property is optional and may be omitted from the JSON. Be sure to include it if you use the showcommentcolumn="true" textbox attribute.
Copy the following code into a .js file and save it to whatever "components" folder is appropriate.
const Ci = Components.interfaces;
const CLASS_ID = Components.ID("6224daa1-71a2-4d1a-ad90-01ca1c08e323");
const CLASS_NAME = "Simple AutoComplete";
const CONTRACT_ID = "@mozilla.org/autocomplete/search;1?name=simple-autocomplete";
// Implements nsIAutoCompleteResult
function SimpleAutoCompleteResult(searchString, searchResult,
defaultIndex, errorDescription,
results, comments) {
this._searchString = searchString;
this._searchResult = searchResult;
this._defaultIndex = defaultIndex;
this._errorDescription = errorDescription;
this._results = results;
this._comments = comments;
}
SimpleAutoCompleteResult.prototype = {
_searchString: "",
_searchResult: 0,
_defaultIndex: 0,
_errorDescription: "",
_results: [],
_comments: [],
/**
* The original search string
*/
get searchString() {
return this._searchString;
},
/**
* The result code of this result object, either:
* RESULT_IGNORED (invalid searchString)
* RESULT_FAILURE (failure)
* RESULT_NOMATCH (no matches found)
* RESULT_SUCCESS (matches found)
*/
get searchResult() {
return this._searchResult;
},
/**
* Index of the default item that should be entered if none is selected
*/
get defaultIndex() {
return this._defaultIndex;
},
/**
* A string describing the cause of a search failure
*/
get errorDescription() {
return this._errorDescription;
},
/**
* The number of matches
*/
get matchCount() {
return this._results.length;
},
/**
* Get the value of the result at the given index
*/
getValueAt: function(index) {
return this._results[index];
},
/**
* Get the comment of the result at the given index
*/
getCommentAt: function(index) {
return this._comments[index];
},
/**
* Get the style hint for the result at the given index
*/
getStyleAt: function(index) {
if (!this._comments[index])
return null; // not a category label, so no special styling
if (index == 0)
return "suggestfirst"; // category label on first line of results
return "suggesthint"; // category label on any other line of results
},
/**
* Remove the value at the given index from the autocomplete results.
* If removeFromDb is set to true, the value should be removed from
* persistent storage as well.
*/
removeValueAt: function(index, removeFromDb) {
this._results.splice(index, 1);
this._comments.splice(index, 1);
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteResult) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
// Implements nsIAutoCompleteSearch
function SimpleAutoCompleteSearch() {
}
SimpleAutoCompleteSearch.prototype = {
/*
* Search for a given string and notify a listener (either synchronously
* or asynchronously) of the result
*
* @param searchString - The string to search for
* @param searchParam - An extra parameter
* @param previousResult - A previous result to use for faster searchinig
* @param listener - A listener to notify when the search is complete
*/
startSearch: function(searchString, searchParam, result, listener) {
// This autocomplete source assumes the developer attached a JSON string
// to the the "autocompletesearchparam" attribute or "searchParam" property
// of the <textbox> element. The JSON is converted into an array and used
// as the source of match data. Any values that match the search string
// are moved into temporary arrays and passed to the AutoCompleteResult
if (searchParam.length > 0) {
var searchResults = eval("(" + searchParam + ")");
var results = [];
var comments = [];
for (i=0; i<searchResults.length; i++) {
if (searchResults[i].value.indexOf(searchString) == 0) {
results.push(searchResults[i].value);
if (searchResults[i].comment)
comments.push(searchResults[i].comment);
else
comments.push(null);
}
}
var newResult = new SimpleAutoCompleteResult(searchString, Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, comments);
listener.onSearchResult(this, newResult);
}
},
/*
* Stop an asynchronous search that is in progress
*/
stopSearch: function() {
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteSearch) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
// Factory
var SimpleAutoCompleteSearchFactory = {
singleton: null,
createInstance: function (aOuter, aIID) {
if (aOuter != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (this.singleton == null)
this.singleton = new SimpleAutoCompleteSearch();
return this.singleton.QueryInterface(aIID);
}
};
// Module
var SimpleAutoCompleteSearchModule = {
registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
},
unregisterSelf: function(aCompMgr, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
},
getClassObject: function(aCompMgr, aCID, aIID) {
if (!aIID.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
if (aCID.equals(CLASS_ID))
return SimpleAutoCompleteSearchFactory;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
canUnload: function(aCompMgr) { return true; }
};
// Module initialization
function NSGetModule(aCompMgr, aFileSpec) { return SimpleAutoCompleteSearchModule; }