Bug 1300989 - Implement the profile filling feature when a user selects one.; r?MattN draft
authorSean Lee <selee@mozilla.com>
Sat, 07 Jan 2017 09:22:19 +0800
changeset 479834 6265c7f6c780dd63e020a1bb491ff3846a116317
parent 479830 5c90730881cea96b6b843f65f87a41b3e32c61c5
child 544800 798933f65daf72d26a705f2bea3f630964fa21fe
push id44378
push userbmo:selee@mozilla.com
push dateTue, 07 Feb 2017 10:36:38 +0000
reviewersMattN
bugs1300989
milestone54.0a1
Bug 1300989 - Implement the profile filling feature when a user selects one.; r?MattN MozReview-Commit-ID: 5oMgvdO5RD1
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/content/FormAutofillContent.js
toolkit/components/satchel/AutoCompletePopup.jsm
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -32,16 +32,18 @@
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileStorage",
                                   "resource://formautofill/ProfileStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillUtils",
+                                  "resource://formautofill/FormAutofillUtils.jsm");
 
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 let FormAutofillParent = {
   _profileStore: null,
 
   /**
    * Initializes ProfileStorage and registers the message handler.
@@ -142,44 +144,32 @@ let FormAutofillParent = {
     } else {
       profiles = this._profileStore.getAll();
     }
 
     target.messageManager.sendAsyncMessage("FormAutofill:Profiles", profiles);
   },
 
   /**
-   * Transforms a word with hyphen into camel case.
-   * (e.g. transforms "address-type" into "addressType".)
-   *
-   * @private
-   * @param   {string} str The original string with hyphen.
-   * @returns {string} The camel-cased output string.
-   */
-  _camelCase(str) {
-    return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
-  },
-
-  /**
    * Get the corresponding value from the specified profile according to a valid
    * @autocomplete field name.
    *
    * Note that the field name doesn't need to match the property name defined in
    * Profile object. This method can transform the raw data to fulfill it. (e.g.
    * inputting "country-name" as "fieldName" will get a full name transformed
    * from the country code that is recorded in "country" field.)
    *
    * @private
    * @param   {Profile} profile   The specified profile.
    * @param   {string}  fieldName A valid @autocomplete field name.
    * @returns {string}  The corresponding value. Returns "undefined" if there's
    *                    no matching field.
    */
   _getDataByFieldName(profile, fieldName) {
-    let key = this._camelCase(fieldName);
+    let key = FormAutofillUtils.toCamelCase(fieldName);
 
     // TODO: Transform the raw profile data to fulfill "fieldName" here.
 
     return profile[key];
   },
 
   /**
    * Fills in the "fields" array by the specified profile.
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -0,0 +1,37 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["FormAutofillUtils"];
+
+this.FormAutofillUtils = {
+  /**
+   * Transforms a word with hyphen into camel case.
+   * (e.g. transforms "address-type" into "addressType".)
+   *
+   * @private
+   * @param   {string} str The original string with hyphen.
+   * @returns {string} The camel-cased output string.
+   */
+  toCamelCase(str) {
+    if (!str.includes("-")) {
+      return str;
+    }
+    return str.toLowerCase().replace(/-([a-z])/g, s => s[1].toUpperCase());
+  },
+
+  generateFullName(firstName, lastName, middleName) {
+    // TODO: The implementation should depend on the L10N spec, but a simplified
+    // rule is used here.
+    let fullName = firstName;
+    if (middleName) {
+      fullName += " " + middleName;
+    }
+    if (lastName) {
+      fullName += " " + lastName;
+    }
+    return fullName;
+  },
+};
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -5,31 +5,40 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ProfileAutoCompleteResult"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillUtils",
+                                  "resource://formautofill/FormAutofillUtils.jsm");
+
 this.ProfileAutoCompleteResult = function(searchString,
-                                           fieldName,
-                                           matchingProfiles,
-                                           {resultCode = null}) {
+                                          focusedFieldName,
+                                          existingFieldNames,
+                                          matchingProfiles,
+                                          {resultCode = null}) {
   this.searchString = searchString;
-  this._fieldName = fieldName;
+  this._focusedFieldName = focusedFieldName;
+  this._existingFieldNames = existingFieldNames;
   this._matchingProfiles = matchingProfiles;
 
   if (resultCode) {
     this.searchResult = resultCode;
   } else if (matchingProfiles.length > 0) {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
   } else {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
   }
+
+  this._popupLabels = this._generateLabels(this._focusedFieldName,
+                                           this._existingFieldNames,
+                                           this._matchingProfiles);
 };
 
 ProfileAutoCompleteResult.prototype = {
 
   // The user's query string
   searchString: "",
 
   // The default item that should be entered if none is selected
@@ -37,57 +46,109 @@ ProfileAutoCompleteResult.prototype = {
 
   // The reason the search failed
   errorDescription: "",
 
   // The result code of this result object.
   searchResult: null,
 
   // The autocomplete attribute of the focused input field
-  _fieldName: "",
+  _focusedFieldName: "",
+
+  // The autocomplete attributes of all input fields in the form.
+  _existingFieldNames: null,
 
   // The matching profiles contains the information for filling forms.
   _matchingProfiles: null,
 
+  _popupLabels: null,
+
   /**
    * @returns {number} The number of results
    */
   get matchCount() {
     return this._matchingProfiles.length;
   },
 
   _checkIndexBounds(index) {
     if (index < 0 || index >= this._matchingProfiles.length) {
       throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
     }
   },
 
+  _getSecondaryLabel(focusedFieldName, existingFieldNames, profile) {
+    /* TODO: Since "name" is a special case here, so the secondary "name" label
+       will be refined when the handling rule for "name" is ready.
+    */
+    const possibleNameFields = ["givenName", "additionalName", "familyName"];
+    focusedFieldName = possibleNameFields.includes(focusedFieldName) ?
+                       "name" : focusedFieldName;
+    if (!profile.name) {
+      profile.name = FormAutofillUtils.generateFullName(profile.givenName,
+                                                        profile.familyName,
+                                                        profile.additionalName);
+    }
+
+    const secondaryLabelOrder = [
+      "streetAddress",  // Street address
+      "name",           // Full name if needed
+      "addressLevel2",  // City/Town
+      "organization",   // Company or organization name
+      "addressLevel1",  // Province/State (Standardized code if possible)
+      "country",        // Country
+      "postalCode",     // Postal code
+      "tel",            // Phone number
+      "email",          // Email address
+    ];
+
+    for (const currentFieldName of secondaryLabelOrder) {
+      if (focusedFieldName != currentFieldName &&
+          existingFieldNames.includes(currentFieldName) &&
+          profile[currentFieldName]) {
+        return profile[currentFieldName];
+      }
+    }
+
+    return ""; // Nothing matched.
+  },
+
+  _generateLabels(focusedFieldName, existingFieldNames, profiles) {
+    return profiles.map(profile => {
+      return {
+        primary: profile[focusedFieldName],
+        secondary: this._getSecondaryLabel(focusedFieldName,
+                                           existingFieldNames,
+                                           profile),
+      };
+    });
+  },
+
   /**
    * Retrieves a result
    * @param   {number} index The index of the result requested
    * @returns {string} The result at the specified index
    */
   getValueAt(index) {
     this._checkIndexBounds(index);
-    return this._matchingProfiles[index].guid;
+    return this._popupLabels[index].primary;
   },
 
   getLabelAt(index) {
     this._checkIndexBounds(index);
-    return this._matchingProfiles[index].organization;
+    return JSON.stringify(this._popupLabels[index]);
   },
 
   /**
    * Retrieves a comment (metadata instance)
    * @param   {number} index The index of the comment requested
    * @returns {string} The comment at the specified index
    */
   getCommentAt(index) {
     this._checkIndexBounds(index);
-    return this._matchingProfiles[index].streetAddress;
+    return JSON.stringify(this._matchingProfiles[index]);
   },
 
   /**
    * Retrieves a style hint specific to a particular index.
    * @param   {number} index The index of the style hint requested
    * @returns {string} The style hint at the specified index
    */
   getStyleAt(index) {
--- a/browser/extensions/formautofill/content/FormAutofillContent.js
+++ b/browser/extensions/formautofill/content/FormAutofillContent.js
@@ -13,16 +13,19 @@
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAutoCompleteResult",
                                   "resource://formautofill/ProfileAutoCompleteResult.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
                                   "resource://gre/modules/FormLikeFactory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillUtils",
+                                  "resource://formautofill/FormAutofillUtils.jsm");
+
 
 const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                              .getService(Ci.nsIFormFillController);
 
 const AUTOFILL_FIELDS_THRESHOLD = 3;
 
 /**
  * Returns the autocomplete information of fields according to heuristics.
@@ -238,19 +241,24 @@ AutofillProfileAutoCompleteSearch.protot
     this.forceStop = false;
     let info = this.getInputDetails();
 
     this.getProfiles({info, searchString}).then((profiles) => {
       if (this.forceStop) {
         return;
       }
 
-      // TODO: Set formInfo for ProfileAutoCompleteResult
-      // let formInfo = this.getFormDetails();
-      let result = new ProfileAutoCompleteResult(searchString, info, profiles, {});
+      let existingFieldNames = this.getFormDetails().map(record => {
+        return FormAutofillUtils.toCamelCase(record.fieldName);
+      });
+      let result = new ProfileAutoCompleteResult(searchString,
+                                                 FormAutofillUtils.toCamelCase(info.fieldName),
+                                                 existingFieldNames,
+                                                 profiles,
+                                                 {});
 
       listener.onSearchResult(this, result);
     });
   },
 
   /**
    * Stops an asynchronous search that is in progress
    */
@@ -334,20 +342,28 @@ let ProfileAutocomplete = {
 };
 
 /**
  * Handles content's interactions.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
 var FormAutofillContent = {
+  MESSAGES: [
+    "FormAutoComplete:HandleEnter",
+  ],
+
   init() {
     ProfileAutocomplete.ensureRegistered();
 
     addEventListener("DOMContentLoaded", this);
+
+    for (let messageName of this.MESSAGES) {
+      addMessageListener(messageName, this);
+    }
   },
 
   handleEvent(evt) {
     if (!evt.isTrusted) {
       return;
     }
 
     switch (evt.type) {
@@ -368,17 +384,18 @@ var FormAutofillContent = {
    * @returns {Object|null}
    *          Return target input's information that cloned from content cache
    *          (or return null if the information is not found in the cache).
    */
   getInputDetails(element) {
     for (let formDetails of this._formsDetails) {
       for (let detail of formDetails) {
         if (element == detail.element) {
-          return this._serializeInfo(detail);
+          detail.fieldName = FormAutofillUtils.toCamelCase(detail.fieldName);
+          return detail;
         }
       }
     }
     return null;
   },
 
   /**
    * Get the form's information from cache which is created after page identified.
@@ -387,22 +404,44 @@ var FormAutofillContent = {
    * @returns {Array<Object>|null}
    *          Return target form's information that cloned from content cache
    *          (or return null if the information is not found in the cache).
    *
    */
   getFormDetails(element) {
     for (let formDetails of this._formsDetails) {
       if (formDetails.some((detail) => detail.element == element)) {
-        return formDetails.map((detail) => this._serializeInfo(detail));
+        return formDetails;
       }
     }
     return null;
   },
 
+  receiveMessage(message) {
+    let data = message.data;
+    if (!data.isPopupSelection ||
+        data.selectedIndex == -1 ||
+        data.fullResult.style != "autofill-profile") {
+      return;
+    }
+
+    let profile = JSON.parse(data.fullResult.comment);
+
+    let formDetails = this.getFormDetails(formFillController.focusedInput);
+    for (let inputInfo of formDetails) {
+      if (inputInfo.element === formFillController.focusedInput) {
+        continue;
+      }
+      let value = profile[FormAutofillUtils.toCamelCase(inputInfo.fieldName)];
+      if (value) {
+        inputInfo.element.value = value;
+      }
+    }
+  },
+
   /**
    * Create a clone the information object without element reference.
    *
    * @param {Object} detail Profile autofill information for specific input.
    * @returns {Object}
    *          Return a copy of cached information object without element reference
    *          since it's not needed for creating result.
    */
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -31,17 +31,17 @@ var AutoCompleteResultView = {
   },
 
   getValueAt(index) {
     return this.results[index].value;
   },
 
   getLabelAt(index) {
     // Unused by richlist autocomplete - see getCommentAt.
-    return "";
+    return this.results[index].comment;
   },
 
   getCommentAt(index) {
     // The richlist autocomplete popup uses comment for its main
     // display of an item, which is why we're returning the label
     // here instead.
     return this.results[index].label;
   },
@@ -274,20 +274,32 @@ this.AutoCompletePopup = {
    * Despite its name, this handleEnter is only called when the user clicks on
    * one of the items in the popup since the popup is rendered in the parent process.
    * The real controller's handleEnter is called directly in the content process
    * for other methods of completing a selection (e.g. using the tab or enter
    * keys) since the field with focus is in that process.
    */
   handleEnter(aIsPopupSelection) {
     if (this.openedPopup) {
-      this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
-        selectedIndex: this.openedPopup.selectedIndex,
+      let selectedIndex = this.openedPopup.selectedIndex;
+      let messageData = {
+        selectedIndex,
         isPopupSelection: aIsPopupSelection,
-      });
+      };
+      if (aIsPopupSelection && this.openedPopup.selectedIndex !== -1) {
+/*
+        let popup = this.openedPopup;
+        messageData.value = popup.view.getValueAt(selectedIndex);
+        messageData.label = popup.view.getLabelAt(selectedIndex);
+        messageData.comment = popup.view.getCommentAt(selectedIndex);
+        messageData.style = popup.view.getStyleAt(selectedIndex);
+*/
+        messageData.fullResult = this.openedPopup.view.results[selectedIndex];
+      }
+      this.sendMessageToBrowser("FormAutoComplete:HandleEnter", messageData);
     }
   },
 
   /**
    * If a browser exists that AutoCompletePopup knows about,
    * sends it a message. Otherwise, this is a no-op.
    *
    * @param {string} msgName