summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs')
-rw-r--r--toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs489
1 files changed, 13 insertions, 476 deletions
diff --git a/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs b/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs
index 2799d2e955..3bdff897d5 100644
--- a/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs
+++ b/toolkit/components/satchel/FormHistoryAutoComplete.sys.mjs
@@ -3,187 +3,12 @@
* 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/. */
-import {
- GenericAutocompleteItem,
- sendFillRequestToParent,
-} from "resource://gre/modules/FillHelpers.sys.mjs";
-
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
-});
+import { sendFillRequestToParent } from "resource://gre/modules/FillHelpers.sys.mjs";
const formFillController = Cc[
"@mozilla.org/satchel/form-fill-controller;1"
].getService(Ci.nsIFormFillController);
-function isAutocompleteDisabled(aField) {
- if (!aField) {
- return false;
- }
-
- if (aField.autocomplete !== "") {
- return aField.autocomplete === "off";
- }
-
- return aField.form?.autocomplete === "off";
-}
-
-/**
- * An abstraction to talk with the FormHistory database over
- * the message layer. FormHistoryClient will take care of
- * figuring out the most appropriate message manager to use,
- * and what things to send.
- *
- * It is assumed that FormHistoryAutoComplete will only ever use
- * one instance at a time, and will not attempt to perform more
- * than one search request with the same instance at a time.
- * However, FormHistoryAutoComplete might call remove() any number of
- * times with the same instance of the client.
- *
- * @param {object} clientInfo
- * Info required to build the FormHistoryClient
- * @param {Node} clientInfo.formField
- * A DOM node that we're requesting form history for.
- * @param {string} clientInfo.inputName
- * The name of the input to do the FormHistory look-up with.
- * If this is searchbar-history, then formField needs to be null,
- * otherwise constructing will throw.
- */
-export class FormHistoryClient {
- constructor({ formField, inputName }) {
- if (formField) {
- if (inputName == this.SEARCHBAR_ID) {
- throw new Error(
- "FormHistoryClient constructed with both a formField and an inputName. " +
- "This is not supported, and only empty results will be returned."
- );
- }
- const window = formField.ownerGlobal;
- this.windowGlobal = window.windowGlobalChild;
- }
-
- this.inputName = inputName;
- this.id = FormHistoryClient.nextRequestID++;
- }
-
- static nextRequestID = 1;
- SEARCHBAR_ID = "searchbar-history";
- cancelled = false;
- inputName = "";
-
- getActor() {
- return this.windowGlobal?.getActor("FormHistory");
- }
-
- /**
- * Query FormHistory for some results.
- *
- * @param {string} searchString
- * The string to search FormHistory for. See
- * FormHistory.getAutoCompleteResults.
- * @param {object} params
- * An Object with search properties. See
- * FormHistory.getAutoCompleteResults.
- * @param {string} scenarioName
- * Optional autocompletion scenario name.
- * @param {Function} callback
- * A callback function that will take a single
- * argument (the found entries).
- */
- requestAutoCompleteResults(searchString, params, scenarioName, callback) {
- this.cancelled = false;
-
- // Use the actor if possible, otherwise for the searchbar,
- // use the more roundabout per-process message manager which has
- // no sendQuery method.
- const actor = this.getActor();
- if (actor) {
- actor
- .sendQuery("FormHistory:AutoCompleteSearchAsync", {
- searchString,
- params,
- scenarioName,
- })
- .then(
- results => this.handleAutoCompleteResults(results, callback),
- () => this.cancel()
- );
- } else {
- this.callback = callback;
- Services.cpmm.addMessageListener(
- "FormHistory:AutoCompleteSearchResults",
- this
- );
- Services.cpmm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
- id: this.id,
- searchString,
- params,
- scenarioName,
- });
- }
- }
-
- handleAutoCompleteResults(results, callback) {
- if (this.cancelled) {
- return;
- }
-
- if (!callback) {
- console.error("FormHistoryClient received response with no callback");
- return;
- }
-
- callback(results);
- this.cancel();
- }
-
- /**
- * Cancel an in-flight results request. This ensures that the
- * callback that requestAutoCompleteResults was passed is never
- * called from this FormHistoryClient.
- */
- cancel() {
- if (this.callback) {
- Services.cpmm.removeMessageListener(
- "FormHistory:AutoCompleteSearchResults",
- this
- );
- this.callback = null;
- }
- this.cancelled = true;
- }
-
- /**
- * Remove an item from FormHistory.
- *
- * @param {string} value
- *
- * The value to remove for this particular
- * field.
- *
- * @param {string} guid
- *
- * The guid for the item being removed.
- */
- remove(value, guid) {
- const actor = this.getActor() || Services.cpmm;
- actor.sendAsyncMessage("FormHistory:RemoveEntry", {
- inputName: this.inputName,
- value,
- guid,
- });
- }
-
- receiveMessage(msg) {
- const { id, results } = msg.data;
- if (id == this.id) {
- this.handleAutoCompleteResults(results, this.callback);
- }
- }
-}
-
/**
* This autocomplete result combines 3 arrays of entries, fixedEntries and
* externalEntries.
@@ -197,10 +22,10 @@ export class FormHistoryClient {
* @implements {nsIAutoCompleteResult}
*/
export class FormHistoryAutoCompleteResult {
- constructor(client, entries, fieldName, searchString) {
- this.client = client;
+ constructor(input, entries, inputName, searchString) {
+ this.input = input;
this.entries = entries;
- this.fieldName = fieldName;
+ this.inputName = inputName;
this.searchString = searchString;
}
@@ -210,9 +35,9 @@ export class FormHistoryAutoCompleteResult {
]);
// private
- client = null;
+ input = null;
entries = null;
- fieldName = null;
+ inputName = null;
#fixedEntries = [];
externalEntries = [];
@@ -221,68 +46,6 @@ export class FormHistoryAutoCompleteResult {
this.removeDuplicateHistoryEntries();
}
- canSearchIncrementally(searchString) {
- const prevSearchString = this.searchString.trim();
- return (
- prevSearchString.length > 1 &&
- searchString.includes(prevSearchString.toLowerCase())
- );
- }
-
- incrementalSearch(searchString) {
- this.searchString = searchString;
- searchString = searchString.trim().toLowerCase();
- this.#fixedEntries = this.#fixedEntries.filter(item =>
- item.label.toLowerCase().includes(searchString)
- );
-
- const searchTokens = searchString.split(/\s+/);
- // We have a list of results for a shorter search string, so just
- // filter them further based on the new search string and add to a new array.
- let filteredEntries = [];
- for (const entry of this.entries) {
- // Remove results that do not contain the token
- // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
- if (searchTokens.some(tok => !entry.textLowerCase.includes(tok))) {
- continue;
- }
- this.#calculateScore(entry, searchString, searchTokens);
- filteredEntries.push(entry);
- }
- filteredEntries.sort((a, b) => b.totalScore - a.totalScore);
- this.entries = filteredEntries;
- this.removeDuplicateHistoryEntries();
- }
-
- /*
- * #calculateScore
- *
- * entry -- an nsIAutoCompleteResult entry
- * aSearchString -- current value of the input (lowercase)
- * searchTokens -- array of tokens of the search string
- *
- * Returns: an int
- */
- #calculateScore(entry, aSearchString, searchTokens) {
- let boundaryCalc = 0;
- // for each word, calculate word boundary weights
- for (const token of searchTokens) {
- if (entry.textLowerCase.startsWith(token)) {
- boundaryCalc++;
- }
- if (entry.textLowerCase.includes(" " + token)) {
- boundaryCalc++;
- }
- }
- boundaryCalc = boundaryCalc * this._boundaryWeight;
- // now add more weight if we have a traditional prefix match and
- // multiply boundary bonuses by boundary weight
- if (entry.textLowerCase.startsWith(aSearchString)) {
- boundaryCalc += this._prefixWeight;
- }
- entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
- }
-
/**
* Remove items from history list that are already present in fixed list.
* We do this rather than the opposite ( i.e. remove items from fixed list)
@@ -389,7 +152,13 @@ export class FormHistoryAutoCompleteResult {
removeValueAt(index) {
if (this.#isFormHistoryEntry(index)) {
const [removedEntry] = this.entries.splice(index, 1);
- this.client.remove(removedEntry.text, removedEntry.guid);
+ const actor =
+ this.input.ownerGlobal.windowGlobalChild.getActor("FormHistory");
+ actor.sendAsyncMessage("FormHistory:RemoveEntry", {
+ inputName: this.inputName,
+ value: removedEntry.text,
+ guid: removedEntry.guid,
+ });
}
}
@@ -400,13 +169,6 @@ export class FormHistoryAutoCompleteResult {
export class FormHistoryAutoComplete {
constructor() {
- // Preferences. Add observer so we get notified of changes.
- this._prefBranch = Services.prefs.getBranch("browser.formfill.");
- this._prefBranch.addObserver("", this.observer, true);
- this.observer._self = this;
-
- this._debug = this._prefBranch.getBoolPref("debug");
- this._enabled = this._prefBranch.getBoolPref("enable");
Services.obs.addObserver(this, "autocomplete-will-enter-text");
}
@@ -416,237 +178,12 @@ export class FormHistoryAutoComplete {
"nsISupportsWeakReference",
]);
- // Only one query via FormHistoryClient is performed at a time, and the
- // most recent FormHistoryClient which will be stored in _pendingClient
- // while the query is being performed. It will be cleared when the query
- // finishes, is cancelled, or an error occurs. If a new query occurs while
- // one is already pending, the existing one is cancelled.
- #pendingClient = null;
-
- observer = {
- _self: null,
-
- QueryInterface: ChromeUtils.generateQI([
- "nsIObserver",
- "nsISupportsWeakReference",
- ]),
-
- observe(_subject, topic, data) {
- const self = this._self;
-
- if (topic == "nsPref:changed") {
- const prefName = data;
- self.log(`got change to ${prefName} preference`);
-
- switch (prefName) {
- case "debug":
- self._debug = self._prefBranch.getBoolPref(prefName);
- break;
- case "enable":
- self._enabled = self._prefBranch.getBoolPref(prefName);
- break;
- }
- }
- },
- };
-
// AutoCompleteE10S needs to be able to call autoCompleteSearchAsync without
// going through IDL in order to pass a mock DOM object field.
get wrappedJSObject() {
return this;
}
- /*
- * log
- *
- * Internal function for logging debug messages to the Error Console
- * window
- */
- log(message) {
- if (!this._debug) {
- return;
- }
- Services.console.logStringMessage("FormHistoryAutoComplete: " + message);
- }
-
- /*
- * autoCompleteSearchAsync
- *
- * aInputName -- |name| or |id| attribute value from the form input being
- * autocompleted
- * aUntrimmedSearchString -- current value of the input
- * aField -- HTMLInputElement being autocompleted (may be null if from chrome)
- * aPreviousResult -- previous search result, if any.
- * aAddDataList -- add results from list=datalist for aField.
- * aListener -- nsIFormHistoryAutoCompleteObserver that listens for the nsIAutoCompleteResult
- * that may be returned asynchronously.
- */
- autoCompleteSearchAsync(
- aInputName,
- aUntrimmedSearchString,
- aField,
- aPreviousResult,
- aAddDataList,
- aListener
- ) {
- // Guard against void DOM strings filtering into this code.
- if (typeof aInputName === "object") {
- aInputName = "";
- }
- if (typeof aUntrimmedSearchString === "object") {
- aUntrimmedSearchString = "";
- }
-
- const client = new FormHistoryClient({
- formField: aField,
- inputName: aInputName,
- });
-
- function reportSearchResult(result) {
- aListener?.onSearchCompletion(result);
- }
-
- // If we have datalist results, they become our "empty" result.
- const result = new FormHistoryAutoCompleteResult(
- client,
- [],
- aInputName,
- aUntrimmedSearchString
- );
-
- if (aAddDataList) {
- result.fixedEntries = this.getDataListSuggestions(aField);
- }
-
- if (!this._enabled) {
- reportSearchResult(result);
- return;
- }
-
- // Don't allow form inputs (aField != null) to get results from
- // search bar history.
- if (aInputName == "searchbar-history" && aField) {
- this.log(`autoCompleteSearch for input name "${aInputName}" is denied`);
- reportSearchResult(result);
- return;
- }
-
- if (isAutocompleteDisabled(aField)) {
- this.log("autoCompleteSearch not allowed due to autcomplete=off");
- reportSearchResult(result);
- return;
- }
-
- const searchString = aUntrimmedSearchString.trim().toLowerCase();
- const prevResult = aPreviousResult?.wrappedJSObject;
- if (prevResult?.canSearchIncrementally(searchString)) {
- this.log("Using previous autocomplete result");
- prevResult.incrementalSearch(aUntrimmedSearchString);
- reportSearchResult(prevResult);
- } else {
- this.log("Creating new autocomplete search result.");
- this.getAutoCompleteValues(
- client,
- aInputName,
- searchString,
- lazy.FormScenarios.detect({ input: aField }).signUpForm
- ? "SignUpFormScenario"
- : "",
- ({ formHistoryEntries, externalEntries }) => {
- formHistoryEntries ??= [];
- externalEntries ??= [];
-
- if (aField?.maxLength > -1) {
- result.entries = formHistoryEntries.filter(
- el => el.text.length <= aField.maxLength
- );
- } else {
- result.entries = formHistoryEntries;
- }
-
- result.externalEntries.push(
- ...externalEntries.map(
- entry =>
- new GenericAutocompleteItem(
- entry.image,
- entry.title,
- entry.subtitle,
- entry.fillMessageName,
- entry.fillMessageData
- )
- )
- );
-
- result.removeDuplicateHistoryEntries();
- reportSearchResult(result);
- }
- );
- }
- }
-
- getDataListSuggestions(aField) {
- const items = [];
-
- if (!aField?.list) {
- return items;
- }
-
- const upperFieldValue = aField.value.toUpperCase();
-
- for (const option of aField.list.options) {
- const label = option.label || option.text || option.value || "";
-
- if (!label.toUpperCase().includes(upperFieldValue)) {
- continue;
- }
-
- items.push({
- label,
- value: option.value,
- });
- }
-
- return items;
- }
-
- stopAutoCompleteSearch() {
- if (this.#pendingClient) {
- this.#pendingClient.cancel();
- this.#pendingClient = null;
- }
- }
-
- /*
- * Get the values for an autocomplete list given a search string.
- *
- * client - a FormHistoryClient instance to perform the search with
- * fieldname - fieldname field within form history (the form input name)
- * searchString - string to search for
- * scenarioName - Optional autocompletion scenario name.
- * callback - called when the values are available. Passed an array of objects,
- * containing properties for each result. The callback is only called
- * when successful.
- */
- getAutoCompleteValues(
- client,
- fieldname,
- searchString,
- scenarioName,
- callback
- ) {
- this.stopAutoCompleteSearch();
- client.requestAutoCompleteResults(
- searchString,
- { fieldname },
- scenarioName,
- entries => {
- this.#pendingClient = null;
- callback(entries);
- }
- );
- this.#pendingClient = client;
- }
-
async observe(subject, topic, data) {
switch (topic) {
case "autocomplete-will-enter-text": {