Bug 1920646 - part 2: Make HTMLEditor handle insertHTML as inserting plaintext converted from given source r=m_kato

Differential Revision: https://phabricator.services.mozilla.com/D223909
This commit is contained in:
Masayuki Nakano 2024-10-02 21:42:10 +00:00
parent 7e9bbe23c8
commit dc004f95b6
3 changed files with 225 additions and 13 deletions

View File

@ -188,6 +188,19 @@ class TextComposition final {
*/
bool IsComposing() const { return mIsComposing; }
/**
* If we're requesting IME to commit or cancel composition, or we've already
* requested it, or we've already known this composition has been ended in
* IME, we don't need to request commit nor cancel composition anymore and
* shouldn't do so if we're in content process for not committing/canceling
* "current" composition in native IME. So, when this returns true,
* RequestIMEToCommit() does nothing.
*/
[[nodiscard]] bool CanRequsetIMEToCommitOrCancelComposition() const {
return !mIsRequestingCommit && !mIsRequestingCancel &&
!mRequestedToCommitOrCancel && !mHasReceivedCommitEvent;
}
/**
* Returns true if editor has started or already ended handling an event which
* is modifying the composition string and/or IME selections.
@ -412,19 +425,6 @@ class TextComposition final {
// when DispatchCompositionEvent() is called.
bool mWasCompositionStringEmpty;
/**
* If we're requesting IME to commit or cancel composition, or we've already
* requested it, or we've already known this composition has been ended in
* IME, we don't need to request commit nor cancel composition anymore and
* shouldn't do so if we're in content process for not committing/canceling
* "current" composition in native IME. So, when this returns true,
* RequestIMEToCommit() does nothing.
*/
bool CanRequsetIMEToCommitOrCancelComposition() const {
return !mIsRequestingCommit && !mIsRequestingCancel &&
!mRequestedToCommitOrCancel && !mHasReceivedCommitEvent;
}
/**
* GetEditorBase() returns EditorBase pointer of mEditorBaseWeak.
*/

View File

@ -44,6 +44,7 @@
#include "mozilla/OwningNonNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/Result.h"
#include "mozilla/TextComposition.h"
#include "nsAString.h"
#include "nsCOMPtr.h"
#include "nsCRTGlue.h" // for CRLF
@ -290,6 +291,54 @@ nsresult HTMLEditor::InsertHTMLAsAction(const nsAString& aInString,
return EditorBase::ToGenericNSResult(rv);
}
const RefPtr<Element> editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
return NS_ERROR_FAILURE;
}
if (editingHost->IsContentEditablePlainTextOnly()) {
nsAutoString plaintextString;
nsresult rv = nsContentUtils::ConvertToPlainText(
aInString, plaintextString, nsIDocumentEncoder::OutputLFLineBreak,
0u /* never wrap lines*/);
if (NS_FAILED(rv)) {
NS_WARNING("nsContentUtils::ConvertToPlainText() failed");
return EditorBase::ToGenericNSResult(rv);
}
Maybe<AutoPlaceholderBatch> treatAsOneTransaction;
const auto EnsureAutoPlaceholderBatch = [&]() {
if (treatAsOneTransaction.isNothing()) {
treatAsOneTransaction.emplace(*this, ScrollSelectionIntoView::Yes,
__FUNCTION__);
}
};
if (mComposition &&
mComposition->CanRequsetIMEToCommitOrCancelComposition()) {
EnsureAutoPlaceholderBatch();
CommitComposition();
if (NS_WARN_IF(Destroyed())) {
return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(editingHost !=
ComputeEditingHost(LimitInBodyElement::No))) {
return EditorBase::ToGenericNSResult(
NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
if (MOZ_LIKELY(!plaintextString.IsEmpty())) {
EnsureAutoPlaceholderBatch();
rv = InsertTextAsSubAction(plaintextString, SelectionHandling::Delete);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::InsertTextAsSubAction() failed");
} else if (!SelectionRef().IsCollapsed()) {
EnsureAutoPlaceholderBatch();
rv = DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::DeleteSelectionAsSubAction() failed");
}
return EditorBase::ToGenericNSResult(rv);
}
AutoPlaceholderBatch treatAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
rv = InsertHTMLWithContextAsSubAction(aInString, u""_ns, u""_ns, u""_ns,

View File

@ -0,0 +1,163 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="timeout" content="long">
<meta name="variant" content="?white-space=normal">
<meta name="variant" content="?white-space=pre">
<meta name="variant" content="?white-space=pre-line">
<meta name="variant" content="?white-space=pre-wrap">
<title>Pasting rich text into contenteditable=plaintext-only</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../include/editor-test-utils.js"></script>
<script>
"use strict";
const searchParams = new URLSearchParams(document.location.search);
const whiteSpace = searchParams.get("white-space");
const useBR = whiteSpace == "normal";
const collapseWhiteSpaces = whiteSpace == "normal" || whiteSpace == "pre-line";
addEventListener("load", () => {
const editingHost = document.createElement("div");
editingHost.style.whiteSpace = whiteSpace;
editingHost.contentEditable = "plaintext-only";
document.body.appendChild(editingHost);
editingHost.focus();
editingHost.getBoundingClientRect();
const utils = new EditorTestUtils(editingHost);
for (const data of [
{
insertHTML: "plaintext",
expected: "plaintext",
},
{
// line breaks should not be preformatted
insertHTML: "1st line\n2nd line",
expected: "1st line 2nd line",
},
{
// preformatted line breaks should appear as-is
insertHTML: "<pre>1st line\n2nd line</pre>",
expected: useBR
? "1st line<br>2nd line"
: ["1st line<br>2nd line", "1st line\n2nd line"],
},
{
// text should be inserted into the <b>
initialInnerHTML: "<b>{}</b>",
insertHTML: "plaintext",
expected: "<b>plaintext</b>",
},
{
// text should be inserted into the <b>
initialInnerHTML: "<b>{}<br></b>",
insertHTML: "plaintext",
expected: ["<b>plaintext</b>", "<b>plaintext<br></b>"],
},
{
// text should be inserted into the <b>
initialInnerHTML: "<b>A[]B</b>",
insertHTML: "plaintext",
expected: "<b>AplaintextB</b>",
},
{
// text should be inserted into the <span> even if it's meaningless
initialInnerHTML: "<span>A[]B</span>",
insertHTML: "plaintext",
expected: "<span>AplaintextB</span>",
},
{
// inserting one paragraph should cause inserting only its contents.
// (but it's okay other serialized text.)
insertHTML: "<div>abc</div>",
expected: "abc",
},
{
// inserting one paragraph should cause inserting only its contents.
// (but it's okay other serialized text.)
insertHTML: "<div>abc<br>def</div>",
expected: useBR ? "abc<br>def" : ["abc<br>def", "abc\ndef"],
},
{
// inserting 2 or more paragraphs should be handled as multiple lines
insertHTML: "<div>abc</div><div>def</div>",
expected: useBR ? "abc<br>def" : ["abc<br>def", "abc\ndef"],
},
{
// inserting 2 or more paragraphs should be handled as multiple lines
insertHTML: "<div>abc<br>def</div><div>ghi<br>jkl</div>",
expected: useBR
? "abc<br>def<br>ghi<br>jkl"
: ["abc<br>def<br>ghi<br>jkl",
"abc\ndef\nghi\njkl"],
},
{
// <noscript> content should not be inserted
insertHTML: "<noscript>no script</noscript>",
expected: "",
},
{
// <noframes> content should not be inserted
insertHTML: "<noframes>no frames</noframes>",
expected: "",
},
{
// <script> content should not be inserted
insertHTML: `<script>script</${"script"}>`,
expected: "",
},
{
// <style> content should not be inserted
insertHTML: "<style>style</style>",
expected: "",
},
{
// <head> content should not be inserted
insertHTML: "<html><head><title>title</title></head><body>body</body></html>",
expected: "body",
},
{
// white-spaces should be collapsed
insertHTML: "plain text",
expected: "plain text",
},
{
// white-spaces should be collapsed
insertHTML: "<span>plain text</span>",
expected: "plain text",
},
{
// preformatted white-spaces should not be collapsed
insertHTML: "<pre>plain text</pre>",
expected: !collapseWhiteSpaces
? "plain text"
: ["plain &nbsp;text", "plain&nbsp; text", "plain&nbsp;&nbsp;text",
"plain \u00A0text", "plain\u00A0 text", "plain\u00A0\u00A0text"],
},
{
// even if inserting HTML is empty, selected text should be deleted
initialInnerHTML: "A[B]C",
insertHTML: "",
expected: "AC",
},
]) {
test(() => {
utils.setupEditingHost(data.initialInnerHTML ? data.initialInnerHTML : "");
document.execCommand("insertHTML", false, data.insertHTML);
if (Array.isArray(data.expected)) {
assert_in_array(editingHost.innerHTML, data.expected);
} else {
assert_equals(editingHost.innerHTML, data.expected);
}
}, `execCommand("insertHTML", false, "${data.insertHTML}") when "${
data.initialInnerHTML ? data.initialInnerHTML : ""
}"`);
}
}, {once: true});
</script>
</head>
<body></body>
</html>