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 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
How to Parse a CSV String With JavaScript, Which Contains Comma in Data
How to Match a Whole Word in JavaScript
How to Post a File from a Form With Axios
How to Provide Named Parameters in a Function Call in JavaScript
Merge Two Array of Objects Based on a Key
JavaScript Function to Add X Months to a Date
Why Does String to Number Comparison Work in JavaScript
Jquery $(Document).Ready and Updatepanels
Angularjs Routing Without the Hash '#'
I Keep Getting "Uncaught Syntaxerror: Unexpected Token O"
Converting a Js Object to an Array Using Jquery
Best Way to Detect When a User Leaves a Web Page
Whats the Best Way to Update an Object in an Array in Reactjs
How to Find Object in Array by Property in JavaScript
JavaScript Es6 Promise for Loop
Using Bitwise or 0 to Floor a Number
How to Detect My Browser Version and Operating System Using JavaScript