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

1

11

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);
}
Corporate answered 22/11, 2016 at 17:5 Comment(0)
I
13

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(range.startContainer != treeWalker.currentNode) {
            if(treeWalker.currentNode instanceof Text) {
                lastNodeLength += treeWalker.currentNode.length;
            } else if(treeWalker.currentNode instanceof HTMLBRElement ||
                      treeWalker.currentNode instanceof HTMLImageElement /* ||
                      treeWalker.currentNode instanceof HTMLDivElement*/)
            {
                lastNodeLength++;
            }
        }
    }
    
    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>
Ilowell answered 22/11, 2016 at 18:16 Comment(9)
Your code is cool but i need to detect all html contents of the div not onle the text, because I will be inserting some characters into the div at the caret position. Look at my question edit.Corporate
@doggiebrezy I made it work like you want it. See my updated answer. I've tested it on latest Chrome and Firefox. It does not work on IE though.Ilowell
thanks for your edit but please help me with one final thing. Now I want to insert some text or image at the caret position and so I split the div.html but the issue is that assuming the caret position is 11, the inserted text doesnt count that image as one but the whole img.html there by misplacing the point at which it is meant to be inserted, please can you help me count how many string in the div.html including the img elementCorporate
@doggiebrezy I think that's easier to just add something (text or image) to the currentNode of the selection. I'll work on it when I find some time soon.Ilowell
Thanks @christoslytras. I did figure out a way. and it works perfect. Thanks so much for your contribution, I appreciateCorporate
add some line breaks and the caret position = NaN :(Genovevagenre
@Genovevagenre I have updated the code so it counts the BR elements, but it misses the first break. I don't have much time to investigate why is this happening and fix it, but now it works and there is no NaN results. Please let me know if you find a solution for the missing BR after each line.Ilowell
I´m crazy searching the solution... I changed few things and I noticed too that it misses the first break, but I'm looking to fix it, but at this moment, I didn't found a solution. If you have time, I would appreciate your help. Thanks!!Genovevagenre
@doggie brezy Can you please tell me how you added the img/text at the endCrapshooter

© 2022 - 2024 — McMap. All rights reserved.