/* 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 https://mozilla.org/MPL/2.0/. */ const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "console", function () { return console.createInstance({ prefix: "SmartFormFillChild" }); }); ChromeUtils.defineLazyGetter(lazy, "formFillController", function () { return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService( Ci.nsIFormFillController ); }); ChromeUtils.defineESModuleGetters(lazy, { FormHistoryAutoCompleteResult: "resource://gre/modules/FormHistoryAutoComplete.sys.mjs", SmartFormFillDocument: "moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillDocument.sys.mjs", }); /** @typedef {import("moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillController.sys.mjs").FillFormResult} FillFormResult */ /** @typedef {import("moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillDocument.sys.mjs").SmartFormFillDocument} SmartFormFillDocument */ /** @typedef {import("moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillDocument.sys.mjs").FormData} FormData */ /** @typedef {import("moz-src:///browser/components/aiwindow/ui/modules/SmartFormFillDocument.sys.mjs").FocusedForm} FocusedForm */ /** * @typedef {{ * data: FillFormResult, * name: "SmartFormFill:FillForm", * } | * { * data?: undefined, * name: "SmartFormFill:GetFocusedForm" * } | * { * data?: undefined, * name: "SmartFormFill:RefreshAutocomplete" * }} SmartFormFillMessage */ /** * Child actor for Smart Form Fill. */ export class SmartFormFillChild extends JSWindowActorChild { /** * Document manager for the current page. * * @type {SmartFormFillDocument | null} */ #smartFormFillDocument = null; /** * Whether the actor has been destroyed. * * @type {boolean} */ #destroyed = false; /** * Current document preparation request. * * @type {Promise | null} */ #documentPreparationPromise = null; /** * Prepares the child actor's document. */ actorCreated() { if (this.document.readyState !== "loading") { this.#prepareDocument(); } } /** * Prepares Smart Form Fill after the document loads. * * @param {Event} event * @returns {Promise} */ async handleEvent(event) { if (event.type !== "DOMContentLoaded") { return; } await this.#prepareDocument(); } /** * Ensures document preparation has completed. * * @returns {Promise} */ #prepareDocument() { if (this.#documentPreparationPromise) { return this.#documentPreparationPromise; } if (this.#smartFormFillDocument || this.#destroyed) { return Promise.resolve(); } const documentPreparationPromise = this.#setUpDocument() .catch(error => { this.#smartFormFillDocument?.destroy(); this.#smartFormFillDocument = null; if (!this.#destroyed) { lazy.console.error( "Smart Form Fill document preparation failed", error ); } }) .finally(() => { if (this.#documentPreparationPromise === documentPreparationPromise) { this.#documentPreparationPromise = null; } }); this.#documentPreparationPromise = documentPreparationPromise; return documentPreparationPromise; } /** * Sets up Smart Form Fill for the current document. * * @returns {Promise} */ async #setUpDocument() { if (this.#destroyed) { return; } const isSmartWindow = await this.sendQuery( "SmartFormFill:IsSmartWindow" ).catch(error => { if (!this.#destroyed) { lazy.console.error("Could not determine if in Smart Window", error); } return false; }); if (!isSmartWindow || this.#destroyed) { return; } this.#smartFormFillDocument = new lazy.SmartFormFillDocument(this.document); await this.#smartFormFillDocument.initialize(this.#onFormUpdate.bind(this)); this.#registerAutocompleteFields(); } /** * Forwards parent messages to the document manager. * * @param {SmartFormFillMessage} message * @returns {Promise} */ async receiveMessage({ data, name }) { if ( !this.#smartFormFillDocument && this.document.readyState !== "loading" ) { await this.#prepareDocument(); } if (!this.#smartFormFillDocument) { return null; } switch (name) { case "SmartFormFill:FillForm": this.#smartFormFillDocument.fillForm(data); return undefined; case "SmartFormFill:GetFocusedForm": return this.#smartFormFillDocument.getFocusedForm(); case "SmartFormFill:RefreshAutocomplete": this.#refreshAutocomplete(); return undefined; } return null; } /** * Destroys document state. */ didDestroy() { this.#smartFormFillDocument?.destroy(); this.#smartFormFillDocument = null; this.#documentPreparationPromise = null; this.#destroyed = true; } /** * Callback for SmartFormFillDocument to use when it * detects a form update * * @param {Array} formDataList */ #onFormUpdate(formDataList) { if (!formDataList || this.#destroyed) { return; } this.#registerAutocompleteFields(); this.sendAsyncMessage("SmartFormFill:FormUpdate", formDataList); } /** * Reruns an open autocomplete search after Smart Form Fill changes state. */ #refreshAutocomplete() { const autocompleteActor = this.manager.getActor("AutoComplete"); const focusedElement = this.document.activeElement; if ( !autocompleteActor?.popupOpen || !this.#smartFormFillDocument?.isSupportedField(focusedElement) || lazy.formFillController.controlledElement !== focusedElement ) { return; } const autocompleteInput = lazy.formFillController.QueryInterface( Ci.nsIAutoCompleteInput ); autocompleteInput.controller.startSearch( autocompleteInput.controller.searchString ); } /* * AutoComplete-related functions */ /** * Registers each supported Smart Form Fill field with AutoCompleteChild. * * Registration allows the form-fill controller to start autocomplete searches * for the field and associates this actor with the field as a result provider. * * Called after document preparation and when tracked forms change. * * @private */ #registerAutocompleteFields() { const autocompleteActor = this.manager.getActor("AutoComplete"); if (!autocompleteActor) { return; } for (const field of this.#smartFormFillDocument.getSupportedFields()) { autocompleteActor.markAsAutoCompletableField(field, this); } } /** * Determines whether AutoCompleteChild should request Smart Form Fill results * for the focused input. * * AutoCompleteChild calls this before sending an autocomplete search to the * parent process. * * @param {HTMLInputElement | HTMLTextAreaElement} input * The input associated with the autocomplete search. * @returns {boolean} * Whether Smart Form Fill supports the input. */ shouldSearchForAutoComplete(input) { return this.#smartFormFillDocument?.isSupportedField(input) ?? false; } /** * Provides the options object required by the autocomplete provider contract. * * @returns {object} Empty provider options. */ getAutoCompleteSearchOption() { return {}; } /** * Converts results returned by SmartFormFillParent into an * nsIAutoCompleteResult that AutoCompleteChild can give to the autocomplete * controller. * * AutoCompleteChild calls this after the parent-process search completes. * * @param {string} searchString * The current value used for the autocomplete search. * @param {HTMLInputElement | HTMLTextAreaElement} input * The input associated with the search. * @param {{ entries: Array } | null | undefined} records * Results returned by SmartFormFillParent.searchAutoCompleteEntries(). * @returns {FormHistoryAutoCompleteResult | null} * The autocomplete result, or null when no Smart Form Fill entry is available. */ searchResultToAutoCompleteResult(searchString, input, records) { if (!records?.entries?.length) { return null; } const result = new lazy.FormHistoryAutoCompleteResult( input, [], input.name, searchString ); result.externalEntries.push(...records.entries); return result; } /** * Name used by AutoCompleteChild to identify this autocomplete provider and * route searches to the corresponding parent actor. * * @returns {string} */ get actorName() { return "SmartFormFill"; } }