Bug 1880710 - Make `HTMLEditor::IsEmpty` check whether the editing host is empty r=m_kato

It checks whether the document body itself is empty or not.  However, if there
is only a shadow DOM hosts, it considers the content is empty.  Therefore,
`HTMLEditor::HandleDeleteSelection` does nothing.

I think that it should check whether current editing host is empty or not.
That does not work without selection ranges, but I think it's fine in most
cases.

Differential Revision: https://phabricator.services.mozilla.com/D202274
This commit is contained in:
Masayuki Nakano 2024-02-22 07:32:18 +00:00
Родитель 381a551e0f
Коммит 3719643bde
4 изменённых файлов: 106 добавлений и 8 удалений

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

@ -5949,14 +5949,23 @@ bool HTMLEditor::IsEmpty() const {
return true;
}
// XXX Oddly, we check body or document element's state instead of
// active editing host. Must be a bug.
Element* bodyOrDocumentElement = GetRoot();
if (!bodyOrDocumentElement) {
return true;
const Element* activeElement =
GetDocument() ? GetDocument()->GetActiveElement() : nullptr;
const Element* editingHostOrBodyOrRootElement =
activeElement && activeElement->IsEditable()
? ComputeEditingHost(*activeElement, LimitInBodyElement::No)
: ComputeEditingHost(LimitInBodyElement::No);
if (MOZ_UNLIKELY(!editingHostOrBodyOrRootElement)) {
// If there is no active element nor no selection range in the document,
// let's check entire the document as what we do traditionally.
editingHostOrBodyOrRootElement = GetRoot();
if (!editingHostOrBodyOrRootElement) {
return true;
}
}
for (nsIContent* childContent = bodyOrDocumentElement->GetFirstChild();
for (nsIContent* childContent =
editingHostOrBodyOrRootElement->GetFirstChild();
childContent; childContent = childContent->GetNextSibling()) {
if (!childContent->IsText() || childContent->Length()) {
return false;

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

@ -80,7 +80,7 @@
ok(true, "nsIEditor.documentIsEmpty should throw an exception when no editing host has focus");
}
document.querySelector("div[contenteditable]").focus();
todo_is(getHTMLEditor().documentIsEmpty, true,
is(getHTMLEditor().documentIsEmpty, true,
"nsIEditor.documentIsEmpty should be true when editing host does not have contents");
document.body.innerHTML = "<div contenteditable><br></div>";

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

@ -32,7 +32,15 @@ class EditorTestUtils {
sendKey(key, modifier) {
if (!modifier) {
return this.window.test_driver.send_keys(this.editingHost, key)
// send_keys requires element in the light DOM.
const elementInLightDOM = (e => {
const doc = e.ownerDocument;
while (e.getRootNode({composed:false}) !== doc) {
e = e.getRootNode({composed:false}).host;
}
return e;
})(this.editingHost);
return this.window.test_driver.send_keys(elementInLightDOM, key)
.catch(() => {
return new this.window.test_driver.Actions()
.keyDown(key)

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

@ -0,0 +1,81 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Delete editor in a shadow</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="../include/editor-test-utils.js"></script>
<script>
"use strict";
addEventListener("load", () => {
const shadowRoot = document.body.attachShadow({mode: "open"});
for (const tag of ["input", "textarea"]) {
promise_test(async t => {
const textControl = document.createElement(tag);
textControl.value = "text";
shadowRoot.appendChild(textControl);
textControl.focus();
textControl.selectionStart = textControl.value.length;
const utils = new EditorTestUtils(textControl);
await utils.sendBackspaceKey();
assert_equals(
textControl.value,
"tex",
`Backspace in ${t.name} should delete character before the caret`
);
textControl.value = "text";
textControl.selectionStart = textControl.selectionEnd = 0;
await utils.sendDeleteKey();
assert_equals(
textControl.value,
"ext",
`Delete in ${t.name} should delete character after the caret`
);
textControl.value = "text";
textControl.select();
await utils.sendBackspaceKey();
assert_equals(
textControl.value,
"",
`Backspace after selecting all text in ${t.name} should delete all text`
);
}, `<${tag}> in shadow of the <body>`);
}
promise_test(async t => {
const editingHost = document.createElement("div");
editingHost.setAttribute("contenteditable", "");
shadowRoot.appendChild(editingHost);
const utils = new EditorTestUtils(editingHost);
utils.setupEditingHost("text[]");
await utils.sendBackspaceKey();
assert_equals(
editingHost.textContent,
"tex",
`Backspace in ${t.name} should delete character before the caret`
);
utils.setupEditingHost("[]text");
await utils.sendDeleteKey();
assert_equals(
editingHost.textContent,
"ext",
`Delete in ${t.name} should delete character after the caret`
);
utils.setupEditingHost("[text]");
await utils.sendBackspaceKey();
assert_equals(
editingHost.textContent,
"",
`Backspace after selecting all text in ${t.name} should delete all text`
);
}, "<div contenteditable> in shadow of the <body>");
}, {once: true});
</script>
</head>
<body></body>
</html>