Bug 698237 - Invalidate affected frames when a range in a selection is modified. r=smaug
authorMats Palmgren <matspal@gmail.com>
Sat, 24 Dec 2011 14:26:03 +0100
changeset 83314 91909393c5a0c1621fc8e327ceecfb3df0da24d5
parent 83313 94b5440efa60f2b3782898b3947c8afde2ca9ae9
child 83315 4b2c62d75deadce231799f9f77dd5cf5aa93b476
push id4349
push usermpalmgren@mozilla.com
push dateSat, 24 Dec 2011 13:29:43 +0000
treeherdermozilla-inbound@4b2c62d75dea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs698237
milestone12.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 698237 - Invalidate affected frames when a range in a selection is modified. r=smaug
content/base/src/nsRange.cpp
content/base/src/nsRange.h
layout/reftests/selection/modify-range-ref.html
layout/reftests/selection/modify-range.html
layout/reftests/selection/reftest.list
--- a/content/base/src/nsRange.cpp
+++ b/content/base/src/nsRange.cpp
@@ -79,16 +79,42 @@ nsresult NS_NewContentSubtreeIterator(ns
     if (!nsContentUtils::CanCallerAccess(node_)) {                                 \
       return NS_ERROR_DOM_SECURITY_ERR;                                            \
     }                                                                              \
     if (mIsDetached) {                                                             \
       return NS_ERROR_DOM_INVALID_STATE_ERR;                                       \
     }                                                                              \
   PR_END_MACRO
 
+static void InvalidateAllFrames(nsINode* aNode)
+{
+  NS_PRECONDITION(aNode, "bad arg");
+
+  nsIFrame* frame = nsnull;
+  switch (aNode->NodeType()) {
+    case nsIDOMNode::TEXT_NODE:
+    case nsIDOMNode::ELEMENT_NODE:
+    {
+      nsIContent* content = static_cast<nsIContent*>(aNode);
+      frame = content->GetPrimaryFrame();
+      break;
+    }
+    case nsIDOMNode::DOCUMENT_NODE:
+    {
+      nsIDocument* doc = static_cast<nsIDocument*>(aNode);
+      nsIPresShell* shell = doc ? doc->GetShell() : nsnull;
+      frame = shell ? shell->GetRootFrame() : nsnull;
+      break;
+    }
+  }
+  for (nsIFrame* f = frame; f; f = f->GetNextContinuation()) {
+    f->InvalidateFrameSubtree();
+  }
+}
+
 // Utility routine to detect if a content node is completely contained in a range
 // If outNodeBefore is returned true, then the node starts before the range does.
 // If outNodeAfter is returned true, then the node ends after the range does.
 // Note that both of the above might be true.
 // If neither are true, the node is contained inside of the range.
 // XXX - callers responsibility to ensure node in same doc as range! 
 
 // static
@@ -934,16 +960,17 @@ nsINode* nsIRange::IsValidBoundary(nsINo
 }
 
 NS_IMETHODIMP
 nsRange::SetStart(nsIDOMNode* aParent, PRInt32 aOffset)
 {
   VALIDATE_ACCESS(aParent);
 
   nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
+  AutoInvalidateSelection atEndOfBlock(this);
   return SetStart(parent, aOffset);
 }
 
 /* virtual */ nsresult
 nsRange::SetStart(nsINode* aParent, PRInt32 aOffset)
 {
   nsINode* newRoot = IsValidBoundary(aParent);
   NS_ENSURE_TRUE(newRoot, NS_ERROR_DOM_RANGE_INVALID_NODE_TYPE_ERR);
@@ -995,16 +1022,17 @@ nsRange::SetStartAfter(nsIDOMNode* aSibl
   return SetStart(nParent, IndexOf(aSibling) + 1);
 }
 
 NS_IMETHODIMP
 nsRange::SetEnd(nsIDOMNode* aParent, PRInt32 aOffset)
 {
   VALIDATE_ACCESS(aParent);
 
+  AutoInvalidateSelection atEndOfBlock(this);
   nsCOMPtr<nsINode> parent = do_QueryInterface(aParent);
   return SetEnd(parent, aOffset);
 }
 
 
 /* virtual */ nsresult
 nsRange::SetEnd(nsINode* aParent, PRInt32 aOffset)
 {
@@ -1062,16 +1090,17 @@ nsRange::SetEndAfter(nsIDOMNode* aSiblin
 NS_IMETHODIMP
 nsRange::Collapse(bool aToStart)
 {
   if(mIsDetached)
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   if (!mIsPositioned)
     return NS_ERROR_NOT_INITIALIZED;
 
+  AutoInvalidateSelection atEndOfBlock(this);
   if (aToStart)
     DoSetRange(mStartParent, mStartOffset, mStartParent, mStartOffset, mRoot);
   else
     DoSetRange(mEndParent, mEndOffset, mEndParent, mEndOffset, mRoot);
 
   return NS_OK;
 }
 
@@ -1087,30 +1116,32 @@ nsRange::SelectNode(nsIDOMNode* aN)
   nsINode* newRoot = IsValidBoundary(parent);
   NS_ENSURE_TRUE(newRoot, NS_ERROR_DOM_RANGE_INVALID_NODE_TYPE_ERR);
 
   PRInt32 index = parent->IndexOf(node);
   if (index < 0) {
     return NS_ERROR_DOM_RANGE_INVALID_NODE_TYPE_ERR;
   }
 
+  AutoInvalidateSelection atEndOfBlock(this);
   DoSetRange(parent, index, parent, index + 1, newRoot);
   
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsRange::SelectNodeContents(nsIDOMNode* aN)
 {
   VALIDATE_ACCESS(aN);
 
   nsCOMPtr<nsINode> node = do_QueryInterface(aN);
   nsINode* newRoot = IsValidBoundary(node);
   NS_ENSURE_TRUE(newRoot, NS_ERROR_DOM_RANGE_INVALID_NODE_TYPE_ERR);
   
+  AutoInvalidateSelection atEndOfBlock(this);
   DoSetRange(node, 0, node, GetNodeLength(node), newRoot);
   
   return NS_OK;
 }
 
 // The Subtree Content Iterator only returns subtrees that are
 // completely within a given range. It doesn't return a CharacterData
 // node that contains either the start or end point of the range.,
@@ -2330,16 +2361,20 @@ nsRange::ToString(nsAString& aReturn)
 
 
 NS_IMETHODIMP
 nsRange::Detach()
 {
   if(mIsDetached)
     return NS_ERROR_DOM_INVALID_STATE_ERR;
 
+  if (IsInSelection()) {
+    ::InvalidateAllFrames(GetRegisteredCommonAncestor());
+  }
+
   mIsDetached = true;
 
   DoSetRange(nsnull, 0, nsnull, 0, nsnull);
   
   return NS_OK;
 }
 
 // nsIDOMNSRange interface
@@ -2586,8 +2621,43 @@ nsRange::GetUsedFontFaces(nsIDOMFontFace
     }
 
     nsLayoutUtils::GetFontFacesForFrames(frame, fontFaceList);
   }
 
   fontFaceList.forget(aResult);
   return NS_OK;
 }
+
+nsINode*
+nsRange::GetRegisteredCommonAncestor()
+{
+  NS_ASSERTION(IsInSelection(),
+               "GetRegisteredCommonAncestor only valid for range in selection");
+  nsINode* ancestor = GetNextRangeCommonAncestor(mStartParent);
+  while (ancestor) {
+    RangeHashTable* ranges =
+      static_cast<RangeHashTable*>(ancestor->GetProperty(nsGkAtoms::range));
+    if (ranges->GetEntry(this)) {
+      break;
+    }
+    ancestor = GetNextRangeCommonAncestor(ancestor->GetNodeParent());
+  }
+  NS_ASSERTION(ancestor, "can't find common ancestor for selected range");
+  return ancestor;
+}
+
+/* static */ bool nsRange::AutoInvalidateSelection::mIsNested;
+
+nsRange::AutoInvalidateSelection::~AutoInvalidateSelection()
+{
+  NS_ASSERTION(mWasInSelection == mRange->IsInSelection(),
+               "Range got unselected in AutoInvalidateSelection block");
+  if (!mCommonAncestor) {
+    return;
+  }
+  mIsNested = false;
+  ::InvalidateAllFrames(mCommonAncestor);
+  nsINode* commonAncestor = mRange->GetRegisteredCommonAncestor();
+  if (commonAncestor != mCommonAncestor) {
+    ::InvalidateAllFrames(commonAncestor);
+  }
+}
--- a/content/base/src/nsRange.h
+++ b/content/base/src/nsRange.h
@@ -160,16 +160,50 @@ public:
 protected:
   // CharacterDataChanged set aNotInsertedYet to true to disable an assertion
   // and suppress re-registering a range common ancestor node since
   // the new text node of a splitText hasn't been inserted yet.
   // CharacterDataChanged does the re-registering when needed.
   void DoSetRange(nsINode* aStartN, PRInt32 aStartOffset,
                   nsINode* aEndN, PRInt32 aEndOffset,
                   nsINode* aRoot, bool aNotInsertedYet = false);
+
+  /**
+   * For a range for which IsInSelection() is true, return the common
+   * ancestor for the range.  This method uses the selection bits and
+   * nsGkAtoms::range property on the nodes to quickly find the ancestor.
+   * That is, it's a faster version of GetCommonAncestor that only works
+   * for ranges in a Selection.  The method will assert and the behavior
+   * is undefined if called on a range where IsInSelection() is false.
+   */
+  nsINode* GetRegisteredCommonAncestor();
+
+  struct NS_STACK_CLASS AutoInvalidateSelection
+  {
+    AutoInvalidateSelection(nsRange* aRange) : mRange(aRange)
+    {
+#ifdef DEBUG
+      mWasInSelection = mRange->IsInSelection();
+#endif
+      if (!mRange->IsInSelection() || mIsNested) {
+        return;
+      }
+      mIsNested = true;
+      NS_ASSERTION(!mRange->IsDetached(), "detached range in selection");
+      mCommonAncestor = mRange->GetRegisteredCommonAncestor();
+    }
+    ~AutoInvalidateSelection();
+    nsRange* mRange;
+    nsRefPtr<nsINode> mCommonAncestor;
+#ifdef DEBUG
+    bool mWasInSelection;
+#endif
+    static bool mIsNested;
+  };
+  
 };
 
 // Make a new nsIDOMRange object
 nsresult NS_NewRange(nsIDOMRange** aInstancePtrResult);
 
 // Make a new nsIRangeUtils object
 nsresult NS_NewRangeUtils(nsIRangeUtils** aInstancePtrResult);
 
new file mode 100644
--- /dev/null
+++ b/layout/reftests/selection/modify-range-ref.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+    <title>Testcase for bug </title>
+<script>
+var tests_done = 0;
+var tests = [
+  'window.getSelection().getRangeAt(0).setEnd(document.getElementsByTagName("pre")[0].firstChild,9)',
+  'window.getSelection().getRangeAt(0).setEndAfter(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).setEndBefore(document.getElementsByTagName("pre")[0].childNodes[1])',
+  'pre=document.getElementsByTagName("pre")[0]; r=window.getSelection().getRangeAt(0); r.setEnd(pre.childNodes[1],3); r.setStartAfter(pre.firstChild)',
+  'window.getSelection().getRangeAt(0).setStartBefore(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).selectNode(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).selectNodeContents(document.getElementsByTagName("pre")[0])',
+  'window.getSelection().getRangeAt(0).collapse(true)',
+  'window.getSelection().getRangeAt(0).surroundContents(document.createElement("span"))',
+  'window.getSelection().getRangeAt(0).setStart(document,0)',
+  'window.getSelection().getRangeAt(0).detach()',
+  'window.getSelection().getRangeAt(0).extractContents()',
+  'window.getSelection().getRangeAt(0).deleteContents()'
+];
+function init_iframe(d) {
+  var pre = d.createElement('pre');
+  pre.appendChild(d.createTextNode('first\nfirst\n'));
+  pre.appendChild(d.createTextNode('second'));
+  d.documentElement.appendChild(pre);
+  var text = pre.firstChild;
+  var sel = d.defaultView.getSelection();
+  var r = d.createRange();
+  r.setStart(text,0)
+  r.setEnd(text,3)
+  sel.addRange(r);
+  d.documentElement.offsetHeight;
+}
+function test_iframe(iframe, i) {
+  iframe.contentDocument.write(
+    '<'+'style>span { text-decoration:underline; } <'+'/style>' +
+    '<'+'script>' + 
+        'window.parent.init_iframe(document);' +
+        'setTimeout(function(){' + window.parent.tests[i] + '; sel=window.getSelection(); try{r=sel.getRangeAt(0); sel.removeRange(r); sel.addRange(r);}catch(e){};  ++window.parent.tests_done; },0)' +
+    '<'+'/script>'
+  );
+}
+function create_iframe(i) {
+  var div = document.createElement('div');
+  document.body.appendChild(div);
+  div.innerHTML = "<iframe src='about:blank' style='height:6em; width:12em; float:left;' frameborder='0' onload='test_iframe(this,"+i+")'><iframe>"
+}
+
+var id;
+function check_if_done() {
+  if (tests_done == tests.length) {
+    clearInterval(id);
+    document.documentElement.className = "";
+  }
+}
+
+function test() {
+  for (i = 0; i < tests.length; ++i) {
+    create_iframe(i);
+  }
+  id = setInterval(check_if_done,500);
+}
+</script>
+
+</head>
+<body onload="test()"></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/selection/modify-range.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+    <title>Testcase for bug </title>
+<script>
+var tests_done = 0;
+var tests = [
+  'window.getSelection().getRangeAt(0).setEnd(document.getElementsByTagName("pre")[0].firstChild,9)',
+  'window.getSelection().getRangeAt(0).setEndAfter(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).setEndBefore(document.getElementsByTagName("pre")[0].childNodes[1])',
+  'pre=document.getElementsByTagName("pre")[0]; r=window.getSelection().getRangeAt(0); r.setEnd(pre.childNodes[1],3); r.setStartAfter(pre.firstChild)',
+  'window.getSelection().getRangeAt(0).setStartBefore(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).selectNode(document.getElementsByTagName("pre")[0].firstChild)',
+  'window.getSelection().getRangeAt(0).selectNodeContents(document.getElementsByTagName("pre")[0])',
+  'window.getSelection().getRangeAt(0).collapse(true)',
+  'window.getSelection().getRangeAt(0).surroundContents(document.createElement("span"))',
+  'window.getSelection().getRangeAt(0).setStart(document,0)',
+  'window.getSelection().getRangeAt(0).detach()',
+  'window.getSelection().getRangeAt(0).extractContents()',
+  'window.getSelection().getRangeAt(0).deleteContents()'
+];
+function init_iframe(d) {
+  var pre = d.createElement('pre');
+  pre.appendChild(d.createTextNode('first\nfirst\n'));
+  pre.appendChild(d.createTextNode('second'));
+  d.documentElement.appendChild(pre);
+  var text = pre.firstChild;
+  var sel = d.defaultView.getSelection();
+  var r = d.createRange();
+  r.setStart(text,0)
+  r.setEnd(text,3)
+  sel.addRange(r);
+  d.documentElement.offsetHeight;
+}
+function test_iframe(iframe, i) {
+  iframe.contentDocument.write(
+    '<'+'style>span { text-decoration:underline; } <'+'/style>' +
+    '<'+'script>' + 
+        'window.parent.init_iframe(document);' +
+        'setTimeout(function(){' + window.parent.tests[i] + '; ++window.parent.tests_done; },0)' +
+    '<'+'/script>'
+  );
+}
+function create_iframe(i) {
+  var div = document.createElement('div');
+  document.body.appendChild(div);
+  div.innerHTML = "<iframe src='about:blank' style='height:6em; width:12em; float:left;' frameborder='0' onload='test_iframe(this,"+i+")'><iframe>"
+}
+
+var id;
+function check_if_done() {
+  if (tests_done == tests.length) {
+    clearInterval(id);
+    document.documentElement.className = "";
+  }
+}
+
+function test() {
+  for (i = 0; i < tests.length; ++i) {
+    create_iframe(i);
+  }
+  id = setInterval(check_if_done,500);
+}
+</script>
+
+</head>
+<body onload="test()"></body>
+</html>
--- a/layout/reftests/selection/reftest.list
+++ b/layout/reftests/selection/reftest.list
@@ -26,9 +26,10 @@
 == extend-4b.html extend-4-ref.html
 fails-if(Android) != pseudo-element-of-native-anonymous.html pseudo-element-of-native-anonymous-ref.html # bug 676641
 # These tests uses Highlight and HighlightText color keywords, they are not same as text selection color on Mac.
 fails-if(cocoaWidget) == non-themed-widget.html non-themed-widget-ref.html
 fails-if(cocoaWidget) == themed-widget.html themed-widget-ref.html
 == addrange-1.html addrange-ref.html
 == addrange-2.html addrange-ref.html
 == splitText-normalize.html splitText-normalize-ref.html
+== modify-range.html modify-range-ref.html
 == dom-mutations.html dom-mutations-ref.html