doggie brezy doggie brezy - 17 days ago 4
Javascript Question

how to get the caret position of a contenteditable div which contains images

I have this contentedittable div

<div contenteditable="true" id="text">minubyv<img src="images/smiley/Emoji Smiley-01.png" class="emojiText" />iubyvt</div>


Here is an image description of the code output
enter image description here

so I want to get the caret position of the div and lets assume that the cursor is after the last character. And this is my code for getting the caret position

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

var update = function() {
console.log(getCaretPosition(this));
};
$('#text').on("mousedown mouseup keydown keyup", update);


But the problem is that it returns
6
instead of
14
. The caret position goes back to
0
after the image. Please is there a way I can get the caret position to be
14
in this case.

EDIT

I want to also insert some element starting from the caret position. so this is my function to do that

selectStart = 0;
var update = function() {
selectStart = getCaretPosition(this);
};
function insertEmoji(svg){
input = $('div#text').html();
beforeCursor = input.substring(0, selectStart);
afterCursor = input.substring(selectStart, input.length);
emoji = '<img src="images/smiley/'+svg+'.png" class="emojiText" />';
$('div#text').html(beforeCursor+emoji+afterCursor);
}

Answer

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>

I wrote my own function, based on Tim Down's, that works like you want it. I changed the treeWalker to filter NodeFilter.ELEMENT_NODE insted of NodeFilter.SHOW_TEXT, and now <img/> elements also get processed inside our loop. I start by storing the range.startOffset and then recurse through all the selection tree nodes. If it finds an img node, then it adds just 1 to the position; if the current node element is different than our range.startContainer, then it adds that node's length. The position is altered by a different variable lastNodeLength that is adds to the charCount at each loop. Finally, it adds whatever is left in the lastNodeLength to the charCount when it exists the loop and we have the correct final caret position, including image elements.

Final working code (it returns 14 at the end, exactly as it should and you want)

function getCharacterOffsetWithin_final(range, node) {
    var treeWalker = document.createTreeWalker(
        node,
        NodeFilter.ELEMENT_NODE,
        function(node) {
            var nodeRange = document.createRange();
            nodeRange.selectNodeContents(node);
            return nodeRange.compareBoundaryPoints(Range.END_TO_END, range) < 1 ?
                NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
        },
        false
    );

    var charCount = 0, lastNodeLength = 0;

    if (range.startContainer.nodeType == 3) {
        charCount += range.startOffset;
    }

    while (treeWalker.nextNode()) {
        charCount += lastNodeLength;
        lastNodeLength = 0;
        
        if(treeWalker.currentNode.nodeName.toLowerCase() == 'img') {
            lastNodeLength++;
        } else {
        	if(range.startContainer != treeWalker.currentNode) {
            	lastNodeLength += treeWalker.currentNode.length;
        	}
        }
    }
    
    return charCount + lastNodeLength;
}

var update = function() {
    var el = document.getElementById("text");
    var range = window.getSelection().getRangeAt(0);
    console.log("Caret pos: " + getCharacterOffsetWithin_final(range, el))
};
$('#text').on("mouseup 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 contenteditable="true" src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>