119

I need to move caret to end of contenteditable node like on Gmail notes widget.

I read threads on StackOverflow, but those solutions are based on using inputs and they doesn't work with contenteditable elements.

Vito Gentile
  • 13,336
  • 9
  • 61
  • 96
avsej
  • 3,822
  • 4
  • 26
  • 31

9 Answers9

289

Geowa4's solution will work for a textarea, but not for a contenteditable element.

This solution is for moving the caret to the end of a contenteditable element. It should work in all browsers which support contenteditable.

function setEndOfContenteditable(contentEditableElement)
{
    var range,selection;
    if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
    {
        range = document.createRange();//Create a range (a range is a like the selection but invisible)
        range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        selection = window.getSelection();//get the selection object (allows you to change selection)
        selection.removeAllRanges();//remove any selections already made
        selection.addRange(range);//make the range you have just created the visible selection
    }
    else if(document.selection)//IE 8 and lower
    { 
        range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
        range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        range.select();//Select the range (make it the visible selection
    }
}

It can be used by code similar to:

elem = document.getElementById('txt1');//This is the element that you want to move the caret to the end of
setEndOfContenteditable(elem);
Nico Burns
  • 16,639
  • 10
  • 40
  • 54
  • 1
    geowa4's solution will work for textarea's in chrome, it will not work for contenteditable elements in any browser. mine works for contenteditable elements, but not for textareas. – Nico Burns Oct 05 '10 at 18:11
  • 5
    This is the correct answer to this question, perfect, thanks Nico. – Rob Apr 30 '12 at 10:17
  • You can then move the cursor around with `selection.modify('move', 'left', 'character')`. Works in Chrome for me, see [Selection#modify](https://developer.mozilla.org/en/DOM/Selection/modify) and [Selection](https://developer.mozilla.org/en/DOM/Selection) docs. – Lance Jul 06 '12 at 00:12
  • Yes, although I don't really recommend this as older versions of FF ( <4 ) don't support it, and I don't think opera does either. Ie has it's own version: http://msdn.microsoft.com/en-us/library/ie/ms536616(v=vs.85).aspx. Or see http://code.google.com/p/rangy/ for a cross browser range implementation, which includes methods like these (see the textrange module) – Nico Burns Jul 06 '12 at 12:34
  • 8
    The `selectNodeContents` part of Nico's was giving me errors in both Chrome and FF (didn't test other browsers) until I found out that I apparently needed to add `.get(0)` to the element that I was feeding the function. I guess this has to do with me using jQuery instead of bare JS? I learned this from @jwarzech at [question 4233265](http://stackoverflow.com/questions/4233265/contenteditable-set-caret-at-the-end-of-the-text-cross-browser). Thanks to all! – Max Starkenburg Sep 07 '12 at 02:05
  • 5
    Yes, the function expects a DOM element, not a jQuery object. `.get(0)` retrieves the dom element which jQuery stores internally. You can also append `[0]`, which is equivalent to `.get(0)` in this context. – Nico Burns Sep 07 '12 at 16:00
  • This through a error "Error: The operation is insecure." for range.selectNodeContents – Ross Nov 22 '12 at 09:08
  • @Ross Are you using an Iframe? I think the `document` from `document.createRange()` might have to be the same document that the node (element) you are trying to select is in... – Nico Burns Nov 22 '12 at 15:28
  • Is there a nicer version that uses the `Rangy` library? – Hengjie Nov 15 '13 at 02:21
  • @Hengje: Yes! I believe `var range = rangy.createRange().selectNodeContents(contentEditableElement).collapse(false); rangy.getSelection().setSingleRange(range);` should work (although I haven't tested it). This is essentially the first fork of the above function with DOM range calls replaced with rangy calls. – Nico Burns Nov 18 '13 at 05:26
  • TypeError: Argument 1 of Range.selectNodeContents does not implement interface Node. – Arvind Bhardwaj Feb 19 '14 at 03:47
  • 1
    @Nico Burns: I tried your method and it didn't work on FireFox. – Lewis May 24 '14 at 18:05
  • These two lines worked for me to place the cursor at the end: `range.collapse(false); range.setEnd(contentEditableElement, 2);` – thdoan Oct 23 '18 at 01:04
  • 1
    make sure to call yourElement.focus() before trying to position the cursor (as in this answer), otherwise it won't work. cheers! – Greg Sadetsky Jun 02 '19 at 23:36
  • This works fine. Tested on chrome 71.0.3578.98 and WebView on Android 5.1. – maswerdna Aug 01 '19 at 18:31
  • Solution works great but when I try to get the range after that the end offset doesn't reflect the fact the cursor is at the end. Any idea why? – user1732055 Oct 07 '19 at 18:40
  • @user1732055 Not off the top of my head. Which browser is this? Could you explain more context about what you are trying to do? (maybe write it up as a question?) – Nico Burns Oct 08 '19 at 12:10
  • @NicoBurns Yes. After using that code and getting the windo.getSelection() range, the offsets don't reflect the cursor positioning unless something happens. I have a onKeyUp event and after I move the cursor the range is not updated unless I key up a second time. This is in Chrome – user1732055 Oct 08 '19 at 16:34
47

If you don't care about older browsers, this one did the trick for me.

// [optional] make sure focus is on the element
yourContentEditableElement.focus();
// select all the content in the element
document.execCommand('selectAll', false, null);
// collapse selection to the end
document.getSelection().collapseToEnd();
Juank
  • 6,096
  • 1
  • 28
  • 28
31

There is also another problem.

The Nico Burns's solution works if the contenteditable div doesn't contain other multilined elements.

For instance, if a div contains other divs, and these other divs contain other stuff inside, could occur some problems.

In order to solve them, I've arranged the following solution, that is an improvement of the Nico's one:

//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {

    //From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
    var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];

    //From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
    Array.prototype.contains = function(obj) {
        var i = this.length;
        while (i--) {
            if (this[i] === obj) {
                return true;
            }
        }
        return false;
    }

    //Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
    function canContainText(node) {
        if(node.nodeType == 1) { //is an element node
            return !voidNodeTags.contains(node.nodeName);
        } else { //is not an element node
            return false;
        }
    };

    function getLastChildElement(el){
        var lc = el.lastChild;
        while(lc && lc.nodeType != 1) {
            if(lc.previousSibling)
                lc = lc.previousSibling;
            else
                break;
        }
        return lc;
    }

    //Based on Nico Burns's answer
    cursorManager.setEndOfContenteditable = function(contentEditableElement)
    {

        while(getLastChildElement(contentEditableElement) &&
              canContainText(getLastChildElement(contentEditableElement))) {
            contentEditableElement = getLastChildElement(contentEditableElement);
        }

        var range,selection;
        if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
        {    
            range = document.createRange();//Create a range (a range is a like the selection but invisible)
            range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
            range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
            selection = window.getSelection();//get the selection object (allows you to change selection)
            selection.removeAllRanges();//remove any selections already made
            selection.addRange(range);//make the range you have just created the visible selection
        }
        else if(document.selection)//IE 8 and lower
        { 
            range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
            range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
            range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
            range.select();//Select the range (make it the visible selection
        }
    }

}( window.cursorManager = window.cursorManager || {}));

Usage:

var editableDiv = document.getElementById("my_contentEditableDiv");
cursorManager.setEndOfContenteditable(editableDiv);

In this way, the cursor is surely positioned at the end of the last element, eventually nested.

EDIT #1: In order to be more generic, the while statement should consider also all the other tags which cannot contain text. These elements are named void elements, and in this question there are some methods on how to test if an element is void. So, assuming that exists a function called canContainText that returns true if the argument is not a void element, the following line of code:

contentEditableElement.lastChild.tagName.toLowerCase() != 'br'

should be replaced with:

canContainText(getLastChildElement(contentEditableElement))

EDIT #2: The above code is fully updated, with every changes described and discussed

Community
  • 1
  • 1
Vito Gentile
  • 13,336
  • 9
  • 61
  • 96
  • Interesting, I would have expected the browser to take care of this case automatically (not that I'm surprised that it doesn't, browsers never seem to do the intuitive things with contenteditable). Do you have an example of HTML where your solution works but mine doesn't? – Nico Burns Nov 18 '13 at 05:18
  • In my code there was one other error. I fixed it. Now, you can verify that my code works in [this page](http://html5demos.com/contenteditable), while yours doesn't – Vito Gentile Nov 18 '13 at 16:48
  • I am getting an error using your function, the console says `Uncaught TypeError: Cannot read property 'nodeType' of null` and this is from the getLastChildElement function being called. Do you know what might be causing this problem? – Derek Jan 02 '15 at 21:56
  • @VitoGentile it's a little old answer but I want to notice that your solution only takes care of block elements, if there is in-line elements inside, then the cursor will be positioned after that inline element (like span, em ...), an easy fix is to consider inline elements as void tags and add them to voidNodeTags so they will be skipped. – medBouzid May 01 '16 at 00:23
19

It's possible to do set cursor to the end through the range:

setCaretToEnd(target/*: HTMLDivElement*/) {
  const range = document.createRange();
  const sel = window.getSelection();
  range.selectNodeContents(target);
  range.collapse(false);
  sel.removeAllRanges();
  sel.addRange(range);
  target.focus();
  range.detach(); // optimization

  // set scroll to the end if multiline
  target.scrollTop = target.scrollHeight; 
}
am0wa
  • 7,724
  • 3
  • 38
  • 33
  • Using the code above it does the trick - but I want to be able to then have the ability to move the cursor anywhere within the content editable div and continue typing from that point - e.g user has recognised a typo for instance... How would I amend your code above to this? – Zabs Feb 16 '18 at 11:45
  • 2
    @Zabs it's fairly easy: do not invoke `setCaretToEnd()` each time - invoke it only when you need it: e.g. after Copy-Paste, or after restricting the message length. – am0wa Feb 21 '18 at 10:15
  • This worked for me. after the user selects a tag, I move the cursor in the contenteditable div to the end. – Ayudh May 04 '20 at 09:27
  • Nice solution that isn't from the stone ages like 99% of SO answers and isn't deprecated – Martin Dawson Oct 06 '21 at 13:28
16

A shorter and readable version using only selection (without range):

function setEndOfContenteditable(elem) {
    let sel = window.getSelection();
    sel.selectAllChildren(elem);
    sel.collapseToEnd();
}
<p id="pdemo" contenteditable>
A paragraph <span id="txt1" style="background-color: #0903">span text node <i>span italic</i></span> a paragraph.
<p>

<button onclick="pdemo.focus(); setEndOfContenteditable(txt1)">set caret</button>

Quite useful: https://javascript.info/selection-range

Avatar
  • 14,622
  • 9
  • 119
  • 198
5

Moving cursor to the end of editable span in response to focus event:

  moveCursorToEnd(el){
    if(el.innerText && document.createRange)
    {
      window.setTimeout(() =>
        {
          let selection = document.getSelection();
          let range = document.createRange();

          range.setStart(el.childNodes[0],el.innerText.length);
          range.collapse(true);
          selection.removeAllRanges();
          selection.addRange(range);
        }
      ,1);
    }
  }

And calling it in event handler (React here):

onFocus={(e) => this.moveCursorToEnd(e.target)}} 
Maxim Saplin
  • 4,115
  • 38
  • 29
1

The problem with contenteditable <div> and <span> is resolved when you start typing in it initially. One workaround for this could be triggering a focus event on your div element and on that function, clear, and refill what was already in the div element. This way the problem is resolved and finally you can place the cursor at the end using range and selection. Worked for me.

  moveCursorToEnd(e : any) {
    let placeholderText = e.target.innerText;
    e.target.innerText = '';
    e.target.innerText = placeholderText;

    if(e.target.innerText && document.createRange)
    {
      let range = document.createRange();
      let selection = window.getSelection();
      range.selectNodeContents(e.target);
      range.setStart(e.target.firstChild,e.target.innerText.length);
      range.setEnd(e.target.firstChild,e.target.innerText.length);
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

In HTML code:

<div contentEditable="true" (focus)="moveCursorToEnd($event)"></div>
Dharman
  • 30,962
  • 25
  • 85
  • 135
Nida
  • 11
  • 1
0

I had a similar problem trying to make a element editable. It was possible in Chrome and FireFox but in FireFox the caret either went to the beginning of the input or it went one space after the end of the input. Very confusing to the end-user I think, trying to edit the content.

I found no solution trying several things. Only thing that worked for me was to "go around the problem" by putting a plain old text-input INSIDE my . Now it works. Seems like "content-editable" is still bleeding edge tech, which may or may not work as you would like it to work, depending on the context.

Panu Logic
  • 2,193
  • 1
  • 17
  • 21
0

I know this question already has an answer but I thought a simple one-liner might help future people who find this:

document.getSelection().modify("move", "forward", "documentboundary");

This gets the current selected element and moves the cursor forward (depending on language - right to ignore language) to the end of the document.

see here for more info on what you can do with Selections.