Bug 1417803 - Part 3: Use _activeItems to record the information of the current focused input. r=lchang,ralin draft
authorSean Lee <selee@mozilla.com>
Mon, 11 Dec 2017 19:28:47 +0800
changeset 712914 1f095c98771c29c317639a4636c82a52bb56e851
parent 712913 64be11aca32ce8b0b9b5097eecc4a9b412bc03be
child 744189 f8a7974dbaf8fb08009b1401352e7e5a75d33273
push id93484
push userbmo:selee@mozilla.com
push dateTue, 19 Dec 2017 03:33:17 +0000
reviewerslchang, ralin
bugs1417803
milestone59.0a1
Bug 1417803 - Part 3: Use _activeItems to record the information of the current focused input. r=lchang,ralin MozReview-Commit-ID: 4j72RXk2Wpb
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/test/unit/test_getFormInputDetails.js
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -90,45 +90,40 @@ 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) {
-    this.log.debug("startSearch: for", searchString, "with input", formFillController.focusedInput);
-
+    let {activeInput, activeSection, activeFieldDetail, savedFieldNames} = FormAutofillContent;
     this.forceStop = false;
 
-    let savedFieldNames = FormAutofillContent.savedFieldNames;
+    this.log.debug("startSearch: for", searchString, "with input", activeInput);
 
-    let focusedInput = formFillController.focusedInput;
-    let info = FormAutofillContent.getInputDetails(focusedInput);
-    let isAddressField = FormAutofillUtils.isAddressField(info.fieldName);
-    let isInputAutofilled = info.state == FIELD_STATES.AUTO_FILLED;
-    let handler = FormAutofillContent.getFormHandler(focusedInput);
-    let activeSection = handler.activeSection;
+    let isAddressField = FormAutofillUtils.isAddressField(activeFieldDetail.fieldName);
+    let isInputAutofilled = activeFieldDetail.state == FIELD_STATES.AUTO_FILLED;
     let allFieldNames = activeSection.allFieldNames;
     let filledRecordGUID = activeSection.getFilledRecordGUID();
     let searchPermitted = isAddressField ?
                           FormAutofillUtils.isAutofillAddressesEnabled :
                           FormAutofillUtils.isAutofillCreditCardsEnabled;
     let AutocompleteResult = isAddressField ? AddressResult : CreditCardResult;
 
-    ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = focusedInput;
+    ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput;
     // Fallback to form-history if ...
     //   - specified autofill feature is pref off.
     //   - no profile can fill the currently-focused input.
     //   - the current form has already been populated.
     //   - (address only) less than 3 inputs are covered by all saved fields in the storage.
-    if (!searchPermitted || !savedFieldNames.has(info.fieldName) ||
+    if (!searchPermitted || !savedFieldNames.has(activeFieldDetail.fieldName) ||
         (!isInputAutofilled && filledRecordGUID) || (isAddressField &&
         allFieldNames.filter(field => savedFieldNames.has(field)).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)) {
-      if (focusedInput.autocomplete == "off") {
+      if (activeInput.autocomplete == "off") {
         // Create a dummy result as an empty search result.
         let result = new AutocompleteResult("", "", [], [], {});
         listener.onSearchResult(this, result);
         return;
       }
       let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"]
                           .createInstance(Ci.nsIAutoCompleteSearch);
       formHistory.startSearch(searchString, searchParam, previousResult, {
@@ -142,17 +137,17 @@ AutofillProfileAutoCompleteSearch.protot
 
     if (isInputAutofilled) {
       let result = new AutocompleteResult(searchString, "", [], [], {isInputAutofilled});
       listener.onSearchResult(this, result);
       ProfileAutocomplete.lastProfileAutoCompleteResult = result;
       return;
     }
 
-    let infoWithoutElement = Object.assign({}, info);
+    let infoWithoutElement = Object.assign({}, activeFieldDetail);
     delete infoWithoutElement.elementWeakRef;
 
     let data = {
       collectionName: isAddressField ? ADDRESSES_COLLECTION_NAME : CREDITCARDS_COLLECTION_NAME,
       info: infoWithoutElement,
       searchString,
     };
 
@@ -160,20 +155,21 @@ AutofillProfileAutoCompleteSearch.protot
       if (this.forceStop) {
         return;
       }
       // Sort addresses by timeLastUsed for showing the lastest used address at top.
       records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
 
       let adaptedRecords = activeSection.getAdaptedProfiles(records);
       let result = null;
+      let handler = FormAutofillContent.activeHandler;
       let isSecure = InsecurePasswordUtils.isFormSecure(handler.form);
 
       result = new AutocompleteResult(searchString,
-                                      info.fieldName,
+                                      activeFieldDetail.fieldName,
                                       allFieldNames,
                                       adaptedRecords,
                                       {isSecure, isInputAutofilled});
       listener.onSearchResult(this, result);
       ProfileAutocomplete.lastProfileAutoCompleteResult = result;
     });
   },
 
@@ -249,21 +245,21 @@ let ProfileAutocomplete = {
     this._lastAutoCompleteResult = null;
 
     Services.obs.removeObserver(this, "autocomplete-will-enter-text");
   },
 
   observe(subject, topic, data) {
     switch (topic) {
       case "autocomplete-will-enter-text": {
-        if (!formFillController.focusedInput) {
+        if (!FormAutofillContent.activeInput) {
           // The observer notification is for autocomplete in a different process.
           break;
         }
-        this._fillFromAutocompleteRow(formFillController.focusedInput);
+        this._fillFromAutocompleteRow(FormAutofillContent.activeInput);
         break;
       }
     }
   },
 
   _frameMMFromWindow(contentWindow) {
     return contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell)
@@ -278,65 +274,58 @@ let ProfileAutocomplete = {
       throw new Error("Invalid autocomplete selectedIndex");
     }
 
     return selectedIndexResult[0];
   },
 
   _fillFromAutocompleteRow(focusedInput) {
     this.log.debug("_fillFromAutocompleteRow:", focusedInput);
-    let formDetails = FormAutofillContent.getFormDetails(focusedInput);
+    let formDetails = FormAutofillContent.activeFormDetails;
     if (!formDetails) {
       // The observer notification is for a different frame.
       return;
     }
 
     let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal);
     if (selectedIndex == -1 ||
         !this.lastProfileAutoCompleteResult ||
         this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));
-    let {fieldName} = FormAutofillContent.getInputDetails(focusedInput);
-    let formHandler = FormAutofillContent.getFormHandler(focusedInput);
+    let {fieldName} = FormAutofillContent.activeFieldDetail;
 
-    formHandler.autofillFormFields(profile).then(() => {
+    FormAutofillContent.activeHandler.autofillFormFields(profile).then(() => {
       autocompleteController.searchString = profile[fieldName];
     });
   },
 
   _clearProfilePreview() {
-    let focusedInput = formFillController.focusedInput || this.lastProfileAutoCompleteFocusedInput;
-    if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
+    if (!this.lastProfileAutoCompleteFocusedInput || !FormAutofillContent.activeSection) {
       return;
     }
 
-    let formHandler = FormAutofillContent.getFormHandler(focusedInput);
-
-    formHandler.activeSection.clearPreviewedFormFields();
+    FormAutofillContent.activeSection.clearPreviewedFormFields();
   },
 
   _previewSelectedProfile(selectedIndex) {
-    let focusedInput = formFillController.focusedInput;
-    if (!focusedInput || !FormAutofillContent.getFormDetails(focusedInput)) {
+    if (!FormAutofillContent.activeInput || !FormAutofillContent.activeFormDetails) {
       // The observer notification is for a different process/frame.
       return;
     }
 
     if (!this.lastProfileAutoCompleteResult ||
         this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       return;
     }
 
     let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));
-    let formHandler = FormAutofillContent.getFormHandler(focusedInput);
-
-    formHandler.activeSection.previewFormFields(profile);
+    FormAutofillContent.activeSection.previewFormFields(profile);
   },
 };
 
 /**
  * Handles content's interactions for the process.
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
@@ -347,16 +336,22 @@ var FormAutofillContent = {
    */
   _formsDetails: new WeakMap(),
 
   /**
    * @type {Set} Set of the fields with usable values in any saved profile.
    */
   savedFieldNames: null,
 
+  /**
+   * @type {Object} The object where to store the active items, e.g. element,
+   * handler, section, and field detail.
+   */
+  _activeItems: {},
+
   init() {
     FormAutofillUtils.defineLazyLogGetter(this, "FormAutofillContent");
 
     Services.cpmm.addMessageListener("FormAutofill:enabledStatus", this);
     Services.cpmm.addMessageListener("FormAutofill:savedFieldNames", this);
     Services.obs.addObserver(this, "earlyformsubmit");
 
     let autofillEnabled = Services.cpmm.initialProcessData.autofillEnabled;
@@ -437,125 +432,157 @@ var FormAutofillContent = {
       }
       case "FormAutofill:savedFieldNames": {
         this.savedFieldNames = data;
       }
     }
   },
 
   /**
-   * Get the input's information from cache which is created after page identified.
-   *
-   * @param {HTMLInputElement} element Focused input which triggered profile searching
-   * @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) {
-    let formDetails = this.getFormDetails(element);
-    for (let detail of formDetails) {
-      let detailElement = detail.elementWeakRef.get();
-      if (detailElement && element == detailElement) {
-        return detail;
-      }
-    }
-    return null;
-  },
-
-  /**
    * Get the form's handler from cache which is created after page identified.
    *
    * @param {HTMLInputElement} element Focused input which triggered profile searching
    * @returns {Array<Object>|null}
    *          Return target form's handler from content cache
    *          (or return null if the information is not found in the cache).
    *
    */
-  getFormHandler(element) {
+  _getFormHandler(element) {
     let rootElement = FormLikeFactory.findRootForField(element);
     return this._formsDetails.get(rootElement);
   },
 
   /**
-   * Get the form's information from cache which is created after page identified.
+   * Get the active form's information from cache which is created after page
+   * identified.
    *
-   * @param {HTMLInputElement} element Focused input which triggered profile searching
    * @returns {Array<Object>|null}
    *          Return target form's information from content cache
    *          (or return null if the information is not found in the cache).
    *
    */
-  getFormDetails(element) {
-    let formHandler = this.getFormHandler(element);
+  get activeFormDetails() {
+    let formHandler = this.activeHandler;
     return formHandler ? formHandler.fieldDetails : null;
   },
 
-  getAllFieldNames(element) {
-    let formHandler = this.getFormHandler(element);
-    return formHandler ? formHandler.activeSection.allFieldNames : null;
+  /**
+   * All active items should be updated according the active element of
+   * `formFillController.focusedInput`. All of them including element,
+   * handler, section, and field detail, can be retrieved by their own getters.
+   *
+   * @param {HTMLElement|null} element The active item should be updated based
+   * on this or `formFillController.focusedInput` will be taken.
+   */
+  updateActiveInput(element) {
+    element = element || formFillController.focusedInput;
+    let handler = this._getFormHandler(element);
+    if (handler) {
+      handler.focusedInput = element;
+    }
+    this._activeItems = {
+      handler,
+      elementWeakRef: Cu.getWeakReference(element),
+      section: handler ? handler.activeSection : null,
+      fieldDetail: null,
+    };
+  },
+
+  get activeInput() {
+    return this._activeItems.elementWeakRef.get();
+  },
+
+  get activeHandler() {
+    return this._activeItems.handler;
+  },
+
+  get activeSection() {
+    return this._activeItems.section;
+  },
+
+  /**
+   * Get the active input's information from cache which is created after page
+   * identified.
+   *
+   * @returns {Object|null}
+   *          Return the active input's information that cloned from content cache
+   *          (or return null if the information is not found in the cache).
+   */
+  get activeFieldDetail() {
+    if (!this._activeItems.fieldDetail) {
+      let formDetails = this.activeFormDetails;
+      if (!formDetails) {
+        return null;
+      }
+      for (let detail of formDetails) {
+        let detailElement = detail.elementWeakRef.get();
+        if (detailElement && this.activeInput == detailElement) {
+          this._activeItems.fieldDetail = detail;
+          break;
+        }
+      }
+    }
+    return this._activeItems.fieldDetail;
   },
 
   identifyAutofillFields(element) {
     this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location);
 
     if (!this.savedFieldNames) {
       this.log.debug("identifyAutofillFields: savedFieldNames are not known yet");
       Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
     }
 
-    let formHandler = this.getFormHandler(element);
+    let formHandler = this._getFormHandler(element);
     if (!formHandler) {
       let formLike = FormLikeFactory.createFromField(element);
       formHandler = new FormAutofillHandler(formLike);
     } else if (!formHandler.updateFormIfNeeded(element)) {
-      formHandler.focusedInput = element;
       this.log.debug("No control is removed or inserted since last collection.");
       return;
     }
 
     let validDetails = formHandler.collectFormFields();
 
     this._formsDetails.set(formHandler.form.rootElement, formHandler);
     this.log.debug("Adding form handler to _formsDetails:", formHandler);
 
     validDetails.forEach(detail =>
       this._markAsAutofillField(detail.elementWeakRef.get())
     );
-    formHandler.focusedInput = element;
   },
 
   clearForm() {
-    let focusedInput = formFillController.focusedInput || ProfileAutocomplete._lastAutoCompleteFocusedInput;
+    let focusedInput = this.activeInput || ProfileAutocomplete._lastAutoCompleteFocusedInput;
     if (!focusedInput) {
       return;
     }
 
-    let formHandler = this.getFormHandler(focusedInput);
-    formHandler.activeSection.clearPopulatedForm();
+    this.activeSection.clearPopulatedForm();
     autocompleteController.searchString = "";
   },
 
   previewProfile(doc) {
     let docWin = doc.ownerGlobal;
     let selectedIndex = ProfileAutocomplete._getSelectedIndex(docWin);
     let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
-    let focusedInput = formFillController.focusedInput;
+    let focusedInput = this.activeInput;
     let mm = this._messageManagerFromWindow(docWin);
 
     if (selectedIndex === -1 ||
         !focusedInput ||
         !lastAutoCompleteResult ||
         lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {});
 
       ProfileAutocomplete._clearProfilePreview();
     } else {
-      let focusedInputDetails = this.getInputDetails(focusedInput);
+      let focusedInputDetails = this.activeFieldDetail;
       let profile = JSON.parse(lastAutoCompleteResult.getCommentAt(selectedIndex));
-      let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
+      let allFieldNames = FormAutofillContent.activeSection.allFieldNames;
       let profileFields = allFieldNames.filter(fieldName => !!profile[fieldName]);
 
       let focusedCategory = FormAutofillUtils.getCategoryFromFieldName(focusedInputDetails.fieldName);
       let categories = FormAutofillUtils.getCategoriesFromFieldNames(profileFields);
       mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {
         focusedCategory,
         categories,
       });
@@ -583,17 +610,17 @@ var FormAutofillContent = {
               .getInterface(Ci.nsIWebNavigation)
               .QueryInterface(Ci.nsIDocShell)
               .QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIContentFrameMessageManager);
   },
 
   _onKeyDown(e) {
     let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
-    let focusedInput = formFillController.focusedInput;
+    let focusedInput = FormAutofillContent.activeInput;
 
     if (e.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_RETURN || !lastAutoCompleteResult ||
         !focusedInput || focusedInput != ProfileAutocomplete.lastProfileAutoCompleteFocusedInput) {
       return;
     }
 
     let selectedIndex = ProfileAutocomplete._getSelectedIndex(e.target.ownerGlobal);
     let selectedRowStyle = lastAutoCompleteResult.getStyleAt(selectedIndex);
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -34,31 +34,33 @@ var FormAutofillFrameScript = {
 
     setTimeout(() => {
       FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
       this._hasPendingTask = false;
       this._nextHandleElement = null;
       // This is for testing purpose only which sends a message to indicate that the
       // form has been identified, and ready to open popup.
       sendAsyncMessage("FormAutofill:FieldsIdentified");
+      FormAutofillContent.updateActiveInput();
     });
   },
 
   init() {
     addEventListener("focusin", this);
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutofill:ClearForm", this);
     addMessageListener("FormAutoComplete:PopupClosed", this);
     addMessageListener("FormAutoComplete:PopupOpened", this);
   },
 
   handleEvent(evt) {
     if (!evt.isTrusted || !FormAutofillUtils.isAutofillEnabled) {
       return;
     }
+    FormAutofillContent.updateActiveInput();
 
     let element = evt.target;
     if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
       return;
     }
     this._nextHandleElement = element;
 
     if (!this._alreadyDOMContentLoaded) {
--- a/browser/extensions/formautofill/test/unit/test_getFormInputDetails.js
+++ b/browser/extensions/formautofill/test/unit/test_getFormInputDetails.js
@@ -83,30 +83,31 @@ TESTCASES.forEach(testcase => {
     do_print("Starting testcase: " + testcase.description);
 
     let doc = MockDocument.createTestDocument(
               "http://localhost:8080/test/", testcase.document);
 
     for (let i in testcase.targetInput) {
       let input = doc.getElementById(testcase.targetInput[i]);
       FormAutofillContent.identifyAutofillFields(input);
+      FormAutofillContent.updateActiveInput(input);
 
       // Put the input element reference to `element` to make sure the result of
-      // `getInputDetails` contains the same input element.
+      // `activeFieldDetail` contains the same input element.
       testcase.expectedResult[i].input.elementWeakRef = Cu.getWeakReference(input);
 
-      inputDetailAssertion(FormAutofillContent.getInputDetails(input),
+      inputDetailAssertion(FormAutofillContent.activeFieldDetail,
                            testcase.expectedResult[i].input);
 
       let formDetails = testcase.expectedResult[i].form;
       for (let formDetail of formDetails) {
         // Compose a query string to get the exact reference of <input>/<select>
         // element, e.g. #form1 > *[autocomplete="street-address"]
         let queryString = "#" + testcase.expectedResult[i].formId + " > *[autocomplete=" + formDetail.fieldName + "]";
         formDetail.elementWeakRef = Cu.getWeakReference(doc.querySelector(queryString));
       }
 
-      FormAutofillContent.getFormDetails(input).forEach((detail, index) => {
+      FormAutofillContent.activeFormDetails.forEach((detail, index) => {
         inputDetailAssertion(detail, formDetails[index]);
       });
     }
   });
 });