Bug 662591 - HTMLEditor should set caret to start of first editable text node or before first editable inline node r=m_kato
authorMasayuki Nakano <masayuki@d-toybox.com>
Fri, 02 Mar 2018 14:20:25 +0900
changeset 461711 575562183a3bc245c50dcf45d8dc21043b4565ae
parent 461710 490f87831c110acaa3eb966679ad94a1eaf51dd9
child 461715 a007dd56b9947a93c276e82275d7065db1949c9e
child 461720 d7a43e59a1b4ea7c294e7645c4f77d798fe7be67
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato
bugs662591
milestone60.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 662591 - HTMLEditor should set caret to start of first editable text node or before first editable inline node r=m_kato Currently, HTMLEditor doesn't initialize caret position when it gets focus by itself in most cases. Only when it's in designMode, it may move caret to the first visible (not checking CSS actually). In most cases, caret position is adjusted when EditorBase::InitializeSelection() calls Selection::SetAncestorLimiter(). If selected range is outside of new limiter, it moves caret to start of the new limiter. However, this is really different behavior from the other browsers. The other browsers try to move caret to the first editable text node or before the first editable content such as <img>, <input>, etc. This difference causes a serious incompatible issue with Draft.js. It doesn't initialize caret position when it gets focus but it assumes that caret is always set to before <br> element if there is no other content. So, let's try to behave as what other browsers do as far as possible. This patch makes editor behave as: * if selection is already in the editing host except start of the editing host, does nothing. * if there is non-editable element before any editable node, move caret to start of the editing host. * if there is editable text node or element node which cannot have a text node, move its start or before it. * if there is no editable nodes which can contain text nodes, move caret to start of the editing host. Note that before applying this patch, in designMode, BeginningOfDocument() used document element instead of <body> element. Therefore, it may set odd position if <head> element has some text nodes with <script> or <style>. However, this doesn't make sense and for making more consistent behavior between designMode and contenteditable, this patch makes it use editing host (it's <body> element if it's in designMode). MozReview-Commit-ID: 5neYoTMq6Cc
editor/libeditor/EditorBase.cpp
editor/libeditor/EditorBase.h
editor/libeditor/HTMLEditor.cpp
editor/libeditor/HTMLEditor.h
editor/libeditor/tests/test_contenteditable_focus.html
editor/nsIEditor.idl
testing/web-platform/moz.build
testing/web-platform/mozilla/meta/MANIFEST.json
testing/web-platform/mozilla/tests/editor/initial_selection_on_focus.html
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -5084,30 +5084,33 @@ EditorBase::HandleInlineSpellCheck(EditA
 
 already_AddRefed<nsIContent>
 EditorBase::FindSelectionRoot(nsINode* aNode)
 {
   nsCOMPtr<nsIContent> rootContent = GetRoot();
   return rootContent.forget();
 }
 
+void
+EditorBase::InitializeSelectionAncestorLimit(Selection& aSelection,
+                                             nsIContent& aAncestorLimit)
+{
+  aSelection.SetAncestorLimiter(&aAncestorLimit);
+}
+
 nsresult
 EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
 {
   nsCOMPtr<nsINode> targetNode = do_QueryInterface(aFocusEventTarget);
   NS_ENSURE_TRUE(targetNode, NS_ERROR_INVALID_ARG);
   nsCOMPtr<nsIContent> selectionRootContent = FindSelectionRoot(targetNode);
   if (!selectionRootContent) {
     return NS_OK;
   }
 
-  bool isTargetDoc =
-    targetNode->NodeType() == nsINode::DOCUMENT_NODE &&
-    targetNode->HasFlag(NODE_IS_EDITABLE);
-
   RefPtr<Selection> selection = GetSelection();
   NS_ENSURE_STATE(selection);
 
   nsCOMPtr<nsIPresShell> presShell = GetPresShell();
   NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
 
   nsCOMPtr<nsISelectionController> selectionController =
     GetSelectionController();
@@ -5125,34 +5128,28 @@ EditorBase::InitializeSelection(nsIDOMEv
 
   // Init selection
   selectionController->SetDisplaySelection(
                          nsISelectionController::SELECTION_ON);
   selectionController->SetSelectionFlags(
                          nsISelectionDisplay::DISPLAY_ALL);
   selectionController->RepaintSelection(
                          nsISelectionController::SELECTION_NORMAL);
+
   // If the computed selection root isn't root content, we should set it
   // as selection ancestor limit.  However, if that is root element, it means
   // there is not limitation of the selection, then, we must set nullptr.
   // NOTE: If we set a root element to the ancestor limit, some selection
   // methods don't work fine.
   if (selectionRootContent->GetParent()) {
-    selection->SetAncestorLimiter(selectionRootContent);
+    InitializeSelectionAncestorLimit(*selection, *selectionRootContent);
   } else {
     selection->SetAncestorLimiter(nullptr);
   }
 
-  // XXX What case needs this?
-  if (isTargetDoc) {
-    if (!selection->RangeCount()) {
-      BeginningOfDocument();
-    }
-  }
-
   // If there is composition when this is called, we may need to restore IME
   // selection because if the editor is reframed, this already forgot IME
   // selection and the transaction.
   if (mComposition && mComposition->IsMovingToNewTextNode()) {
     // We need to look for the new text node from current selection.
     // XXX If selection is changed during reframe, this doesn't work well!
     nsRange* firstRange = selection->GetRangeAt(0);
     if (NS_WARN_IF(!firstRange)) {
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -728,16 +728,29 @@ protected:
    * placeholder transaction to wrap up any further transaction while the
    * batch is open.  The advantage of this is that placeholder transactions
    * can later merge, if needed.  Merging is unavailable between transaction
    * manager batches.
    */
   void BeginPlaceholderTransaction(nsAtom* aTransactionName);
   void EndPlaceholderTransaction();
 
+  /**
+   * InitializeSelectionAncestorLimit() is called by InitializeSelection().
+   * When this is called, each implementation has to call
+   * aSelection.SetAncestorLimiter() with aAnotherLimit.
+   *
+   * @param aSelection          The selection.
+   * @param aAncestorLimit      New ancestor limit of aSelection.  This always
+   *                            has parent node.  So, it's always safe to
+   *                            call SetAncestorLimit() with this node.
+   */
+  virtual void InitializeSelectionAncestorLimit(Selection& aSelection,
+                                                nsIContent& aAncestorLimit);
+
 public:
   /**
    * All editor operations which alter the doc should be prefaced
    * with a call to StartOperation, naming the action and direction.
    */
   NS_IMETHOD StartOperation(EditAction opID,
                             nsIEditor::EDirection aDirection);
 
--- a/editor/libeditor/HTMLEditor.cpp
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -502,90 +502,182 @@ HTMLEditor::InitRules()
     mRules = new HTMLEditRules();
   }
   return mRules->Init(this);
 }
 
 NS_IMETHODIMP
 HTMLEditor::BeginningOfDocument()
 {
+  return MaybeCollapseSelectionAtFirstEditableNode(false);
+}
+
+void
+HTMLEditor::InitializeSelectionAncestorLimit(Selection& aSelection,
+                                             nsIContent& aAncestorLimit)
+{
+  // Hack for initializing selection.
+  // HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to
+  // collapse selection at first editable text node or inline element which
+  // cannot have text nodes as its children.  However, selection has already
+  // set into the new editing host by user, we should not change it.  For
+  // solving this issue, we should do nothing if selection range is in active
+  // editing host except it's not collapsed at start of the editing host since
+  // aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection
+  // at start of the new limiter if focus node of aSelection is outside of the
+  // editing host.  However, we need to check here if selection is already
+  // collapsed at start of the editing host because it's possible JS to do it.
+  // In such case, we should not modify selection with calling
+  // MaybeCollapseSelectionAtFirstEditableNode().
+
+  // Basically, we should try to collapse selection at first editable node
+  // in HTMLEditor.
+  bool tryToCollapseSelectionAtFirstEditableNode = true;
+  if (aSelection.RangeCount() == 1 && aSelection.IsCollapsed()) {
+    Element* editingHost = GetActiveEditingHost();
+    nsRange* range = aSelection.GetRangeAt(0);
+    if (range->GetStartContainer() == editingHost &&
+        !range->StartOffset()) {
+      // JS or user operation has already collapsed selection at start of
+      // the editing host.  So, we don't need to try to change selection
+      // in this case.
+      tryToCollapseSelectionAtFirstEditableNode = false;
+    }
+  }
+
+  EditorBase::InitializeSelectionAncestorLimit(aSelection, aAncestorLimit);
+
+  // XXX Do we need to check if we still need to change selection?  E.g.,
+  //     we could have already lost focus while we're changing the ancestor
+  //     limiter because it may causes "selectionchange" event.
+  if (tryToCollapseSelectionAtFirstEditableNode) {
+    MaybeCollapseSelectionAtFirstEditableNode(true);
+  }
+}
+
+nsresult
+HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
+              bool aIgnoreIfSelectionInEditingHost)
+{
   // XXX Why doesn't this check if the document is alive?
   if (!IsInitialized()) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
-  // Get the selection
   RefPtr<Selection> selection = GetSelection();
-  NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
-
-  // Get the root element.
-  nsCOMPtr<Element> rootElement = GetRoot();
-  if (!rootElement) {
-    NS_WARNING("GetRoot() returned a null pointer (mRootElement is null)");
+  if (NS_WARN_IF(!selection)) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+
+  // Use editing host.  If you use root element here, selection may be
+  // moved to <head> element, e.g., if there is a text node in <script>
+  // element.  So, we should use active editing host.
+  RefPtr<Element> editingHost = GetActiveEditingHost();
+  if (NS_WARN_IF(!editingHost)) {
     return NS_OK;
   }
 
-  // Find first editable thingy
-  bool done = false;
-  nsCOMPtr<nsINode> curNode = rootElement.get(), selNode;
-  int32_t curOffset = 0, selOffset = 0;
-  while (!done) {
-    WSRunObject wsObj(this, curNode, curOffset);
+  // If selection range is already in the editing host and the range is not
+  // start of the editing host, we shouldn't reset selection.  E.g., window
+  // is activated when the editor had focus before inactivated.
+  if (aIgnoreIfSelectionInEditingHost && selection->RangeCount() == 1) {
+    nsRange* range = selection->GetRangeAt(0);
+    if (!range->Collapsed() ||
+        range->GetStartContainer() != editingHost.get() ||
+        range->StartOffset()) {
+      return NS_OK;
+    }
+  }
+
+  // Find first editable and visible node.
+  EditorRawDOMPoint pointToPutCaret(editingHost, 0);
+  for (;;) {
+    WSRunObject wsObj(this, pointToPutCaret.GetContainer(),
+                      pointToPutCaret.Offset());
     int32_t visOffset = 0;
     WSType visType;
     nsCOMPtr<nsINode> visNode;
-    wsObj.NextVisibleNode(curNode, curOffset, address_of(visNode), &visOffset,
-                          &visType);
+    wsObj.NextVisibleNode(pointToPutCaret.GetContainer(),
+                          pointToPutCaret.Offset(),
+                          address_of(visNode), &visOffset, &visType);
+
+    // If we meet a non-editable node first, we should move caret to start of
+    // the editing host (perhaps, user may want to insert something before
+    // the first non-editable node? Chromium behaves so).
+    if (visNode && !visNode->IsEditable()) {
+      pointToPutCaret.Set(editingHost, 0);
+      break;
+    }
+
+    // WSRunObject::NextVisibleNode() returns WSType::special and the "special"
+    // node when it meets empty inline element.  In this case, we should go to
+    // next sibling.  For example, if current editor is:
+    // <div contenteditable><span></span><b><br></b></div>
+    // then, we should put caret at the <br> element.  So, let's check if
+    // found node is an empty inline container element.
+    if (visType == WSType::special && visNode &&
+        TagCanContainTag(*visNode->NodeInfo()->NameAtom(),
+                         *nsGkAtoms::textTagName)) {
+      pointToPutCaret.Set(visNode);
+      DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
+      NS_WARNING_ASSERTION(advanced,
+        "Failed to advance offset from found empty inline container element");
+      continue;
+    }
+
+    // If there is editable and visible text node, move caret at start of it.
     if (visType == WSType::normalWS || visType == WSType::text) {
-      selNode = visNode;
-      selOffset = visOffset;
-      done = true;
-    } else if (visType == WSType::br || visType == WSType::special) {
-      selNode = visNode->GetParentNode();
-      selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
-      done = true;
-    } else if (visType == WSType::otherBlock) {
-      // By definition of WSRunObject, a block element terminates a
-      // whitespace run. That is, although we are calling a method that is
-      // named "NextVisibleNode", the node returned might not be
-      // visible/editable!
-      //
-      // If the given block does not contain any visible/editable items, we
-      // want to skip it and continue our search.
-
-      if (!IsContainer(visNode)) {
-        // However, we were given a block that is not a container.  Since the
-        // block can not contain anything that's visible, such a block only
-        // makes sense if it is visible by itself, like a <hr>.  We want to
-        // place the caret in front of that block.
-        selNode = visNode->GetParentNode();
-        selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
-        done = true;
-      } else {
-        bool isEmptyBlock;
-        if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) &&
-            isEmptyBlock) {
-          // Skip the empty block
-          curNode = visNode->GetParentNode();
-          curOffset = curNode ? curNode->ComputeIndexOf(visNode) : -1;
-          curOffset++;
-        } else {
-          curNode = visNode;
-          curOffset = 0;
-        }
-        // Keep looping
-      }
+      pointToPutCaret.Set(visNode, visOffset);
+      break;
+    }
+
+    // If there is editable <br> or something inline special element like
+    // <img>, <input>, etc, move caret before it.
+    if (visType == WSType::br || visType == WSType::special) {
+      pointToPutCaret.Set(visNode);
+      break;
+    }
+
+    // If there is no visible/editable node except another block element in
+    // current editing host, we should move caret to very first of the editing
+    // host.
+    // XXX This may not make sense, but Chromium behaves so.  Therefore, the
+    //     reason why we do this is just compatibility with Chromium.
+    if (visType != WSType::otherBlock) {
+      pointToPutCaret.Set(editingHost, 0);
+      break;
+    }
+
+    // By definition of WSRunObject, a block element terminates a whitespace
+    // run. That is, although we are calling a method that is named
+    // "NextVisibleNode", the node returned might not be visible/editable!
+
+    // However, we were given a block that is not a container.  Since the
+    // block can not contain anything that's visible, such a block only
+    // makes sense if it is visible by itself, like a <hr>.  We want to
+    // place the caret in front of that block.
+    if (!IsContainer(visNode)) {
+      pointToPutCaret.Set(visNode);
+      break;
+    }
+
+    // If the given block does not contain any visible/editable items, we want
+    // to skip it and continue our search.
+    bool isEmptyBlock;
+    if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) && isEmptyBlock) {
+      // Skip the empty block
+      pointToPutCaret.Set(visNode);
+      DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
+      NS_WARNING_ASSERTION(advanced,
+        "Failed to advance offset from the found empty block node");
     } else {
-      // Else we found nothing useful
-      selNode = curNode;
-      selOffset = curOffset;
-      done = true;
+      pointToPutCaret.Set(visNode, 0);
     }
   }
-  return selection->Collapse(selNode, selOffset);
+  return selection->Collapse(pointToPutCaret);
 }
 
 nsresult
 HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent)
 {
   // NOTE: When you change this method, you should also change:
   //   * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
 
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -322,16 +322,20 @@ public:
   }
 
 protected:
   virtual ~HTMLEditor();
 
   using EditorBase::IsBlockNode;
   virtual bool IsBlockNode(nsINode *aNode) override;
 
+  virtual void
+  InitializeSelectionAncestorLimit(Selection& aSelection,
+                                   nsIContent& aAncestorLimit) override;
+
 public:
   // XXX Why don't we move following methods above for grouping by the origins?
   NS_IMETHOD SetFlags(uint32_t aFlags) override;
 
   NS_IMETHOD Paste(int32_t aSelectionType) override;
   NS_IMETHOD CanPaste(int32_t aSelectionType, bool* aCanPaste) override;
 
   NS_IMETHOD PasteTransferable(nsITransferable* aTransferable) override;
@@ -535,16 +539,42 @@ public:
 
   /**
    * Modifies the table containing the selection according to the
    * activation of an inline table editing UI element
    * @param aUIAnonymousElement [IN] the inline table editing UI element
    */
   nsresult DoInlineTableEditingAction(const Element& aUIAnonymousElement);
 
+  /**
+   * MaybeCollapseSelectionAtFirstEditableNode() may collapse selection at
+   * proper position to staring to edit.  If there is a non-editable node
+   * before any editable text nodes or inline elements which can have text
+   * nodes as their children, collapse selection at start of the editing
+   * host.  If there is an editable text node which is not collapsed, collapses
+   * selection at the start of the text node.  If there is an editable inline
+   * element which cannot have text nodes as its child, collapses selection at
+   * before the element node.  Otherwise, collapses selection at start of the
+   * editing host.
+   *
+   * @param aIgnoreIfSelectionInEditingHost
+   *                        This method does nothing if selection is in the
+   *                        editing host except if it's collapsed at start of
+   *                        the editing host.
+   *                        Note that if selection ranges were outside of
+   *                        current selection limiter, selection was collapsed
+   *                        at the start of the editing host therefore, if
+   *                        you call this with setting this to true, you can
+   *                        keep selection ranges if user has already been
+   *                        changed.
+   */
+  nsresult
+  MaybeCollapseSelectionAtFirstEditableNode(
+    bool aIgnoreIfSelectionInEditingHost);
+
 protected:
   class BlobReader final : public nsIEditorBlobListener
   {
   public:
     BlobReader(dom::BlobImpl* aBlob, HTMLEditor* aHTMLEditor,
                bool aIsSafe, nsIDOMDocument* aSourceDoc,
                nsIDOMNode* aDestinationNode, int32_t aDestOffset,
                bool aDoDeleteSelection);
--- a/editor/libeditor/tests/test_contenteditable_focus.html
+++ b/editor/libeditor/tests/test_contenteditable_focus.html
@@ -96,42 +96,42 @@ function runTestsInternal()
   editor.focus();
   is(SpecialPowers.unwrap(fm.focusedElement), editor,
      "editor didn't get focus");
   is(selection.rangeCount, 1,
      "there is no selection range when editor has focus");
   var range = selection.getRangeAt(0);
   ok(range.collapsed, "the selection range isn't collapsed");
   var startNode = range.startContainer;
-  is(startNode.nodeType, 1, "the caret isn't set to the div node");
-  is(startNode, editor, "the caret isn't set to the editor");
+  is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
+  is(startNode, editor.firstChild, "the caret isn't set to the editor");
   ok(selCon.caretVisible, "caret isn't visible in the editor");
   // Move focus to other editor
   otherEditor.focus();
   is(SpecialPowers.unwrap(fm.focusedElement), otherEditor,
      "the other editor didn't get focus");
   is(selection.rangeCount, 1,
      "there is no selection range when the other editor has focus");
   range = selection.getRangeAt(0);
   ok(range.collapsed, "the selection range isn't collapsed");
   var startNode = range.startContainer;
-  is(startNode.nodeType, 1, "the caret isn't set to the div node");
-  is(startNode, otherEditor, "the caret isn't set to the other editor");
+  is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the text node");
+  is(startNode, otherEditor.firstChild, "the caret isn't set to the other editor");
   ok(selCon.caretVisible, "caret isn't visible in the other editor");
   // Move focus to inputTextInEditor
   inputTextInEditor.focus();
   is(SpecialPowers.unwrap(fm.focusedElement), inputTextInEditor,
      "inputTextInEditor didn't get focus #2");
   is(selection.rangeCount, 1, "selection range is lost from the document");
   range = selection.getRangeAt(0);
   ok(range.collapsed, "the selection range isn't collapsed");
   var startNode = range.startContainer;
-  is(startNode.nodeType, 1, "the caret isn't set to the div node");
+  is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
   // XXX maybe, the caret can stay on the other editor if it's better.
-  is(startNode, editor,
+  is(startNode, editor.firstChild,
      "the caret should stay on the other editor");
   ok(selCon.caretVisible,
      "caret isn't visible in the inputTextInEditor");
   // Move focus to the other editor again
   otherEditor.focus();
   is(SpecialPowers.unwrap(fm.focusedElement), otherEditor,
      "the other editor didn't get focus #2");
   // Set selection to the span element in the editor.
--- a/editor/nsIEditor.idl
+++ b/editor/nsIEditor.idl
@@ -325,17 +325,28 @@ interface nsIEditor  : nsISupports
     */
   boolean canPasteTransferable([optional] in nsITransferable aTransferable);
 
   /* ------------ Selection methods -------------- */
 
   /** sets the document selection to the entire contents of the document */
   void selectAll();
 
-  /** sets the document selection to the beginning of the document */
+  /**
+   * Collapses selection at start of the document.  If it's an HTML editor,
+   * collapses selection at start of current editing host (<body> element if
+   * it's in designMode) instead.  If there is a non-editable node before any
+   * editable text nodes or inline elements which can have text nodes as their
+   * children, collapses selection at start of the editing host.  If there is
+   * an editable text node which is not collapsed, collapses selection at
+   * start of the text node.  If there is an editable inline element which
+   * cannot have text nodes as its child, collapses selection at before the
+   * element node.  Otherwise, collapses selection at start of the editing
+   * host.
+   */
   void beginningOfDocument();
 
   /** sets the document selection to the end of the document */
   void endOfDocument();
 
   /* ------------ Node manipulation methods -------------- */
 
   /**
--- a/testing/web-platform/moz.build
+++ b/testing/web-platform/moz.build
@@ -58,16 +58,19 @@ with Files("mozilla/README"):
     BUG_COMPONENT = ("Testing", "web-platform-tests")
 
 with Files("mozilla/meta/**"):
     BUG_COMPONENT = ("Testing", "web-platform-tests")
 
 with Files("mozilla/tests/dom/**"):
     BUG_COMPONENT = ("Core", "DOM")
 
+with Files("mozilla/tests/editor/**"):
+    BUG_COMPONENT = ("Core", "Editor")
+
 with Files("mozilla/tests/fetch/**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 with Files("mozilla/tests/focus/**"):
     BUG_COMPONENT = ("Core", "Editor")
 
 with Files("mozilla/tests/html/**"):
     BUG_COMPONENT = ("Core", "DOM")
--- a/testing/web-platform/mozilla/meta/MANIFEST.json
+++ b/testing/web-platform/mozilla/meta/MANIFEST.json
@@ -464,16 +464,22 @@
     ]
    ],
    "dom/throttling/throttling-ws.window.js": [
     [
      "/_mozilla/dom/throttling/throttling-ws.window.html",
      {}
     ]
    ],
+   "editor/initial_selection_on_focus.html": [
+    [
+     "/_mozilla/editor/initial_selection_on_focus.html",
+     {}
+    ]
+   ],
    "fetch/api/redirect/redirect-referrer.https.html": [
     [
      "/_mozilla/fetch/api/redirect/redirect-referrer.https.html",
      {}
     ]
    ],
    "focus/Range_collapse.html": [
     [
@@ -1017,16 +1023,20 @@
   "dom/throttling/throttling-webrtc.window.js": [
    "02e6acec2ff275e0e935cb6d903d348f98d5d437",
    "testharness"
   ],
   "dom/throttling/throttling-ws.window.js": [
    "67a981ba2a4d08b684947ed42aba6648dcd262b4",
    "testharness"
   ],
+  "editor/initial_selection_on_focus.html": [
+   "da3d0ff5305658e18f51a4f19b34927fb2691e60",
+   "testharness"
+  ],
   "fetch/api/redirect/redirect-referrer-mixed-content.js": [
    "f9d7ec9cf9fa8c847e45664b05482e3f8c191385",
    "support"
   ],
   "fetch/api/redirect/redirect-referrer.https.html": [
    "99cbd16b78771f35e075e4012d8dbc5dce3209c0",
    "testharness"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/tests/editor/initial_selection_on_focus.html
@@ -0,0 +1,360 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>initial selection on focus of contenteditable</title>
+<!-- if you move this file into cross-browser's directly, you should include
+     editing/include/tests.js for using addBrackets() and get rid of it from
+     this file. -->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<p id="staticText">out of editor</p>
+<div id="editor" contenteditable style="min-height: 1em;"></div>
+<script>
+"use strict";
+
+(function() {
+  var tests = [
+    { description: "empty editor should set focus to start of it",
+      content: "{}",
+    },
+    { description: "editor should set selection to start of the text node",
+      content: "[]abc",
+    },
+    { description: "editor should set selection to before the <br> node",
+      content: "{}<br>",
+    },
+    { description: "editor should set selection to before the first <br> node",
+      content: "{}<br><br>",
+    },
+
+    { description: "editor should set selection to start of the text node in the <p> node",
+      content: "<p>[]abc</p>",
+    },
+    { description: "editor should set selection to before the <br> node in the <p> node",
+      content: "<p>{}<br></p>",
+    },
+    { description: "editor should set selection to before the first <br> node in the <p> node",
+      content: "<p>{}<br><br></p>",
+    },
+
+    { description: "editor should set selection to start of the text node in the <span> node",
+      content: "<span>[]abc</span>",
+    },
+    { description: "editor should set selection to before the <br> node in the <span> node",
+      content: "<span>{}<br></span>",
+    },
+    { description: "editor should set selection to before the first <br> node in the <span> node",
+      content: "<span>{}<br><br></span>",
+    },
+
+    { description: "editor should set selection to before the empty <span> node",
+      content: "{}<span></span>",
+    },
+    { description: "editor should set selection to before the empty <b> node",
+      content: "{}<b></b>",
+    },
+    { description: "editor should set selection to before the empty <i> node",
+      content: "{}<i></i>",
+    },
+    { description: "editor should set selection to before the empty <u> node",
+      content: "{}<u></u>",
+    },
+    { description: "editor should set selection to before the empty <s> node",
+      content: "{}<s></s>",
+    },
+    { description: "editor should set selection to before the empty <code> node",
+      content: "{}<code></code>",
+    },
+    { description: "editor should set selection to before the empty <a> node",
+      content: "{}<a href=\"foo.html\"></a>",
+    },
+    { description: "editor should set selection to before the empty <foobar> node",
+      content: "{}<foobar></foobar>",
+    },
+    { description: "editor should set selection to before the <input> node",
+      content: "{}<input>",
+    },
+    { description: "editor should set selection to before the <img> node",
+      content: "{}<img alt=\"foo\">",
+    },
+
+    { description: "editor should set selection to start of the text node in the second <span> node",
+      content: "<span></span><span>[]abc</span>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <span> node",
+      content: "<span></span><span>{}<br></span>",
+    },
+    { description: "editor should set selection to start of the text node in the first <span> node #1",
+      content: "<span>[]abc</span><span>abc</span>",
+    },
+    { description: "editor should set selection to start of the text node in the first <span> node #2",
+      content: "<span>[]abc</span><span><br></span>",
+    },
+    { description: "editor should set selection to before the <br> node in the first <span> node #1",
+      content: "<span>{}<br></span><span><br></span>",
+    },
+    { description: "editor should set selection to before the <br> node in the first <span> node #2",
+      content: "<span>{}<br></span><span>abc</span>",
+    },
+
+    { description: "editor should set selection to start of the text node in the second <span> node since the text node in the first <span> node is only whitespaces",
+      content: "<span> </span><span>[]abc</span>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <span> node since the text node in the first <span> node is only whitespaces",
+      content: "<span> </span><span>{}<br></span>",
+    },
+    { description: "editor should set selection to start of the text node in the second <span> node even if there is a whitespace only text node before the first <span> node",
+      content: " <span></span><span>[]abc</span>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <span> node even if there is a whitespace only text node before the first <span> node",
+      content: " <span></span><span>{}<br></span>",
+    },
+
+    { description: "editor should set selection to start of the text node in the second <p> node",
+      content: "<p></p><p>[]abc</p>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <p> node",
+      content: "<p></p><p>{}<br></p>",
+    },
+    { description: "editor should set selection to start of the text node in the first <p> node #1",
+      content: "<p>[]abc</p><p>abc</p>",
+    },
+    { description: "editor should set selection to start of the text node in the first <p> node #2",
+      content: "<p>[]abc</p><p><br></p>",
+    },
+    { description: "editor should set selection to before the <br> node in the first <p> node #1",
+      content: "<p>{}<br></p><p><br></p>",
+    },
+    { description: "editor should set selection to before the <br> node in the first <p> node #2",
+      content: "<p>{}<br></p><p>abc</p>",
+    },
+
+    { description: "editor should set selection to start of the text node in the second <p> node since the text node in the first <p> node is only whitespaces",
+      content: "<p> </p><p>[]abc</p>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <p> node since the text node in the first <p> node is only whitespaces",
+      content: "<p> </p><p>{}<br></p>",
+    },
+    { description: "editor should set selection to start of the text node in the second <p> node even if there is a whitespace only text node before the first <p> node",
+      content: " <p></p><p>[]abc</p>",
+    },
+    { description: "editor should set selection to before the <br> node in the second <p> node even if there is a whitespace only text node before the first <p> node",
+      content: " <p></p><p>{}<br></p>",
+    },
+
+    { description: "editor should set selection to start of the text node in the <span> node in the second <p> node",
+      content: "<p><span></span></p><p><span>[]abc</span></p>",
+    },
+    { description: "editor should set selection to before the <br> node in the <span> node in the second <p> node",
+      content: "<p><span></span></p><p><span>{}<br></span></p>",
+    },
+    { description: "editor should set selection to start of the text node in the <span> node in the first <p> node #1",
+      content: "<p><span>[]abc</span></p><p><span>abc</span></p>",
+    },
+    { description: "editor should set selection to start of the text node in the <span> node in the first <p> node #2",
+      content: "<p><span>[]abc</span></p><p><span><br></span></p>",
+    },
+    { description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #1",
+      content: "<p><span>{}<br></span></p><p><span><br></span></p>",
+    },
+    { description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #2",
+      content: "<p><span>{}<br></span></p><p><span>abc</span></p>",
+    },
+
+    { description: "editor should set focus to before the non-editable <span> node",
+      content: "{}<span contenteditable=\"false\"></span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node even if it has a text node",
+      content: "{}<span contenteditable=\"false\">abc</span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node even if it has a <br> node",
+      content: "{}<span contenteditable=\"false\"><br></span>",
+    },
+
+    { description: "editor should set focus to before the non-editable empty <span> node followed by a text node",
+      content: "{}<span contenteditable=\"false\"></span><span>abc</span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node having a text node and followed by another text node",
+      content: "{}<span contenteditable=\"false\">abc</span><span>def</span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by a text node",
+      content: "{}<span contenteditable=\"false\"><br></span><span>abc</span>",
+    },
+    { description: "editor should set focus to before the non-editable empty <span> node followed by a <br> node",
+      content: "{}<span contenteditable=\"false\"></span><span><br></span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node having text node and followed by a <br> node",
+      content: "{}<span contenteditable=\"false\">abc</span><span><br></span>",
+    },
+    { description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by another <br> node",
+      content: "{}<span contenteditable=\"false\"><br></span><span><br></span>",
+    },
+
+    { description: "editor should set focus to before the non-editable empty <p> node followed by a text node",
+      content: "{}<p contenteditable=\"false\"></p><p>abc</p>",
+    },
+    { description: "editor should set focus to before the non-editable <p> node having a text node and followed by another text node",
+      content: "{}<p contenteditable=\"false\">abc</p><p>def</p>",
+    },
+    { description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by a text node",
+      content: "{}<p contenteditable=\"false\"><br></p><p>abc</p>",
+    },
+    { description: "editor should set focus to before the non-editable empty <p> node followed by a <br> node",
+      content: "{}<p contenteditable=\"false\"></p><p><br></p>",
+    },
+    { description: "editor should set focus to before the non-editable <p> node having text node and followed by a <br> node",
+      content: "{}<p contenteditable=\"false\">abc</p><p><br></p>",
+    },
+    { description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by another <br> node",
+      content: "{}<p contenteditable=\"false\"><br></p><p><br></p>",
+    },
+
+    { description: "editor should set focus to start of it if there is non-editable node before first editable text node",
+      content: "{}<span></span><span contenteditable=\"false\"></span><span>abc</span>",
+    },
+    { description: "editor should set focus to start of it if there is non-editable node having a text node before first editable text node",
+      content: "{}<span></span><span contenteditable=\"false\">abc</span><span>def</span>",
+    },
+    { description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable text node",
+      content: "{}<span></span><span contenteditable=\"false\"><br></span><span>abc</span>",
+    },
+    { description: "editor should set focus to start of it if there is non-editable node before first editable <br> node",
+      content: "{}<span></span><span contenteditable=\"false\"></span><span><br></span>",
+    },
+    { description: "editor should set focus to start of it if there is non-editable node having a text node before first editable <br> node",
+      content: "{}<span></span><span contenteditable=\"false\">abc</span><span><br></span>",
+    },
+    { description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable <br> node",
+      content: "{}<span></span><span contenteditable=\"false\"><br></span><span><br></span>",
+    },
+
+    { description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node",
+      content: "<span>[]abc</span><span contenteditable=\"false\"></span>",
+    },
+    { description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having another text node",
+      content: "<span>[]abc</span><span contenteditable=\"false\">def</span>",
+    },
+    { description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having a <br> node",
+      content: "<span>[]abc</span><span contenteditable=\"false\"><br></span>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node",
+      content: "<span>{}<br></span><span contenteditable=\"false\"></span>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a text node",
+      content: "<span>{}<br></span><span contenteditable=\"false\">abc</span>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a <br> node",
+      content: "<span>{}<br></span><span contenteditable=\"false\"><br></span>",
+    },
+
+    { description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node",
+      content: "<p>[]abc</p><p contenteditable=\"false\"></p>",
+    },
+    { description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having another text node",
+      content: "<p>[]abc</p><p contenteditable=\"false\">def</p>",
+    },
+    { description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having a <br> node",
+      content: "<p>[]abc</p><p contenteditable=\"false\"><br></p>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node",
+      content: "<p>{}<br></p><p contenteditable=\"false\"></p>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a text node",
+      content: "<p>{}<br></p><p contenteditable=\"false\">abc</p>",
+    },
+    { description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a <br> node",
+      content: "<p>{}<br></p><p contenteditable=\"false\"><br></p>",
+    },
+  ];
+
+  // This function is copied from editing/include/tests.js
+  function addBrackets(range) {
+    // Handle the collapsed case specially, to avoid confusingly getting the
+    // markers backwards in some cases
+    if (range.startContainer.nodeType == Node.TEXT_NODE ||
+        range.startContainer.nodeType == Node.COMMENT_NODE) {
+      if (range.collapsed) {
+        range.startContainer.insertData(range.startOffset, "[]");
+      } else {
+        range.startContainer.insertData(range.startOffset, "[");
+      }
+    } else {
+      var marker = range.collapsed ? "{}" : "{";
+      if (range.startOffset != range.startContainer.childNodes.length &&
+          range.startContainer.childNodes[range.startOffset].nodeType == Node.TEXT_NODE) {
+        range.startContainer.childNodes[range.startOffset].insertData(0, marker);
+      } else if (range.startOffset != 0 &&
+                 range.startContainer.childNodes[range.startOffset - 1].nodeType == Node.TEXT_NODE) {
+        range.startContainer.childNodes[range.startOffset - 1].appendData(marker);
+      } else {
+        // Seems to serialize as I'd want even for tables . . . IE doesn't
+        // allow undefined to be passed as the second argument (it throws
+        // an exception), so we have to explicitly check the number of
+        // children and pass null.
+        range.startContainer.insertBefore(document.createTextNode(marker),
+          range.startContainer.childNodes.length == range.startOffset ?
+            null : range.startContainer.childNodes[range.startOffset]);
+      }
+    }
+    if (range.collapsed) {
+      return;
+    }
+    if (range.endContainer.nodeType == Node.TEXT_NODE ||
+        range.endContainer.nodeType == Node.COMMENT_NODE) {
+      range.endContainer.insertData(range.endOffset, "]");
+    } else {
+      if (range.endOffset != range.endContainer.childNodes.length &&
+          range.endContainer.childNodes[range.endOffset].nodeType == Node.TEXT_NODE) {
+        range.endContainer.childNodes[range.endOffset].insertData(0, "}");
+      } else if (range.endOffset != 0 &&
+                 range.endContainer.childNodes[range.endOffset - 1].nodeType == Node.TEXT_NODE) {
+        range.endContainer.childNodes[range.endOffset - 1].appendData("}");
+      } else {
+        range.endContainer.insertBefore(document.createTextNode("}"),
+          range.endContainer.childNodes.length == range.endOffset ?
+            null : range.endContainer.childNodes[range.endOffset]);
+      }
+    }
+  }
+
+  var editor = document.getElementById("editor");
+  var textInP = document.getElementById("staticText").firstChild;
+  var selection = document.getSelection();
+  for (var i = 0; i < tests.length; i++) {
+    test(function() {
+      // Select outside the editor.
+      editor.blur();
+      selection.collapse(textInP);
+
+      // Initialize the editor content.
+      editor.innerHTML = tests[i].content.replace(/[{}\[\]]/g, "");
+
+      // Make the editor focused.
+      editor.focus();
+
+      assert_equals(selection.rangeCount, 1);
+      if (selection.rangeCount) {
+        addBrackets(selection.getRangeAt(0));
+        assert_equals(editor.innerHTML, tests[i].content);
+      }
+    }, tests[i].description);
+  }
+
+  test(function() {
+    // Check if selection is initialized after temporarily blurred.
+    editor.innerHTML = "<p>abc</p><p>def</p>";
+    editor.focus();
+    // Move selection to the second paragraph.
+    selection.collapse(editor.firstChild.nextSibling.firstChild);
+    // Reset focus.
+    editor.blur();
+    editor.focus();
+    // Then, selection should still be in the second paragraph.
+    assert_equals(selection.rangeCount, 1);
+    if (selection.rangeCount) {
+      addBrackets(selection.getRangeAt(0));
+      assert_equals(editor.innerHTML, "<p>abc</p><p>[]def</p>");
+    }
+  }, "editor shouldn't reset selection when it gets focus again");
+})();
+</script>