Bug 1304501 - Properly disable trimUrl on autofill. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 21 Sep 2016 21:55:00 +0200
changeset 425627 f6519c6f245199db550e2d999931c65154d38427
parent 425549 de5d73a0568d1c3d50da32169026cc68ee09b1ae
child 533968 30f3ef5ea98fc840e6422a0439a7cde2a393cc9c
push id32478
push usermak77@bonardo.net
push dateSat, 15 Oct 2016 08:56:27 +0000
reviewersadw
bugs1304501
milestone52.0a1
Bug 1304501 - Properly disable trimUrl on autofill. r=adw MozReview-Commit-ID: IxCOWkqFYV0
browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
browser/base/content/test/urlbar/browser_urlbarDecode.js
browser/base/content/urlbarBindings.xml
toolkit/components/autocomplete/nsAutoCompleteController.cpp
toolkit/components/autocomplete/nsIAutoCompleteInput.idl
toolkit/components/satchel/nsFormFillController.cpp
toolkit/content/widgets/autocomplete.xml
--- a/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
@@ -1,81 +1,49 @@
-/* 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/. */
-
 // This test ensures that autoFilled values are not trimmed, unless the user
 // selects from the autocomplete popup.
 
-function test() {
-  waitForExplicitFinish();
-
+add_task(function* setup() {
   const PREF_TRIMURL = "browser.urlbar.trimURLs";
   const PREF_AUTOFILL = "browser.urlbar.autoFill";
 
-  registerCleanupFunction(function () {
+  registerCleanupFunction(function* () {
     Services.prefs.clearUserPref(PREF_TRIMURL);
     Services.prefs.clearUserPref(PREF_AUTOFILL);
+    yield PlacesTestUtils.clearHistory();
     gURLBar.handleRevert();
   });
   Services.prefs.setBoolPref(PREF_TRIMURL, true);
   Services.prefs.setBoolPref(PREF_AUTOFILL, true);
 
   // Adding a tab would hit switch-to-tab, so it's safer to just add a visit.
-  let callback = {
-    handleError:  function () {},
-    handleResult: function () {},
-    handleCompletion: continue_test
-  };
-  let history = Cc["@mozilla.org/browser/history;1"]
-                  .getService(Ci.mozIAsyncHistory);
-  history.updatePlaces({ uri: NetUtil.newURI("http://www.autofilltrimurl.com/whatever")
-                       , visits: [ { transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED
-                                   , visitDate:      Date.now() * 1000
-                                   } ]
-                       }, callback);
+  yield PlacesTestUtils.addVisits({
+    uri: "http://www.autofilltrimurl.com/whatever",
+    transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
+  });
+});
+
+function* promiseSearch(searchtext) {
+  gURLBar.focus();
+  gURLBar.inputField.value = searchtext.substr(0, searchtext.length -1);
+  EventUtils.synthesizeKey(searchtext.substr(-1, 1), {});
+  yield promiseSearchComplete();
 }
 
-function continue_test() {
-  function test_autoFill(aTyped, aExpected, aCallback) {
-    info(`Testing with input: ${aTyped}`);
-    gURLBar.inputField.value = aTyped.substr(0, aTyped.length - 1);
-    gURLBar.focus();
-    gURLBar.selectionStart = aTyped.length - 1;
-    gURLBar.selectionEnd = aTyped.length - 1;
+add_task(function* () {
+  yield promiseSearch("http://");
+  is(gURLBar.inputField.value, "http://", "Autofilled value is as expected");
+});
 
-    EventUtils.synthesizeKey(aTyped.substr(-1), {});
-    waitForSearchComplete(function () {
-      info(`Got value: ${gURLBar.textValue}`);
-      is(gURLBar.textValue, aExpected, "Autofilled value is as expected");
-      aCallback();
-    });
-  }
+add_task(function* () {
+  yield promiseSearch("http://au");
+  is(gURLBar.inputField.value, "http://autofilltrimurl.com/", "Autofilled value is as expected");
+});
 
-  test_autoFill("http://", "http://", function () {
-    test_autoFill("http://au", "http://autofilltrimurl.com/", function () {
-      test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () {
-        // Now ensure selecting from the popup correctly trims.
-        is(gURLBar.controller.matchCount, 2, "Found the expected number of matches");
-        EventUtils.synthesizeKey("VK_DOWN", {});
-        is(gURLBar.textValue, "www.autofilltrimurl.com/whatever", "trim was applied correctly");
-        gURLBar.closePopup();
-        PlacesTestUtils.clearHistory().then(finish);
-      });
-    });
-  });
-}
+add_task(function* () {
+  yield promiseSearch("http://www.autofilltrimurl.com");
+  is(gURLBar.inputField.value, "http://www.autofilltrimurl.com/", "Autofilled value is as expected");
 
-var gOnSearchComplete = null;
-function waitForSearchComplete(aCallback) {
-  info("Waiting for onSearchComplete");
-  if (!gOnSearchComplete) {
-    gOnSearchComplete = gURLBar.onSearchComplete;
-    registerCleanupFunction(() => {
-      gURLBar.onSearchComplete = gOnSearchComplete;
-    });
-  }
-  gURLBar.onSearchComplete = function () {
-    ok(gURLBar.popupOpen, "The autocomplete popup is correctly open");
-    gOnSearchComplete.apply(gURLBar);
-    aCallback();
-  }
-}
+  // Now ensure selecting from the popup correctly trims.
+  is(gURLBar.controller.matchCount, 2, "Found the expected number of matches");
+  EventUtils.synthesizeKey("VK_DOWN", {});
+  is(gURLBar.inputField.value, "www.autofilltrimurl.com/whatever", "trim was applied correctly");
+});
--- a/browser/base/content/test/urlbar/browser_urlbarDecode.js
+++ b/browser/base/content/test/urlbar/browser_urlbarDecode.js
@@ -20,19 +20,21 @@ add_task(function* injectJSON() {
     yield checkInput(inputStr);
   }
   gURLBar.value = "";
   gURLBar.handleRevert();
   gURLBar.blur();
 });
 
 add_task(function losslessDecode() {
-  let url = "http://example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+  let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
+  let url = "http://" + urlNoScheme;
   gURLBar.textValue = url;
-  Assert.equal(gURLBar.inputField.value, url,
+  // Since this is directly setting textValue, it is expected to be trimmed.
+  Assert.equal(gURLBar.inputField.value, urlNoScheme,
                "The string displayed in the textbox should not be escaped");
   gURLBar.value = "";
   gURLBar.handleRevert();
   gURLBar.blur();
 });
 
 add_task(function* actionURILosslessDecode() {
   let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -142,17 +142,17 @@ file, You can obtain one at http://mozil
 
       <!--
         onBeforeValueGet is called by the base-binding's .value getter.
         It can return an object with a "value" property, to override the
         return value of the getter.
       -->
       <method name="onBeforeValueGet">
         <body><![CDATA[
-          return {value: this._value};
+          return { value: this._value };
         ]]></body>
       </method>
 
       <!--
         onBeforeValueSet is called by the base-binding's .value setter.
         It should return the value that the setter should use.
       -->
       <method name="onBeforeValueSet">
@@ -914,63 +914,51 @@ file, You can obtain one at http://mozil
             case "underflow":
               this._contentIsCropped = false;
               this._hideURLTooltip();
               break;
           }
         ]]></body>
       </method>
 
-      <property name="textValue">
-        <getter><![CDATA[
-          return this.inputField.value;
-        ]]></getter>
-        <setter>
-          <![CDATA[
+      <!--
+        onBeforeTextValueSet is called by the base-binding's .textValue getter.
+        It should return the value that the getter should use.
+      -->
+      <method name="onBeforeTextValueGet">
+        <body><![CDATA[
+          return { value: this.inputField.value };
+        ]]></body>
+      </method>
+
+      <!--
+        onBeforeTextValueSet is called by the base-binding's .textValue setter.
+        It should return the value that the setter should use.
+      -->
+      <method name="onBeforeTextValueSet">
+        <parameter name="aValue"/>
+        <body><![CDATA[
+          let val = aValue;
           let uri;
           try {
             uri = makeURI(val);
           } catch (ex) {}
 
           if (uri) {
             // Do not touch moz-action URIs at all.  They depend on being
             // properly encoded and decoded and will break if decoded
             // unexpectedly.
             if (!this._parseActionUrl(val)) {
               val = losslessDecodeURI(uri);
             }
           }
 
-          // Trim popup selected values, but never trim results coming from
-          // autofill.
-          let styles = new Set(
-            this.popup.selectedIndex == -1 ? [] :
-            this.mController.getStyleAt(this.popup.selectedIndex).split(/\s+/)
-          );
-          if (this.popup.selectedIndex == -1 ||
-              this.mController
-                  .getStyleAt(this.popup.selectedIndex)
-                  .split(/\s+/).indexOf("autofill") >= 0) {
-            this._disableTrim = true;
-          }
-          this.value = val;
-          this._disableTrim = false;
-
-          // Completing a result should simulate the user typing the result, so
-          // fire an input event.
-          let evt = document.createEvent("UIEvents");
-          evt.initUIEvent("input", true, false, window, 0);
-          this.mIgnoreInput = true;
-          this.dispatchEvent(evt);
-          this.mIgnoreInput = false;
-
-          return this.value;
-          ]]>
-        </setter>
-      </property>
+          return val;
+        ]]></body>
+      </method>
 
       <method name="_parseActionUrl">
         <parameter name="aUrl"/>
         <body><![CDATA[
           const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
           if (!MOZ_ACTION_REGEX.test(aUrl))
             return null;
 
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -18,16 +18,30 @@
 #include "nsITreeColumns.h"
 #include "nsIObserverService.h"
 #include "nsIDOMKeyEvent.h"
 #include "mozilla/Services.h"
 #include "mozilla/ModuleUtils.h"
 
 static const char *kAutoCompleteSearchCID = "@mozilla.org/autocomplete/search;1?name=";
 
+namespace {
+
+void
+SetTextValue(nsIAutoCompleteInput* aInput,
+             const nsString& aValue,
+             uint16_t aReason) {
+  nsresult rv = aInput->SetTextValueWithReason(aValue, aReason);
+  if (NS_FAILED(rv)) {
+    aInput->SetTextValue(aValue);
+  }
+}
+
+} // anon namespace
+
 NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController)
   tmp->SetInput(nullptr);
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsAutoCompleteController)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInput)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSearches)
@@ -478,26 +492,31 @@ nsAutoCompleteController::HandleKeyNavig
             // If the result is the previously autofilled string, then restore
             // the search string and selection that existed when the result was
             // autofilled.  Else, fill the result and move the caret to the end.
             int32_t start;
             if (value.Equals(mPlaceholderCompletionString,
                              nsCaseInsensitiveStringComparator())) {
               start = mSearchString.Length();
               value = mPlaceholderCompletionString;
+              SetTextValue(input, value,
+                           nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
             } else {
               start = value.Length();
+              SetTextValue(input, value,
+                           nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
             }
-            input->SetTextValue(value);
+
             input->SelectTextRange(start, value.Length());
           }
           mCompletedSelectionIndex = selectedIndex;
         } else {
           // Nothing is selected, so fill in the last typed value
-          input->SetTextValue(mSearchString);
+          SetTextValue(input, mSearchString,
+                       nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
           input->SelectTextRange(mSearchString.Length(), mSearchString.Length());
           mCompletedSelectionIndex = -1;
         }
       }
     } else {
 #ifdef XP_MACOSX
       // on Mac, only show the popup if the caret is at the start or end of
       // the input and there is no selection, so that the default defined key
@@ -582,17 +601,18 @@ nsAutoCompleteController::HandleKeyNavig
       int32_t selectedIndex;
       popup->GetSelectedIndex(&selectedIndex);
       bool shouldComplete;
       input->GetCompleteDefaultIndex(&shouldComplete);
       if (selectedIndex >= 0) {
         // The pop-up is open and has a selection, take its value
         nsAutoString value;
         if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
-          input->SetTextValue(value);
+          SetTextValue(input, value,
+                       nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
           input->SelectTextRange(value.Length(), value.Length());
         }
       }
       else if (shouldComplete) {
         // We usually try to preserve the casing of what user has typed, but
         // if he wants to autocomplete, we will replace the value with the
         // actual autocomplete result. Note that the autocomplete input can also
         // be showing e.g. "bar >> foo bar" if the search matched "bar", a
@@ -607,17 +627,18 @@ nsAutoCompleteController::HandleKeyNavig
           int32_t pos = inputValue.Find(" >> ");
           if (pos > 0) {
             inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
           } else {
             suggestedValue = inputValue;
           }
 
           if (value.Equals(suggestedValue, nsCaseInsensitiveStringComparator())) {
-            input->SetTextValue(value);
+            SetTextValue(input, value,
+                         nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
             input->SelectTextRange(value.Length(), value.Length());
           }
         }
       }
 
       // Close the pop-up even if nothing was selected
       ClearSearchTimer();
       ClosePopup();
@@ -1522,17 +1543,17 @@ nsAutoCompleteController::EnterMatch(boo
   }
 
   nsCOMPtr<nsIObserverService> obsSvc =
     mozilla::services::GetObserverService();
   NS_ENSURE_STATE(obsSvc);
   obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", nullptr);
 
   if (!value.IsEmpty()) {
-    input->SetTextValue(value);
+    SetTextValue(input, value, nsIAutoCompleteInput::TEXTVALUE_REASON_ENTERMATCH);
     input->SelectTextRange(value.Length(), value.Length());
     mSearchString = value;
   }
 
   obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
   ClosePopup();
 
   bool cancel;
@@ -1562,17 +1583,17 @@ nsAutoCompleteController::RevertTextValu
     NS_ENSURE_STATE(obsSvc);
     obsSvc->NotifyObservers(input, "autocomplete-will-revert-text", nullptr);
 
     nsAutoString inputValue;
     input->GetTextValue(inputValue);
     // Don't change the value if it is the same to prevent sending useless events.
     // NOTE: how can |RevertTextValue| be called with inputValue != oldValue?
     if (!oldValue.Equals(inputValue)) {
-      input->SetTextValue(oldValue);
+      SetTextValue(input, oldValue, nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
     }
 
     obsSvc->NotifyObservers(input, "autocomplete-did-revert-text", nullptr);
   }
 
   return NS_OK;
 }
 
@@ -1888,17 +1909,18 @@ nsAutoCompleteController::CompleteValue(
 
   if (aValue.IsEmpty() ||
       StringBeginsWith(aValue, mSearchString,
                        nsCaseInsensitiveStringComparator())) {
     // aValue is empty (we were asked to clear mInput), or mSearchString
     // matches the beginning of aValue.  In either case we can simply
     // autocomplete to aValue.
     mPlaceholderCompletionString = aValue;
-    input->SetTextValue(aValue);
+    SetTextValue(input, aValue,
+                 nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
   } else {
     nsresult rv;
     nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
     nsAutoCString scheme;
     if (NS_SUCCEEDED(ios->ExtractScheme(NS_ConvertUTF16toUTF8(aValue), scheme))) {
       // Trying to autocomplete a URI from somewhere other than the beginning.
       // Only succeed if the missing portion is "http://"; otherwise do not
@@ -1910,24 +1932,26 @@ nsAutoCompleteController::CompleteValue(
           !scheme.LowerCaseEqualsLiteral("http") ||
           !Substring(aValue, findIndex, mSearchStringLength).Equals(
             mSearchString, nsCaseInsensitiveStringComparator())) {
         return NS_OK;
       }
 
       mPlaceholderCompletionString = mSearchString +
         Substring(aValue, mSearchStringLength + findIndex, endSelect);
-      input->SetTextValue(mPlaceholderCompletionString);
+      SetTextValue(input, mPlaceholderCompletionString,
+                   nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
 
       endSelect -= findIndex; // We're skipping this many characters of aValue.
     } else {
       // Autocompleting something other than a URI from the middle.
       // Use the format "searchstring >> full string" to indicate to the user
       // what we are going to replace their search string with.
-      input->SetTextValue(mSearchString + NS_LITERAL_STRING(" >> ") + aValue);
+      SetTextValue(input, mSearchString + NS_LITERAL_STRING(" >> ") + aValue,
+                   nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
 
       endSelect = mSearchString.Length() + 4 + aValue.Length();
 
       // Reset the last search completion.
       mPlaceholderCompletionString.Truncate();
     }
   }
 
--- a/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
@@ -4,61 +4,61 @@
 
 #include "nsISupports.idl"
 #include "nsIAutoCompleteController.idl"
 
 interface nsIAutoCompletePopup;
 
 [scriptable, uuid(B068E70F-F82C-4C12-AD87-82E271C5C180)]
 interface nsIAutoCompleteInput : nsISupports
-{  
+{
   /*
    * The result view that will be used to display results
    */
   readonly attribute nsIAutoCompletePopup popup;
-  
+
   /*
    * The controller.
    */
   readonly attribute nsIAutoCompleteController controller;
 
-  /* 
+  /*
    * Indicates if the popup is currently open
    */
   attribute boolean popupOpen;
 
   /*
    * Option to disable autocomplete functionality
-   */ 
+   */
   attribute boolean disableAutoComplete;
-  
-  /* 
+
+  /*
    * If a search result has its defaultIndex set, this will optionally
    * try to complete the text in the textbox to the entire text of the
    * result at the default index as the user types
    */
   attribute boolean completeDefaultIndex;
 
   /*
    * complete text in the textbox as the user selects from the dropdown
    * options if set to true
    */
   attribute boolean completeSelectedIndex;
 
-  /* 
+  /*
    * Option for completing to the default result whenever the user hits
    * enter or the textbox loses focus
    */
   attribute boolean forceComplete;
-    
+
   /*
    * Option to open the popup only after a certain number of results are available
    */
   attribute unsigned long minResultsForPopup;
-  
+
   /*
    * The maximum number of rows to show in the autocomplete popup.
    */
   attribute unsigned long maxRows;
 
   /*
    * Option to show a second column in the popup which contains
    * the comment for each autocomplete result
@@ -66,42 +66,61 @@ interface nsIAutoCompleteInput : nsISupp
   attribute boolean showCommentColumn;
 
   /*
    * Option to show a third column in the popup which contains
    * an additional image for each autocomplete result
    */
   attribute boolean showImageColumn;
 
-  /* 
+  /*
    * Number of milliseconds after a keystroke before a search begins
    */
   attribute unsigned long timeout;
 
   /*
    * An extra parameter to configure searches with.
    */
   attribute AString searchParam;
 
   /*
    * The number of autocomplete session to search
    */
   readonly attribute unsigned long searchCount;
-  
+
   /*
    * Get the name of one of the autocomplete search session objects
    */
   ACString getSearchAt(in unsigned long index);
 
   /*
-   * The value of text in the autocomplete textbox
+   * The value of text in the autocomplete textbox.
+   *
+   * @note when setting a new value, the controller always first tries to use
+   *       setTextboxValueWithReason, and only if that throws (unimplemented),
+   *       fallbacks to the textValue's setter.  If a reason is not provided,
+   *       the implementation should assume TEXTVALUE_REASON_UNKNOWN, but it
+   *       should only happen in testing code.
    */
   attribute AString textValue;
 
   /*
+   * Set the value of text in the autocomplete textbox, providing a reason to
+   * the autocomplete view.
+   */
+  const unsigned short TEXTVALUE_REASON_UNKNOWN = 0;
+  const unsigned short TEXTVALUE_REASON_COMPLETEDEFAULT = 1;
+  const unsigned short TEXTVALUE_REASON_COMPLETESELECTED = 2;
+  const unsigned short TEXTVALUE_REASON_REVERT = 3;
+  const unsigned short TEXTVALUE_REASON_ENTERMATCH = 4;
+
+  void setTextValueWithReason(in AString aValue,
+                              in unsigned short aReason);
+
+  /*
    * Report the starting index of the cursor in the textbox
    */
   readonly attribute long selectionStart;
 
   /*
    * Report the ending index of the cursor in the textbox
    */
   readonly attribute long selectionEnd;
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -517,16 +517,23 @@ nsFormFillController::SetTextValue(const
     mSuppressOnInput = true;
     editable->SetUserInput(aTextValue);
     mSuppressOnInput = false;
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsFormFillController::SetTextValueWithReason(const nsAString & aTextValue,
+                                             uint16_t aReason)
+{
+  return SetTextValue(aTextValue);
+}
+
+NS_IMETHODIMP
 nsFormFillController::GetSelectionStart(int32_t *aSelectionStart)
 {
   if (mFocusedInput)
     mFocusedInput->GetSelectionStart(aSelectionStart);
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -183,33 +183,53 @@
       <method name="getSearchAt">
         <parameter name="aIndex"/>
         <body><![CDATA[
           this.initSearchNames();
           return this.mSearchNames[aIndex];
         ]]></body>
       </method>
 
-      <property name="textValue"
-                onget="return this.value;">
+      <method name="setTextValueWithReason">
+        <parameter name="aValue"/>
+        <parameter name="aReason"/>
+        <body><![CDATA[
+          if (aReason == Components.interfaces.nsIAutoCompleteInput
+                                   .TEXTVALUE_REASON_COMPLETEDEFAULT) {
+            this._disableTrim = true;
+          }
+          this.textValue = aValue;
+          this._disableTrim = false;
+        ]]></body>
+      </method>
+
+      <property name="textValue">
+        <getter><![CDATA[
+          if (typeof this.onBeforeTextValueGet == "function") {
+            let result = this.onBeforeTextValueGet();
+            if (result) {
+              return result.value;
+            }
+          }
+          return this.value;
+        ]]></getter>
         <setter><![CDATA[
-          // Completing a result should simulate the user typing the result,
-          // so fire an input event.
-          // Trim popup selected values, but never trim results coming from
-          // autofill.
-          if (this.popup.selectedIndex == -1)
-            this._disableTrim = true;
+          if (typeof this.onBeforeTextValueSet == "function")
+            val = this.onBeforeTextValueSet(val);
+
           this.value = val;
-          this._disableTrim = false;
 
-          var evt = document.createEvent("UIEvents");
+          // Completing a result should simulate the user typing the result, so
+          // fire an input event.
+          let evt = document.createEvent("UIEvents");
           evt.initUIEvent("input", true, false, window, 0);
           this.mIgnoreInput = true;
           this.dispatchEvent(evt);
           this.mIgnoreInput = false;
+
           return this.value;
         ]]></setter>
       </property>
 
       <method name="selectTextRange">
         <parameter name="aStartIndex"/>
         <parameter name="aEndIndex"/>
         <body><![CDATA[