mirror of
https://github.com/HeyPuter/firefox.git
synced 2026-11-13 20:14:57 -10:00
`nsIEditor.setShouldTxnSetSelection` can preserve selection across multiple editing. Therefore, we cannot manage the state only in the stack. It's used only in comm-central, and used only with `insertNode` and `deleteNode`. Therefore, adding new param to them to preserve selection must be enough. While I'm writing this patch, I realized that `input` event is not fired by these methods because nobody set a placeholder transaction. That may lead Thunderbird only IME crash bugs due to `IMEContentObserver` is not notified editor properly. Therefore, this may fix some Thunderbird only crashes. Note that `deleteNode` should not update selection. However, I'm not 100% sure that. Therefore, I add new param to `deleteNode` too. However, some reviewers think it's unnecessary, I'll remove it before landing. Finally, `beforeinput` and `input` caused by the method calls start updating selection. However, I think that it should be better behavior. If Thunderbird needs to guarantee that selection is set to whether the user expected when it calls these methods with preserving selection. Differential Revision: https://phabricator.services.mozilla.com/D196004
215 lines
6.7 KiB
HTML
215 lines
6.7 KiB
HTML
<!doctype>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>nsIEditor.insertNode</title>
|
|
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
|
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
|
|
<script>
|
|
"use strict";
|
|
|
|
function stringifyInputEvent(aEvent) {
|
|
if (!aEvent) {
|
|
return "null";
|
|
}
|
|
return `${aEvent.type}: { inputType=${aEvent.inputType} }`;
|
|
}
|
|
|
|
function getRangeDescription(range) {
|
|
function getNodeDescription(node) {
|
|
if (!node) {
|
|
return "null";
|
|
}
|
|
switch (node.nodeType) {
|
|
case Node.TEXT_NODE:
|
|
return `${node.nodeName} "${node.data}"`;
|
|
case Node.ELEMENT_NODE:
|
|
return `<${node.nodeName.toLowerCase()}>`;
|
|
default:
|
|
return `${node.nodeName}`;
|
|
}
|
|
}
|
|
if (range === null) {
|
|
return "null";
|
|
}
|
|
if (range === undefined) {
|
|
return "undefined";
|
|
}
|
|
return range.startContainer == range.endContainer &&
|
|
range.startOffset == range.endOffset
|
|
? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
|
|
: `(${getNodeDescription(range.startContainer)}, ${
|
|
range.startOffset
|
|
}) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
|
|
}
|
|
|
|
SimpleTest.waitForExplicitFinish();
|
|
SimpleTest.waitForFocus(() => {
|
|
const editingHost = document.querySelector("div[contenteditable]");
|
|
const editor =
|
|
SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
|
|
|
|
editingHost.focus();
|
|
|
|
let events = [];
|
|
editingHost.addEventListener("input", event => events.push(event));
|
|
|
|
(function test_insert_text_to_start() {
|
|
editor.insertNode(document.createTextNode("abc"), editingHost, 0);
|
|
is(
|
|
editingHost.innerHTML,
|
|
"abc<br>",
|
|
"test_insert_text_to_start: insertNode() should insert new text node at start of the container"
|
|
);
|
|
is(
|
|
events.length,
|
|
1,
|
|
"test_insert_text_to_start: Only one input event should be fired when insertNode() inserts a text node"
|
|
);
|
|
is(
|
|
stringifyInputEvent(events[0]),
|
|
stringifyInputEvent({ type: "input", inputType: "" }),
|
|
"test_insert_text_to_start: input event should be fired when inserting a node"
|
|
);
|
|
is(
|
|
getRangeDescription(getSelection().getRangeAt(0)),
|
|
getRangeDescription({
|
|
startContainer: editingHost,
|
|
startOffset: 1,
|
|
endContainer: editingHost,
|
|
endOffset: 1,
|
|
}),
|
|
"test_insert_text_to_start: insertNode() should collapse selection after the inserted text node"
|
|
);
|
|
})();
|
|
|
|
(function test_insert_span_to_big_index() {
|
|
events = [];
|
|
editingHost.innerHTML = "abc";
|
|
const span = document.createElement("span");
|
|
span.textContent = "def";
|
|
editor.insertNode(span, editingHost, 1000);
|
|
is(
|
|
editingHost.innerHTML,
|
|
"abc<span>def</span>",
|
|
"test_insert_span_to_big_index: insertNode() with big index should insert new node at end of the container"
|
|
);
|
|
is(
|
|
events.length,
|
|
1,
|
|
"test_insert_span_to_big_index: Only one input event should be fired when insertNode() inserts a node"
|
|
);
|
|
is(
|
|
stringifyInputEvent(events[0]),
|
|
stringifyInputEvent({ type: "input", inputType: "" }),
|
|
"test_insert_span_to_big_index: input event should be fired when inserting a node"
|
|
);
|
|
is(
|
|
getRangeDescription(getSelection().getRangeAt(0)),
|
|
getRangeDescription({
|
|
startContainer: editingHost,
|
|
startOffset: 2,
|
|
endContainer: editingHost,
|
|
endOffset: 2,
|
|
}),
|
|
"test_insert_span_to_big_index: insertNode() should collapse selection after the inserted node"
|
|
);
|
|
})();
|
|
|
|
(function test_preserve_selection() {
|
|
events = [];
|
|
editingHost.innerHTML = "abc";
|
|
const span = document.createElement("span");
|
|
span.textContent = "def";
|
|
getSelection().collapse(editingHost, 0);
|
|
editor.insertNode(span, editingHost, 1, true);
|
|
is(
|
|
editingHost.innerHTML,
|
|
"abc<span>def</span>",
|
|
"test_preserve_selection: insertNode() should insert new node at end of the container"
|
|
);
|
|
is(
|
|
events.length,
|
|
1,
|
|
"test_preserve_selection: Only one input event should be fired when insertNode() inserts a node"
|
|
);
|
|
is(
|
|
stringifyInputEvent(events[0]),
|
|
stringifyInputEvent({ type: "input", inputType: "" }),
|
|
"test_preserve_selection: input event should be fired when inserting a node"
|
|
);
|
|
is(
|
|
getRangeDescription(getSelection().getRangeAt(0)),
|
|
getRangeDescription({
|
|
startContainer: editingHost,
|
|
startOffset: 0,
|
|
endContainer: editingHost,
|
|
endOffset: 0,
|
|
}),
|
|
"test_preserve_selection: insertNode() should not collapse selection after the inserted node"
|
|
);
|
|
})();
|
|
|
|
(function test_not_preserve_selection_nested_by_beforeinput() {
|
|
editingHost.innerHTML = "abc";
|
|
const span1 = document.createElement("span");
|
|
span1.textContent = "def";
|
|
const span2 = document.createElement("span");
|
|
span2.textContent = "ghi";
|
|
getSelection().collapse(editingHost, 0);
|
|
editingHost.addEventListener("beforeinput", () => {
|
|
editor.insertNode(span1, editingHost, 1);
|
|
}, {once: true});
|
|
editor.insertNode(span2, editingHost, 2, true);
|
|
is(
|
|
editingHost.innerHTML,
|
|
"abc<span>def</span><span>ghi</span>",
|
|
"test_not_preserve_selection_nested_by_beforeinput: both insertNode() should work"
|
|
);
|
|
is(
|
|
getRangeDescription(getSelection().getRangeAt(0)),
|
|
getRangeDescription({
|
|
startContainer: editingHost,
|
|
startOffset: 2,
|
|
endContainer: editingHost,
|
|
endOffset: 2,
|
|
}),
|
|
"test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection"
|
|
);
|
|
})();
|
|
|
|
(function test_not_preserve_selection_nested_by_input() {
|
|
editingHost.innerHTML = "abc";
|
|
const span1 = document.createElement("span");
|
|
span1.textContent = "def";
|
|
const span2 = document.createElement("span");
|
|
span2.textContent = "ghi";
|
|
getSelection().collapse(editingHost, 0);
|
|
editingHost.addEventListener("input", () => {
|
|
editor.insertNode(span2, editingHost, 2);
|
|
}, {once: true});
|
|
editor.insertNode(span1, editingHost, 1, true);
|
|
is(
|
|
editingHost.innerHTML,
|
|
"abc<span>def</span><span>ghi</span>",
|
|
"test_not_preserve_selection_nested_by_input: both insertNode() should work"
|
|
);
|
|
is(
|
|
getRangeDescription(getSelection().getRangeAt(0)),
|
|
getRangeDescription({
|
|
startContainer: editingHost,
|
|
startOffset: 3,
|
|
endContainer: editingHost,
|
|
endOffset: 3,
|
|
}),
|
|
"test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection"
|
|
);
|
|
})();
|
|
|
|
SimpleTest.finish();
|
|
});
|
|
</script>
|
|
</head>
|
|
<body><div contenteditable><br></div></body>
|
|
</html>
|