Contenteditable DIV - how can I determine if the cursor is at the start or end of the content

I would use a similar approach to yours except using the toString() method of Range objects rather than cloneContents() to avoid unnecessary cloning. Also, in IE < 9 (which doesn't support ranges), you can use a similar approach with the text property of TextRange.

Note that this will have issues when there are leading and/or trailing line breaks in the content because the toString() method of a range works just like the textContent property of a node and only considers text nodes, therefore not taking into account line breaks implied by <br> or block elements. Also CSS is not taken into account: for example, text inside elements that are hidden via display: none is included.

Here's an example:

Live demo: http://jsfiddle.net/YA3Pu/1/

Code:

function getSelectionTextInfo(el) {
    var atStart = false, atEnd = false;
    var selRange, testRange;
    if (window.getSelection) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            selRange = sel.getRangeAt(0);
            testRange = selRange.cloneRange();

            testRange.selectNodeContents(el);
            testRange.setEnd(selRange.startContainer, selRange.startOffset);
            atStart = (testRange.toString() == "");

            testRange.selectNodeContents(el);
            testRange.setStart(selRange.endContainer, selRange.endOffset);
            atEnd = (testRange.toString() == "");
        }
    } else if (document.selection && document.selection.type != "Control") {
        selRange = document.selection.createRange();
        testRange = selRange.duplicate();

        testRange.moveToElementText(el);
        testRange.setEndPoint("EndToStart", selRange);
        atStart = (testRange.text == "");

        testRange.moveToElementText(el);
        testRange.setEndPoint("StartToEnd", selRange);
        atEnd = (testRange.text == "");
    }

    return { atStart: atStart, atEnd: atEnd };
}

This is how I ended up solving this. My proposed solution above worked sometimes but there were way to many edge cases, so I ended up considering how much text was before or after the cursor, and if that was 0 characters, then I was at the start or end:

handle_keydown = function(e) {
  // Get the current cusor position
  range = window.getSelection().getRangeAt(0)
  // Create a new range to deal with text before the cursor
  pre_range = document.createRange();
  // Have this range select the entire contents of the editable div
  pre_range.selectNodeContents(this);
  // Set the end point of this range to the start point of the cursor
  pre_range.setEnd(range.startContainer, range.startOffset);
  // Fetch the contents of this range (text before the cursor)
  this_text = pre_range.cloneContents();
  // If the text's length is 0, we're at the start of the div.
  at_start = this_text.textContent.length === 0;
  // Rinse and repeat for text after the cursor to determine if we're at the end.
  post_range = document.createRange();
  post_range.selectNodeContents(this);
  post_range.setStart(range.endContainer, range.endOffset);
  next_text = post_range.cloneContents();
  at_end = next_text.textContent.length === 0;
}

Still not entirely sure there are any other edge cases, as I'm not entirely sure how to unit test this, since it requires mouse interaction - there's probably a library to deal with this somewhere.


I figured out this pretty consistent and short method:

function isAtTextEnd() {
    var sel = window.getSelection(),
      offset = sel.focusOffset;
  sel.modify ("move","forward","character");
  if (offset == sel.focusOffset) return true;
  else {
    sel.modify ("move","backward","character");
    return false;
  }
}

The key: try to force move it one character forward - if it actually moved: not at the end (move it one character back), if it didn't - it's at the end (no need to move back, it didn't move).
Implementing for start of text is the opposite, and is "left as an exercise for the reader"...

Cavities:

  • MDN marks modify as "Non-standard", though the compatibility table shows a pretty wide support (tested to work on the latest Chrome and Firefox, according to the table - not supported in Edge).
    I tried using the more supported extend() for it - however, it seems that, weirdly, the extend does work even when at the end of text.

  • If you check if after a user initiates a move of the caret (e.g. in a keyboard or mouse event handler) - you should handle cases where the check forces the caret to move in an unexpected way.