Search code examples
javascriptcontenteditablecaret

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


My HTML:

<div id="text" contenteditable="true">abcd<img src="icon.gif"/>efgh</div>

My caret = 5; so I want to set the caret poisition to be immediately after the image as the image is treated as 1 character.

So I wrote thise code:

var node = document.querySelector("div");
node.focus();
var textNode = node.firstChild;
var caret = 5; 
var range = document.createRange();
range.setStart(textNode, caret);
range.setEnd(textNode, caret);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);

But I get this error:

Uncaught DOMException: Failed to execute 'setStart' on 'Range': The offset 5 is larger than or equal to the node's length (4).

Please suggest how I might achieve this ? I might have several images before the caret position I desire and each image would be treated as 1 character.


Solution

  • 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>