mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
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:
parent
f9da675446
commit
ddb6212fd9
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,26 +436,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) {
|
||||
|
@ -1555,4 +1555,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
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
#include "mozilla/dom/MouseEvent.h"
|
||||
#include "mozilla/MouseEvents.h"
|
||||
#include "mozilla/BasePrincipal.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsIContent.h"
|
||||
#include "nsIScreenManager.h"
|
||||
@ -310,6 +311,25 @@ uint16_t MouseEvent::MozInputSource() const {
|
||||
} // namespace mozilla::dom
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
void WidgetDragEvent::UpdateDefaultPreventedOnContent(
|
||||
dom::EventTarget* aTarget) {
|
||||
MOZ_ASSERT(DefaultPrevented());
|
||||
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 (Element* rootElement = GetExposedRoot()) {
|
||||
if (rootElement->OwnerDoc()->GetUnretargetedFocusedContent() !=
|
||||
rootElement) {
|
||||
selectionController->SelectionWillLoseFocus();
|
||||
} else {
|
||||
// We leave this selection as the focused one. When the focus returns, it
|
||||
@ -5916,6 +5917,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,7 +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/MouseEvent.h" // for MouseEvent
|
||||
#include "mozilla/dom/HTMLTextAreaElement.h"
|
||||
#include "mozilla/dom/MouseEvent.h" // for MouseEvent
|
||||
#include "mozilla/dom/Selection.h"
|
||||
|
||||
#include "nsAString.h"
|
||||
@ -154,20 +155,21 @@ nsresult EditorEventListener::InstallToEditor() {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// For non-html editor, ie.TextEditor, we want to preserve
|
||||
// the event handling order to ensure listeners that are
|
||||
// added to <input> and <texarea> still working as expected.
|
||||
EventListenerFlags flags = mEditorBase->IsHTMLEditor()
|
||||
? TrustedEventsAtSystemGroupCapture()
|
||||
: TrustedEventsAtSystemGroupBubble();
|
||||
#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->AddEventListenerByType(this, u"keydown"_ns, flags);
|
||||
eventListenerManager->AddEventListenerByType(this, u"keyup"_ns, flags);
|
||||
#endif
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->AddEventListenerByType(
|
||||
this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
|
||||
eventListenerManager->AddEventListenerByType(this, u"keypress"_ns, flags);
|
||||
eventListenerManager->AddEventListenerByType(this, u"dragover"_ns, flags);
|
||||
eventListenerManager->AddEventListenerByType(this, u"dragleave"_ns, flags);
|
||||
eventListenerManager->AddEventListenerByType(this, u"drop"_ns, flags);
|
||||
// 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.
|
||||
@ -236,20 +238,17 @@ void EditorEventListener::UninstallFromEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
EventListenerFlags flags = mEditorBase->IsHTMLEditor()
|
||||
? TrustedEventsAtSystemGroupCapture()
|
||||
: TrustedEventsAtSystemGroupBubble();
|
||||
#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"keydown"_ns, flags);
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"keyup"_ns, flags);
|
||||
#endif
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"dragover"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"dragleave"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(
|
||||
this, u"drop"_ns, TrustedEventsAtSystemGroupBubble());
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"keypress"_ns, flags);
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"dragover"_ns, flags);
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"dragleave"_ns, flags);
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"drop"_ns, flags);
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"mousedown"_ns,
|
||||
TrustedEventsAtCapture());
|
||||
eventListenerManager->RemoveEventListenerByType(this, u"mouseup"_ns,
|
||||
@ -315,10 +314,36 @@ 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();
|
||||
|
||||
if (DetachedFromEditor()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
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(
|
||||
internalEvent->GetDOMEventTarget())) {
|
||||
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 +878,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 +892,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
|
||||
|
@ -54,6 +54,8 @@
|
||||
#include "mozilla/dom/HTMLAnchorElement.h"
|
||||
#include "mozilla/dom/HTMLBodyElement.h"
|
||||
#include "mozilla/dom/HTMLBRElement.h"
|
||||
#include "mozilla/dom/HTMLButtonElement.h"
|
||||
#include "mozilla/dom/HTMLSummaryElement.h"
|
||||
#include "mozilla/dom/NameSpaceConstants.h"
|
||||
#include "mozilla/dom/Selection.h"
|
||||
|
||||
@ -6890,7 +6892,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 {
|
||||
@ -7049,6 +7063,20 @@ bool HTMLEditor::IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent) const {
|
||||
}
|
||||
}
|
||||
|
||||
// Space event for <button> and <summary> with contenteditable
|
||||
// should be handle by the themselves.
|
||||
if (aGUIEvent->mMessage == eKeyPress &&
|
||||
aGUIEvent->AsKeyboardEvent()->ShouldWorkAsSpaceKey()) {
|
||||
nsGenericHTMLElement* element =
|
||||
HTMLButtonElement::FromNode(eventTargetNode);
|
||||
if (!element) {
|
||||
element = HTMLSummaryElement::FromNode(eventTargetNode);
|
||||
}
|
||||
|
||||
if (element && element->IsContentEditable()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// This HTML editor is for contenteditable. We need to check the validity
|
||||
// of the target.
|
||||
if (NS_WARN_IF(!eventTargetNode->IsContent())) {
|
||||
|
@ -271,6 +271,7 @@ skip-if = os == 'android'
|
||||
[test_label_contenteditable.html]
|
||||
[test_middle_click_paste.html]
|
||||
skip-if = headless
|
||||
[test_nested_editor.html]
|
||||
[test_nsIEditorMailSupport_insertAsCitedQuotation.html]
|
||||
[test_nsIEditorMailSupport_insertTextWithQuotations.html]
|
||||
skip-if = xorigin # Testing internal API for comm-central
|
||||
|
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() {
|
||||
return new Promise(r => {
|
||||
let focusInParent = document.getElementById("focusme");
|
||||
if (focusInParent) {
|
||||
r(focusInParent);
|
||||
return;
|
||||
}
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user