Bug 1817723 - Allow HTMLEditor can receive events when the focus is switched between elements in the same shadow tree. r=masayuki

The EditorEventListener for HTMLEditor is registered on document,
which is problematic because it can't receive events when the focus is
switched between elements in the same shadow tree due to shadow dom
encapsulation.

We fix this by moving the EditorEventListener to nsWindowRoot so the
events can always be received.

Differential Revision: https://phabricator.services.mozilla.com/D178215
This commit is contained in:
Sean Feng
2023-05-23 22:57:16 +00:00
parent 8288075a62
commit 3e889ec933
11 changed files with 215 additions and 43 deletions

View File

@@ -924,6 +924,10 @@ nsDocShellTreeOwner::HandleEvent(Event* aEvent) {
handler->CanDropLink(dragEvent, false, &canDropLink);
if (canDropLink) {
aEvent->PreventDefault();
WidgetDragEvent* asWidgetDropEvent =
dragEvent->WidgetEventPtr()->AsDragEvent();
asWidgetDropEvent->UpdateDefaultPreventedOnContent(
asWidgetDropEvent->mCurrentTarget);
}
} else if (eventType.EqualsLiteral("drop")) {
nsIWebNavigation* webnav = static_cast<nsIWebNavigation*>(mWebBrowser);
@@ -974,6 +978,10 @@ nsDocShellTreeOwner::HandleEvent(Event* aEvent) {
} else {
aEvent->StopPropagation();
aEvent->PreventDefault();
WidgetDragEvent* asWidgetDropEvent =
dragEvent->WidgetEventPtr()->AsDragEvent();
asWidgetDropEvent->UpdateDefaultPreventedOnContent(
asWidgetDropEvent->mCurrentTarget);
}
}

View File

@@ -438,26 +438,13 @@ void Event::PreventDefaultInternal(bool aCalledByDefaultHandler,
return;
}
WidgetDragEvent* dragEvent = mEvent->AsDragEvent();
if (!dragEvent) {
return;
}
nsIPrincipal* principal = nullptr;
nsCOMPtr<nsINode> node =
nsINode::FromEventTargetOrNull(mEvent->mCurrentTarget);
if (node) {
principal = node->NodePrincipal();
} else {
nsCOMPtr<nsIScriptObjectPrincipal> sop =
do_QueryInterface(mEvent->mCurrentTarget);
if (sop) {
principal = sop->GetPrincipal();
// If this is called by default handlers, the caller will call
// UpdateDefaultPreventedOnContentFor when necessary.
if (!aCalledByDefaultHandler) {
if (WidgetDragEvent* dragEvent = mEvent->AsDragEvent()) {
dragEvent->UpdateDefaultPreventedOnContent(dragEvent->mCurrentTarget);
}
}
if (principal && !principal->IsSystemPrincipal()) {
dragEvent->mDefaultPreventedOnContent = true;
}
}
void Event::SetEventType(const nsAString& aEventTypeArg) {
@@ -875,7 +862,6 @@ bool Event::IsDragExitEnabled(JSContext* aCx, JSObject* aGlobal) {
return StaticPrefs::dom_event_dragexit_enabled() ||
nsContentUtils::IsSystemCaller(aCx);
}
} // namespace mozilla::dom
using namespace mozilla;

View File

@@ -1536,4 +1536,33 @@ void EventDispatcher::GetComposedPathFor(WidgetEvent* aEvent,
}
}
void EventChainPreVisitor::IgnoreCurrentTargetBecauseOfShadowDOMRetargeting() {
mCanHandle = false;
mIgnoreBecauseOfShadowDOM = true;
EventTarget* target = nullptr;
auto getWindow = [this]() -> nsPIDOMWindowOuter* {
nsINode* node = nsINode::FromEventTargetOrNull(this->mParentTarget);
if (!node) {
return nullptr;
}
Document* doc = node->GetComposedDoc();
if (!doc) {
return nullptr;
}
return doc->GetWindow();
};
// The HTMLEditor is registered to nsWindowRoot, so we
// want to dispatch events to it.
if (nsCOMPtr<nsPIDOMWindowOuter> win = getWindow()) {
target = win->GetParentTarget();
}
SetParentTarget(target, false);
mEventTargetAtParent = nullptr;
}
} // namespace mozilla

View File

@@ -169,12 +169,7 @@ class MOZ_STACK_CLASS EventChainPreVisitor final : public EventChainVisitor {
}
}
void IgnoreCurrentTargetBecauseOfShadowDOMRetargeting() {
mCanHandle = false;
mIgnoreBecauseOfShadowDOM = true;
SetParentTarget(nullptr, false);
mEventTargetAtParent = nullptr;
}
void IgnoreCurrentTargetBecauseOfShadowDOMRetargeting();
/**
* Member that must be set in GetEventTargetParent by event targets. If set to

View File

@@ -310,6 +310,24 @@ uint16_t MouseEvent::MozInputSource() const {
} // namespace mozilla::dom
using namespace mozilla;
void WidgetDragEvent::UpdateDefaultPreventedOnContent(
dom::EventTarget* aTarget) {
nsIPrincipal* principal = nullptr;
nsINode* node = nsINode::FromEventTargetOrNull(aTarget);
if (node) {
principal = node->NodePrincipal();
} else {
nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aTarget);
if (sop) {
principal = sop->GetPrincipal();
}
}
if (principal && !principal->IsSystemPrincipal()) {
mDefaultPreventedOnContent = true;
}
}
using namespace mozilla::dom;
already_AddRefed<MouseEvent> NS_NewDOMMouseEvent(EventTarget* aOwner,

View File

@@ -5609,8 +5609,9 @@ nsresult EditorBase::FinalizeSelection() {
// TODO: Running script from here makes harder to handle blur events. We
// should do this asynchronously.
focusManager->UpdateCaretForCaretBrowsingMode();
if (nsCOMPtr<nsINode> node = do_QueryInterface(GetDOMEventTarget())) {
if (node->OwnerDoc()->GetUnretargetedFocusedContent() != node) {
if (RefPtr<Element> rootElement = GetExposedRoot()) {
if (rootElement->OwnerDoc()->GetUnretargetedFocusedContent() !=
rootElement) {
selectionController->SelectionWillLoseFocus();
}
}
@@ -5912,6 +5913,19 @@ bool EditorBase::CanKeepHandlingFocusEvent(
if (!focusManager->GetFocusedElement()) {
return false;
}
// If there's an HTMLEditor registered in the target document and we
// are not that HTMLEditor (for cases like nested documents), let
// that HTMLEditor to handle the focus event.
if (IsHTMLEditor()) {
const HTMLEditor* precedentHTMLEditor =
aOriginalEventTargetNode.OwnerDoc()->GetHTMLEditor();
if (precedentHTMLEditor && precedentHTMLEditor != this) {
return false;
}
}
const nsIContent* exposedTargetContent =
aOriginalEventTargetNode.AsContent()
->FindFirstNonChromeOnlyAccessContent();

View File

@@ -28,7 +28,9 @@
#include "mozilla/dom/Element.h" // for Element
#include "mozilla/dom/Event.h" // for Event
#include "mozilla/dom/EventTarget.h" // for EventTarget
#include "mozilla/dom/MouseEvent.h" // for MouseEvent
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLTextAreaElement.h"
#include "mozilla/dom/MouseEvent.h" // for MouseEvent
#include "mozilla/dom/Selection.h"
#include "nsAString.h"
@@ -156,18 +158,18 @@ nsresult EditorEventListener::InstallToEditor() {
#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
eventListenerManager->AddEventListenerByType(
this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture());
#endif
eventListenerManager->AddEventListenerByType(
this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
this, u"dragover"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
this, u"dragleave"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->AddEventListenerByType(
this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
this, u"drop"_ns, TrustedEventsAtSystemGroupCapture());
// XXX We should add the mouse event listeners as system event group.
// E.g., web applications cannot prevent middle mouse paste by
// preventDefault() of click event at bubble phase.
@@ -238,18 +240,18 @@ void EditorEventListener::UninstallFromEditor() {
#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
eventListenerManager->RemoveEventListenerByType(
this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture());
#endif
eventListenerManager->RemoveEventListenerByType(
this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
this, u"dragover"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
this, u"dragleave"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(
this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
this, u"drop"_ns, TrustedEventsAtSystemGroupCapture());
eventListenerManager->RemoveEventListenerByType(this, u"mousedown"_ns,
TrustedEventsAtCapture());
eventListenerManager->RemoveEventListenerByType(this, u"mouseup"_ns,
@@ -315,10 +317,34 @@ NS_IMETHODIMP EditorEventListener::HandleEvent(Event* aEvent) {
// each event handler would just ignore the event. So, in this method,
// you don't need to check if the QI succeeded before each call.
WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
// For nested documents with multiple HTMLEditor registered on different
// nsWindowRoot, make sure the HTMLEditor for the original event target
// handles the events.
if (mEditorBase->IsHTMLEditor()) {
MOZ_ASSERT(aEvent->GetCurrentTarget()->IsRootWindow());
nsCOMPtr<nsINode> originalEventTargetNode =
nsINode::FromEventTargetOrNull(aEvent->GetOriginalTarget());
if (originalEventTargetNode &&
mEditorBase != originalEventTargetNode->OwnerDoc()->GetHTMLEditor()) {
return NS_OK;
}
}
switch (internalEvent->mMessage) {
// dragover and drop
case eDragOver:
case eDrop: {
// The editor which is registered on nsWindowRoot shouldn't handle
// drop events when it can be handled by Input or TextArea element on
// the chain.
if (aEvent->GetCurrentTarget()->IsRootWindow() &&
TextControlElement::FromEventTargetOrNull(
aEvent->GetOriginalTarget())) {
return NS_OK;
}
// aEvent should be grabbed by the caller since this is
// nsIDOMEventListener method. However, our clang plugin cannot check it
// if we use Event::As*Event(). So, we need to grab it by ourselves.
@@ -853,9 +879,13 @@ nsresult EditorEventListener::DragOverOrDrop(DragEvent* aDragEvent) {
}
aDragEvent->PreventDefault();
WidgetDragEvent* asWidgetEvent = aDragEvent->WidgetEventPtr()->AsDragEvent();
asWidgetEvent->UpdateDefaultPreventedOnContent(asWidgetEvent->mTarget);
aDragEvent->StopImmediatePropagation();
if (aDragEvent->WidgetEventPtr()->mMessage == eDrop) {
if (asWidgetEvent->mMessage == eDrop) {
RefPtr<EditorBase> editorBase = mEditorBase;
nsresult rv = editorBase->HandleDropEvent(aDragEvent);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
@@ -863,7 +893,7 @@ nsresult EditorEventListener::DragOverOrDrop(DragEvent* aDragEvent) {
return rv;
}
MOZ_ASSERT(aDragEvent->WidgetEventPtr()->mMessage == eDragOver);
MOZ_ASSERT(asWidgetEvent->mMessage == eDragOver);
// If we handle the dragged item, we need to adjust drop effect here
// because once DataTransfer is retrieved, DragEvent has initialized it

View File

@@ -6874,7 +6874,19 @@ EventTarget* HTMLEditor::GetDOMEventTarget() const {
// whether Init() was ever called. So we need to get the document
// ourselves, if it exists.
MOZ_ASSERT(IsInitialized(), "The HTMLEditor has not been initialized yet");
return GetDocument();
Document* doc = GetDocument();
if (!doc) {
return nullptr;
}
// Register the EditorEventListener to the parent of window.
//
// The advantage of this approach is HTMLEditor can still
// receive events when shadow dom is involved.
if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
return win->GetParentTarget();
}
return nullptr;
}
bool HTMLEditor::ShouldReplaceRootElement() const {

View File

@@ -249,6 +249,7 @@ support-files =
[test_composition_event_created_in_chrome.html]
[test_composition_with_highlight_in_texteditor.html]
[test_contenteditable_focus.html]
[test_nested_editor.html]
[test_cut_copy_delete_command_enabled.html]
[test_cut_copy_password.html]
[test_defaultParagraphSeparatorBR_between_blocks.html]

View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<title> Test for nested contenteditable elements </title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<template id="focus-iframe-contenteditable-in-div">
<div contenteditable>
<iframe srcdoc="<div id='focusme' contenteditable></div>"></iframe>
</div>
</template>
<template id="focus-contenteditable-parent-along-with-iframe">
<div id='focusme' contenteditable></div>
<iframe srcdoc="<div contenteditable></div>"></iframe>
</template>
<template id="focus-iframe-textarea-in-div">
<div contenteditable>
<iframe srcdoc="<textarea id='focusme'></textarea>"></iframe>
</div>
</template>
<template id="focus-textarea-parent-along-with-iframe">
<textarea id='focusme' contenteditable></textarea>
<iframe srcdoc="<div contenteditable></div>"></iframe>
</template>
<script>
"use strict";
async function runTest() {
function findFocusme() {
// eslint-disable-next-line consistent-return
return new Promise(r => {
let focusInParent = document.getElementById("focusme");
if (focusInParent) {
return r(focusInParent);
}
document.querySelector("iframe").addEventListener("load", function() {
return r(document.querySelector("iframe").contentDocument.getElementById("focusme"));
});
});
}
const focusme = await findFocusme();
focusme.focus();
synthesizeKey("abc");
if (focusme.nodeName === "TEXTAREA") {
is(focusme.value, "abc");
} else {
is(focusme.innerHTML, "abc");
}
}
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(async () => {
for (const template of document.querySelectorAll("template")) {
const content = template.content.cloneNode(true);
document.body.appendChild(content);
await runTest();
document.body.innerHTML = "";
}
SimpleTest.finish();
});
</script>
</body>
</html>

View File

@@ -370,6 +370,8 @@ class WidgetDragEvent : public WidgetMouseEvent {
mDefaultPreventedOnContent = aEvent.mDefaultPreventedOnContent;
}
void UpdateDefaultPreventedOnContent(dom::EventTarget* aTarget);
/**
* Should be called before dispatching the DOM tree if this event is
* synthesized for tests because drop effect is initialized before