Bug 1789967 - part 4: Make HTMLEditor::SelectAllInternal work without selection range r=m_kato

It may be called even when there is no selection range and focused element.
However, it assumes that there is a selection range, and an editable element
has focus.  Therefore, now, if there is an editing host and user tries to
do "Select All" without clicking somewhere before doing it, "Select All" does
nothing.

Differential Revision: https://phabricator.services.mozilla.com/D157409
This commit is contained in:
Masayuki Nakano 2022-09-22 06:27:38 +00:00
parent a353ab7e90
commit fc89971ea6
5 changed files with 232 additions and 42 deletions

View File

@ -4338,52 +4338,84 @@ nsresult HTMLEditor::SelectAllInternal() {
return NS_ERROR_EDITOR_DESTROYED;
}
// XXX Perhaps, we should check whether we still have focus since composition
// event listener may have already moved focus to different editing
// host or other element. So, perhaps, we need to retrieve anchor node
// before committing composition and check if selection is still in
// same editing host.
nsINode* anchorNode = SelectionRef().GetAnchorNode();
if (NS_WARN_IF(!anchorNode) || NS_WARN_IF(!anchorNode->IsContent())) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIContent> anchorContent = anchorNode->AsContent();
nsCOMPtr<nsIContent> rootContent;
if (anchorContent->HasIndependentSelection()) {
SelectionRef().SetAncestorLimiter(nullptr);
rootContent = mRootElement;
if (NS_WARN_IF(!rootContent)) {
return NS_ERROR_UNEXPECTED;
auto GetBodyElementIfElementIsParentOfHTMLBody =
[](const Element& aElement) -> Element* {
if (!aElement.OwnerDoc()->IsHTMLDocument()) {
return const_cast<Element*>(&aElement);
}
} else {
RefPtr<PresShell> presShell = GetPresShell();
rootContent = anchorContent->GetSelectionRootContent(presShell);
if (NS_WARN_IF(!rootContent)) {
return NS_ERROR_UNEXPECTED;
}
// If the document is HTML document (not XHTML document), we should
// select all children of the `<body>` element instead of `<html>`
// element.
if (Document* document = GetDocument()) {
if (document->IsHTMLDocument()) {
if (HTMLBodyElement* bodyElement = document->GetBodyElement()) {
if (nsContentUtils::ContentIsFlattenedTreeDescendantOf(bodyElement,
rootContent)) {
rootContent = bodyElement;
HTMLBodyElement* bodyElement = aElement.OwnerDoc()->GetBodyElement();
return bodyElement && nsContentUtils::ContentIsFlattenedTreeDescendantOf(
bodyElement, &aElement)
? bodyElement
: const_cast<Element*>(&aElement);
};
nsCOMPtr<nsIContent> selectionRootContent =
[&]() MOZ_CAN_RUN_SCRIPT -> nsIContent* {
RefPtr<Element> elementToBeSelected = [&]() -> Element* {
// If there is at least one selection range, we should compute the
// selection root from the anchor node.
if (SelectionRef().RangeCount()) {
if (nsIContent* content =
nsIContent::FromNodeOrNull(SelectionRef().GetAnchorNode())) {
if (content->IsElement()) {
return content->AsElement();
}
if (Element* parentElement =
content->GetParentElementCrossingShadowRoot()) {
return parentElement;
}
}
}
// If no element contains a selection range, we should select all children
// of the focused element at least.
if (Element* focusedElement = GetFocusedElement()) {
return focusedElement;
}
// of the body or document element.
Element* bodyOrDocumentElement = GetRoot();
NS_WARNING_ASSERTION(bodyOrDocumentElement,
"There was no element in the document");
return bodyOrDocumentElement;
}();
// If the element to be selected is <input type="text"> or <textarea>,
// GetSelectionRootContent() returns its anonymous <div> element, but we
// want to select all of the document or selection limiter. Therefore,
// we should use its parent to compute the selection root.
if (elementToBeSelected->HasIndependentSelection()) {
Element* parentElement = elementToBeSelected->GetParentElement();
if (MOZ_LIKELY(parentElement)) {
elementToBeSelected = parentElement;
}
}
// Then, compute the selection root content to select all including
// elementToBeSelected.
RefPtr<PresShell> presShell = GetPresShell();
nsIContent* computedSelectionRootContent =
elementToBeSelected->GetSelectionRootContent(presShell);
if (NS_WARN_IF(!computedSelectionRootContent)) {
return nullptr;
}
if (MOZ_UNLIKELY(!computedSelectionRootContent->IsElement())) {
return computedSelectionRootContent;
}
return GetBodyElementIfElementIsParentOfHTMLBody(
*computedSelectionRootContent->AsElement());
}();
if (NS_WARN_IF(!selectionRootContent)) {
return NS_ERROR_FAILURE;
}
Maybe<Selection::AutoUserInitiated> userSelection;
if (!rootContent->IsEditable()) {
// XXX Do we need to mark it as "user initiated" for
// `Document.execCommand("selectAll")`?
if (!selectionRootContent->IsEditable()) {
userSelection.emplace(SelectionRef());
}
ErrorResult error;
SelectionRef().SelectAllChildren(*rootContent, error);
SelectionRef().SelectAllChildren(*selectionRootContent, error);
NS_WARNING_ASSERTION(!error.Failed(),
"Selection::SelectAllChildren() failed");
return error.StealNSResult();

View File

@ -10,10 +10,6 @@
</style>
<script>
window.onload = () => {
getSelection().collapse(
document.querySelector("dl"),
document.querySelector("dl").childNodes.length
);
document.execCommand("selectAll");
document.execCommand("selectAll");
document.execCommand("backColor", false, "r");

View File

@ -1,3 +0,0 @@
[selectall-without-focus.html]
[execCommand('selectAll') should select all content in the document even if the document body ends with editable content]
expected: FAIL

View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>Select All in focused editor</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";
addEventListener("DOMContentLoaded", () => {
const editingHost = document.querySelector("div[contenteditable]");
test(() => {
editingHost.focus();
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_false(
rangeText.includes("preceding text"),
"Selection should not contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("editable text"),
"Selection should contain the editable text in the editing host"
);
assert_false(
rangeText.includes("following text"),
"Selection should not contain the following text of the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the editing host");
test(() => {
editingHost.focus();
getSelection().removeAllRanges();
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_false(
rangeText.includes("preceding text"),
"Selection should not contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("editable text"),
"Selection should contain the editable text in the editing host"
);
assert_false(
rangeText.includes("following text"),
"Selection should not contain the following text of the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the editing host when it has focus but no selection range");
test(() => {
editingHost.focus();
editingHost.innerHTML = "preceding editable text<input value='input value'>following editable text";
getSelection().collapse(editingHost.querySelector("input"), 0);
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_false(
rangeText.includes("preceding text"),
"Selection should not contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("preceding editable text"),
"Selection should contain the preceding editable text of <input> in the editing host"
);
assert_true(
rangeText.includes("following editable text"),
"Selection should contain the following editable text of <input> in the editing host"
);
assert_false(
rangeText.includes("following text"),
"Selection should not contain the following text of the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the editing host when selection collapsed in the <input>");
test(() => {
editingHost.focus();
editingHost.innerHTML = "preceding editable text<textarea>textarea value</textarea>following editable text";
getSelection().collapse(editingHost.querySelector("textarea"), 0);
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_false(
rangeText.includes("preceding text"),
"Selection should not contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("preceding editable text"),
"Selection should contain the preceding editable text of <textarea> in the editing host"
);
assert_true(
rangeText.includes("following editable text"),
"Selection should contain the following editable text of <textarea> in the editing host"
);
assert_false(
rangeText.includes("following text"),
"Selection should not contain the following text of the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the editing host when selection collapsed in the <textarea>");
});
</script>
</head>
<body>
<p>preceding text</p>
<div contenteditable>editable text</div>
<p>following text</p>
</body>
</html>

View File

@ -26,6 +26,46 @@ addEventListener("DOMContentLoaded", () => {
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the document even if the document body ends with editable content");
test(() => {
document.querySelector("p").innerHTML = "preceding text <input value='input value'>";
getSelection().collapse(document.querySelector("input"), 0);
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_true(
rangeText.includes("preceding text"),
"Selection should contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("editable text"),
"Selection should contain the editable text in the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the document when selection is in <input>");
test(() => {
document.querySelector("p").innerHTML = "preceding text <textarea>textarea value</textarea>";
getSelection().collapse(document.querySelector("textarea"), 0);
document.execCommand("selectAll");
assert_false(
getSelection().isCollapsed,
'Selection should not be collapsed after calling document.execCommand("selectAll")'
);
const rangeText = getSelection().toString();
assert_true(
rangeText.includes("preceding text"),
"Selection should contain the preceding text of the editing host"
);
assert_true(
rangeText.includes("editable text"),
"Selection should contain the editable text in the editing host"
);
getSelection().removeAllRanges();
}, "execCommand('selectAll') should select all content in the document when selection is in <textarea>");
});
</script>
</head>