From 43a805714a91d52b7bc233f3f2320b6025042345 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Wed, 20 Nov 2024 04:35:32 +0000 Subject: [PATCH] Bug 1930277 - Make `nsHTMLCopyEncoder::RangeNodeContext::IncludeInContext` not treat inline editing host as an contextual inline element r=edgar,dom-core It treats some inline elements as contextual elements. Then, they will be preserved in copied HTML fragment. However, if an inline element is an editing host, we don't want to contain it to the copied fragment because pasting it causes duplicating same style into same editing host. So, if the style includes relative style like `font-size: 2em`, it will cause bigger text than the surrounding text. Additionally, the inline editing host usually has a border but we don't want to make it appear in editable text. Unfortunately, with this change, we stop copying the text style specified to the inline editing host. However, this is same behavior as when the editing host is a block element like `
`. Note that pasted text will be merged into the inline editing host style. Therefore, if and only if the destination has different style from the editing host, the result might be different from the expected one by the user. However, this is a long standing issue, see bug 1428046, for example. Differential Revision: https://phabricator.services.mozilla.com/D228623 --- dom/serializers/nsDocumentEncoder.cpp | 11 +- editor/libeditor/tests/test_dragdrop.html | 128 ++++++++++++++++++ ...e-in-styled-inline-editing-host.https.html | 37 +++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 testing/web-platform/tests/editing/other/copy-paste-in-styled-inline-editing-host.https.html diff --git a/dom/serializers/nsDocumentEncoder.cpp b/dom/serializers/nsDocumentEncoder.cpp index c10594820f04..9b5a63018239 100644 --- a/dom/serializers/nsDocumentEncoder.cpp +++ b/dom/serializers/nsDocumentEncoder.cpp @@ -1799,9 +1799,16 @@ nsHTMLCopyEncoder::EncodeToStringWithContext(nsAString& aContextString, bool nsHTMLCopyEncoder::RangeNodeContext::IncludeInContext( nsINode& aNode) const { - nsCOMPtr content(nsIContent::FromNodeOrNull(&aNode)); + const nsIContent* const content = nsIContent::FromNodeOrNull(&aNode); + if (!content) { + return false; + } - if (!content) return false; + // If it's an inline editing host, we should not treat it gives a context to + // avoid to duplicate its style. + if (content->IsEditingHost()) { + return false; + } return content->IsAnyOfHTMLElements( nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::a, nsGkAtoms::tt, diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html index 11e29d3b091b..7e7b4b5190fe 100644 --- a/editor/libeditor/tests/test_dragdrop.html +++ b/editor/libeditor/tests/test_dragdrop.html @@ -3527,6 +3527,134 @@ async function doTest() { document.removeEventListener("drop", onDrop); })(); + // -------- Test dragging inline contenteditable to same contenteditable + await (async function test_dragging_from_inline_contenteditable_to_itself() { + const description = "dragging text in inline contenteditable to same contenteditable"; + container.innerHTML = `dragme!!MMMM`; + const contenteditable = document.querySelector("span[contenteditable]"); + const span = contenteditable.querySelector("span"); + selection.setBaseAndExtent(contenteditable.firstChild, 0, contenteditable.firstChild, "dragme".length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "dragme", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "dragme", + `${description}: dataTransfer should have selected text as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + const kExpectedOffsets = isAndroidException ? [4,4] : [2,2]; + if (isAndroidException) { + todo_is(contenteditable.innerHTML, "!!MMdragmeMM", + `${description}: dragged range should be moved in inline contenteditable`); + is(contenteditable.innerHTML, "!!MMMMdragme", + `${description}: dragged range should be moved in inline contenteditable`); + } else { + is(contenteditable.innerHTML, "!!MMdragmeMM", + `${description}: dragged range should be moved in inline contenteditable`); + } + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on inline contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: "dragme".length}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "dragme"}, + {type: "text/plain", data: "dragme"}], + [{startContainer: span.firstChild, startOffset: kExpectedOffsets[0], + endContainer: span.firstChild, endOffset: kExpectedOffsets[1]}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on inline contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "dragme"}, + {type: "text/plain", data: "dragme"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on inline contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging inline contenteditable to other inline contenteditable + await (async function test_dragging_from_inline_contenteditable_to_other_inline_contenteditable() { + const description = "dragging text in inline contenteditable to other inline contenteditable"; + container.innerHTML = 'dragme!!
MM'; + const contenteditable = document.querySelector("div#container > span[contenteditable]"); + const otherContenteditable = document.querySelector("div#container > span[contenteditable] ~ span[contenteditable]"); + selection.setBaseAndExtent(contenteditable.firstChild, 0, contenteditable.firstChild, "dragme".length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "dragme", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "dragme", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + const kExpectedOffsets = isAndroidException ? [2,2] : [1,1]; + is(contenteditable.innerHTML, "!!", + `${description}: dragged range should be removed from inline contenteditable`); + if (isAndroidException) { + todo_is(otherContenteditable.innerHTML, "MdragmeM", + `${description}: dragged content should be inserted into other inline contenteditable`); + is(otherContenteditable.innerHTML, "MMdragme", + `${description}: dragged content should be inserted into other inline contenteditable`); + } else { + is(otherContenteditable.innerHTML, "MdragmeM", + `${description}: dragged content should be inserted into other inline contenteditable`); + } + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on inline contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: "dragme".length}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "dragme"}, + {type: "text/plain", data: "dragme"}], + [{startContainer: otherContenteditable.firstChild, startOffset: kExpectedOffsets[0], + endContainer: otherContenteditable.firstChild, endOffset: kExpectedOffsets[1]}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on inline contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "dragme"}, + {type: "text/plain", data: "dragme"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other inline contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + // We need to clean up contenteditable=plaintext-only before the pref enabling it is cleared. container.innerHTML = ""; diff --git a/testing/web-platform/tests/editing/other/copy-paste-in-styled-inline-editing-host.https.html b/testing/web-platform/tests/editing/other/copy-paste-in-styled-inline-editing-host.https.html new file mode 100644 index 000000000000..d1d875f9b411 --- /dev/null +++ b/testing/web-platform/tests/editing/other/copy-paste-in-styled-inline-editing-host.https.html @@ -0,0 +1,37 @@ + + + + + +Copying text in styled inline editing host should not duplicate the editing host + + + + + + + + + +ABC
+ +