Bug 1357365 - part 2: Make `TypeInState::OnSelectionChange()` stop inserting new content into the link if clicked outside it r=m_kato

When mouse button is clicked outside a link element but caret is positioned
start or end of the link, our traditional behavior keeps inserting new content
into the link.  But this is different from the other browsers, and it does
not make sense to treat such selection change is intended to keep typing in
the link element.

Therefore, this patch makes `TypeInState::OnSelectionChange()` handle
selection change reason is `mousedown` and `mouseup` cases.  However,
it cannot know whether the event was fired in the parent link element or
not.  Therefore, this patch makes `HTMLEditorEventListener` notifies
`TypeInState` of mouse events via `HTMLEditor`.

Differential Revision: https://phabricator.services.mozilla.com/D101001
This commit is contained in:
Masayuki Nakano 2021-01-13 01:55:27 +00:00
Родитель 0028b2127b
Коммит 4b218fc374
8 изменённых файлов: 357 добавлений и 38 удалений

Просмотреть файл

@ -143,6 +143,9 @@ class Event : public nsISupports, public nsWrapperCache {
virtual void DuplicatePrivateData();
bool IsDispatchStopped();
WidgetEvent* WidgetEventPtr();
const WidgetEvent* WidgetEventPtr() const {
return const_cast<Event*>(this)->WidgetEventPtr();
}
virtual void Serialize(IPC::Message* aMsg, bool aSerializeInterfaceType);
virtual bool Deserialize(const IPC::Message* aMsg, PickleIterator* aIter);
void SetOwner(EventTarget* aOwner);

Просмотреть файл

@ -212,6 +212,62 @@ class HTMLEditUtils final {
static bool IsNonListSingleLineContainer(nsINode& aNode);
static bool IsSingleLineContainer(nsINode& aNode);
/**
* IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a
* link.
*/
template <typename PT, typename CT>
static bool IsPointAtEdgeOfLink(const EditorDOMPointBase<PT, CT>& aPoint,
Element** aFoundLinkElement = nullptr) {
if (aFoundLinkElement) {
*aFoundLinkElement = nullptr;
}
if (!aPoint.IsInContentNode()) {
return false;
}
if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) {
return false;
}
// XXX Assuming it's not in an empty text node because it's unrealistic edge
// case.
bool maybeStartOfAnchor = aPoint.IsStartOfContainer();
for (EditorRawDOMPoint point(aPoint.GetContainer());
point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
: point.IsAtLastContent());
point.Set(point.GetContainer())) {
if (HTMLEditUtils::IsLink(point.GetContainer())) {
// Now, we're at start or end of <a href>.
if (aFoundLinkElement) {
*aFoundLinkElement = do_AddRef(point.ContainerAsElement()).take();
}
return true;
}
}
return false;
}
/**
* IsContentInclusiveDescendantOfLink() returns true if aContent is a
* descendant of a link element.
* Note that this returns true even if editing host of aContent is in a link
* element.
*/
static bool IsContentInclusiveDescendantOfLink(
nsIContent& aContent, Element** aFoundLinkElement = nullptr) {
if (aFoundLinkElement) {
*aFoundLinkElement = nullptr;
}
for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
if (HTMLEditUtils::IsLink(element)) {
if (aFoundLinkElement) {
*aFoundLinkElement = do_AddRef(element).take();
}
return true;
}
}
return false;
}
/**
* GetLastLeafChild() returns rightmost leaf content in aNode. It depends on
* aChildBlockBoundary whether this scans into a block child or treat

Просмотреть файл

@ -700,6 +700,24 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
return rv;
}
void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) {
if (mTypeInState) {
// mTypeInState will be notified of selection change even if aMouseDownEvent
// is not an acceptable event for this editor. Therefore, we need to notify
// it of this event too.
mTypeInState->PreHandleMouseEvent(aMouseDownEvent);
}
}
void HTMLEditor::PreHandleMouseUp(const MouseEvent& aMouseUpEvent) {
if (mTypeInState) {
// mTypeInState will be notified of selection change even if aMouseUpEvent
// is not an acceptable event for this editor. Therefore, we need to notify
// it of this event too.
mTypeInState->PreHandleMouseEvent(aMouseUpEvent);
}
}
nsresult HTMLEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent) {
// NOTE: When you change this method, you should also change:
// * editor/libeditor/tests/test_htmleditor_keyevent_handling.html

Просмотреть файл

@ -165,6 +165,14 @@ class HTMLEditor final : public TextEditor,
MOZ_CAN_RUN_SCRIPT NS_IMETHOD InsertLineBreak() override;
/**
* PreHandleMouseDown() and PreHandleMouseUp() are called before
* HTMLEditorEventListener handles them. The coming event may be
* non-acceptable event.
*/
void PreHandleMouseDown(const dom::MouseEvent& aMouseDownEvent);
void PreHandleMouseUp(const dom::MouseEvent& aMouseUpEvent);
MOZ_CAN_RUN_SCRIPT virtual nsresult HandleKeyPressEvent(
WidgetKeyboardEvent* aKeyboardEvent) override;
virtual nsIContent* GetFocusedContent() const override;

Просмотреть файл

@ -230,6 +230,9 @@ nsresult HTMLEditorEventListener::ListenToWindowResizeEvent(bool aListen) {
}
nsresult HTMLEditorEventListener::MouseUp(MouseEvent* aMouseEvent) {
MOZ_ASSERT(aMouseEvent);
MOZ_ASSERT(aMouseEvent->IsTrusted());
if (DetachedFromEditor()) {
return NS_OK;
}
@ -239,6 +242,8 @@ nsresult HTMLEditorEventListener::MouseUp(MouseEvent* aMouseEvent) {
RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
MOZ_ASSERT(htmlEditor);
htmlEditor->PreHandleMouseUp(*aMouseEvent);
if (NS_WARN_IF(!aMouseEvent->GetTarget())) {
return NS_ERROR_FAILURE;
}
@ -369,6 +374,9 @@ nsresult HTMLEditorEventListener::HandleSecondaryMouseButtonDown(
}
nsresult HTMLEditorEventListener::MouseDown(MouseEvent* aMouseEvent) {
MOZ_ASSERT(aMouseEvent);
MOZ_ASSERT(aMouseEvent->IsTrusted());
if (NS_WARN_IF(!aMouseEvent) || DetachedFromEditor()) {
return NS_OK;
}
@ -384,6 +392,8 @@ nsresult HTMLEditorEventListener::MouseDown(MouseEvent* aMouseEvent) {
RefPtr<HTMLEditor> htmlEditor = mEditorBase->AsHTMLEditor();
MOZ_ASSERT(htmlEditor);
htmlEditor->PreHandleMouseDown(*aMouseEvent);
if (!IsAcceptableMouseEvent(*htmlEditor, aMouseEvent)) {
return EditorEventListener::MouseDown(aMouseEvent);
}

Просмотреть файл

@ -11,6 +11,7 @@
#include "mozilla/EditorBase.h"
#include "mozilla/mozalloc.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/Selection.h"
#include "nsAString.h"
#include "nsDebug.h"
@ -50,7 +51,12 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(TypeInState, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(TypeInState, Release)
TypeInState::TypeInState() : mRelativeFontSize(0) { Reset(); }
TypeInState::TypeInState()
: mRelativeFontSize(0),
mMouseDownFiredInLinkElement(false),
mMouseUpFiredInLinkElement(false) {
Reset();
}
TypeInState::~TypeInState() {
// Call Reset() to release any data that may be in
@ -78,6 +84,31 @@ nsresult TypeInState::UpdateSelState(Selection* aSelection) {
return NS_OK;
}
void TypeInState::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) {
MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown ||
aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp);
bool& eventFiredInLinkElement =
aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown
? mMouseDownFiredInLinkElement
: mMouseUpFiredInLinkElement;
eventFiredInLinkElement = false;
if (aMouseDownOrUpEvent.DefaultPrevented()) {
return;
}
// If mouse button is down or up in a link element, we shouldn't unlink
// it when we get a notification of selection change.
EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget();
if (NS_WARN_IF(!target)) {
return;
}
nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
if (NS_WARN_IF(!targetContent)) {
return;
}
eventFiredInLinkElement =
HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
}
void TypeInState::OnSelectionChange(Selection& aSelection, int16_t aReason) {
// XXX: Selection currently generates bogus selection changed notifications
// XXX: (bug 140303). It can notify us when the selection hasn't actually
@ -89,7 +120,23 @@ void TypeInState::OnSelectionChange(Selection& aSelection, int16_t aReason) {
// XXX: This code temporarily fixes the problem where clicking the mouse in
// XXX: the same location clears the type-in-state.
bool mouseEventFiredInLinkElement = false;
if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON)) {
MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON)) !=
(nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON));
bool& eventFiredInLinkElement =
aReason & nsISelectionListener::MOUSEDOWN_REASON
? mMouseDownFiredInLinkElement
: mMouseUpFiredInLinkElement;
mouseEventFiredInLinkElement = eventFiredInLinkElement;
eventFiredInLinkElement = false;
}
bool unlink = false;
bool resetAllStyles = true;
if (aSelection.IsCollapsed() && aSelection.RangeCount()) {
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(aSelection));
@ -97,36 +144,65 @@ void TypeInState::OnSelectionChange(Selection& aSelection, int16_t aReason) {
return;
}
if (mLastSelectionPoint == selectionStartPoint) {
// We got a bogus selection changed notification!
return;
}
// If caret comes from outside of <a href> element, we should clear "link"
// style after reset.
if (aReason == nsISelectionListener::KEYPRESS_REASON &&
mLastSelectionPoint.IsSet() && selectionStartPoint.IsInTextNode() &&
(selectionStartPoint.IsStartOfContainer() ||
selectionStartPoint.IsEndOfContainer()) &&
// If we're moving in same text node, we can assume that we should
// stay in the <a href>.
mLastSelectionPoint.GetContainer() !=
selectionStartPoint.GetContainer()) {
// XXX Assuming it's not empty text node because it's unrealistic edge
// case.
bool maybeStartOfAnchor = selectionStartPoint.IsStartOfContainer();
for (EditorRawDOMPoint point(selectionStartPoint.GetContainer());
point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
: point.IsAtLastContent());
point.Set(point.GetContainer())) {
// TODO: We should check editing host boundary here.
if (HTMLEditUtils::IsLink(point.GetContainer())) {
// Now, we're at start or end of <a href>.
unlink = !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
point.GetContainer());
break;
RefPtr<Element> linkElement;
if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint,
getter_AddRefs(linkElement))) {
// If caret comes from outside of <a href> element, we should clear "link"
// style after reset.
if (aReason == nsISelectionListener::KEYPRESS_REASON) {
MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON)));
if (mLastSelectionPoint == selectionStartPoint) {
// We got a bogus selection changed notification!
return;
}
if (mLastSelectionPoint.IsSet() && selectionStartPoint.IsInTextNode() &&
// If we're moving in same text node, we can assume that we should
// stay in the <a href>.
mLastSelectionPoint.GetContainer() !=
selectionStartPoint.GetContainer()) {
// XXX Assuming it's not empty text node because it's unrealistic edge
// case.
bool maybeStartOfAnchor = selectionStartPoint.IsStartOfContainer();
for (EditorRawDOMPoint point(selectionStartPoint.GetContainer());
point.IsSet() && (maybeStartOfAnchor ? point.IsStartOfContainer()
: point.IsAtLastContent());
point.Set(point.GetContainer())) {
// TODO: We should check editing host boundary here.
if (HTMLEditUtils::IsLink(point.GetContainer())) {
// Now, we're at start or end of <a href>.
unlink =
!mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
point.GetContainer());
break;
}
}
}
} else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON)) {
if (mLastSelectionPoint == selectionStartPoint) {
// If all styles are cleared or link style is explicitly set, we
// shouldn't reset them without caret move.
if (AreAllStylesCleared() || IsLinkStyleSet()) {
return;
}
// And if non-link styles are cleared or some styles are set, we
// shouldn't reset them too, but we may need to change the link
// style.
if (AreSomeStylesSet() ||
(AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
resetAllStyles = false;
}
}
// If the corresponding mouse event is fired in a link element,
// we should keep treating inputting content as content in the link,
// but otherwise, i.e., clicked outside the link, we should stop
// treating inputting content as content in the link.
unlink = !mouseEventFiredInLinkElement;
}
} else if (mLastSelectionPoint == selectionStartPoint) {
return;
}
mLastSelectionPoint = selectionStartPoint;
@ -137,10 +213,24 @@ void TypeInState::OnSelectionChange(Selection& aSelection, int16_t aReason) {
mLastSelectionPoint.Clear();
}
Reset();
if (resetAllStyles) {
Reset();
if (unlink) {
ClearProp(nsGkAtoms::a, nullptr);
}
return;
}
if (unlink == IsExplicitlyLinkStyleCleared()) {
return;
}
// Even if we shouldn't touch existing style, we need to set/clear only link
// style in some cases.
if (unlink) {
ClearProp(nsGkAtoms::a, nullptr);
} else if (!unlink) {
RemovePropFromClearedList(nsGkAtoms::a, nullptr);
}
}
@ -302,7 +392,7 @@ bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr,
if (FindPropInList(aProp, aAttr, nullptr, mClearedArray, outIndex)) {
return true;
}
if (FindPropInList(nullptr, nullptr, nullptr, mClearedArray, outIndex)) {
if (AreAllStylesCleared()) {
// special case for all props cleared
outIndex = -1;
return true;
@ -312,7 +402,7 @@ bool TypeInState::IsPropCleared(nsAtom* aProp, nsAtom* aAttr,
bool TypeInState::FindPropInList(nsAtom* aProp, nsAtom* aAttr,
nsAString* outValue,
nsTArray<PropItem*>& aList,
const nsTArray<PropItem*>& aList,
int32_t& outIndex) {
if (aAttr == nsGkAtoms::_empty) {
aAttr = nullptr;

Просмотреть файл

@ -26,6 +26,7 @@ class nsINode;
namespace mozilla {
namespace dom {
class MouseEvent;
class Selection;
} // namespace dom
@ -88,6 +89,15 @@ class TypeInState final {
nsresult UpdateSelState(dom::Selection* aSelection);
/**
* PreHandleMouseEvent() is called when `HTMLEditorEventListener` receives
* "mousedown" and "mouseup" events. Note that `aMouseDownOrUpEvent` may not
* be acceptable event for the `HTMLEditor`, but this is called even in
* the case because the event may cause a following `OnSelectionChange()`
* call.
*/
void PreHandleMouseEvent(const dom::MouseEvent& aMouseDownOrUpEvent);
void OnSelectionChange(dom::Selection& aSelection, int16_t aReason);
void SetProp(nsAtom* aProp, nsAtom* aAttr, const nsAString& aValue);
@ -117,7 +127,8 @@ class TypeInState final {
nsAtom* aAttr = nullptr, nsString* outValue = nullptr);
static bool FindPropInList(nsAtom* aProp, nsAtom* aAttr, nsAString* outValue,
nsTArray<PropItem*>& aList, int32_t& outIndex);
const nsTArray<PropItem*>& aList,
int32_t& outIndex);
protected:
virtual ~TypeInState();
@ -130,10 +141,33 @@ class TypeInState final {
bool IsPropCleared(nsAtom* aProp, nsAtom* aAttr);
bool IsPropCleared(nsAtom* aProp, nsAtom* aAttr, int32_t& outIndex);
bool IsLinkStyleSet() const {
int32_t unusedIndex = -1;
return FindPropInList(nsGkAtoms::a, nullptr, nullptr, mSetArray,
unusedIndex);
}
bool IsExplicitlyLinkStyleCleared() const {
int32_t unusedIndex = -1;
return FindPropInList(nsGkAtoms::a, nullptr, nullptr, mClearedArray,
unusedIndex);
}
bool IsOnlyLinkStyleCleared() const {
return mClearedArray.Length() == 1 && IsExplicitlyLinkStyleCleared();
}
bool AreAllStylesCleared() const {
int32_t unusedIndex = -1;
return FindPropInList(nullptr, nullptr, nullptr, mClearedArray,
unusedIndex);
}
bool AreSomeStylesSet() const { return !mSetArray.IsEmpty(); }
bool AreSomeStylesCleared() const { return !mClearedArray.IsEmpty(); }
nsTArray<PropItem*> mSetArray;
nsTArray<PropItem*> mClearedArray;
EditorDOMPoint mLastSelectionPoint;
int32_t mRelativeFontSize;
bool mMouseDownFiredInLinkElement;
bool mMouseUpFiredInLinkElement;
};
} // namespace mozilla

Просмотреть файл

@ -9,8 +9,8 @@
<body>
<p id="display"></p>
<div id="content">
<div contenteditable></div>
<iframe srcdoc="<!doctype html><html><body></body></html>"></iframe>
<div contenteditable style="padding: 5px;"></div>
<iframe srcdoc="<!doctype html><html><body style='padding: 5px;'></body></html>"></iframe>
</div>
<pre id="test">
@ -18,7 +18,7 @@
SimpleTest.waitForExplicitFinish();
function doTest(editor, selection) {
function doTest(editor, selection, win) {
const kDescription =
editor.getAttribute("contenteditable") === null ? "designMode" : "contenteditable";
@ -135,6 +135,48 @@ function doTest(editor, selection) {
is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>",
`${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to start of the preceding <b> (invisible <br>)`);
for (const startPos of [2, 0]) {
editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><a href="about:blank">XYabc</a></p>',
`${kDescription}: Typing X and Y after clicking left half of the first character of the link ${
startPos === 0 ? "(without caret move)" : ""
} should insert them to start of the link`);
editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p>XY<a href="about:blank">abc</a></p>',
`${kDescription}: Typing X and Y after clicking "before" the first character of the link ${
startPos === 0 ? "(without caret move)" : ""
} should insert them to before the link`);
editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><b><a href="about:blank">XYabc</a></b></p>',
`${kDescription}: Typing X and Y after clicking left half of the first character of the link in <b> ${
startPos === 0 ? "(without caret move)" : ""
} should insert them to start of the link`);
editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><b>XY<a href="about:blank">abc</a></b></p>',
`${kDescription}: Typing X and Y after clicking "before" the first character of the link in <b> ${
startPos === 0 ? "(without caret move)" : ""
} should insert them to before the link`);
}
// At end
editor.innerHTML = "<p><a href=\"about:blank\">abc</a>def</p>";
selection.collapse(editor.querySelector("a[href]").firstChild, 2);
@ -253,20 +295,78 @@ function doTest(editor, selection) {
synthesizeKey("Y");
is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a><b>XYdef</b></p>",
`${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert to start of next <b> (invisible <br>)`);
for (const startPos of [1, 3]) {
editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><a href="about:blank">abcXY</a></p>',
`${kDescription}: Typing X and Y after clicking right half of the last character of the link ${
startPos === 3 ? "(without caret move)" : ""
} should insert them to end of the link`);
editor.innerHTML = '<p><a href="about:blank">abc</a></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><a href="about:blank">abc</a>XY</p>',
`${kDescription}: Typing X and Y after clicking "after" the last character of the link ${
startPos === 3 ? "(without caret move)" : ""
} should insert them to after the link`);
editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><b><a href="about:blank">abcXY</a></b></p>',
`${kDescription}: Typing X and Y after clicking right half of the last character of the link in <b> ${
startPos === 3 ? "(without caret move)" : ""
} should insert them to end of the link`);
editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>';
selection.collapse(editor.querySelector("a").firstChild, startPos);
synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win);
synthesizeKey("X");
synthesizeKey("Y");
is(editor.innerHTML, '<p><b><a href="about:blank">abc</a>XY</b></p>',
`${kDescription}: Typing X and Y after clicking "after" the last character of the link in <b> ${
startPos === 3 ? "(without caret move)" : ""
} should insert them to after the link`);
}
// at middle of link
editor.innerHTML = '<p><a href="about:blank">abcde</a></p>';
selection.collapse(editor.querySelector("a").firstChild, 0);
synthesizeMouseAtCenter(editor.querySelector("a"), {}, win);
synthesizeKey("X");
synthesizeKey("Y");
if (selection.focusOffset == 4) {
is(editor.innerHTML, '<p><a href="about:blank">abXYcde</a></p>',
`${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`);
} else if (selection.focusOffset == 5) {
is(editor.innerHTML, '<p><a href="about:blank">abcXYde</a></p>',
`${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`);
} else {
ok(false, `selection is collapsed at unexpected offset got ${selection.focusOffset} but expected 2 or 3`);
}
}
SimpleTest.waitForFocus(() => {
let editor = document.querySelector("[contenteditable]");
let selection = getSelection();
editor.focus();
doTest(editor, selection);
doTest(editor, selection, window);
let iframe = document.querySelector("iframe");
editor = iframe.contentDocument.body;
selection = iframe.contentWindow.getSelection();
iframe.contentDocument.designMode = "on";
iframe.contentWindow.focus();
doTest(editor, selection);
doTest(editor, selection, iframe.contentWindow);
SimpleTest.finish();
});