autofillEditForms.mjs (8232B)
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 /* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", 10 }); 11 12 class EditAutofillForm { 13 constructor(elements) { 14 this._elements = elements; 15 } 16 17 /** 18 * Fill the form with a record object. 19 * 20 * @param {object} [record = {}] 21 */ 22 loadRecord(record = {}) { 23 for (let field of this._elements.form.elements) { 24 let value = record[field.id]; 25 value = typeof value == "undefined" ? "" : value; 26 27 if (record.guid) { 28 field.value = value; 29 } else if (field.localName == "select") { 30 this.setDefaultSelectedOptionByValue(field, value); 31 } else { 32 // Use .defaultValue instead of .value to avoid setting the `dirty` flag 33 // which triggers form validation UI. 34 field.defaultValue = value; 35 } 36 } 37 if (!record.guid) { 38 // Reset the dirty value flag and validity state. 39 this._elements.form.reset(); 40 } else { 41 for (let field of this._elements.form.elements) { 42 this.updatePopulatedState(field); 43 this.updateCustomValidity(field); 44 } 45 } 46 } 47 48 setDefaultSelectedOptionByValue(select, value) { 49 for (let option of select.options) { 50 option.defaultSelected = option.value == value; 51 } 52 } 53 54 /** 55 * Get a record from the form suitable for a save/update in storage. 56 * 57 * @returns {object} 58 */ 59 buildFormObject() { 60 let initialObject = {}; 61 if (this.hasMailingAddressFields) { 62 // Start with an empty string for each mailing-address field so that any 63 // fields hidden for the current country are blanked in the return value. 64 initialObject = { 65 "street-address": "", 66 "address-level3": "", 67 "address-level2": "", 68 "address-level1": "", 69 "postal-code": "", 70 }; 71 } 72 73 return Array.from(this._elements.form.elements).reduce((obj, input) => { 74 if (!input.disabled) { 75 obj[input.id] = input.value; 76 } 77 return obj; 78 }, initialObject); 79 } 80 81 /** 82 * Handle events 83 * 84 * @param {DOMEvent} event 85 */ 86 handleEvent(event) { 87 switch (event.type) { 88 case "change": { 89 this.handleChange(event); 90 break; 91 } 92 case "input": { 93 this.handleInput(event); 94 break; 95 } 96 } 97 } 98 99 /** 100 * Handle change events 101 * 102 * @param {DOMEvent} event 103 */ 104 handleChange(event) { 105 this.updatePopulatedState(event.target); 106 } 107 108 /** 109 * Handle input events 110 */ 111 handleInput(_e) {} 112 113 /** 114 * Attach event listener 115 */ 116 attachEventListeners() { 117 this._elements.form.addEventListener("input", this); 118 } 119 120 /** 121 * Set the field-populated attribute if the field has a value. 122 * 123 * @param {DOMElement} field The field that will be checked for a value. 124 */ 125 updatePopulatedState(field) { 126 let span = field.parentNode.querySelector(".label-text"); 127 if (!span) { 128 return; 129 } 130 span.toggleAttribute("field-populated", !!field.value.trim()); 131 } 132 133 /** 134 * Run custom validity routines specific to the field and type of form. 135 * 136 * @param {DOMElement} _field The field that will be validated. 137 */ 138 updateCustomValidity(_field) {} 139 } 140 141 export class EditCreditCard extends EditAutofillForm { 142 /** 143 * @param {HTMLElement[]} elements 144 * @param {object} record with a decrypted cc-number 145 * @param {object} addresses in an object with guid keys for the billing address picker. 146 */ 147 constructor(elements, record, addresses) { 148 super(elements); 149 150 this._addresses = addresses; 151 Object.assign(this._elements, { 152 ccNumber: this._elements.form.querySelector("#cc-number"), 153 invalidCardNumberStringElement: this._elements.form.querySelector( 154 "#invalidCardNumberString" 155 ), 156 month: this._elements.form.querySelector("#cc-exp-month"), 157 year: this._elements.form.querySelector("#cc-exp-year"), 158 billingAddress: this._elements.form.querySelector("#billingAddressGUID"), 159 billingAddressRow: 160 this._elements.form.querySelector(".billingAddressRow"), 161 }); 162 163 this.attachEventListeners(); 164 this.loadRecord(record, addresses); 165 } 166 167 loadRecord(record, addresses, preserveFieldValues) { 168 // _record must be updated before generateYears and generateBillingAddressOptions are called. 169 this._record = record; 170 this._addresses = addresses; 171 this.generateBillingAddressOptions(preserveFieldValues); 172 if (!preserveFieldValues) { 173 // Re-generating the months will reset the selected option. 174 this.generateMonths(); 175 // Re-generating the years will reset the selected option. 176 this.generateYears(); 177 super.loadRecord(record); 178 } 179 } 180 181 generateMonths() { 182 const count = 12; 183 184 // Clear the list 185 this._elements.month.textContent = ""; 186 187 // Empty month option 188 this._elements.month.appendChild(new Option()); 189 190 // Populate month list. Format: "month number - month name" 191 let dateFormat = new Intl.DateTimeFormat(navigator.language, { 192 month: "long", 193 }).format; 194 for (let i = 0; i < count; i++) { 195 let monthNumber = (i + 1).toString(); 196 let monthName = dateFormat(new Date(1970, i)); 197 let option = new Option(); 198 option.value = monthNumber; 199 // XXX: Bug 1446164 - Localize this string. 200 option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`; 201 this._elements.month.appendChild(option); 202 } 203 } 204 205 generateYears() { 206 const count = 11; 207 const currentYear = new Date().getFullYear(); 208 const ccExpYear = this._record && this._record["cc-exp-year"]; 209 210 // Clear the list 211 this._elements.year.textContent = ""; 212 213 // Provide an empty year option 214 this._elements.year.appendChild(new Option()); 215 216 if (ccExpYear && ccExpYear < currentYear) { 217 this._elements.year.appendChild(new Option(ccExpYear)); 218 } 219 220 for (let i = 0; i < count; i++) { 221 let year = currentYear + i; 222 let option = new Option(year); 223 this._elements.year.appendChild(option); 224 } 225 226 if (ccExpYear && ccExpYear > currentYear + count) { 227 this._elements.year.appendChild(new Option(ccExpYear)); 228 } 229 } 230 231 generateBillingAddressOptions(preserveFieldValues) { 232 let billingAddressGUID; 233 if (preserveFieldValues && this._elements.billingAddress.value) { 234 billingAddressGUID = this._elements.billingAddress.value; 235 } else if (this._record) { 236 billingAddressGUID = this._record.billingAddressGUID; 237 } 238 239 this._elements.billingAddress.textContent = ""; 240 241 this._elements.billingAddress.appendChild(new Option("", "")); 242 243 let hasAddresses = false; 244 for (let [guid, address] of Object.entries(this._addresses)) { 245 hasAddresses = true; 246 let selected = guid == billingAddressGUID; 247 let option = new Option( 248 lazy.FormAutofillUtils.getAddressLabel(address), 249 guid, 250 selected, 251 selected 252 ); 253 this._elements.billingAddress.appendChild(option); 254 } 255 256 this._elements.billingAddressRow.hidden = !hasAddresses; 257 } 258 259 attachEventListeners() { 260 this._elements.form.addEventListener("change", this); 261 super.attachEventListeners(); 262 } 263 264 handleInput(event) { 265 // Clear the error message if cc-number is valid 266 if ( 267 event.target == this._elements.ccNumber && 268 lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value) 269 ) { 270 this._elements.ccNumber.setCustomValidity(""); 271 } 272 super.handleInput(event); 273 } 274 275 updateCustomValidity(field) { 276 super.updateCustomValidity(field); 277 278 // Mark the cc-number field as invalid if the number is empty or invalid. 279 if ( 280 field == this._elements.ccNumber && 281 !lazy.FormAutofillUtils.isCCNumber(field.value) 282 ) { 283 let invalidCardNumberString = 284 this._elements.invalidCardNumberStringElement.textContent; 285 field.setCustomValidity(invalidCardNumberString || " "); 286 } 287 } 288 }