/* 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")); };