-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make
AdjustCaretFrameForLineEnd
stop scanning text node if first no…
…de is editable but reached non-editable one When pressing `ArrowLeft` key at: ``` <p>abc</p><p><span contenteditable=false>def</span>{}<br></p> ``` , caret is moved into the non-editable text because `nsCaret::GetCaretFrameForNodeOffset` looks for a preceding text node from the `BRFrame`. This is required if the preceding text node ends with a collapsible white-space but followed by a `<br>` because the text frame should not contain the white-space rect and `BRFrame` frame should be next to it, i.e., the white-space looks like a overflown content. So, for rendering a caret, the method needs to return non-editable text frame even in the case, but for considering new caret position in the DOM tree, it should not return non-editable text frame. Therefore, this patch adds a param to `nsCaret::GetCaretFrameForNodeOffset()` which forcibly return editable content frame or not and makes `Selection::GetPrimaryOrCaretFrameForNodeOffset` call it with the new option because its callers are the handler of caret navigation. Differential Revision: https://phabricator.services.mozilla.com/D196259 bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1612076 gecko-commit: 7e32f178b02fef938f803492c4bfd6d897ffcb21 gecko-reviewers: emilio
- Loading branch information
1 parent
967023d
commit 1cea438
Showing
1 changed file
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
<!doctype html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Don't move caret to non-editable node from a editable node</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> | ||
"use strict"; | ||
|
||
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})`; | ||
} | ||
|
||
function sendArrowRightKey() { | ||
const kArrowRight = "\uE014"; | ||
return new test_driver.Actions() | ||
.keyDown(kArrowRight) | ||
.keyUp(kArrowRight) | ||
.send(); | ||
} | ||
|
||
function sendArrowLeftKey() { | ||
const kArrowLeft = "\uE012"; | ||
return new test_driver.Actions() | ||
.keyDown(kArrowLeft) | ||
.keyUp(kArrowLeft) | ||
.send(); | ||
} | ||
|
||
promise_test(async () => { | ||
await new Promise(resolve => { | ||
addEventListener("load", resolve, {once: true}); | ||
}); | ||
}, "Initializing tests"); | ||
|
||
promise_test(async t => { | ||
const editingHost = document.querySelector("div[contenteditable]"); | ||
editingHost.focus(); | ||
const p = editingHost.querySelector("p"); | ||
getSelection().collapse(p.firstChild, "abc".length); | ||
await sendArrowRightKey(); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: p.nextSibling, | ||
startOffset: 0, | ||
endContainer: p.nextSibling, | ||
endOffset: 0, | ||
}), | ||
); | ||
}, `${t.name}: first arrow-right should move caret before non-editable text`); | ||
await sendArrowRightKey(); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: p.nextSibling, | ||
startOffset: 1, | ||
endContainer: p.nextSibling, | ||
endOffset: 1, | ||
}), | ||
); | ||
}, `${t.name}: second arrow-right should move caret after non-editable text`); | ||
}, "Move caret from end of editable text node to <br> following non-editable text in next paragraph"); | ||
|
||
promise_test(async t => { | ||
const editingHost = document.querySelector("div[contenteditable]"); | ||
editingHost.focus(); | ||
const p = editingHost.querySelector("p"); | ||
getSelection().collapse(p.nextSibling, 1); | ||
await sendArrowLeftKey(); | ||
assert_false( | ||
editingHost.querySelector("[contenteditable=false]").contains(getSelection().focusNode), | ||
"focus node should not be the non-editable nodes" | ||
); | ||
assert_false( | ||
editingHost.querySelector("[contenteditable=false]").contains(getSelection().anchorNode), | ||
"anchor node should not be the non-editable nodes" | ||
); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: p.nextSibling, | ||
startOffset: 0, | ||
endContainer: p.nextSibling, | ||
endOffset: 0, | ||
}), | ||
); | ||
}, `${t.name}: first arrow-left should move caret before non-editable text`); | ||
}, "Move caret from <br> following non-editable text to end of preceding editable text in next paragraph"); | ||
|
||
promise_test(async t => { | ||
const editingHost = document.querySelector("div[contenteditable] + div[contenteditable]"); | ||
editingHost.focus(); | ||
const p = editingHost.querySelector("p"); | ||
getSelection().collapse(p.firstChild, 0); | ||
await sendArrowRightKey(); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: p.nextSibling, | ||
startOffset: 0, | ||
endContainer: p.nextSibling, | ||
endOffset: 0, | ||
}), | ||
); | ||
}, `${t.name}: first arrow-right should move caret before non-editable text`); | ||
await sendArrowRightKey(); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: editingHost.querySelector("[contenteditable=false]").nextSibling, | ||
startOffset: 0, | ||
endContainer: editingHost.querySelector("[contenteditable=false]").nextSibling, | ||
endOffset: 0, | ||
}), | ||
); | ||
}, `${t.name}: second arrow-right should move caret after non-editable text`); | ||
}, "Move caret from empty editable paragraph to editable text following non-editable text in next paragraph"); | ||
|
||
promise_test(async t => { | ||
const editingHost = document.querySelector("div[contenteditable] + div[contenteditable]"); | ||
editingHost.focus(); | ||
const p = editingHost.querySelector("p"); | ||
getSelection().collapse(editingHost.querySelector("[contenteditable=false]").nextSibling, 0); | ||
await sendArrowLeftKey(); | ||
assert_false( | ||
editingHost.querySelector("[contenteditable=false]").contains(getSelection().focusNode), | ||
"focus node should not be the non-editable nodes" | ||
); | ||
assert_false( | ||
editingHost.querySelector("[contenteditable=false]").contains(getSelection().anchorNode), | ||
"anchor node should not be the non-editable nodes" | ||
); | ||
test(() => { | ||
assert_equals( | ||
getRangeDescription(getSelection().getRangeAt(0)), | ||
getRangeDescription({ | ||
startContainer: p.nextSibling, | ||
startOffset: 0, | ||
endContainer: p.nextSibling, | ||
endOffset: 0, | ||
}), | ||
); | ||
}, `${t.name}: first arrow-left should move caret before non-editable text`); | ||
}, "Move caret from start of text following non-editable text to empty preceding editable paragraph"); | ||
</script> | ||
</head> | ||
<body> | ||
<div contenteditable> | ||
<p>abc</p><p><span contenteditable="false">def</span><br></p> | ||
</div> | ||
<div contenteditable> | ||
<p><br></p><p><span contenteditable="false">abc</span>def</p> | ||
</div> | ||
</body> | ||
</html> |