How can I keep the cursor in the right place when typing inside of a <div id="richTextBox" contenteditable="true"></div>
whose innerHTML changes on each keystroke? The act of replacing the innerHTML messes up the cursor position.
The reason I change the innerHTML is because I am adding <span>
tags. It's part of a code highlighting program. The span tags allow me to place the correct color highlights.
I am using the below code from a StackOverflow answer as a band aid for the moment, but it has a significant bug. If you hit enter, the cursor stays at the old spot, or goes to a random spot. That's because the algorithm counts how many characters from the beginning the cursor is. But it doesn't count HTML tags or line breaks as characters. And the richTextBox inserts <br>
to make enters.
Ideas for fixing:
window.getSelection()
and document.createRange()
, but I could not get that to work.// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
class Cursor {
static getCurrentCursorPosition(parentElement) {
var selection = window.getSelection(),
charCount = -1,
node;
if (selection.focusNode) {
if (Cursor._isChildOf(selection.focusNode, parentElement)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node === parentElement) {
break;
}
if (node.previousSibling) {
node = node.previousSibling;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) {
break;
}
}
}
}
}
return charCount;
}
static setCurrentCursorPosition(chars, element) {
if (chars >= 0) {
var selection = window.getSelection();
let range = Cursor._createRange(element, { count: chars });
if (range) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
static _createRange(node, chars, range) {
if (!range) {
range = document.createRange()
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count >0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length;
} else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else {
for (var lp = 0; lp < node.childNodes.length; lp++) {
range = Cursor._createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
}
static _isChildOf(node, parentElement) {
while (node !== null) {
if (node === parentElement) {
return true;
}
node = node.parentNode;
}
return false;
}
}
window.addEventListener('DOMContentLoaded', (e) => {
let richText = document.getElementById('rich-text');
richText.addEventListener('input', function(e) {
let offset = Cursor.getCurrentCursorPosition(richText);
// Pretend we do stuff with innerHTML here. The innerHTML will end up getting replaced with slightly changed code.
let s = richText.innerHTML;
richText.innerHTML = "";
richText.innerHTML = s;
Cursor.setCurrentCursorPosition(offset, richText);
richText.focus(); // blinks the cursor
});
});
body {
margin: 1em;
}
#rich-text {
width: 100%;
height: 450px;
border: 1px solid black;
cursor: text;
overflow: scroll;
resize: both;
/* in Chrome, must have display: inline-block for contenteditable=true to prevent it from adding <div> <p> and <span> when you type. */
display: inline-block;
}
<p>
Click somewhere in the middle of line 1. Hit enter. Start typing. Cursor is in the wrong place.
</p>
<p>
Reset. Click somewhere in the middle of line 1. Hit enter. Hit enter again. Cursor goes to some random place.
</p>
<div id="rich-text" contenteditable="true">Testing 123<br />Testing 456</div>
Google Chrome v83, Windows 7
The issue seems to be that adding a new line adds a <br>
, but as you are still in the parent element, previous DOM children are not taken into account, and the selection.focusOffset
only gives the value of 4
.
It may help to add a newline to the end of the innerHtml
, as it is being stripped when you remove and re-add it. + "\n"
to the end of line 100 on the Fiddle would do.
Your main problem though is that getCurrentCursorPosition
you copied from that other StackOverflow question doesn't actually work.
I'd suggest you go through some of the other answers to this question:
Get contentEditable caret index position, and console.log
what they output and see which one works best for your edge-cases.
If you don't want to write it yourself, then Caret.js (part of the At.js editor library) would be useful.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With