Finding cursor position in a contenteditable div

Several days ago (last year, actually) I started researching the problem of implementing a browser-based rich text editor. Among other questions that need to be answered there is the following one: if we use a div element as the text editor or a text display widget (e.g. when capturing user input with a hidden textarea), how shall we track the current caret position?

As usual, there are two solutions: for Internet Explorer and everything else :-) Both involve installing keyup and click event handlers:

<div id="test" contenteditable="true" onclick="getCursorPos();" onkeyup="getCursorPos();">Lots of text for testing</div>

As a rule, cursor position and the current selection are tightly wired together: when nothing is selected, the current selection begins and ends at the cursor position. So, we retrieve the selection range (in the code below, we assume that there is only one selection range in the window. I leave proper handling of the situation where multiple selection ranges exist as an exercise for the reader).
Now, the actual findNode function depends on the method you build the text editor and on your algorithmic skills. When you use a single long text node which includes all the text in the editor, it will be different from the case when every character is a separate text node. Besides, you are welcome to use optimized search algorithms if you are going to work with large documents. The important part here is that the anchorNode property references the first character in the selection, and you will only need to find its index among the other characters [1]. An example findNode() function is given below.
The cursor position and the selection display some dexterity in their mutual dependency. The quirk is that when you click on a character, the cursor is sometimes positioned by different math than the selection’s anchor. For instance, the cursor is positioned before the character if you click on its left 50% and after the character if you click on its right 50%, while if it is included in the selection or not depends on its geometry or the percentages are merely different. So, in order to reflect this, we add the selection object’s anchorOffset (actually, the selection length in characters) [2] to the value returned by the findNode function.
To make things work for Internet Explorer, we have to rely on a proprietary undocumented possibility. It appears that the second Unicode character in the bookmark of the selection range contains the value we need plus 11. Currently, I don’t know the meaning of this number, and whether its exact value changes depending on circumstances and can be calculated somehow.

function getCursorPos() {
var cursorPos;
if (window.getSelection) {
var selObj = window.getSelection();
var selRange = selObj.getRangeAt(0);
cursorPos =  findNode(selObj.anchorNode.parentNode.childNodes, selObj.anchorNode
) + selObj.anchorOffset;
/* FIXME the following works wrong in Opera when the document is longer than 32767 chars */
alert(cursorPos);
}
else if (document.selection) {
var range = document.selection.createRange();
var bookmark = range.getBookmark();
/* FIXME the following works wrong when the document is longer than 65535 chars */
cursorPos = bookmark.charCodeAt(2) - 11; /* Undocumented function [3] */
alert(cursorPos);
}
}

function findNode(list, node) {
for (var i = 0; i < list.length; i++) {
if (list[i] == node) {
return i;
}
}
return -1;
}

Well, that’s all. Solution tested in Firefox 3, Opera 9, Google Chrome, Safari 3.1 and IE7.

17 Responses to Finding cursor position in a contenteditable div

  1. wera says:

    How can this fellow find his solutions?…

  2. [...] Finding cursor position in a contentEditable div. Up-to-date, cross-browser. [...]

  3. gary says:

    ok I tried this and it works apart from it seems to always start at 43 at the 0 index???

  4. Geethan says:

    Is there any other method for not to rely on IE’s proprietary/undocumented method??

  5. Rich says:

    Hi,
    when I use html tags inside the div the pointer doesnt show the correct position. Any ideas how to get it to show the right position?
    I cant add HTML code here but you can add for example a span inside the div: span /span

  6. Sharief Shaik says:

    Hi,

    I was able to find the cursor position in the contenteditable DIV but the function doesn’t return the cursor position if I have input(append) some html content(contenteditable=”false”) inside the DIV. The getCursorPOs function starts behaving randomly. Can you explain this behaviour.

    Thanks in advance.

  7. Amit Agarwal says:

    Hey I am able to get the caret position but how do I set it back after changing innerHTML?

  8. craig says:

    Great, This is the shortest way to get caret position I’ve seen.

    How do you use the acquired position to set cursor position ?

  9. Tapas says:

    Hi,

    Thanks for the post. How can we set a cursor position for an editable div ?

    • rumith says:

      Actually, I don’t know. Besides, I advise that you look at what Google has done for its Docs web app: they do not use a content-editable div; rather, they have reimplemented the whole document rendering from scratch in JavaScript. It’s a lot of hard work, but it appears to be the most cross-platform and stable way to do it.

  10. Thank you for this post. Funny how the universe gives you what you need.

  11. Djekkoo says:

    I’ve just had the same problem, and with combining some different stuff i’ve come to this end.
    (testen in chrome, ff, ie)


    // get pos
    function getPos() {
    // vars laden
    var obj = O("content");
    var select_start = 0;
    var select_end = 0;
    // actief maken
    obj.focus();

    // positie bepalen
    if (window.getSelection) { // ff-chr
    // obj maken
    var sel = window.getSelection();
    select_start = sel.focusOffset;
    select_end = sel.anchorOffset;
    }
    else if (document.selection) {// ie
    // vars laden
    var range = document.selection.createRange();
    var range2 = range.duplicate();
    if (range.text != '') {// iets geselecteerd
    // range + dummy maken / bewerken
    range2.moveToElementText(obj);
    range2.setEndPoint('EndToEnd', range);
    // rekenen
    select_start = range2.text.length - range.text.length;
    select_end = begin + range.text.length;
    } else {// niets geselecteerd
    // kopieren
    range2.moveToElementText(obj);
    var Positie = -1;
    while(range2.inRange(range)) {
    range2.moveStart('character');
    Positie++;
    }
    select_start = Positie;
    select_end = Positie;
    }
    }
    // returnen
    return (new Array(select_start, select_end));
    }

  12. Damien says:

    All of your scripts have a bug, they don’t return the good number if it encouters a linebreak.

    • rumith says:

      Which browsers did you test them against? P.S. Patches are welcome, I’ve long since abandoned this whole thing.

      • temp1e says:

        it definitely happens on the newest chrome. Function can’t return the correct value when there is a line break

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.