Bug 1300989 - Implement the profile filling feature when a user selects one.; r?MattN
MozReview-Commit-ID: 5oMgvdO5RD1
--- 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