Bug 1300989 - Part 1: Implement the profile filling feature when a user selects one.; r?MattN
MozReview-Commit-ID: 5oMgvdO5RD1
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -82,111 +82,159 @@ AutofillProfileAutoCompleteSearch.protot
* or asynchronously) of the result
*
* @param {string} searchString the string to search for
* @param {string} searchParam
* @param {Object} previousResult a previous result to use for faster searchinig
* @param {Object} listener the listener to notify when the search is complete
*/
startSearch(searchString, searchParam, previousResult, listener) {
+ let focusedInput = formFillController.focusedInput;
this.forceStop = false;
- let info = this.getInputDetails();
+ let info = FormAutofillContent.getInputDetails(focusedInput);
- this.getProfiles({info, searchString}).then((profiles) => {
+ 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 allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
+ let result = new ProfileAutoCompleteResult(searchString,
+ info.fieldName,
+ allFieldNames,
+ profiles,
+ {});
listener.onSearchResult(this, result);
+ ProfileAutocomplete.setProfileAutoCompleteResult(result);
});
},
/**
* Stops an asynchronous search that is in progress
*/
stopSearch() {
+ ProfileAutocomplete.setProfileAutoCompleteResult(null);
this.forceStop = true;
},
/**
* Get the profile data from parent process for AutoComplete result.
*
* @private
* @param {Object} data
* Parameters for querying the corresponding result.
* @param {string} data.searchString
* The typed string for filtering out the matched profile.
* @param {string} data.info
* The input autocomplete property's information.
* @returns {Promise}
* Promise that resolves when profiles returned from parent process.
*/
- getProfiles(data) {
+ _getProfiles(data) {
return new Promise((resolve) => {
Services.cpmm.addMessageListener("FormAutofill:Profiles", function getResult(result) {
Services.cpmm.removeMessageListener("FormAutofill:Profiles", getResult);
resolve(result.data);
});
Services.cpmm.sendAsyncMessage("FormAutofill:GetProfiles", data);
});
},
-
-
- /**
- * Get the input's information from FormAutofillContent's cache.
- *
- * @returns {Object}
- * Target input's information that cached in FormAutofillContent.
- */
- getInputDetails() {
- // TODO: Maybe we'll need to wait for cache ready if detail is empty.
- return FormAutofillContent.getInputDetails(formFillController.focusedInput);
- },
-
- /**
- * Get the form's information from FormAutofillContent's cache.
- *
- * @returns {Array<Object>}
- * Array of the inputs' information for the target form.
- */
- getFormDetails() {
- // TODO: Maybe we'll need to wait for cache ready if details is empty.
- return FormAutofillContent.getFormDetails(formFillController.focusedInput);
- },
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillProfileAutoCompleteSearch]);
let ProfileAutocomplete = {
+ _lastAutoCompleteResult: null,
_registered: false,
_factory: null,
ensureRegistered() {
if (this._registered) {
return;
}
this._factory = new AutocompleteFactory();
this._factory.register(AutofillProfileAutoCompleteSearch);
this._registered = true;
+
+ Services.obs.addObserver(this, "autocomplete-will-enter-text", false);
},
ensureUnregistered() {
if (!this._registered) {
return;
}
this._factory.unregister();
this._factory = null;
this._registered = false;
+
+ Services.obs.removeObserver(this, "autocomplete-will-enter-text");
+ },
+
+ setProfileAutoCompleteResult(result) {
+ this._lastAutoCompleteResult = result;
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "autocomplete-will-enter-text": {
+ if (!formFillController.focusedInput) {
+ break;
+ }
+ this._fillFromAutocompleteRow(formFillController.focusedInput);
+ break;
+ }
+ }
+ },
+
+ _frameMMFromWindow(contentWindow) {
+ return contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ },
+
+ _fillFromAutocompleteRow(focusedInput) {
+ let formDetails = FormAutofillContent.getFormDetails(focusedInput);
+ if (!formDetails) {
+ // The observer notification was for a different frame.
+ return;
+ }
+
+ let mm = this._frameMMFromWindow(focusedInput.ownerDocument.defaultView);
+ let selectedIndexResult = mm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
+ if (selectedIndexResult.length != 1 || !Number.isInteger(selectedIndexResult[0])) {
+ throw new Error("Invalid autocomplete selectedIndex");
+ }
+ let selectedIndex = selectedIndexResult[0];
+
+ if (selectedIndex == -1 ||
+ !this._lastAutoCompleteResult ||
+ this._lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
+ return;
+ }
+
+ let profile = JSON.parse(this._lastAutoCompleteResult.getCommentAt(selectedIndex));
+
+ // TODO: FormAutofillHandler.autofillFormFields will be used for filling
+ // fields logic eventually.
+ for (let inputInfo of formDetails) {
+ // Skip filling the value of focused input which is filled in
+ // FormFillController.
+ if (inputInfo.element === focusedInput) {
+ continue;
+ }
+ let value = profile[inputInfo.fieldName];
+ if (value) {
+ inputInfo.element.setUserInput(value);
+ }
+ }
},
};
/**
* Handles content's interactions for the process.
*
* NOTE: Declares it by "var" to make it accessible in unit tests.
*/
@@ -211,17 +259,17 @@ 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);
+ return detail;
}
}
}
return null;
},
/**
* Get the form's information from cache which is created after page identified.
@@ -230,34 +278,25 @@ 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;
},
- /**
- * 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.
- */
- _serializeInfo(detail) {
- let info = Object.assign({}, detail);
- delete info.element;
- return info;
+ getAllFieldNames(element) {
+ let formDetails = this.getFormDetails(element);
+ return formDetails.map(record => record.fieldName);
},
_identifyAutofillFields(doc) {
let forms = [];
this._formsDetails = [];
// Collects root forms from inputs.
for (let field of doc.getElementsByTagName("input")) {
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -106,16 +106,19 @@ FormAutofillHandler.prototype = {
* fieldName: Value originally provided to the user interface.
* value: String with which the field should be updated.
* index: Index to match the input in fieldDetails
* }],
* }
*/
autofillFormFields(autofillResult) {
for (let field of autofillResult) {
+ // TODO: Skip filling the value of focused input which is filled in
+ // FormFillController.
+
// Get the field details, if it was processed by the user interface.
let fieldDetail = this.fieldDetails[field.index];
// Avoid the invalid value set
if (!fieldDetail || !field.value) {
continue;
}
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -107,20 +107,18 @@ FormAutofillParent.prototype = {
},
/**
* Add/remove message listener and broadcast the status to frames while the
* form autofill status changed.
*/
_onStatusChanged() {
if (this._enabled) {
- Services.ppmm.addMessageListener("FormAutofill:PopulateFieldValues", this);
Services.ppmm.addMessageListener("FormAutofill:GetProfiles", this);
} else {
- Services.ppmm.removeMessageListener("FormAutofill:PopulateFieldValues", this);
Services.ppmm.removeMessageListener("FormAutofill:GetProfiles", this);
}
Services.mm.broadcastAsyncMessage("FormAutofill:enabledStatus", this._enabled);
},
/**
* Query pref (and storage) status to determine the overall status for
@@ -136,19 +134,16 @@ FormAutofillParent.prototype = {
* Handles the message coming from FormAutofillContent.
*
* @param {string} message.name The name of the message.
* @param {object} message.data The data of the message.
* @param {nsIFrameMessageManager} message.target Caller's message manager.
*/
receiveMessage({name, data, target}) {
switch (name) {
- case "FormAutofill:PopulateFieldValues":
- this._populateFieldValues(data, target);
- break;
case "FormAutofill:GetProfiles":
this._getProfiles(data, target);
break;
case "FormAutofill:getEnabledStatus":
Services.ppmm.broadcastAsyncMessage("FormAutofill:enabledStatus",
this._enabled);
break;
}
@@ -171,41 +166,22 @@ FormAutofillParent.prototype = {
* @private
*/
_uninit() {
if (this._profileStore) {
this._profileStore._saveImmediately();
this._profileStore = null;
}
- Services.ppmm.removeMessageListener("FormAutofill:PopulateFieldValues", this);
Services.ppmm.removeMessageListener("FormAutofill:GetProfiles", this);
Services.obs.removeObserver(this, "advanced-pane-loaded");
Services.prefs.removeObserver(ENABLED_PREF, this);
},
/**
- * Populates the field values and notifies content to fill in. Exception will
- * be thrown if there's no matching profile.
- *
- * @private
- * @param {string} data.guid
- * Indicates which profile to populate
- * @param {Fields} data.fields
- * The "fields" array collected from content.
- * @param {nsIFrameMessageManager} target
- * Content's message manager.
- */
- _populateFieldValues({guid, fields}, target) {
- this._profileStore.notifyUsed(guid);
- this._fillInFields(this._profileStore.get(guid), fields);
- target.sendAsyncMessage("FormAutofill:fillForm", {fields});
- },
-
- /**
* Get the profile data from profile store and return profiles back to content process.
*
* @private
* @param {string} data.searchString
* The typed string for filtering out the matched profile.
* @param {string} data.info
* The input autocomplete property's information.
* @param {nsIFrameMessageManager} target
@@ -217,47 +193,11 @@ FormAutofillParent.prototype = {
if (info && info.fieldName) {
profiles = this._profileStore.getByFilter({searchString, info});
} else {
profiles = this._profileStore.getAll();
}
target.sendAsyncMessage("FormAutofill:Profiles", profiles);
},
-
- /**
- * 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) {
- // TODO: Transform the raw profile data to fulfill "fieldName" here.
- return profile[fieldName];
- },
-
- /**
- * Fills in the "fields" array by the specified profile.
- *
- * @private
- * @param {Profile} profile The specified profile to fill in.
- * @param {Fields} fields The "fields" array collected from content.
- */
- _fillInFields(profile, fields) {
- for (let field of fields) {
- let value = this._getDataByFieldName(profile, field.fieldName);
- if (value !== undefined) {
- field.value = value;
- }
- }
- },
};
this.EXPORTED_SYMBOLS = ["FormAutofillParent"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -0,0 +1,22 @@
+/* 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 = {
+ 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,89 +5,159 @@
"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,
+ allFieldNames,
+ matchingProfiles,
+ {resultCode = null}) {
this.searchString = searchString;
- this._fieldName = fieldName;
+ this._focusedFieldName = focusedFieldName;
+ this._allFieldNames = allFieldNames;
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._allFieldNames,
+ this._matchingProfiles);
};
ProfileAutoCompleteResult.prototype = {
// The user's query string
searchString: "",
// The default item that should be entered if none is selected
defaultIndex: 0,
// The reason the search failed
errorDescription: "",
// The result code of this result object.
searchResult: null,
- // The autocomplete attribute of the focused input field
- _fieldName: "",
+ // The field name of the focused input.
+ _focusedFieldName: "",
+
+ // All field names in the form which contains the focused input.
+ _allFieldNames: null,
// The matching profiles contains the information for filling forms.
_matchingProfiles: null,
+ // An array of primary and secondary labels for each profiles.
+ _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);
}
},
/**
+ * Get the secondary label based on the focused field name and related field names
+ * in the same form.
+ * @param {string} focusedFieldName The field name of the focused input
+ * @param {Array<Object>} allFieldNames The field names in the same section
+ * @param {object} profile The profile providing the labels to show.
+ * @returns {string} The secondary label
+ */
+ _getSecondaryLabel(focusedFieldName, allFieldNames, 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 = ["given-name", "additional-name", "family-name"];
+ focusedFieldName = possibleNameFields.includes(focusedFieldName) ?
+ "name" : focusedFieldName;
+ if (!profile.name) {
+ profile.name = FormAutofillUtils.generateFullName(profile["given-name"],
+ profile["family-name"],
+ profile["additional-name"]);
+ }
+
+ const secondaryLabelOrder = [
+ "street-address", // Street address
+ "name", // Full name if needed
+ "address-level2", // City/Town
+ "organization", // Company or organization name
+ "address-level1", // Province/State (Standardized code if possible)
+ "country", // Country
+ "postal-code", // Postal code
+ "tel", // Phone number
+ "email", // Email address
+ ];
+
+ for (const currentFieldName of secondaryLabelOrder) {
+ if (focusedFieldName != currentFieldName &&
+ allFieldNames.includes(currentFieldName) &&
+ profile[currentFieldName]) {
+ return profile[currentFieldName];
+ }
+ }
+
+ return ""; // Nothing matched.
+ },
+
+ _generateLabels(focusedFieldName, allFieldNames, profiles) {
+ return profiles.map(profile => {
+ return {
+ primary: profile[focusedFieldName],
+ secondary: this._getSecondaryLabel(focusedFieldName,
+ allFieldNames,
+ 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) {