tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

addressFormLayout.mjs (6296B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
      8  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
      9 });
     10 
     11 // Defines template descriptors for generating elements in convertLayoutToUI.
     12 const fieldTemplates = {
     13  commonAttributes(item) {
     14    return {
     15      id: item.fieldId,
     16      name: item.fieldId,
     17      required: item.required,
     18      value: item.value ?? "",
     19      // Conditionally add pattern attribute since pattern=""/false/undefined
     20      // results in weird behaviour.
     21      ...(item.pattern && { pattern: item.pattern }),
     22    };
     23  },
     24  input(item) {
     25    return {
     26      tag: "input",
     27      type: item.type ?? "text",
     28      ...this.commonAttributes(item),
     29    };
     30  },
     31  textarea(item) {
     32    return {
     33      tag: "textarea",
     34      ...this.commonAttributes(item),
     35    };
     36  },
     37  select(item) {
     38    return {
     39      tag: "select",
     40      children: item.options.map(({ value, text }) => ({
     41        tag: "option",
     42        selected: value === item.value,
     43        value,
     44        text,
     45      })),
     46      ...this.commonAttributes(item),
     47    };
     48  },
     49 };
     50 
     51 /**
     52 * Creates an HTML element with specified attributes and children.
     53 *
     54 * @param {string} tag - Tag name for the element to create.
     55 * @param {object} options - Options object containing attributes and children.
     56 * @param {object} options.attributes - Element's Attributes/Props (id, class, etc.)
     57 * @param {Array} options.children - Element's children (array of objects with tag and options).
     58 * @returns {HTMLElement} The newly created element.
     59 */
     60 const createElement = (tag, { children = [], ...attributes }) => {
     61  const element = document.createElement(tag);
     62 
     63  for (let [attributeName, attributeValue] of Object.entries(attributes)) {
     64    if (attributeName in element) {
     65      element[attributeName] = attributeValue;
     66    } else {
     67      element.setAttribute(attributeName, attributeValue);
     68    }
     69  }
     70 
     71  for (let { tag: childTag, ...childRest } of children) {
     72    element.appendChild(createElement(childTag, childRest));
     73  }
     74 
     75  return element;
     76 };
     77 
     78 /**
     79 * Generator that creates UI elements from `fields` object, using localization from `l10nStrings`.
     80 *
     81 * @param {Array} fields - Array of objects as returned from `FormAutofillUtils.getFormLayout`.
     82 * @param {object} l10nStrings - Key-value pairs for field label localization.
     83 * @yields {HTMLElement} - A localized label element with constructed from a field.
     84 */
     85 function* convertLayoutToUI(fields, l10nStrings) {
     86  for (const item of fields) {
     87    // eslint-disable-next-line no-nested-ternary
     88    const fieldTag = item.options
     89      ? "select"
     90      : item.multiline
     91        ? "textarea"
     92        : "input";
     93 
     94    const fieldUI = {
     95      label: {
     96        id: `${item.fieldId}-container`,
     97        class: `container ${item.newLine ? "new-line" : ""}`,
     98      },
     99      field: fieldTemplates[fieldTag](item),
    100      span: {
    101        class: "label-text",
    102        textContent: l10nStrings[item.l10nId] ?? "",
    103      },
    104    };
    105 
    106    const label = createElement("label", fieldUI.label);
    107    const { tag, ...rest } = fieldUI.field;
    108    const span = createElement("span", fieldUI.span);
    109    label.appendChild(span);
    110    const field = createElement(tag, rest);
    111    label.appendChild(field);
    112    yield label;
    113  }
    114 }
    115 
    116 /**
    117 * Retrieves the current form data from the current form element on the page.
    118 * NOTE: We are intentionally not using FormData here because on iOS we have states where
    119 *       selects are disabled and FormData ignores disabled elements. We want getCurrentFormData
    120 *       to always refelect the exact state of the form.
    121 *
    122 * @returns {object} An object containing key-value pairs of form data.
    123 */
    124 export const getCurrentFormData = () => {
    125  const formData = {};
    126  for (const element of document.querySelector("form").elements) {
    127    formData[element.name] = element.value ?? "";
    128  }
    129  return formData;
    130 };
    131 
    132 /**
    133 * Checks if the form can be submitted based on the number of non-empty values.
    134 * TODO(Bug 1891734): Add address validation. Right now we don't do any validation. (2 fields mimics the old behaviour ).
    135 *
    136 * @returns {boolean} True if the form can be submitted
    137 */
    138 export const canSubmitForm = () => {
    139  const formData = getCurrentFormData();
    140  const validValues = Object.values(formData).filter(Boolean);
    141  return validValues.length >= 2;
    142 };
    143 
    144 /**
    145 * Generates a form layout based on record data and localization strings.
    146 *
    147 * @param {HTMLFormElement} formElement - Target form element.
    148 * @param {object} record - Address record, includes at least country code defaulted to FormAutofill.DEFAULT_REGION.
    149 * @param {object} l10nStrings - Localization strings map.
    150 */
    151 export const createFormLayoutFromRecord = (
    152  formElement,
    153  record = { country: lazy.FormAutofill.DEFAULT_REGION },
    154  l10nStrings = {}
    155 ) => {
    156  // Always clear select values because they are not persisted between countries.
    157  // For example from US with state NY, we don't want the address-level1 to be NY
    158  // when changing to another country that doesn't have state options
    159  const selects = formElement.querySelectorAll("select:not(#country)");
    160  for (const select of selects) {
    161    select.value = "";
    162  }
    163 
    164  // Get old data to persist before clearing form
    165  const formData = getCurrentFormData();
    166  record = {
    167    ...record,
    168    ...formData,
    169  };
    170 
    171  formElement.innerHTML = "";
    172  const fields = lazy.FormAutofillUtils.getFormLayout(record);
    173 
    174  const layoutGenerator = convertLayoutToUI(fields, l10nStrings);
    175 
    176  for (const fieldElement of layoutGenerator) {
    177    formElement.appendChild(fieldElement);
    178  }
    179 
    180  document.querySelector("#country").addEventListener(
    181    "change",
    182    ev =>
    183      // Allow some time for the user to type
    184      // before we set the new country and re-render
    185      setTimeout(() => {
    186        record.country = ev.target.value;
    187        createFormLayoutFromRecord(formElement, record, l10nStrings);
    188      }, 300),
    189    { once: true }
    190  );
    191 
    192  // Used to notify tests that the form has been updated and is ready
    193  window.dispatchEvent(new CustomEvent("FormReadyForTests"));
    194 };