Ctrl-Enter handler: Position correctly when selection is empty

In empty <input>s and the note editor.

Also add comment explaining possible alternate approach.

Fixes #4763
This commit is contained in:
Abe Jellinek 2024-10-17 13:54:43 -04:00
parent bb6085bbe0
commit 1f5b7f5414

View file

@ -22,6 +22,20 @@ export function getTargetElement(doc) {
* @returns {{ clientX: number, clientY: number }}
*/
function getContextMenuPosition(targetElement) {
// In theory, we could find the caret position using some privileged-code
// magic:
//
// let x = {}, y = {}, w = {}, h = {};
// Cc["@mozilla.org/accessibilityService;1"].getService(Ci.nsIAccessibilityService)
// .getAccessibleFor(targetElement)
// .QueryInterface(Ci.nsIAccessibleText)
// .getCaretRect(x, y, w, h);
//
// But this returns hardware screen units, doesn't work for selections, and
// doesn't *precisely* correspond to the position of the caret on screen
// without adjustments.
// Consider revisiting if the manual method gets too weighed down by edge cases.
let selection;
if (targetElement.editor?.selection) {
selection = targetElement.editor.selection;
@ -41,23 +55,53 @@ function getContextMenuPosition(targetElement) {
let anchorToEnd;
if (selection) {
let range = selection.getRangeAt(0);
// Easy case: We have a range with rects, so use its bounding rect
if (range.getClientRects().length) {
rect = range.getBoundingClientRect();
// ...Except an empty <input>'s text node will be anchored
// to the bottom with height 0, so move the rect to the top
if (rect.height === 0
&& range.collapsed
&& range.startContainer.isNativeAnonymous
&& range.startContainer.parentElement?.getClientRects().length) {
// We know we have a mutable DOMRect here, so we can
// mutate without cloning
rect.y = range.startContainer.parentElement.getBoundingClientRect().y;
anchorToBottom = false;
anchorToEnd = true;
}
}
// If the selection is between lines in an editor, it'll be
// inside the editor's native anonymous text node and won't
// have any rects for some reason.
// If the caret is between lines in an editor, it'll be inside the
// editor's native anonymous text node and won't have any rects for
// some reason.
// If that's the case, use the text node's bounds.
else if (range.startContainer === range.endContainer && range.startContainer.isNativeAnonymous
else if (range.collapsed && range.startContainer.isNativeAnonymous
&& range.startContainer.firstChild?.nodeType === Node.TEXT_NODE) {
let quads = range.startContainer.firstChild.getBoxQuads();
rect = quads[quads.length - 1].getBounds();
}
// In a contenteditable (ProseMirror), when the selection is in an
// empty block or at the end of a block, it won't be within a text node
// and won't have any rects. Place at the end of the previous node.
else if (range.startContainer === range.endContainer
&& range.startOffset === range.endOffset
&& range.startOffset < range.startContainer.childNodes.length) {
range = range.cloneRange();
range.selectNode(range.startContainer.childNodes[range.startOffset]);
rect = range.getBoundingClientRect();
anchorToBottom = false;
anchorToEnd = true;
}
else {
rect = range.commonAncestorContainer.getBoundingClientRect();
}
anchorToBottom = !range.collapsed;
anchorToEnd = range.collapsed;
anchorToBottom ??= !range.collapsed;
anchorToEnd ??= range.collapsed;
}
else {
rect = targetElement.getBoundingClientRect();