1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /* eslint no-unused-vars: ["error", {args: "none"}] */
10 ChromeUtils.defineESModuleGetters(lazy, {
11 ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
12 LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
13 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
16 const gFormFillController = Cc[
17 "@mozilla.org/satchel/form-fill-controller;1"
18 ].getService(Ci.nsIFormFillController);
20 let autoCompleteListeners = new Set();
22 export class AutoCompleteChild extends JSWindowActorChild {
27 this._popupOpen = false;
30 static addPopupStateListener(listener) {
31 autoCompleteListeners.add(listener);
34 static removePopupStateListener(listener) {
35 autoCompleteListeners.delete(listener);
38 receiveMessage(message) {
39 switch (message.name) {
40 case "AutoComplete:HandleEnter": {
41 this.selectedIndex = message.data.selectedIndex;
44 "@mozilla.org/autocomplete/controller;1"
45 ].getService(Ci.nsIAutoCompleteController);
46 controller.handleEnter(message.data.isPopupSelection);
50 case "AutoComplete:PopupClosed": {
51 this._popupOpen = false;
52 this.notifyListeners(message.name, message.data);
56 case "AutoComplete:PopupOpened": {
57 this._popupOpen = true;
58 this.notifyListeners(message.name, message.data);
62 case "AutoComplete:Focus": {
63 // XXX See bug 1582722
64 // Before bug 1573836, the messages here didn't match
65 // ("AutoComplete:Focus" versus "AutoComplete:RequestFocus")
66 // so this was never called. However this._input is actually a
67 // nsIAutoCompleteInput, which doesn't have a focus() method, so it
68 // wouldn't have worked anyway. So for now, I have just disabled this.
79 notifyListeners(messageName, data) {
80 for (let listener of autoCompleteListeners) {
82 listener.popupStateChanged(messageName, data, this.contentWindow);
93 set selectedIndex(index) {
94 this.sendAsyncMessage("AutoComplete:SetSelectedIndex", { index });
98 // selectedIndex getter must be synchronous because we need the
99 // correct value when the controller is in controller::HandleEnter.
100 // We can't easily just let the parent inform us the new value every
101 // time it changes because not every action that can change the
102 // selectedIndex is trivial to catch (e.g. moving the mouse over the
104 let selectedIndexResult = Services.cpmm.sendSyncMessage(
105 "AutoComplete:GetSelectedIndex",
107 browsingContext: this.browsingContext,
112 selectedIndexResult.length != 1 ||
113 !Number.isInteger(selectedIndexResult[0])
115 throw new Error("Invalid autocomplete selectedIndex");
117 return selectedIndexResult[0];
121 return this._popupOpen;
124 openAutocompletePopup(input, element) {
125 if (this._popupOpen || !input || !element?.isConnected) {
129 let rect = lazy.LayoutUtils.getElementBoundingScreenRect(element);
130 let window = element.ownerGlobal;
131 let dir = window.getComputedStyle(element).direction;
132 let results = this.getResultsFromController(input);
133 let formOrigin = lazy.LoginHelper.getLoginOrigin(
134 element.ownerDocument.documentURI
136 let inputElementIdentifier = lazy.ContentDOMReference.get(element);
138 this.sendAsyncMessage("AutoComplete:MaybeOpenPopup", {
142 inputElementIdentifier,
150 // We set this here instead of just waiting for the
151 // PopupClosed message to do it so that we don't end
152 // up in a state where the content thinks that a popup
153 // is open when it isn't (or soon won't be).
154 this._popupOpen = false;
155 this.sendAsyncMessage("AutoComplete:ClosePopup", {});
159 if (this._popupOpen) {
160 let results = this.getResultsFromController(this._input);
161 this.sendAsyncMessage("AutoComplete:Invalidate", { results });
165 selectBy(reverse, page) {
166 Services.cpmm.sendSyncMessage("AutoComplete:SelectBy", {
167 browsingContext: this.browsingContext,
173 getResultsFromController(inputField) {
180 let controller = inputField.controller;
181 if (!(controller instanceof Ci.nsIAutoCompleteController)) {
185 for (let i = 0; i < controller.matchCount; ++i) {
187 result.value = controller.getValueAt(i);
188 result.label = controller.getLabelAt(i);
189 result.comment = controller.getCommentAt(i);
190 result.style = controller.getStyleAt(i);
191 result.image = controller.getImageAt(i);
192 results.push(result);
198 getNoRollupOnEmptySearch(input) {
199 const providers = this.providersByInput(input);
200 return Array.from(providers).find(p => p.actorName == "LoginManager");
203 // Store the input to interested autocomplete providers mapping
204 #providersByInput = new WeakMap();
206 // This functions returns the interested providers that have called
207 // `markAsAutoCompletableField` for the given input and also the hard-coded
208 // autocomplete providers based on input type.
209 providersByInput(input) {
210 const providers = new Set(this.#providersByInput.get(input));
212 if (input.hasBeenTypePassword) {
214 input.ownerGlobal.windowGlobalChild.getActor("LoginManager")
217 // The current design is that FormHisotry doesn't call `markAsAutoCompletable`
218 // for every eligilbe input. Instead, when FormFillController receives a focus event,
219 // it would control the <input> if the <input> is eligible to show form history.
220 // Because of the design, we need to ask FormHistory whether to search for autocomplete entries
221 // for every startSearch call
223 input.ownerGlobal.windowGlobalChild.getActor("FormHistory")
230 * This API should be used by an autocomplete entry provider to mark an input field
231 * as eligible for autocomplete for its type.
232 * When users click on an autocompletable input, we will search autocomplete entries
233 * from all the providers that have called this API for the given <input>.
235 * An autocomplete provider should be a JSWindowActor and implements the following
237 * - string actorName()
238 * - bool shouldSearchForAutoComplete(element);
239 * - jsval getAutoCompleteSearchOption(element);
240 * - jsval searchResultToAutoCompleteResult(searchString, element, record);
241 * See `FormAutofillChild` for example
243 * @param input - The HTML <input> element that is considered autocompletable by the
245 * @param provider - A module that provides autocomplete entries for a <input>, for example,
246 * FormAutofill provides address or credit card autocomplete entries,
247 * LoginManager provides logins entreis.
249 markAsAutoCompletableField(input, provider) {
250 gFormFillController.markAsAutoCompletableField(input);
252 let providers = this.#providersByInput.get(input);
254 providers = new Set();
255 this.#providersByInput.set(input, providers);
257 providers.add(provider);
260 // Record the current ongoing search request. This is used by stopSearch
261 // to prevent notifying the autocomplete controller after receiving search request
262 // results that were issued prior to the call to stop the search.
263 #ongoingSearches = new Set();
265 async startSearch(searchString, input, listener) {
266 // TODO: This should be removed once we implement triggering autocomplete
268 this.lastProfileAutoCompleteFocusedInput = input;
270 // For all the autocomplete entry providers that previsouly marked
271 // this <input> as autocompletable, ask the provider whether we should
272 // search for autocomplete entries in the parent. This is because the current
273 // design doesn't rely on the provider constantly monitor the <input> and
274 // then mark/unmark an input. The provider generally calls the
275 // `markAsAutoCompletbleField` when it sees an <input> is eliglbe for autocomplete.
276 // Here we ask the provider to exam the <input> more detailedly to see
277 // whether we need to search for autocomplete entries at the time users
278 // click on the <input>
279 const providers = this.providersByInput(input);
280 const data = Array.from(providers)
281 .filter(p => p.shouldSearchForAutoComplete(input, searchString))
283 actorName: p.actorName,
284 options: p.getAutoCompleteSearchOption(input, searchString),
289 // We don't return empty result when no provider requests seaching entries in the
290 // parent because for some special cases, the autocomplete entries are coming
291 // from the content. For example, <datalist>.
293 const promise = this.sendQuery("AutoComplete:StartSearch", {
297 this.#ongoingSearches.add(promise);
298 result = await promise.catch(e => {
299 this.#ongoingSearches.delete(promise);
303 // If the search is stopped, don't report back.
304 if (!this.#ongoingSearches.delete(promise)) {
309 for (const provider of providers) {
310 // Search result could be empty. However, an autocomplete provider might
311 // want to show an autoclmplete popup when there is no search result. For example,
312 // <datalist> for FormHisotry, insecure warning for LoginManager.
313 const searchResult = result.find(r => r.actorName == provider.actorName);
314 const acResult = provider.searchResultToAutoCompleteResult(
320 // We have not yet supported showing autocomplete entries from multiple providers,
321 // Note: The prioty is defined in AutoCompleteParent.
323 this.lastProfileAutoCompleteResult = acResult;
324 listener.onSearchCompletion(acResult);
328 this.lastProfileAutoCompleteResult = null;
332 this.lastProfileAutoCompleteResult = null;
333 this.#ongoingSearches.clear();
337 AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([
338 "nsIAutoCompletePopup",