Bug 1900426 - Implement DnD for shadow-crossing selection r=jjaschke,smaug,dom-core

This patches allows
  1. when the selection is shadow-crossing, dragging it will have the
  correct the drag image correctly displayed. It's hard to test, so
  no tests for this.

  2. The selection can now be serialized and dropped.
  `test_drag_drop_shadow_crossing_selection.html` is the test for
  this.

Differential Revision: https://phabricator.services.mozilla.com/D217318
This commit is contained in:
Sean Feng 2024-08-26 19:09:03 +00:00
parent 4956c93b73
commit ba8d59f743
12 changed files with 226 additions and 39 deletions

View File

@ -830,11 +830,12 @@ nsresult DragDataProducer::GetDraggableSelectionData(
*outImageOrLinkNode = nullptr;
*outDragSelectedText = false;
if (!inSelection->IsCollapsed()) {
if (!inSelection->AreNormalAndCrossShadowBoundaryRangesCollapsed()) {
if (inSelection->ContainsNode(*inRealTargetNode, false, IgnoreErrors())) {
// track down the anchor node, if any, for the url
nsINode* selectionStart = inSelection->GetAnchorNode();
nsINode* selectionEnd = inSelection->GetFocusNode();
nsINode* selectionStart =
inSelection->GetMayCrossShadowBoundaryAnchorNode();
nsINode* selectionEnd = inSelection->GetMayCrossShadowBoundaryFocusNode();
// look for a selection around a single node, like an image.
// in this case, drag the image, rather than a serialization of the HTML

View File

@ -3605,8 +3605,6 @@ class nsContentUtils {
aCallback);
static nsINode* GetCommonAncestorHelper(nsINode* aNode1, nsINode* aNode2);
static nsINode* GetCommonShadowIncludingAncestorHelper(nsINode* aNode1,
nsINode* aNode2);
static nsIContent* GetCommonFlattenedTreeAncestorHelper(
nsIContent* aContent1, nsIContent* aContent2);

View File

@ -404,7 +404,12 @@ nsresult nsCopySupport::GetTransferableForSelection(
NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
NS_ENSURE_TRUE(aTransferable, NS_ERROR_NULL_POINTER);
const uint32_t additionalFlags = nsIDocumentEncoder::SkipInvisibleContent;
const uint32_t additionalFlags =
StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()
? nsIDocumentEncoder::SkipInvisibleContent |
nsIDocumentEncoder::AllowCrossShadowBoundary
: nsIDocumentEncoder::SkipInvisibleContent;
return EncodeDocumentWithContextAndCreateTransferable(
*aDoc, aSel, additionalFlags, aTransferable);
}

View File

@ -3023,6 +3023,19 @@ already_AddRefed<DOMRect> nsRange::GetBoundingClientRect(bool aClampToEdge,
already_AddRefed<DOMRectList> nsRange::GetClientRects(bool aClampToEdge,
bool aFlushLayout) {
return GetClientRectsInner(AllowRangeCrossShadowBoundary::No, aClampToEdge,
aFlushLayout);
}
already_AddRefed<DOMRectList> nsRange::GetAllowCrossShadowBoundaryClientRects(
bool aClampToEdge, bool aFlushLayout) {
return GetClientRectsInner(AllowRangeCrossShadowBoundary::Yes, aClampToEdge,
aFlushLayout);
}
already_AddRefed<DOMRectList> nsRange::GetClientRectsInner(
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundaryRange,
bool aClampToEdge, bool aFlushLayout) {
if (!mIsPositioned) {
return nullptr;
}
@ -3031,11 +3044,20 @@ already_AddRefed<DOMRectList> nsRange::GetClientRects(bool aClampToEdge,
nsLayoutUtils::RectListBuilder builder(rectList);
const auto& startRef =
aAllowCrossShadowBoundaryRange == AllowRangeCrossShadowBoundary::Yes
? MayCrossShadowBoundaryStartRef()
: mStart;
const auto& endRef =
aAllowCrossShadowBoundaryRange == AllowRangeCrossShadowBoundary::Yes
? MayCrossShadowBoundaryEndRef()
: mEnd;
CollectClientRectsAndText(
&builder, nullptr, this, mStart.Container(),
*mStart.Offset(RangeBoundary::OffsetFilter::kValidOffsets),
mEnd.Container(),
*mEnd.Offset(RangeBoundary::OffsetFilter::kValidOffsets), aClampToEdge,
&builder, nullptr, this, startRef.Container(),
*startRef.Offset(RangeBoundary::OffsetFilter::kValidOffsets),
endRef.Container(),
*endRef.Offset(RangeBoundary::OffsetFilter::kValidOffsets), aClampToEdge,
aFlushLayout);
return rectList.forget();
}

View File

@ -264,6 +264,10 @@ class nsRange final : public mozilla::dom::AbstractRange,
bool aFlushLayout = true);
already_AddRefed<DOMRectList> GetClientRects(bool aClampToEdge = true,
bool aFlushLayout = true);
// ChromeOnly
already_AddRefed<DOMRectList> GetAllowCrossShadowBoundaryClientRects(
bool aClampToEdge = true, bool aFlushLayout = true);
void GetClientRectsAndTexts(mozilla::dom::ClientRectsAndTexts& aResult,
ErrorResult& aErr);
@ -368,6 +372,10 @@ class nsRange final : public mozilla::dom::AbstractRange,
*/
bool IsPartOfOneSelectionOnly() const { return mSelections.Length() == 1; };
already_AddRefed<DOMRectList> GetClientRectsInner(
AllowRangeCrossShadowBoundary = AllowRangeCrossShadowBoundary::No,
bool aClampToEdge = true, bool aFlushLayout = true);
public:
/**
* This helper function gets rects and correlated text for the given range.

View File

@ -1243,6 +1243,8 @@ skip-if = [
["test_domwindowutils.html"]
skip-if = ["os == 'android'"] # Bug 1525959
["test_drag_drop_shadow_crossing_selection.html"]
["test_element.matches.html"]
["test_elementTraversal.html"]

View File

@ -0,0 +1,119 @@
<!doctype html>
<title>Test dnd for shadow-crossing selection</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
<style>
</style>
<div>
<span id="outer1">Outer1</span>
<div id="host">
<template shadowrootmode="open">
<span>Inner1</span>
<span>Inner2</span>
<span id="inner3">Inner3</span>
</template>
</div>
<span id="outer2">Outer2</span>
<div id="host2">
<template shadowrootmode="open">
<span id="inner4">Inner4</span>
</template>
</div>
</div>
<input id="dropZone" />
<script>
const selection = window.getSelection();
async function waitForEvent(event) {
return new Promise(r => {
addEventListener(event, function(e) {
r(e.target);
}, { once : true});
});
}
async function waitForDropEvent() {
return new Promise(r => {
addEventListener("drop", function(e) {
r(event.dataTransfer.getData('text/html'));
}, { once : true});
});
}
async function run(startNode, startOffset, endNode, endOffset, expectedValue, expectedTarget, expectedHTML, assertionMessage) {
selection.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
const waitForDragStart = waitForEvent("dragstart");
const waitForDragEnd = waitForEvent("dragend");
const waitForDrop = waitForDropEvent();
await synthesizePlainDragAndDrop({
srcSelection: selection,
destElement: dropZone
});
const dragStartTarget = await waitForDragStart;
const dragEndTarget = await waitForDragEnd;
const htmlData = await waitForDrop;
is(dropZone.value, expectedValue, assertionMessage);
is(dragStartTarget, dragEndTarget, "dragstart and dragend should have the same target");
is(dragStartTarget, expectedTarget, "dragstart target should be the same as expectedTarget");
is(htmlData, expectedHTML);
selection.empty();
dropZone.value = '';
}
add_task(async function runTests() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.shadowdom.selection_across_boundary.enabled", true],
["ui.dragThresholdX", 4], // bug 1873142
["ui.dragThresholdY", 4], // bug 1873142
],
});
// synthesizePlainDragAndDrop would use the focused node to initiate DnD, so
// the expectedTarget is provided based this.
// light to shadow
let sel = [outer1.firstChild, 2, host.shadowRoot.getElementById("inner3").firstChild, 5];
await run(
...sel,
"ter1 Inner1 Inner2 Inner",
host, // expectedTarget - focused node is inside the shadow dom, hence the host is the target to preserve encapsulation.
"<span id=\"outer1\">ter1</span>\n <div id=\"host\">\n <span>Inner1</span>\n <span>Inner2</span>\n <span id=\"inner3\">Inner</span></div>",
"start is in light DOM and end is in shadow DOM");
// light to light
sel = [outer1.firstChild, 2, outer2.firstChild, 6];
await run(
...sel,
"ter1 Inner1 Inner2 Inner3 Outer2",
outer2.firstChild, // expectedTarget - focused node is outer2.firstChild
"<span id=\"outer1\">ter1</span>\n <div id=\"host\">\n <span>Inner1</span>\n <span>Inner2</span>\n <span id=\"inner3\">Inner3</span>\n </div>\n <span id=\"outer2\">Outer2</span>",
"start is in light DOM and end is in light DOM"
);
// shadow to light
sel = [host.shadowRoot.getElementById("inner3").firstChild, 2, outer2.firstChild, 6];
await run(
...sel,
"ner3 Outer2",
outer2.firstChild, // expectedTarget - focused node is outer2.firstChild
"<div id=\"host\"><span id=\"inner3\">ner3</span>\n </div>\n <span id=\"outer2\">Outer2</span>",
"start is in shadow DOM and end is in light DOM"
);
// shadow to shadow
sel = [host.shadowRoot.getElementById("inner3").firstChild, 2, host2.shadowRoot.getElementById("inner4").firstChild, 6];
await run(
...sel,
"ner3 Outer2 Inner4 ",
host2, // expectedTarget - focused node is inside the shadow dom, hence the host is the target to preserve encapsulation.
"<div id=\"host\"><span id=\"inner3\">ner3</span>\n </div>\n <span id=\"outer2\">Outer2</span>\n <div id=\"host2\">\n <span id=\"inner4\">Inner4</span>\n </div>",
"start is in shadow DOM and end is in shadow DOM"
);
});
</script>

View File

@ -1588,9 +1588,8 @@ class nsHTMLCopyEncoder : public nsDocumentEncoder {
nsINode* aCommon);
static nsCOMPtr<nsINode> GetChildAt(nsINode* aParent, int32_t aOffset);
static bool IsMozBR(Element* aNode);
static nsresult GetNodeLocation(nsINode* inChild,
nsCOMPtr<nsINode>* outParent,
int32_t* outOffset);
nsresult GetNodeLocation(nsINode* inChild, nsCOMPtr<nsINode>* outParent,
int32_t* outOffset);
bool IsRoot(nsINode* aNode);
static bool IsFirstNode(nsINode* aNode);
static bool IsLastNode(nsINode* aNode);
@ -2044,7 +2043,15 @@ nsresult nsHTMLCopyEncoder::GetPromotedPoint(Endpoint aWhere, nsINode* aNode,
node = parent;
rv = GetNodeLocation(node, address_of(parent), &offset);
NS_ENSURE_SUCCESS(rv, rv);
if (offset == -1) // we hit generated content; STOP
// When node is the shadow root and parent is the shadow host,
// the offset would also be -1, and we'd like to keep going.
const bool isGeneratedContent =
offset == -1 &&
ShadowDOMSelectionHelpers::GetShadowRoot(
parent,
mFlags & nsIDocumentEncoder::AllowCrossShadowBoundary) != node;
if (isGeneratedContent) // we hit generated content; STOP
{
// back up a bit
parent = node;
@ -2096,7 +2103,9 @@ nsresult nsHTMLCopyEncoder::GetNodeLocation(nsINode* inChild,
return NS_ERROR_NULL_POINTER;
}
nsIContent* parent = child->GetParent();
nsINode* parent = mFlags & nsIDocumentEncoder::AllowCrossShadowBoundary
? child->GetParentOrShadowHostNode()
: child->GetParent();
if (!parent) {
return NS_ERROR_NULL_POINTER;
}

View File

@ -82,6 +82,7 @@ partial interface Range {
// http://dvcs.w3.org/hg/csswg/raw-file/tip/cssom-view/Overview.html#extensions-to-the-range-interface
partial interface Range {
DOMRectList? getClientRects();
[ChromeOnly] DOMRectList? getAllowCrossShadowBoundaryClientRects();
DOMRect getBoundingClientRect();
};

View File

@ -23,6 +23,10 @@ interface Selection {
readonly attribute boolean isCollapsed;
[ChromeOnly]
readonly attribute boolean areNormalAndCrossShadowBoundaryRangesCollapsed;
[ChromeOnly]
readonly attribute Node? mayCrossShadowBoundaryFocusNode;
/**
* Returns the number of ranges in the selection.
*/

View File

@ -231,7 +231,6 @@ PresShell::CapturingContentInfo PresShell::sCapturingContentInfo;
// RangePaintInfo is used to paint ranges to offscreen buffers
struct RangePaintInfo {
RefPtr<nsRange> mRange;
nsDisplayListBuilder mBuilder;
nsDisplayList mList;
@ -243,9 +242,8 @@ struct RangePaintInfo {
// to paint them at this resolution.
float mResolution = 1.0;
RangePaintInfo(nsRange* aRange, nsIFrame* aFrame)
: mRange(aRange),
mBuilder(aFrame, nsDisplayListBuilderMode::Painting, false),
explicit RangePaintInfo(nsIFrame* aFrame)
: mBuilder(aFrame, nsDisplayListBuilderMode::Painting, false),
mList(&mBuilder) {
MOZ_COUNT_CTOR(RangePaintInfo);
mBuilder.BeginFrame();
@ -4777,24 +4775,27 @@ nsRect PresShell::ClipListToRange(nsDisplayListBuilder* aBuilder,
nsIFrame* frame = i->Frame();
nsIContent* content = frame->GetContent();
if (content) {
bool atStart = (content == aRange->GetStartContainer());
bool atEnd = (content == aRange->GetEndContainer());
bool atStart =
content == aRange->GetMayCrossShadowBoundaryStartContainer();
bool atEnd = content == aRange->GetMayCrossShadowBoundaryEndContainer();
if ((atStart || atEnd) && frame->IsTextFrame()) {
auto [frameStartOffset, frameEndOffset] = frame->GetOffsets();
int32_t hilightStart =
atStart ? std::max(static_cast<int32_t>(aRange->StartOffset()),
int32_t highlightStart =
atStart ? std::max(static_cast<int32_t>(
aRange->MayCrossShadowBoundaryStartOffset()),
frameStartOffset)
: frameStartOffset;
int32_t hilightEnd =
atEnd ? std::min(static_cast<int32_t>(aRange->EndOffset()),
int32_t highlightEnd =
atEnd ? std::min(static_cast<int32_t>(
aRange->MayCrossShadowBoundaryEndOffset()),
frameEndOffset)
: frameEndOffset;
if (hilightStart < hilightEnd) {
if (highlightStart < highlightEnd) {
// determine the location of the start and end edges of the range.
nsPoint startPoint, endPoint;
frame->GetPointFromOffset(hilightStart, &startPoint);
frame->GetPointFromOffset(hilightEnd, &endPoint);
frame->GetPointFromOffset(highlightStart, &startPoint);
frame->GetPointFromOffset(highlightEnd, &endPoint);
// The clip rectangle is determined by taking the the start and
// end points of the range, offset from the reference frame.
@ -4828,8 +4829,9 @@ nsRect PresShell::ClipListToRange(nsDisplayListBuilder* aBuilder,
// Don't try to descend into subdocuments.
// If this ever changes we'd need to add handling for subdocuments with
// different zoom levels.
else if (content->GetUncomposedDoc() ==
aRange->GetStartContainer()->GetUncomposedDoc()) {
else if (content->GetComposedDoc() ==
aRange->GetMayCrossShadowBoundaryStartContainer()
->GetComposedDoc()) {
// if the node is within the range, append it to the temporary list
bool before, after;
nsresult rv =
@ -4874,14 +4876,18 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
// If the start or end of the range is the document, just use the root
// frame, otherwise get the common ancestor of the two endpoints of the
// range.
nsINode* startContainer = aRange->GetStartContainer();
nsINode* endContainer = aRange->GetEndContainer();
nsINode* startContainer = aRange->GetMayCrossShadowBoundaryStartContainer();
nsINode* endContainer = aRange->GetMayCrossShadowBoundaryEndContainer();
Document* doc = startContainer->GetComposedDoc();
if (startContainer == doc || endContainer == doc) {
ancestorFrame = rootFrame;
} else {
nsINode* ancestor = nsContentUtils::GetClosestCommonInclusiveAncestor(
startContainer, endContainer);
nsINode* ancestor =
StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()
? nsContentUtils::GetClosestCommonShadowIncludingInclusiveAncestor(
startContainer, endContainer)
: nsContentUtils::GetClosestCommonInclusiveAncestor(startContainer,
endContainer);
NS_ASSERTION(!ancestor || ancestor->IsContent(),
"common ancestor is not content");
@ -4906,7 +4912,7 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
}
// get a display list containing the range
auto info = MakeUnique<RangePaintInfo>(aRange, ancestorFrame);
auto info = MakeUnique<RangePaintInfo>(ancestorFrame);
info->mBuilder.SetIncludeAllOutOfFlows();
if (aForPrimarySelection) {
info->mBuilder.SetSelectedFramesOnly();
@ -4914,7 +4920,9 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
info->mBuilder.EnterPresShell(ancestorFrame);
ContentSubtreeIterator subtreeIter;
nsresult rv = subtreeIter.Init(aRange);
nsresult rv = StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()
? subtreeIter.InitWithAllowCrossShadowBoundary(aRange)
: subtreeIter.Init(aRange);
if (NS_FAILED(rv)) {
return nullptr;
}
@ -4937,7 +4945,13 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
}
for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
nsCOMPtr<nsINode> node = subtreeIter.GetCurrentNode();
BuildDisplayListForNode(node);
if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
for (nsINode* node : ShadowIncludingTreeIterator(*node)) {
BuildDisplayListForNode(node);
}
} else {
BuildDisplayListForNode(node);
}
}
if (endContainer != startContainer &&
endContainer->NodeType() == nsINode::TEXT_NODE) {

View File

@ -3386,7 +3386,9 @@ function _nodeIsFlattenedTreeDescendantOf(
}
function _computeSrcElementFromSrcSelection(aSrcSelection) {
let srcElement = aSrcSelection.focusNode;
let srcElement = _EU_maybeUnwrap(
_EU_maybeWrap(aSrcSelection).mayCrossShadowBoundaryFocusNode
);
while (_EU_maybeWrap(srcElement).isNativeAnonymous) {
srcElement = _getFlattenedTreeParentNode(srcElement);
}
@ -3488,7 +3490,9 @@ async function synthesizePlainDragAndDrop(aParams) {
}
// Use last selection client rect because nsIDragSession.sourceNode is
// initialized from focus node which is usually in last rect.
let selectionRectList = srcSelection.getRangeAt(0).getClientRects();
let selectionRectList = SpecialPowers.wrap(
srcSelection.getRangeAt(0)
).getAllowCrossShadowBoundaryClientRects();
let lastSelectionRect = selectionRectList[selectionRectList.length - 1];
if (logFunc) {
logFunc(