Bug 1665550 - part 4: Make `EventStateManager` update `mGestureDownFrameOwner` when anonymous nodes in `<input>` or `<textarea>` are replaced r=smaug

`EventStateManager` gives up to track gesture to start a drag if mouse down
content which is stored in `mGestureDownFrameOwner` gets lost its primary frame.

When user tries to start to drag selected text in `<input>` or `<textarea>`
element, mouse down content is an anonymous node in `TextControlElement`. So,
if a reflow occurs after `mousedown` event, the anonymous `<div>` element
is replaced with new one and `EventStateManager` gives up to track it.

Therefore, this patch makes `EventStateManager` do similar things as
`nsBaseDragService`.  When `nsTextControlFrame` notifies of remove/add
the anonymous nodes, `EventStateManager` tries to keep tracking gesture with
a new anonymous node.

Differential Revision: https://phabricator.services.mozilla.com/D119488
This commit is contained in:
Masayuki Nakano 2021-07-14 01:20:20 +00:00
Родитель a171e12db6
Коммит 04b41051dd
5 изменённых файлов: 219 добавлений и 11 удалений

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

@ -4,12 +4,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "EventStateManager.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/Attributes.h"
#include "mozilla/EditorBase.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventForwards.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/EventStates.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IMEStateManager.h"
@ -21,6 +22,7 @@
#include "mozilla/ScopeExit.h"
#include "mozilla/ScrollTypes.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TouchEvents.h"
@ -254,6 +256,7 @@ EventStateManager::EventStateManager()
mRClickCount(0),
mShouldAlwaysUseLineDeltas(false),
mShouldAlwaysUseLineDeltasInitialized(false),
mGestureDownInTextControl(false),
mInTouchDrag(false),
m_haveShutdown(false) {
if (sESMInstanceCount == 0) {
@ -1880,6 +1883,10 @@ void EventStateManager::BeginTrackingRemoteDragGesture(
nsIContent* aContent, RemoteDragStartData* aDragStartData) {
mGestureDownContent = aContent;
mGestureDownFrameOwner = aContent;
mGestureDownInTextControl =
aContent && aContent->IsInNativeAnonymousSubtree() &&
TextControlElement::FromNodeOrNull(
aContent->GetClosestNativeAnonymousSubtreeRootParent());
mGestureDownDragStartData = aDragStartData;
}
@ -1892,6 +1899,7 @@ void EventStateManager::BeginTrackingRemoteDragGesture(
void EventStateManager::StopTrackingDragGesture(bool aClearInChildProcesses) {
mGestureDownContent = nullptr;
mGestureDownFrameOwner = nullptr;
mGestureDownInTextControl = false;
mGestureDownDragStartData = nullptr;
// If a content process starts a drag but the mouse is released before the
@ -5788,6 +5796,38 @@ void EventStateManager::ContentRemoved(Document* aDocument,
}
}
void EventStateManager::TextControlRootWillBeRemoved(
TextControlElement& aTextControlElement) {
if (!mGestureDownInTextControl || !mGestureDownFrameOwner ||
!mGestureDownFrameOwner->IsInNativeAnonymousSubtree()) {
return;
}
// If we track gesture to start drag in aTextControlElement, we should keep
// tracking it with aTextContrlElement itself for now because this may be
// caused by reframing aTextControlElement which may not be intended by the
// user.
if (&aTextControlElement ==
mGestureDownFrameOwner->GetClosestNativeAnonymousSubtreeRootParent()) {
mGestureDownFrameOwner = &aTextControlElement;
}
}
void EventStateManager::TextControlRootAdded(
Element& aAnonymousDivElement, TextControlElement& aTextControlElement) {
if (!mGestureDownInTextControl ||
mGestureDownFrameOwner != &aTextControlElement) {
return;
}
// If we track gesture to start drag in aTextControlElement, but the frame
// owner is the text control element itself, the anonymous nodes in it are
// recreated by a reframe. If so, we should keep tracking it with the
// recreated native anonymous node.
mGestureDownFrameOwner =
aAnonymousDivElement.GetFirstChild()
? aAnonymousDivElement.GetFirstChild()
: static_cast<nsIContent*>(&aAnonymousDivElement);
}
bool EventStateManager::EventStatusOK(WidgetGUIEvent* aEvent) {
return !(aEvent->mMessage == eMouseDown &&
aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary &&

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

@ -41,6 +41,7 @@ class EnterLeaveDispatcher;
class EventStates;
class IMEContentObserver;
class ScrollbarsForWheel;
class TextControlElement;
class WheelTransaction;
namespace dom {
@ -159,6 +160,19 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
void NativeAnonymousContentRemoved(nsIContent* aAnonContent);
void ContentRemoved(dom::Document* aDocument, nsIContent* aContent);
/**
* Called when a native anonymous <div> element which is root element of
* text editor will be removed.
*/
void TextControlRootWillBeRemoved(TextControlElement& aTextControlElement);
/**
* Called when a native anonymous <div> element which is root element of
* text editor is created.
*/
void TextControlRootAdded(dom::Element& aAnonymousDivElement,
TextControlElement& aTextControlElement);
bool EventStatusOK(WidgetGUIEvent* aEvent);
/**
@ -1177,6 +1191,8 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
bool mShouldAlwaysUseLineDeltas : 1;
bool mShouldAlwaysUseLineDeltasInitialized : 1;
bool mGestureDownInTextControl : 1;
bool mInTouchDrag;
bool m_haveShutdown;

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

@ -3183,6 +3183,106 @@ async function doTest() {
document.removeEventListener("drop", onDrop);
})();
// -------- Test dragging text from an <input> and reframing the <input> element before dragstart.
await (async function test_dragging_from_input_element_and_reframing_input_element_before_dragstart() {
const description = "dragging part of text in <input> element and reframing the <input> element before dragstart";
container.innerHTML = '<input value="Drag Me">';
const input = document.querySelector("div#container > input");
document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
input.setSelectionRange(1, 4);
beforeinputEvents = [];
inputEvents = [];
dragEvents = [];
const onMouseMove = aEvent => {
input.style.display = "none";
document.documentElement.scrollTop;
input.style.display = "";
document.documentElement.scrollTop;
};
const onMouseDown = aEvent => {
document.addEventListener("mousemove", onMouseMove, {once: true});
}
const onDrop = aEvent => {
dragEvents.push(aEvent);
comparePlainText(aEvent.dataTransfer.getData("text/plain"),
input.value.substring(1, 4),
`${description}: dataTransfer should have selected text as "text/plain"`);
is(aEvent.dataTransfer.getData("text/html"), "",
`${description}: dataTransfer should not have data as "text/html"`);
};
document.addEventListener("mousedown", onMouseDown, {once: true});
document.addEventListener("drop", onDrop);
if (
await trySynthesizePlainDragAndDrop(
description,
{
srcSelection: SpecialPowers.wrap(input).editor.selection,
destElement: dropZone,
}
)
) {
is(beforeinputEvents.length, 0,
`${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`);
is(inputEvents.length, 0,
`${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`);
is(dragEvents.length, 1,
`${description}: only one "drop" event should be fired`);
}
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("drop", onDrop);
})();
// -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragstart.
await (async function test_dragging_from_textarea_element_and_reframing_textarea_element_before_dragstart() {
const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragstart";
container.innerHTML = "<textarea>Some Text To Drag</textarea>";
const textarea = document.querySelector("div#container > textarea");
document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues.
textarea.setSelectionRange(1, 7);
beforeinputEvents = [];
inputEvents = [];
dragEvents = [];
const onMouseMove = aEvent => {
textarea.style.display = "none";
document.documentElement.scrollTop;
textarea.style.display = "";
document.documentElement.scrollTop;
};
const onMouseDown = aEvent => {
document.addEventListener("mousemove", onMouseMove, {once: true});
}
const onDrop = aEvent => {
dragEvents.push(aEvent);
comparePlainText(aEvent.dataTransfer.getData("text/plain"),
textarea.value.substring(1, 7),
`${description}: dataTransfer should have selected text as "text/plain"`);
is(aEvent.dataTransfer.getData("text/html"), "",
`${description}: dataTransfer should not have data as "text/html"`);
};
document.addEventListener("mousedown", onMouseDown, {once: true});
document.addEventListener("drop", onDrop);
if (
await trySynthesizePlainDragAndDrop(
description,
{
srcSelection: SpecialPowers.wrap(textarea).editor.selection,
destElement: dropZone,
}
)
) {
is(beforeinputEvents.length, 0,
`${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`);
is(inputEvents.length, 0,
`${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`);
is(dragEvents.length, 1,
`${description}: only one "drop" event should be fired`);
}
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("drop", onDrop);
})();
document.removeEventListener("beforeinput", onBeforeinput);
document.removeEventListener("input", onInput);
SimpleTest.finish();

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

@ -32,6 +32,7 @@
#include "nsILayoutHistoryState.h"
#include "nsFocusManager.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresState.h"
#include "mozilla/TextEditor.h"
@ -166,6 +167,13 @@ void nsTextControlFrame::DestroyFrom(nsIFrame* aDestructRoot,
}
}
}
// Otherwise, EventStateManager may track gesture to start drag with native
// anonymous nodes in the text control element.
else if (textControlElement->GetPresContext(Element::eForComposedDoc)) {
textControlElement->GetPresContext(Element::eForComposedDoc)
->EventStateManager()
->TextControlRootWillBeRemoved(*textControlElement);
}
// If we're a subclass like nsNumberControlFrame, then it owns the root of the
// anonymous subtree where mRootNode is.
@ -1350,6 +1358,20 @@ nsTextControlFrame::EditorInitializer::Run() {
}
}
}
// Otherwise, EventStateManager may be tracking gesture to start a drag.
else if (TextControlElement* textControlElement =
TextControlElement::FromNode(mFrame->GetContent())) {
if (nsPresContext* presContext =
textControlElement->GetPresContext(Element::eForComposedDoc)) {
if (TextEditor* textEditor =
textControlElement->GetTextEditorWithoutCreation()) {
if (Element* anonymousDivElement = textEditor->GetRoot()) {
presContext->EventStateManager()->TextControlRootAdded(
*anonymousDivElement, *textControlElement);
}
}
}
}
mFrame->FinishedInitializer();
return NS_OK;

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

@ -3095,6 +3095,28 @@ async function synthesizePlainDragAndDrop(aParams) {
await new Promise(r => setTimeout(r, 0));
let mouseDownEvent;
function onMouseDown(aEvent) {
mouseDownEvent = aEvent;
if (logFunc) {
logFunc(`"${aEvent.type}" event is fired`);
}
if (
!srcElement.contains(
_EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget)
)
) {
// If srcX and srcY does not point in one of rects in srcElement,
// "mousedown" target is not in srcElement. Such case must not
// be expected by this API users so that we should throw an exception
// for making debugging easier.
throw new Error(
'event target of "mousedown" is not srcElement nor its descendant'
);
}
}
try {
srcWindow.addEventListener("mousedown", onMouseDown, { capture: true });
synthesizeMouse(
srcElement,
srcX,
@ -3105,6 +3127,14 @@ async function synthesizePlainDragAndDrop(aParams) {
if (logFunc) {
logFunc(`mousedown at ${srcX}, ${srcY}`);
}
if (!mouseDownEvent) {
throw new Error('"mousedown" event is not fired');
}
} finally {
srcWindow.removeEventListener("mousedown", onMouseDown, {
capture: true,
});
}
let dragStartEvent;
function onDragStart(aEvent) {
@ -3120,7 +3150,7 @@ async function synthesizePlainDragAndDrop(aParams) {
// If srcX and srcY does not point in one of rects in srcElement,
// "dragstart" target is not in srcElement. Such case must not
// be expected by this API users so that we should throw an exception
// for making debug easier.
// for making debugging easier.
throw new Error(
'event target of "dragstart" is not srcElement nor its descendant'
);