Get contentEditable caret position
The following code assumes:
- There is always a single text node within the editable
<div>
and no other nodes - The editable div does not have the CSS
white-space
property set topre
If you need a more general approach that will work content with nested elements, try this answer:
https://stackoverflow.com/a/4812022/96100
Code:
function getCaretPosition(editableDiv) { var caretPos = 0, sel, range; if (window.getSelection) { sel = window.getSelection(); if (sel.rangeCount) { range = sel.getRangeAt(0); if (range.commonAncestorContainer.parentNode == editableDiv) { caretPos = range.endOffset; } } } else if (document.selection && document.selection.createRange) { range = document.selection.createRange(); if (range.parentElement() == editableDiv) { var tempEl = document.createElement("span"); editableDiv.insertBefore(tempEl, editableDiv.firstChild); var tempRange = range.duplicate(); tempRange.moveToElementText(tempEl); tempRange.setEndPoint("EndToEnd", range); caretPos = tempRange.text.length; } } return caretPos;}
#caretposition { font-weight: bold;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script><div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div><div id="caretposition">0</div><script> var update = function() { $('#caretposition').html(getCaretPosition(this)); }; $('#contentbox').on("mousedown mouseup keydown keyup", update);</script>
get cursor position in contenteditable div with html-tags
This gives you what you want. It will give you the length of the html also.
https://jsfiddle.net/ko5Lhbeo/
Change this:
$('.msg').on('keyup',function(e){
$('.cursor').html(getCaretPosition(document.getElementById('msg')));
})
to this:
// Added DOMSubtreeModified event here to fire whenever any modifications to the sub tree
$('.msg').on('keyup DOMSubtreeModified',function(e) {
$('.cursor').html($('#msg').html().length);
})
Get caret (cursor) position in contentEditable area containing HTML content
UPDATE
I've written a simpler version of this that also works in IE < 9:
https://stackoverflow.com/a/4812022/96100
Old Answer
This is actually a more useful result than a character offset within the text of the whole document: the startOffset
property of a DOM Range (which is what window.getSelection().getRangeAt()
returns) is an offset relative to its startContainer
property (which isn't necessarily always a text node, by the way). However, if you really want a character offset, here's a function that will do it.
Here's a live example: http://jsfiddle.net/timdown/2YcaX/
Here's the function:
function getCharacterOffsetWithin(range, node) {
var treeWalker = document.createTreeWalker(
node,
NodeFilter.SHOW_TEXT,
function(node) {
var nodeRange = document.createRange();
nodeRange.selectNode(node);
return nodeRange.compareBoundaryPoints(Range.END_TO_END, range) < 1 ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
},
false
);
var charCount = 0;
while (treeWalker.nextNode()) {
charCount += treeWalker.currentNode.length;
}
if (range.startContainer.nodeType == 3) {
charCount += range.startOffset;
}
return charCount;
}
how to get the caret position of a contenteditable div which contains images
See Tim Down's answer on Get a range's start and end offset's relative to its parent container.
Try to use the function he has to get the selection index with nested elements like this:
function getCaretCharacterOffsetWithin(element) { var caretOffset = 0; var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var sel; if (typeof win.getSelection != "undefined") { sel = win.getSelection(); if (sel.rangeCount > 0) { var range = win.getSelection().getRangeAt(0); var preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } } else if ( (sel = doc.selection) && sel.type != "Control") { var textRange = sel.createRange(); var preCaretTextRange = doc.body.createTextRange(); preCaretTextRange.moveToElementText(element); preCaretTextRange.setEndPoint("EndToEnd", textRange); caretOffset = preCaretTextRange.text.length; } return caretOffset;}
var update = function() { console.log(getCaretCharacterOffsetWithin(this));};$('#text').on("mousedown mouseup keydown keyup", update);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><div contenteditable="true" id="text">minubyv<img src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>
How do you get and set the caret position in a contenteditable?
This seems to work for me but I've only tested it for my use cases.
GET
function getCaretIndex(win, contentEditable) {
var index = 0;
var selection = win.getSelection();
var textNodes = textNodesUnder(contentEditable);
for(var i = 0; i < textNodes.length; i++) {
var node = textNodes[i];
var isSelectedNode = node === selection.focusNode;
if(isSelectedNode) {
index += selection.focusOffset;
break;
}
else {
index += node.textContent.length;
}
}
return index;
}
SET
function setCaretIndex(win, contentEditable, newCaretIndex) {
var cumulativeIndex = 0;
var relativeIndex = 0;
var targetNode = null;
var textNodes = textNodesUnder(contentEditable);
for(var i = 0; i < textNodes.length; i++) {
var node = textNodes[i];
if(newCaretIndex <= cumulativeIndex + node.textContent.length) {
targetNode = node;
relativeIndex = newCaretIndex - cumulativeIndex;
break;
}
cumulativeIndex += node.textContent.length;
}
var range = win.document.createRange();
range.setStart(targetNode, relativeIndex);
range.setEnd(targetNode, relativeIndex);
range.collapse();
var sel = win.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
REQUIRED HELPER
function textNodesUnder(node) { // https://stackoverflow.com/a/10730777/3245937
var all = [];
for (node=node.firstChild;node;node=node.nextSibling){
if (node.nodeType==3) {
all.push(node);
}
else {
all = all.concat(textNodesUnder(node));
}
}
return all;
}
TEST (just call this function)
It loops through the text in a contenteditable, sets the caret index, then reads it. Console output is: (setIndex | getIndex)
function testContentEditable() {
document.body.innerHTML = "<div contenteditable></div>"
var ce = document.querySelector("[contenteditable]");
ce.focus();
ce.innerHTML = "HELLO <span data-foo='true' style='text-decoration: underline;'><span style='color:red;'>WORLD</span> MY</span> NAME IS BOB";
var i = 0;
var intv = setInterval(function() {
if(i == ce.innerText.length) {
clearInterval(intv);
}
setCaretIndex(window, ce, i);
var currentIndex = getCaretIndex(window, ce);
console.log(i + " | " + currentIndex);
i++;
}, 100);
}
FIDDLE
function getCaretIndex(win, contentEditable) {
var index = 0;
var selection = win.getSelection();
var textNodes = textNodesUnder(contentEditable);
for(var i = 0; i < textNodes.length; i++) {
var node = textNodes[i];
var isSelectedNode = node === selection.focusNode;
if(isSelectedNode) {
index += selection.focusOffset;
break;
}
else {
index += node.textContent.length;
}
}
return index;
}
function setCaretIndex(win, contentEditable, newCaretIndex) {
var cumulativeIndex = 0;
var relativeIndex = 0;
var targetNode = null;
var textNodes = textNodesUnder(contentEditable);
for(var i = 0; i < textNodes.length; i++) {
var node = textNodes[i];
if(newCaretIndex <= cumulativeIndex + node.textContent.length) {
targetNode = node;
relativeIndex = newCaretIndex - cumulativeIndex;
break;
}
cumulativeIndex += node.textContent.length;
}
var range = win.document.createRange();
range.setStart(targetNode, relativeIndex);
range.setEnd(targetNode, relativeIndex);
range.collapse();
var sel = win.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function textNodesUnder(node) { // https://stackoverflow.com/a/10730777/3245937
var all = [];
for (node=node.firstChild;node;node=node.nextSibling){
if (node.nodeType==3) {
all.push(node);
}
else {
all = all.concat(textNodesUnder(node));
}
}
return all;
}
function testContentEditable() {
document.body.innerHTML = "<div contenteditable></div>"
var ce = document.querySelector("[contenteditable]");
ce.focus();
ce.innerHTML = "HELLO <span data-foo='true' style='text-decoration: underline;'><span style='color:red;'>WORLD</span> MY</span> NAME IS BOB";
var i = 0;
var intv = setInterval(function() {
if(i == ce.innerText.length) {
clearInterval(intv);
}
setCaretIndex(window, ce, i);
var currentIndex = getCaretIndex(window, ce);
console.log(i + " | " + currentIndex);
i++;
}, 100);
}
testContentEditable();
Set Caret Position in 'contenteditable' div that has children
So, I was experiencing the same issue and decided to write my own routine quickly, it walks through all the child nodes recursively and set the position.
Note how this takes a DOM node as argument, not a jquery object as your original post does
// Move caret to a specific point in a DOM element
function SetCaretPosition(el, pos){
// Loop through all child nodes
for(var node of el.childNodes){
if(node.nodeType == 3){ // we have a text node
if(node.length >= pos){
// finally add our range
var range = document.createRange(),
sel = window.getSelection();
range.setStart(node,pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return -1; // we are done
}else{
pos -= node.length;
}
}else{
pos = SetCaretPosition(node,pos);
if(pos == -1){
return -1; // no need to finish the for loop
}
}
}
return pos; // needed because of recursion stuff
}
I hope this'll help you!
Related Topics
Why Is 'Replace' Property Deprecated in Angularjs Directives
Access Outside Variable in Loop from JavaScript Closure
How to Load Images Dynamically (Or Lazily) When Users Scrolls Them into View
Making a JavaScript String SQL Friendly
Webhoster Inserts a JavaScript Which Brokes My Code How to Remove It
Jquery $("#Radiobutton").Change(...) Not Firing During De-Selection
How to Select Element That Does Not Have Specific Class
Chrome Autofill/Autocomplete No Value for Password
Input Type=File Show Only Button
Using Jquery How to Get Click Coordinates on the Target Element
How to Access an SQLite Database from JavaScript
Getattribute() Versus Element Object Properties
Use Functions Defined in Es6 Module Directly in HTML
Getelementsbyname() Not Working
How to "Await" for a Callback to Return