Source code
Revision control
Copy as Markdown
Other Tools
/* 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
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<void> | 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<void>}
*/
async handleEvent(event) {
if (event.type !== "DOMContentLoaded") {
return;
}
await this.#prepareDocument();
}
/**
* Ensures document preparation has completed.
*
* @returns {Promise<void>}
*/
#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<void>}
*/
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<FocusedForm | null | undefined>}
*/
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<FormData>} 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<object> } | 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";
}
}