mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			288 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			288 lines
		
	
	
	
		
			8 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/. */
 | 
						|
 | 
						|
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
class EditAutofillForm {
 | 
						|
  constructor(elements) {
 | 
						|
    this._elements = elements;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fill the form with a record object.
 | 
						|
   *
 | 
						|
   * @param  {object} [record = {}]
 | 
						|
   */
 | 
						|
  loadRecord(record = {}) {
 | 
						|
    for (let field of this._elements.form.elements) {
 | 
						|
      let value = record[field.id];
 | 
						|
      value = typeof value == "undefined" ? "" : value;
 | 
						|
 | 
						|
      if (record.guid) {
 | 
						|
        field.value = value;
 | 
						|
      } else if (field.localName == "select") {
 | 
						|
        this.setDefaultSelectedOptionByValue(field, value);
 | 
						|
      } else {
 | 
						|
        // Use .defaultValue instead of .value to avoid setting the `dirty` flag
 | 
						|
        // which triggers form validation UI.
 | 
						|
        field.defaultValue = value;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (!record.guid) {
 | 
						|
      // Reset the dirty value flag and validity state.
 | 
						|
      this._elements.form.reset();
 | 
						|
    } else {
 | 
						|
      for (let field of this._elements.form.elements) {
 | 
						|
        this.updatePopulatedState(field);
 | 
						|
        this.updateCustomValidity(field);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  setDefaultSelectedOptionByValue(select, value) {
 | 
						|
    for (let option of select.options) {
 | 
						|
      option.defaultSelected = option.value == value;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get a record from the form suitable for a save/update in storage.
 | 
						|
   *
 | 
						|
   * @returns {object}
 | 
						|
   */
 | 
						|
  buildFormObject() {
 | 
						|
    let initialObject = {};
 | 
						|
    if (this.hasMailingAddressFields) {
 | 
						|
      // Start with an empty string for each mailing-address field so that any
 | 
						|
      // fields hidden for the current country are blanked in the return value.
 | 
						|
      initialObject = {
 | 
						|
        "street-address": "",
 | 
						|
        "address-level3": "",
 | 
						|
        "address-level2": "",
 | 
						|
        "address-level1": "",
 | 
						|
        "postal-code": "",
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    return Array.from(this._elements.form.elements).reduce((obj, input) => {
 | 
						|
      if (!input.disabled) {
 | 
						|
        obj[input.id] = input.value;
 | 
						|
      }
 | 
						|
      return obj;
 | 
						|
    }, initialObject);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle events
 | 
						|
   *
 | 
						|
   * @param  {DOMEvent} event
 | 
						|
   */
 | 
						|
  handleEvent(event) {
 | 
						|
    switch (event.type) {
 | 
						|
      case "change": {
 | 
						|
        this.handleChange(event);
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "input": {
 | 
						|
        this.handleInput(event);
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle change events
 | 
						|
   *
 | 
						|
   * @param  {DOMEvent} event
 | 
						|
   */
 | 
						|
  handleChange(event) {
 | 
						|
    this.updatePopulatedState(event.target);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle input events
 | 
						|
   */
 | 
						|
  handleInput(_e) {}
 | 
						|
 | 
						|
  /**
 | 
						|
   * Attach event listener
 | 
						|
   */
 | 
						|
  attachEventListeners() {
 | 
						|
    this._elements.form.addEventListener("input", this);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set the field-populated attribute if the field has a value.
 | 
						|
   *
 | 
						|
   * @param {DOMElement} field The field that will be checked for a value.
 | 
						|
   */
 | 
						|
  updatePopulatedState(field) {
 | 
						|
    let span = field.parentNode.querySelector(".label-text");
 | 
						|
    if (!span) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    span.toggleAttribute("field-populated", !!field.value.trim());
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Run custom validity routines specific to the field and type of form.
 | 
						|
   *
 | 
						|
   * @param {DOMElement} _field The field that will be validated.
 | 
						|
   */
 | 
						|
  updateCustomValidity(_field) {}
 | 
						|
}
 | 
						|
 | 
						|
export class EditCreditCard extends EditAutofillForm {
 | 
						|
  /**
 | 
						|
   * @param {HTMLElement[]} elements
 | 
						|
   * @param {object} record with a decrypted cc-number
 | 
						|
   * @param {object} addresses in an object with guid keys for the billing address picker.
 | 
						|
   */
 | 
						|
  constructor(elements, record, addresses) {
 | 
						|
    super(elements);
 | 
						|
 | 
						|
    this._addresses = addresses;
 | 
						|
    Object.assign(this._elements, {
 | 
						|
      ccNumber: this._elements.form.querySelector("#cc-number"),
 | 
						|
      invalidCardNumberStringElement: this._elements.form.querySelector(
 | 
						|
        "#invalidCardNumberString"
 | 
						|
      ),
 | 
						|
      month: this._elements.form.querySelector("#cc-exp-month"),
 | 
						|
      year: this._elements.form.querySelector("#cc-exp-year"),
 | 
						|
      billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
 | 
						|
      billingAddressRow:
 | 
						|
        this._elements.form.querySelector(".billingAddressRow"),
 | 
						|
    });
 | 
						|
 | 
						|
    this.attachEventListeners();
 | 
						|
    this.loadRecord(record, addresses);
 | 
						|
  }
 | 
						|
 | 
						|
  loadRecord(record, addresses, preserveFieldValues) {
 | 
						|
    // _record must be updated before generateYears and generateBillingAddressOptions are called.
 | 
						|
    this._record = record;
 | 
						|
    this._addresses = addresses;
 | 
						|
    this.generateBillingAddressOptions(preserveFieldValues);
 | 
						|
    if (!preserveFieldValues) {
 | 
						|
      // Re-generating the months will reset the selected option.
 | 
						|
      this.generateMonths();
 | 
						|
      // Re-generating the years will reset the selected option.
 | 
						|
      this.generateYears();
 | 
						|
      super.loadRecord(record);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  generateMonths() {
 | 
						|
    const count = 12;
 | 
						|
 | 
						|
    // Clear the list
 | 
						|
    this._elements.month.textContent = "";
 | 
						|
 | 
						|
    // Empty month option
 | 
						|
    this._elements.month.appendChild(new Option());
 | 
						|
 | 
						|
    // Populate month list. Format: "month number - month name"
 | 
						|
    let dateFormat = new Intl.DateTimeFormat(navigator.language, {
 | 
						|
      month: "long",
 | 
						|
    }).format;
 | 
						|
    for (let i = 0; i < count; i++) {
 | 
						|
      let monthNumber = (i + 1).toString();
 | 
						|
      let monthName = dateFormat(new Date(1970, i));
 | 
						|
      let option = new Option();
 | 
						|
      option.value = monthNumber;
 | 
						|
      // XXX: Bug 1446164 - Localize this string.
 | 
						|
      option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
 | 
						|
      this._elements.month.appendChild(option);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  generateYears() {
 | 
						|
    const count = 11;
 | 
						|
    const currentYear = new Date().getFullYear();
 | 
						|
    const ccExpYear = this._record && this._record["cc-exp-year"];
 | 
						|
 | 
						|
    // Clear the list
 | 
						|
    this._elements.year.textContent = "";
 | 
						|
 | 
						|
    // Provide an empty year option
 | 
						|
    this._elements.year.appendChild(new Option());
 | 
						|
 | 
						|
    if (ccExpYear && ccExpYear < currentYear) {
 | 
						|
      this._elements.year.appendChild(new Option(ccExpYear));
 | 
						|
    }
 | 
						|
 | 
						|
    for (let i = 0; i < count; i++) {
 | 
						|
      let year = currentYear + i;
 | 
						|
      let option = new Option(year);
 | 
						|
      this._elements.year.appendChild(option);
 | 
						|
    }
 | 
						|
 | 
						|
    if (ccExpYear && ccExpYear > currentYear + count) {
 | 
						|
      this._elements.year.appendChild(new Option(ccExpYear));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  generateBillingAddressOptions(preserveFieldValues) {
 | 
						|
    let billingAddressGUID;
 | 
						|
    if (preserveFieldValues && this._elements.billingAddress.value) {
 | 
						|
      billingAddressGUID = this._elements.billingAddress.value;
 | 
						|
    } else if (this._record) {
 | 
						|
      billingAddressGUID = this._record.billingAddressGUID;
 | 
						|
    }
 | 
						|
 | 
						|
    this._elements.billingAddress.textContent = "";
 | 
						|
 | 
						|
    this._elements.billingAddress.appendChild(new Option("", ""));
 | 
						|
 | 
						|
    let hasAddresses = false;
 | 
						|
    for (let [guid, address] of Object.entries(this._addresses)) {
 | 
						|
      hasAddresses = true;
 | 
						|
      let selected = guid == billingAddressGUID;
 | 
						|
      let option = new Option(
 | 
						|
        lazy.FormAutofillUtils.getAddressLabel(address),
 | 
						|
        guid,
 | 
						|
        selected,
 | 
						|
        selected
 | 
						|
      );
 | 
						|
      this._elements.billingAddress.appendChild(option);
 | 
						|
    }
 | 
						|
 | 
						|
    this._elements.billingAddressRow.hidden = !hasAddresses;
 | 
						|
  }
 | 
						|
 | 
						|
  attachEventListeners() {
 | 
						|
    this._elements.form.addEventListener("change", this);
 | 
						|
    super.attachEventListeners();
 | 
						|
  }
 | 
						|
 | 
						|
  handleInput(event) {
 | 
						|
    // Clear the error message if cc-number is valid
 | 
						|
    if (
 | 
						|
      event.target == this._elements.ccNumber &&
 | 
						|
      lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
 | 
						|
    ) {
 | 
						|
      this._elements.ccNumber.setCustomValidity("");
 | 
						|
    }
 | 
						|
    super.handleInput(event);
 | 
						|
  }
 | 
						|
 | 
						|
  updateCustomValidity(field) {
 | 
						|
    super.updateCustomValidity(field);
 | 
						|
 | 
						|
    // Mark the cc-number field as invalid if the number is empty or invalid.
 | 
						|
    if (
 | 
						|
      field == this._elements.ccNumber &&
 | 
						|
      !lazy.FormAutofillUtils.isCCNumber(field.value)
 | 
						|
    ) {
 | 
						|
      let invalidCardNumberString =
 | 
						|
        this._elements.invalidCardNumberStringElement.textContent;
 | 
						|
      field.setCustomValidity(invalidCardNumberString || " ");
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |