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