Skip to content

Commit

Permalink
Refactor - auto update credential provider script (#20139)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
github-actions[bot] committed May 6, 2024
1 parent a357831 commit 42b9be1
Showing 1 changed file with 188 additions and 0 deletions.
188 changes: 188 additions & 0 deletions firefox-ios/Client/Assets/CC_Script/addressFormLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* 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/. */

ChromeUtils.defineESModuleGetters(this, {
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
});

/* exported canSubmitForm, getCurrentFormData */

// 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.
*/
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
*/
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.
*/
const createFormLayoutFromRecord = (
formElement,
record = { country: 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 = 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"));
};

0 comments on commit 42b9be1

Please sign in to comment.