80

I have a div with contenteditable set and I am capturing keypress using jquery to call preventDefault() when the enter key is pressed. Similar to this question which inserts text at the cursor, I would like to directly insert html, for brevity we'll say its a br tag. Using the answer to the question above actually works in IE as it uses the range.pasteHTML method, but in other browsers the br tag would appear as plain text and not html. How could I modify the answer to insert html and not text ?

Community
  • 1
  • 1
Niall
  • 803
  • 1
  • 7
  • 4

4 Answers4

199

In most browsers, you can use the insertNode() method of the Range you obtain from the selection. In IE < 9 you can use pasteHTML(), as you mentioned. Below is a function to do this in all major browsers. If content is already selected, it is replaced, so this is effectively a paste operation. Also, I added code to place the caret after the end of the inserted content.

jsFiddle: http://jsfiddle.net/jwvha/1/

Code:

function pasteHtmlAtCaret(html) {
    var 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
            // only relatively recently standardized and is not supported in
            // some browsers (IE9, for one)
            var el = document.createElement("div");
            el.innerHTML = html;
            var 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);
    }
}

UPDATE 21 AUGUST 2013

As requested in the comments, here is an updated example with an extra parameter that specifies whether or not to select the inserted content.

Demo: http://jsfiddle.net/timdown/jwvha/527/

Code:

function pasteHtmlAtCaret(html, selectPastedContent) {
    var 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
            // only relatively recently standardized and is not supported in
            // some browsers (IE9, for one)
            var el = document.createElement("div");
            el.innerHTML = html;
            var frag = document.createDocumentFragment(), node, lastNode;
            while ( (node = el.firstChild) ) {
                lastNode = frag.appendChild(node);
            }
            var firstNode = frag.firstChild;
            range.insertNode(frag);

            // Preserve the selection
            if (lastNode) {
                range = range.cloneRange();
                range.setStartAfter(lastNode);
                if (selectPastedContent) {
                    range.setStartBefore(firstNode);
                } else {
                    range.collapse(true);
                }
                sel.removeAllRanges();
                sel.addRange(range);
            }
        }
    } else if ( (sel = document.selection) && sel.type != "Control") {
        // IE < 9
        var originalRange = sel.createRange();
        originalRange.collapse(true);
        sel.createRange().pasteHTML(html);
        if (selectPastedContent) {
            range = sel.createRange();
            range.setEndPoint("StartToStart", originalRange);
            range.select();
        }
    }
}
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • @Tim Just wondering, since I wouldn't want the users adding html everywhere (since the function is a button), how is it possible to only let this work in a certain div or element? – Lucas Aug 28 '12 at 07:04
  • 1
    @think123: You could use a function like the following to check the selection is contained within a particular node: http://stackoverflow.com/a/8340432/96100 – Tim Down Sep 07 '12 at 14:29
  • @TimDown is that jQuery compatible? (the `isOrContains` function) – Lucas Sep 07 '12 at 20:56
  • @think123: Everything is jQuery compatible: jQuery is just a library. – Tim Down Sep 08 '12 at 09:29
  • @TimDown I've tried using the code you wrote (http://jsfiddle.net/jwvha/1/) but it doesn't work when a jQuery library is loaded (http://jsfiddle.net/jwvha/209/). Any idea why this is happening? How can I remedy this? Thanks! Edit: When jquery is loaded, Chrome gives this console message when the button is clicked: "Uncaught ReferenceError: pasteHtmlAtCaret is not defined" – tundoopani Sep 19 '12 at 07:34
  • 1
    @tundoopani: That's because jsFiddle is placing the `pasteHtmlAtCaret ()` function in an `onload` handler where nothing else can see it. See http://jsfiddle.net/jwvha/211/ for the fix. – Tim Down Sep 19 '12 at 08:51
  • thanks Tim, but how we save focus on changed area after inner change?, for example SO editor, when we highlight a text and click on B, text still is selected or again become selected (highlighted). – Vahid Aug 21 '13 at 20:04
  • @Vahid: Here's a jsFiddle demo with an improved version of the function that has a flag to allow selecting the inserted content: http://jsfiddle.net/jwvha/468/ – Tim Down Aug 21 '13 at 22:09
  • For some reason rang.insertNode(frag) to iFrame it did not work for me, but seems I have other problem. However when I isolate the problem it look working. http://jsfiddle.net/mELCy/ – Laith Shadeed Dec 16 '13 at 11:56
  • Thanks @TimDown! Is there any way to get the browser to respect undo/redo when you inject html at the cursor? – Matt Jan 15 '14 at 11:56
  • 5
    @Matt: Using `document.execCommand()` usually works with the browser's undo stack, so you may be able to use `document.execCommand("InsertHTML", false, "Some bold text")`. However, I haven't tested that undo will still work with that, and IE does not support that command. Finally, there is an UndoManager spec in the works that will be the solution for this in the long term and is starting to get implemented in browsers: https://dvcs.w3.org/hg/undomanager/raw-file/tip/undomanager.html – Tim Down Jan 15 '14 at 12:29
  • Thanks a lot @TimDown! It turns out undo/redo does work with `insertHTML` :-) only pesky IE won't get the native undo/redo :-/ – Matt Jan 23 '14 at 14:07
  • @Matt: IE 11 has some new undo commands that may help. See http://stackoverflow.com/a/8544210/96100 and http://stackoverflow.com/a/20239519/96100 – Tim Down Jan 23 '14 at 14:44
  • @TimDown can you please tell me why IE insists on keeping the cursor before the node once inserted? I really need it to appear after. FF and Chrome appear to do this fine. Any suggestions? – scniro May 15 '14 at 20:40
  • @scniro: Which version of IE? – Tim Down May 27 '14 at 09:47
  • Thanks Tim, it worked perfectly! I just needed to adjust the code a bit to support elements from ` – gdoron Jul 06 '14 at 11:36
  • Why not using `document.execCommand("InsertHTML")` for browsers other than IE? (It better for undo/redo functions) – Taha Jahangir Feb 27 '15 at 17:42
  • @TahaJahangir: That may well be a good idea, but I'm not sure how consistently implemented it is. – Tim Down Feb 27 '15 at 17:56
  • @TimDown when I add another div tag and before click on button, click on added div tag then after click on button, in this scenario above code is not working correctly. Can you resolve it? Thanks. – Prashant16 Mar 23 '15 at 12:54
  • @gdoron in the example you posted above there is a parameter called window. What should I give it ? The id of the iframe or what? I'm not used to js. – Ced Sep 06 '15 at 10:03
  • 1
    @Ced, the window object of the iframe. Forgive me for referencing [this site](http://www.w3schools.com/jsref/prop_frame_contentwindow.asp), but this is the simplest example I found in the 10 secs I searched for you... – gdoron Sep 06 '15 at 10:58
  • Hi Jim, I want to use this code for compatible newline (enter keydown event) behaviour across browsers. I add "


    " on enter after some controls. But your code adds "


    in a nested way. How can I resolve that?
    – horse Apr 24 '16 at 15:54
  • Hi Tim,when there is nothing inside b tag,lets say-.its not working, i means when the user is typing - the text is not rendering in bold. Can you please help me with this. – Ujjwal Kumar Gupta Jan 06 '17 at 13:35
  • Why do you need createDocumentFragment()? What if we simply use range.insertNode(el.firstChild); https://codepen.io/greensuisse/pen/brELMa – greensuisse Jul 30 '17 at 11:33
  • @greensuisse: There may be multiple elements to insert and you'd only insert the first. https://codepen.io/anon/pen/YxqBvX – Tim Down Aug 01 '17 at 10:28
  • anyone got this to work with `document.execCommand('insertHtml')` AND putting the cursor after the inserted HTML? when i'm inserting an html element which is `contenteditable="false"` the cursor gets "swallowed" and I can't even move it. The problem with document.execCommand is, that you don't get a reference to the newly inserted element. – w.stoettinger Oct 23 '17 at 08:47
  • Very nice script but there is a problem when the content of the editable div field is larger than the field. Could you help? https://stackoverflow.com/questions/48224189/contenteditable-displaying-last-inserted-html-element Thanks! – Baylock Jan 15 '18 at 09:15
  • There are some way to make this functions works passing an id attribute of the contenteditable ? – Sangar82 Feb 13 '18 at 09:50
  • @TimDown how can we add new text outside of because i don't want to add next text inside bold tag...? – zulqarnain Aug 13 '19 at 17:48
  • This does not work in IE11. IE11 returns 0 on sel.rangeCount – Alex from Jitbit Aug 26 '19 at 08:27
  • @Alex: works for me, but it relies on the contenteditable element having focus. – Tim Down Aug 27 '19 at 09:15
  • @TimDown that is the problem, I'm responding to a `click` (usually wysiwyg-editor's toolbar buttons are outside the editing area) – Alex from Jitbit Aug 27 '19 at 09:55
  • 1
    @Alex: Your options are either 1) to use the `mousedown` event instead and prevent the default click action of the button; 2) make the toolbar button unselectable, or 3) save the selection before clicking the toolbar button (maybe via the `mousedown` event) and restore it afterwards (but before doing the insertion) – Tim Down Aug 27 '19 at 14:47
  • @TimDown when inserting a button as html, the cursor or caret placement is inside the button text. Do you have any solution for that? Thanks. Here is the [link](http://jsfiddle.net/7fd5hbu9/1/) – Abhishek Matta Dec 11 '20 at 16:30
  • Mine is not inserting at the caret position.@TimDown Do you know anything about this – Abhishek Matta Feb 19 '21 at 04:57
  • @shawnbatra: Works fine for me. Which browser? – Tim Down Mar 08 '21 at 09:56
  • @TimDown Thanks for replying. I make it work but in Firefox, I have inserted a button at cursor position and then I was not able to write before the button(Can't place cursor at start if button is the first element) Here's the [fiddle](http://jsfiddle.net/krwhse3p/1/) – Abhishek Matta Mar 08 '21 at 13:00
15
var doc = document.getElementById("your_iframe").contentWindow.document;

// IE <= 10
if (document.selection){
    var range = doc.selection.createRange();
        range.pasteHTML("<b>Some bold text</b>");

// IE 11 && Firefox, Opera .....
}else if(document.getSelection){
    var range = doc.getSelection().getRangeAt(0);
    var nnode = doc.createElement("b");
    range.surroundContents(nnode);
    nnode.innerHTML = "Some bold text";
};
idmean
  • 14,540
  • 9
  • 54
  • 83
user3126867
  • 610
  • 5
  • 8
1

by reading quickly and hoping not to be off topic, here is a track for those who, like me, need to insert code at the cursor level of a div:

document.getElementById('editeur').contentWindow.document.execCommand('insertHTML', false, '<br />');

'editeur' is iframe :

<iframe id="editeur" src="contenu_editeur_wysiwyg.php">
</iframe>

contenu_editeur_wysiwyg.php :

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div>
</div>
</body>
</html>

don't forget :

document.getElementById('editeur').contentDocument.designMode = "on";
delaio
  • 39
  • 1
-1
var r = getSelection().getRangeAt(0);
r.insertNode(r.createContextualFragment('<b>Hello</b>'));

//select this range
getSelection().removeAllRanges();
getSelection().addRange(r);
//collapse to end/start 
getSelection().collapseToEnd() 
Alex Gulakov
  • 87
  • 1
  • 5
  • This won't work for content added in the middle of an input for example, the selection after insertion would always move to the end. – Mario Estrada Aug 27 '20 at 00:20