This one builds on @alockwood05's answer and provides both get and set functionality for a caret with nested tags inside the contenteditable div as well as the offsets within nodes so that you have a solution that is both serializable and de-serializable by offsets as well.
I'm using this solution in a cross-platform code editor that needs to get the caret start/end position prior to syntax highlighting via a lexer/parser and then set it back immediately afterward.
function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
for (let node of parent.childNodes) {
if (countingState.done) break;
if (node === endNode) {
countingState.done = true;
countingState.offsetInNode = offset;
return countingState;
}
if (node.nodeType === Node.TEXT_NODE) {
countingState.offsetInNode = offset;
countingState.count += node.length;
} else if (node.nodeType === Node.ELEMENT_NODE) {
countUntilEndContainer(node, endNode, offset, countingState);
} else {
countingState.error = true;
}
}
return countingState;
}
function countUntilOffset(parent, offset, countingState = {count: 0}) {
for (let node of parent.childNodes) {
if (countingState.done) break;
if (node.nodeType === Node.TEXT_NODE) {
if (countingState.count <= offset && offset < countingState.count + node.length)
{
countingState.offsetInNode = offset - countingState.count;
countingState.node = node;
countingState.done = true;
return countingState;
}
else {
countingState.count += node.length;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
countUntilOffset(node, offset, countingState);
} else {
countingState.error = true;
}
}
return countingState;
}
function getCaretPosition()
{
let editor = document.getElementById('editor');
let sel = window.getSelection();
if (sel.rangeCount === 0) { return null; }
let range = sel.getRangeAt(0);
let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
let offsets = { start: start, end: end, offsets: offsetsCounts };
return offsets;
}
function setCaretPosition(start, end)
{
let editor = document.getElementById('editor');
let sel = window.getSelection();
if (sel.rangeCount === 0) { return null; }
let range = sel.getRangeAt(0);
let startNode = countUntilOffset(editor, start);
let endNode = countUntilOffset(editor, end);
let newRange = new Range();
newRange.setStart(startNode.node, startNode.offsetInNode);
newRange.setEnd(endNode.node, endNode.offsetInNode);
sel.removeAllRanges();
sel.addRange(newRange);
return true;
}