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 99460 32b0dd97f69b7c9faa87d867eb6f229621e5536c
parent 99459 a6be10cfee3ff1ec96ad31fa188b8bff4a9a1742
child 99461 cef3bda40d408055d940eff66fe6c96bdee6cf1d
push idunknown
push userunknown
push dateunknown
reviewersehsan
bugs754668
milestone15.0a1
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"];