194 lines
6.1 KiB
JavaScript
194 lines
6.1 KiB
JavaScript
/* 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 ?? "",
|
|
// Conditionally add pattern attribute since pattern=""/false/undefined
|
|
// results in weird behaviour.
|
|
...(item.pattern && { pattern: item.pattern }),
|
|
};
|
|
},
|
|
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 span = createElement("span", fieldUI.span);
|
|
label.appendChild(span);
|
|
const field = createElement(tag, rest);
|
|
label.appendChild(field);
|
|
yield label;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the current form data from the current form element on the page.
|
|
* NOTE: We are intentionally not using FormData here because on iOS we have states where
|
|
* selects are disabled and FormData ignores disabled elements. We want getCurrentFormData
|
|
* to always refelect the exact state of the form.
|
|
*
|
|
* @returns {object} An object containing key-value pairs of form data.
|
|
*/
|
|
export const getCurrentFormData = () => {
|
|
const formData = {};
|
|
for (const element of document.querySelector("form").elements) {
|
|
formData[element.name] = element.value ?? "";
|
|
}
|
|
return formData;
|
|
};
|
|
|
|
/**
|
|
* 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"));
|
|
};
|