30

I'm inserting an element into a contentEditable div but the browser sets the position of the cursor before the inserted element. Is it possible to set the cursor right after the inserted element so that the user keeps typing without having to re-adjust the cursor position?

Elie
  • 6,915
  • 7
  • 31
  • 35
  • Related: http://stackoverflow.com/questions/2920150/insert-text-at-cursor-in-a-content-editable-div – payne Jan 29 '11 at 02:46
  • That doesn't answer my question. I am able to insert the element at the caret position, but I need to place the caret right after the inserted element. – Elie Jan 29 '11 at 02:48
  • Have you tried to simulate a keyboard event after inserting the string, such as "end" (keycode #35) key on the keyboard. – jnkrois Jan 29 '11 at 03:10
  • That didn't work for some reason (tested on Google Chrome) – Elie Jan 29 '11 at 03:29
  • 1
    While it is possible to simulate a key event, it's not possible in general to simulate the actual UI effect of a key event. – Tim Down Jan 29 '11 at 12:22

3 Answers3

29

The following function will do it. DOM Level 2 Range objects make this easy in most browsers. In IE, you need to insert a marker element after the node you're inserting, move the selection to it and then remove it.

Live example: http://jsfiddle.net/timdown/4N4ZD/

Code:

function insertNodeAtCaret(node) {
    if (typeof window.getSelection != "undefined") {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            var range = sel.getRangeAt(0);
            range.collapse(false);
            range.insertNode(node);
            range = range.cloneRange();
            range.selectNodeContents(node);
            range.collapse(false);
            sel.removeAllRanges();
            sel.addRange(range);
        }
    } else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
        var html = (node.nodeType == 1) ? node.outerHTML : node.data;
        var id = "marker_" + ("" + Math.random()).slice(2);
        html += '<span id="' + id + '"></span>';
        var textRange = document.selection.createRange();
        textRange.collapse(false);
        textRange.pasteHTML(html);
        var markerSpan = document.getElementById(id);
        textRange.moveToElementText(markerSpan);
        textRange.select();
        markerSpan.parentNode.removeChild(markerSpan);
    }
}

Alternatively, you could use my Rangy library. The equivalent code there would be

function insertNodeAtCaret(node) {
    var sel = rangy.getSelection();
    if (sel.rangeCount) {
        var range = sel.getRangeAt(0);
        range.collapse(false);
        range.insertNode(node);
        range.collapseAfter(node);
        sel.setSingleRange(range);
    }
}
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • 4
    Heh. This is funny: "DOM Level 2 Range objects make this easy in most browsers." Been trying various permutations of this for three days with little success. (I know, not a helpful comment, but just had to say it) – eon Jan 13 '12 at 15:45
  • @eon: :) Perhaps I should have qualified that: "Compared to the nasty hack necessary in IE < 9, DOM Level 2 Range objects make this easy in most browsers." – Tim Down Jan 13 '12 at 16:06
  • 1
    Only seemed to work for text node. Tried giving it a span element and the cursor ends up before the span?? – user984003 May 19 '13 at 19:22
  • This one also worked for a non-text element (at least an empty li) http://stackoverflow.com/questions/9828623/inserting-cursor-after-an-inserted-node – user984003 May 19 '13 at 19:34
1

If you're inserting an empty div, p or span, I believe there needs to be "something" inside the newly created element for the range to grab onto -- and in order to put the caret inside there.

Here's my hack that seems to work OK in Chrome. The idea is simply to put a temporary string inside the element, then remove it once the caret is in there.

// Get the selection and range
var idoc = document; // (In my case it's an iframe document)
var sel = idoc.getSelection();
var range = sel.getRangeAt(0);

// Create a node to insert
var p = idoc.createElement("p"); // Could be a div, span or whatever

// Add "something" to the node.
var temp = idoc.createTextNode("anything");
p.appendChild(temp);
// -- or --
//p.innerHTML = "anything";

// Do the magic (what rangy showed above)
range.collapse(false);
range.insertNode( p );
range = range.cloneRange();
range.selectNodeContents(p);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);

// Clear the non
p.removeChild(p.firstChild);
// -- or --
//p.innerHTML = "";
bob
  • 7,539
  • 2
  • 46
  • 42
1

Here's what worked for me, using Rangy, in a VueJS context.

// When the user clicks the button to open the popup to enter
// the URL, run this function to save the location of the user's
// selection and the selected text.
newSaveSel: function() {
  if (this.savedSel) {
    rangy.removeMarkers(this.savedSel);
  }
  // Save the location of the selected text
  this.savedSel = rangy.saveSelection();
  // Save the selected text
  this.savedSelText = rangy.getSelection().toString();
  this.showLinkPopup = true;
  console.log('newSavedSel', this.savedSel);
},
surroundRange: function() {
  // Restore the user's selected text. This is necessary since
  // the selection is lost when the user stars entering text.
  if (this.savedSel) {
    rangy.restoreSelection(this.savedSel, true);
    this.savedSel = null;
  }
  // Surround the selected text with the anchor element
  var sel = rangy.getSelection();

  var range = sel.rangeCount ? sel.getRangeAt(0) : null;
  if (range) {
    // Create the new anchor element
    var el = document.createElement("a");
    el.style.backgroundColor = "pink";
    el.href = this.anchorHref;
    el.innerHTML = this.savedSelText;
    if (this.checked) {
      el.target = "_blank";
    }
    // Delete the originally selected text
    range.deleteContents();
    // Insert the anchor tag
    range.insertNode(el);
    // Ensure that the caret appears at the end
    sel.removeAllRanges();
    range = range.cloneRange();
    range.selectNode(el);
    range.collapse(false);
    sel.addRange(range);
    this.showLinkPopup = false; 
  }
},
Dox
  • 571
  • 1
  • 5
  • 15