65

I have been looking high and low for an answer but failed.

Is there a cross-browser solution to replace selected text in contenteditable div?
I simply want users to highlight some text and replace the highlighted text with xxxxx.

vsync
  • 118,978
  • 58
  • 307
  • 400
Judy
  • 765
  • 1
  • 8
  • 9

3 Answers3

115

The following will do the job in all the major browsers:

function replaceSelectedText(replacementText) {
    var sel, range;
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.rangeCount) {
            range = sel.getRangeAt(0);
            range.deleteContents();
            range.insertNode(document.createTextNode(replacementText));
        }
    } else if (document.selection && document.selection.createRange) {
        range = document.selection.createRange();
        range.text = replacementText;
    }
}
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • This does not work for me in Chrome (pasting into INPUT, TEXTAREA, or content-editable). I suspect it's because Chrome does not support multiple ranges. It's falling into the window.getSelection case, but then sel.rangeCount is false, and there is no "else" clause.. – David Jeske Aug 15 '12 at 15:51
  • 3
    @DavidJeske: This definitely does work in Chrome for contenteditable elements, but not in inputs or textareas: in non-IE browsers, this code is specific to selections within regular content. – Tim Down Aug 15 '12 at 16:17
  • 1
    @TimDown I'm using some similar `rangy` code (to replace a text node on an iframe). Is it normal that using `sel.setSingleRange(range)` does not make the `replacementText` selected? – Sebastien Lorber Feb 28 '14 at 19:21
  • 1
    Not what OP has asked for, but just a reminder for others, that `getSelection` doesn't catch the selected text in Firefox when working with form fields: https://bugzilla.mozilla.org/show_bug.cgi?id=85686 – AXO Oct 31 '15 at 23:44
  • 3
    This solution has a bug: try to undo pasted text. – Alex Zinkevych Nov 20 '17 at 10:05
  • @АлексейЗинкевич: Could you be more specific? If you mean that running this code doesn't add anything to the browser's undo stack then that's true, but difficult to solve. – Tim Down Nov 20 '17 at 11:32
  • 5
    @TimDown Yes, undo stack is not handled in case of modifying it with range methods. So it would be better to add some checks like this: `if (document.queryCommandSupported('insertText')) { document.execCommand( 'insertText', false, replacementText ); } else { range.deleteContents(); range.insertNode(document.createTextNode(replacementText)); }` – Alex Zinkevych Nov 20 '17 at 13:50
  • 1
    @TimDown How would we do this for pasting HTML? – Explosion Feb 16 '21 at 22:15
  • @TimDown yeah how to replace with html – Dee Apr 11 '21 at 09:16
  • @Explosion i found out how to insert html, createElement, set innerHTML, and range.insertNode – Dee Apr 11 '21 at 09:19
3

As I posted a working example on how to add emoji in between the letters in contentEditable div? , you can use this for replacing the needed text in content editable div

  function pasteHtmlAtCaret(html) {
        let sel, range;
        if (window.getSelection) {
          // IE9 and non-IE
          sel = window.getSelection();
          if (sel.getRangeAt && sel.rangeCount) {
            range = sel.getRangeAt(0);
            range.deleteContents();

            // Range.createContextualFragment() would be useful here but is
            // non-standard and not supported in all browsers (IE9, for one)
            const el = document.createElement("div");
            el.innerHTML = html;
            let frag = document.createDocumentFragment(),
              node,
              lastNode;
            while ((node = el.firstChild)) {
              lastNode = frag.appendChild(node);
            }
            range.insertNode(frag);

            // Preserve the selection
            if (lastNode) {
              range = range.cloneRange();
              range.setStartAfter(lastNode);
              range.collapse(true);
              sel.removeAllRanges();
              sel.addRange(range);
            }
          }
        } else if (document.selection && document.selection.type != "Control") {
          // IE < 9
          document.selection.createRange().pasteHTML(html);
        }
      }

      function addToDiv(event) {
        const emoji = event.target.value;
        const chatBox = document.getElementById("chatbox");
        chatBox.focus();
        pasteHtmlAtCaret(`<b>${emoji}</b>`);
      }
      function generateEmojiIcon(emoji) {
        const input = document.createElement("input");
        input.type = "button";
        input.value = emoji;
        input.innerText = emoji;
        input.addEventListener("click", addToDiv);
        return input;
      }
      const emojis = [
        {
          emoji: "",
        },
        {
          emoji: "❤️",
        },
      ];
      emojis.forEach((emoji) => {
        document
          .getElementById("emojis")
          .appendChild(generateEmojiIcon(emoji.emoji));
      });
      #emojis span {
        cursor: pointer;
      }
      #chatbox {
        border: 1px solid;
      }
 <button
  type="button"
  onclick="document.getElementById('chatbox').focus(); 
  pasteHtmlAtCaret('<b>INSERTED</b>'); "
>
  Paste HTML
</button>
    <div id="emojis"></div>
    <div id="chatbox" contenteditable></div>
SeyyedKhandon
  • 5,197
  • 8
  • 37
  • 68
  • why does all the text become bold after clicking paste html? And how can the pasted text only be bold and the other ones be normal? – SAF Nov 24 '20 at 16:08
  • @SAF you can replace tag `b` in `INSERTED` and `${emoji}` in the js with what ever you like. You can use innerText instead of that with a little changes in the snippet code... – SeyyedKhandon Nov 24 '20 at 17:04
  • About the later question, I should explore with it a little more... – SeyyedKhandon Nov 24 '20 at 17:17
1

I needed to replace text nodes, and keep html entities when pasting back. This is how I solved the problem, not sure about data in textnodes, maybe it's better to use textContent or something else

let selection = window.getSelection()
  , range = selection.getRangeAt(0)
  , fragment = range.extractContents();

fragment.childNodes.forEach( e => e.data && (e.data = doSomething(e.data)) );
range.insertNode(fragment);
killovv
  • 51
  • 3