Bug 1427954 - Split autofill dialog logic from field logic. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Wed, 14 Mar 2018 18:12:38 -0700
changeset 408635 2f8bd1f13394a18448db08a7b62bf6c8afaabf99
parent 408634 89a5d0d89c0595fc31ad4ec6edf6e82ba70b60a7
child 408636 e2eae899dc4ab8352dffdfeacc974ccdc83fa3bf
push id100996
push userbtara@mozilla.com
push dateSat, 17 Mar 2018 10:37:43 +0000
treeherdermozilla-inbound@97160a734959 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1427954
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1427954 - Split autofill dialog logic from field logic. r=sfoster We want to re-use the field/form logic but not the dialog-specific logic so this separates them out. Custom Elements aren't enabled by default yet so we can't use them yet. MozReview-Commit-ID: 60hMpgSOmpp
browser/extensions/formautofill/content/autofillEditForms.js
browser/extensions/formautofill/content/editAddress.xhtml
browser/extensions/formautofill/content/editCreditCard.xhtml
browser/extensions/formautofill/content/editDialog.js
copy from browser/extensions/formautofill/content/editDialog.js
copy to browser/extensions/formautofill/content/autofillEditForms.js
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -6,39 +6,20 @@
 
 "use strict";
 
 const AUTOFILL_BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
 const REGIONS_BUNDLE_URI = "chrome://global/locale/regionNames.properties";
 
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
-                               "resource://formautofill/FormAutofillStorage.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
-                               "resource://formautofill/MasterPassword.jsm");
-
-class EditDialog {
-  constructor(subStorageName, elements, record) {
-    this._storageInitPromise = formAutofillStorage.initialize();
-    this._subStorageName = subStorageName;
+class EditAutofillForm {
+  constructor(elements, record) {
     this._elements = elements;
     this._record = record;
-    this.localizeDocument();
-    window.addEventListener("DOMContentLoaded", this, {once: true});
-  }
-
-  async init() {
-    if (this._record) {
-      await this.loadInitialValues(this._record);
-    }
-    this.attachEventListeners();
-    // For testing only: loadInitialValues for credit card is an async method, and tests
-    // need to wait until the values have been filled before editing the fields.
-    window.dispatchEvent(new CustomEvent("FormReady"));
   }
 
   uninit() {
     this.detachEventListeners();
     this._elements = null;
   }
 
   /**
@@ -54,171 +35,89 @@ class EditDialog {
     }
   }
 
   /**
    * Get inputs from the form.
    * @returns {object}
    */
   buildFormObject() {
-    return Array.from(document.forms[0].elements).reduce((obj, input) => {
+    return Array.from(this._elements.form.elements).reduce((obj, input) => {
       if (input.value) {
         obj[input.id] = input.value;
       }
       return obj;
     }, {});
   }
 
   /**
-   * Get storage and ensure it has been initialized.
-   * @returns {object}
-   */
-  async getStorage() {
-    await this._storageInitPromise;
-    return formAutofillStorage[this._subStorageName];
-  }
-
-  /**
-   * Asks FormAutofillParent to save or update an record.
-   * @param  {object} record
-   * @param  {string} guid [optional]
-   */
-  async saveRecord(record, guid) {
-    let storage = await this.getStorage();
-    if (guid) {
-      storage.update(guid, record);
-    } else {
-      storage.add(record);
-    }
-  }
-
-  /**
    * Handle events
    *
    * @param  {DOMEvent} event
    */
   handleEvent(event) {
     switch (event.type) {
-      case "DOMContentLoaded": {
-        this.init();
-        break;
-      }
       case "unload": {
         this.uninit();
         break;
       }
-      case "click": {
-        this.handleClick(event);
-        break;
-      }
       case "input": {
         this.handleInput(event);
         break;
       }
-      case "keypress": {
-        this.handleKeyPress(event);
-        break;
-      }
       case "change": {
         this.handleChange(event);
         break;
       }
-      case "contextmenu": {
-        if (!(event.target instanceof HTMLInputElement) &&
-            !(event.target instanceof HTMLTextAreaElement)) {
-          event.preventDefault();
-        }
-        break;
-      }
-    }
-  }
-
-  /**
-   * Handle click events
-   *
-   * @param  {DOMEvent} event
-   */
-  handleClick(event) {
-    if (event.target == this._elements.cancel) {
-      window.close();
-    }
-    if (event.target == this._elements.save) {
-      this.handleSubmit();
     }
   }
 
   /**
    * Handle input events
    *
    * @param  {DOMEvent} event
    */
-  handleInput(event) {
-    // Toggle disabled attribute on the save button based on
-    // whether the form is filled or empty.
-    if (Object.keys(this.buildFormObject()).length == 0) {
-      this._elements.save.setAttribute("disabled", true);
-    } else {
-      this._elements.save.removeAttribute("disabled");
-    }
-  }
-
-  /**
-   * Handle key press events
-   *
-   * @param  {DOMEvent} event
-   */
-  handleKeyPress(event) {
-    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
-      window.close();
-    }
-  }
+  handleInput(event) {}
 
   /**
    * Attach event listener
    */
   attachEventListeners() {
-    window.addEventListener("keypress", this);
-    window.addEventListener("contextmenu", this);
-    this._elements.controlsContainer.addEventListener("click", this);
-    document.addEventListener("input", this);
+    this._elements.form.addEventListener("input", this);
   }
 
   /**
    * Remove event listener
    */
   detachEventListeners() {
-    window.removeEventListener("keypress", this);
-    window.removeEventListener("contextmenu", this);
-    this._elements.controlsContainer.removeEventListener("click", this);
-    document.removeEventListener("input", this);
+    this._elements.form.removeEventListener("input", this);
   }
 
   // An interface to be inherited.
   localizeDocument() {}
 
   // An interface to be inherited.
-  handleSubmit(event) {}
-
-  // An interface to be inherited.
   handleChange(event) {}
 }
 
-class EditAddress extends EditDialog {
+class EditAddress extends EditAutofillForm {
   constructor(elements, record) {
     let country = record ? record.country :
                   FormAutofillUtils.supportedCountries.find(supported => supported == FormAutofillUtils.DEFAULT_REGION);
-    super("addresses", elements, record || {country});
+    super(elements, record || {country});
 
     Object.assign(this._elements, {
       addressLevel1Label: this._elements.form.querySelector("#address-level1-container > span"),
       postalCodeLabel: this._elements.form.querySelector("#postal-code-container > span"),
       country: this._elements.form.querySelector("#country"),
     });
 
+    this.localizeDocument();
     this.formatForm(country);
+    this.attachEventListeners();
   }
 
   /**
    * Format the form based on country. The address-level1 and postal-code labels
    * should be specific to the given country.
    * @param  {string} country
    */
   formatForm(country) {
@@ -256,58 +155,52 @@ class EditAddress extends EditDialog {
     // Hide the remaining fields
     for (let field of fields) {
       let container = document.getElementById(`${field}-container`);
       container.style.display = "none";
     }
   }
 
   localizeDocument() {
-    if (this._record) {
-      this._elements.title.dataset.localization = "editAddressTitle";
-    }
     let fragment = document.createDocumentFragment();
     for (let country of FormAutofillUtils.supportedCountries) {
       let option = new Option();
       option.value = country;
       option.dataset.localization = country.toLowerCase();
       fragment.appendChild(option);
     }
     this._elements.country.appendChild(fragment);
     FormAutofillUtils.localizeMarkup(REGIONS_BUNDLE_URI, this._elements.country);
   }
 
-  async handleSubmit() {
-    await this.saveRecord(this.buildFormObject(), this._record ? this._record.guid : null);
-    window.close();
-  }
-
   handleChange(event) {
     this.formatForm(event.target.value);
   }
 
   attachEventListeners() {
     this._elements.country.addEventListener("change", this);
     super.attachEventListeners();
   }
 
   detachEventListeners() {
     this._elements.country.removeEventListener("change", this);
     super.detachEventListeners();
   }
 }
 
-class EditCreditCard extends EditDialog {
+class EditCreditCard extends EditAutofillForm {
   constructor(elements, record) {
-    super("creditCards", elements, record);
+    super(elements, record);
     Object.assign(this._elements, {
       ccNumber: this._elements.form.querySelector("#cc-number"),
       year: this._elements.form.querySelector("#cc-exp-year"),
     });
+    this.localizeDocument();
     this.generateYears();
+    this.attachEventListeners();
   }
 
   generateYears() {
     const count = 11;
     const currentYear = new Date().getFullYear();
     const ccExpYear = this._record && this._record["cc-exp-year"];
 
     if (ccExpYear && ccExpYear < currentYear) {
@@ -321,47 +214,19 @@ class EditCreditCard extends EditDialog 
     }
 
     if (ccExpYear && ccExpYear > currentYear + count) {
       this._elements.year.appendChild(new Option(ccExpYear));
     }
   }
 
   localizeDocument() {
-    if (this._record) {
-      this._elements.title.dataset.localization = "editCreditCardTitle";
-    }
     FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
   }
 
-  /**
-   * Decrypt cc-number first and fill the form.
-   * @param  {object} creditCard
-   */
-  async loadInitialValues(creditCard) {
-    let decryptedCC = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
-    super.loadInitialValues(Object.assign({}, creditCard, {"cc-number": decryptedCC}));
-  }
-
-  async handleSubmit() {
-    let creditCard = this.buildFormObject();
-    // Show error on the cc-number field if it's empty or invalid
-    if (!FormAutofillUtils.isCCNumber(creditCard["cc-number"])) {
-      this._elements.ccNumber.setCustomValidity(true);
-      return;
-    }
-
-    // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
-    // APIs are refactored to be async functions (bug 1399367).
-    if (await MasterPassword.ensureLoggedIn()) {
-      await this.saveRecord(creditCard, this._record ? this._record.guid : null);
-    }
-    window.close();
-  }
-
   handleInput(event) {
     // Clear the error message if cc-number is valid
     if (event.target == this._elements.ccNumber &&
         FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)) {
       this._elements.ccNumber.setCustomValidity("");
     }
     super.handleInput(event);
   }
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -8,16 +8,17 @@
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml" width="620">
 <head>
   <title data-localization="addNewAddressTitle"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editDialog.css"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editAddress.css"/>
   <link rel="stylesheet" href="chrome://formautofill/skin/editDialog.css"/>
   <script src="chrome://formautofill/content/editDialog.js"></script>
+  <script src="chrome://formautofill/content/autofillEditForms.js"></script>
 </head>
 <body dir="&locale.dir;">
   <form id="form" autocomplete="off">
     <div>
       <div id="name-container">
         <label id="given-name-container">
           <span data-localization="givenName"/>
           <input id="given-name" type="text"/>
@@ -69,19 +70,26 @@
     </label>
   </form>
   <div id="controls-container">
     <button id="cancel" data-localization="cancelBtnLabel"/>
     <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
   </div>
   <script type="application/javascript"><![CDATA[
     "use strict";
-    /* global EditAddress */
-    new EditAddress({
+
+    let record = window.arguments && window.arguments[0];
+    /* import-globals-from autofillEditForms.js */
+    let fieldContainer = new EditAddress({
+      form: document.getElementById("form"),
+    }, record);
+
+    /* import-globals-from editDialog.js */
+    new EditAddressDialog({
       title: document.querySelector("title"),
-      form: document.getElementById("form"),
+      fieldContainer,
       controlsContainer: document.getElementById("controls-container"),
       cancel: document.getElementById("cancel"),
       save: document.getElementById("save"),
-    }, window.arguments && window.arguments[0]);
+    }, record);
   ]]></script>
 </body>
 </html>
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -8,16 +8,17 @@
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml" width="500" style="width: 500px">
 <head>
   <title data-localization="addNewCreditCardTitle"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editDialog.css"/>
   <link rel="stylesheet" href="chrome://formautofill-shared/skin/editCreditCard.css"/>
   <link rel="stylesheet" href="chrome://formautofill/skin/editDialog.css"/>
   <script src="chrome://formautofill/content/editDialog.js"></script>
+  <script src="chrome://formautofill/content/autofillEditForms.js"></script>
 </head>
 <body dir="&locale.dir;">
   <form id="form" autocomplete="off">
     <label>
       <span data-localization="cardNumber"/>
       <input id="cc-number" type="text"/>
     </label>
     <label>
@@ -47,19 +48,26 @@
     </div>
   </form>
   <div id="controls-container">
     <button id="cancel" data-localization="cancelBtnLabel"/>
     <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
   </div>
   <script type="application/javascript"><![CDATA[
     "use strict";
-    /* global EditCreditCard */
-    new EditCreditCard({
+
+    let record = window.arguments && window.arguments[0];
+    /* import-globals-from autofillEditForms.js */
+    let fieldContainer = new EditCreditCard({
+      form: document.getElementById("form"),
+    }, record);
+
+    /* import-globals-from editDialog.js */
+    new EditCreditCardDialog({
       title: document.querySelector("title"),
-      form: document.getElementById("form"),
+      fieldContainer,
       controlsContainer: document.getElementById("controls-container"),
       cancel: document.getElementById("cancel"),
       save: document.getElementById("save"),
-    }, window.arguments && window.arguments[0]);
+    }, record);
   ]]></script>
 </body>
 </html>
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/editDialog.js
@@ -1,27 +1,26 @@
 /* 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/. */
 
-/* exported EditAddress, EditCreditCard */
+/* exported EditAddressDialog, EditCreditCardDialog */
 
 "use strict";
 
 const AUTOFILL_BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
-const REGIONS_BUNDLE_URI = "chrome://global/locale/regionNames.properties";
 
 ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                                "resource://formautofill/FormAutofillStorage.jsm");
 ChromeUtils.defineModuleGetter(this, "MasterPassword",
                                "resource://formautofill/MasterPassword.jsm");
 
-class EditDialog {
+class AutofillEditDialog {
   constructor(subStorageName, elements, record) {
     this._storageInitPromise = formAutofillStorage.initialize();
     this._subStorageName = subStorageName;
     this._elements = elements;
     this._record = record;
     this.localizeDocument();
     window.addEventListener("DOMContentLoaded", this, {once: true});
   }
@@ -50,29 +49,16 @@ class EditDialog {
       let input = document.getElementById(field);
       if (input) {
         input.value = record[field];
       }
     }
   }
 
   /**
-   * Get inputs from the form.
-   * @returns {object}
-   */
-  buildFormObject() {
-    return Array.from(document.forms[0].elements).reduce((obj, input) => {
-      if (input.value) {
-        obj[input.id] = input.value;
-      }
-      return obj;
-    }, {});
-  }
-
-  /**
    * Get storage and ensure it has been initialized.
    * @returns {object}
    */
   async getStorage() {
     await this._storageInitPromise;
     return formAutofillStorage[this._subStorageName];
   }
 
@@ -112,20 +98,16 @@ class EditDialog {
       case "input": {
         this.handleInput(event);
         break;
       }
       case "keypress": {
         this.handleKeyPress(event);
         break;
       }
-      case "change": {
-        this.handleChange(event);
-        break;
-      }
       case "contextmenu": {
         if (!(event.target instanceof HTMLInputElement) &&
             !(event.target instanceof HTMLTextAreaElement)) {
           event.preventDefault();
         }
         break;
       }
     }
@@ -148,17 +130,17 @@ class EditDialog {
   /**
    * Handle input events
    *
    * @param  {DOMEvent} event
    */
   handleInput(event) {
     // Toggle disabled attribute on the save button based on
     // whether the form is filled or empty.
-    if (Object.keys(this.buildFormObject()).length == 0) {
+    if (Object.keys(this._elements.fieldContainer.buildFormObject()).length == 0) {
       this._elements.save.setAttribute("disabled", true);
     } else {
       this._elements.save.removeAttribute("disabled");
     }
   }
 
   /**
    * Handle key press events
@@ -188,146 +170,41 @@ class EditDialog {
     window.removeEventListener("keypress", this);
     window.removeEventListener("contextmenu", this);
     this._elements.controlsContainer.removeEventListener("click", this);
     document.removeEventListener("input", this);
   }
 
   // An interface to be inherited.
   localizeDocument() {}
-
-  // An interface to be inherited.
-  handleSubmit(event) {}
-
-  // An interface to be inherited.
-  handleChange(event) {}
 }
 
-class EditAddress extends EditDialog {
+class EditAddressDialog extends AutofillEditDialog {
   constructor(elements, record) {
     let country = record ? record.country :
                   FormAutofillUtils.supportedCountries.find(supported => supported == FormAutofillUtils.DEFAULT_REGION);
     super("addresses", elements, record || {country});
-
-    Object.assign(this._elements, {
-      addressLevel1Label: this._elements.form.querySelector("#address-level1-container > span"),
-      postalCodeLabel: this._elements.form.querySelector("#postal-code-container > span"),
-      country: this._elements.form.querySelector("#country"),
-    });
-
-    this.formatForm(country);
-  }
-
-  /**
-   * Format the form based on country. The address-level1 and postal-code labels
-   * should be specific to the given country.
-   * @param  {string} country
-   */
-  formatForm(country) {
-    const {addressLevel1Label, postalCodeLabel, fieldsOrder} = FormAutofillUtils.getFormFormat(country);
-    this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
-    this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
-    FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
-    this.arrangeFields(fieldsOrder);
-  }
-
-  arrangeFields(fieldsOrder) {
-    let fields = [
-      "name",
-      "organization",
-      "street-address",
-      "address-level2",
-      "address-level1",
-      "postal-code",
-    ];
-    let inputs = [];
-    for (let i = 0; i < fieldsOrder.length; i++) {
-      let {fieldId, newLine} = fieldsOrder[i];
-      let container = document.getElementById(`${fieldId}-container`);
-      inputs.push(...container.querySelectorAll("input, textarea, select"));
-      container.style.display = "flex";
-      container.style.order = i;
-      container.style.pageBreakAfter = newLine ? "always" : "auto";
-      // Remove the field from the list of fields
-      fields.splice(fields.indexOf(fieldId), 1);
-    }
-    for (let i = 0; i < inputs.length; i++) {
-      // Assign tabIndex starting from 1
-      inputs[i].tabIndex = i + 1;
-    }
-    // Hide the remaining fields
-    for (let field of fields) {
-      let container = document.getElementById(`${field}-container`);
-      container.style.display = "none";
-    }
   }
 
   localizeDocument() {
     if (this._record) {
       this._elements.title.dataset.localization = "editAddressTitle";
     }
-    let fragment = document.createDocumentFragment();
-    for (let country of FormAutofillUtils.supportedCountries) {
-      let option = new Option();
-      option.value = country;
-      option.dataset.localization = country.toLowerCase();
-      fragment.appendChild(option);
-    }
-    this._elements.country.appendChild(fragment);
-    FormAutofillUtils.localizeMarkup(REGIONS_BUNDLE_URI, this._elements.country);
+    FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
   }
 
   async handleSubmit() {
-    await this.saveRecord(this.buildFormObject(), this._record ? this._record.guid : null);
+    await this.saveRecord(this._elements.fieldContainer.buildFormObject(), this._record ? this._record.guid : null);
     window.close();
   }
-
-  handleChange(event) {
-    this.formatForm(event.target.value);
-  }
-
-  attachEventListeners() {
-    this._elements.country.addEventListener("change", this);
-    super.attachEventListeners();
-  }
-
-  detachEventListeners() {
-    this._elements.country.removeEventListener("change", this);
-    super.detachEventListeners();
-  }
 }
 
-class EditCreditCard extends EditDialog {
+class EditCreditCardDialog extends AutofillEditDialog {
   constructor(elements, record) {
     super("creditCards", elements, record);
-    Object.assign(this._elements, {
-      ccNumber: this._elements.form.querySelector("#cc-number"),
-      year: this._elements.form.querySelector("#cc-exp-year"),
-    });
-    this.generateYears();
-  }
-
-  generateYears() {
-    const count = 11;
-    const currentYear = new Date().getFullYear();
-    const ccExpYear = this._record && this._record["cc-exp-year"];
-
-    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));
-    }
   }
 
   localizeDocument() {
     if (this._record) {
       this._elements.title.dataset.localization = "editCreditCardTitle";
     }
     FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
   }
@@ -337,32 +214,23 @@ class EditCreditCard extends EditDialog 
    * @param  {object} creditCard
    */
   async loadInitialValues(creditCard) {
     let decryptedCC = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
     super.loadInitialValues(Object.assign({}, creditCard, {"cc-number": decryptedCC}));
   }
 
   async handleSubmit() {
-    let creditCard = this.buildFormObject();
+    let creditCard = this._elements.fieldContainer.buildFormObject();
     // Show error on the cc-number field if it's empty or invalid
     if (!FormAutofillUtils.isCCNumber(creditCard["cc-number"])) {
       this._elements.ccNumber.setCustomValidity(true);
       return;
     }
 
     // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
     // APIs are refactored to be async functions (bug 1399367).
     if (await MasterPassword.ensureLoggedIn()) {
       await this.saveRecord(creditCard, this._record ? this._record.guid : null);
     }
     window.close();
   }
-
-  handleInput(event) {
-    // Clear the error message if cc-number is valid
-    if (event.target == this._elements.ccNumber &&
-        FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)) {
-      this._elements.ccNumber.setCustomValidity("");
-    }
-    super.handleInput(event);
-  }
 }