mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-04 02:57:38 +00:00
d8ef09459f
The new architecture no longer has the notion of an "active" section. When autocomplete is triggered, we pass the "element id" of the triggered element and use this id to indicate which form we are currently processing. Because of this change, we no longer need the code that deals with an active section, field, etc. Differential Revision: https://phabricator.services.mozilla.com/D211553
317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* 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/. */
|
|
|
|
/* eslint no-unused-vars: ["error", {args: "none"}] */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
|
|
LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
|
|
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
|
});
|
|
|
|
const gFormFillController = Cc[
|
|
"@mozilla.org/satchel/form-fill-controller;1"
|
|
].getService(Ci.nsIFormFillController);
|
|
|
|
export class AutoCompleteChild extends JSWindowActorChild {
|
|
constructor() {
|
|
super();
|
|
|
|
this._input = null;
|
|
this._popupOpen = false;
|
|
}
|
|
|
|
receiveMessage(message) {
|
|
switch (message.name) {
|
|
case "AutoComplete:HandleEnter": {
|
|
this.selectedIndex = message.data.selectedIndex;
|
|
|
|
let controller = Cc[
|
|
"@mozilla.org/autocomplete/controller;1"
|
|
].getService(Ci.nsIAutoCompleteController);
|
|
controller.handleEnter(message.data.isPopupSelection);
|
|
break;
|
|
}
|
|
|
|
case "AutoComplete:PopupClosed": {
|
|
this._popupOpen = false;
|
|
break;
|
|
}
|
|
|
|
case "AutoComplete:PopupOpened": {
|
|
this._popupOpen = true;
|
|
break;
|
|
}
|
|
|
|
case "AutoComplete:Focus": {
|
|
// XXX See bug 1582722
|
|
// Before bug 1573836, the messages here didn't match
|
|
// ("AutoComplete:Focus" versus "AutoComplete:RequestFocus")
|
|
// so this was never called. However this._input is actually a
|
|
// nsIAutoCompleteInput, which doesn't have a focus() method, so it
|
|
// wouldn't have worked anyway. So for now, I have just disabled this.
|
|
/*
|
|
if (this._input) {
|
|
this._input.focus();
|
|
}
|
|
*/
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
get input() {
|
|
return this._input;
|
|
}
|
|
|
|
set selectedIndex(index) {
|
|
this.sendAsyncMessage("AutoComplete:SetSelectedIndex", { index });
|
|
}
|
|
|
|
get selectedIndex() {
|
|
// selectedIndex getter must be synchronous because we need the
|
|
// correct value when the controller is in controller::HandleEnter.
|
|
// We can't easily just let the parent inform us the new value every
|
|
// time it changes because not every action that can change the
|
|
// selectedIndex is trivial to catch (e.g. moving the mouse over the
|
|
// list).
|
|
let selectedIndexResult = Services.cpmm.sendSyncMessage(
|
|
"AutoComplete:GetSelectedIndex",
|
|
{
|
|
browsingContext: this.browsingContext,
|
|
}
|
|
);
|
|
|
|
if (
|
|
selectedIndexResult.length != 1 ||
|
|
!Number.isInteger(selectedIndexResult[0])
|
|
) {
|
|
throw new Error("Invalid autocomplete selectedIndex");
|
|
}
|
|
return selectedIndexResult[0];
|
|
}
|
|
|
|
get popupOpen() {
|
|
return this._popupOpen;
|
|
}
|
|
|
|
openAutocompletePopup(input, element) {
|
|
if (this._popupOpen || !input || !element?.isConnected) {
|
|
return;
|
|
}
|
|
|
|
let rect = lazy.LayoutUtils.getElementBoundingScreenRect(element);
|
|
let window = element.ownerGlobal;
|
|
let dir = window.getComputedStyle(element).direction;
|
|
let results = this.getResultsFromController(input);
|
|
let formOrigin = lazy.LoginHelper.getLoginOrigin(
|
|
element.ownerDocument.documentURI
|
|
);
|
|
let inputElementIdentifier = lazy.ContentDOMReference.get(element);
|
|
|
|
this.sendAsyncMessage("AutoComplete:MaybeOpenPopup", {
|
|
results,
|
|
rect,
|
|
dir,
|
|
inputElementIdentifier,
|
|
formOrigin,
|
|
});
|
|
|
|
this._input = input;
|
|
}
|
|
|
|
closePopup() {
|
|
// We set this here instead of just waiting for the
|
|
// PopupClosed message to do it so that we don't end
|
|
// up in a state where the content thinks that a popup
|
|
// is open when it isn't (or soon won't be).
|
|
this._popupOpen = false;
|
|
this.sendAsyncMessage("AutoComplete:ClosePopup", {});
|
|
}
|
|
|
|
invalidate() {
|
|
if (this._popupOpen) {
|
|
let results = this.getResultsFromController(this._input);
|
|
this.sendAsyncMessage("AutoComplete:Invalidate", { results });
|
|
}
|
|
}
|
|
|
|
selectBy(reverse, page) {
|
|
Services.cpmm.sendSyncMessage("AutoComplete:SelectBy", {
|
|
browsingContext: this.browsingContext,
|
|
reverse,
|
|
page,
|
|
});
|
|
}
|
|
|
|
getResultsFromController(inputField) {
|
|
let results = [];
|
|
|
|
if (!inputField) {
|
|
return results;
|
|
}
|
|
|
|
let controller = inputField.controller;
|
|
if (!(controller instanceof Ci.nsIAutoCompleteController)) {
|
|
return results;
|
|
}
|
|
|
|
for (let i = 0; i < controller.matchCount; ++i) {
|
|
let result = {};
|
|
result.value = controller.getValueAt(i);
|
|
result.label = controller.getLabelAt(i);
|
|
result.comment = controller.getCommentAt(i);
|
|
result.style = controller.getStyleAt(i);
|
|
result.image = controller.getImageAt(i);
|
|
results.push(result);
|
|
}
|
|
|
|
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 FormHistory 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) {
|
|
// 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 FormHistory, 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) {
|
|
listener.onSearchCompletion(acResult);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
stopSearch() {
|
|
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([
|
|
"nsIAutoCompletePopup",
|
|
]);
|