Bug 1881095 - Add the copy paste support for ShadowDOM selection r=jjaschke,smaug,dom-core

This patch allows nsDocumentEncoder to serialize contents
in shadow trees when those contents are selected.

Differential Revision: https://phabricator.services.mozilla.com/D211577
This commit is contained in:
Sean Feng 2024-06-10 18:59:50 +00:00
Родитель 470c67617d
Коммит df07581747
10 изменённых файлов: 590 добавлений и 61 удалений

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

@ -2932,9 +2932,10 @@ nsresult nsContentUtils::GetInclusiveAncestors(nsINode* aNode,
}
// static
nsresult nsContentUtils::GetInclusiveAncestorsAndOffsets(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>* aAncestorNodes,
nsTArray<Maybe<uint32_t>>* aAncestorOffsets) {
template <typename GetParentFunc>
nsresult static GetInclusiveAncestorsAndOffsetsHelper(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>& aAncestorNodes,
nsTArray<Maybe<uint32_t>>& aAncestorOffsets, GetParentFunc aGetParentFunc) {
NS_ENSURE_ARG_POINTER(aNode);
if (!aNode->IsContent()) {
@ -2942,33 +2943,52 @@ nsresult nsContentUtils::GetInclusiveAncestorsAndOffsets(
}
nsIContent* content = aNode->AsContent();
if (!aAncestorNodes->IsEmpty()) {
if (!aAncestorNodes.IsEmpty()) {
NS_WARNING("aAncestorNodes is not empty");
aAncestorNodes->Clear();
aAncestorNodes.Clear();
}
if (!aAncestorOffsets->IsEmpty()) {
if (!aAncestorOffsets.IsEmpty()) {
NS_WARNING("aAncestorOffsets is not empty");
aAncestorOffsets->Clear();
aAncestorOffsets.Clear();
}
// insert the node itself
aAncestorNodes->AppendElement(content);
aAncestorOffsets->AppendElement(Some(aOffset));
aAncestorNodes.AppendElement(content);
aAncestorOffsets.AppendElement(Some(aOffset));
// insert all the ancestors
nsIContent* child = content;
nsIContent* parent = child->GetParent();
nsIContent* parent = aGetParentFunc(child);
while (parent) {
aAncestorNodes->AppendElement(parent);
aAncestorOffsets->AppendElement(parent->ComputeIndexOf(child));
aAncestorNodes.AppendElement(parent->AsContent());
aAncestorOffsets.AppendElement(parent->ComputeIndexOf(child));
child = parent;
parent = parent->GetParent();
parent = aGetParentFunc(child);
}
return NS_OK;
}
nsresult nsContentUtils::GetInclusiveAncestorsAndOffsets(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>& aAncestorNodes,
nsTArray<Maybe<uint32_t>>& aAncestorOffsets) {
return GetInclusiveAncestorsAndOffsetsHelper(
aNode, aOffset, aAncestorNodes, aAncestorOffsets,
[](nsIContent* aContent) { return aContent->GetParent(); });
}
nsresult nsContentUtils::GetShadowIncludingAncestorsAndOffsets(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>& aAncestorNodes,
nsTArray<Maybe<uint32_t>>& aAncestorOffsets) {
return GetInclusiveAncestorsAndOffsetsHelper(
aNode, aOffset, aAncestorNodes, aAncestorOffsets,
[](nsIContent* aContent) -> nsIContent* {
return nsIContent::FromNodeOrNull(
aContent->GetParentOrShadowHostNode());
});
}
template <typename Node, typename GetParentFunc>
static Node* GetCommonAncestorInternal(Node* aNode1, Node* aNode2,
GetParentFunc aGetParentFunc) {

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

@ -485,8 +485,21 @@ class nsContentUtils {
* This method just sucks.
*/
static nsresult GetInclusiveAncestorsAndOffsets(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>* aAncestorNodes,
nsTArray<mozilla::Maybe<uint32_t>>* aAncestorOffsets);
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>& aAncestorNodes,
nsTArray<mozilla::Maybe<uint32_t>>& aAncestorOffsets);
/*
* https://dom.spec.whatwg.org/#concept-shadow-including-ancestor.
*
* Similar as the GetInclusiveAncestorsAndOffsets method, except this
* will use host elements as the parent for shadow roots.
*
* When the current content is a ShadowRoot, the offset of it from
* its ancestor (the host element) will be Nothing().
*/
static nsresult GetShadowIncludingAncestorsAndOffsets(
nsINode* aNode, uint32_t aOffset, nsTArray<nsIContent*>& aAncestorNodes,
nsTArray<mozilla::Maybe<uint32_t>>& aAncestorOffsets);
/**
* Returns the closest common inclusive ancestor

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

@ -132,13 +132,14 @@ static nsresult EncodeForTextUnicode(nsIDocumentEncoder& aEncoder,
aSerializationResult.Assign(buf);
} else {
// Redo the encoding, but this time use pretty printing.
flags =
nsIDocumentEncoder::OutputSelectionOnly |
nsIDocumentEncoder::OutputAbsoluteLinks |
nsIDocumentEncoder::SkipInvisibleContent |
nsIDocumentEncoder::OutputDropInvisibleBreak |
(aAdditionalEncoderFlags & (nsIDocumentEncoder::OutputNoScriptContent |
nsIDocumentEncoder::OutputRubyAnnotation));
flags = nsIDocumentEncoder::OutputSelectionOnly |
nsIDocumentEncoder::OutputAbsoluteLinks |
nsIDocumentEncoder::SkipInvisibleContent |
nsIDocumentEncoder::OutputDropInvisibleBreak |
(aAdditionalEncoderFlags &
(nsIDocumentEncoder::OutputNoScriptContent |
nsIDocumentEncoder::OutputRubyAnnotation |
nsIDocumentEncoder::AllowCrossShadowBoundary));
mimeType.AssignLiteral(kTextMime);
rv = aEncoder.Init(&aDocument, mimeType, flags);
@ -345,6 +346,11 @@ nsresult nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
uint32_t additionalFlags = nsIDocumentEncoder::SkipInvisibleContent;
if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
additionalFlags |= nsIDocumentEncoder::AllowCrossShadowBoundary;
}
if (aWithRubyAnnotation) {
additionalFlags |= nsIDocumentEncoder::OutputRubyAnnotation;
}
@ -898,7 +904,7 @@ bool nsCopySupport::FireClipboardEvent(EventMessage aEventMessage,
// XXX this is probably the wrong editable flag to check
if (originalEventMessage != eCut || targetElement->IsEditable()) {
// get the data from the selection if any
if (sel->IsCollapsed()) {
if (sel->AreNormalAndCrossShadowBoundaryRangesCollapsed()) {
if (aActionTaken) {
*aActionTaken = true;
}

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

@ -0,0 +1,312 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function getLoadContext() {
var Ci = SpecialPowers.Ci;
return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext);
}
var clipboard = SpecialPowers.Services.clipboard;
var documentViewer = SpecialPowers.wrap(
window
).docShell.docViewer.QueryInterface(SpecialPowers.Ci.nsIDocumentViewerEdit);
function getClipboardData(mime) {
var transferable = SpecialPowers.Cc[
"@mozilla.org/widget/transferable;1"
].createInstance(SpecialPowers.Ci.nsITransferable);
transferable.init(getLoadContext());
transferable.addDataFlavor(mime);
clipboard.getData(
transferable,
1,
SpecialPowers.wrap(window).browsingContext.currentWindowContext
);
var data = SpecialPowers.createBlankObject();
transferable.getTransferData(mime, data);
return data;
}
function testClipboardValue(suppressHTMLCheck, mime, expected) {
if (suppressHTMLCheck && mime == "text/html") {
return null;
}
var data = SpecialPowers.wrap(getClipboardData(mime));
is(
data.value == null
? data.value
: data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data,
expected,
mime + " value in the clipboard"
);
return data.value;
}
function testSelectionToString(expected) {
const flags =
SpecialPowers.Ci.nsIDocumentEncoder.SkipInvisibleContent |
SpecialPowers.Ci.nsIDocumentEncoder.AllowCrossShadowBoundary;
is(
SpecialPowers.wrap(window)
.getSelection()
.toStringWithFormat("text/plain", flags, 0)
.replace(/\r\n/g, "\n"),
expected,
"Selection.toString"
);
}
function testHtmlClipboardValue(suppressHTMLCheck, mime, expected) {
// For Windows, navigator.platform returns "Win32".
var expectedValue = expected;
if (navigator.platform.includes("Win")) {
// Windows has extra content.
var expectedValue =
kTextHtmlPrefixClipboardDataWindows +
expected.replace(/\n/g, "\n") +
kTextHtmlSuffixClipboardDataWindows;
}
testClipboardValue(suppressHTMLCheck, mime, expectedValue);
}
function testPasteText(textarea, expected) {
textarea.value = "";
textarea.focus();
textarea.editor.paste(1);
is(textarea.value, expected, "value of the textarea after the paste");
}
async function copySelectionToClipboard() {
await SimpleTest.promiseClipboardChange(
() => true,
() => {
documentViewer.copySelection();
}
);
ok(clipboard.hasDataMatchingFlavors(["text/plain"], 1), "check text/plain");
ok(clipboard.hasDataMatchingFlavors(["text/html"], 1), "check text/html");
}
async function testCopyPasteShadowDOM() {
var textarea = SpecialPowers.wrap(document.getElementById("input"));
function clear() {
textarea.blur();
var sel = window.getSelection();
sel.removeAllRanges();
}
async function copySelectionToClipboardShadow(
anchorNode,
anchorOffset,
focusNode,
focusOffset
) {
clear();
var sel = window.getSelection();
sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
await copySelectionToClipboard();
}
info(
"Test 1: Both start and end are in light DOM, the range has contents in Shadow DOM."
);
await copySelectionToClipboardShadow(
document.getElementById("title"),
0,
document.getElementById("host1"),
1
);
testSelectionToString("This is a draggable bit of text.\nShadow Content1 ");
testClipboardValue(
false,
"text/plain",
"This is a draggable bit of text.\nShadow Content1 "
);
testHtmlClipboardValue(
false,
"text/html",
'<div id="title" title="title to have a long HTML line">This is a <em>draggable</em> bit of text.</div>\n <div id="host1">\n <span id="shadow-content">Shadow Content1</span>\n </div>'
);
testPasteText(textarea, "This is a draggable bit of text.\nShadow Content1 ");
info("Test 2: Start is in Shadow DOM and end is in light DOM.");
await copySelectionToClipboardShadow(
document.getElementById("host1").shadowRoot.getElementById("shadow-content")
.firstChild,
3,
document.getElementById("light-content").firstChild,
5
);
testSelectionToString("dow Content1\nLight");
testClipboardValue(false, "text/plain", "dow Content1\nLight");
testHtmlClipboardValue(
false,
"text/html",
'<div id="host1"><span id="shadow-content">dow Content1</span>\n </div>\n\n <span id="light-content">Light</span>'
);
info("Test 3: Start is in light DOM and end is in shadow DOM.");
await copySelectionToClipboardShadow(
document.getElementById("light-content").firstChild,
3,
document.getElementById("host2").shadowRoot.getElementById("shadow-content")
.firstChild,
5
);
testSelectionToString("ht Content\nShado");
testClipboardValue(false, "text/plain", "ht Content\nShado");
testHtmlClipboardValue(
false,
"text/html",
'<span id="light-content">ht Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shado</span></div>'
);
info("Test 4: start is in light DOM and end is a nested shadow DOM.\n");
await copySelectionToClipboardShadow(
document.getElementById("light-content").firstChild,
3,
document
.getElementById("host2")
.shadowRoot.getElementById("nested-host")
.shadowRoot.getElementById("nested-shadow-content").firstChild,
5
);
testSelectionToString("ht Content\nShadow Content2\nNeste");
testClipboardValue(false, "text/plain", "ht Content\nShadow Content2\nNeste");
testHtmlClipboardValue(
false,
"text/html",
'<span id="light-content">ht Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shadow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div></div>'
);
info("Test 5: Both start and end are in shadow DOM but in different trees.");
await copySelectionToClipboardShadow(
document.getElementById("host1").shadowRoot.getElementById("shadow-content")
.firstChild,
3,
document
.getElementById("host2")
.shadowRoot.getElementById("nested-host")
.shadowRoot.getElementById("nested-shadow-content").firstChild,
5
);
testSelectionToString("dow Content1\nLight Content\nShadow Content2\nNeste");
testClipboardValue(
false,
"text/plain",
"dow Content1\nLight Content\nShadow Content2\nNeste"
);
testHtmlClipboardValue(
false,
"text/html",
'<div id="host1"><span id="shadow-content">dow Content1</span>\n </div>\n\n <span id="light-content">Light Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shadow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div></div>'
);
info(
"Test 6: Start is in a shadow tree and end is in a nested shadow tree within the same shadow tree."
);
await copySelectionToClipboardShadow(
document.getElementById("host2").shadowRoot.getElementById("shadow-content")
.firstChild,
3,
document
.getElementById("host2")
.shadowRoot.getElementById("nested-host")
.shadowRoot.getElementById("nested-shadow-content").firstChild,
5
);
testSelectionToString("dow Content2\nNeste");
testClipboardValue(false, "text/plain", "dow Content2\nNeste");
testHtmlClipboardValue(
false,
"text/html",
'<span id="shadow-content">dow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div>'
);
info(
"Test 7: End is at a slotted content where the slot element is before the regular shadow dom contents."
);
await copySelectionToClipboardShadow(
document.getElementById("light-content2").firstChild,
3,
document.getElementById("slotted1").firstChild,
8
);
testSelectionToString("ht Content\nShadow Content2 slotted1");
testClipboardValue(
false,
"text/plain",
"ht Content\nShadow Content2 slotted1"
);
testHtmlClipboardValue(
false,
"text/html",
'<span id="light-content2">ht Content</span>\n <div id="host3">\n <slot name="slot1"></slot>\n <span id="shadow-content">Shadow Content2</span>\n <slot name="slot2"></slot>\n <span slot="slot1" id="slotted1">slotted1</span></div>'
);
info(
"Test 8: End is at a slotted content where the slot element is after the regular shadow dom contents"
);
await copySelectionToClipboardShadow(
document.getElementById("light-content2").firstChild,
3,
document.getElementById("slotted2").firstChild,
8
);
testSelectionToString("ht Content\nShadow Content2 slotted1slotted2");
testClipboardValue(
false,
"text/plain",
"ht Content\nShadow Content2 slotted1slotted2"
);
testHtmlClipboardValue(
false,
"text/html",
'<span id="light-content2">ht Content</span>\n <div id="host3">\n <slot name="slot1"></slot>\n <span id="shadow-content">Shadow Content2</span>\n <slot name="slot2"></slot>\n <span slot="slot1" id="slotted1">slotted1</span><span slot="slot2" id="slotted2">slotted2</span></div>'
);
info(
"Test 9: things still work as expected with a more complex shadow tree."
);
await copySelectionToClipboardShadow(
document.getElementById("slotted3").firstChild,
3,
document.getElementById("slotted4").firstChild,
8
);
testSelectionToString(
" Shadow Content2\nShadowNested Nested Slotted\ntted1slotted2"
);
testClipboardValue(
false,
"text/plain",
" Shadow Content2\nShadowNested Nested Slotted\ntted1slotted2"
);
testHtmlClipboardValue(
false,
"text/html",
'\n <slot name="slot1"></slot>\n <span id="shadow-content">Shadow Content2</span>\n <div id="nestedHost">\n <slot></slot>\n <span>ShadowNested</span>\n \n \n <span>Nested Slotted</span>\n </div>\n <slot name="slot2"></slot>\n <span slot="slot1" id="slotted3">tted1</span><span slot="slot2" id="slotted4">slotted2</span>'
);
// FIXME: This behaviour is not expected and we'll fix it in bug 1901053
info("Test 10: Slot element is always serialized even if it's not visible");
await copySelectionToClipboardShadow(
document.getElementById("light-content3").firstChild,
0,
document.getElementById("host5").shadowRoot.querySelector("span")
.firstChild,
5
);
testSelectionToString("Light Content\ndefault value Shado Slotted ");
testClipboardValue(
false,
"text/plain",
"Light Content\ndefault value Shado Slotted "
);
testHtmlClipboardValue(
false,
"text/html",
'<span id="light-content3">Light Content</span>\n \n <div id="host5">\n <slot>default value</slot>\n <span>Shado</span>\n \n <span>Slotted</span>\n </div>'
);
}

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

@ -59,6 +59,7 @@ support-files = [
"bug1576154.sjs",
"chrome/bug418986-1.js",
"copypaste.js",
"copypaste_shadow_dom.js",
"delayedServerEvents.sjs",
"eventsource_message.sjs",
"eventsource_reconnect.sjs",
@ -1182,6 +1183,12 @@ skip-if = [
"headless", #bug 904183
]
["test_copypaste_shadow_dom.html"]
skip-if = [
"os == 'android'",
"headless", #bug 904183
]
["test_copypaste.xhtml"]
skip-if = ["headless"] #bug 904183

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

@ -0,0 +1,93 @@
<!DOCTYPE HTML>
<html>
<!--
This test is copied from test_copypaste.html, and the main purpose of it is
to test copy pasting works when the selected contents have shadow trees involved.
-->
<head>
<title>Test for copy/paste</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="copypaste_shadow_dom.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
addLoadEvent(() => {
add_task(async function test_copyhtml() {
await testCopyPasteShadowDOM();
});
});
</script>
</pre>
<div>
<textarea id="input" cols="40" rows="10"></textarea>
<div id="title" title="title to have a long HTML line">This is a <em>draggable</em> bit of text.</div>
<div id="host1">
<template shadowrootmode="open">
<span id="shadow-content">Shadow Content1</span>
</template>
</div>
<span id="light-content">Light Content</span>
<div id="host2">
<template shadowrootmode="open">
<span id="shadow-content">Shadow Content2</span>
<div id="nested-host">
<template shadowrootmode="open">
<span id="nested-shadow-content">Nested Shadow</span>
</template>
</div>
</template>
</div>
<span id="light-content2">Light Content</span>
<div id="host3">
<template shadowrootmode="open">
<slot name="slot1"></slot>
<span id="shadow-content">Shadow Content2</span>
<slot name="slot2"></slot>
</template>
<span slot="slot1" id="slotted1">slotted1</span>
<span slot="slot2" id="slotted2">slotted2</span>
</div>
<!--A more complex shadow tree-->
<div id="host4">
<template shadowrootmode="open">
<slot name="slot1"></slot>
<span id="shadow-content">Shadow Content2</span>
<div id="nestedHost">
<template shadowrootmode="open">
<slot></slot>
<span>ShadowNested</span>
</template>
<span>Nested Slotted</span>
</div>
<slot name="slot2"></slot>
</template>
<span slot="slot1" id="slotted3">slotted1</span>
<span slot="slot2" id="slotted4">slotted2</span>
</div>
<span id="light-content3">Light Content</span>
<!--A shadow host with <slot> that have a default value-->
<div id="host5">
<template shadowrootmode="open">
<slot>default value</slot>
<span>ShadowContent</span>
</template>
<span>Slotted</span>
</div>
</div>
</body>
</html>

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

@ -44,6 +44,7 @@
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/ProcessingInstruction.h"
#include "mozilla/dom/ShadowRoot.h"
#include "mozilla/dom/AbstractRange.h"
#include "mozilla/dom/Text.h"
#include "mozilla/Encoding.h"
#include "mozilla/IntegerRange.h"
@ -242,7 +243,8 @@ class nsDocumentEncoder : public nsIDocumentEncoder {
protected:
virtual ~nsDocumentEncoder();
void Initialize(bool aClearCachedSerializer = true);
void Initialize(bool aClearCachedSerializer = true,
bool aAllowCrossShadowBoundary = false);
/**
* @param aMaxLength As described at
@ -427,7 +429,7 @@ class nsDocumentEncoder : public nsIDocumentEncoder {
mNodeSerializer{aNodeSerializer},
mRangeContextSerializer{aRangeContextSerializer} {}
void Initialize();
void Initialize(bool aAllowCrossShadowBoundary);
/**
* @param aDepth the distance (number of `GetParent` calls) from aNode to
@ -493,18 +495,22 @@ class nsDocumentEncoder : public nsIDocumentEncoder {
const NodeSerializer& mNodeSerializer;
RangeContextSerializer& mRangeContextSerializer;
bool mAllowCrossShadowBoundary = false;
};
RangeSerializer mRangeSerializer;
};
void nsDocumentEncoder::RangeSerializer::Initialize() {
void nsDocumentEncoder::RangeSerializer::Initialize(
bool aAllowCrossShadowBoundary) {
mContextInfoDepth = {};
mStartRootIndex = 0;
mEndRootIndex = 0;
mHaltRangeHint = false;
mClosestCommonInclusiveAncestorOfRange = nullptr;
mRangeBoundariesInclusiveAncestorsAndOffsets = {};
mAllowCrossShadowBoundary = aAllowCrossShadowBoundary;
}
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDocumentEncoder)
@ -540,10 +546,11 @@ nsDocumentEncoder::nsDocumentEncoder(
nsDocumentEncoder::nsDocumentEncoder()
: nsDocumentEncoder(MakeUnique<RangeNodeContext>()) {}
void nsDocumentEncoder::Initialize(bool aClearCachedSerializer) {
void nsDocumentEncoder::Initialize(bool aClearCachedSerializer,
bool aAllowCrossShadowBoundary) {
mFlags = 0;
mWrapColumn = 72;
mRangeSerializer.Initialize();
mRangeSerializer.Initialize(aAllowCrossShadowBoundary);
mNeedsPreformatScanning = false;
mRangeContextSerializer.mDisableContextSerialize = false;
mEncodingScope = {};
@ -598,7 +605,8 @@ nsresult nsDocumentEncoder::SerializeSelection() {
// Excel. Each separate block of <tr></tr> produced above will be wrapped
// by the immediate context. This assumes that you can't select cells that
// are multiple selections from two tables simultaneously.
node = range->GetStartContainer();
node = ShadowDOMSelectionHelpers::GetStartContainer(
range, mFlags & nsIDocumentEncoder::AllowCrossShadowBoundary);
NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
if (node != prevNode) {
if (prevNode) {
@ -714,7 +722,8 @@ nsDocumentEncoder::NativeInit(Document* aDocument, const nsAString& aMimeType,
uint32_t aFlags) {
if (!aDocument) return NS_ERROR_INVALID_ARG;
Initialize(!mMimeType.Equals(aMimeType));
Initialize(!mMimeType.Equals(aMimeType),
aFlags & nsIDocumentEncoder::AllowCrossShadowBoundary);
mDocument = aDocument;
@ -947,12 +956,28 @@ nsresult nsDocumentEncoder::NodeSerializer::SerializeToStringRecursive(
NS_ENSURE_SUCCESS(rv, rv);
}
ShadowRoot* shadowRoot = ShadowDOMSelectionHelpers::GetShadowRoot(
aNode, mFlags & nsIDocumentEncoder::AllowCrossShadowBoundary);
if (shadowRoot) {
MOZ_ASSERT(StaticPrefs::dom_shadowdom_selection_across_boundary_enabled());
// Serialize the ShadowRoot first when the entire node needs to be
// serialized.
SerializeToStringRecursive(shadowRoot, aSerializeRoot, aMaxLength);
}
nsINode* node = fixupNodeDeterminer.IsSerializationOfFixupChildrenNeeded()
? maybeFixedNode
: aNode;
for (nsINode* child = node->GetFirstChildOfTemplateOrNode(); child;
child = child->GetNextSibling()) {
if (shadowRoot &&
(!child->IsContent() || !child->AsContent()->GetAssignedSlot())) {
// Since this node is a shadow host, we skip the children that are not
// slotted because they aren't visible.
continue;
}
rv = SerializeToStringRecursive(child, SerializeRoot::eYes, aMaxLength);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -1042,10 +1067,14 @@ nsresult nsDocumentEncoder::RangeSerializer::SerializeTextNode(
nsINode& aNode, const nsIContent& aContent,
const StartAndEndContent& aStartAndEndContent,
const nsRange& aRange) const {
const int32_t startOffset =
(aStartAndEndContent.mStart == &aContent) ? aRange.StartOffset() : 0;
const int32_t endOffset =
(aStartAndEndContent.mEnd == &aContent) ? aRange.EndOffset() : -1;
const int32_t startOffset = (aStartAndEndContent.mStart == &aContent)
? ShadowDOMSelectionHelpers::StartOffset(
&aRange, mAllowCrossShadowBoundary)
: 0;
const int32_t endOffset = (aStartAndEndContent.mEnd == &aContent)
? ShadowDOMSelectionHelpers::EndOffset(
&aRange, mAllowCrossShadowBoundary)
: -1;
return mNodeSerializer.SerializeTextNode(aNode, startOffset, endOffset);
}
@ -1143,17 +1172,19 @@ nsDocumentEncoder::RangeSerializer::SerializeNodePartiallyContainedInRange(
// intermediate points on the list use the endOffset of the
// location of the ancestor, rather than just past it. So we need
// to add one here in order to include it in the children we serialize.
if (&aNode != aRange.GetEndContainer()) {
const nsINode* endContainer = ShadowDOMSelectionHelpers::GetEndContainer(
&aRange, mAllowCrossShadowBoundary);
if (&aNode != endContainer) {
MOZ_ASSERT(*endOffset != UINT32_MAX);
endOffset.ref()++;
}
}
if (*endOffset) {
nsresult rv = SerializeChildrenOfContent(aContent, *startOffset,
*endOffset, &aRange, aDepth);
NS_ENSURE_SUCCESS(rv, rv);
}
MOZ_ASSERT(endOffset.isSome());
nsresult rv = SerializeChildrenOfContent(aContent, *startOffset, *endOffset,
&aRange, aDepth);
NS_ENSURE_SUCCESS(rv, rv);
// serialize the end of this node
if (&aNode != mClosestCommonInclusiveAncestorOfRange) {
nsresult rv = mNodeSerializer.SerializeNodeEnd(aNode);
@ -1167,6 +1198,17 @@ nsDocumentEncoder::RangeSerializer::SerializeNodePartiallyContainedInRange(
nsresult nsDocumentEncoder::RangeSerializer::SerializeChildrenOfContent(
nsIContent& aContent, uint32_t aStartOffset, uint32_t aEndOffset,
const nsRange* aRange, int32_t aDepth) {
ShadowRoot* shadowRoot = ShadowDOMSelectionHelpers::GetShadowRoot(
&aContent, mAllowCrossShadowBoundary);
if (shadowRoot) {
// Serialize the ShadowRoot first when the entire node needs to be
// serialized.
SerializeRangeNodes(aRange, shadowRoot, aDepth + 1);
}
if (!aEndOffset) {
return NS_OK;
}
// serialize the children of this node that are in the range
nsIContent* childAsNode = aContent.GetFirstChild();
uint32_t j = 0;
@ -1178,6 +1220,12 @@ nsresult nsDocumentEncoder::RangeSerializer::SerializeChildrenOfContent(
MOZ_ASSERT(j == aStartOffset);
for (; childAsNode && j < aEndOffset; ++j) {
if (shadowRoot && !childAsNode->GetAssignedSlot()) {
childAsNode = childAsNode->GetNextSibling();
// Since this node is a shadow host, we skip the children that are not
// slotted because they aren't visible.
continue;
}
nsresult rv{NS_OK};
if ((j == aStartOffset) || (j == aEndOffset - 1)) {
rv = SerializeRangeNodes(aRange, childAsNode, aDepth + 1);
@ -1264,7 +1312,10 @@ bool nsDocumentEncoder::RangeSerializer::HasInvisibleParentAndShouldBeSkipped(
nsresult nsDocumentEncoder::RangeSerializer::SerializeRangeToString(
const nsRange* aRange) {
if (!aRange || aRange->Collapsed()) return NS_OK;
if (!aRange || (aRange->Collapsed() && (!mAllowCrossShadowBoundary ||
!aRange->MayCrossShadowBoundary()))) {
return NS_OK;
}
// Consider a case where the boundary of the selection is ShadowRoot (ie, the
// first child of ShadowRoot is selected, so ShadowRoot is the container hence
@ -1273,19 +1324,24 @@ nsresult nsDocumentEncoder::RangeSerializer::SerializeRangeToString(
// SerializeRangeContextStart doesn't support this case.
mClosestCommonInclusiveAncestorOfRange =
aRange->GetClosestCommonInclusiveAncestor(
AllowRangeCrossShadowBoundary::No);
mAllowCrossShadowBoundary ? AllowRangeCrossShadowBoundary::Yes
: AllowRangeCrossShadowBoundary::No);
if (!mClosestCommonInclusiveAncestorOfRange) {
return NS_OK;
}
nsINode* startContainer = aRange->GetStartContainer();
nsINode* startContainer = ShadowDOMSelectionHelpers::GetStartContainer(
aRange, mAllowCrossShadowBoundary);
NS_ENSURE_TRUE(startContainer, NS_ERROR_FAILURE);
int32_t startOffset = aRange->StartOffset();
const int32_t startOffset =
ShadowDOMSelectionHelpers::StartOffset(aRange, mAllowCrossShadowBoundary);
nsINode* endContainer = aRange->GetEndContainer();
nsINode* endContainer = ShadowDOMSelectionHelpers::GetEndContainer(
aRange, mAllowCrossShadowBoundary);
NS_ENSURE_TRUE(endContainer, NS_ERROR_FAILURE);
int32_t endOffset = aRange->EndOffset();
const int32_t endOffset =
ShadowDOMSelectionHelpers::EndOffset(aRange, mAllowCrossShadowBoundary);
mContextInfoDepth = {};
mCommonInclusiveAncestors.Clear();
@ -1304,12 +1360,21 @@ nsresult nsDocumentEncoder::RangeSerializer::SerializeRangeToString(
nsContentUtils::GetInclusiveAncestors(mClosestCommonInclusiveAncestorOfRange,
mCommonInclusiveAncestors);
nsContentUtils::GetInclusiveAncestorsAndOffsets(
startContainer, startOffset, &inclusiveAncestorsOfStart,
&inclusiveAncestorsOffsetsOfStart);
nsContentUtils::GetInclusiveAncestorsAndOffsets(
endContainer, endOffset, &inclusiveAncestorsOfEnd,
&inclusiveAncestorsOffsetsOfEnd);
if (mAllowCrossShadowBoundary) {
nsContentUtils::GetShadowIncludingAncestorsAndOffsets(
startContainer, startOffset, inclusiveAncestorsOfStart,
inclusiveAncestorsOffsetsOfStart);
nsContentUtils::GetShadowIncludingAncestorsAndOffsets(
endContainer, endOffset, inclusiveAncestorsOfEnd,
inclusiveAncestorsOffsetsOfEnd);
} else {
nsContentUtils::GetInclusiveAncestorsAndOffsets(
startContainer, startOffset, inclusiveAncestorsOfStart,
inclusiveAncestorsOffsetsOfStart);
nsContentUtils::GetInclusiveAncestorsAndOffsets(
endContainer, endOffset, inclusiveAncestorsOfEnd,
inclusiveAncestorsOffsetsOfEnd);
}
nsCOMPtr<nsIContent> commonContent =
nsIContent::FromNodeOrNull(mClosestCommonInclusiveAncestorOfRange);
@ -1545,7 +1610,7 @@ nsHTMLCopyEncoder::Init(Document* aDocument, const nsAString& aMimeType,
if (!aDocument) return NS_ERROR_INVALID_ARG;
mIsTextWidget = false;
Initialize();
Initialize(true, aFlags & nsIDocumentEncoder::AllowCrossShadowBoundary);
mIsCopying = true;
mDocument = aDocument;
@ -1751,11 +1816,13 @@ nsresult nsHTMLCopyEncoder::PromoteRange(nsRange* inRange) {
if (!inRange->IsPositioned()) {
return NS_ERROR_UNEXPECTED;
}
nsCOMPtr<nsINode> startNode = inRange->GetStartContainer();
uint32_t startOffset = inRange->StartOffset();
nsCOMPtr<nsINode> endNode = inRange->GetEndContainer();
uint32_t endOffset = inRange->EndOffset();
nsCOMPtr<nsINode> common = inRange->GetClosestCommonInclusiveAncestor();
nsCOMPtr<nsINode> startNode =
inRange->GetMayCrossShadowBoundaryStartContainer();
const uint32_t startOffset = inRange->MayCrossShadowBoundaryStartOffset();
nsCOMPtr<nsINode> endNode = inRange->GetMayCrossShadowBoundaryEndContainer();
const uint32_t endOffset = inRange->MayCrossShadowBoundaryEndOffset();
nsCOMPtr<nsINode> common = inRange->GetClosestCommonInclusiveAncestor(
AllowRangeCrossShadowBoundary::Yes);
nsCOMPtr<nsINode> opStartNode;
nsCOMPtr<nsINode> opEndNode;
@ -1781,11 +1848,19 @@ nsresult nsHTMLCopyEncoder::PromoteRange(nsRange* inRange) {
// set the range to the new values
ErrorResult err;
inRange->SetStart(*opStartNode, static_cast<uint32_t>(opStartOffset), err);
const bool allowRangeCrossShadowBoundary =
mFlags & nsIDocumentEncoder::AllowCrossShadowBoundary;
inRange->SetStart(*opStartNode, static_cast<uint32_t>(opStartOffset), err,
allowRangeCrossShadowBoundary
? AllowRangeCrossShadowBoundary::Yes
: AllowRangeCrossShadowBoundary::No);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
inRange->SetEnd(*opEndNode, static_cast<uint32_t>(opEndOffset), err);
inRange->SetEnd(*opEndNode, static_cast<uint32_t>(opEndOffset), err,
allowRangeCrossShadowBoundary
? AllowRangeCrossShadowBoundary::Yes
: AllowRangeCrossShadowBoundary::No);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}

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

@ -235,6 +235,7 @@ interface nsIDocumentEncoder : nsISupports
*/
const unsigned long RequiresReinitAfterOutput = (1 << 28);
const unsigned long AllowCrossShadowBoundary = (1 << 29);
/**
* Initialize with a pointer to the document and the mime type.
* Resets wrap column to 72 and resets node fixup.

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

@ -31,6 +31,7 @@
#include "mozilla/dom/Text.h"
#include "mozilla/intl/Segmenter.h"
#include "mozilla/intl/UnicodeProperties.h"
#include "mozilla/dom/AbstractRange.h"
#include "nsUnicodeProperties.h"
#include "mozilla/Span.h"
#include "mozilla/Preferences.h"

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

@ -3101,7 +3101,8 @@ void AutoCopyListener::OnSelectionChange(Document* aDocument,
return; // Don't care if we are still dragging.
}
if (!aDocument || aSelection.IsCollapsed()) {
if (!aDocument ||
aSelection.AreNormalAndCrossShadowBoundaryRangesCollapsed()) {
#ifdef DEBUG_CLIPBOARD
fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
#endif