Bug 754668 - Prevent nsTextEditorState to mark the value as changed when it has been requested otherwise. r=ehsan
authorMounir Lamouri <mounir.lamouri@gmail.com>
Thu, 31 May 2012 18:01:30 +0200
changeset 95406 32b0dd97f69b7c9faa87d867eb6f229621e5536c
parent 95405 a6be10cfee3ff1ec96ad31fa188b8bff4a9a1742
child 95407 cef3bda40d408055d940eff66fe6c96bdee6cf1d
push id22812
push useremorley@mozilla.com
push dateFri, 01 Jun 2012 14:30:06 +0000
treeherdermozilla-central@12ab69851e05 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs754668
milestone15.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 754668 - Prevent nsTextEditorState to mark the value as changed when it has been requested otherwise. r=ehsan
content/html/content/src/nsHTMLInputElement.cpp
content/html/content/src/nsHTMLTextAreaElement.cpp
content/html/content/src/nsTextEditorState.cpp
content/html/content/src/nsTextEditorState.h
content/html/content/test/forms/test_required_attribute.html
--- a/content/html/content/src/nsHTMLInputElement.cpp
+++ b/content/html/content/src/nsHTMLInputElement.cpp
@@ -1345,17 +1345,17 @@ nsHTMLInputElement::SetValueInternal(con
       if (!mParserCreating) {
         SanitizeValue(value);
       }
 
       if (aSetValueChanged) {
         SetValueChanged(true);
       }
 
-      mInputData.mState->SetValue(value, aUserInput);
+      mInputData.mState->SetValue(value, aUserInput, aSetValueChanged);
 
       // This call might be useless in some situations because if the element is
       // a single line text control, nsTextEditorState::SetValue will call
       // nsHTMLInputElement::OnValueChanged which is going to call UpdateState()
       // if the element is focused. This bug 665547.
       if (PlaceholderApplies() &&
           HasAttr(kNameSpaceID_None, nsGkAtoms::placeholder)) {
         UpdateState(true);
--- a/content/html/content/src/nsHTMLTextAreaElement.cpp
+++ b/content/html/content/src/nsHTMLTextAreaElement.cpp
@@ -534,17 +534,17 @@ nsHTMLTextAreaElement::SetPlaceholderCla
 nsresult
 nsHTMLTextAreaElement::SetValueInternal(const nsAString& aValue,
                                         bool aUserInput)
 {
   // Need to set the value changed flag here, so that
   // nsTextControlFrame::UpdateValueDisplay retrieves the correct value
   // if needed.
   SetValueChanged(true);
-  mState.SetValue(aValue, aUserInput);
+  mState.SetValue(aValue, aUserInput, true);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP 
 nsHTMLTextAreaElement::SetValue(const nsAString& aValue)
 {
   SetValueInternal(aValue, false);
--- a/content/html/content/src/nsTextEditorState.cpp
+++ b/content/html/content/src/nsTextEditorState.cpp
@@ -625,16 +625,17 @@ public:
   virtual ~nsTextInputListener();
 
   /** SetEditor gives an address to the editor that will be accessed
    *  @param aEditor the editor this listener calls for editing operations
    */
   void SetFrame(nsTextControlFrame *aFrame){mFrame = aFrame;}
 
   void SettingValue(bool aValue) { mSettingValue = aValue; }
+  void SetValueChanged(bool aSetValueChanged) { mSetValueChanged = aSetValueChanged; }
 
   NS_DECL_ISUPPORTS
 
   NS_DECL_NSISELECTIONLISTENER
 
   NS_DECL_NSIDOMEVENTLISTENER
 
   NS_DECL_NSIEDITOROBSERVER
@@ -662,30 +663,36 @@ protected:
    * notification (when this state changes we update undo and redo menus)
    */
   bool            mHadRedoItems;
   /**
    * Whether we're in the process of a SetValue call, and should therefore
    * refrain from calling OnValueChanged.
    */
   bool mSettingValue;
+  /**
+   * Whether we are in the process of a SetValue call that doesn't want
+   * |SetValueChanged| to be called.
+   */
+  bool mSetValueChanged;
 };
 
 
 /*
  * nsTextInputListener implementation
  */
 
 nsTextInputListener::nsTextInputListener(nsITextControlElement* aTxtCtrlElement)
 : mFrame(nsnull)
 , mTxtCtrlElement(aTxtCtrlElement)
 , mSelectionWasCollapsed(true)
 , mHadUndoItems(false)
 , mHadRedoItems(false)
 , mSettingValue(false)
+, mSetValueChanged(true)
 {
 }
 
 nsTextInputListener::~nsTextInputListener() 
 {
 }
 
 NS_IMPL_ISUPPORTS4(nsTextInputListener,
@@ -852,17 +859,19 @@ nsTextInputListener::EditAction()
   }
 
   if (!weakFrame.IsAlive()) {
     return NS_OK;
   }
 
   // Make sure we know we were changed (do NOT set this to false if there are
   // no undo items; JS could change the value and we'd still need to save it)
-  frame->SetValueChanged(true);
+  if (mSetValueChanged) {
+    frame->SetValueChanged(true);
+  }
 
   if (!mSettingValue) {
     mTxtCtrlElement->OnValueChanged(true);
   }
 
   return NS_OK;
 }
 
@@ -1311,17 +1320,17 @@ nsTextEditorState::PrepareEditor(const n
     // Now call SetValue() which will make the necessary editor calls to set
     // the default value.  Make sure to turn off undo before setting the default
     // value, and turn it back on afterwards. This will make sure we can't undo
     // past the default value.
 
     rv = newEditor->EnableUndo(false);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    SetValue(defaultValue, false);
+    SetValue(defaultValue, false, false);
 
     rv = newEditor->EnableUndo(true);
     NS_ASSERTION(NS_SUCCEEDED(rv),"Transaction Manager must have failed");
 
     // Now restore the original editor flags.
     rv = newEditor->SetFlags(editorFlags);
     NS_ENSURE_SUCCESS(rv, rv);
   }
@@ -1495,17 +1504,17 @@ nsTextEditorState::UnbindFromFrame(nsTex
     mTextListener = nsnull;
   }
 
   mBoundFrame = nsnull;
 
   // Now that we don't have a frame any more, store the value in the text buffer.
   // The only case where we don't do this is if a value transfer is in progress.
   if (!mValueTransferInProgress) {
-    SetValue(value, false);
+    SetValue(value, false, false);
   }
 
   if (mRootNode && mMutationObserver) {
     mRootNode->RemoveMutationObserver(mMutationObserver);
     mMutationObserver = nsnull;
   }
 
   // Unbind the anonymous content from the tree.
@@ -1714,17 +1723,18 @@ nsTextEditorState::GetValue(nsAString& a
       mTextCtrlElement->GetDefaultValueFromContent(aValue);
     } else {
       aValue = NS_ConvertUTF8toUTF16(*mValue);
     }
   }
 }
 
 void
-nsTextEditorState::SetValue(const nsAString& aValue, bool aUserInput)
+nsTextEditorState::SetValue(const nsAString& aValue, bool aUserInput,
+                            bool aSetValueChanged)
 {
   if (mEditor && mBoundFrame) {
     // The InsertText call below might flush pending notifications, which
     // could lead into a scheduled PrepareEditor to be called.  That will
     // lead to crashes (or worse) because we'd be initializing the editor
     // before InsertText returns.  This script blocker makes sure that
     // PrepareEditor cannot be called prematurely.
     nsAutoScriptBlocker scriptBlocker;
@@ -1819,38 +1829,40 @@ nsTextEditorState::SetValue(const nsAStr
         mEditor->GetFlags(&savedFlags);
         flags = savedFlags;
         flags &= ~(nsIPlaintextEditor::eEditorDisabledMask);
         flags &= ~(nsIPlaintextEditor::eEditorReadonlyMask);
         flags |= nsIPlaintextEditor::eEditorDontEchoPassword;
         mEditor->SetFlags(flags);
 
         mTextListener->SettingValue(true);
+        mTextListener->SetValueChanged(aSetValueChanged);
 
         // Also don't enforce max-length here
         PRInt32 savedMaxLength;
         plaintextEditor->GetMaxTextLength(&savedMaxLength);
         plaintextEditor->SetMaxTextLength(-1);
 
         if (insertValue.IsEmpty()) {
           mEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
         } else {
           plaintextEditor->InsertText(insertValue);
         }
 
+        mTextListener->SetValueChanged(true);
         mTextListener->SettingValue(false);
 
         if (!weakFrame.IsAlive()) {
           // If the frame was destroyed because of a flush somewhere inside
           // InsertText, mBoundFrame here will be false.  But it's also possible
           // for the frame to go away because of another reason (such as deleting
           // the existing selection -- see bug 574558), in which case we don't
           // need to reset the value here.
           if (!mBoundFrame) {
-            SetValue(newValue, false);
+            SetValue(newValue, false, aSetValueChanged);
           }
           valueSetter.Cancel();
           return;
         }
 
         if (!IsSingleLineTextControl()) {
           mCachedValue = newValue;
         }
--- a/content/html/content/src/nsTextEditorState.h
+++ b/content/html/content/src/nsTextEditorState.h
@@ -128,17 +128,18 @@ public:
   nsIEditor* GetEditor();
   nsISelectionController* GetSelectionController() const;
   nsFrameSelection* GetConstFrameSelection();
   nsresult BindToFrame(nsTextControlFrame* aFrame);
   void UnbindFromFrame(nsTextControlFrame* aFrame);
   nsresult PrepareEditor(const nsAString *aValue = nsnull);
   void InitializeKeyboardEventListeners();
 
-  void SetValue(const nsAString& aValue, bool aUserInput);
+  void SetValue(const nsAString& aValue, bool aUserInput,
+                bool aSetValueAsChanged);
   void GetValue(nsAString& aValue, bool aIgnoreWrap) const;
   void EmptyValue() { if (mValue) mValue->Truncate(); }
   bool IsEmpty() const { return mValue ? mValue->IsEmpty() : true; }
 
   nsresult CreatePlaceholderNode();
 
   nsIContent* GetRootNode() {
     if (!mRootNode)
--- a/content/html/content/test/forms/test_required_attribute.html
+++ b/content/html/content/test/forms/test_required_attribute.html
@@ -2,21 +2,16 @@
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=345822
 -->
 <head>
   <title>Test for Bug 345822</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <style>
-    input, textarea { background-color: rgb(0,0,0) !important; }
-    :valid   { background-color: rgb(0,255,0) !important; }
-    :invalid { background-color: rgb(255,0,0) !important; }
-  </style>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345822">Mozilla Bug 345822</a>
 <p id="display"></p>
 <div id="content">
   <form>
   </form>
 </div>
@@ -29,24 +24,30 @@ function checkNotSufferingFromBeingMissi
 {
   ok(!element.validity.valueMissing,
     "Element should not suffer from value missing");
   ok(element.validity.valid, "Element should be valid");
   ok(element.checkValidity(), "Element should be valid");
   is(element.validationMessage, "",
     "Validation message should be the empty string");
 
-  if (element.type != 'radio' && element.type != 'checkbox') {
-    is(window.getComputedStyle(element, null).getPropertyValue('background-color'),
-       doNotApply ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)",
-       "The pseudo-class is not correctly applied to " + element.localName);
+  if (doNotApply) {
+    ok(!element.mozMatchesSelector(':valid'), ":valid should not apply");
+    ok(!element.mozMatchesSelector(':invalid'), ":invalid should not apply");
+    ok(!element.mozMatchesSelector(':-moz-ui-valid'), ":-moz-ui-valid should not apply");
+    ok(!element.mozMatchesSelector(':-moz-ui-invalid'), ":-moz-ui-invalid should not apply");
+  } else {
+    ok(element.mozMatchesSelector(':valid'), ":valid should apply");
+    ok(!element.mozMatchesSelector(':invalid'), ":invalid should not apply");
+    ok(element.mozMatchesSelector(':-moz-ui-valid'), ":-moz-ui-valid should apply");
+    ok(!element.mozMatchesSelector(':-moz-ui-invalid'), ":-moz-ui-invalid should not apply");
   }
 }
 
-function checkSufferingFromBeingMissing(element)
+function checkSufferingFromBeingMissing(element, hasMozUIInvalid)
 {
   ok(element.validity.valueMissing, "Element should suffer from value missing");
   ok(!element.validity.valid, "Element should not be valid");
   ok(!element.checkValidity(), "Element should not be valid");
 
   if (element.type == 'checkbox')
   {
     is(element.validationMessage,
@@ -67,50 +68,61 @@ function checkSufferingFromBeingMissing(
   }
   else // text fields
   {
     is(element.validationMessage,
       "Please fill out this field.",
       "Validation message is wrong");
   }
 
-  if (element.type != 'radio' && element.type != 'checkbox') {
-    is(window.getComputedStyle(element, null).getPropertyValue('background-color'),
-       "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
-  }
+  ok(!element.mozMatchesSelector(':valid'), ":valid should apply");
+  ok(element.mozMatchesSelector(':invalid'), ":invalid should not apply");
+  ok(!element.mozMatchesSelector(':-moz-ui-valid'), ":-moz-ui-valid should not apply");
+  is(element.mozMatchesSelector(':-moz-ui-invalid'), hasMozUIInvalid, ":-moz-ui-invalid expected state is " + hasMozUIInvalid);
 }
 
 function checkTextareaRequiredValidity()
 {
   var element = document.createElement('textarea');
   document.forms[0].appendChild(element);
 
   element.value = '';
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
   element.required = true;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.readOnly = true;
   checkNotSufferingFromBeingMissing(element, true);
 
   element.readOnly = false;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.value = 'foo';
   checkNotSufferingFromBeingMissing(element);
 
   element.value = '';
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
+  element.focus();
+  element.required = true;
+  element.value = 'foobar';
+  element.blur();
+  element.form.reset();
+  checkSufferingFromBeingMissing(element, false);
+
+  // TODO: for the moment, a textarea outside of a document is mutable.
+  element.value = ''; // To make -moz-ui-valid apply.
+  element.required = false;
   document.forms[0].removeChild(element);
+  checkNotSufferingFromBeingMissing(element);
 }
 
 function checkInputRequiredNotApply(type, isBarred)
 {
   var element = document.createElement('input');
   element.type = type;
   document.forms[0].appendChild(element);
 
@@ -119,97 +131,117 @@ function checkInputRequiredNotApply(type
   checkNotSufferingFromBeingMissing(element, isBarred);
 
   element.required = true;
   checkNotSufferingFromBeingMissing(element, isBarred);
 
   element.required = false;
 
   document.forms[0].removeChild(element);
+  checkNotSufferingFromBeingMissing(element, isBarred);
 }
 
 function checkInputRequiredValidity(type)
 {
   var element = document.createElement('input');
   element.type = type;
   document.forms[0].appendChild(element);
 
   element.value = '';
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
   element.required = true;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.readOnly = true;
   checkNotSufferingFromBeingMissing(element, true);
 
   element.readOnly = false;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   if (element.type == 'email') {
     element.value = 'foo@bar.com';
   } else if (element.type == 'url') {
     element.value = 'http://mozilla.org/';
   } else {
     element.value = 'foo';
   }
   checkNotSufferingFromBeingMissing(element);
 
   element.value = '';
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
+  element.focus();
+  element.required = true;
+  element.value = 'foobar';
+  element.blur();
+  element.form.reset();
+  checkSufferingFromBeingMissing(element, false);
+
+  element.required = true;
+  element.value = ''; // To make :-moz-ui-valid apply.
   document.forms[0].removeChild(element);
   checkNotSufferingFromBeingMissing(element);
 }
 
 function checkInputRequiredValidityForCheckbox()
 {
   var element = document.createElement('input');
   element.type = 'checkbox';
   document.forms[0].appendChild(element);
 
   element.checked = false;
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
   element.required = true;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.checked = true;
   checkNotSufferingFromBeingMissing(element);
 
   element.checked = false;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
+  element.focus();
+  element.required = true;
+  element.checked = true;
+  element.blur();
+  element.form.reset();
+  checkSufferingFromBeingMissing(element, false);
+
+  element.required = true;
+  element.checked = false;
   document.forms[0].removeChild(element);
+  checkNotSufferingFromBeingMissing(element);
 }
 
 function checkInputRequiredValidityForRadio()
 {
   var element = document.createElement('input');
   element.type = 'radio';
   element.name = 'test'
   document.forms[0].appendChild(element);
 
   element.checked = false;
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
   element.required = true;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.checked = true;
   checkNotSufferingFromBeingMissing(element);
 
   element.checked = false;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   // A required radio button should not suffer from value missing if another
   // radio button from the same group is checked.
   var element2 = document.createElement('input');
   element2.type = 'radio';
   element2.name = 'test';
 
   element2.checked = true;
@@ -217,43 +249,54 @@ function checkInputRequiredValidityForRa
   document.forms[0].appendChild(element2);
 
   // Adding a checked radio should make required radio in the group not
   // suffering from being missing.
   checkNotSufferingFromBeingMissing(element);
 
   element.checked = false;
   element2.checked = false;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   // The other radio button should not be disabled.
   // A disabled checked radio button in the radio group
   // is enough to not suffer from value missing.
   element2.checked = true;
   element2.disabled = true;
   checkNotSufferingFromBeingMissing(element);
 
   // If a radio button is not required but another radio button is required in
   // the same group, the not required radio button should suffer from value
   // missing.
   element2.disabled = false;
   element2.checked = false;
   element.required = false;
   element2.required = true;
-  checkSufferingFromBeingMissing(element);
-  checkSufferingFromBeingMissing(element2);
+  checkSufferingFromBeingMissing(element, true);
+  checkSufferingFromBeingMissing(element2, true);
 
   element.checked = true;
   checkNotSufferingFromBeingMissing(element2);
 
   // The checked radio is not in the group anymore, element2 should be invalid.
-  document.forms[0].removeChild(element);
-  checkSufferingFromBeingMissing(element2);
+  element.form.removeChild(element);
+  checkNotSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element2, true);
 
+  element2.focus();
+  element2.required = true;
+  element2.checked = true;
+  element2.blur();
+  element2.form.reset();
+  checkSufferingFromBeingMissing(element2, false);
+
+  element2.required = true;
+  element2.checked = false;
   document.forms[0].removeChild(element2);
+  checkNotSufferingFromBeingMissing(element2);
 }
 
 function checkInputRequiredValidityForFile()
 {
   var element = document.createElement('input');
   element.type = 'file'
   document.forms[0].appendChild(element);
 
@@ -278,29 +321,39 @@ function checkInputRequiredValidityForFi
 
   var file = createFileWithData("345822_file", "file content");
 
   element.value = "";
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
   element.required = true;
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.value = file.path;
   checkNotSufferingFromBeingMissing(element);
 
   element.value = "";
-  checkSufferingFromBeingMissing(element);
+  checkSufferingFromBeingMissing(element, true);
 
   element.required = false;
   checkNotSufferingFromBeingMissing(element);
 
+  element.focus();
+  element.value = file.path;
+  element.required = true;
+  element.blur();
+  element.form.reset();
+  checkSufferingFromBeingMissing(element, false);
+
+  element.required = true;
+  element.value = '';
   file.remove(false);
   document.forms[0].removeChild(element);
+  checkNotSufferingFromBeingMissing(element);
 }
 
 checkTextareaRequiredValidity();
 
 // The require attribute behavior depend of the input type.
 // First of all, checks for types that make the element barred from
 // constraint validation.
 var typeBarredFromConstraintValidation = ["hidden", "button", "reset", "submit", "image"];