Get Contenteditable Caret Position

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 to pre

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 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;
}

Get and set cursor position with contenteditable div

A good rich-text editor is one of the harder things to do currently, and is pretty much a project by itself (unfriendly API, huge number of corner cases, cross-browser differences, the list goes on). I would strongly advise you to try and find an existing solution.

Some libraries that can be used include:

  • Quill (http://quilljs.com)
  • WYSGIHTML (http://wysihtml.com)
  • CodeMirror library (http://codemirror.net)

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>

Accounting for ` br `s in contenteditable caret position

I modified your demo to serialize the position as a container/offset pair instead of just a position. The container is serialized as a simple array of indexes into the childNodes collection of each node starting from a reference node (which in this case is the contenteditable element, of course).

It's not completely clear to me what you intend to use this for, but since it mirrors the selection model it should hopefully give you much less pain.

const $el = $('ce'),

$startContainer = $('start-container'),

$startOffset = $('start-offset'),

$endContainer = $('end-container'),

$endOffset = $('end-offset');



function pathFromNode(node, reference) {

function traverse(node, acc) {

if (node === reference) {

return acc;

} else {

const parent = node.parentNode;

const index = [...parent.childNodes].indexOf(node);

return traverse(parent, [index, ...acc]);

}

}

return traverse(node, []);

}

function nodeFromPath(path, reference) {

if (path.length === 0) {

return reference;

} else {

const [index, ...rest] = path;

const next = reference.childNodes[index];

return nodeFromPath(rest, next);

}

}

function getCaret(el) {

const range = document.getSelection().getRangeAt(0);

return {

start: {

container: pathFromNode(range.startContainer, el),

offset: range.startOffset

},

end: {

container: pathFromNode(range.endContainer, el),

offset: range.endOffset

}

};

}

function setCaret(el, start, end) {

const range = document.createRange();

range.setStart(nodeFromPath(start.container, el), start.offset);

range.setEnd(nodeFromPath(end.container, el), end.offset);

sel = document.getSelection();

sel.removeAllRanges();

sel.addRange(range);

}

function update() {

const pos = getCaret($el);

$startContainer.value = JSON.stringify(pos.start.container);

$startOffset.value = pos.start.offset;

$endContainer.value = JSON.stringify(pos.end.container);

$endOffset.value = pos.end.offset;

}

$el.addEventListener('keyup', update);

$el.addEventListener('click', update);

$('set').addEventListener('click', () => {

const start = {

container: JSON.parse($startContainer.value),

offset: $startOffset.value

};

const end = {

container: JSON.parse($endContainer.value),

offset: $endOffset.value

};

setCaret($el, start, end);

});

function $(sel) {

return document.getElementById(sel);

}
input {

width: 40px;

}

[contenteditable] {

white-space: pre;

}
(updates on click & keyup)<br/>

<label>Start: <input id="start-container" type="text"/><input id="start-offset" type="number"/></label><br/>

<label>End: <input id="end-container" type="text"/><input id="end-offset" type="number"/></label><br/>

<button id="set">Set</button>

<p></p>

<!-- inline BR's behave differently from <br> on their own separate line

<div id="ce" contenteditable>012345<br><br><br>9012345</div>

-->

<!-- get/set caret needs to work with these examples as well

* <br> at beginning

<div id="ce" contenteditable><br>12345<br><br><br>9012345</div>

* <br>'s wrapped in a <div>

-->

<div id="ce" contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>

How to set caret position of a contenteditable div containing combination of text and element nodes

Your textNode has 3 children (1 text, 1 element, 1 text) and therefore you can't just use firstChild.

You need to iterate over the childNodes of the <div> and track the character count where the nodeType of the childNode equals Node.TEXT_NODE (see here on MDN). Where the character count is less than the value of caret you can deduct that from caret and move onto the next text node.

Per your condition that:

I desire and each image would be treated as 1 character

The code will deduct 1 from caret where nodeType == 1 i.e. Node.ELEMENT_NODE

Here is a code example with multiple icons:

var node = document.querySelector("div");

node.focus();

var caret = 24;

var child;

var childNodeIndex = 0;

for(var i=0; i<node.childNodes.length; i++) {

child = node.childNodes[i];

// Node.ELEMENT_NODE == 1

// Node.TEXT_NODE == 3

if(child.nodeType == Node.TEXT_NODE) {

// keep track of caret across text childNodes

if(child.length <= caret) {

caret -= child.length;

} else {

break;

}

} else if (child.nodeType == Node.ELEMENT_NODE) {

// condition that 'each image would be treated as 1 character'

if(caret > 0) {

caret -= 1;

} else {

break;

}

};

childNodeIndex += 1;

}

var textNode = node.childNodes[childNodeIndex];

// your original code continues here...

var range = document.createRange();

range.setStart(textNode, caret);

range.setEnd(textNode, caret);

var sel = window.getSelection();

sel.removeAllRanges();

sel.addRange(range);
<div id="text" contenteditable="true">a<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/><img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>b<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>cdefghijkl<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>mnopq<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>rst<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>uvw<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/>xyz<img src="https://www.splitbrain.org/_static/ico/circular/ico/add.png"/></div>


Related Topics



Leave a reply



Submit