Bug 1257363 - If deleting empty block, position caret at the start/end of text node (if any). r=masayuki a=rkent THUNDERBIRD470b1_2016053115_RELBRANCH
authorJorg K
Thu, 31 Mar 2016 14:20:59 -0700
branchTHUNDERBIRD470b1_2016053115_RELBRANCH
changeset 324505 1c1ec913bf3e1bfadca37a05b600529367951680
parent 324501 cf6ec12bd62001b93387ffb184a8841644255b5e
child 324506 8bca5da83fc79e77ddfb3e7dc372c9278cf89ab7
push id6041
push userkent@caspia.com
push dateWed, 01 Jun 2016 21:32:47 +0000
treeherdermozilla-beta@4ebc7380df86 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki, rkent
bugs1257363
milestone47.0
Bug 1257363 - If deleting empty block, position caret at the start/end of text node (if any). r=masayuki a=rkent MozReview-Commit-ID: GXLTnJLFiop
editor/libeditor/nsHTMLEditRules.cpp
editor/libeditor/nsHTMLEditRules.h
editor/libeditor/tests/chrome.ini
editor/libeditor/tests/test_bug1257363.html
--- a/editor/libeditor/nsHTMLEditRules.cpp
+++ b/editor/libeditor/nsHTMLEditRules.cpp
@@ -1912,17 +1912,17 @@ nsHTMLEditRules::WillDeleteSelection(Sel
   NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
 
 
   if (bCollapsed) {
     // If we are inside an empty block, delete it.
     NS_ENSURE_STATE(mHTMLEditor);
     nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost();
     NS_ENSURE_TRUE(host, NS_ERROR_FAILURE);
-    res = CheckForEmptyBlock(startNode, host, aSelection, aHandled);
+    res = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled);
     NS_ENSURE_SUCCESS(res, res);
     if (*aHandled) {
       return NS_OK;
     }
 
     // Test for distance between caret and text that will be deleted
     res = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode),
                                     startOffset, aAction, aCancel);
@@ -4945,16 +4945,17 @@ nsHTMLEditRules::AlignBlockContents(nsID
 ///////////////////////////////////////////////////////////////////////////
 // CheckForEmptyBlock: Called by WillDeleteSelection to detect and handle
 //                     case of deleting from inside an empty block.
 //
 nsresult
 nsHTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode,
                                     Element* aBodyNode,
                                     Selection* aSelection,
+                                    nsIEditor::EDirection aAction,
                                     bool* aHandled)
 {
   // If the editing host is an inline element, bail out early.
   if (IsInlineNode(GetAsDOMNode(aBodyNode))) {
     return NS_OK;
   }
   NS_ENSURE_STATE(mHTMLEditor);
   nsCOMPtr<nsIEditor> kungFuDeathGrip(mHTMLEditor);
@@ -5006,19 +5007,41 @@ nsHTMLEditRules::CheckForEmptyBlock(nsIN
           // Adjust selection to be right before it
           res = aSelection->Collapse(listParent, listOffset);
           NS_ENSURE_SUCCESS(res, res);
         }
         // Else just let selection percolate up.  We'll adjust it in
         // AfterEdit()
       }
     } else {
-      // Adjust selection to be right after it
-      res = aSelection->Collapse(blockParent, offset + 1);
-      NS_ENSURE_SUCCESS(res, res);
+      if (aAction == nsIEditor::eNext) {
+        // Adjust selection to be right after it.
+        res = aSelection->Collapse(blockParent, offset + 1);
+        NS_ENSURE_SUCCESS(res, res);
+
+        // Move to the start of the next node if it's a text.
+        nsCOMPtr<nsIContent> nextNode = mHTMLEditor->GetNextNode(blockParent,
+                                                                 offset + 1, true);
+        if (mHTMLEditor->IsTextNode(nextNode)) {
+          res = aSelection->Collapse(nextNode, 0);
+          NS_ENSURE_SUCCESS(res, res);
+        }
+      } else {
+        // Move to the end of the previous node if it's a text.
+        nsCOMPtr<nsIContent> priorNode = mHTMLEditor->GetPriorNode(blockParent,
+                                                                   offset,
+                                                                   true);
+        if (mHTMLEditor->IsTextNode(priorNode)) {
+          res = aSelection->Collapse(priorNode, priorNode->TextLength());
+          NS_ENSURE_SUCCESS(res, res);
+        } else {
+          res = aSelection->Collapse(blockParent, offset + 1);
+          NS_ENSURE_SUCCESS(res, res);
+        }
+      }
     }
     NS_ENSURE_STATE(mHTMLEditor);
     res = mHTMLEditor->DeleteNode(emptyBlock);
     *aHandled = true;
     NS_ENSURE_SUCCESS(res, res);
   }
   return NS_OK;
 }
--- a/editor/libeditor/nsHTMLEditRules.h
+++ b/editor/libeditor/nsHTMLEditRules.h
@@ -248,16 +248,17 @@ protected:
                                     nsIDOMDocument* aDoc);
   nsresult IsEmptyBlock(nsIDOMNode *aNode,
                         bool *outIsEmptyBlock,
                         bool aMozBRDoesntCount = false,
                         bool aListItemsNotEmpty = false);
   nsresult CheckForEmptyBlock(nsINode* aStartNode,
                               mozilla::dom::Element* aBodyNode,
                               mozilla::dom::Selection* aSelection,
+                              nsIEditor::EDirection aAction,
                               bool* aHandled);
   nsresult CheckForInvisibleBR(nsIDOMNode *aBlock, nsHTMLEditRules::BRLocation aWhere,
                                nsCOMPtr<nsIDOMNode> *outBRNode, int32_t aOffset=0);
   nsresult ExpandSelectionForDeletion(mozilla::dom::Selection* aSelection);
   bool IsFirstNode(nsIDOMNode *aNode);
   bool IsLastNode(nsIDOMNode *aNode);
   nsresult NormalizeSelection(mozilla::dom::Selection* aSelection);
   void GetPromotedPoint(RulesEndpoint aWhere, nsIDOMNode* aNode,
--- a/editor/libeditor/tests/chrome.ini
+++ b/editor/libeditor/tests/chrome.ini
@@ -26,16 +26,17 @@ skip-if = buildapp == 'mulet'
 [test_bug1101392.html]
 [test_bug1140105.html]
 [test_bug1140617.xul]
 [test_bug1153237.html]
 [test_bug1154791.html]
 [test_bug1248128.html]
 [test_bug1248185.html]
 [test_bug1250010.html]
+[test_bug1257363.html]
 [test_composition_event_created_in_chrome.html]
 [test_contenteditable_text_input_handling.html]
 [test_dragdrop.html]
 skip-if = buildapp == 'mulet'
 [test_htmleditor_keyevent_handling.html]
 [test_selection_move_commands.xul]
 [test_texteditor_keyevent_handling.html]
 skip-if = (debug && os=='win') || (os == 'linux') # Bug 1116205, leaks on windows debug, fails delete key on linux
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1257363.html
@@ -0,0 +1,182 @@
+<!DOCTYPE>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1257363
+-->
+<head>
+  <title>Test for Bug 1257363</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<div id="display">
+</div>
+
+<div id="backspaceCSS" contenteditable><p style="color:red;">12345</p>67</div>
+<div id="backspace" contenteditable><p><font color="red">12345</font></p>67</div>
+<div id="deleteCSS" contenteditable><p style="color:red;">x</p></div>
+<div id="delete" contenteditable><p><font color="red">y</font></p></div>
+
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 1257363 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+
+  // ***** Backspace test *****
+  var div = document.getElementById("backspaceCSS");
+  div.focus();
+  synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+  var sel = window.getSelection();
+  var selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 5, "offset should be 5");
+
+  // Return and backspace should take us to where we started.
+  synthesizeKey("VK_RETURN", {});
+  synthesizeKey("VK_BACK_SPACE", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 5, "offset should be 5");
+
+  // Add an "a" to the end of the paragraph.
+  synthesizeKey("a", {});
+
+  // Return and forward delete should take us to the following line.
+  synthesizeKey("VK_RETURN", {});
+  synthesizeKey("VK_DELETE", {});
+
+  // Add a "b" to the start.
+  synthesizeKey("b", {});
+
+  is(div.innerHTML, "<p style=\"color:red;\">12345a</p>b67",
+                    "unexpected HTML");
+
+  // Let's repeat the whole thing, but a font tag instead of CSS.
+  // The behaviour is different since the font is carried over.
+  div = document.getElementById("backspace");
+  div.focus();
+  synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+  sel = window.getSelection();
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 5, "offset should be 5");
+
+  // Return and backspace should take us to where we started.
+  synthesizeKey("VK_RETURN", {});
+  synthesizeKey("VK_BACK_SPACE", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 5, "offset should be 5");
+
+  // Add an "a" to the end of the paragraph.
+  synthesizeKey("a", {});
+
+  // Return and forward delete should take us to the following line.
+  synthesizeKey("VK_RETURN", {});
+  synthesizeKey("VK_DELETE", {});
+
+  // Add a "b" to the start.
+  synthesizeKey("b", {});
+
+  // Here we get a somewhat ugly result since the red sticks.
+  is(div.innerHTML, "<p><font color=\"red\">12345a</font></p><font color=\"red\">b</font>67",
+                    "unexpected HTML");
+
+  // ***** Delete test *****
+  div = document.getElementById("deleteCSS");
+  div.focus();
+  synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+  var sel = window.getSelection();
+  var selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 1, "offset should be 1");
+
+  // left, enter should create a new empty paragraph before
+  // but leave the selection at the start of the existing paragraph.
+  synthesizeKey("VK_LEFT", {});
+  synthesizeKey("VK_RETURN", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+  is(selRange.endOffset, 0, "offset should be 0");
+  is(selRange.endContainer.nodeValue, "x", "we should be in the text node with the x");
+
+  // Now moving up into the new empty paragraph.
+  synthesizeKey("VK_UP", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "P", "selection should be the new empty paragraph");
+  is(selRange.endOffset, 0, "offset should be 0");
+
+  // Forward delete should now take us to where we started.
+  synthesizeKey("VK_DELETE", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+  is(selRange.endOffset, 0, "offset should be 0");
+
+  // Add an "a" to the start of the paragraph.
+  synthesizeKey("a", {});
+
+  is(div.innerHTML, "<p style=\"color:red;\">ax</p>",
+                    "unexpected HTML");
+
+  // Let's repeat the whole thing, but a font tag instead of CSS.
+  div = document.getElementById("delete");
+  div.focus();
+  synthesizeMouse(div, 100, 2, {}); /* click behind and down */
+
+  var sel = window.getSelection();
+  var selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node");
+  is(selRange.endOffset, 1, "offset should be 1");
+
+  // left, enter should create a new empty paragraph before
+  // but leave the selection at the start of the existing paragraph.
+  synthesizeKey("VK_LEFT", {});
+  synthesizeKey("VK_RETURN", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+  is(selRange.endOffset, 0, "offset should be 0");
+  is(selRange.endContainer.nodeValue, "y", "we should be in the text node with the y");
+
+  // Now moving up into the new empty paragraph.
+  synthesizeKey("VK_UP", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "FONT", "selection should be the font tag");
+  is(selRange.endOffset, 0, "offset should be 0");
+  is(selRange.endContainer.parentNode.nodeName, "P", "the parent of the font should be a paragraph");
+
+  // Forward delete should now take us to where we started.
+  synthesizeKey("VK_DELETE", {});
+
+  selRange = sel.getRangeAt(0);
+  is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node");
+  is(selRange.endOffset, 0, "offset should be 0");
+
+  // Add an "a" to the start of the paragraph.
+  synthesizeKey("a", {});
+
+  is(div.innerHTML, "<p><font color=\"red\">ay</font></p>",
+                    "unexpected HTML");
+
+  SimpleTest.finish();
+
+});
+
+</script>
+</body>
+
+</html>