The accepted answer is wrong. Whether or not that has worked 2 years ago I can't tell. It works for text content, yes, but not for input and textarea values, which was what the question specifically was about.
Just try it out here: select the page text and it works, select input box text and it doesn't.
document.addEventListener('mouseup', function() { console.log(window.getSelection().getRangeAt(0).getBoundingClientRect()) })
<p>Lorem ipsum ... <input value="bla bla"/></p>
<textarea style="width: 100px; height: 50px;"> Lorem ipsum bla bla bla
</textarea>
<p>Input <b>box</b><p>
<input value="Lorem"/>
So what does work? I have found somewhere during frantic searching an approach which seems utterly crazy, but possibly is the only thing that works.
Make a hidden (but not un-rendered) div on which you apply all computed style properties from your input box.
How do you know what the selected input box is? The getSelection() range is meaningless if your have select text in an input box. You must find the input box by document.activeElement (because it has the focus). Now you have that input box with selectionStart and selectionEnd properties. Copy that input's computed style properties onto your hidden div.
Now and put the value text from the input box inside, up until the start of your selected text, e.g., input.value.substring(0, input.selectionStart)
.
Now append a little after that text, and then measure the position of that span, relative to the div.
Finally add that relative position to the position of your real input box. And voilà you have the position of the cursor!
Crazy, I know. And I will do it now because I know of no other solution.
Here it is:
function showPopUp() {
let popUpPos = getSelection()
.getRangeAt(0)
.getBoundingClientRect();
if(popUpPos.x == 0 && popUpPos.y == 0) {
const input = document.activeElement;
const div = document.createElement('div');
for(style of input.computedStyleMap())
div.style[style[0]] = style[1].toString();
div.textContent = input.value.substring(0, input.selectionStart);
const span = document.createElement('span');
div.insertBefore(span, null);
document.body.insertBefore(div, null);
const [divPos, spanPos, inputPos] =
[div, span, input].map(e => e.getBoundingClientRect());
popUpPos = {
x: inputPos.x + (spanPos.x - divPos.x),
y: inputPos.y + (spanPos.y - divPos.y)
};
}
const popUp = document.body.insertAdjacentHTML('beforeend',
`<div style="position: absolute;
top: ${popUpPos.y}px;
left: ${popUpPos.x}px;
border: 2px solid green;'">Look!</div>`);
}
document.addEventListener('mouseup', showPopUp);
<p>Lorem ipsum ... <input value="bla bla"/></p>
<textarea style="width: 100px; height: 50px;"> Lorem ipsum bla bla bla
</textarea>
<p>Input <b>box</b><p>
<input value="Lorem"/>
Now as I have done this to meet the needs of a real app, I found a whole slur of little issues that I had to overcome. Here is an (incomplete) list:
- a default textarea has no width or height style, to copy that, need to actually read the getBoundingClientRect() to size the hidden div
- scrollbar! need to scroll the hidden div to the same location
- word wrapping, right before the span, put an unbreakable zero-width space and put the text after the selection in after the span, the span then wraps with the following word as needed.
- vertical positioning of the pop up under the current line requires you use the bottom of the span, not the y position. (Not sure if left and x are different in some right-to-left writing schemes perhaps?)