toolkit/components/places/nsTaggingService.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 const Cc = Components.classes;
michael@0 7 const Ci = Components.interfaces;
michael@0 8 const Cr = Components.results;
michael@0 9
michael@0 10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 11 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 12 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
michael@0 13
michael@0 14 const TOPIC_SHUTDOWN = "places-shutdown";
michael@0 15
michael@0 16 /**
michael@0 17 * The Places Tagging Service
michael@0 18 */
michael@0 19 function TaggingService() {
michael@0 20 // Observe bookmarks changes.
michael@0 21 PlacesUtils.bookmarks.addObserver(this, false);
michael@0 22
michael@0 23 // Cleanup on shutdown.
michael@0 24 Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
michael@0 25 }
michael@0 26
michael@0 27 TaggingService.prototype = {
michael@0 28 /**
michael@0 29 * Creates a tag container under the tags-root with the given name.
michael@0 30 *
michael@0 31 * @param aTagName
michael@0 32 * the name for the new tag.
michael@0 33 * @returns the id of the new tag container.
michael@0 34 */
michael@0 35 _createTag: function TS__createTag(aTagName) {
michael@0 36 var newFolderId = PlacesUtils.bookmarks.createFolder(
michael@0 37 PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX
michael@0 38 );
michael@0 39 // Add the folder to our local cache, so we can avoid doing this in the
michael@0 40 // observer that would have to check itemType.
michael@0 41 this._tagFolders[newFolderId] = aTagName;
michael@0 42
michael@0 43 return newFolderId;
michael@0 44 },
michael@0 45
michael@0 46 /**
michael@0 47 * Checks whether the given uri is tagged with the given tag.
michael@0 48 *
michael@0 49 * @param [in] aURI
michael@0 50 * url to check for
michael@0 51 * @param [in] aTagName
michael@0 52 * the tag to check for
michael@0 53 * @returns the item id if the URI is tagged with the given tag, -1
michael@0 54 * otherwise.
michael@0 55 */
michael@0 56 _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
michael@0 57 var tagId = this._getItemIdForTag(aTagName);
michael@0 58 if (tagId == -1)
michael@0 59 return -1;
michael@0 60 // Using bookmarks service API for this would be a pain.
michael@0 61 // Until tags implementation becomes sane, go the query way.
michael@0 62 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
michael@0 63 .DBConnection;
michael@0 64 let stmt = db.createStatement(
michael@0 65 "SELECT id FROM moz_bookmarks "
michael@0 66 + "WHERE parent = :tag_id "
michael@0 67 + "AND fk = (SELECT id FROM moz_places WHERE url = :page_url)"
michael@0 68 );
michael@0 69 stmt.params.tag_id = tagId;
michael@0 70 stmt.params.page_url = aURI.spec;
michael@0 71 try {
michael@0 72 if (stmt.executeStep()) {
michael@0 73 return stmt.row.id;
michael@0 74 }
michael@0 75 }
michael@0 76 finally {
michael@0 77 stmt.finalize();
michael@0 78 }
michael@0 79 return -1;
michael@0 80 },
michael@0 81
michael@0 82 /**
michael@0 83 * Returns the folder id for a tag, or -1 if not found.
michael@0 84 * @param [in] aTag
michael@0 85 * string tag to search for
michael@0 86 * @returns integer id for the bookmark folder for the tag
michael@0 87 */
michael@0 88 _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
michael@0 89 for (var i in this._tagFolders) {
michael@0 90 if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
michael@0 91 return parseInt(i);
michael@0 92 }
michael@0 93 return -1;
michael@0 94 },
michael@0 95
michael@0 96 /**
michael@0 97 * Makes a proper array of tag objects like { id: number, name: string }.
michael@0 98 *
michael@0 99 * @param aTags
michael@0 100 * Array of tags. Entries can be tag names or concrete item id.
michael@0 101 * @return Array of tag objects like { id: number, name: string }.
michael@0 102 *
michael@0 103 * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
michael@0 104 * a valid tag.
michael@0 105 */
michael@0 106 _convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags)
michael@0 107 {
michael@0 108 return aTags.map(function (val)
michael@0 109 {
michael@0 110 let tag = { _self: this };
michael@0 111 if (typeof(val) == "number" && this._tagFolders[val]) {
michael@0 112 // This is a tag folder id.
michael@0 113 tag.id = val;
michael@0 114 // We can't know the name at this point, since a previous tag could
michael@0 115 // want to change it.
michael@0 116 tag.__defineGetter__("name", function () this._self._tagFolders[this.id]);
michael@0 117 }
michael@0 118 else if (typeof(val) == "string" && val.length > 0) {
michael@0 119 // This is a tag name.
michael@0 120 tag.name = val;
michael@0 121 // We can't know the id at this point, since a previous tag could
michael@0 122 // have created it.
michael@0 123 tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name));
michael@0 124 }
michael@0 125 else {
michael@0 126 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 127 }
michael@0 128 return tag;
michael@0 129 }, this);
michael@0 130 },
michael@0 131
michael@0 132 // nsITaggingService
michael@0 133 tagURI: function TS_tagURI(aURI, aTags)
michael@0 134 {
michael@0 135 if (!aURI || !aTags || !Array.isArray(aTags)) {
michael@0 136 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 137 }
michael@0 138
michael@0 139 // This also does some input validation.
michael@0 140 let tags = this._convertInputMixedTagsArray(aTags);
michael@0 141
michael@0 142 let taggingService = this;
michael@0 143 PlacesUtils.bookmarks.runInBatchMode({
michael@0 144 runBatched: function (aUserData)
michael@0 145 {
michael@0 146 tags.forEach(function (tag)
michael@0 147 {
michael@0 148 if (tag.id == -1) {
michael@0 149 // Tag does not exist yet, create it.
michael@0 150 this._createTag(tag.name);
michael@0 151 }
michael@0 152
michael@0 153 if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
michael@0 154 // The provided URI is not yet tagged, add a tag for it.
michael@0 155 // Note that bookmarks under tag containers must have null titles.
michael@0 156 PlacesUtils.bookmarks.insertBookmark(
michael@0 157 tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
michael@0 158 );
michael@0 159 }
michael@0 160
michael@0 161 // Try to preserve user's tag name casing.
michael@0 162 // Rename the tag container so the Places view matches the most-recent
michael@0 163 // user-typed value.
michael@0 164 if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
michael@0 165 // this._tagFolders is updated by the bookmarks observer.
michael@0 166 PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name);
michael@0 167 }
michael@0 168 }, taggingService);
michael@0 169 }
michael@0 170 }, null);
michael@0 171 },
michael@0 172
michael@0 173 /**
michael@0 174 * Removes the tag container from the tags root if the given tag is empty.
michael@0 175 *
michael@0 176 * @param aTagId
michael@0 177 * the itemId of the tag element under the tags root
michael@0 178 */
michael@0 179 _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) {
michael@0 180 let count = 0;
michael@0 181 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
michael@0 182 .DBConnection;
michael@0 183 let stmt = db.createStatement(
michael@0 184 "SELECT count(*) AS count FROM moz_bookmarks "
michael@0 185 + "WHERE parent = :tag_id"
michael@0 186 );
michael@0 187 stmt.params.tag_id = aTagId;
michael@0 188 try {
michael@0 189 if (stmt.executeStep()) {
michael@0 190 count = stmt.row.count;
michael@0 191 }
michael@0 192 }
michael@0 193 finally {
michael@0 194 stmt.finalize();
michael@0 195 }
michael@0 196
michael@0 197 if (count == 0) {
michael@0 198 PlacesUtils.bookmarks.removeItem(aTagId);
michael@0 199 }
michael@0 200 },
michael@0 201
michael@0 202 // nsITaggingService
michael@0 203 untagURI: function TS_untagURI(aURI, aTags)
michael@0 204 {
michael@0 205 if (!aURI || (aTags && !Array.isArray(aTags))) {
michael@0 206 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 207 }
michael@0 208
michael@0 209 if (!aTags) {
michael@0 210 // Passing null should clear all tags for aURI, see the IDL.
michael@0 211 // XXXmano: write a perf-sensitive version of this code path...
michael@0 212 aTags = this.getTagsForURI(aURI);
michael@0 213 }
michael@0 214
michael@0 215 // This also does some input validation.
michael@0 216 let tags = this._convertInputMixedTagsArray(aTags);
michael@0 217
michael@0 218 let taggingService = this;
michael@0 219 PlacesUtils.bookmarks.runInBatchMode({
michael@0 220 runBatched: function (aUserData)
michael@0 221 {
michael@0 222 tags.forEach(function (tag)
michael@0 223 {
michael@0 224 if (tag.id != -1) {
michael@0 225 // A tag could exist.
michael@0 226 let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
michael@0 227 if (itemId != -1) {
michael@0 228 // There is a tagged item.
michael@0 229 PlacesUtils.bookmarks.removeItem(itemId);
michael@0 230 }
michael@0 231 }
michael@0 232 }, taggingService);
michael@0 233 }
michael@0 234 }, null);
michael@0 235 },
michael@0 236
michael@0 237 // nsITaggingService
michael@0 238 getURIsForTag: function TS_getURIsForTag(aTagName) {
michael@0 239 if (!aTagName || aTagName.length == 0)
michael@0 240 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 241
michael@0 242 let uris = [];
michael@0 243 let tagId = this._getItemIdForTag(aTagName);
michael@0 244 if (tagId == -1)
michael@0 245 return uris;
michael@0 246
michael@0 247 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
michael@0 248 .DBConnection;
michael@0 249 let stmt = db.createStatement(
michael@0 250 "SELECT h.url FROM moz_places h "
michael@0 251 + "JOIN moz_bookmarks b ON b.fk = h.id "
michael@0 252 + "WHERE b.parent = :tag_id "
michael@0 253 );
michael@0 254 stmt.params.tag_id = tagId;
michael@0 255 try {
michael@0 256 while (stmt.executeStep()) {
michael@0 257 try {
michael@0 258 uris.push(Services.io.newURI(stmt.row.url, null, null));
michael@0 259 } catch (ex) {}
michael@0 260 }
michael@0 261 }
michael@0 262 finally {
michael@0 263 stmt.finalize();
michael@0 264 }
michael@0 265
michael@0 266 return uris;
michael@0 267 },
michael@0 268
michael@0 269 // nsITaggingService
michael@0 270 getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
michael@0 271 if (!aURI)
michael@0 272 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 273
michael@0 274 var tags = [];
michael@0 275 var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI);
michael@0 276 for (var i=0; i < bookmarkIds.length; i++) {
michael@0 277 var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]);
michael@0 278 if (this._tagFolders[folderId])
michael@0 279 tags.push(this._tagFolders[folderId]);
michael@0 280 }
michael@0 281
michael@0 282 // sort the tag list
michael@0 283 tags.sort(function(a, b) {
michael@0 284 return a.toLowerCase().localeCompare(b.toLowerCase());
michael@0 285 });
michael@0 286 if (aCount)
michael@0 287 aCount.value = tags.length;
michael@0 288 return tags;
michael@0 289 },
michael@0 290
michael@0 291 __tagFolders: null,
michael@0 292 get _tagFolders() {
michael@0 293 if (!this.__tagFolders) {
michael@0 294 this.__tagFolders = [];
michael@0 295
michael@0 296 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
michael@0 297 .DBConnection;
michael@0 298 let stmt = db.createStatement(
michael@0 299 "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root "
michael@0 300 );
michael@0 301 stmt.params.tags_root = PlacesUtils.tagsFolderId;
michael@0 302 try {
michael@0 303 while (stmt.executeStep()) {
michael@0 304 this.__tagFolders[stmt.row.id] = stmt.row.title;
michael@0 305 }
michael@0 306 }
michael@0 307 finally {
michael@0 308 stmt.finalize();
michael@0 309 }
michael@0 310 }
michael@0 311
michael@0 312 return this.__tagFolders;
michael@0 313 },
michael@0 314
michael@0 315 // nsITaggingService
michael@0 316 get allTags() {
michael@0 317 var allTags = [];
michael@0 318 for (var i in this._tagFolders)
michael@0 319 allTags.push(this._tagFolders[i]);
michael@0 320 // sort the tag list
michael@0 321 allTags.sort(function(a, b) {
michael@0 322 return a.toLowerCase().localeCompare(b.toLowerCase());
michael@0 323 });
michael@0 324 return allTags;
michael@0 325 },
michael@0 326
michael@0 327 // nsITaggingService
michael@0 328 get hasTags() {
michael@0 329 return this._tagFolders.length > 0;
michael@0 330 },
michael@0 331
michael@0 332 // nsIObserver
michael@0 333 observe: function TS_observe(aSubject, aTopic, aData) {
michael@0 334 if (aTopic == TOPIC_SHUTDOWN) {
michael@0 335 PlacesUtils.bookmarks.removeObserver(this);
michael@0 336 Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
michael@0 337 }
michael@0 338 },
michael@0 339
michael@0 340 /**
michael@0 341 * If the only bookmark items associated with aURI are contained in tag
michael@0 342 * folders, returns the IDs of those items. This can be the case if
michael@0 343 * the URI was bookmarked and tagged at some point, but the bookmark was
michael@0 344 * removed, leaving only the bookmark items in tag folders. If the URI is
michael@0 345 * either properly bookmarked or not tagged just returns and empty array.
michael@0 346 *
michael@0 347 * @param aURI
michael@0 348 * A URI (string) that may or may not be bookmarked
michael@0 349 * @returns an array of item ids
michael@0 350 */
michael@0 351 _getTaggedItemIdsIfUnbookmarkedURI:
michael@0 352 function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) {
michael@0 353 var itemIds = [];
michael@0 354 var isBookmarked = false;
michael@0 355
michael@0 356 // Using bookmarks service API for this would be a pain.
michael@0 357 // Until tags implementation becomes sane, go the query way.
michael@0 358 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
michael@0 359 .DBConnection;
michael@0 360 let stmt = db.createStatement(
michael@0 361 "SELECT id, parent "
michael@0 362 + "FROM moz_bookmarks "
michael@0 363 + "WHERE fk = (SELECT id FROM moz_places WHERE url = :page_url)"
michael@0 364 );
michael@0 365 stmt.params.page_url = aURI.spec;
michael@0 366 try {
michael@0 367 while (stmt.executeStep() && !isBookmarked) {
michael@0 368 if (this._tagFolders[stmt.row.parent]) {
michael@0 369 // This is a tag entry.
michael@0 370 itemIds.push(stmt.row.id);
michael@0 371 }
michael@0 372 else {
michael@0 373 // This is a real bookmark, so the bookmarked URI is not an orphan.
michael@0 374 isBookmarked = true;
michael@0 375 }
michael@0 376 }
michael@0 377 }
michael@0 378 finally {
michael@0 379 stmt.finalize();
michael@0 380 }
michael@0 381
michael@0 382 return isBookmarked ? [] : itemIds;
michael@0 383 },
michael@0 384
michael@0 385 // nsINavBookmarkObserver
michael@0 386 onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
michael@0 387 aURI, aTitle) {
michael@0 388 // Nothing to do if this is not a tag.
michael@0 389 if (aFolderId != PlacesUtils.tagsFolderId ||
michael@0 390 aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
michael@0 391 return;
michael@0 392
michael@0 393 this._tagFolders[aItemId] = aTitle;
michael@0 394 },
michael@0 395
michael@0 396 onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
michael@0 397 aItemType, aURI) {
michael@0 398 // Item is a tag folder.
michael@0 399 if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
michael@0 400 delete this._tagFolders[aItemId];
michael@0 401 }
michael@0 402 // Item is a bookmark that was removed from a non-tag folder.
michael@0 403 else if (aURI && !this._tagFolders[aFolderId]) {
michael@0 404 // If the only bookmark items now associated with the bookmark's URI are
michael@0 405 // contained in tag folders, the URI is no longer properly bookmarked, so
michael@0 406 // untag it.
michael@0 407 let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI);
michael@0 408 for (let i = 0; i < itemIds.length; i++) {
michael@0 409 try {
michael@0 410 PlacesUtils.bookmarks.removeItem(itemIds[i]);
michael@0 411 } catch (ex) {}
michael@0 412 }
michael@0 413 }
michael@0 414 // Item is a tag entry. If this was the last entry for this tag, remove it.
michael@0 415 else if (aURI && this._tagFolders[aFolderId]) {
michael@0 416 this._removeTagIfEmpty(aFolderId);
michael@0 417 }
michael@0 418 },
michael@0 419
michael@0 420 onItemChanged: function TS_onItemChanged(aItemId, aProperty,
michael@0 421 aIsAnnotationProperty, aNewValue,
michael@0 422 aLastModified, aItemType) {
michael@0 423 if (aProperty == "title" && this._tagFolders[aItemId])
michael@0 424 this._tagFolders[aItemId] = aNewValue;
michael@0 425 },
michael@0 426
michael@0 427 onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex,
michael@0 428 aNewParent, aNewIndex, aItemType) {
michael@0 429 if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent &&
michael@0 430 PlacesUtils.tagsFolderId != aNewParent)
michael@0 431 delete this._tagFolders[aItemId];
michael@0 432 },
michael@0 433
michael@0 434 onItemVisited: function () {},
michael@0 435 onBeginUpdateBatch: function () {},
michael@0 436 onEndUpdateBatch: function () {},
michael@0 437
michael@0 438 //////////////////////////////////////////////////////////////////////////////
michael@0 439 //// nsISupports
michael@0 440
michael@0 441 classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
michael@0 442
michael@0 443 _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService),
michael@0 444
michael@0 445 QueryInterface: XPCOMUtils.generateQI([
michael@0 446 Ci.nsITaggingService
michael@0 447 , Ci.nsINavBookmarkObserver
michael@0 448 , Ci.nsIObserver
michael@0 449 ])
michael@0 450 };
michael@0 451
michael@0 452
michael@0 453 function TagAutoCompleteResult(searchString, searchResult,
michael@0 454 defaultIndex, errorDescription,
michael@0 455 results, comments) {
michael@0 456 this._searchString = searchString;
michael@0 457 this._searchResult = searchResult;
michael@0 458 this._defaultIndex = defaultIndex;
michael@0 459 this._errorDescription = errorDescription;
michael@0 460 this._results = results;
michael@0 461 this._comments = comments;
michael@0 462 }
michael@0 463
michael@0 464 TagAutoCompleteResult.prototype = {
michael@0 465
michael@0 466 /**
michael@0 467 * The original search string
michael@0 468 */
michael@0 469 get searchString() {
michael@0 470 return this._searchString;
michael@0 471 },
michael@0 472
michael@0 473 /**
michael@0 474 * The result code of this result object, either:
michael@0 475 * RESULT_IGNORED (invalid searchString)
michael@0 476 * RESULT_FAILURE (failure)
michael@0 477 * RESULT_NOMATCH (no matches found)
michael@0 478 * RESULT_SUCCESS (matches found)
michael@0 479 */
michael@0 480 get searchResult() {
michael@0 481 return this._searchResult;
michael@0 482 },
michael@0 483
michael@0 484 /**
michael@0 485 * Index of the default item that should be entered if none is selected
michael@0 486 */
michael@0 487 get defaultIndex() {
michael@0 488 return this._defaultIndex;
michael@0 489 },
michael@0 490
michael@0 491 /**
michael@0 492 * A string describing the cause of a search failure
michael@0 493 */
michael@0 494 get errorDescription() {
michael@0 495 return this._errorDescription;
michael@0 496 },
michael@0 497
michael@0 498 /**
michael@0 499 * The number of matches
michael@0 500 */
michael@0 501 get matchCount() {
michael@0 502 return this._results.length;
michael@0 503 },
michael@0 504
michael@0 505 get typeAheadResult() false,
michael@0 506
michael@0 507 /**
michael@0 508 * Get the value of the result at the given index
michael@0 509 */
michael@0 510 getValueAt: function PTACR_getValueAt(index) {
michael@0 511 return this._results[index];
michael@0 512 },
michael@0 513
michael@0 514 getLabelAt: function PTACR_getLabelAt(index) {
michael@0 515 return this.getValueAt(index);
michael@0 516 },
michael@0 517
michael@0 518 /**
michael@0 519 * Get the comment of the result at the given index
michael@0 520 */
michael@0 521 getCommentAt: function PTACR_getCommentAt(index) {
michael@0 522 return this._comments[index];
michael@0 523 },
michael@0 524
michael@0 525 /**
michael@0 526 * Get the style hint for the result at the given index
michael@0 527 */
michael@0 528 getStyleAt: function PTACR_getStyleAt(index) {
michael@0 529 if (!this._comments[index])
michael@0 530 return null; // not a category label, so no special styling
michael@0 531
michael@0 532 if (index == 0)
michael@0 533 return "suggestfirst"; // category label on first line of results
michael@0 534
michael@0 535 return "suggesthint"; // category label on any other line of results
michael@0 536 },
michael@0 537
michael@0 538 /**
michael@0 539 * Get the image for the result at the given index
michael@0 540 */
michael@0 541 getImageAt: function PTACR_getImageAt(index) {
michael@0 542 return null;
michael@0 543 },
michael@0 544
michael@0 545 /**
michael@0 546 * Get the image for the result at the given index
michael@0 547 */
michael@0 548 getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) {
michael@0 549 return this.getValueAt(index);
michael@0 550 },
michael@0 551
michael@0 552 /**
michael@0 553 * Remove the value at the given index from the autocomplete results.
michael@0 554 * If removeFromDb is set to true, the value should be removed from
michael@0 555 * persistent storage as well.
michael@0 556 */
michael@0 557 removeValueAt: function PTACR_removeValueAt(index, removeFromDb) {
michael@0 558 this._results.splice(index, 1);
michael@0 559 this._comments.splice(index, 1);
michael@0 560 },
michael@0 561
michael@0 562 // nsISupports
michael@0 563 QueryInterface: XPCOMUtils.generateQI([
michael@0 564 Ci.nsIAutoCompleteResult
michael@0 565 ])
michael@0 566 };
michael@0 567
michael@0 568 // Implements nsIAutoCompleteSearch
michael@0 569 function TagAutoCompleteSearch() {
michael@0 570 XPCOMUtils.defineLazyServiceGetter(this, "tagging",
michael@0 571 "@mozilla.org/browser/tagging-service;1",
michael@0 572 "nsITaggingService");
michael@0 573 }
michael@0 574
michael@0 575 TagAutoCompleteSearch.prototype = {
michael@0 576 _stopped : false,
michael@0 577
michael@0 578 /*
michael@0 579 * Search for a given string and notify a listener (either synchronously
michael@0 580 * or asynchronously) of the result
michael@0 581 *
michael@0 582 * @param searchString - The string to search for
michael@0 583 * @param searchParam - An extra parameter
michael@0 584 * @param previousResult - A previous result to use for faster searching
michael@0 585 * @param listener - A listener to notify when the search is complete
michael@0 586 */
michael@0 587 startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
michael@0 588 var searchResults = this.tagging.allTags;
michael@0 589 var results = [];
michael@0 590 var comments = [];
michael@0 591 this._stopped = false;
michael@0 592
michael@0 593 // only search on characters for the last tag
michael@0 594 var index = Math.max(searchString.lastIndexOf(","),
michael@0 595 searchString.lastIndexOf(";"));
michael@0 596 var before = '';
michael@0 597 if (index != -1) {
michael@0 598 before = searchString.slice(0, index+1);
michael@0 599 searchString = searchString.slice(index+1);
michael@0 600 // skip past whitespace
michael@0 601 var m = searchString.match(/\s+/);
michael@0 602 if (m) {
michael@0 603 before += m[0];
michael@0 604 searchString = searchString.slice(m[0].length);
michael@0 605 }
michael@0 606 }
michael@0 607
michael@0 608 if (!searchString.length) {
michael@0 609 var newResult = new TagAutoCompleteResult(searchString,
michael@0 610 Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
michael@0 611 listener.onSearchResult(self, newResult);
michael@0 612 return;
michael@0 613 }
michael@0 614
michael@0 615 var self = this;
michael@0 616 // generator: if yields true, not done
michael@0 617 function doSearch() {
michael@0 618 var i = 0;
michael@0 619 while (i < searchResults.length) {
michael@0 620 if (self._stopped)
michael@0 621 yield false;
michael@0 622 // for each match, prepend what the user has typed so far
michael@0 623 if (searchResults[i].toLowerCase()
michael@0 624 .indexOf(searchString.toLowerCase()) == 0 &&
michael@0 625 comments.indexOf(searchResults[i]) == -1) {
michael@0 626 results.push(before + searchResults[i]);
michael@0 627 comments.push(searchResults[i]);
michael@0 628 }
michael@0 629
michael@0 630 ++i;
michael@0 631
michael@0 632 /* TODO: bug 481451
michael@0 633 * For each yield we pass a new result to the autocomplete
michael@0 634 * listener. The listener appends instead of replacing previous results,
michael@0 635 * causing invalid matchCount values.
michael@0 636 *
michael@0 637 * As a workaround, all tags are searched through in a single batch,
michael@0 638 * making this synchronous until the above issue is fixed.
michael@0 639 */
michael@0 640
michael@0 641 /*
michael@0 642 // 100 loops per yield
michael@0 643 if ((i % 100) == 0) {
michael@0 644 var newResult = new TagAutoCompleteResult(searchString,
michael@0 645 Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
michael@0 646 listener.onSearchResult(self, newResult);
michael@0 647 yield true;
michael@0 648 }
michael@0 649 */
michael@0 650 }
michael@0 651
michael@0 652 let searchResult = results.length > 0 ?
michael@0 653 Ci.nsIAutoCompleteResult.RESULT_SUCCESS :
michael@0 654 Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
michael@0 655 var newResult = new TagAutoCompleteResult(searchString, searchResult, 0,
michael@0 656 "", results, comments);
michael@0 657 listener.onSearchResult(self, newResult);
michael@0 658 yield false;
michael@0 659 }
michael@0 660
michael@0 661 // chunk the search results via the generator
michael@0 662 var gen = doSearch();
michael@0 663 while (gen.next());
michael@0 664 gen.close();
michael@0 665 },
michael@0 666
michael@0 667 /**
michael@0 668 * Stop an asynchronous search that is in progress
michael@0 669 */
michael@0 670 stopSearch: function PTACS_stopSearch() {
michael@0 671 this._stopped = true;
michael@0 672 },
michael@0 673
michael@0 674 //////////////////////////////////////////////////////////////////////////////
michael@0 675 //// nsISupports
michael@0 676
michael@0 677 classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"),
michael@0 678
michael@0 679 _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch),
michael@0 680
michael@0 681 QueryInterface: XPCOMUtils.generateQI([
michael@0 682 Ci.nsIAutoCompleteSearch
michael@0 683 ])
michael@0 684 };
michael@0 685
michael@0 686 let component = [TaggingService, TagAutoCompleteSearch];
michael@0 687 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial