toolkit/components/satchel/nsFormAutoComplete.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
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
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
michael@0 14 "resource://gre/modules/BrowserUtils.jsm");
michael@0 15 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
michael@0 16 "resource://gre/modules/Deprecated.jsm");
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
michael@0 18 "resource://gre/modules/FormHistory.jsm");
michael@0 19
michael@0 20 function FormAutoComplete() {
michael@0 21 this.init();
michael@0 22 }
michael@0 23
michael@0 24 /**
michael@0 25 * FormAutoComplete
michael@0 26 *
michael@0 27 * Implements the nsIFormAutoComplete interface in the main process.
michael@0 28 */
michael@0 29 FormAutoComplete.prototype = {
michael@0 30 classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
michael@0 31 QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
michael@0 32
michael@0 33 _prefBranch : null,
michael@0 34 _debug : true, // mirrors browser.formfill.debug
michael@0 35 _enabled : true, // mirrors browser.formfill.enable preference
michael@0 36 _agedWeight : 2,
michael@0 37 _bucketSize : 1,
michael@0 38 _maxTimeGroupings : 25,
michael@0 39 _timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000,
michael@0 40 _expireDays : null,
michael@0 41 _boundaryWeight : 25,
michael@0 42 _prefixWeight : 5,
michael@0 43
michael@0 44 // Only one query is performed at a time, which will be stored in _pendingQuery
michael@0 45 // while the query is being performed. It will be cleared when the query finishes,
michael@0 46 // is cancelled, or an error occurs. If a new query occurs while one is already
michael@0 47 // pending, the existing one is cancelled. The pending query will be an
michael@0 48 // mozIStoragePendingStatement object.
michael@0 49 _pendingQuery : null,
michael@0 50
michael@0 51 init : function() {
michael@0 52 // Preferences. Add observer so we get notified of changes.
michael@0 53 this._prefBranch = Services.prefs.getBranch("browser.formfill.");
michael@0 54 this._prefBranch.addObserver("", this.observer, true);
michael@0 55 this.observer._self = this;
michael@0 56
michael@0 57 this._debug = this._prefBranch.getBoolPref("debug");
michael@0 58 this._enabled = this._prefBranch.getBoolPref("enable");
michael@0 59 this._agedWeight = this._prefBranch.getIntPref("agedWeight");
michael@0 60 this._bucketSize = this._prefBranch.getIntPref("bucketSize");
michael@0 61 this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
michael@0 62 this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
michael@0 63 this._expireDays = this._prefBranch.getIntPref("expire_days");
michael@0 64 },
michael@0 65
michael@0 66 observer : {
michael@0 67 _self : null,
michael@0 68
michael@0 69 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
michael@0 70 Ci.nsISupportsWeakReference]),
michael@0 71
michael@0 72 observe : function (subject, topic, data) {
michael@0 73 let self = this._self;
michael@0 74 if (topic == "nsPref:changed") {
michael@0 75 let prefName = data;
michael@0 76 self.log("got change to " + prefName + " preference");
michael@0 77
michael@0 78 switch (prefName) {
michael@0 79 case "agedWeight":
michael@0 80 self._agedWeight = self._prefBranch.getIntPref(prefName);
michael@0 81 break;
michael@0 82 case "debug":
michael@0 83 self._debug = self._prefBranch.getBoolPref(prefName);
michael@0 84 break;
michael@0 85 case "enable":
michael@0 86 self._enabled = self._prefBranch.getBoolPref(prefName);
michael@0 87 break;
michael@0 88 case "maxTimeGroupings":
michael@0 89 self._maxTimeGroupings = self._prefBranch.getIntPref(prefName);
michael@0 90 break;
michael@0 91 case "timeGroupingSize":
michael@0 92 self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000;
michael@0 93 break;
michael@0 94 case "bucketSize":
michael@0 95 self._bucketSize = self._prefBranch.getIntPref(prefName);
michael@0 96 break;
michael@0 97 case "boundaryWeight":
michael@0 98 self._boundaryWeight = self._prefBranch.getIntPref(prefName);
michael@0 99 break;
michael@0 100 case "prefixWeight":
michael@0 101 self._prefixWeight = self._prefBranch.getIntPref(prefName);
michael@0 102 break;
michael@0 103 default:
michael@0 104 self.log("Oops! Pref not handled, change ignored.");
michael@0 105 }
michael@0 106 }
michael@0 107 }
michael@0 108 },
michael@0 109
michael@0 110
michael@0 111 /*
michael@0 112 * log
michael@0 113 *
michael@0 114 * Internal function for logging debug messages to the Error Console
michael@0 115 * window
michael@0 116 */
michael@0 117 log : function (message) {
michael@0 118 if (!this._debug)
michael@0 119 return;
michael@0 120 dump("FormAutoComplete: " + message + "\n");
michael@0 121 Services.console.logStringMessage("FormAutoComplete: " + message);
michael@0 122 },
michael@0 123
michael@0 124
michael@0 125 autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
michael@0 126 Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
michael@0 127
michael@0 128 let result = null;
michael@0 129 let listener = {
michael@0 130 onSearchCompletion: function (r) result = r
michael@0 131 };
michael@0 132 this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
michael@0 133
michael@0 134 // Just wait for the result to to be available.
michael@0 135 let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread;
michael@0 136 while (!result && this._pendingQuery) {
michael@0 137 thread.processNextEvent(true);
michael@0 138 }
michael@0 139
michael@0 140 return result;
michael@0 141 },
michael@0 142
michael@0 143 autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
michael@0 144 this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
michael@0 145 },
michael@0 146
michael@0 147 /*
michael@0 148 * autoCompleteSearchShared
michael@0 149 *
michael@0 150 * aInputName -- |name| attribute from the form input being autocompleted.
michael@0 151 * aUntrimmedSearchString -- current value of the input
michael@0 152 * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
michael@0 153 * aPreviousResult -- previous search result, if any.
michael@0 154 * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
michael@0 155 * that may be returned asynchronously.
michael@0 156 */
michael@0 157 _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
michael@0 158 function sortBytotalScore (a, b) {
michael@0 159 return b.totalScore - a.totalScore;
michael@0 160 }
michael@0 161
michael@0 162 let result = null;
michael@0 163 if (!this._enabled) {
michael@0 164 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
michael@0 165 if (aListener) {
michael@0 166 aListener.onSearchCompletion(result);
michael@0 167 }
michael@0 168 return;
michael@0 169 }
michael@0 170
michael@0 171 // don't allow form inputs (aField != null) to get results from search bar history
michael@0 172 if (aInputName == 'searchbar-history' && aField) {
michael@0 173 this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
michael@0 174 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
michael@0 175 if (aListener) {
michael@0 176 aListener.onSearchCompletion(result);
michael@0 177 }
michael@0 178 return;
michael@0 179 }
michael@0 180
michael@0 181 this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
michael@0 182 let searchString = aUntrimmedSearchString.trim().toLowerCase();
michael@0 183
michael@0 184 // reuse previous results if:
michael@0 185 // a) length greater than one character (others searches are special cases) AND
michael@0 186 // b) the the new results will be a subset of the previous results
michael@0 187 if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
michael@0 188 searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
michael@0 189 this.log("Using previous autocomplete result");
michael@0 190 result = aPreviousResult;
michael@0 191 result.wrappedJSObject.searchString = aUntrimmedSearchString;
michael@0 192
michael@0 193 let searchTokens = searchString.split(/\s+/);
michael@0 194 // We have a list of results for a shorter search string, so just
michael@0 195 // filter them further based on the new search string and add to a new array.
michael@0 196 let entries = result.wrappedJSObject.entries;
michael@0 197 let filteredEntries = [];
michael@0 198 for (let i = 0; i < entries.length; i++) {
michael@0 199 let entry = entries[i];
michael@0 200 // Remove results that do not contain the token
michael@0 201 // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
michael@0 202 if(searchTokens.some(function (tok) entry.textLowerCase.indexOf(tok) < 0))
michael@0 203 continue;
michael@0 204 this._calculateScore(entry, searchString, searchTokens);
michael@0 205 this.log("Reusing autocomplete entry '" + entry.text +
michael@0 206 "' (" + entry.frecency +" / " + entry.totalScore + ")");
michael@0 207 filteredEntries.push(entry);
michael@0 208 }
michael@0 209 filteredEntries.sort(sortBytotalScore);
michael@0 210 result.wrappedJSObject.entries = filteredEntries;
michael@0 211
michael@0 212 if (aListener) {
michael@0 213 aListener.onSearchCompletion(result);
michael@0 214 }
michael@0 215 } else {
michael@0 216 this.log("Creating new autocomplete search result.");
michael@0 217
michael@0 218 // Start with an empty list.
michael@0 219 result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
michael@0 220
michael@0 221 let processEntry = function(aEntries) {
michael@0 222 if (aField && aField.maxLength > -1) {
michael@0 223 result.entries =
michael@0 224 aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
michael@0 225 } else {
michael@0 226 result.entries = aEntries;
michael@0 227 }
michael@0 228
michael@0 229 if (aListener) {
michael@0 230 aListener.onSearchCompletion(result);
michael@0 231 }
michael@0 232 }
michael@0 233
michael@0 234 this.getAutoCompleteValues(aInputName, searchString, processEntry);
michael@0 235 }
michael@0 236 },
michael@0 237
michael@0 238 stopAutoCompleteSearch : function () {
michael@0 239 if (this._pendingQuery) {
michael@0 240 this._pendingQuery.cancel();
michael@0 241 this._pendingQuery = null;
michael@0 242 }
michael@0 243 },
michael@0 244
michael@0 245 /*
michael@0 246 * Get the values for an autocomplete list given a search string.
michael@0 247 *
michael@0 248 * fieldName - fieldname field within form history (the form input name)
michael@0 249 * searchString - string to search for
michael@0 250 * callback - called when the values are available. Passed an array of objects,
michael@0 251 * containing properties for each result. The callback is only called
michael@0 252 * when successful.
michael@0 253 */
michael@0 254 getAutoCompleteValues : function (fieldName, searchString, callback) {
michael@0 255 let params = {
michael@0 256 agedWeight: this._agedWeight,
michael@0 257 bucketSize: this._bucketSize,
michael@0 258 expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
michael@0 259 fieldname: fieldName,
michael@0 260 maxTimeGroupings: this._maxTimeGroupings,
michael@0 261 timeGroupingSize: this._timeGroupingSize,
michael@0 262 prefixWeight: this._prefixWeight,
michael@0 263 boundaryWeight: this._boundaryWeight
michael@0 264 }
michael@0 265
michael@0 266 this.stopAutoCompleteSearch();
michael@0 267
michael@0 268 let results = [];
michael@0 269 let processResults = {
michael@0 270 handleResult: aResult => {
michael@0 271 results.push(aResult);
michael@0 272 },
michael@0 273 handleError: aError => {
michael@0 274 this.log("getAutocompleteValues failed: " + aError.message);
michael@0 275 },
michael@0 276 handleCompletion: aReason => {
michael@0 277 this._pendingQuery = null;
michael@0 278 if (!aReason) {
michael@0 279 callback(results);
michael@0 280 }
michael@0 281 }
michael@0 282 };
michael@0 283
michael@0 284 this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
michael@0 285 },
michael@0 286
michael@0 287 /*
michael@0 288 * _calculateScore
michael@0 289 *
michael@0 290 * entry -- an nsIAutoCompleteResult entry
michael@0 291 * aSearchString -- current value of the input (lowercase)
michael@0 292 * searchTokens -- array of tokens of the search string
michael@0 293 *
michael@0 294 * Returns: an int
michael@0 295 */
michael@0 296 _calculateScore : function (entry, aSearchString, searchTokens) {
michael@0 297 let boundaryCalc = 0;
michael@0 298 // for each word, calculate word boundary weights
michael@0 299 for each (let token in searchTokens) {
michael@0 300 boundaryCalc += (entry.textLowerCase.indexOf(token) == 0);
michael@0 301 boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0);
michael@0 302 }
michael@0 303 boundaryCalc = boundaryCalc * this._boundaryWeight;
michael@0 304 // now add more weight if we have a traditional prefix match and
michael@0 305 // multiply boundary bonuses by boundary weight
michael@0 306 boundaryCalc += this._prefixWeight *
michael@0 307 (entry.textLowerCase.
michael@0 308 indexOf(aSearchString) == 0);
michael@0 309 entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
michael@0 310 }
michael@0 311
michael@0 312 }; // end of FormAutoComplete implementation
michael@0 313
michael@0 314 /**
michael@0 315 * FormAutoCompleteChild
michael@0 316 *
michael@0 317 * Implements the nsIFormAutoComplete interface in a child content process,
michael@0 318 * and forwards the auto-complete requests to the parent process which
michael@0 319 * also implements a nsIFormAutoComplete interface and has
michael@0 320 * direct access to the FormHistory database.
michael@0 321 */
michael@0 322 function FormAutoCompleteChild() {
michael@0 323 this.init();
michael@0 324 }
michael@0 325
michael@0 326 FormAutoCompleteChild.prototype = {
michael@0 327 classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
michael@0 328 QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
michael@0 329
michael@0 330 _debug: false,
michael@0 331 _enabled: true,
michael@0 332
michael@0 333 /*
michael@0 334 * init
michael@0 335 *
michael@0 336 * Initializes the content-process side of the FormAutoComplete component,
michael@0 337 * and add a listener for the message that the parent process sends when
michael@0 338 * a result is produced.
michael@0 339 */
michael@0 340 init: function() {
michael@0 341 this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
michael@0 342 this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
michael@0 343 this.log("init");
michael@0 344 },
michael@0 345
michael@0 346 /*
michael@0 347 * log
michael@0 348 *
michael@0 349 * Internal function for logging debug messages
michael@0 350 */
michael@0 351 log : function (message) {
michael@0 352 if (!this._debug)
michael@0 353 return;
michael@0 354 dump("FormAutoCompleteChild: " + message + "\n");
michael@0 355 },
michael@0 356
michael@0 357 autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
michael@0 358 // This function is deprecated
michael@0 359 },
michael@0 360
michael@0 361 autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
michael@0 362 this.log("autoCompleteSearchAsync");
michael@0 363
michael@0 364 this._pendingListener = aListener;
michael@0 365
michael@0 366 let rect = BrowserUtils.getElementBoundingScreenRect(aField);
michael@0 367
michael@0 368 let window = aField.ownerDocument.defaultView;
michael@0 369 let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 370 .getInterface(Ci.nsIDocShell)
michael@0 371 .sameTypeRootTreeItem
michael@0 372 .QueryInterface(Ci.nsIDocShell);
michael@0 373
michael@0 374 let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 375 .getInterface(Ci.nsIContentFrameMessageManager);
michael@0 376
michael@0 377 mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
michael@0 378 inputName: aInputName,
michael@0 379 untrimmedSearchString: aUntrimmedSearchString,
michael@0 380 left: rect.left,
michael@0 381 top: rect.top,
michael@0 382 width: rect.width,
michael@0 383 height: rect.height
michael@0 384 });
michael@0 385
michael@0 386 mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
michael@0 387 function searchFinished(message) {
michael@0 388 mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
michael@0 389 let result = new FormAutoCompleteResult(
michael@0 390 null,
michael@0 391 [{text: res} for (res of message.data.results)],
michael@0 392 null,
michael@0 393 null
michael@0 394 );
michael@0 395 if (aListener) {
michael@0 396 aListener.onSearchCompletion(result);
michael@0 397 }
michael@0 398 }
michael@0 399 );
michael@0 400
michael@0 401 this.log("autoCompleteSearchAsync message was sent");
michael@0 402 },
michael@0 403
michael@0 404 stopAutoCompleteSearch : function () {
michael@0 405 this.log("stopAutoCompleteSearch");
michael@0 406 },
michael@0 407 }; // end of FormAutoCompleteChild implementation
michael@0 408
michael@0 409 // nsIAutoCompleteResult implementation
michael@0 410 function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
michael@0 411 this.formHistory = formHistory;
michael@0 412 this.entries = entries;
michael@0 413 this.fieldName = fieldName;
michael@0 414 this.searchString = searchString;
michael@0 415 }
michael@0 416
michael@0 417 FormAutoCompleteResult.prototype = {
michael@0 418 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
michael@0 419 Ci.nsISupportsWeakReference]),
michael@0 420
michael@0 421 // private
michael@0 422 formHistory : null,
michael@0 423 entries : null,
michael@0 424 fieldName : null,
michael@0 425
michael@0 426 _checkIndexBounds : function (index) {
michael@0 427 if (index < 0 || index >= this.entries.length)
michael@0 428 throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
michael@0 429 },
michael@0 430
michael@0 431 // Allow autoCompleteSearch to get at the JS object so it can
michael@0 432 // modify some readonly properties for internal use.
michael@0 433 get wrappedJSObject() {
michael@0 434 return this;
michael@0 435 },
michael@0 436
michael@0 437 // Interfaces from idl...
michael@0 438 searchString : null,
michael@0 439 errorDescription : "",
michael@0 440 get defaultIndex() {
michael@0 441 if (entries.length == 0)
michael@0 442 return -1;
michael@0 443 else
michael@0 444 return 0;
michael@0 445 },
michael@0 446 get searchResult() {
michael@0 447 if (this.entries.length == 0)
michael@0 448 return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
michael@0 449 return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
michael@0 450 },
michael@0 451 get matchCount() {
michael@0 452 return this.entries.length;
michael@0 453 },
michael@0 454
michael@0 455 getValueAt : function (index) {
michael@0 456 this._checkIndexBounds(index);
michael@0 457 return this.entries[index].text;
michael@0 458 },
michael@0 459
michael@0 460 getLabelAt: function(index) {
michael@0 461 return getValueAt(index);
michael@0 462 },
michael@0 463
michael@0 464 getCommentAt : function (index) {
michael@0 465 this._checkIndexBounds(index);
michael@0 466 return "";
michael@0 467 },
michael@0 468
michael@0 469 getStyleAt : function (index) {
michael@0 470 this._checkIndexBounds(index);
michael@0 471 return "";
michael@0 472 },
michael@0 473
michael@0 474 getImageAt : function (index) {
michael@0 475 this._checkIndexBounds(index);
michael@0 476 return "";
michael@0 477 },
michael@0 478
michael@0 479 getFinalCompleteValueAt : function (index) {
michael@0 480 return this.getValueAt(index);
michael@0 481 },
michael@0 482
michael@0 483 removeValueAt : function (index, removeFromDB) {
michael@0 484 this._checkIndexBounds(index);
michael@0 485
michael@0 486 let [removedEntry] = this.entries.splice(index, 1);
michael@0 487
michael@0 488 if (removeFromDB) {
michael@0 489 this.formHistory.update({ op: "remove",
michael@0 490 fieldname: this.fieldName,
michael@0 491 value: removedEntry.text });
michael@0 492 }
michael@0 493 }
michael@0 494 };
michael@0 495
michael@0 496
michael@0 497 let remote = Services.appinfo.browserTabsRemote;
michael@0 498 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT && remote) {
michael@0 499 // Register the stub FormAutoComplete module in the child which will
michael@0 500 // forward messages to the parent through the process message manager.
michael@0 501 let component = [FormAutoCompleteChild];
michael@0 502 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
michael@0 503 } else {
michael@0 504 let component = [FormAutoComplete];
michael@0 505 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
michael@0 506 }

mercurial