Skip to content

Commit

Permalink
Bug 1789967 - part 3: Make `HTMLEditor::CollapseSelectionToEndOfLastL…
Browse files Browse the repository at this point in the history
…eafNodeOfDocument` and `HTMLEditor::InitEditorContentAndSelection` do nothing if the document is partially editable r=m_kato

They and their callees work with the result of `GetRoot()` which is the document
element or the body element.  If the body is not editable, `Selection` should
not be updated in non-editable region nor `<br>` elements should not be
inserted in both non-focused editable elements and non-editable elements.
Therefore, they should run only when the document element or the `<body>`
element is editable.

To keep testing crashtests as reported, this patch makes tests which have
`contenteditable` except `<html>` and `<body>` initialize `Selection` as
what we've done.  And clean up the tests for helping to port them to WPT
in the future (bug 1725850).

Differential Revision: https://phabricator.services.mozilla.com/D157408
  • Loading branch information
masayuki-nakano committed Sep 22, 2022
1 parent b1f3f5e commit a353ab7
Show file tree
Hide file tree
Showing 63 changed files with 1,005 additions and 578 deletions.
2 changes: 1 addition & 1 deletion accessible/tests/mochitest/textcaret/test_general.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
turnCaretBrowsing(true);

// test caret offsets
testCaretOffset(document, 15);
testCaretOffset(document, 0); // because of no selection ranges
testCaretOffset("textbox", -1);
testCaretOffset("textarea", -1);
testCaretOffset("p", -1);
Expand Down
85 changes: 55 additions & 30 deletions dom/base/test/test_bug1101364.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,67 @@
<pre id="test">
<script class="testbody" type="text/javascript">

async function test()
{
var iframe1 = document.getElementById('test1');
iframe1.focus();
var docShell = SpecialPowers.wrap(iframe1.contentWindow).docShell;

// test1
docShell.doCommand("cmd_selectAll");
var withoutContenteditable = await snapshotWindow(iframe1.contentWindow);
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(async () => {
await (async () => {
const iframe = document.getElementById("test1");
iframe.focus();
const docShell = SpecialPowers.wrap(iframe.contentWindow).docShell;

iframe1.contentDocument.getElementById('testDiv').setAttribute('contentEditable', true);
docShell.doCommand("cmd_selectAll");
var withContenteditable = await snapshotWindow(iframe1.contentWindow);
dump(withoutContenteditable.toDataURL());
dump(withContenteditable.toDataURL());
docShell.doCommand("cmd_selectAll");
info(
"Waiting for getting screenshot of \"Select All\" without contenteditable..."
);
const withoutContenteditable = await snapshotWindow(iframe.contentWindow);

ok(compareSnapshots(withoutContenteditable, withContenteditable, true)[0], 'Select all should look identical');
iframe.contentDocument
.getElementById("testDiv")
.setAttribute("contentEditable", true);
docShell.doCommand("cmd_selectAll");
info(
"Waiting for getting screenshot of \"Select All\" in contenteditable..."
);
const withContenteditable = await snapshotWindow(iframe.contentWindow);
const result =
compareSnapshots(withoutContenteditable, withContenteditable, true);
ok(
result[0],
`Select all should look identical\ngot: ${
result[2]
}\nexpected: ${result[1]}`
);
})();

// test2
var iframe2 = document.getElementById('test2');
iframe2.focus();
var docShell = SpecialPowers.wrap(iframe2.contentWindow).docShell;
var test2Inner = iframe2.contentDocument.getElementById('test2Inner');
test2Inner.style.MozUserSelect = 'text';
docShell.doCommand("cmd_selectAll");
var withoutUserSelect = await snapshotWindow(iframe2.contentWindow);
await (async () => {
const iframe = document.getElementById("test2");
iframe.focus();
iframe.contentDocument.querySelector("div[contenteditable]").focus();
const docShell = SpecialPowers.wrap(iframe.contentWindow).docShell;
const test2Inner = iframe.contentDocument.getElementById("test2Inner");
test2Inner.style.MozUserSelect = "text";
docShell.doCommand("cmd_selectAll");
info(
"Waiting for getting screenshot of \"Select All\" in contenteditable (use-select: text)..."
);
const withoutUserSelect = await snapshotWindow(iframe.contentWindow);

test2Inner.style.MozUserSelect = 'none';
docShell.doCommand("cmd_selectAll");
var withUserSelect = await snapshotWindow(iframe2.contentWindow);
ok(compareSnapshots(withoutUserSelect, withUserSelect, true)[0], 'Editable fields should ignore user select style');
test2Inner.style.MozUserSelect = "none";
docShell.doCommand("cmd_selectAll");
info(
"Waiting for getting screenshot of \"Select All\" in contenteditable (use-select: none)..."
);
const withUserSelect = await snapshotWindow(iframe.contentWindow);
const result = compareSnapshots(withoutUserSelect, withUserSelect, true);
ok(
result[0],
`Editable fields should ignore user select style\ngot: ${
result[2]
}\nexpected: ${result[1]}`
);
})();

SimpleTest.finish();
}
window.onload = function() { setTimeout(test, 0); };
SimpleTest.waitForExplicitFinish();
});
</script>
</pre>
</body>
Expand Down
12 changes: 10 additions & 2 deletions editor/libeditor/HTMLEditSubActionHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ HTMLEditor::CreateRangeIncludingAdjuscentWhiteSpaces(
nsresult HTMLEditor::InitEditorContentAndSelection() {
MOZ_ASSERT(IsEditActionDataAvailable());

// We should do nothing with the result of GetRoot() if only a part of the
// document is editable.
if (!EntireDocumentIsEditable()) {
return NS_OK;
}

nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
if (NS_FAILED(rv)) {
NS_WARNING(
Expand All @@ -141,8 +147,8 @@ nsresult HTMLEditor::InitEditorContentAndSelection() {
// If the selection hasn't been set up yet, set it up collapsed to the end of
// our editable content.
// XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
// removed by the web app and if they call `Selection::AddRange()`,
// it may cause multiple selection ranges.
// removed by the web app and if they call `Selection::AddRange()` without
// checking the range count, it may cause multiple selection ranges.
if (!SelectionRef().RangeCount()) {
nsresult rv = CollapseSelectionToEndOfLastLeafNodeOfDocument();
if (NS_FAILED(rv)) {
Expand All @@ -154,6 +160,8 @@ nsresult HTMLEditor::InitEditorContentAndSelection() {
}

if (IsInPlaintextMode()) {
// XXX Should we do this in HTMLEditor? It's odd to guarantee that last
// empty line is visible only when it's in the plain text mode.
nsresult rv = EnsurePaddingBRElementInMultilineEditor();
if (NS_FAILED(rv)) {
NS_WARNING(
Expand Down
13 changes: 13 additions & 0 deletions editor/libeditor/HTMLEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,13 @@ bool HTMLEditor::IsInDesignMode() const {
return document && document->IsInDesignMode();
}

bool HTMLEditor::EntireDocumentIsEditable() const {
Document* document = GetDocument();
return document && document->GetDocumentElement() &&
(document->GetDocumentElement()->IsEditable() ||
(document->GetBody() && document->GetBody()->IsEditable()));
}

void HTMLEditor::CreateEventListeners() {
// Don't create the handler twice
if (!mEventListener) {
Expand Down Expand Up @@ -807,6 +814,12 @@ NS_IMETHODIMP HTMLEditor::EndOfDocument() {
nsresult HTMLEditor::CollapseSelectionToEndOfLastLeafNodeOfDocument() const {
MOZ_ASSERT(IsEditActionDataAvailable());

// We should do nothing with the result of GetRoot() if only a part of the
// document is editable.
if (!EntireDocumentIsEditable()) {
return NS_OK;
}

RefPtr<Element> bodyOrDocumentElement = GetRoot();
if (NS_WARN_IF(!bodyOrDocumentElement)) {
return NS_ERROR_NULL_POINTER;
Expand Down
9 changes: 8 additions & 1 deletion editor/libeditor/HTMLEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -667,10 +667,17 @@ class HTMLEditor final : public EditorBase,
}

/**
* Retruns true if we're in designMode.
* Return true if we're in designMode.
*/
bool IsInDesignMode() const;

/**
* Return true if entire the document is editable (although the document
* may have non-editable nodes, e.g.,
* <body contenteditable><div contenteditable="false"></div></body>
*/
bool EntireDocumentIsEditable() const;

/**
* Basically, this always returns true if we're for `contenteditable` or
* `designMode` editor in web apps. However, e.g., Composer of SeaMonkey
Expand Down
25 changes: 13 additions & 12 deletions editor/libeditor/crashtests/1134545.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@
<!-- saved from url=(0065)https://bug1134545.bugzilla.mozilla.org/attachment.cgi?id=8566418 -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<script>

function boom()
{
textNode = document.createTextNode(" ");
x.appendChild(textNode);
x.setAttribute('contenteditable', "true");
textNode.remove();
window.getSelection().selectAllChildren(textNode);
document.execCommand("increasefontsize", false, null);
function onLoad() {
// For emulating the traditional behavior, collapse Selection to end of the
// <body>, i.e., at the text node after the <div contenteditable>.
getSelection().collapse(document.body, document.body.childNodes.length);
const textNode = document.createTextNode(" ");
const editingHost = document.querySelector("div[contenteditable]");
editingHost.appendChild(textNode);
editingHost.setAttribute('contenteditable', "true");
textNode.remove();
getSelection().selectAllChildren(textNode);
document.execCommand("increasefontsize");
}

</script>
</head>
<body onload="boom();">
<div id="x" contenteditable="true"></div>
<body onload="onLoad();">
<div contenteditable></div>


</body></html>
13 changes: 8 additions & 5 deletions editor/libeditor/crashtests/1274050.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<body>
<table contenteditable="true"></table>
<table contenteditable></table>
</body>
<script>
window.onload = function() {
document.execCommand("useCSS", false, false);
document.designMode = 'on';
document.execCommand("insertunorderedlist", false);
document.execCommand("justifyfull", false);
// For emulating the traditional behavior, collapse Selection to end of the
// <body>, i.e., at the text node after the <table>.
getSelection().collapse(document.body, document.body.childNodes.length);
document.execCommand("styleWithCSS", false, false);
document.designMode = "on";
document.execCommand("insertUnorderedList");
document.execCommand("justifyFull");
};
</script>
27 changes: 15 additions & 12 deletions editor/libeditor/crashtests/1317704.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
<head>
<meta charset="utf-8">
<script>
addEventListener('DOMContentLoaded', function(){
document.documentElement.className = 'lizard';
setTimeout(function(){
document.execCommand('selectAll', false, null);
document.designMode = 'on';
document.execCommand('removeformat', false, null);
}, 0);
addEventListener('DOMContentLoaded', () => {
// For emulating the traditional behavior, collapse Selection to end of the
// <body>, i.e., at the text node after the last invalid </span>.
getSelection().collapse(document.body, document.body.childNodes.length);
document.documentElement.className = 'lizard';
setTimeout(() => {
document.execCommand('selectAll');
document.designMode = 'on';
document.execCommand('removeformat');
}, 0);
});
</script>
<style>
.lizard{
-webkit-user-select:all;
.lizard {
-webkit-user-select: all;
}
*{
position:fixed;
display:table-column;
* {
position: fixed;
display: table-column;
}
</style>
</head>
Expand Down
79 changes: 58 additions & 21 deletions editor/libeditor/crashtests/1388075.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
<html>
<head>
<script>
try { o1 = document.createElement('style'); } catch(e) { }
try { o2 = document.createElement('output') } catch(e) { }
try { o3 = document.createElement('input') } catch(e) { }
try { o4 = document.createElement('script'); } catch(e) { }
try { o5 = o2.cloneNode(false); } catch(e) { }
try { document.documentElement.appendChild(o1) } catch(e) { }
try { o1.outerHTML = '<a contenteditable=\'true\'>'; } catch(e) { }
try { document.documentElement.appendChild(o3) } catch(e) { }
try { o7 = document.createTextNode(' '); } catch(e) { }
try { o4.appendChild(o7) } catch(e) { }
try { document.documentElement.appendChild(o4) } catch(e) { }
try { o6 = window.getSelection() } catch(e) { }
try { o3.select() } catch(e) { }
try { document.replaceChild(document.documentElement, document.documentElement); } catch(e) { }
try { o6.setBaseAndExtent(o7, 0, o5, 0) } catch(e) { }
try { document.designMode = 'on'; } catch(e) { }
try { document.execCommand('insertimage', false, 'http://localhost/') } catch(e) { }
</script>
</head>
<head>
<script>
try {
var style = document.createElement("style");
} catch(e) {}
try {
var output = document.createElement("output");
} catch(e) {}
try {
var input = document.createElement("input");
} catch(e) {}
try {
var script = document.createElement("script");
} catch(e) {}
try {
var clonedOutput = output.cloneNode(false);
} catch(e) {}
try {
document.documentElement.appendChild(style);
} catch(e) {}
try {
style.outerHTML = '<a contenteditable="true">';
} catch(e) {}
// For emulating the traditional behavior, collapse Selection to end of the
// <body> which must be empty (<style> was added after the <body>).
getSelection().collapse(document.body, document.body.childNodes.length);
try {
document.documentElement.appendChild(input);
} catch(e) {}
try {
var text = document.createTextNode(" ");
} catch(e) {}
try {
script.appendChild(text);
} catch(e) {}
try {
document.documentElement.appendChild(script);
} catch(e) {}
try {
var selection = getSelection();
} catch(e) {}
try {
input.select();
} catch(e) {}
try {
document.replaceChild(document.documentElement, document.documentElement);
} catch(e) {}
try {
selection.setBaseAndExtent(text, 0, clonedOutput, 0);
} catch(e) {}
try {
document.designMode = "on";
} catch(e) {}
try {
document.execCommand("insertImage", false, "http://localhost/");
} catch(e) {}
</script>
</head>
</html>
Loading

0 comments on commit a353ab7

Please sign in to comment.