title: Fix cursor position when replacing innerHTML of <div contenteditable="true">
body: <p>How can I keep the cursor in the right place when typing inside of a <code>&lt;div id=&quot;richTextBox&quot; contenteditable=&quot;true&quot;&gt;&lt;/div&gt;</code> whose innerHTML changes on each keystroke? The act of replacing the innerHTML messes up the cursor position.</p>
  <p>The reason I change the innerHTML is because I am adding <code>&lt;span&gt;</code> tags. It's part of a code highlighting program. The span tags allow me to place the correct color  highlights.</p>
  <p>I am using the below code from a StackOverflow answer as a band aid for the moment, but it has a significant bug. If you hit <em><strong>enter</strong></em>, the cursor stays at the old spot, or goes to a random spot. That's because the algorithm counts how many characters from the beginning the cursor is. But it doesn't count HTML tags or line breaks as characters. And the richTextBox  inserts <code>&lt;br&gt;</code> to make enters.</p>
  <p>Ideas for fixing:</p>
  <ul>
  <li>Fix the below code? See <a href="https://jsfiddle.net/AdmiralAkbar2/jou3vq9w/12/" rel="nofollow noreferrer">Fiddle</a></li>
  <li>Replace with simpler code? I tried a bunch of simpler stuff involving <code>window.getSelection()</code> and <code>document.createRange()</code>, but I could not get that to work.</li>
  <li>Replace with a richTextBox library or module that doesn't have this bug?</li>
  </ul>
  <h2>Screenshot</h2>
  <p><a href="https://i.stack.imgur.com/6dlqV.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/6dlqV.png" alt="screenshot of richTextBox rendered in JSFiddle" /></a></p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>// Credit to Liam (Stack Overflow)
  // https://stackoverflow.com/a/41034697/3480193
  class Cursor {
    static getCurrentCursorPosition(parentElement) {
      var selection = window.getSelection(),
        charCount = -1,
        node;
  
      if (selection.focusNode) {
        if (Cursor._isChildOf(selection.focusNode, parentElement)) {
          node = selection.focusNode; 
          charCount = selection.focusOffset;
  
          while (node) {
            if (node === parentElement) {
              break;
            }
  
            if (node.previousSibling) {
              node = node.previousSibling;
              charCount += node.textContent.length;
            } else {
              node = node.parentNode;
              if (node === null) {
                break;
              }
            }
          }
        }
      }
  
      return charCount;
    }
  
    static setCurrentCursorPosition(chars, element) {
      if (chars &gt;= 0) {
        var selection = window.getSelection();
  
        let range = Cursor._createRange(element, { count: chars });
  
        if (range) {
          range.collapse(false);
          selection.removeAllRanges();
          selection.addRange(range);
        }
      }
    }
  
    static _createRange(node, chars, range) {
      if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
      }
  
      if (chars.count === 0) {
        range.setEnd(node, chars.count);
      } else if (node &amp;&amp; chars.count &gt;0) {
        if (node.nodeType === Node.TEXT_NODE) {
          if (node.textContent.length &lt; chars.count) {
            chars.count -= node.textContent.length;
          } else {
            range.setEnd(node, chars.count);
            chars.count = 0;
          }
        } else {
          for (var lp = 0; lp &lt; node.childNodes.length; lp++) {
            range = Cursor._createRange(node.childNodes[lp], chars, range);
  
            if (chars.count === 0) {
            break;
            }
          }
        }
      } 
  
      return range;
    }
  
    static _isChildOf(node, parentElement) {
      while (node !== null) {
        if (node === parentElement) {
          return true;
        }
        node = node.parentNode;
      }
  
      return false;
    }
  }
  
  window.addEventListener('DOMContentLoaded', (e) =&gt; {
    let richText = document.getElementById('rich-text');
  
    richText.addEventListener('input', function(e) {
      let offset = Cursor.getCurrentCursorPosition(richText);
      // Pretend we do stuff with innerHTML here. The innerHTML will end up getting replaced with slightly changed code.
      let s = richText.innerHTML;
      richText.innerHTML = "";
      richText.innerHTML = s;
      Cursor.setCurrentCursorPosition(offset, richText);
      richText.focus(); // blinks the cursor
    });
  });</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    margin: 1em;
  }
  
  #rich-text {
    width: 100%;
    height: 450px;
    border: 1px solid black;
    cursor: text;
    overflow: scroll;
    resize: both;
    /* in Chrome, must have display: inline-block for contenteditable=true to prevent it from adding &lt;div&gt; &lt;p&gt; and &lt;span&gt; when you type. */
    display: inline-block;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;p&gt;
  Click somewhere in the middle of line 1. Hit enter. Start typing. Cursor is in the wrong place.
  &lt;/p&gt;
  
  &lt;p&gt;
  Reset. Click somewhere in the middle of line 1. Hit enter. Hit enter again. Cursor goes to some random place.
  &lt;/p&gt;
  
  &lt;div id="rich-text" contenteditable="true"&gt;Testing 123&lt;br /&gt;Testing 456&lt;/div&gt;</code></pre>
  </div>
  </div>
  </p>
  <h2>Browser</h2>
  <p>Google Chrome v83, Windows 7</p>