[javascript] Get contentEditable caret index position

I'm finding tons of good, crossbrowser anwers on how to SET the cursor or caret index position in a contentEditable element, but none on how to GET or find its index...

What I want to do is know the index of the caret within this div, on keyup.

So, when the user is typing text, I can at any point know its cursor's index within the contentEditable element.

EDIT: I'm looking for the INDEX within the div contents (text), not the cursor coordinates.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});

This question is related to javascript contenteditable caret cursor-position

The answer is


As this took me forever to figure out using the new window.getSelection API I am going to share for posterity. Note that MDN suggests there is wider support for window.getSelection, however, your mileage may vary.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Here is a jsfiddle that fires on keyup. Note however, that rapid directional key presses, as well as rapid deletion seems to be skip events.


function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}

A straight forward way, that iterates through all the chidren of the contenteditable div until it hits the endContainer. Then I add the end container offset and we have the character index. Should work with any number of nestings. uses recursion.

Note: requires a poly fill for ie to support Element.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }

Kinda late to the party, but in case anyone else is struggling. None of the Google searches I've found for the past two days have come up with anything that works, but I came up with a concise and elegant solution that will always work no matter how many nested tags you have:

_x000D_
_x000D_
function cursor_position() {_x000D_
    var sel = document.getSelection();_x000D_
    sel.modify("extend", "backward", "paragraphboundary");_x000D_
    var pos = sel.toString().length;_x000D_
    if(sel.anchorNode != undefined) sel.collapseToEnd();_x000D_
_x000D_
    return pos;_x000D_
}_x000D_
_x000D_
// Demo:_x000D_
var elm = document.querySelector('[contenteditable]');_x000D_
elm.addEventListener('click', printCaretPosition)_x000D_
elm.addEventListener('keydown', printCaretPosition)_x000D_
_x000D_
function printCaretPosition(){_x000D_
  console.log( cursor_position(), 'length:', this.textContent.trim().length )_x000D_
}
_x000D_
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>
_x000D_
_x000D_
_x000D_

It selects all the way back to the beginning of the paragraph and then counts the length of the string to get the current position and then undoes the selection to return the cursor to the current position. If you want to do this for an entire document (more than one paragraph), then change paragraphboundary to documentboundary or whatever granularity for your case. Check out the API for more details. Cheers! :)


Try this:

Caret.js Get caret postion and offset from text field

https://github.com/ichord/Caret.js

demo: http://ichord.github.com/Caret.js


window.getSelection - vs - document.selection

This one works for me:

_x000D_
_x000D_
function getCaretCharOffset(element) {_x000D_
  var caretOffset = 0;_x000D_
_x000D_
  if (window.getSelection) {_x000D_
    var range = window.getSelection().getRangeAt(0);_x000D_
    var preCaretRange = range.cloneRange();_x000D_
    preCaretRange.selectNodeContents(element);_x000D_
    preCaretRange.setEnd(range.endContainer, range.endOffset);_x000D_
    caretOffset = preCaretRange.toString().length;_x000D_
  } _x000D_
_x000D_
  else if (document.selection && document.selection.type != "Control") {_x000D_
    var textRange = document.selection.createRange();_x000D_
    var preCaretTextRange = document.body.createTextRange();_x000D_
    preCaretTextRange.moveToElementText(element);_x000D_
    preCaretTextRange.setEndPoint("EndToEnd", textRange);_x000D_
    caretOffset = preCaretTextRange.text.length;_x000D_
  }_x000D_
_x000D_
  return caretOffset;_x000D_
}_x000D_
_x000D_
_x000D_
// Demo:_x000D_
var elm = document.querySelector('[contenteditable]');_x000D_
elm.addEventListener('click', printCaretPosition)_x000D_
elm.addEventListener('keydown', printCaretPosition)_x000D_
_x000D_
function printCaretPosition(){_x000D_
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )_x000D_
}
_x000D_
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>
_x000D_
_x000D_
_x000D_

The calling line depends on event type, for key event use this:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

for mouse event use this:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

on these two cases I take care for break lines by adding the target index


If you set the editable div style to "display:inline-block; white-space: pre-wrap" you don't get new child divs when you enter a new line, you just get LF character (i.e. &#10);.

_x000D_
_x000D_
function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }
_x000D_
click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123&#13;&#10;456&#10;789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">
_x000D_
_x000D_
_x000D_

What I noticed was when you press "enter" in the editable div, it creates a new node, so the focusOffset resets to zero. This is why I've had to add a range variable, and extend it from the child nodes' focusOffset back to the start of eDiv (and thus capturing all text in-between).


A few wrinkles that I don't see being addressed in other answers:

  1. the element can contain multiple levels of child nodes (e.g. child nodes that have child nodes that have child nodes...)
  2. a selection can consist of different start and end positions (e.g. multiple chars are selected)
  3. the node containing a Caret start/end may not be either the element or its direct children

Here's a way to get start and end positions as offsets to the element's textContent value:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}

This one builds on @alockwood05's answer and provides both get and set functionality for a caret with nested tags inside the contenteditable div as well as the offsets within nodes so that you have a solution that is both serializable and de-serializable by offsets as well.

I'm using this solution in a cross-platform code editor that needs to get the caret start/end position prior to syntax highlighting via a lexer/parser and then set it back immediately afterward.

function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
            countingState.done = true;
            countingState.offsetInNode = offset;
            return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            countingState.offsetInNode = offset;
            countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilEndContainer(node, endNode, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}

//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

Note: the range object its self can be stored in a variable, and can be re-selected at any time unless the contents of the contenteditable div change.

Reference for IE 8 and lower: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Reference for standards (all other) browsers: https://developer.mozilla.org/en/DOM/range (its the mozilla docs, but code works in chrome, safari, opera and ie9 too)


_x000D_
_x000D_
$("#editable").on('keydown keyup mousedown mouseup',function(e){_x000D_
     _x000D_
       if($(window.getSelection().anchorNode).is($(this))){_x000D_
       $('#position').html('0')_x000D_
       }else{_x000D_
         $('#position').html(window.getSelection().anchorOffset);_x000D_
       }_x000D_
 });
_x000D_
body{_x000D_
  padding:40px;_x000D_
}_x000D_
#editable{_x000D_
  height:50px;_x000D_
  width:400px;_x000D_
  border:1px solid #000;_x000D_
}_x000D_
#editable p{_x000D_
  margin:0;_x000D_
  padding:0;_x000D_
}
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>_x000D_
<div contenteditable="true" id="editable">move the cursor to see position</div>_x000D_
<div>_x000D_
position : <span id="position"></span>_x000D_
</div>
_x000D_
_x000D_
_x000D_


Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to contenteditable

React.js: onChange event for contentEditable How to set caret(cursor) position in contenteditable element (div)? Get contentEditable caret index position contenteditable change events Set cursor position on contentEditable <div>

Examples related to caret

How to get the focused element with jQuery? Styling text input caret How to set caret(cursor) position in contenteditable element (div)? Get contentEditable caret index position How to get the caret column (not pixels) position in a textarea, in characters, from the start?

Examples related to cursor-position

Setting cursor at the end of any text of a textbox How to set caret(cursor) position in contenteditable element (div)? jQuery: Get the cursor position of text in input without browser specific code? Get contentEditable caret index position How to move screen without moving cursor in Vim? Set cursor position on contentEditable <div>