I'm trying to make a dropdown menu follow the cursor in a Rich Text Editor for the web. Using the following I'm able to get the cursor's coordinates no problem:
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return;
const position = sel.getRangeAt(0).getBoundingClientRect());
However, if I try to use this after a \n
character it returns the position of the cursor after the newline char rather than the beginning of the new line (where the cursor actually appears in the window):
Is there a way to avoid this?
Edit: Based on the ments below here's a more in depth version of what I'm trying to achieve.
I'm currently building a text editor with React and Slate.js (). It's a more robust version of a contentEditable ponent at its heart but allows you to drop in an editable text field into a page. Because of the node structure I'm using, I want there to be soft breaks between paragraphs rather than new <div />
elements. Because this is non-standard behavior for contentEditable, it is very difficult to make a small example without recreating the whole app.
Edit (further responses to ments): The raw HTML of the text element looks like this:
<span data-slate-string="true">working until newline
see?
</span>
you can see that slate literally translates the break to a \n character which is what I think is causing the problem.
I'm trying to make a dropdown menu follow the cursor in a Rich Text Editor for the web. Using the following I'm able to get the cursor's coordinates no problem:
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return;
const position = sel.getRangeAt(0).getBoundingClientRect());
However, if I try to use this after a \n
character it returns the position of the cursor after the newline char rather than the beginning of the new line (where the cursor actually appears in the window):
Is there a way to avoid this?
Edit: Based on the ments below here's a more in depth version of what I'm trying to achieve.
I'm currently building a text editor with React and Slate.js (https://github./ianstormtaylor/slate). It's a more robust version of a contentEditable ponent at its heart but allows you to drop in an editable text field into a page. Because of the node structure I'm using, I want there to be soft breaks between paragraphs rather than new <div />
elements. Because this is non-standard behavior for contentEditable, it is very difficult to make a small example without recreating the whole app.
Edit (further responses to ments): The raw HTML of the text element looks like this:
<span data-slate-string="true">working until newline
see?
</span>
you can see that slate literally translates the break to a \n character which is what I think is causing the problem.
Share Improve this question edited Jun 20, 2020 at 9:12 CommunityBot 11 silver badge asked Jan 16, 2020 at 10:23 GomiNoSenseiGomiNoSensei 4731 gold badge5 silver badges11 bronze badges 7- The newline character is at the end of the line you've created it. The cursor position does not reflect to the clientRect object. – Teemu Commented Jan 16, 2020 at 10:38
- 1 Please minimal reproducible example. In what event are you calling this code? – Kaiido Commented Jan 16, 2020 at 13:16
- @Teemu does this mean that there's no way of getting the visual cursor position then? – GomiNoSensei Commented Jan 16, 2020 at 14:38
- Perhaps you can check what the actual selection contains - and if it has a newline character at the end, then modify the position of your menu accordingly? (Like set the x coordinate to 0 to start on the left again, and add one line height’s worth of pixels to the y value you got.) – 04FS Commented Jan 16, 2020 at 14:44
- I was writing once something to restore user selection accross multiple elements. I guess I know what happens but can you first share how actually HTML elements look like after this newline (not ponents)? – Zydnar Commented Jan 16, 2020 at 14:44
2 Answers
Reset to default 4Even when using the default contenteditable of the browser there is indeed a weird behavior when the cursor is set to a new line: the Range's getClientRects()
will be empty and thus getBoundingClientRect()
will return a full 0 DOMRect.
Here is a simple demo demonstrating the issue:
const target = document.getElementById('target');
document.onselectionchange = (e) => {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) {
return;
}
const range = sel.getRangeAt(0);
const position = range.getBoundingClientRect();
floater.style.top = position.bottom + 'px';
floater.style.left = position.right + 'px';
}
#floater {
position: absolute;
width: 20px;
height: 30px;
background: #DDAADDCC;
pointer-events: none;
bottom: 0;
}
<div id="target" contenteditable>Type here and enter new lines</div>
<div id="floater"></div>
For this, there is a simple workaround which consists in selecting the contents of the current Range's container:
// check if we have client rects
const rects = range.getClientRects();
if(!rects.length) {
// probably new line buggy behavior
if(range.startContainer && range.collapsed) {
// explicitely select the contents
range.selectNodeContents(range.startContainer);
}
}
const target = document.getElementById('target');
document.onselectionchange = (e) => {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) {
return;
}
const range = sel.getRangeAt(0);
// check if we have client rects
const rects = range.getClientRects();
if(!rects.length) {
// probably new line buggy behavior
if(range.startContainer && range.collapsed) {
// explicitely select the contents
range.selectNodeContents(range.startContainer);
}
}
const position = range.getBoundingClientRect();
floater.style.top = position.bottom + 'px';
floater.style.left = position.right + 'px';
}
#floater {
position: absolute;
width: 20px;
height: 30px;
background: #DDAADDCC;
pointer-events: none;
bottom: 0;
}
<div id="target" contenteditable>Type here and enter new lines</div>
<div id="floater"></div>
Now OP seems to be in a different issue, since they do deal with soft-breaks \n
and a white-space: pre
.
However I was able to reproduce it only from my Firefox., Chrome behaving "as expected" in this case...
So in my Firefox, the DOMRect will not be all 0, but it will be the one before the line break.
To demonstrate this case, click on the empty line:
const target = document.getElementById('target');
document.onselectionchange = (e) => {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) {
return;
}
const range = sel.getRangeAt(0);
const position = range.getBoundingClientRect();
floater.style.top = position.bottom + 'px';
floater.style.left = position.right + 'px';
}
#target {
white-space: pre;
}
#floater {
position: absolute;
width: 20px;
height: 30px;
background: #DDAADDCC;
pointer-events: none;
bottom: 0;
}
<div id="target" contenteditable>Click on the below empty line
Click on the above empty line</div>
<div id="floater"></div>
And to workaround this case, it's a bit more plex...
We need to check what is the character before our Range, if it's a new line, then we need to update our range by selecting the next character. But doing so, we'd also move the cursor, so we actually need to do it from a cloned Range. But since Chrome doesn't behave like this, we need to also check if the previous character was on a different line, which bees a problem when there is no such previous character...
const target = document.getElementById('target');
document.onselectionchange = (e) => {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) {
return;
}
const range = sel.getRangeAt(0);
// we can still workaround the default behavior too
const rects = range.getClientRects();
if(!rects.length) {
if(range.startContainer && range.collapsed) {
range.selectNodeContents(range.startContainer);
}
}
let position = range.getBoundingClientRect();
const char_before = range.startContainer.textContent[range.startOffset - 1];
// if we are on a \n
if(range.collapsed && char_before === "\n") {
// create a clone of our Range so we don't mess with the visible one
const clone = range.cloneRange();
// check if we are experiencing a bug
clone.setStart(range.startContainer, range.startOffset-1);
if(clone.getBoundingClientRect().top === position.top) {
// make it select the next character
clone.setStart(range.startContainer, range.startOffset + 1 );
position = clone.getBoundingClientRect();
}
}
floater.style.top = position.bottom + 'px';
floater.style.left = position.right + 'px';
}
#target {
white-space: pre;
}
#floater {
position: absolute;
width: 20px;
height: 30px;
background: #DDAADDCC;
pointer-events: none;
bottom: 0;
}
<div id="target" contenteditable>Click on the below empty line
Click on the above empty line</div>
<div id="floater"></div>
This works:
Insert a "zero width space" in the range and again call getBoundingClientRect.
Then remove the the space.
function rangeRect(r){
let rect = r.getBoundingClientRect();
if (r.collapsed && rect.top===0 && rect.left===0) {
let tmpNode = document.createTextNode('\ufeff');
r.insertNode(tmpNode);
rect = r.getBoundingClientRect();
tmpNode.remove();
}
return rect;
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744904126a4600151.html
评论列表(0条)