Bug 1300996 - Part 2: Show preview text on and highlight the fields that would be filled. r=MattN, lchang draft
authorRay Lin <ralin@mozilla.com>
Mon, 24 Apr 2017 10:55:29 +0800
changeset 591433 7394e1e0407829fcc4604d764e8cfd8afc326ef3
parent 590888 e0af703a4a6291c53995fd00319bf8a6b259109c
child 591434 bfdab8771859c820704bb7f01d72c7782298a4b6
push id63058
push userbmo:ralin@mozilla.com
push dateFri, 09 Jun 2017 03:19:03 +0000
reviewersMattN, lchang
bugs1300996
milestone55.0a1
Bug 1300996 - Part 2: Show preview text on and highlight the fields that would be filled. r=MattN, lchang MozReview-Commit-ID: DMgVhz2lvZ1
browser/extensions/formautofill/FormAutofillHandler.jsm
browser/extensions/formautofill/skin/shared/autocomplete-item.css
browser/extensions/formautofill/test/unit/head.js
browser/extensions/formautofill/test/unit/test_autofillFormFields.js
browser/extensions/formautofill/test/unit/test_collectFormFields.js
dom/html/nsTextEditorState.cpp
toolkit/modules/tests/modules/MockDocument.jsm
--- 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);