summaryrefslogtreecommitdiffstats
path: root/browser/extensions/formautofill/content/addressFormLayout.mjs
blob: 5e48e6afaad1f92f626d5d6941266ec516098b43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/* 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"));
};