зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
e577d75b11
Коммит
a171e12db6
|
@ -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();
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче