Insert html at caret in a contenteditable div
Asked Answered
P

4

81

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 ?

Pennell answered 14/7, 2011 at 8:57 Comment(0)
A
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();
        }
    }
}
Assignment answered 14/7, 2011 at 9:47 Comment(37)
@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?Friedafriedberg
@think123: You could use a function like the following to check the selection is contained within a particular node: https://mcmap.net/q/82975/-how-to-know-if-selected-text-is-inside-a-specific-divAssignment
@TimDown is that jQuery compatible? (the isOrContains function)Friedafriedberg
@think123: Everything is jQuery compatible: jQuery is just a library.Assignment
@TimDown I've tried using the code you wrote (jsfiddle.net/jwvha/1) but it doesn't work when a jQuery library is loaded (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"Gusgusba
@tundoopani: That's because jsFiddle is placing the pasteHtmlAtCaret () function in an onload handler where nothing else can see it. See jsfiddle.net/jwvha/211 for the fix.Assignment
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).Luteous
@Vahid: Here's a jsFiddle demo with an improved version of the function that has a flag to allow selecting the inserted content: jsfiddle.net/jwvha/468Assignment
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. jsfiddle.net/mELCyWork
Thanks @TimDown! Is there any way to get the browser to respect undo/redo when you inject html at the cursor?Bosom
@Matt: Using document.execCommand() usually works with the browser's undo stack, so you may be able to use document.execCommand("InsertHTML", false, "<b>Some bold text</b>"). 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: dvcs.w3.org/hg/undomanager/raw-file/tip/undomanager.htmlAssignment
Thanks a lot @TimDown! It turns out undo/redo does work with insertHTML :-) only pesky IE won't get the native undo/redo :-/Bosom
@Matt: IE 11 has some new undo commands that may help. See https://mcmap.net/q/82976/-can-a-range-insertnode-be-undone-using-browser-39-s-undo-in-a-contenteditable-div and https://mcmap.net/q/82977/-internet-explorer-alternative-to-document-execcommand-quot-inserttext-quot-for-text-insertion-that-can-be-undone-redone-by-the-userAssignment
@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?Someday
@scniro: Which version of IE?Assignment
Thanks Tim, it worked perfectly! I just needed to adjust the code a bit to support elements from <iframe>. My version is hereBeckford
Why not using document.execCommand("InsertHTML") for browsers other than IE? (It better for undo/redo functions)Corporal
@TahaJahangir: That may well be a good idea, but I'm not sure how consistently implemented it is.Assignment
@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.Sidesman
@Beckford 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.Sclerosis
@Ced, the window object of the iframe. Forgive me for referencing this site, but this is the simplest example I found in the 10 secs I searched for you...Beckford
Hi Jim, I want to use this code for compatible newline (enter keydown event) behaviour across browsers. I add "<p><br></p>" on enter after some controls. But your code adds "<p><br></p> in a nested way. How can I resolve that?Sapiential
Hi Tim,when there is nothing inside b tag,lets say-<b></b>.its not working, i means when the user is typing - the text is not rendering in bold. Can you please help me with this.Explicit
Why do you need createDocumentFragment()? What if we simply use range.insertNode(el.firstChild); codepen.io/greensuisse/pen/brELMaAleece
@greensuisse: There may be multiple elements to insert and you'd only insert the first. codepen.io/anon/pen/YxqBvXAssignment
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.Porkpie
Very nice script but there is a problem when the content of the editable div field is larger than the field. Could you help? #48224689 Thanks!Merrymaking
There are some way to make this functions works passing an id attribute of the contenteditable ?Harvin
@TimDown how can we add new text outside of <b> because i don't want to add next text inside bold tag...?Ballast
This does not work in IE11. IE11 returns 0 on sel.rangeCountPerfumery
@Alex: works for me, but it relies on the contenteditable element having focus.Assignment
@TimDown that is the problem, I'm responding to a click (usually wysiwyg-editor's toolbar buttons are outside the editing area)Perfumery
@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)Assignment
@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 linkBrest
Mine is not inserting at the caret position.@TimDown Do you know anything about thisBrest
@shawnbatra: Works fine for me. Which browser?Assignment
@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 fiddleBrest
P
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";
};
Polysyllable answered 21/12, 2013 at 13:18 Comment(0)
T
0

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";
Thielen answered 18/8, 2019 at 19:52 Comment(0)
B
-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() 
Bill answered 15/4, 2016 at 21:31 Comment(1)
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.Massimo

© 2022 - 2024 — McMap. All rights reserved.