summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/content/autofillEditForms.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/formautofill/content/autofillEditForms.js')
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js640
1 files changed, 0 insertions, 640 deletions
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 || " ");
- }
- }
-}