summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/nsTaggingService.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/places/nsTaggingService.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/places/nsTaggingService.js')
-rw-r--r--toolkit/components/places/nsTaggingService.js709
1 files changed, 709 insertions, 0 deletions
diff --git a/toolkit/components/places/nsTaggingService.js b/toolkit/components/places/nsTaggingService.js
new file mode 100644
index 000000000..1fad67a82
--- /dev/null
+++ b/toolkit/components/places/nsTaggingService.js
@@ -0,0 +1,709 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const TOPIC_SHUTDOWN = "places-shutdown";
+
+/**
+ * The Places Tagging Service
+ */
+function TaggingService() {
+ // Observe bookmarks changes.
+ PlacesUtils.bookmarks.addObserver(this, false);
+
+ // Cleanup on shutdown.
+ Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
+}
+
+TaggingService.prototype = {
+ /**
+ * Creates a tag container under the tags-root with the given name.
+ *
+ * @param aTagName
+ * the name for the new tag.
+ * @param aSource
+ * a change source constant from nsINavBookmarksService::SOURCE_*.
+ * @returns the id of the new tag container.
+ */
+ _createTag: function TS__createTag(aTagName, aSource) {
+ var newFolderId = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aGuid */ null, aSource
+ );
+ // Add the folder to our local cache, so we can avoid doing this in the
+ // observer that would have to check itemType.
+ this._tagFolders[newFolderId] = aTagName;
+
+ return newFolderId;
+ },
+
+ /**
+ * Checks whether the given uri is tagged with the given tag.
+ *
+ * @param [in] aURI
+ * url to check for
+ * @param [in] aTagName
+ * the tag to check for
+ * @returns the item id if the URI is tagged with the given tag, -1
+ * otherwise.
+ */
+ _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
+ var tagId = this._getItemIdForTag(aTagName);
+ if (tagId == -1)
+ return -1;
+ // Using bookmarks service API for this would be a pain.
+ // Until tags implementation becomes sane, go the query way.
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT id FROM moz_bookmarks
+ WHERE parent = :tag_id
+ AND fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url)`
+ );
+ stmt.params.tag_id = tagId;
+ stmt.params.page_url = aURI.spec;
+ try {
+ if (stmt.executeStep()) {
+ return stmt.row.id;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+ return -1;
+ },
+
+ /**
+ * Returns the folder id for a tag, or -1 if not found.
+ * @param [in] aTag
+ * string tag to search for
+ * @returns integer id for the bookmark folder for the tag
+ */
+ _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
+ for (var i in this._tagFolders) {
+ if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
+ return parseInt(i);
+ }
+ return -1;
+ },
+
+ /**
+ * Makes a proper array of tag objects like { id: number, name: string }.
+ *
+ * @param aTags
+ * Array of tags. Entries can be tag names or concrete item id.
+ * @param trim [optional]
+ * Whether to trim passed-in named tags. Defaults to false.
+ * @return Array of tag objects like { id: number, name: string }.
+ *
+ * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
+ * a valid tag.
+ */
+ _convertInputMixedTagsArray(aTags, trim=false) {
+ // Handle sparse array with a .filter.
+ return aTags.filter(tag => tag !== undefined)
+ .map(idOrName => {
+ let tag = {};
+ if (typeof(idOrName) == "number" && this._tagFolders[idOrName]) {
+ // This is a tag folder id.
+ tag.id = idOrName;
+ // We can't know the name at this point, since a previous tag could
+ // want to change it.
+ tag.__defineGetter__("name", () => this._tagFolders[tag.id]);
+ }
+ else if (typeof(idOrName) == "string" && idOrName.length > 0 &&
+ idOrName.length <= Ci.nsITaggingService.MAX_TAG_LENGTH) {
+ // This is a tag name.
+ tag.name = trim ? idOrName.trim() : idOrName;
+ // We can't know the id at this point, since a previous tag could
+ // have created it.
+ tag.__defineGetter__("id", () => this._getItemIdForTag(tag.name));
+ }
+ else {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ return tag;
+ });
+ },
+
+ // nsITaggingService
+ tagURI: function TS_tagURI(aURI, aTags, aSource)
+ {
+ if (!aURI || !aTags || !Array.isArray(aTags)) {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+
+ // This also does some input validation.
+ let tags = this._convertInputMixedTagsArray(aTags, true);
+
+ let taggingFunction = () => {
+ for (let tag of tags) {
+ if (tag.id == -1) {
+ // Tag does not exist yet, create it.
+ this._createTag(tag.name, aSource);
+ }
+
+ if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
+ // The provided URI is not yet tagged, add a tag for it.
+ // Note that bookmarks under tag containers must have null titles.
+ PlacesUtils.bookmarks.insertBookmark(
+ tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aTitle */ null, /* aGuid */ null, aSource
+ );
+ }
+
+ // Try to preserve user's tag name casing.
+ // Rename the tag container so the Places view matches the most-recent
+ // user-typed value.
+ if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
+ // this._tagFolders is updated by the bookmarks observer.
+ PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name, aSource);
+ }
+ }
+ };
+
+ // Use a batch only if creating more than 2 tags.
+ if (tags.length < 3) {
+ taggingFunction();
+ } else {
+ PlacesUtils.bookmarks.runInBatchMode(taggingFunction, null);
+ }
+ },
+
+ /**
+ * Removes the tag container from the tags root if the given tag is empty.
+ *
+ * @param aTagId
+ * the itemId of the tag element under the tags root
+ * @param aSource
+ * a change source constant from nsINavBookmarksService::SOURCE_*
+ */
+ _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId, aSource) {
+ let count = 0;
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT count(*) AS count FROM moz_bookmarks
+ WHERE parent = :tag_id`
+ );
+ stmt.params.tag_id = aTagId;
+ try {
+ if (stmt.executeStep()) {
+ count = stmt.row.count;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ if (count == 0) {
+ PlacesUtils.bookmarks.removeItem(aTagId, aSource);
+ }
+ },
+
+ // nsITaggingService
+ untagURI: function TS_untagURI(aURI, aTags, aSource)
+ {
+ if (!aURI || (aTags && !Array.isArray(aTags))) {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aTags) {
+ // Passing null should clear all tags for aURI, see the IDL.
+ // XXXmano: write a perf-sensitive version of this code path...
+ aTags = this.getTagsForURI(aURI);
+ }
+
+ // This also does some input validation.
+ let tags = this._convertInputMixedTagsArray(aTags);
+
+ let isAnyTagNotTrimmed = tags.some(tag => /^\s|\s$/.test(tag.name));
+ if (isAnyTagNotTrimmed) {
+ Deprecated.warning("At least one tag passed to untagURI was not trimmed",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
+ }
+
+ let untaggingFunction = () => {
+ for (let tag of tags) {
+ if (tag.id != -1) {
+ // A tag could exist.
+ let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
+ if (itemId != -1) {
+ // There is a tagged item.
+ PlacesUtils.bookmarks.removeItem(itemId, aSource);
+ }
+ }
+ }
+ };
+
+ // Use a batch only if creating more than 2 tags.
+ if (tags.length < 3) {
+ untaggingFunction();
+ } else {
+ PlacesUtils.bookmarks.runInBatchMode(untaggingFunction, null);
+ }
+ },
+
+ // nsITaggingService
+ getURIsForTag: function TS_getURIsForTag(aTagName) {
+ if (!aTagName || aTagName.length == 0)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ if (/^\s|\s$/.test(aTagName)) {
+ Deprecated.warning("Tag passed to getURIsForTag was not trimmed",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
+ }
+
+ let uris = [];
+ let tagId = this._getItemIdForTag(aTagName);
+ if (tagId == -1)
+ return uris;
+
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT h.url FROM moz_places h
+ JOIN moz_bookmarks b ON b.fk = h.id
+ WHERE b.parent = :tag_id`
+ );
+ stmt.params.tag_id = tagId;
+ try {
+ while (stmt.executeStep()) {
+ try {
+ uris.push(Services.io.newURI(stmt.row.url, null, null));
+ } catch (ex) {}
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ return uris;
+ },
+
+ // nsITaggingService
+ getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
+ if (!aURI)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ var tags = [];
+ var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI);
+ for (var i=0; i < bookmarkIds.length; i++) {
+ var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]);
+ if (this._tagFolders[folderId])
+ tags.push(this._tagFolders[folderId]);
+ }
+
+ // sort the tag list
+ tags.sort(function(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ });
+ if (aCount)
+ aCount.value = tags.length;
+ return tags;
+ },
+
+ __tagFolders: null,
+ get _tagFolders() {
+ if (!this.__tagFolders) {
+ this.__tagFolders = [];
+
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root "
+ );
+ stmt.params.tags_root = PlacesUtils.tagsFolderId;
+ try {
+ while (stmt.executeStep()) {
+ this.__tagFolders[stmt.row.id] = stmt.row.title;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+ }
+
+ return this.__tagFolders;
+ },
+
+ // nsITaggingService
+ get allTags() {
+ var allTags = [];
+ for (var i in this._tagFolders)
+ allTags.push(this._tagFolders[i]);
+ // sort the tag list
+ allTags.sort(function(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ });
+ return allTags;
+ },
+
+ // nsITaggingService
+ get hasTags() {
+ return this._tagFolders.length > 0;
+ },
+
+ // nsIObserver
+ observe: function TS_observe(aSubject, aTopic, aData) {
+ if (aTopic == TOPIC_SHUTDOWN) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
+ }
+ },
+
+ /**
+ * If the only bookmark items associated with aURI are contained in tag
+ * folders, returns the IDs of those items. This can be the case if
+ * the URI was bookmarked and tagged at some point, but the bookmark was
+ * removed, leaving only the bookmark items in tag folders. If the URI is
+ * either properly bookmarked or not tagged just returns and empty array.
+ *
+ * @param aURI
+ * A URI (string) that may or may not be bookmarked
+ * @returns an array of item ids
+ */
+ _getTaggedItemIdsIfUnbookmarkedURI:
+ function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) {
+ var itemIds = [];
+ var isBookmarked = false;
+
+ // Using bookmarks service API for this would be a pain.
+ // Until tags implementation becomes sane, go the query way.
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT id, parent
+ FROM moz_bookmarks
+ WHERE fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url)`
+ );
+ stmt.params.page_url = aURI.spec;
+ try {
+ while (stmt.executeStep() && !isBookmarked) {
+ if (this._tagFolders[stmt.row.parent]) {
+ // This is a tag entry.
+ itemIds.push(stmt.row.id);
+ }
+ else {
+ // This is a real bookmark, so the bookmarked URI is not an orphan.
+ isBookmarked = true;
+ }
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ return isBookmarked ? [] : itemIds;
+ },
+
+ // nsINavBookmarkObserver
+ onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
+ aURI, aTitle) {
+ // Nothing to do if this is not a tag.
+ if (aFolderId != PlacesUtils.tagsFolderId ||
+ aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
+ return;
+
+ this._tagFolders[aItemId] = aTitle;
+ },
+
+ onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
+ aItemType, aURI, aGuid, aParentGuid,
+ aSource) {
+ // Item is a tag folder.
+ if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
+ delete this._tagFolders[aItemId];
+ }
+ // Item is a bookmark that was removed from a non-tag folder.
+ else if (aURI && !this._tagFolders[aFolderId]) {
+ // If the only bookmark items now associated with the bookmark's URI are
+ // contained in tag folders, the URI is no longer properly bookmarked, so
+ // untag it.
+ let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI);
+ for (let i = 0; i < itemIds.length; i++) {
+ try {
+ PlacesUtils.bookmarks.removeItem(itemIds[i], aSource);
+ } catch (ex) {}
+ }
+ }
+ // Item is a tag entry. If this was the last entry for this tag, remove it.
+ else if (aURI && this._tagFolders[aFolderId]) {
+ this._removeTagIfEmpty(aFolderId, aSource);
+ }
+ },
+
+ onItemChanged: function TS_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue,
+ aLastModified, aItemType) {
+ if (aProperty == "title" && this._tagFolders[aItemId])
+ this._tagFolders[aItemId] = aNewValue;
+ },
+
+ onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex, aItemType) {
+ if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent &&
+ PlacesUtils.tagsFolderId != aNewParent)
+ delete this._tagFolders[aItemId];
+ },
+
+ onItemVisited: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+
+ // nsISupports
+
+ classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsITaggingService
+ , Ci.nsINavBookmarkObserver
+ , Ci.nsIObserver
+ ])
+};
+
+
+function TagAutoCompleteResult(searchString, searchResult,
+ defaultIndex, errorDescription,
+ results, comments) {
+ this._searchString = searchString;
+ this._searchResult = searchResult;
+ this._defaultIndex = defaultIndex;
+ this._errorDescription = errorDescription;
+ this._results = results;
+ this._comments = comments;
+}
+
+TagAutoCompleteResult.prototype = {
+
+ /**
+ * 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 PTACR_getValueAt(index) {
+ return this._results[index];
+ },
+
+ getLabelAt: function PTACR_getLabelAt(index) {
+ return this.getValueAt(index);
+ },
+
+ /**
+ * Get the comment of the result at the given index
+ */
+ getCommentAt: function PTACR_getCommentAt(index) {
+ return this._comments[index];
+ },
+
+ /**
+ * Get the style hint for the result at the given index
+ */
+ getStyleAt: function PTACR_getStyleAt(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
+ */
+ getImageAt: function PTACR_getImageAt(index) {
+ return null;
+ },
+
+ /**
+ * Get the image for the result at the given index
+ */
+ getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) {
+ return this.getValueAt(index);
+ },
+
+ /**
+ * 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 PTACR_removeValueAt(index, removeFromDb) {
+ this._results.splice(index, 1);
+ this._comments.splice(index, 1);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteResult
+ ])
+};
+
+// Implements nsIAutoCompleteSearch
+function TagAutoCompleteSearch() {
+ XPCOMUtils.defineLazyServiceGetter(this, "tagging",
+ "@mozilla.org/browser/tagging-service;1",
+ "nsITaggingService");
+}
+
+TagAutoCompleteSearch.prototype = {
+ _stopped : false,
+
+ /*
+ * 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 searching
+ * @param listener - A listener to notify when the search is complete
+ */
+ startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
+ var searchResults = this.tagging.allTags;
+ var results = [];
+ var comments = [];
+ this._stopped = false;
+
+ // only search on characters for the last tag
+ var index = Math.max(searchString.lastIndexOf(","),
+ searchString.lastIndexOf(";"));
+ var before = '';
+ if (index != -1) {
+ before = searchString.slice(0, index+1);
+ searchString = searchString.slice(index+1);
+ // skip past whitespace
+ var m = searchString.match(/\s+/);
+ if (m) {
+ before += m[0];
+ searchString = searchString.slice(m[0].length);
+ }
+ }
+
+ if (!searchString.length) {
+ var newResult = new TagAutoCompleteResult(searchString,
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
+ listener.onSearchResult(self, newResult);
+ return;
+ }
+
+ var self = this;
+ // generator: if yields true, not done
+ function* doSearch() {
+ var i = 0;
+ while (i < searchResults.length) {
+ if (self._stopped)
+ yield false;
+ // for each match, prepend what the user has typed so far
+ if (searchResults[i].toLowerCase()
+ .indexOf(searchString.toLowerCase()) == 0 &&
+ !comments.includes(searchResults[i])) {
+ results.push(before + searchResults[i]);
+ comments.push(searchResults[i]);
+ }
+
+ ++i;
+
+ /* TODO: bug 481451
+ * For each yield we pass a new result to the autocomplete
+ * listener. The listener appends instead of replacing previous results,
+ * causing invalid matchCount values.
+ *
+ * As a workaround, all tags are searched through in a single batch,
+ * making this synchronous until the above issue is fixed.
+ */
+
+ /*
+ // 100 loops per yield
+ if ((i % 100) == 0) {
+ var newResult = new TagAutoCompleteResult(searchString,
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
+ listener.onSearchResult(self, newResult);
+ yield true;
+ }
+ */
+ }
+
+ let searchResult = results.length > 0 ?
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS :
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
+ var newResult = new TagAutoCompleteResult(searchString, searchResult, 0,
+ "", results, comments);
+ listener.onSearchResult(self, newResult);
+ yield false;
+ }
+
+ // chunk the search results via the generator
+ var gen = doSearch();
+ while (gen.next().value);
+ },
+
+ /**
+ * Stop an asynchronous search that is in progress
+ */
+ stopSearch: function PTACS_stopSearch() {
+ this._stopped = true;
+ },
+
+ // nsISupports
+
+ classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch
+ ])
+};
+
+var component = [TaggingService, TagAutoCompleteSearch];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);