I'm using the following code to select the element
const selectEnd = (element: HTMLElement, position: number) => {
element.focus();
const range = document.createRange();
const sel = window.getSelection();
if (position !== -1) {
range.setStart(element, Math.min(position, element.childNodes.length));
} else {
range.setStart(element, element.childNodes.length);
}
sel?.removeAllRanges();
sel?.addRange(range);
};
After selecting the element the following rect variable will always be undefined
const selection = window.getSelection();
if (!selection) return;
const range = selection.getRangeAt(0).cloneRange();
range.collapse(true);
const rect = range.getClientRects()[0];
But after an onInput event happens on the client the range.getClientRects function will return a value.
I'm trying to get range.getClientRects() to return a variable after the focus call without the user needing to input anything.
CodePudding user response:
You felt on CSSWG#2514.
The problem is that browsers aren't able to calculate the DOMRects
composing a collapsed Range
which is anchored to an Element
(instead of a Node
).
This CKEditor issue lists a few cases where this can happen, along with a few potential workarounds.
1. A collapsed Range
in an Element
that does have content. (Most probably your case)
Setting the Range
on the closest TextNode
would allow the browsers to calculate the boxes correctly:
const el = document.querySelector("div");
const range = new Range();
// to show the "bug"
range.selectNode(el);
range.collapse(true);
console.log("selecting the Element", range.getClientRects().length); // 0
// the workaround
range.setStart(el.firstChild, 0);
range.collapse(true);
console.log("selecting the Node", range.getClientRects().length); // 1
<div>target</div>
Though i's also possible that the element's firstChild
isn't a TextNode
either, so you actually need a method to retrieve the first TextNode
in that Element
:
function getFirstTextNode(target) {
return document.createNodeIterator(target, NodeFilter.SHOW_TEXT).nextNode();
}
const el = document.querySelector("div");
const range = new Range();
// to show how our previous version fails
range.setStart(el.firstChild, 0);
range.collapse(true);
console.log("selecting the firstChild", range.getClientRects().length); // 0
// the workaround
const firstNode = getFirstTextNode(el);
range.setStart(firstNode, 0);
range.collapse(true);
console.log("selecting the first TextNode", range.getClientRects().length); // 1
<div><span></span>target</div>
2. A Range
in an empty Element
, (and thus without TextNode
to select)
For this one you can try appending a Zero-Width-Space (ZWSP) character inside the Element
, take your measure on the newly created TextNode
, remove the character.
function getFirstTextNode(target) {
return document.createNodeIterator(target, NodeFilter.SHOW_TEXT).nextNode();
}
const el = document.querySelector("div");
const range = new Range();
let firstTextNode = getFirstTextNode(el);
const wasEmpty = !firstTextNode;
if (wasEmpty) {
console.log("inserting a ZWSP");
firstTextNode = new Text("\u200B");
el.append(firstTextNode);
}
range.setStart(firstTextNode, 0);
range.collapse(true);
const rect = range.getClientRects()[0];
if (wasEmpty) {
firstTextNode.remove();
}
console.log(rect); // DOMRect
<div></div>
3. A Range
that sits in-between two elements
...</div>|<div>...
Here again you can use the ZWSP trick. Detecting this might become a bit more terse though, and given OP apparently wants to set the caret at the beginning of the Element
, that doesn't seem like a possibility for them. So I'll leave the example off from this answer.