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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -438,25 +438,12 @@ void Event::PreventDefaultInternal(bool aCalledByDefaultHandler,
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetDragEvent* dragEvent = mEvent->AsDragEvent();
|
||||
if (!dragEvent) {
|
||||
return;
|
||||
// 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);
|
||||
}
|
||||
|
||||
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 (principal && !principal->IsSystemPrincipal()) {
|
||||
dragEvent->mDefaultPreventedOnContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include "mozilla/dom/Element.h" // for Element
|
||||
#include "mozilla/dom/Event.h" // for Event
|
||||
#include "mozilla/dom/EventTarget.h" // for EventTarget
|
||||
#include "mozilla/dom/HTMLInputElement.h"
|
||||
#include "mozilla/dom/HTMLTextAreaElement.h"
|
||||
#include "mozilla/dom/MouseEvent.h" // for MouseEvent
|
||||
#include "mozilla/dom/Selection.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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
77
editor/libeditor/tests/test_nested_editor.html
Normal file
77
editor/libeditor/tests/test_nested_editor.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user