Bug 288392, DOMSubtreeModified event, r=peterv, sr=jst
authorOlli.Pettay@helsinki.fi
Sat, 24 Mar 2007 05:18:02 -0700
changeset 62 e2a2df21a600959a6fb32ae894c432e069ff26c8
parent 61 b4b593e59a96e55f95c387b45199d0002a395024
child 63 2aac9b7da06a8377eb5d5d28d4b004e4b55849f9
push idunknown
push userunknown
push dateunknown
reviewerspeterv, jst
bugs288392
milestone1.9a3pre
Bug 288392, DOMSubtreeModified event, r=peterv, sr=jst
content/base/public/nsIDocument.h
content/base/src/nsContentUtils.cpp
content/base/src/nsDocument.cpp
content/base/src/nsDocument.h
content/base/src/nsGenericDOMDataNode.cpp
content/base/src/nsGenericElement.cpp
content/base/src/nsRange.cpp
content/events/public/nsIEventListenerManager.h
content/events/src/nsEventListenerManager.cpp
content/html/content/src/nsGenericHTMLElement.cpp
content/xul/content/src/nsXULElement.cpp
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -85,21 +85,22 @@ class nsHTMLStyleSheet;
 class nsIHTMLCSSStyleSheet;
 class nsILayoutHistoryState;
 class nsIVariant;
 class nsIDOMUserDataHandler;
 template<class E> class nsCOMArray;
 class nsIDocumentObserver;
 class nsBindingManager;
 class nsIDOMNodeList;
+class mozAutoSubtreeModified;
 
 // IID for the nsIDocument interface
 #define NS_IDOCUMENT_IID      \
-{ 0x0b8acf09, 0x7877, 0x4928, \
- { 0x8b, 0xfb, 0x6d, 0x7c, 0x6d, 0x53, 0xff, 0x32 } }
+{ 0x1b8ed19c, 0xb87d, 0x4058, \
+  { 0x92, 0x2a, 0xff, 0xbc, 0x36, 0x29, 0x3b, 0xd7 } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 //----------------------------------------------------------------------
 
 // Document interface.  This is implemented by all document objects in
 // Gecko.
@@ -841,26 +842,40 @@ public:
   virtual nsresult GetContentListFor(nsIContent* aContent,
                                      nsIDOMNodeList** aResult) = 0;
 
   /**
    * See FlushSkinBindings on nsBindingManager
    */
   virtual void FlushSkinBindings() = 0;
 
+  /**
+   * Returns PR_TRUE if one or more mutation events are being dispatched.
+   */
+  virtual PRBool MutationEventBeingDispatched() = 0;
+
 protected:
   ~nsIDocument()
   {
     // XXX The cleanup of mNodeInfoManager (calling DropDocumentReference and
     //     releasing it) happens in the nsDocument destructor. We'd prefer to
     //     do it here but nsNodeInfoManager is a concrete class that we don't
     //     want to expose to users of the nsIDocument API outside of Gecko.
     // XXX Same thing applies to mBindingManager
   }
 
+  /**
+   * These methods should be called before and after dispatching
+   * a mutation event.
+   * To make this easy and painless, use the mozAutoSubtreeModified helper class.
+   */
+  virtual void WillDispatchMutationEvent(nsINode* aTarget) = 0;
+  virtual void MutationEventDispatched(nsINode* aTarget) = 0;
+  friend class mozAutoSubtreeModified;
+
   nsString mDocumentTitle;
   nsCOMPtr<nsIURI> mDocumentURI;
   nsCOMPtr<nsIURI> mDocumentBaseURI;
 
   nsWeakPtr mDocumentLoadGroup;
 
   nsWeakPtr mDocumentContainer;
 
@@ -947,16 +962,58 @@ private:
 
 #define MOZ_AUTO_DOC_UPDATE_PASTE2(tok,line) tok##line
 #define MOZ_AUTO_DOC_UPDATE_PASTE(tok,line) \
   MOZ_AUTO_DOC_UPDATE_PASTE2(tok,line)
 #define MOZ_AUTO_DOC_UPDATE(doc,type,notify) \
   mozAutoDocUpdate MOZ_AUTO_DOC_UPDATE_PASTE(_autoDocUpdater_, __LINE__) \
   (doc,type,notify)
 
+/**
+ * mozAutoSubtreeModified batches DOM mutations so that a DOMSubtreeModified
+ * event is dispatched, if necessary, when the outermost mozAutoSubtreeModified
+ * object is deleted.
+ */
+class mozAutoSubtreeModified
+{
+public:
+  /**
+   * @param aSubTreeOwner The document in which a subtree will be modified.
+   * @param aTarget       The target of the possible DOMSubtreeModified event.
+   *                      Can be nsnull, in which case mozAutoSubtreeModified
+   *                      is just used to batch DOM mutations.
+   */
+  mozAutoSubtreeModified(nsIDocument* aSubtreeOwner, nsINode* aTarget)
+  {
+    UpdateTarget(aSubtreeOwner, aTarget);
+  }
+
+  ~mozAutoSubtreeModified()
+  {
+    UpdateTarget(nsnull, nsnull);
+  }
+
+  void UpdateTarget(nsIDocument* aSubtreeOwner, nsINode* aTarget)
+  {
+    if (mSubtreeOwner) {
+      mSubtreeOwner->MutationEventDispatched(mTarget);
+    }
+
+    mTarget = aTarget;
+    mSubtreeOwner = aSubtreeOwner;
+    if (mSubtreeOwner) {
+      mSubtreeOwner->WillDispatchMutationEvent(mTarget);
+    }
+  }
+
+private:
+  nsCOMPtr<nsINode>     mTarget;
+  nsCOMPtr<nsIDocument> mSubtreeOwner;
+};
+
 // XXX These belong somewhere else
 nsresult
 NS_NewHTMLDocument(nsIDocument** aInstancePtrResult);
 
 nsresult
 NS_NewXMLDocument(nsIDocument** aInstancePtrResult);
 
 #ifdef MOZ_SVG
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -2815,16 +2815,22 @@ PRBool
 nsContentUtils::HasMutationListeners(nsINode* aNode,
                                      PRUint32 aType)
 {
   nsIDocument* doc = aNode->GetOwnerDoc();
   if (!doc) {
     return PR_FALSE;
   }
 
+  // To batch DOMSubtreeModified properly, all mutation events should be
+  // processed if one is being processed already.
+  if (doc->MutationEventBeingDispatched()) {
+    return PR_TRUE;
+  }
+
   // global object will be null for documents that don't have windows.
   nsCOMPtr<nsPIDOMWindow> window;
   window = do_QueryInterface(doc->GetScriptGlobalObject());
   if (window && !window->HasMutationListeners(aType)) {
     return PR_FALSE;
   }
 
   // If we have a window, we can check it for mutation listeners now.
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -140,16 +140,17 @@ static NS_DEFINE_CID(kDOMEventGroupCID, 
 
 #include "nsICharsetAlias.h"
 #include "nsIParser.h"
 #include "nsIContentSink.h"
 
 #include "nsDateTimeFormatCID.h"
 #include "nsIDateTimeFormat.h"
 #include "nsEventDispatcher.h"
+#include "nsMutationEvent.h"
 #include "nsIDOMXPathEvaluator.h"
 #include "nsDOMCID.h"
 
 #include "nsLayoutStatics.h"
 #include "nsIJSContextStack.h"
 #include "nsIXPConnect.h"
 #include "nsCycleCollector.h"
 
@@ -5699,16 +5700,82 @@ nsDocument::OnPageHide(PRBool aPersisted
 
   // Now send out a PageHide event.
   nsPageTransitionEvent event(PR_TRUE, NS_PAGE_HIDE, aPersisted);
   DispatchEventToWindow(&event);
 
   mVisible = PR_FALSE;
 }
 
+void
+nsDocument::WillDispatchMutationEvent(nsINode* aTarget)
+{
+  NS_ASSERTION(mSubtreeModifiedDepth != 0 ||
+               mSubtreeModifiedTargets.Count() == 0,
+               "mSubtreeModifiedTargets not cleared after dispatching?");
+  ++mSubtreeModifiedDepth;
+  if (aTarget) {
+    mSubtreeModifiedTargets.AppendObject(aTarget);
+  }
+}
+
+void
+nsDocument::MutationEventDispatched(nsINode* aTarget)
+{
+  --mSubtreeModifiedDepth;
+  if (mSubtreeModifiedDepth == 0) {
+    PRInt32 count = mSubtreeModifiedTargets.Count();
+    if (!count) {
+      return;
+    }
+
+    nsCOMPtr<nsPIDOMWindow> window;
+    window = do_QueryInterface(GetScriptGlobalObject());
+    if (window &&
+        !window->HasMutationListeners(NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED)) {
+      mSubtreeModifiedTargets.Clear();
+      return;
+    }
+
+    nsCOMArray<nsINode> realTargets;
+    for (PRInt32 i = 0; i < count; ++i) {
+      nsINode* possibleTarget = mSubtreeModifiedTargets[i];
+      nsCOMPtr<nsIContent> content = do_QueryInterface(possibleTarget);
+      if (content && content->IsAnonymousForEvents()) {
+        if (realTargets.IndexOf(possibleTarget) == -1) {
+          realTargets.AppendObject(possibleTarget);
+        }
+        continue;
+      }
+
+      nsINode* commonAncestor = nsnull;
+      PRInt32 realTargetCount = realTargets.Count();
+      for (PRInt32 j = 0; j < realTargetCount; ++j) {
+        commonAncestor =
+          nsContentUtils::GetCommonAncestor(possibleTarget, realTargets[j]);
+        if (commonAncestor) {
+          realTargets.ReplaceObjectAt(commonAncestor, j);
+          break;
+        }
+      }
+      if (!commonAncestor) {
+        realTargets.AppendObject(possibleTarget);
+      }
+    }
+
+    mSubtreeModifiedTargets.Clear();
+
+    PRInt32 realTargetCount = realTargets.Count();
+    for (PRInt32 k = 0; k < realTargetCount; ++k) {
+      nsMutationEvent mutation(PR_TRUE, NS_MUTATION_SUBTREEMODIFIED);
+      nsEventDispatcher::Dispatch(realTargets[k], nsnull, &mutation);
+    }
+  }
+}
+
 static PRUint32 GetURIHash(nsIURI* aURI)
 {
   nsCAutoString str;
   aURI->GetSpec(str);
   return HashString(str);
 }
 
 void
--- a/content/base/src/nsDocument.h
+++ b/content/base/src/nsDocument.h
@@ -504,16 +504,23 @@ public:
                                  const PRInt32 aStandalone);
   virtual void GetXMLDeclaration(nsAString& aVersion,
                                  nsAString& aEncoding,
                                  nsAString& Standalone);
   virtual PRBool IsScriptEnabled();
 
   virtual void OnPageShow(PRBool aPersisted);
   virtual void OnPageHide(PRBool aPersisted);
+  
+  virtual void WillDispatchMutationEvent(nsINode* aTarget);
+  virtual void MutationEventDispatched(nsINode* aTarget);
+  virtual PRBool MutationEventBeingDispatched()
+  {
+    return (mSubtreeModifiedDepth > 0);
+  }
 
   // nsINode
   virtual PRBool IsNodeOfType(PRUint32 aFlags) const;
   virtual nsIContent *GetChildAt(PRUint32 aIndex) const;
   virtual PRInt32 IndexOf(nsINode* aPossibleChild) const;
   virtual PRUint32 GetChildCount() const;
   virtual nsresult InsertChildAt(nsIContent* aKid, PRUint32 aIndex,
                                  PRBool aNotify);
@@ -822,14 +829,17 @@ private:
   // A map from unvisited URI hashes to content elements
   nsTHashtable<nsUint32ToContentHashEntry> mLinkMap;
   // URIs whose visitedness has changed while we were hidden
   nsCOMArray<nsIURI> mVisitednessChangedURIs;
 
   // Member to store out last-selected stylesheet set.
   nsString mLastStyleSheetSet;
 
+  nsCOMArray<nsINode> mSubtreeModifiedTargets;
+  PRUint32            mSubtreeModifiedDepth;
+
   // Our update nesting level
   PRUint32 mUpdateNestLevel;
 };
 
 
 #endif /* nsDocument_h___ */
--- a/content/base/src/nsGenericDOMDataNode.cpp
+++ b/content/base/src/nsGenericDOMDataNode.cpp
@@ -465,16 +465,17 @@ nsGenericDOMDataNode::SetTextInternal(PR
 
       mutation.mPrevAttrValue = oldValue;
       if (aLength > 0) {
         nsAutoString val;
         mText.AppendTo(val);
         mutation.mNewAttrValue = do_GetAtom(val);
       }
 
+      mozAutoSubtreeModified subtree(GetOwnerDoc(), this);
       nsEventDispatcher::Dispatch(this, nsnull, &mutation);
     }
 
     CharacterDataChangeInfo info = {
       aOffset == textLength,
       aOffset,
       endOffset,
       aLength
--- a/content/base/src/nsGenericElement.cpp
+++ b/content/base/src/nsGenericElement.cpp
@@ -341,16 +341,19 @@ nsNode3Tearoff::GetTextContent(nsAString
   nsContentUtils::GetNodeTextContent(mContent, PR_TRUE, aTextContent);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNode3Tearoff::SetTextContent(const nsAString &aTextContent)
 {
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(mContent->GetOwnerDoc(), nsnull);
+
   nsCOMPtr<nsIDOMNode> node(do_QueryInterface(mContent));
   NS_ASSERTION(node, "We have an nsIContent which doesn't support nsIDOMNode");
 
   PRUint16 nodeType;
   node->GetNodeType(&nodeType);
   if (nodeType == nsIDOMNode::DOCUMENT_TYPE_NODE ||
       nodeType == nsIDOMNode::NOTATION_NODE) {
     return NS_OK;
@@ -1662,16 +1665,19 @@ nsGenericElement::JoinTextNodes(nsIConte
   }
 
   return rv;
 }
 
 nsresult
 nsGenericElement::Normalize()
 {
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(GetOwnerDoc(), nsnull);
+
   nsresult result = NS_OK;
   PRUint32 index, count = GetChildCount();
 
   for (index = 0; (index < count) && (NS_OK == result); index++) {
     nsIContent *child = GetChildAt(index);
 
     nsCOMPtr<nsIDOMNode> node = do_QueryInterface(child);
     if (node) {
@@ -2338,16 +2344,17 @@ nsGenericElement::doInsertChildAt(nsICon
     } else {
       nsNodeUtils::ContentInserted(container, aKid, aIndex);
     }
 
     if (nsContentUtils::HasMutationListeners(aKid,
           NS_EVENT_BITS_MUTATION_NODEINSERTED)) {
       nsMutationEvent mutation(PR_TRUE, NS_MUTATION_NODEINSERTED);
       mutation.mRelatedNode = do_QueryInterface(container);
+      mozAutoSubtreeModified subtree(container->GetOwnerDoc(), container);
       nsEventDispatcher::Dispatch(aKid, nsnull, &mutation);
     }
   }
 
   return NS_OK;
 }
 
 nsresult
@@ -2382,21 +2389,23 @@ nsGenericElement::doRemoveChildAt(PRUint
   NS_PRECONDITION(aKid && aKid->GetParent() == aParent &&
                   aKid == container->GetChildAt(aIndex) &&
                   container->IndexOf(aKid) == (PRInt32)aIndex, "Bogus aKid");
 
   mozAutoDocUpdate updateBatch(aDocument, UPDATE_CONTENT_MODEL, aNotify);
 
   nsMutationGuard guard;
 
+  mozAutoSubtreeModified subtree(nsnull, nsnull);
   if (aNotify &&
       nsContentUtils::HasMutationListeners(aKid,
         NS_EVENT_BITS_MUTATION_NODEREMOVED)) {
     nsMutationEvent mutation(PR_TRUE, NS_MUTATION_NODEREMOVED);
     mutation.mRelatedNode = do_QueryInterface(container);
+    subtree.UpdateTarget(container->GetOwnerDoc(), container);
     nsEventDispatcher::Dispatch(aKid, nsnull, &mutation);
   }
 
   // Someone may have removed the kid or any of its siblings while that event
   // was processing.
   if (guard.Mutated(0)) {
     aIndex = container->IndexOf(aKid);
     if (NS_STATIC_CAST(PRInt32, aIndex) < 0) {
@@ -3321,16 +3330,17 @@ nsGenericElement::SetAttrAndNotify(PRInt
     GetAttr(aNamespaceID, aName, newValue);
     if (!newValue.IsEmpty()) {
       mutation.mNewAttrValue = do_GetAtom(newValue);
     }
     if (!aOldValue.IsEmpty()) {
       mutation.mPrevAttrValue = do_GetAtom(aOldValue);
     }
     mutation.mAttrChange = modType;
+    mozAutoSubtreeModified subtree(GetOwnerDoc(), this);
     nsEventDispatcher::Dispatch(this, nsnull, &mutation);
   }
 
   if (aNotify) {
     nsNodeUtils::AttributeChanged(this, aNamespaceID, aName, modType);
   }
   
   if (aNamespaceID == kNameSpaceID_XMLEvents && 
@@ -3548,16 +3558,17 @@ nsGenericElement::UnsetAttr(PRInt32 aNam
     mutation.mAttrName = aName;
 
     nsAutoString value;
     oldValue.ToString(value);
     if (!value.IsEmpty())
       mutation.mPrevAttrValue = do_GetAtom(value);
     mutation.mAttrChange = nsIDOMMutationEvent::REMOVAL;
 
+    mozAutoSubtreeModified subtree(GetOwnerDoc(), this);
     nsEventDispatcher::Dispatch(this, nsnull, &mutation);
   }
 
   return NS_OK;
 }
 
 const nsAttrName*
 nsGenericElement::GetAttrNameAt(PRUint32 aIndex) const
--- a/content/base/src/nsRange.cpp
+++ b/content/base/src/nsRange.cpp
@@ -1082,16 +1082,19 @@ CollapseRangeAfterDelete(nsIDOMRange *aR
   return aRange->Collapse(PR_FALSE);
 }
 
 nsresult nsRange::DeleteContents()
 { 
   if(IsDetached())
     return NS_ERROR_DOM_INVALID_STATE_ERR;
 
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(mRoot ? mRoot->GetOwnerDoc(): nsnull, nsnull);
+
   // Save the range end points locally to avoid interference
   // of Range gravity during our edits!
 
   nsCOMPtr<nsIDOMNode> startContainer = do_QueryInterface(mStartParent);
   PRInt32              startOffset = mStartOffset;
   nsCOMPtr<nsIDOMNode> endContainer = do_QueryInterface(mEndParent);
   PRInt32              endOffset = mEndOffset;
 
@@ -1271,16 +1274,19 @@ nsRange::CompareBoundaryPoints(PRUint16 
   return NS_OK;
 }
 
 nsresult nsRange::ExtractContents(nsIDOMDocumentFragment** aReturn)
 { 
   if(mIsDetached)
     return NS_ERROR_DOM_INVALID_STATE_ERR;
 
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(mRoot ? mRoot->GetOwnerDoc(): nsnull, nsnull);
+
   // XXX_kin: The spec says that nodes that are completely in the
   // XXX_kin: range should be moved into the document fragment, not
   // XXX_kin: copied. This method will have to be rewritten using
   // XXX_kin: DeleteContents() as a template, with the charData cloning
   // XXX_kin: code from CloneContents() merged in.
 
   nsresult res = CloneContents(aReturn);
   if (NS_FAILED(res))
--- a/content/events/public/nsIEventListenerManager.h
+++ b/content/events/public/nsIEventListenerManager.h
@@ -174,17 +174,18 @@ public:
    * listeners registered.
    */
   virtual PRBool HasUnloadListeners() = 0;
 
   /**
    * Returns the mutation bits depending on which mutation listeners are
    * registered to this listener manager.
    * @note If a listener is an nsIDOMMutationListener, all possible mutation
-   *       event bits are returned.
+   *       event bits are returned. All bits are also returned if one of the
+   *       event listeners is registered to handle DOMSubtreeModified events.
    */
   virtual PRUint32 MutationListenerBits() = 0;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsIEventListenerManager,
                               NS_IEVENTLISTENERMANAGER_IID)
 
 nsresult
--- a/content/events/src/nsEventListenerManager.cpp
+++ b/content/events/src/nsEventListenerManager.cpp
@@ -522,17 +522,21 @@ nsEventListenerManager::AddEventListener
     }
 
     if (!window) {
       window = do_QueryInterface(mTarget);
     }
     if (window) {
       NS_ASSERTION(window->IsInnerWindow(),
                    "Setting mutation listener bits on outer window?");
-      window->SetMutationListeners(MutationBitForEventType(aType));
+      // If aType is NS_MUTATION_SUBTREEMODIFIED, we need to listen all
+      // mutations.
+      window->SetMutationListeners((aType == NS_MUTATION_SUBTREEMODIFIED) ?
+                                   kAllMutationBits :
+                                   MutationBitForEventType(aType));
     }
   }
 
   return NS_OK;
 }
 
 nsresult
 nsEventListenerManager::RemoveEventListener(nsIDOMEventListener *aListener, 
@@ -1863,16 +1867,19 @@ nsEventListenerManager::MutationListener
   if (mMayHaveMutationListeners) {
     PRInt32 i, count = mListeners.Count();
     for (i = 0; i < count; ++i) {
       nsListenerStruct* ls = NS_STATIC_CAST(nsListenerStruct*,
                                             mListeners.FastElementAt(i));
       if (ls &&
           (ls->mEventType >= NS_MUTATION_START &&
            ls->mEventType <= NS_MUTATION_END)) {
+        if (ls->mEventType == NS_MUTATION_SUBTREEMODIFIED) {
+          return kAllMutationBits;
+        }
         bits |= MutationBitForEventType(ls->mEventType);
       }
     }
   }
   return bits;
 }
 
 PRBool
--- a/content/html/content/src/nsGenericHTMLElement.cpp
+++ b/content/html/content/src/nsGenericHTMLElement.cpp
@@ -757,16 +757,19 @@ nsGenericHTMLElement::GetInnerHTML(nsASt
 
 nsresult
 nsGenericHTMLElement::SetInnerHTML(const nsAString& aInnerHTML)
 {
   // This BeginUpdate/EndUpdate pair is important to make us reenable the
   // scriptloader before the last EndUpdate call.
   mozAutoDocUpdate updateBatch(GetCurrentDoc(), UPDATE_CONTENT_MODEL, PR_TRUE);
 
+  // Batch possible DOMSubtreeModified events.
+  mozAutoSubtreeModified subtree(GetOwnerDoc(), nsnull);
+
   // Remove childnodes
   nsContentUtils::SetNodeTextContent(this, EmptyString(), PR_FALSE);
 
   nsCOMPtr<nsIDOMDocumentFragment> df;
 
   nsCOMPtr<nsIDocument> doc = GetOwnerDoc();
 
   // Strong ref since appendChild can fire events
--- a/content/xul/content/src/nsXULElement.cpp
+++ b/content/xul/content/src/nsXULElement.cpp
@@ -1493,16 +1493,17 @@ nsXULElement::UnsetAttr(PRInt32 aNameSpa
 
         mutation.mRelatedNode = attrNode;
         mutation.mAttrName = aName;
 
         if (!oldValue.IsEmpty())
           mutation.mPrevAttrValue = do_GetAtom(oldValue);
         mutation.mAttrChange = nsIDOMMutationEvent::REMOVAL;
 
+        mozAutoSubtreeModified subtree(GetOwnerDoc(), this);
         nsEventDispatcher::Dispatch(NS_STATIC_CAST(nsIContent*, this),
                                     nsnull, &mutation);
     }
 
     if (doc) {
         nsXBLBinding *binding = doc->BindingManager()->GetBinding(this);
         if (binding)
             binding->AttributeChanged(aName, aNameSpaceID, PR_TRUE, aNotify);