mozilla

Revision 37902 of How to implement a custom autocomplete search component

  • Revision slug: How_to_implement_custom_autocomplete_search_component
  • Revision title: How to implement a custom autocomplete search component
  • Revision id: 37902
  • Created:
  • Creator: MarkFinkle
  • Is current revision? No
  • Comment 16 words added, 76 words removed

Revision Content

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 the autocompletesearchparam attribute or searchParam property 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 nsIAutoCompleteSearch interface.
  • 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 nsIAutoCompleteResult interface.

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
  },

  /**
   * Get the image for the result at the given index
   * The return value is expected to be an URI to the image to display
   */
  getImageAt : function (index) {
    return "";
  },

  /**
   * 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 nativeJSON = Components.classes["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
      var searchResults = nativeJSON.decode(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; }

  

Revision Source

<p>The XUL <a href="/en/XUL/textbox_(Firefox_autocomplete)" title="en/XUL/textbox_(Firefox_autocomplete)">textbox</a> 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:</p>
<ul> <li>An older mechanism that is part of the "xpfe" codebase and is used in applications such as Thunderbird and SeaMonkey.</li> <li>A newer and slightly simplified mechanism that is used in "toolkit" applications such as Firefox and XULRunner.</li>
</ul>
<p>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.</p>
<p>To use autocomplete with a XUL textbox, all you need to do is set some attributes:</p>
<pre class="eval">&lt;textbox type="autocomplete" autocompletesearch="history"/&gt;
</pre>
<p>The <code>type="autocomplete"</code> turns on the autocomplete mechanism and the <code>autocompletesearch="history"</code> sets the source for the autocomplete. There are more options and features, see <a href="/en/XUL/textbox_(Firefox_autocomplete)" title="en/XUL/textbox_(Firefox_autocomplete)">XUL:textbox_(Firefox_autocomplete)</a> for more details. The toolkit mechanism has built-in support for several autocomplete sources, including:</p>
<ul> <li><code>history</code>: Search the browser's URL history</li> <li><code>form-history</code>: Search the values that the user has entered into form fields.</li> <li><code>file</code>: Search file names (GTK builds only). The component uses the <code>autocompletesearchparam</code> attribute or <code>searchParam</code> property to allow the developer to define the default directory otherwise only paths beginning with <code>/</code> or <code>~/</code> will be autocompleted.</li>
</ul>
<p>Sometimes, you may want to build your own autocomplete source. To build a source you need the following:</p>
<ul> <li>Create an XPCOM component that implements the <code>nsIAutoCompleteSearch</code> interface.</li> <li>Make sure the contract ID of your XPCOM component follows the following form: <code>"@mozilla.org/autocomplete/search;1?name=xxx"</code>, where <code>"xxx"</code> is the name of your autocomplete source.</li> <li>Implement the list of matched strings using the <code>nsIAutoCompleteResult</code> interface.</li>
</ul>
<p>The simplest way to make an XPCOM component is to use JavaScript. <a href="/en/How_to_Build_an_XPCOM_Component_in_Javascript" title="en/How_to_Build_an_XPCOM_Component_in_Javascript">How to Build an XPCOM Component in Javascript</a> 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.</p>
<p>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 <code>autocompletesearchparam</code> attribute or <code>searchParam</code> property to allow the developer to define a set of data to use for autocompletion. Use "simple-autocomplete" with a <code>textbox</code> like this:</p>
<pre>&lt;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}]" /&gt;</pre>
<p>Note the format of the JSON. The <code>comment</code> property is optional and may be omitted from the JSON. Be sure to include it if you use the <code>showcommentcolumn="true"</code> textbox attribute.</p>
<p>Copy the following code into a .js file and save it to whatever "components" folder is appropriate.</p>
<pre>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
  },

  /**
   * Get the image for the result at the given index
   * The return value is expected to be an URI to the image to display
   */
  getImageAt : function (index) {
    return "";
  },

  /**
   * 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) &amp;&amp; !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 &lt;textbox&gt; 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 &gt; 0) {
      var nativeJSON = Components.classes["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
      var searchResults = nativeJSON.decode(searchParam);
      var results = [];
      var comments = [];
      for (i=0; i&lt;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) &amp;&amp; !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; }

</pre>
<p>  </p>
Revert to this revision