Bug 1665550 - part 3: Make `nsTextControlFrame` set the source node and selection of drag session to new ones when it's reframed r=smaug

When `nsTextControlFrame` is reframed, `TextEditor`, anonymous `<div>`, its
`Text` and the independent `Selection`s are deleted temporarily and recreated
them.

If users are dragging text in `<input>` or `<textarea>`, the drag session's
source node is set to the anonymous text node in the element and the selection
is set to the independent selection.  So, if the element is reframed during a
drag, the source node is disconnected from the document and `EndDragSession`
failed to dispatch `eDragEnd` event.

Therefore, this patch makes `nsTextControlFrame` replaces the source node and
selection when it's recreated and only when the drag session's original source
node was in the text control element.  For checking which text control had the
anonymous text node, this patch makes `nsTextControlFrame` replaces source
node with the `<input>` or `<textarea>` element when the frame is destroyed.

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

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

@ -3091,6 +3091,98 @@ async function doTest() {
document.removeEventListener("drop", onDrop);
})();
// -------- Test dragging text from an <input> and reframing the <input> element before dragend.
await (async function test_dragging_from_input_element_and_reframing_input_element() {
const description = "dragging part of text in <input> element and reframing the <input> element before dragend";
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 onDragStart = aEvent => {
input.style.display = "none";
document.documentElement.scrollTop;
input.style.display = "";
document.documentElement.scrollTop;
};
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("dragStart", onDragStart);
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("dragStart", onDragStart);
document.removeEventListener("drop", onDrop);
})();
// -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragend.
await (async function test_dragging_from_textarea_element_and_reframing_textarea_element() {
const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragend";
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 onDragStart = aEvent => {
textarea.style.display = "none";
document.documentElement.scrollTop;
textarea.style.display = "";
document.documentElement.scrollTop;
};
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("dragStart", onDragStart);
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("dragStart", onDragStart);
document.removeEventListener("drop", onDrop);
})();
document.removeEventListener("beforeinput", onBeforeinput);
document.removeEventListener("input", onInput);
SimpleTest.finish();

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

@ -34,6 +34,7 @@
#include "nsFocusManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresState.h"
#include "mozilla/TextEditor.h"
#include "nsAttrValueInlines.h"
#include "mozilla/dom/Selection.h"
#include "nsContentUtils.h"
@ -149,6 +150,23 @@ void nsTextControlFrame::DestroyFrom(nsIFrame* aDestructRoot,
mMutationObserver = nullptr;
}
// If there is a drag session, user may be dragging selection in removing
// text node in the text control. If so, we should set source node to the
// text control because another text node may be recreated soon if the text
// control is just reframed.
if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
if (dragSession->IsDraggingTextInTextControl() && mRootNode &&
mRootNode->GetFirstChild()) {
nsCOMPtr<nsINode> sourceNode;
if (NS_SUCCEEDED(
dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
mRootNode->Contains(sourceNode)) {
MOZ_ASSERT(sourceNode->IsText());
dragSession->UpdateSource(textControlElement, nullptr);
}
}
}
// If we're a subclass like nsNumberControlFrame, then it owns the root of the
// anonymous subtree where mRootNode is.
aPostDestroyData.AddAnonymousContent(mRootNode.forget());
@ -447,6 +465,20 @@ bool nsTextControlFrame::ShouldInitializeEagerly() const {
}
}
// If text in the editor is being dragged, we need the editor to create
// new source node for the drag session (TextEditor creates the text node
// in the anonymous <div> element.
if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
if (dragSession->IsDraggingTextInTextControl()) {
nsCOMPtr<nsINode> sourceNode;
if (NS_SUCCEEDED(
dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
sourceNode == textControlElement) {
return true;
}
}
}
return false;
}
@ -1292,6 +1324,33 @@ nsTextControlFrame::EditorInitializer::Run() {
return NS_ERROR_FAILURE;
}
// If there is a drag session which is for dragging text in a text control
// and its source node is the text control element, we're being reframed.
// In this case we should restore the source node of the drag session to
// new text node because it's required for dispatching `dragend` event.
if (nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession()) {
if (dragSession->IsDraggingTextInTextControl()) {
nsCOMPtr<nsINode> sourceNode;
if (NS_SUCCEEDED(
dragSession->GetSourceNode(getter_AddRefs(sourceNode))) &&
mFrame->GetContent() == sourceNode) {
if (TextControlElement* textControlElement =
TextControlElement::FromNode(mFrame->GetContent())) {
if (TextEditor* textEditor =
textControlElement->GetTextEditorWithoutCreation()) {
if (Element* anonymousDivElement = textEditor->GetRoot()) {
if (anonymousDivElement && anonymousDivElement->GetFirstChild()) {
MOZ_ASSERT(anonymousDivElement->GetFirstChild()->IsText());
dragSession->UpdateSource(anonymousDivElement->GetFirstChild(),
textEditor->GetSelection());
}
}
}
}
}
}
}
mFrame->FinishedInitializer();
return NS_OK;
}

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

@ -3002,6 +3002,9 @@ function _computeSrcElementFromSrcSelection(aSrcSelection) {
* destWindow: The window for dispatching event on destElement,
* defaults to the current window object
* expectCancelDragStart: Set to true if the test cancels "dragstart"
* expectSrcElementDisconnected:
* Set to true if srcElement will be disconnected and
* "dragend" event won't be fired.
* logFunc: Set function which takes one argument if you need
* to log rect of target. E.g., `console.log`.
* }
@ -3023,6 +3026,7 @@ async function synthesizePlainDragAndDrop(aParams) {
srcWindow = window,
destWindow = window,
expectCancelDragStart = false,
expectSrcElementDisconnected = false,
logFunc,
} = aParams;
// Don't modify given dragEvent object because we modify dragEvent below and
@ -3341,6 +3345,7 @@ async function synthesizePlainDragAndDrop(aParams) {
await new Promise(r => setTimeout(r, 0));
if (ds.getCurrentSession()) {
const sourceNode = ds.sourceNode;
let dragEndEvent;
function onDragEnd(aEvent) {
dragEndEvent = aEvent;
@ -3356,14 +3361,25 @@ async function synthesizePlainDragAndDrop(aParams) {
'event target of "dragend" is not srcElement nor its descendant'
);
}
if (expectSrcElementDisconnected) {
throw new Error(
`"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${
sourceNode?.isConnected ? "connected" : "null or disconnected"
})`
);
}
}
srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
try {
ds.endDragSession(true, _parseModifiers(dragEvent));
if (!dragEndEvent) {
if (!expectSrcElementDisconnected && !dragEndEvent) {
// eslint-disable-next-line no-unsafe-finally
throw new Error(
'"dragend" event is not fired by nsIDragService.endDragSession()'
`"dragend" event is not fired by nsIDragService.endDragSession()${
ds.sourceNode && !ds.sourceNode.isConnected
? "(sourceNode was disconnected)"
: ""
}`
);
}
} finally {

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

@ -34,6 +34,7 @@
#include "mozilla/PresShell.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/SVGImageContext.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/Unused.h"
#include "mozilla/ViewportUtils.h"
#include "mozilla/dom/BindingDeclarations.h"
@ -64,6 +65,7 @@ nsBaseDragService::nsBaseDragService()
mOnlyChromeDrop(false),
mDoingDrag(false),
mSessionIsSynthesizedForTests(false),
mIsDraggingTextInTextControl(false),
mEndingSession(false),
mHasImage(false),
mUserCancelled(false),
@ -154,6 +156,26 @@ nsBaseDragService::GetSourceNode(nsINode** aSourceNode) {
return NS_OK;
}
void nsBaseDragService::UpdateSource(nsINode* aNewSourceNode,
Selection* aNewSelection) {
MOZ_ASSERT(mSourceNode);
MOZ_ASSERT(aNewSourceNode);
MOZ_ASSERT(mSourceNode->IsInNativeAnonymousSubtree() ||
aNewSourceNode->IsInNativeAnonymousSubtree());
MOZ_ASSERT(mSourceDocument == aNewSourceNode->OwnerDoc());
mSourceNode = aNewSourceNode;
// Don't set mSelection if the session was invoked without selection or
// making it becomes nullptr. The latter occurs when the old frame is
// being destroyed.
if (mSelection && aNewSelection) {
// XXX If the dragging image is created once (e.g., at drag start), the
// image won't be updated unless we notify `DrawDrag` callers.
// However, it must be okay for now to keep using older image of
// Selection.
mSelection = aNewSelection;
}
}
NS_IMETHODIMP
nsBaseDragService::GetTriggeringPrincipal(nsIPrincipal** aPrincipal) {
NS_IF_ADDREF(*aPrincipal = mTriggeringPrincipal);
@ -217,6 +239,10 @@ bool nsBaseDragService::IsSynthesizedForTests() {
return mSessionIsSynthesizedForTests;
}
bool nsBaseDragService::IsDraggingTextInTextControl() {
return mIsDraggingTextInTextControl;
}
uint32_t nsBaseDragService::GetEffectAllowedForTests() {
MOZ_ASSERT(mSessionIsSynthesizedForTests);
return mEffectAllowedForTests;
@ -257,6 +283,10 @@ nsBaseDragService::InvokeDragSession(
mTriggeringPrincipal = aPrincipal;
mCsp = aCsp;
mSourceNode = aDOMNode;
mIsDraggingTextInTextControl =
mSourceNode->IsInNativeAnonymousSubtree() &&
TextControlElement::FromNodeOrNull(
mSourceNode->GetClosestNativeAnonymousSubtreeRootParent());
mContentPolicyType = aContentPolicyType;
mEndDragPoint = LayoutDeviceIntPoint(0, 0);
@ -547,6 +577,7 @@ nsBaseDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
mDoingDrag = false;
mSessionIsSynthesizedForTests = false;
mIsDraggingTextInTextControl = false;
mEffectAllowedForTests = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED;
mEndingSession = false;
mCanDrop = false;

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

@ -149,6 +149,7 @@ class nsBaseDragService : public nsIDragService, public nsIDragSession {
bool mOnlyChromeDrop;
bool mDoingDrag;
bool mSessionIsSynthesizedForTests;
bool mIsDraggingTextInTextControl;
// true if in EndDragSession
bool mEndingSession;

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

@ -19,6 +19,7 @@ native nsSize(nsSize);
webidl DataTransfer;
webidl Document;
webidl Node;
webidl Selection;
[scriptable, builtinclass, uuid(25bce737-73f0-43c7-bc20-c71044a73c5a)]
interface nsIDragSession : nsISupports
@ -58,6 +59,17 @@ interface nsIDragSession : nsISupports
*/
readonly attribute Node sourceNode;
/**
* Replace source node and selection with new ones.
* If sourceNode is a native anonymous node, it may be replaced at reframing.
* If sourceNode is disconnected from the document, we cannot dispatch
* `dragend` event properly.
* When this is called, sourceNode or aNewSourceNode should be a native
* anonymous node.
*/
[notxpcom, nostdcall] void updateSource(in Node aNewSourceNode,
in Selection aNewSelection);
/**
* the triggering principal. This may be different than sourceNode's
* principal when sourceNode is xul:browser and the drag is
@ -122,4 +134,10 @@ interface nsIDragSession : nsISupports
* "drop" event.
*/
void setDragEndPointForTests(in long aScreenX, in long aScreenY);
/**
* Returns true if the session is for dragging text in a text in text control
* element.
*/
[notxpcom, nostdcall] bool isDraggingTextInTextControl();
};