Bug 1300996 - Part 2: Show preview text on and highlight the fields that would be filled. r=MattN, lchang
MozReview-Commit-ID: DMgVhz2lvZ1
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -24,16 +24,18 @@ FormAutofillUtils.defineLazyLogGetter(th
/**
* Handles profile autofill for a DOM Form element.
* @param {FormLike} form Form that need to be auto filled
*/
function FormAutofillHandler(form) {
this.form = form;
this.fieldDetails = [];
+ this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
}
FormAutofillHandler.prototype = {
/**
* DOM Form element to which this object is attached.
*/
form: null,
@@ -53,16 +55,33 @@ FormAutofillHandler.prototype = {
fieldDetails: null,
/**
* String of the filled profile's guid.
*/
filledProfileGUID: null,
/**
+ * A WindowUtils reference of which Window the form belongs
+ */
+ winUtils: null,
+
+ /**
+ * Enum for form autofill MANUALLY_MANAGED_STATES values
+ */
+ fieldStateEnum: {
+ // not themed
+ NORMAL: null,
+ // highlighted
+ AUTO_FILLED: "-moz-autofill",
+ // highlighted && grey color text
+ PREVIEW: "-moz-autofill-preview",
+ },
+
+ /**
* Set fieldDetails from the form about fields that can be autofilled.
*/
collectFormFields() {
let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form);
this.fieldDetails = fieldDetails ? fieldDetails : [];
log.debug("Collected details on", this.fieldDetails.length, "fields");
},
@@ -82,81 +101,131 @@ FormAutofillHandler.prototype = {
for (let fieldDetail of this.fieldDetails) {
// Avoid filling field value in the following cases:
// 1. the focused input which is filled in FormFillController.
// 2. a non-empty input field
// 3. the invalid value set
// 4. value already chosen in select element
let element = fieldDetail.elementWeakRef.get();
- if (!element || element === focusedInput) {
+ if (!element) {
continue;
}
let value = profile[fieldDetail.fieldName];
- if (element instanceof Ci.nsIDOMHTMLInputElement && value) {
- if (element.value) {
- continue;
+ if (element instanceof Ci.nsIDOMHTMLInputElement && !element.value && value) {
+ if (element !== focusedInput) {
+ element.setUserInput(value);
}
- element.setUserInput(value);
+ this.changeFieldState(fieldDetail, "AUTO_FILLED");
} else if (element instanceof Ci.nsIDOMHTMLSelectElement) {
for (let option of element.options) {
if (value === option.textContent || value === option.value) {
// Do not change value if the option is already selected.
// Use case for multiple select is not considered here.
if (option.selected) {
break;
}
// TODO: Using dispatchEvent does not 100% simulate select change.
// Should investigate further in Bug 1365895.
option.selected = true;
element.dispatchEvent(new Event("input", {"bubbles": true}));
element.dispatchEvent(new Event("change", {"bubbles": true}));
+ this.changeFieldState(fieldDetail, "AUTO_FILLED");
break;
}
}
}
+ element.previewValue = "";
}
},
/**
* Populates result to the preview layers with given profile.
*
* @param {Object} profile
* A profile to be previewed with
*/
previewFormFields(profile) {
log.debug("preview profile in autofillFormFields:", profile);
- /*
+
for (let fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.elementWeakRef.get();
let value = profile[fieldDetail.fieldName] || "";
- // Skip the fields that already has text entered
- if (fieldDetail.element.value) {
+ // Skip the field that is null or already has text entered
+ if (!element || element.value) {
+ continue;
+ }
+
+ element.previewValue = value;
+ this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
+ }
+ },
+
+ /**
+ * Clear preview text and background highlight of all fields.
+ */
+ clearPreviewedFormFields() {
+ log.debug("clear previewed fields in:", this.form);
+
+ for (let fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.elementWeakRef.get();
+ if (!element) {
+ log.warn(fieldDetail.fieldName, "is unreachable");
+ continue;
+ }
+
+ element.previewValue = "";
+
+ // We keep the state if this field has
+ // already been auto-filled.
+ if (fieldDetail.state === "AUTO_FILLED") {
continue;
}
- // TODO: Set highlight style and preview text.
+ this.changeFieldState(fieldDetail, "NORMAL");
}
- */
},
- clearPreviewedFormFields() {
- log.debug("clear previewed fields in:", this.form);
- /*
- for (let fieldDetail of this.fieldDetails) {
- // TODO: Clear preview text
+ /**
+ * Change the state of a field to correspond with different presentations.
+ *
+ * @param {Object} fieldDetail
+ * A fieldDetail of which its element is about to update the state.
+ * @param {string} nextState
+ * Used to determine the next state
+ */
+ changeFieldState(fieldDetail, nextState) {
+ let element = fieldDetail.elementWeakRef.get();
- // We keep the highlight of all fields if this form has
- // already been auto-filled with a profile.
- if (this.filledProfileGUID == null) {
- // TODO: Remove highlight style
+ if (!element) {
+ log.warn(fieldDetail.fieldName, "is unreachable while changing state");
+ return;
+ }
+ if (!(nextState in this.fieldStateEnum)) {
+ log.warn(fieldDetail.fieldName, "is trying to change to an invalid state");
+ return;
+ }
+
+ for (let [state, mmStateValue] of Object.entries(this.fieldStateEnum)) {
+ // The NORMAL state is simply the absence of other manually
+ // managed states so we never need to add or remove it.
+ if (!mmStateValue) {
+ continue;
+ }
+
+ if (state == nextState) {
+ this.winUtils.addManuallyManagedState(element, mmStateValue);
+ } else {
+ this.winUtils.removeManuallyManagedState(element, mmStateValue);
}
}
- */
+
+ fieldDetail.state = nextState;
},
/**
* Return the profile that is converted from fieldDetails and only non-empty fields
* are included.
*
* @returns {Object} The new profile that convert from details with trimmed result.
*/
--- a/browser/extensions/formautofill/skin/shared/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/shared/autocomplete-item.css
@@ -30,16 +30,17 @@ xul|richlistitem[originaltype="autofill-
border-bottom: 1px solid rgba(38,38,38,.15);
padding: var(--item-padding-vertical) 0;
padding-inline-start: var(--item-padding-horizontal);
padding-inline-end: var(--item-padding-horizontal);
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
+ background-color: #FFFFFF;
color: -moz-FieldText
}
.profile-item-box:last-child {
border-bottom: 0;
}
.profile-item-box > .profile-item-col {
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -6,16 +6,17 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/FormLikeFactory.jsm");
Cu.import("resource://testing-common/MockDocument.jsm");
Cu.import("resource://testing-common/TestUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
@@ -75,17 +76,16 @@ function getTempFile(leafName) {
file.remove(false);
}
});
return file;
}
function runHeuristicsTest(patterns, fixturePathPrefix) {
- Cu.import("resource://gre/modules/FormLikeFactory.jsm");
Cu.import("resource://formautofill/FormAutofillHeuristics.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
patterns.forEach(testPattern => {
add_task(function* () {
do_print("Starting test fixture: " + testPattern.fixturePath);
let file = do_get_file(fixturePathPrefix + testPattern.fixturePath);
let doc = MockDocument.createTestDocumentFromFile("http://localhost:8080/test/", file);
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -255,17 +255,18 @@ function do_test(testcases, testFn) {
(function() {
let testcase = tc;
add_task(async function() {
do_print("Starting testcase: " + testcase.description);
let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
testcase.document);
let form = doc.querySelector("form");
- let handler = new FormAutofillHandler(form);
+ let formLike = FormLikeFactory.createFromForm(form);
+ let handler = new FormAutofillHandler(formLike);
let promises = [];
handler.fieldDetails = testcase.fieldDetails;
handler.fieldDetails.forEach((field, index) => {
let element = doc.querySelectorAll("input, select")[index];
field.elementWeakRef = Cu.getWeakReference(element);
if (!testcase.profileData[field.fieldName]) {
// Avoid waiting for `change` event of a input with a blank value to
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -86,27 +86,28 @@ for (let tc of TESTCASES) {
(function() {
let testcase = tc;
add_task(function* () {
do_print("Starting testcase: " + testcase.description);
let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
testcase.document);
let form = doc.querySelector("form");
+ let formLike = FormLikeFactory.createFromForm(form);
testcase.fieldDetails.forEach((detail, index) => {
let elementRef;
if (testcase.ids && testcase.ids[index]) {
elementRef = doc.getElementById(testcase.ids[index]);
} else {
elementRef = doc.querySelector("*[autocomplete*='" + detail.fieldName + "']");
}
detail.elementWeakRef = Cu.getWeakReference(elementRef);
});
- let handler = new FormAutofillHandler(form);
+ let handler = new FormAutofillHandler(formLike);
handler.collectFormFields();
handler.fieldDetails.forEach((detail, index) => {
Assert.equal(detail.section, testcase.fieldDetails[index].section);
Assert.equal(detail.addressType, testcase.fieldDetails[index].addressType);
Assert.equal(detail.contactType, testcase.fieldDetails[index].contactType);
Assert.equal(detail.fieldName, testcase.fieldDetails[index].fieldName);
--- a/dom/html/nsTextEditorState.cpp
+++ b/dom/html/nsTextEditorState.cpp
@@ -2812,19 +2812,16 @@ nsTextEditorState::UpdatePlaceholderText
nsContentUtils::RemoveNewlines(placeholderValue);
NS_ASSERTION(mPlaceholderDiv->GetFirstChild(), "placeholder div has no child");
mPlaceholderDiv->GetFirstChild()->SetText(placeholderValue, aNotify);
}
void
nsTextEditorState::SetPreviewText(const nsAString& aValue, bool aNotify)
{
- MOZ_ASSERT(mPreviewDiv, "This function should not be called if "
- "mPreviewDiv isn't set");
-
// If we don't have a preview div, there's nothing to do.
if (!mPreviewDiv)
return;
nsAutoString previewValue(aValue);
nsContentUtils::RemoveNewlines(previewValue);
MOZ_ASSERT(mPreviewDiv->GetFirstChild(), "preview div has no child");
--- a/toolkit/modules/tests/modules/MockDocument.jsm
+++ b/toolkit/modules/tests/modules/MockDocument.jsm
@@ -4,31 +4,37 @@
"use strict";
this.EXPORTED_SYMBOLS = ["MockDocument"]
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.importGlobalProperties(["URL"]);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const MockDocument = {
/**
* Create a document for the given URL containing the given HTML with the ownerDocument of all <form>s having a mocked location.
*/
createTestDocument(aDocumentURL, aContent = "<form>", aType = "text/html") {
let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
createInstance(Ci.nsIDOMParser);
parser.init();
let parsedDoc = parser.parseFromString(aContent, aType);
+ // Assign onwerGlobal to documentElement as well for the form-less
+ // inputs treating it as rootElement.
+ this.mockOwnerGlobalProperty(parsedDoc.documentElement);
+
for (let element of parsedDoc.forms) {
this.mockOwnerDocumentProperty(element, parsedDoc, aDocumentURL);
+ this.mockOwnerGlobalProperty(element);
}
return parsedDoc;
},
mockOwnerDocumentProperty(aElement, aDoc, aURL) {
// Mock the document.location object so we can unit test without a frame. We use a proxy
// instead of just assigning to the property since it's not configurable or writable.
let document = new Proxy(aDoc, {
@@ -43,16 +49,29 @@ const MockDocument = {
});
// Assign element.ownerDocument to the proxy so document.location works.
Object.defineProperty(aElement, "ownerDocument", {
value: document,
});
},
+ mockOwnerGlobalProperty(aElement) {
+ Object.defineProperty(aElement, "ownerGlobal", {
+ value: {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
+ getInterface: () => ({
+ addManuallyManagedState() {},
+ removeManuallyManagedState() {},
+ }),
+ },
+ configurable: true,
+ });
+ },
+
createTestDocumentFromFile(aDocumentURL, aFile) {
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fileStream.init(aFile, -1, -1, 0);
let data = NetUtil.readInputStreamToString(fileStream, fileStream.available());
return this.createTestDocument(aDocumentURL, data);