How to place the caret where it previously was after replacing the html of a contenteditable div?
Asked Answered
P

1

3

I have a contenteditable div where I replace hashtags with clickable links when the user clicks on space or on enter. The user writes down:

I love but there is no fish at home|

He then realizes he made a mistake and then decides to go back and write

I love #sushi | but there is no fish at home

#sushi 

gets replaced by:

 <a href="https://google.com/sushi>#sushi</a>

Notice that the | shows the position of where I want the caret to be when the user presses spacebar. My current "placeCaretAtEnd" function places the caret at the end of the div and NOT behind the link that I just replaced sushi with. Is there a way to alter my current function to place the caret behind the link I just replaced in the text on the position shown above, so that the user can continue typing carelessly? So in raw html:

I love < a> #sushi< /a> | but there is no fish at home

/**
* Trigger when someone releases a key on the field where you can post remarks, posts or reactions
*/
$(document).on("keyup", ".post-input-field", function (event) {

       // if the user has pressed the spacebar (32) or the enter key (13)
       if (event.keyCode === 32 || event.keyCode === 13) {
          let html = $(this).html();
          html = html.replace(/(^|\s)(#\w+)/g, " <a href=#>$2</a>").replace("<br>", ""); 
          $(this).html(html);
          placeCaretAtEnd($(this)[0]);
       }
  });

 /**
 * Place the caret at the end of the textfield
 * @param {object} el - the DOM element where the caret should be placed 
 */
function placeCaretAtEnd(el) {

    el.focus();
    if (typeof window.getSelection != "undefined"
        && typeof document.createRange != "undefined") {
        var range = document.createRange();
        range.selectNodeContents(el);
        range.collapse(false);
        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (typeof document.body.createTextRange != "undefined"){
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(el);
        textRange.collapse(false);
        textRange.select();
    }
 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div contenteditable="true" class="post-input-field">
I love (replace this with #sushi and type space) but there is no fish at home
</div>
Peroxy answered 16/8, 2019 at 15:23 Comment(0)
F
4

rangy.js has a text range module that lets you save selection as indices of the characters that are selected, and then restore it. Since your modifications do not alter innerText, this looks like a perfect fit:

      var sel = rangy.getSelection();
      var savedSel = sel.saveCharacterRanges(this);
      $(this).html(html);
      sel.restoreCharacterRanges(this, savedSel);

Implementing this manually requires careful traversal of the DOM inside contenteditable and careful arithmetics with the indices; this can't be done with a few lines of code.

Your placeCaretAtEnd can't possibly place the caret after the link you've inserted, since it doesn't "know" which (of the possibly multiple) link it is. You have to save this information beforehand.

/**
* Trigger when someone releases a key on the field where you can post remarks, posts or reactions
*/
$(document).on("keyup", ".post-input-field", function (event) {

       // if the user has pressed the spacebar (32) or the enter key (13)
       if (event.keyCode === 32 || event.keyCode === 13) {
          let html = $(this).html();
          html = html.replace(/(^|\s)(#\w+)/g, " <a href=#>$2</a>").replace("<br>", ""); 
          var sel = rangy.getSelection();
          var savedSel = sel.saveCharacterRanges(this);
          $(this).html(html);
          sel.restoreCharacterRanges(this, savedSel);
       }
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-core.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/rangy/1.3.0/rangy-textrange.js"></script>
<div contenteditable="true" class="post-input-field">
I love (replace this with #sushi and type space) but there is no fish at home
</div>
Faun answered 18/8, 2019 at 15:38 Comment(4)
Hi Nick ,how can we do the same thing in sveltekit? can you please take a look, I've tried doing the same but its not that much jqury friendly..this is my code so far function blueColor() { tweet = tweet.replace(/(^|\s)(#\w+)/g, " <a href=#>$2</a>").replace("<br>", ""); var savedSel = sel.saveCharacterRanges(this); sel.restoreCharacterRanges(this, savedSel); }Aran
@Aran your problem is unrelated and you'll have better chances if you ask a separate easy-to-answer question. Your code uses undefined tweet variable and is missing a call to actually update the innerHTML of the contenteditable ($(elt).html(str) in jQuery).Faun
#73649510 here it is but I recently saw you answerAran
Posted there https://mcmap.net/q/1634751/-color-hashtags-while-typing-like-in-twitter-with-svelte (only as a Svelte learning exercise; do not expect to get a production-ready solution to this on stackoverflow...)Faun

© 2022 - 2024 — McMap. All rights reserved.