toolkit/components/satchel/nsFormAutoComplete.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/satchel/nsFormAutoComplete.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,506 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +
     1.9 +const Cc = Components.classes;
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cr = Components.results;
    1.12 +
    1.13 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.14 +Components.utils.import("resource://gre/modules/Services.jsm");
    1.15 +
    1.16 +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
    1.17 +                                  "resource://gre/modules/BrowserUtils.jsm");
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    1.19 +                                  "resource://gre/modules/Deprecated.jsm");
    1.20 +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
    1.21 +                                  "resource://gre/modules/FormHistory.jsm");
    1.22 +
    1.23 +function FormAutoComplete() {
    1.24 +    this.init();
    1.25 +}
    1.26 +
    1.27 +/**
    1.28 + * FormAutoComplete
    1.29 + *
    1.30 + * Implements the nsIFormAutoComplete interface in the main process.
    1.31 + */
    1.32 +FormAutoComplete.prototype = {
    1.33 +    classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
    1.34 +    QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
    1.35 +
    1.36 +    _prefBranch         : null,
    1.37 +    _debug              : true, // mirrors browser.formfill.debug
    1.38 +    _enabled            : true, // mirrors browser.formfill.enable preference
    1.39 +    _agedWeight         : 2,
    1.40 +    _bucketSize         : 1,
    1.41 +    _maxTimeGroupings   : 25,
    1.42 +    _timeGroupingSize   : 7 * 24 * 60 * 60 * 1000 * 1000,
    1.43 +    _expireDays         : null,
    1.44 +    _boundaryWeight     : 25,
    1.45 +    _prefixWeight       : 5,
    1.46 +
    1.47 +    // Only one query is performed at a time, which will be stored in _pendingQuery
    1.48 +    // while the query is being performed. It will be cleared when the query finishes,
    1.49 +    // is cancelled, or an error occurs. If a new query occurs while one is already
    1.50 +    // pending, the existing one is cancelled. The pending query will be an
    1.51 +    // mozIStoragePendingStatement object.
    1.52 +    _pendingQuery       : null,
    1.53 +
    1.54 +    init : function() {
    1.55 +        // Preferences. Add observer so we get notified of changes.
    1.56 +        this._prefBranch = Services.prefs.getBranch("browser.formfill.");
    1.57 +        this._prefBranch.addObserver("", this.observer, true);
    1.58 +        this.observer._self = this;
    1.59 +
    1.60 +        this._debug            = this._prefBranch.getBoolPref("debug");
    1.61 +        this._enabled          = this._prefBranch.getBoolPref("enable");
    1.62 +        this._agedWeight       = this._prefBranch.getIntPref("agedWeight");
    1.63 +        this._bucketSize       = this._prefBranch.getIntPref("bucketSize");
    1.64 +        this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
    1.65 +        this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
    1.66 +        this._expireDays       = this._prefBranch.getIntPref("expire_days");
    1.67 +    },
    1.68 +
    1.69 +    observer : {
    1.70 +        _self : null,
    1.71 +
    1.72 +        QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
    1.73 +                                                Ci.nsISupportsWeakReference]),
    1.74 +
    1.75 +        observe : function (subject, topic, data) {
    1.76 +            let self = this._self;
    1.77 +            if (topic == "nsPref:changed") {
    1.78 +                let prefName = data;
    1.79 +                self.log("got change to " + prefName + " preference");
    1.80 +
    1.81 +                switch (prefName) {
    1.82 +                    case "agedWeight":
    1.83 +                        self._agedWeight = self._prefBranch.getIntPref(prefName);
    1.84 +                        break;
    1.85 +                    case "debug":
    1.86 +                        self._debug = self._prefBranch.getBoolPref(prefName);
    1.87 +                        break;
    1.88 +                    case "enable":
    1.89 +                        self._enabled = self._prefBranch.getBoolPref(prefName);
    1.90 +                        break;
    1.91 +                    case "maxTimeGroupings":
    1.92 +                        self._maxTimeGroupings = self._prefBranch.getIntPref(prefName);
    1.93 +                        break;
    1.94 +                    case "timeGroupingSize":
    1.95 +                        self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000;
    1.96 +                        break;
    1.97 +                    case "bucketSize":
    1.98 +                        self._bucketSize = self._prefBranch.getIntPref(prefName);
    1.99 +                        break;
   1.100 +                    case "boundaryWeight":
   1.101 +                        self._boundaryWeight = self._prefBranch.getIntPref(prefName);
   1.102 +                        break;
   1.103 +                    case "prefixWeight":
   1.104 +                        self._prefixWeight = self._prefBranch.getIntPref(prefName);
   1.105 +                        break;
   1.106 +                    default:
   1.107 +                        self.log("Oops! Pref not handled, change ignored.");
   1.108 +                }
   1.109 +            }
   1.110 +        }
   1.111 +    },
   1.112 +
   1.113 +
   1.114 +    /*
   1.115 +     * log
   1.116 +     *
   1.117 +     * Internal function for logging debug messages to the Error Console
   1.118 +     * window
   1.119 +     */
   1.120 +    log : function (message) {
   1.121 +        if (!this._debug)
   1.122 +            return;
   1.123 +        dump("FormAutoComplete: " + message + "\n");
   1.124 +        Services.console.logStringMessage("FormAutoComplete: " + message);
   1.125 +    },
   1.126 +
   1.127 +
   1.128 +    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
   1.129 +      Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
   1.130 +
   1.131 +      let result = null;
   1.132 +      let listener = {
   1.133 +        onSearchCompletion: function (r) result = r
   1.134 +      };
   1.135 +      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
   1.136 +
   1.137 +      // Just wait for the result to to be available.
   1.138 +      let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread;
   1.139 +      while (!result && this._pendingQuery) {
   1.140 +        thread.processNextEvent(true);
   1.141 +      }
   1.142 +
   1.143 +      return result;
   1.144 +    },
   1.145 +
   1.146 +    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
   1.147 +      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
   1.148 +    },
   1.149 +
   1.150 +    /*
   1.151 +     * autoCompleteSearchShared
   1.152 +     *
   1.153 +     * aInputName    -- |name| attribute from the form input being autocompleted.
   1.154 +     * aUntrimmedSearchString -- current value of the input
   1.155 +     * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
   1.156 +     * aPreviousResult -- previous search result, if any.
   1.157 +     * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
   1.158 +     *              that may be returned asynchronously.
   1.159 +     */
   1.160 +    _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
   1.161 +        function sortBytotalScore (a, b) {
   1.162 +            return b.totalScore - a.totalScore;
   1.163 +        }
   1.164 +
   1.165 +        let result = null;
   1.166 +        if (!this._enabled) {
   1.167 +            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
   1.168 +            if (aListener) {
   1.169 +              aListener.onSearchCompletion(result);
   1.170 +            }
   1.171 +            return;
   1.172 +        }
   1.173 +
   1.174 +        // don't allow form inputs (aField != null) to get results from search bar history
   1.175 +        if (aInputName == 'searchbar-history' && aField) {
   1.176 +            this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
   1.177 +            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
   1.178 +            if (aListener) {
   1.179 +              aListener.onSearchCompletion(result);
   1.180 +            }
   1.181 +            return;
   1.182 +        }
   1.183 +
   1.184 +        this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
   1.185 +        let searchString = aUntrimmedSearchString.trim().toLowerCase();
   1.186 +
   1.187 +        // reuse previous results if:
   1.188 +        // a) length greater than one character (others searches are special cases) AND
   1.189 +        // b) the the new results will be a subset of the previous results
   1.190 +        if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
   1.191 +            searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
   1.192 +            this.log("Using previous autocomplete result");
   1.193 +            result = aPreviousResult;
   1.194 +            result.wrappedJSObject.searchString = aUntrimmedSearchString;
   1.195 +
   1.196 +            let searchTokens = searchString.split(/\s+/);
   1.197 +            // We have a list of results for a shorter search string, so just
   1.198 +            // filter them further based on the new search string and add to a new array.
   1.199 +            let entries = result.wrappedJSObject.entries;
   1.200 +            let filteredEntries = [];
   1.201 +            for (let i = 0; i < entries.length; i++) {
   1.202 +                let entry = entries[i];
   1.203 +                // Remove results that do not contain the token
   1.204 +                // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
   1.205 +                if(searchTokens.some(function (tok) entry.textLowerCase.indexOf(tok) < 0))
   1.206 +                    continue;
   1.207 +                this._calculateScore(entry, searchString, searchTokens);
   1.208 +                this.log("Reusing autocomplete entry '" + entry.text +
   1.209 +                         "' (" + entry.frecency +" / " + entry.totalScore + ")");
   1.210 +                filteredEntries.push(entry);
   1.211 +            }
   1.212 +            filteredEntries.sort(sortBytotalScore);
   1.213 +            result.wrappedJSObject.entries = filteredEntries;
   1.214 +
   1.215 +            if (aListener) {
   1.216 +              aListener.onSearchCompletion(result);
   1.217 +            }
   1.218 +        } else {
   1.219 +            this.log("Creating new autocomplete search result.");
   1.220 +
   1.221 +            // Start with an empty list.
   1.222 +            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
   1.223 +
   1.224 +            let processEntry = function(aEntries) {
   1.225 +              if (aField && aField.maxLength > -1) {
   1.226 +                result.entries =
   1.227 +                  aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
   1.228 +              } else {
   1.229 +                result.entries = aEntries;
   1.230 +              }
   1.231 +
   1.232 +              if (aListener) {
   1.233 +                aListener.onSearchCompletion(result);
   1.234 +              }
   1.235 +            }
   1.236 +
   1.237 +            this.getAutoCompleteValues(aInputName, searchString, processEntry);
   1.238 +        }
   1.239 +    },
   1.240 +
   1.241 +    stopAutoCompleteSearch : function () {
   1.242 +        if (this._pendingQuery) {
   1.243 +            this._pendingQuery.cancel();
   1.244 +            this._pendingQuery = null;
   1.245 +        }
   1.246 +    },
   1.247 +
   1.248 +    /*
   1.249 +     * Get the values for an autocomplete list given a search string.
   1.250 +     *
   1.251 +     *  fieldName - fieldname field within form history (the form input name)
   1.252 +     *  searchString - string to search for
   1.253 +     *  callback - called when the values are available. Passed an array of objects,
   1.254 +     *             containing properties for each result. The callback is only called
   1.255 +     *             when successful.
   1.256 +     */
   1.257 +    getAutoCompleteValues : function (fieldName, searchString, callback) {
   1.258 +        let params = {
   1.259 +            agedWeight:         this._agedWeight,
   1.260 +            bucketSize:         this._bucketSize,
   1.261 +            expiryDate:         1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
   1.262 +            fieldname:          fieldName,
   1.263 +            maxTimeGroupings:   this._maxTimeGroupings,
   1.264 +            timeGroupingSize:   this._timeGroupingSize,
   1.265 +            prefixWeight:       this._prefixWeight,
   1.266 +            boundaryWeight:     this._boundaryWeight
   1.267 +        }
   1.268 +
   1.269 +        this.stopAutoCompleteSearch();
   1.270 +
   1.271 +        let results = [];
   1.272 +        let processResults = {
   1.273 +          handleResult: aResult => {
   1.274 +            results.push(aResult);
   1.275 +          },
   1.276 +          handleError: aError => {
   1.277 +            this.log("getAutocompleteValues failed: " + aError.message);
   1.278 +          },
   1.279 +          handleCompletion: aReason => {
   1.280 +            this._pendingQuery = null;
   1.281 +            if (!aReason) {
   1.282 +              callback(results);
   1.283 +            }
   1.284 +          }
   1.285 +        };
   1.286 +
   1.287 +        this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
   1.288 +    },
   1.289 +
   1.290 +    /*
   1.291 +     * _calculateScore
   1.292 +     *
   1.293 +     * entry    -- an nsIAutoCompleteResult entry
   1.294 +     * aSearchString -- current value of the input (lowercase)
   1.295 +     * searchTokens -- array of tokens of the search string
   1.296 +     *
   1.297 +     * Returns: an int
   1.298 +     */
   1.299 +    _calculateScore : function (entry, aSearchString, searchTokens) {
   1.300 +        let boundaryCalc = 0;
   1.301 +        // for each word, calculate word boundary weights
   1.302 +        for each (let token in searchTokens) {
   1.303 +            boundaryCalc += (entry.textLowerCase.indexOf(token) == 0);
   1.304 +            boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0);
   1.305 +        }
   1.306 +        boundaryCalc = boundaryCalc * this._boundaryWeight;
   1.307 +        // now add more weight if we have a traditional prefix match and
   1.308 +        // multiply boundary bonuses by boundary weight
   1.309 +        boundaryCalc += this._prefixWeight *
   1.310 +                        (entry.textLowerCase.
   1.311 +                         indexOf(aSearchString) == 0);
   1.312 +        entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
   1.313 +    }
   1.314 +
   1.315 +}; // end of FormAutoComplete implementation
   1.316 +
   1.317 +/**
   1.318 + * FormAutoCompleteChild
   1.319 + *
   1.320 + * Implements the nsIFormAutoComplete interface in a child content process,
   1.321 + * and forwards the auto-complete requests to the parent process which
   1.322 + * also implements a nsIFormAutoComplete interface and has
   1.323 + * direct access to the FormHistory database.
   1.324 + */
   1.325 +function FormAutoCompleteChild() {
   1.326 +  this.init();
   1.327 +}
   1.328 +
   1.329 +FormAutoCompleteChild.prototype = {
   1.330 +    classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
   1.331 +    QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
   1.332 +
   1.333 +    _debug: false,
   1.334 +    _enabled: true,
   1.335 +
   1.336 +    /*
   1.337 +     * init
   1.338 +     *
   1.339 +     * Initializes the content-process side of the FormAutoComplete component,
   1.340 +     * and add a listener for the message that the parent process sends when
   1.341 +     * a result is produced.
   1.342 +     */
   1.343 +    init: function() {
   1.344 +      this._debug    = Services.prefs.getBoolPref("browser.formfill.debug");
   1.345 +      this._enabled  = Services.prefs.getBoolPref("browser.formfill.enable");
   1.346 +      this.log("init");
   1.347 +    },
   1.348 +
   1.349 +    /*
   1.350 +     * log
   1.351 +     *
   1.352 +     * Internal function for logging debug messages
   1.353 +     */
   1.354 +    log : function (message) {
   1.355 +      if (!this._debug)
   1.356 +        return;
   1.357 +      dump("FormAutoCompleteChild: " + message + "\n");
   1.358 +    },
   1.359 +
   1.360 +    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
   1.361 +      // This function is deprecated
   1.362 +    },
   1.363 +
   1.364 +    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
   1.365 +      this.log("autoCompleteSearchAsync");
   1.366 +
   1.367 +      this._pendingListener = aListener;
   1.368 +
   1.369 +      let rect = BrowserUtils.getElementBoundingScreenRect(aField);
   1.370 +
   1.371 +      let window = aField.ownerDocument.defaultView;
   1.372 +      let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
   1.373 +                                   .getInterface(Ci.nsIDocShell)
   1.374 +                                   .sameTypeRootTreeItem
   1.375 +                                   .QueryInterface(Ci.nsIDocShell);
   1.376 +
   1.377 +      let mm = topLevelDocshell.QueryInterface(Ci.nsIInterfaceRequestor)
   1.378 +                               .getInterface(Ci.nsIContentFrameMessageManager);
   1.379 +
   1.380 +      mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
   1.381 +        inputName: aInputName,
   1.382 +        untrimmedSearchString: aUntrimmedSearchString,
   1.383 +        left: rect.left,
   1.384 +        top: rect.top,
   1.385 +        width: rect.width,
   1.386 +        height: rect.height
   1.387 +      });
   1.388 +
   1.389 +      mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
   1.390 +        function searchFinished(message) {
   1.391 +          mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
   1.392 +          let result = new FormAutoCompleteResult(
   1.393 +            null,
   1.394 +            [{text: res} for (res of message.data.results)],
   1.395 +            null,
   1.396 +            null
   1.397 +          );
   1.398 +          if (aListener) {
   1.399 +            aListener.onSearchCompletion(result);
   1.400 +          }
   1.401 +        }
   1.402 +      );
   1.403 +
   1.404 +      this.log("autoCompleteSearchAsync message was sent");
   1.405 +    },
   1.406 +
   1.407 +    stopAutoCompleteSearch : function () {
   1.408 +       this.log("stopAutoCompleteSearch");
   1.409 +    },
   1.410 +}; // end of FormAutoCompleteChild implementation
   1.411 +
   1.412 +// nsIAutoCompleteResult implementation
   1.413 +function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) {
   1.414 +    this.formHistory = formHistory;
   1.415 +    this.entries = entries;
   1.416 +    this.fieldName = fieldName;
   1.417 +    this.searchString = searchString;
   1.418 +}
   1.419 +
   1.420 +FormAutoCompleteResult.prototype = {
   1.421 +    QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
   1.422 +                                            Ci.nsISupportsWeakReference]),
   1.423 +
   1.424 +    // private
   1.425 +    formHistory : null,
   1.426 +    entries : null,
   1.427 +    fieldName : null,
   1.428 +
   1.429 +    _checkIndexBounds : function (index) {
   1.430 +        if (index < 0 || index >= this.entries.length)
   1.431 +            throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
   1.432 +    },
   1.433 +
   1.434 +    // Allow autoCompleteSearch to get at the JS object so it can
   1.435 +    // modify some readonly properties for internal use.
   1.436 +    get wrappedJSObject() {
   1.437 +        return this;
   1.438 +    },
   1.439 +
   1.440 +    // Interfaces from idl...
   1.441 +    searchString : null,
   1.442 +    errorDescription : "",
   1.443 +    get defaultIndex() {
   1.444 +        if (entries.length == 0)
   1.445 +            return -1;
   1.446 +        else
   1.447 +            return 0;
   1.448 +    },
   1.449 +    get searchResult() {
   1.450 +        if (this.entries.length == 0)
   1.451 +            return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
   1.452 +        return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
   1.453 +    },
   1.454 +    get matchCount() {
   1.455 +        return this.entries.length;
   1.456 +    },
   1.457 +
   1.458 +    getValueAt : function (index) {
   1.459 +        this._checkIndexBounds(index);
   1.460 +        return this.entries[index].text;
   1.461 +    },
   1.462 +
   1.463 +    getLabelAt: function(index) {
   1.464 +        return getValueAt(index);
   1.465 +    },
   1.466 +
   1.467 +    getCommentAt : function (index) {
   1.468 +        this._checkIndexBounds(index);
   1.469 +        return "";
   1.470 +    },
   1.471 +
   1.472 +    getStyleAt : function (index) {
   1.473 +        this._checkIndexBounds(index);
   1.474 +        return "";
   1.475 +    },
   1.476 +
   1.477 +    getImageAt : function (index) {
   1.478 +        this._checkIndexBounds(index);
   1.479 +        return "";
   1.480 +    },
   1.481 +
   1.482 +    getFinalCompleteValueAt : function (index) {
   1.483 +        return this.getValueAt(index);
   1.484 +    },
   1.485 +
   1.486 +    removeValueAt : function (index, removeFromDB) {
   1.487 +        this._checkIndexBounds(index);
   1.488 +
   1.489 +        let [removedEntry] = this.entries.splice(index, 1);
   1.490 +
   1.491 +        if (removeFromDB) {
   1.492 +          this.formHistory.update({ op: "remove",
   1.493 +                                    fieldname: this.fieldName,
   1.494 +                                    value: removedEntry.text });
   1.495 +        }
   1.496 +    }
   1.497 +};
   1.498 +
   1.499 +
   1.500 +let remote = Services.appinfo.browserTabsRemote;
   1.501 +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT && remote) {
   1.502 +  // Register the stub FormAutoComplete module in the child which will
   1.503 +  // forward messages to the parent through the process message manager.
   1.504 +  let component = [FormAutoCompleteChild];
   1.505 +  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
   1.506 +} else {
   1.507 +  let component = [FormAutoComplete];
   1.508 +  this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
   1.509 +}

mercurial