summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/content
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/content')
-rw-r--r--browser/extensions/formautofill/content/addressFormLayout.mjs187
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js640
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.mjs288
-rw-r--r--browser/extensions/formautofill/content/editAddress.xhtml94
-rw-r--r--browser/extensions/formautofill/content/editCreditCard.xhtml52
-rw-r--r--browser/extensions/formautofill/content/editDialog.mjs (renamed from browser/extensions/formautofill/content/editDialog.js)54
-rw-r--r--browser/extensions/formautofill/content/manageAddresses.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageCreditCards.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageDialog.mjs (renamed from browser/extensions/formautofill/content/manageDialog.js)80
9 files changed, 617 insertions, 800 deletions
diff --git a/browser/extensions/formautofill/content/addressFormLayout.mjs b/browser/extensions/formautofill/content/addressFormLayout.mjs
new file mode 100644
index 0000000000..5e48e6afaa
--- /dev/null
+++ b/browser/extensions/formautofill/content/addressFormLayout.mjs
@@ -0,0 +1,187 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+// Defines template descriptors for generating elements in convertLayoutToUI.
+const fieldTemplates = {
+ commonAttributes(item) {
+ return {
+ id: item.fieldId,
+ name: item.fieldId,
+ required: item.required,
+ value: item.value ?? "",
+ };
+ },
+ input(item) {
+ return {
+ tag: "input",
+ type: item.type ?? "text",
+ ...this.commonAttributes(item),
+ };
+ },
+ textarea(item) {
+ return {
+ tag: "textarea",
+ ...this.commonAttributes(item),
+ };
+ },
+ select(item) {
+ return {
+ tag: "select",
+ children: item.options.map(({ value, text }) => ({
+ tag: "option",
+ selected: value === item.value,
+ value,
+ text,
+ })),
+ ...this.commonAttributes(item),
+ };
+ },
+};
+
+/**
+ * Creates an HTML element with specified attributes and children.
+ *
+ * @param {string} tag - Tag name for the element to create.
+ * @param {object} options - Options object containing attributes and children.
+ * @param {object} options.attributes - Element's Attributes/Props (id, class, etc.)
+ * @param {Array} options.children - Element's children (array of objects with tag and options).
+ * @returns {HTMLElement} The newly created element.
+ */
+const createElement = (tag, { children = [], ...attributes }) => {
+ const element = document.createElement(tag);
+
+ for (let [attributeName, attributeValue] of Object.entries(attributes)) {
+ if (attributeName in element) {
+ element[attributeName] = attributeValue;
+ } else {
+ element.setAttribute(attributeName, attributeValue);
+ }
+ }
+
+ for (let { tag: childTag, ...childRest } of children) {
+ element.appendChild(createElement(childTag, childRest));
+ }
+
+ return element;
+};
+
+/**
+ * Generator that creates UI elements from `fields` object, using localization from `l10nStrings`.
+ *
+ * @param {Array} fields - Array of objects as returned from `FormAutofillUtils.getFormLayout`.
+ * @param {object} l10nStrings - Key-value pairs for field label localization.
+ * @yields {HTMLElement} - A localized label element with constructed from a field.
+ */
+function* convertLayoutToUI(fields, l10nStrings) {
+ for (const item of fields) {
+ // eslint-disable-next-line no-nested-ternary
+ const fieldTag = item.options
+ ? "select"
+ : item.multiline
+ ? "textarea"
+ : "input";
+
+ const fieldUI = {
+ label: {
+ id: `${item.fieldId}-container`,
+ class: `container ${item.newLine ? "new-line" : ""}`,
+ },
+ field: fieldTemplates[fieldTag](item),
+ span: {
+ class: "label-text",
+ textContent: l10nStrings[item.l10nId] ?? "",
+ },
+ };
+
+ const label = createElement("label", fieldUI.label);
+ const { tag, ...rest } = fieldUI.field;
+ const field = createElement(tag, rest);
+ label.appendChild(field);
+ const span = createElement("span", fieldUI.span);
+ label.appendChild(span);
+
+ yield label;
+ }
+}
+
+/**
+ * Retrieves the current form data from the current form element on the page.
+ *
+ * @returns {object} An object containing key-value pairs of form data.
+ */
+export const getCurrentFormData = () => {
+ const formElement = document.querySelector("form");
+ const formData = new FormData(formElement);
+ return Object.fromEntries(formData.entries());
+};
+
+/**
+ * Checks if the form can be submitted based on the number of non-empty values.
+ * TODO(Bug 1891734): Add address validation. Right now we don't do any validation. (2 fields mimics the old behaviour ).
+ *
+ * @returns {boolean} True if the form can be submitted
+ */
+export const canSubmitForm = () => {
+ const formData = getCurrentFormData();
+ const validValues = Object.values(formData).filter(Boolean);
+ return validValues.length >= 2;
+};
+
+/**
+ * Generates a form layout based on record data and localization strings.
+ *
+ * @param {HTMLFormElement} formElement - Target form element.
+ * @param {object} record - Address record, includes at least country code defaulted to FormAutofill.DEFAULT_REGION.
+ * @param {object} l10nStrings - Localization strings map.
+ */
+export const createFormLayoutFromRecord = (
+ formElement,
+ record = { country: lazy.FormAutofill.DEFAULT_REGION },
+ l10nStrings = {}
+) => {
+ // Always clear select values because they are not persisted between countries.
+ // For example from US with state NY, we don't want the address-level1 to be NY
+ // when changing to another country that doesn't have state options
+ const selects = formElement.querySelectorAll("select:not(#country)");
+ for (const select of selects) {
+ select.value = "";
+ }
+
+ // Get old data to persist before clearing form
+ const formData = getCurrentFormData();
+ record = {
+ ...record,
+ ...formData,
+ };
+
+ formElement.innerHTML = "";
+ const fields = lazy.FormAutofillUtils.getFormLayout(record);
+
+ const layoutGenerator = convertLayoutToUI(fields, l10nStrings);
+
+ for (const fieldElement of layoutGenerator) {
+ formElement.appendChild(fieldElement);
+ }
+
+ document.querySelector("#country").addEventListener(
+ "change",
+ ev =>
+ // Allow some time for the user to type
+ // before we set the new country and re-render
+ setTimeout(() => {
+ record.country = ev.target.value;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
+ }, 300),
+ { once: true }
+ );
+
+ // Used to notify tests that the form has been updated and is ready
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
+};
diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js
deleted file mode 100644
index 290b436a64..0000000000
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ /dev/null
@@ -1,640 +0,0 @@
-/* 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/. */
-
-/* exported EditAddress, EditCreditCard */
-/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-
-"use strict";
-
-const { FormAutofill } = ChromeUtils.importESModule(
- "resource://autofill/FormAutofill.sys.mjs"
-);
-const { FormAutofillUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
-);
-
-class EditAutofillForm {
- constructor(elements) {
- this._elements = elements;
- }
-
- /**
- * Fill the form with a record object.
- *
- * @param {object} [record = {}]
- */
- loadRecord(record = {}) {
- for (let field of this._elements.form.elements) {
- let value = record[field.id];
- value = typeof value == "undefined" ? "" : value;
-
- if (record.guid) {
- field.value = value;
- } else if (field.localName == "select") {
- this.setDefaultSelectedOptionByValue(field, value);
- } else {
- // Use .defaultValue instead of .value to avoid setting the `dirty` flag
- // which triggers form validation UI.
- field.defaultValue = value;
- }
- }
- if (!record.guid) {
- // Reset the dirty value flag and validity state.
- this._elements.form.reset();
- } else {
- for (let field of this._elements.form.elements) {
- this.updatePopulatedState(field);
- this.updateCustomValidity(field);
- }
- }
- }
-
- setDefaultSelectedOptionByValue(select, value) {
- for (let option of select.options) {
- option.defaultSelected = option.value == value;
- }
- }
-
- /**
- * Get a record from the form suitable for a save/update in storage.
- *
- * @returns {object}
- */
- buildFormObject() {
- let initialObject = {};
- if (this.hasMailingAddressFields) {
- // Start with an empty string for each mailing-address field so that any
- // fields hidden for the current country are blanked in the return value.
- initialObject = {
- "street-address": "",
- "address-level3": "",
- "address-level2": "",
- "address-level1": "",
- "postal-code": "",
- };
- }
-
- return Array.from(this._elements.form.elements).reduce((obj, input) => {
- if (!input.disabled) {
- obj[input.id] = input.value;
- }
- return obj;
- }, initialObject);
- }
-
- /**
- * Handle events
- *
- * @param {DOMEvent} event
- */
- handleEvent(event) {
- switch (event.type) {
- case "change": {
- this.handleChange(event);
- break;
- }
- case "input": {
- this.handleInput(event);
- break;
- }
- }
- }
-
- /**
- * Handle change events
- *
- * @param {DOMEvent} event
- */
- handleChange(event) {
- this.updatePopulatedState(event.target);
- }
-
- /**
- * Handle input events
- */
- handleInput(_e) {}
-
- /**
- * Attach event listener
- */
- attachEventListeners() {
- this._elements.form.addEventListener("input", this);
- }
-
- /**
- * Set the field-populated attribute if the field has a value.
- *
- * @param {DOMElement} field The field that will be checked for a value.
- */
- updatePopulatedState(field) {
- let span = field.parentNode.querySelector(".label-text");
- if (!span) {
- return;
- }
- span.toggleAttribute("field-populated", !!field.value.trim());
- }
-
- /**
- * Run custom validity routines specific to the field and type of form.
- *
- * @param {DOMElement} _field The field that will be validated.
- */
- updateCustomValidity(_field) {}
-}
-
-class EditAddress extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record
- * @param {object} config
- * @param {boolean} [config.noValidate=undefined] Whether to validate the form
- */
- constructor(elements, record, config) {
- super(elements);
-
- Object.assign(this, config);
- let { form } = this._elements;
- Object.assign(this._elements, {
- addressLevel3Label: form.querySelector(
- "#address-level3-container > .label-text"
- ),
- addressLevel2Label: form.querySelector(
- "#address-level2-container > .label-text"
- ),
- addressLevel1Label: form.querySelector(
- "#address-level1-container > .label-text"
- ),
- postalCodeLabel: form.querySelector(
- "#postal-code-container > .label-text"
- ),
- country: form.querySelector("#country"),
- });
-
- this.populateCountries();
- // Need to populate the countries before trying to set the initial country.
- // Also need to use this._record so it has the default country selected.
- this.loadRecord(record);
- this.attachEventListeners();
-
- form.noValidate = !!config.noValidate;
- }
-
- loadRecord(record) {
- this._record = record;
- if (!record) {
- record = {
- country: FormAutofill.DEFAULT_REGION,
- };
- }
-
- let { addressLevel1Options } = FormAutofillUtils.getFormFormat(
- record.country
- );
- this.populateAddressLevel1(addressLevel1Options, record.country);
-
- super.loadRecord(record);
- this.loadAddressLevel1(record["address-level1"], record.country);
- this.formatForm(record.country);
- }
-
- get hasMailingAddressFields() {
- let { addressFields } = this._elements.form.dataset;
- return (
- !addressFields ||
- addressFields.trim().split(/\s+/).includes("mailing-address")
- );
- }
-
- /**
- * `mailing-address` is a special attribute token to indicate mailing fields + country.
- *
- * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
- * @param {string} addressFields - white-space-separated string of requested address fields to show
- * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
- */
- static computeVisibleFields(mailingFieldsOrder, addressFields) {
- if (addressFields) {
- let requestedFieldClasses = addressFields.trim().split(/\s+/);
- let fieldClasses = [];
- if (requestedFieldClasses.includes("mailing-address")) {
- fieldClasses = fieldClasses.concat(mailingFieldsOrder);
- // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
- requestedFieldClasses.splice(
- requestedFieldClasses.indexOf("mailing-address"),
- 1,
- "country"
- );
- }
-
- for (let fieldClassName of requestedFieldClasses) {
- fieldClasses.push({
- fieldId: fieldClassName,
- newLine: fieldClassName == "name",
- });
- }
- return fieldClasses;
- }
-
- // This is the default which is shown in the management interface and includes all fields.
- return mailingFieldsOrder.concat([
- {
- fieldId: "country",
- },
- {
- fieldId: "tel",
- },
- {
- fieldId: "email",
- newLine: true,
- },
- ]);
- }
-
- /**
- * Format the form based on country. The address-level1 and postal-code labels
- * should be specific to the given country.
- *
- * @param {string} country
- */
- formatForm(country) {
- const {
- addressLevel3L10nId,
- addressLevel2L10nId,
- addressLevel1L10nId,
- addressLevel1Options,
- postalCodeL10nId,
- fieldsOrder: mailingFieldsOrder,
- postalCodePattern,
- countryRequiredFields,
- } = FormAutofillUtils.getFormFormat(country);
-
- document.l10n.setAttributes(
- this._elements.addressLevel3Label,
- addressLevel3L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel2Label,
- addressLevel2L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel1Label,
- addressLevel1L10nId
- );
- document.l10n.setAttributes(
- this._elements.postalCodeLabel,
- postalCodeL10nId
- );
- let addressFields = this._elements.form.dataset.addressFields;
- let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
- let fieldClasses = EditAddress.computeVisibleFields(
- mailingFieldsOrder,
- addressFields
- );
- let requiredFields = new Set(countryRequiredFields);
- if (extraRequiredFields) {
- for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
- requiredFields.add(extraRequiredField);
- }
- }
- this.arrangeFields(fieldClasses, requiredFields);
- this.updatePostalCodeValidation(postalCodePattern);
- this.populateAddressLevel1(addressLevel1Options, country);
- }
-
- /**
- * Update address field visibility and order based on libaddressinput data.
- *
- * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
- * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
- */
- arrangeFields(fieldsOrder, requiredFields) {
- /**
- * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
- */
- let fields = [
- // `name` is a wrapper for the 3 name fields.
- "name",
- "organization",
- "street-address",
- "address-level3",
- "address-level2",
- "address-level1",
- "postal-code",
- "country",
- "tel",
- "email",
- ];
- let inputs = [];
- for (let i = 0; i < fieldsOrder.length; i++) {
- let { fieldId, newLine } = fieldsOrder[i];
-
- let container = this._elements.form.querySelector(
- `#${fieldId}-container`
- );
- let containerInputs = [
- ...container.querySelectorAll("input, textarea, select"),
- ];
- containerInputs.forEach(function (input) {
- input.disabled = false;
- // libaddressinput doesn't list 'country' or 'name' as required.
- input.required =
- fieldId == "country" ||
- fieldId == "name" ||
- requiredFields.has(fieldId);
- });
- inputs.push(...containerInputs);
- container.style.display = "flex";
- container.style.order = i;
- container.style.pageBreakAfter = newLine ? "always" : "auto";
- // Remove the field from the list of fields
- fields.splice(fields.indexOf(fieldId), 1);
- }
- for (let i = 0; i < inputs.length; i++) {
- // Assign tabIndex starting from 1
- inputs[i].tabIndex = i + 1;
- }
- // Hide the remaining fields
- for (let field of fields) {
- let container = this._elements.form.querySelector(`#${field}-container`);
- container.style.display = "none";
- for (let input of [
- ...container.querySelectorAll("input, textarea, select"),
- ]) {
- input.disabled = true;
- }
- }
- }
-
- updatePostalCodeValidation(postalCodePattern) {
- let postalCodeInput = this._elements.form.querySelector("#postal-code");
- if (postalCodePattern && postalCodeInput.style.display != "none") {
- postalCodeInput.setAttribute("pattern", postalCodePattern);
- } else {
- postalCodeInput.removeAttribute("pattern");
- }
- }
-
- /**
- * Set the address-level1 value on the form field (input or select, whichever is present).
- *
- * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
- * @param {string} country The corresponding country
- */
- loadAddressLevel1(addressLevel1Value, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.localName == "input") {
- field.value = addressLevel1Value || "";
- return;
- }
-
- let matchedSelectOption = FormAutofillUtils.findAddressSelectOption(
- field,
- {
- country,
- "address-level1": addressLevel1Value,
- },
- "address-level1"
- );
- if (matchedSelectOption && !matchedSelectOption.selected) {
- field.value = matchedSelectOption.value;
- field.dispatchEvent(new Event("input", { bubbles: true }));
- field.dispatchEvent(new Event("change", { bubbles: true }));
- } else if (addressLevel1Value) {
- // If the option wasn't found, insert an option at the beginning of
- // the select that matches the stored value.
- field.insertBefore(
- new Option(addressLevel1Value, addressLevel1Value, true, true),
- field.firstChild
- );
- }
- }
-
- /**
- * Replace the text input for address-level1 with a select dropdown if
- * a fixed set of names exists. Otherwise show a text input.
- *
- * @param {Map?} options Map of options with regionCode -> name mappings
- * @param {string} country The corresponding country
- */
- populateAddressLevel1(options, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.dataset.country == country) {
- return;
- }
-
- if (!options) {
- if (field.localName == "input") {
- return;
- }
-
- let input = document.createElement("input");
- input.setAttribute("type", "text");
- input.id = "address-level1";
- input.required = field.required;
- input.disabled = field.disabled;
- input.tabIndex = field.tabIndex;
- field.replaceWith(input);
- return;
- }
-
- if (field.localName == "input") {
- let select = document.createElement("select");
- select.id = "address-level1";
- select.required = field.required;
- select.disabled = field.disabled;
- select.tabIndex = field.tabIndex;
- field.replaceWith(select);
- field = select;
- }
-
- field.textContent = "";
- field.dataset.country = country;
- let fragment = document.createDocumentFragment();
- fragment.appendChild(new Option(undefined, undefined, true, true));
- for (let [regionCode, regionName] of options) {
- let option = new Option(regionName, regionCode);
- fragment.appendChild(option);
- }
- field.appendChild(fragment);
- }
-
- populateCountries() {
- let fragment = document.createDocumentFragment();
- // Sort countries by their visible names.
- let countries = [...FormAutofill.countries.entries()].sort((e1, e2) =>
- e1[1].localeCompare(e2[1])
- );
- for (let [country] of countries) {
- const countryName = Services.intl.getRegionDisplayNames(undefined, [
- country.toLowerCase(),
- ]);
- const option = new Option(countryName, country);
- fragment.appendChild(option);
- }
- this._elements.country.appendChild(fragment);
- }
-
- handleChange(event) {
- if (event.target == this._elements.country) {
- this.formatForm(event.target.value);
- }
- super.handleChange(event);
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-}
-
-class EditCreditCard extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record with a decrypted cc-number
- * @param {object} addresses in an object with guid keys for the billing address picker.
- */
- constructor(elements, record, addresses) {
- super(elements);
-
- this._addresses = addresses;
- Object.assign(this._elements, {
- ccNumber: this._elements.form.querySelector("#cc-number"),
- invalidCardNumberStringElement: this._elements.form.querySelector(
- "#invalidCardNumberString"
- ),
- month: this._elements.form.querySelector("#cc-exp-month"),
- year: this._elements.form.querySelector("#cc-exp-year"),
- billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
- billingAddressRow:
- this._elements.form.querySelector(".billingAddressRow"),
- });
-
- this.attachEventListeners();
- this.loadRecord(record, addresses);
- }
-
- loadRecord(record, addresses, preserveFieldValues) {
- // _record must be updated before generateYears and generateBillingAddressOptions are called.
- this._record = record;
- this._addresses = addresses;
- this.generateBillingAddressOptions(preserveFieldValues);
- if (!preserveFieldValues) {
- // Re-generating the months will reset the selected option.
- this.generateMonths();
- // Re-generating the years will reset the selected option.
- this.generateYears();
- super.loadRecord(record);
- }
- }
-
- generateMonths() {
- const count = 12;
-
- // Clear the list
- this._elements.month.textContent = "";
-
- // Empty month option
- this._elements.month.appendChild(new Option());
-
- // Populate month list. Format: "month number - month name"
- let dateFormat = new Intl.DateTimeFormat(navigator.language, {
- month: "long",
- }).format;
- for (let i = 0; i < count; i++) {
- let monthNumber = (i + 1).toString();
- let monthName = dateFormat(new Date(1970, i));
- let option = new Option();
- option.value = monthNumber;
- // XXX: Bug 1446164 - Localize this string.
- option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
- this._elements.month.appendChild(option);
- }
- }
-
- generateYears() {
- const count = 11;
- const currentYear = new Date().getFullYear();
- const ccExpYear = this._record && this._record["cc-exp-year"];
-
- // Clear the list
- this._elements.year.textContent = "";
-
- // Provide an empty year option
- this._elements.year.appendChild(new Option());
-
- if (ccExpYear && ccExpYear < currentYear) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
-
- for (let i = 0; i < count; i++) {
- let year = currentYear + i;
- let option = new Option(year);
- this._elements.year.appendChild(option);
- }
-
- if (ccExpYear && ccExpYear > currentYear + count) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
- }
-
- generateBillingAddressOptions(preserveFieldValues) {
- let billingAddressGUID;
- if (preserveFieldValues && this._elements.billingAddress.value) {
- billingAddressGUID = this._elements.billingAddress.value;
- } else if (this._record) {
- billingAddressGUID = this._record.billingAddressGUID;
- }
-
- this._elements.billingAddress.textContent = "";
-
- this._elements.billingAddress.appendChild(new Option("", ""));
-
- let hasAddresses = false;
- for (let [guid, address] of Object.entries(this._addresses)) {
- hasAddresses = true;
- let selected = guid == billingAddressGUID;
- let option = new Option(
- FormAutofillUtils.getAddressLabel(address),
- guid,
- selected,
- selected
- );
- this._elements.billingAddress.appendChild(option);
- }
-
- this._elements.billingAddressRow.hidden = !hasAddresses;
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-
- handleInput(event) {
- // Clear the error message if cc-number is valid
- if (
- event.target == this._elements.ccNumber &&
- FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
- ) {
- this._elements.ccNumber.setCustomValidity("");
- }
- super.handleInput(event);
- }
-
- updateCustomValidity(field) {
- super.updateCustomValidity(field);
-
- // Mark the cc-number field as invalid if the number is empty or invalid.
- if (
- field == this._elements.ccNumber &&
- !FormAutofillUtils.isCCNumber(field.value)
- ) {
- let invalidCardNumberString =
- this._elements.invalidCardNumberStringElement.textContent;
- field.setCustomValidity(invalidCardNumberString || " ");
- }
- }
-}
diff --git a/browser/extensions/formautofill/content/autofillEditForms.mjs b/browser/extensions/formautofill/content/autofillEditForms.mjs
new file mode 100644
index 0000000000..ca74850acd
--- /dev/null
+++ b/browser/extensions/formautofill/content/autofillEditForms.mjs
@@ -0,0 +1,288 @@
+/* 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-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+class EditAutofillForm {
+ constructor(elements) {
+ this._elements = elements;
+ }
+
+ /**
+ * Fill the form with a record object.
+ *
+ * @param {object} [record = {}]
+ */
+ loadRecord(record = {}) {
+ for (let field of this._elements.form.elements) {
+ let value = record[field.id];
+ value = typeof value == "undefined" ? "" : value;
+
+ if (record.guid) {
+ field.value = value;
+ } else if (field.localName == "select") {
+ this.setDefaultSelectedOptionByValue(field, value);
+ } else {
+ // Use .defaultValue instead of .value to avoid setting the `dirty` flag
+ // which triggers form validation UI.
+ field.defaultValue = value;
+ }
+ }
+ if (!record.guid) {
+ // Reset the dirty value flag and validity state.
+ this._elements.form.reset();
+ } else {
+ for (let field of this._elements.form.elements) {
+ this.updatePopulatedState(field);
+ this.updateCustomValidity(field);
+ }
+ }
+ }
+
+ setDefaultSelectedOptionByValue(select, value) {
+ for (let option of select.options) {
+ option.defaultSelected = option.value == value;
+ }
+ }
+
+ /**
+ * Get a record from the form suitable for a save/update in storage.
+ *
+ * @returns {object}
+ */
+ buildFormObject() {
+ let initialObject = {};
+ if (this.hasMailingAddressFields) {
+ // Start with an empty string for each mailing-address field so that any
+ // fields hidden for the current country are blanked in the return value.
+ initialObject = {
+ "street-address": "",
+ "address-level3": "",
+ "address-level2": "",
+ "address-level1": "",
+ "postal-code": "",
+ };
+ }
+
+ return Array.from(this._elements.form.elements).reduce((obj, input) => {
+ if (!input.disabled) {
+ obj[input.id] = input.value;
+ }
+ return obj;
+ }, initialObject);
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "change": {
+ this.handleChange(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle change events
+ *
+ * @param {DOMEvent} event
+ */
+ handleChange(event) {
+ this.updatePopulatedState(event.target);
+ }
+
+ /**
+ * Handle input events
+ */
+ handleInput(_e) {}
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ this._elements.form.addEventListener("input", this);
+ }
+
+ /**
+ * Set the field-populated attribute if the field has a value.
+ *
+ * @param {DOMElement} field The field that will be checked for a value.
+ */
+ updatePopulatedState(field) {
+ let span = field.parentNode.querySelector(".label-text");
+ if (!span) {
+ return;
+ }
+ span.toggleAttribute("field-populated", !!field.value.trim());
+ }
+
+ /**
+ * Run custom validity routines specific to the field and type of form.
+ *
+ * @param {DOMElement} _field The field that will be validated.
+ */
+ updateCustomValidity(_field) {}
+}
+
+export class EditCreditCard extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record with a decrypted cc-number
+ * @param {object} addresses in an object with guid keys for the billing address picker.
+ */
+ constructor(elements, record, addresses) {
+ super(elements);
+
+ this._addresses = addresses;
+ Object.assign(this._elements, {
+ ccNumber: this._elements.form.querySelector("#cc-number"),
+ invalidCardNumberStringElement: this._elements.form.querySelector(
+ "#invalidCardNumberString"
+ ),
+ month: this._elements.form.querySelector("#cc-exp-month"),
+ year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow:
+ this._elements.form.querySelector(".billingAddressRow"),
+ });
+
+ this.attachEventListeners();
+ this.loadRecord(record, addresses);
+ }
+
+ loadRecord(record, addresses, preserveFieldValues) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
+ this._record = record;
+ this._addresses = addresses;
+ this.generateBillingAddressOptions(preserveFieldValues);
+ if (!preserveFieldValues) {
+ // Re-generating the months will reset the selected option.
+ this.generateMonths();
+ // Re-generating the years will reset the selected option.
+ this.generateYears();
+ super.loadRecord(record);
+ }
+ }
+
+ generateMonths() {
+ const count = 12;
+
+ // Clear the list
+ this._elements.month.textContent = "";
+
+ // Empty month option
+ this._elements.month.appendChild(new Option());
+
+ // Populate month list. Format: "month number - month name"
+ let dateFormat = new Intl.DateTimeFormat(navigator.language, {
+ month: "long",
+ }).format;
+ for (let i = 0; i < count; i++) {
+ let monthNumber = (i + 1).toString();
+ let monthName = dateFormat(new Date(1970, i));
+ let option = new Option();
+ option.value = monthNumber;
+ // XXX: Bug 1446164 - Localize this string.
+ option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+ this._elements.month.appendChild(option);
+ }
+ }
+
+ generateYears() {
+ const count = 11;
+ const currentYear = new Date().getFullYear();
+ const ccExpYear = this._record && this._record["cc-exp-year"];
+
+ // Clear the list
+ this._elements.year.textContent = "";
+
+ // Provide an empty year option
+ this._elements.year.appendChild(new Option());
+
+ if (ccExpYear && ccExpYear < currentYear) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+
+ for (let i = 0; i < count; i++) {
+ let year = currentYear + i;
+ let option = new Option(year);
+ this._elements.year.appendChild(option);
+ }
+
+ if (ccExpYear && ccExpYear > currentYear + count) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+ }
+
+ generateBillingAddressOptions(preserveFieldValues) {
+ let billingAddressGUID;
+ if (preserveFieldValues && this._elements.billingAddress.value) {
+ billingAddressGUID = this._elements.billingAddress.value;
+ } else if (this._record) {
+ billingAddressGUID = this._record.billingAddressGUID;
+ }
+
+ this._elements.billingAddress.textContent = "";
+
+ this._elements.billingAddress.appendChild(new Option("", ""));
+
+ let hasAddresses = false;
+ for (let [guid, address] of Object.entries(this._addresses)) {
+ hasAddresses = true;
+ let selected = guid == billingAddressGUID;
+ let option = new Option(
+ lazy.FormAutofillUtils.getAddressLabel(address),
+ guid,
+ selected,
+ selected
+ );
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+
+ handleInput(event) {
+ // Clear the error message if cc-number is valid
+ if (
+ event.target == this._elements.ccNumber &&
+ lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
+ ) {
+ this._elements.ccNumber.setCustomValidity("");
+ }
+ super.handleInput(event);
+ }
+
+ updateCustomValidity(field) {
+ super.updateCustomValidity(field);
+
+ // Mark the cc-number field as invalid if the number is empty or invalid.
+ if (
+ field == this._elements.ccNumber &&
+ !lazy.FormAutofillUtils.isCCNumber(field.value)
+ ) {
+ let invalidCardNumberString =
+ this._elements.invalidCardNumberStringElement.textContent;
+ field.setCustomValidity(invalidCardNumberString || " ");
+ }
+ }
+}
diff --git a/browser/extensions/formautofill/content/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml
index 47ae4a2a3b..a23fa5ab8c 100644
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -19,65 +19,13 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
<script
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
</head>
<body>
- <form id="form" class="editAddressForm" autocomplete="off">
- <!--
- The <span class="label-text" …/> needs to be after the form field in the same element in
- order to get proper label styling with :focus and :user-invalid
- -->
- <label id="name-container" class="container">
- <input id="name" type="text" required="required" />
- <span data-l10n-id="autofill-address-name" class="label-text" />
- </label>
- <label id="organization-container" class="container">
- <input id="organization" type="text" />
- <span data-l10n-id="autofill-address-organization" class="label-text" />
- </label>
- <label id="street-address-container" class="container">
- <textarea id="street-address" rows="3" />
- <span data-l10n-id="autofill-address-street" class="label-text" />
- </label>
- <label id="address-level3-container" class="container">
- <input id="address-level3" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level2-container" class="container">
- <input id="address-level2" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level1-container" class="container">
- <!-- The address-level1 input will get replaced by a select dropdown
- by autofillEditForms.js when the selected country has provided
- specific options. -->
- <input id="address-level1" type="text" />
- <span class="label-text" />
- </label>
- <label id="postal-code-container" class="container">
- <input id="postal-code" type="text" />
- <span class="label-text" />
- </label>
- <label id="country-container" class="container">
- <select id="country" required="required">
- <option />
- </select>
- <span data-l10n-id="autofill-address-country" class="label-text" />
- </label>
- <label id="tel-container" class="container">
- <input id="tel" type="tel" dir="auto" />
- <span data-l10n-id="autofill-address-tel" class="label-text" />
- </label>
- <label id="email-container" class="container">
- <input id="email" type="email" required="required" />
- <span data-l10n-id="autofill-address-email" class="label-text" />
- </label>
- </form>
+ <form id="form" class="editAddressForm" autocomplete="off"></form>
<div id="controls-container">
<span
id="country-warning-message"
@@ -88,31 +36,25 @@
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</moz-button-group>
</div>
- <script>
- <![CDATA[
- "use strict";
+ <!-- eslint-disable -->
+ <script type="module">
+ import { createFormLayoutFromRecord } from "chrome://formautofill/content/addressFormLayout.mjs";
+ import { EditAddressDialog } from "chrome://formautofill/content/editDialog.mjs";
- const {
- record,
- noValidate,
- } = window.arguments?.[0] ?? {};
+ const { record, noValidate, l10nStrings } = window.arguments?.[0] ?? {};
+ const formElement = document.querySelector("form");
+ formElement.noValidate = !!noValidate;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditAddress({
- form: document.getElementById("form"),
- }, record, {
- noValidate,
- });
-
- /* import-globals-from editDialog.js */
- new EditAddressDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- ]]>
+ new EditAddressDialog(
+ {
+ title: document.querySelector("title"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editCreditCard.xhtml b/browser/extensions/formautofill/content/editCreditCard.xhtml
index 920be841c5..8fceb5709b 100644
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -19,8 +19,6 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
</head>
<body>
<form id="form" class="editCreditCardForm contentPane" autocomplete="off">
@@ -87,36 +85,32 @@
<button id="cancel" data-l10n-id="autofill-cancel-button" />
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</div>
- <script>
- <![CDATA[
- "use strict";
- /* import-globals-from editDialog.js */
+ <!-- eslint-disable -->
+ <script type="module">
+ import { EditCreditCardDialog } from "chrome://formautofill/content/editDialog.mjs";
+ import { EditCreditCard } from "chrome://formautofill/content/autofillEditForms.mjs";
+ const { record } = window.arguments?.[0] ?? {};
- (async () => {
- const {
- record,
- } = window.arguments?.[0] ?? {};
+ const fieldContainer = new EditCreditCard(
+ {
+ form: document.getElementById("form"),
+ },
+ record,
+ []
+ );
- const addresses = {};
- for (let address of await formAutofillStorage.addresses.getAll()) {
- addresses[address.guid] = address;
- }
-
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditCreditCard({
- form: document.getElementById("form"),
- }, record, addresses);
-
- new EditCreditCardDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- })();
- ]]>
+ new EditCreditCardDialog(
+ {
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editDialog.js b/browser/extensions/formautofill/content/editDialog.mjs
index 467acbdd07..5371051e12 100644
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/editDialog.mjs
@@ -2,24 +2,27 @@
* 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/. */
-/* exported EditAddressDialog, EditCreditCardDialog */
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-"use strict";
+import {
+ getCurrentFormData,
+ canSubmitForm,
+} from "chrome://formautofill/content/addressFormLayout.mjs";
-ChromeUtils.defineESModuleGetters(this, {
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
});
class AutofillEditDialog {
constructor(subStorageName, elements, record) {
- this._storageInitPromise = formAutofillStorage.initialize();
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
this._subStorageName = subStorageName;
this._elements = elements;
this._record = record;
this.localizeDocument();
- window.addEventListener("DOMContentLoaded", this, { once: true });
+ window.addEventListener("load", this, { once: true });
}
async init() {
@@ -28,7 +31,7 @@ class AutofillEditDialog {
// For testing only: signal to tests that the dialog is ready for testing.
// This is likely no longer needed since retrieving from storage is fully
// handled in manageDialog.js now.
- window.dispatchEvent(new CustomEvent("FormReady"));
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
}
/**
@@ -38,7 +41,7 @@ class AutofillEditDialog {
*/
async getStorage() {
await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
+ return lazy.formAutofillStorage[this._subStorageName];
}
/**
@@ -63,7 +66,7 @@ class AutofillEditDialog {
*/
handleEvent(event) {
switch (event.type) {
- case "DOMContentLoaded": {
+ case "load": {
this.init();
break;
}
@@ -139,7 +142,8 @@ class AutofillEditDialog {
attachEventListeners() {
window.addEventListener("keypress", this);
window.addEventListener("contextmenu", this);
- this._elements.controlsContainer.addEventListener("click", this);
+ this._elements.save.addEventListener("click", this);
+ this._elements.cancel.addEventListener("click", this);
document.addEventListener("input", this);
}
@@ -148,17 +152,20 @@ class AutofillEditDialog {
recordFormSubmit() {
let method = this._record?.guid ? "edit" : "add";
- AutofillTelemetry.recordManageEvent(this.telemetryType, method);
+ lazy.AutofillTelemetry.recordManageEvent(this.telemetryType, method);
}
}
-class EditAddressDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.ADDRESS;
+export class EditAddressDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.ADDRESS;
constructor(elements, record) {
super("addresses", elements, record);
if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
}
}
@@ -171,9 +178,19 @@ class EditAddressDialog extends AutofillEditDialog {
}
}
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!canSubmitForm()) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
async handleSubmit() {
await this.saveRecord(
- this._elements.fieldContainer.buildFormObject(),
+ getCurrentFormData(),
this._record ? this._record.guid : null
);
this.recordFormSubmit();
@@ -182,8 +199,8 @@ class EditAddressDialog extends AutofillEditDialog {
}
}
-class EditCreditCardDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.CREDIT_CARD;
+export class EditCreditCardDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.CREDIT_CARD;
constructor(elements, record) {
elements.fieldContainer._elements.billingAddress.disabled = true;
@@ -193,7 +210,10 @@ class EditCreditCardDialog extends AutofillEditDialog {
this._onCCNumberFieldBlur.bind(this)
);
if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
}
}
diff --git a/browser/extensions/formautofill/content/manageAddresses.xhtml b/browser/extensions/formautofill/content/manageAddresses.xhtml
index 68e810179e..2c8f0608f7 100644
--- a/browser/extensions/formautofill/content/manageAddresses.xhtml
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -16,7 +16,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -39,9 +38,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageAddresses */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- even though type is set to module.-->
+ <script type="module">
+ import { ManageAddresses } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageAddresses({
records: document.getElementById("addresses"),
controlsContainer: document.getElementById("controls-container"),
@@ -50,5 +52,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageCreditCards.xhtml b/browser/extensions/formautofill/content/manageCreditCards.xhtml
index e7baf9d364..69aae82df9 100644
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -18,7 +18,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -41,9 +40,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageCreditCards */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- eventhough type is set to module -->
+ <script type="module">
+ import { ManageCreditCards } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageCreditCards({
records: document.getElementById("credit-cards"),
controlsContainer: document.getElementById("controls-container"),
@@ -52,5 +54,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageDialog.js b/browser/extensions/formautofill/content/manageDialog.mjs
index ad5cefbb15..bca0f48f40 100644
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.mjs
@@ -2,10 +2,6 @@
* 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/. */
-/* exported ManageAddresses, ManageCreditCards */
-
-"use strict";
-
const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
const EDIT_CREDIT_CARD_URL =
"chrome://formautofill/content/editCreditCard.xhtml";
@@ -20,38 +16,44 @@ const { AutofillTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/shared/AutofillTelemetry.sys.mjs"
);
-ChromeUtils.defineESModuleGetters(this, {
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
});
-this.log = null;
-ChromeUtils.defineLazyGetter(this, "log", () =>
- FormAutofill.defineLogGetter(this, "manageAddresses")
+ChromeUtils.defineLazyGetter(lazy, "log", () =>
+ FormAutofill.defineLogGetter(lazy, "manageAddresses")
+);
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization([" browser/preferences/formAutofill.ftl"], true)
);
class ManageRecords {
constructor(subStorageName, elements) {
- this._storageInitPromise = formAutofillStorage.initialize();
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
this._subStorageName = subStorageName;
this._elements = elements;
this._newRequest = false;
this._isLoadingRecords = false;
this.prefWin = window.opener;
- window.addEventListener("DOMContentLoaded", this, { once: true });
+ window.addEventListener("load", this, { once: true });
}
async init() {
await this.loadRecords();
this.attachEventListeners();
// For testing only: Notify when the dialog is ready for interaction
- window.dispatchEvent(new CustomEvent("FormReady"));
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
}
uninit() {
- log.debug("uninit");
+ lazy.log.debug("uninit");
this.detachEventListeners();
this._elements = null;
}
@@ -72,7 +74,7 @@ class ManageRecords {
*/
async getStorage() {
await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
+ return lazy.formAutofillStorage[this._subStorageName];
}
/**
@@ -146,9 +148,9 @@ class ManageRecords {
* Remove all existing record elements.
*/
clearRecordElements() {
- let parent = this._elements.records;
- while (parent.lastChild) {
- parent.removeChild(parent.lastChild);
+ const parentElement = this._elements.records;
+ while (parentElement.lastChild) {
+ parentElement.removeChild(parentElement.lastChild);
}
}
@@ -186,7 +188,7 @@ class ManageRecords {
* @param {number} selectedCount
*/
updateButtonsStates(selectedCount) {
- log.debug("updateButtonsStates:", selectedCount);
+ lazy.log.debug("updateButtonsStates:", selectedCount);
if (selectedCount == 0) {
this._elements.edit.setAttribute("disabled", "disabled");
this._elements.remove.setAttribute("disabled", "disabled");
@@ -209,7 +211,7 @@ class ManageRecords {
*/
handleEvent(event) {
switch (event.type) {
- case "DOMContentLoaded": {
+ case "load": {
this.init();
break;
}
@@ -302,18 +304,33 @@ class ManageRecords {
}
}
-class ManageAddresses extends ManageRecords {
+export class ManageAddresses extends ManageRecords {
telemetryType = AutofillTelemetry.ADDRESS;
constructor(elements) {
super("addresses", elements);
elements.add.setAttribute(
"search-l10n-ids",
- FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
+ lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
);
AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
}
+ static getAddressL10nStrings() {
+ const l10nIds = [
+ ...lazy.FormAutofillUtils.MANAGE_ADDRESSES_L10N_IDS,
+ ...lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS,
+ ];
+
+ return l10nIds.reduce(
+ (acc, id) => ({
+ ...acc,
+ [id]: lazy.l10n.formatValueSync(id),
+ }),
+ {}
+ );
+ }
+
/**
* Open the edit address dialog to create/edit an address.
*
@@ -325,22 +342,23 @@ class ManageAddresses extends ManageRecords {
// Don't validate in preferences since it's fine for fields to be missing
// for autofill purposes. For PaymentRequest addresses get more validation.
noValidate: true,
+ l10nStrings: ManageAddresses.getAddressL10nStrings(),
});
}
getLabelInfo(address) {
- return { raw: FormAutofillUtils.getAddressLabel(address) };
+ return { raw: lazy.FormAutofillUtils.getAddressLabel(address) };
}
}
-class ManageCreditCards extends ManageRecords {
+export class ManageCreditCards extends ManageRecords {
telemetryType = AutofillTelemetry.CREDIT_CARD;
constructor(elements) {
super("creditCards", elements);
elements.add.setAttribute(
"search-l10n-ids",
- FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
+ lazy.FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
);
this._isDecrypted = false;
@@ -355,22 +373,24 @@ class ManageCreditCards extends ManageRecords {
async openEditDialog(creditCard) {
// Ask for reauth if user is trying to edit an existing credit card.
if (creditCard) {
- const promptMessage = FormAutofillUtils.reauthOSPromptMessage(
+ const promptMessage = lazy.FormAutofillUtils.reauthOSPromptMessage(
"autofill-edit-payment-method-os-prompt-macos",
"autofill-edit-payment-method-os-prompt-windows",
"autofill-edit-payment-method-os-prompt-other"
);
- const loggedIn = await FormAutofillUtils.ensureLoggedIn(promptMessage);
- if (!loggedIn.authenticated) {
+ const verified = await lazy.FormAutofillUtils.verifyUserOSAuth(
+ FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ promptMessage
+ );
+ if (!verified) {
return;
}
}
-
let decryptedCCNumObj = {};
if (creditCard && creditCard["cc-number-encrypted"]) {
try {
- decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(
+ decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt(
creditCard["cc-number-encrypted"]
);
} catch (ex) {
@@ -410,11 +430,11 @@ class ManageCreditCards extends ManageRecords {
// Since the text content is generated by Fluent, aria-label must be
// generated by Fluent also.
const type = creditCard["cc-type"];
- const typeL10nId = CreditCard.getNetworkL10nId(type);
+ const typeL10nId = lazy.CreditCard.getNetworkL10nId(type);
const typeName = typeL10nId
? await document.l10n.formatValue(typeL10nId)
: type ?? ""; // Unknown card type
- return CreditCard.getLabelInfo({
+ return lazy.CreditCard.getLabelInfo({
name: creditCard["cc-name"],
number: creditCard["cc-number"],
month: creditCard["cc-exp-month"],