Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a contenteditable div which contains typical wysiwyg editor html (bold, anchors, lists).

I need to determine if the current cursor is, onKeyDown, at the start and at the end of the div. The reason for this is, based on the cursor position, and the key pressed, I might want to merge this div with the previous div on a backspace, or create a new following div on enter.

I've been fiddling around with ranges, but when you're working with html inside the element, things get pretty complicated.

I'm hoping I must be overlooking some simple solution.

Is there a relatively simple way to determine this - I'm open to using a library like Rangy.

Thanks!

Edit: I'm thinking something along these lines:

$('.mycontenteditable').bind('keydown', handle_keydown)

handle_keydown = function(e) {

  range = window.getSelection().getRangeAt(0)
  start_range = document.createRange()
  start_range.selectNodeContents(this.firstChild)
  start_range.collapse(true) // collapse to start
  is_start = start_range.compareBoundaryPoints(Range.START_TO_START,range)
  end_range = document.createRange()
  end_range.selectNodeContents(this.lastChild)
  end_range.collapse(false)
  is_end = end_range.compareBoundaryPoints(Range.END_TO_END,range)
}

Am I going to run into any odd issues with something like this?

like image 466
Edward M Smith Avatar asked Sep 16 '11 23:09

Edward M Smith


2 Answers

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 };
}
like image 145
Tim Down Avatar answered Sep 30 '22 11:09

Tim Down


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.

like image 32
Edward M Smith Avatar answered Sep 30 '22 10:09

Edward M Smith