--- 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>