summaryrefslogtreecommitdiffstats
path: root/toolkit/actors
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/actors')
-rw-r--r--toolkit/actors/AutoCompleteChild.sys.mjs153
-rw-r--r--toolkit/actors/AutoCompleteParent.sys.mjs107
2 files changed, 253 insertions, 7 deletions
diff --git a/toolkit/actors/AutoCompleteChild.sys.mjs b/toolkit/actors/AutoCompleteChild.sys.mjs
index 3694e497d5..61d48e9124 100644
--- a/toolkit/actors/AutoCompleteChild.sys.mjs
+++ b/toolkit/actors/AutoCompleteChild.sys.mjs
@@ -13,6 +13,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
});
+const gFormFillController = Cc[
+ "@mozilla.org/satchel/form-fill-controller;1"
+].getService(Ci.nsIFormFillController);
+
let autoCompleteListeners = new Set();
export class AutoCompleteChild extends JSWindowActorChild {
@@ -137,6 +141,7 @@ export class AutoCompleteChild extends JSWindowActorChild {
dir,
inputElementIdentifier,
formOrigin,
+ actorName: this.lastAutoCompleteProviderName,
});
this._input = input;
@@ -190,6 +195,154 @@ export class AutoCompleteChild extends JSWindowActorChild {
return results;
}
+
+ getNoRollupOnEmptySearch(input) {
+ const providers = this.providersByInput(input);
+ return Array.from(providers).find(p => p.actorName == "LoginManager");
+ }
+
+ // Store the input to interested autocomplete providers mapping
+ #providersByInput = new WeakMap();
+
+ // This functions returns the interested providers that have called
+ // `markAsAutoCompletableField` for the given input and also the hard-coded
+ // autocomplete providers based on input type.
+ providersByInput(input) {
+ const providers = new Set(this.#providersByInput.get(input));
+
+ if (input.hasBeenTypePassword) {
+ providers.add(
+ input.ownerGlobal.windowGlobalChild.getActor("LoginManager")
+ );
+ } else {
+ // The current design is that FormHisotry doesn't call `markAsAutoCompletable`
+ // for every eligilbe input. Instead, when FormFillController receives a focus event,
+ // it would control the <input> if the <input> is eligible to show form history.
+ // Because of the design, we need to ask FormHistory whether to search for autocomplete entries
+ // for every startSearch call
+ providers.add(
+ input.ownerGlobal.windowGlobalChild.getActor("FormHistory")
+ );
+ }
+ return providers;
+ }
+
+ /**
+ * This API should be used by an autocomplete entry provider to mark an input field
+ * as eligible for autocomplete for its type.
+ * When users click on an autocompletable input, we will search autocomplete entries
+ * from all the providers that have called this API for the given <input>.
+ *
+ * An autocomplete provider should be a JSWindowActor and implements the following
+ * functions:
+ * - string actorName()
+ * - bool shouldSearchForAutoComplete(element);
+ * - jsval getAutoCompleteSearchOption(element);
+ * - jsval searchResultToAutoCompleteResult(searchString, element, record);
+ * See `FormAutofillChild` for example
+ *
+ * @param input - The HTML <input> element that is considered autocompletable by the
+ * given provider
+ * @param provider - A module that provides autocomplete entries for a <input>, for example,
+ * FormAutofill provides address or credit card autocomplete entries,
+ * LoginManager provides logins entreis.
+ */
+ markAsAutoCompletableField(input, provider) {
+ gFormFillController.markAsAutoCompletableField(input);
+
+ let providers = this.#providersByInput.get(input);
+ if (!providers) {
+ providers = new Set();
+ this.#providersByInput.set(input, providers);
+ }
+ providers.add(provider);
+ }
+
+ // Record the current ongoing search request. This is used by stopSearch
+ // to prevent notifying the autocomplete controller after receiving search request
+ // results that were issued prior to the call to stop the search.
+ #ongoingSearches = new Set();
+
+ async startSearch(searchString, input, listener) {
+ // TODO: This should be removed once we implement triggering autocomplete
+ // from the parent.
+ this.lastProfileAutoCompleteFocusedInput = input;
+
+ // For all the autocomplete entry providers that previsouly marked
+ // this <input> as autocompletable, ask the provider whether we should
+ // search for autocomplete entries in the parent. This is because the current
+ // design doesn't rely on the provider constantly monitor the <input> and
+ // then mark/unmark an input. The provider generally calls the
+ // `markAsAutoCompletbleField` when it sees an <input> is eliglbe for autocomplete.
+ // Here we ask the provider to exam the <input> more detailedly to see
+ // whether we need to search for autocomplete entries at the time users
+ // click on the <input>
+ const providers = this.providersByInput(input);
+ const data = Array.from(providers)
+ .filter(p => p.shouldSearchForAutoComplete(input, searchString))
+ .map(p => ({
+ actorName: p.actorName,
+ options: p.getAutoCompleteSearchOption(input, searchString),
+ }));
+
+ let result = [];
+
+ // We don't return empty result when no provider requests seaching entries in the
+ // parent because for some special cases, the autocomplete entries are coming
+ // from the content. For example, <datalist>.
+ if (data.length) {
+ const promise = this.sendQuery("AutoComplete:StartSearch", {
+ searchString,
+ data,
+ });
+ this.#ongoingSearches.add(promise);
+ result = await promise.catch(e => {
+ this.#ongoingSearches.delete(promise);
+ });
+ result ||= [];
+
+ // If the search is stopped, don't report back.
+ if (!this.#ongoingSearches.delete(promise)) {
+ return;
+ }
+ }
+
+ for (const provider of providers) {
+ // Search result could be empty. However, an autocomplete provider might
+ // want to show an autocomplete popup when there is no search result. For example,
+ // <datalist> for FormHisotry, insecure warning for LoginManager.
+ const searchResult = result.find(r => r.actorName == provider.actorName);
+ const acResult = provider.searchResultToAutoCompleteResult(
+ searchString,
+ input,
+ searchResult
+ );
+
+ // We have not yet supported showing autocomplete entries from multiple providers,
+ // Note: The prioty is defined in AutoCompleteParent.
+ if (acResult) {
+ // `lastAutoCompleteProviderName` should be removed once we implement
+ // the mapping of autocomplete entry to provider in the parent process.
+ this.lastAutoCompleteProviderName = provider.actorName;
+
+ this.lastProfileAutoCompleteResult = acResult;
+ listener.onSearchCompletion(acResult);
+ return;
+ }
+ }
+ this.lastProfileAutoCompleteResult = null;
+ }
+
+ stopSearch() {
+ this.lastProfileAutoCompleteResult = null;
+ this.#ongoingSearches.clear();
+ }
+
+ selectEntry() {
+ // we don't need to pass the selected index to the parent process because
+ // the selected index is maintained in the parent.
+ this.sendAsyncMessage("AutoComplete:SelectEntry");
+ }
}
AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([
diff --git a/toolkit/actors/AutoCompleteParent.sys.mjs b/toolkit/actors/AutoCompleteParent.sys.mjs
index 70cbcea44d..4e0b030211 100644
--- a/toolkit/actors/AutoCompleteParent.sys.mjs
+++ b/toolkit/actors/AutoCompleteParent.sys.mjs
@@ -143,9 +143,13 @@ var AutoCompleteResultView = {
this.results = [];
},
- setResults(actor, results) {
+ setResults(actor, results, providerActorName) {
this.currentActor = actor;
this.results = results;
+
+ if (providerActorName) {
+ this.providerActor = actor.manager.getActor(providerActorName);
+ }
},
};
@@ -203,7 +207,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
}
- showPopupWithResults({ rect, dir, results }) {
+ showPopupWithResults({ rect, dir, results, actorName }) {
if (!results.length || this.openedPopup) {
// We shouldn't ever be showing an empty popup, and if we
// already have a popup open, the old one needs to close before
@@ -237,7 +241,8 @@ export class AutoCompleteParent extends JSWindowActorParent {
);
this.openedPopup.style.direction = dir;
- AutoCompleteResultView.setResults(this, results);
+ AutoCompleteResultView.setResults(this, results, actorName);
+
this.openedPopup.view = AutoCompleteResultView;
this.openedPopup.selectedIndex = -1;
@@ -376,7 +381,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
}
- receiveMessage(message) {
+ async receiveMessage(message) {
let browser = this.browsingContext.top.embedderElement;
if (
@@ -394,6 +399,13 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
switch (message.name) {
+ case "AutoComplete:SelectEntry": {
+ if (this.openedPopup) {
+ this.autofillProfile(this.openedPopup.selectedIndex);
+ }
+ break;
+ }
+
case "AutoComplete:SetSelectedIndex": {
let { index } = message.data;
if (this.openedPopup) {
@@ -403,8 +415,14 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
case "AutoComplete:MaybeOpenPopup": {
- let { results, rect, dir, inputElementIdentifier, formOrigin } =
- message.data;
+ let {
+ results,
+ rect,
+ dir,
+ inputElementIdentifier,
+ formOrigin,
+ actorName,
+ } = message.data;
if (lazy.DELEGATE_AUTOCOMPLETE) {
lazy.GeckoViewAutocomplete.delegateSelection({
browsingContext: this.browsingContext,
@@ -413,7 +431,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
formOrigin,
});
} else {
- this.showPopupWithResults({ results, rect, dir });
+ this.showPopupWithResults({ results, rect, dir, actorName });
this.notifyListeners();
}
break;
@@ -433,6 +451,12 @@ export class AutoCompleteParent extends JSWindowActorParent {
this.closePopup();
break;
}
+
+ case "AutoComplete:StartSearch": {
+ const { searchString, data } = message.data;
+ const result = await this.#startSearch(searchString, data);
+ return Promise.resolve(result);
+ }
}
// Returning false to pacify ESLint, but this return value is
// ignored by the messaging infrastructure.
@@ -493,8 +517,77 @@ export class AutoCompleteParent extends JSWindowActorParent {
}
}
+ // This defines the supported autocomplete providers and the prioity to show the autocomplete
+ // entry.
+ #AUTOCOMPLETE_PROVIDERS = ["FormAutofill", "LoginManager", "FormHistory"];
+
+ /**
+ * Search across multiple module to gather autocomplete entries for a given search string.
+ *
+ * @param {string} searchString
+ * The input string used to query autocomplete entries across different
+ * autocomplete providers.
+ * @param {Array<Object>} providers
+ * An array of objects where each object has a `name` used to identify the actor
+ * name of the provider and `options` that are passed to the `searchAutoCompleteEntries`
+ * method of the actor.
+ * @returns {Array<Object>} An array of results objects with `name` of the provider and `entries`
+ * that are returned from the provider module's `searchAutoCompleteEntries` method.
+ */
+ async #startSearch(searchString, providers) {
+ for (const name of this.#AUTOCOMPLETE_PROVIDERS) {
+ const provider = providers.find(p => p.actorName == name);
+ if (!provider) {
+ continue;
+ }
+ const { actorName, options } = provider;
+ const actor =
+ this.browsingContext.currentWindowGlobal.getActor(actorName);
+ const entries = await actor?.searchAutoCompleteEntries(
+ searchString,
+ options
+ );
+
+ // We have not yet supported showing autocomplete entries from multiple providers,
+ if (entries) {
+ return [{ actorName, ...entries }];
+ }
+ }
+ return [];
+ }
+
stopSearch() {}
+ previewAutofillProfile(index) {
+ const actor = AutoCompleteResultView.providerActor;
+ if (!actor) {
+ return;
+ }
+
+ // Clear preview when the selected index is not valid
+ if (index < 0) {
+ actor.previewFields(null);
+ return;
+ }
+
+ const result = AutoCompleteResultView.results[index];
+ actor.previewFields(result);
+ }
+
+ /**
+ * When a field is autocompleted, fill relevant fields
+ */
+ autofillProfile(index) {
+ // Find the provider of this autocomplete
+ const actor = AutoCompleteResultView.providerActor;
+ if (index < 0 || !actor) {
+ return;
+ }
+
+ const result = AutoCompleteResultView.results[index];
+ actor.autofillFields(result);
+ }
+
/**
* Sends a message to the browser that is requesting the input
* that the open popup should be focused.