1

As the title says, I'm trying to find the caret position of the user within a content editable div, after deselecting through clicking in the selected area.

While getting the caret position from generating a range based on properties like anchor/focus nodes and anchor/focus offsets from the Document.getSelection() call works fine in most cases, I noticed that when I highlight a block of text and deselect by clicking within the selection range, function calls associated to (for example) the 'mouseup' event still believe that selection range is selected during this time.

This is visibly correct if you were to put a break point inside the function called upon 'mouseup' (in this case showCaretPos() in the below code), the function being called on 'mouseup' while the text is still in it's selected state.

However upon running a function to check the position of the caret again after the 'mouseup' associated function is fired, we get the correct offset for where the caret should be.

Afterwards the caret is placed where the user clicked within their selection and what I'd like to know is if there is a way to find out where the browser plans to place the caret after de-selection happens in terms of nodes and offsets.

Essentially the second example linked, but put together for pasting convenience:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
    <title>Sandbox</title>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
Non-editable text. Editable is below:
<div id="test" contenteditable="true">Hello, some <b>bold</b> and <i>italic and <b>bold</b></i> text</div>
<div id="caretPos"></div>
</body>
<script>
    function getCaretCharacterOffsetWithin(element) {
        var caretOffset = 0;
        var doc = element.ownerDocument || element.document;
        var win = doc.defaultView || doc.parentWindow;
        var sel;
        if (typeof win.getSelection != "undefined") {
            sel = win.getSelection();
            if (sel.rangeCount > 0) {
                var range = win.getSelection().getRangeAt(0);
                var preCaretRange = range.cloneRange();
                preCaretRange.selectNodeContents(element);
                preCaretRange.setEnd(range.endContainer, range.endOffset);
                caretOffset = preCaretRange.toString().length;
            }
        } else if ( (sel = doc.selection) && sel.type != "Control") {
            var textRange = sel.createRange();
            var preCaretTextRange = doc.body.createTextRange();
            preCaretTextRange.moveToElementText(element);
            preCaretTextRange.setEndPoint("EndToEnd", textRange);
            caretOffset = preCaretTextRange.text.length;
        }
        return caretOffset;
    }

    function showCaretPos() {
        var el = document.getElementById("test");
        var caretPosEl = document.getElementById("caretPos");
        caretPosEl.innerHTML = "Caret position: " + getCaretCharacterOffsetWithin(el);
    }

    document.body.onkeyup = showCaretPos;
    document.body.onmouseup = showCaretPos;
</script>
</html>

Example 1 from question Get a range's start and end offset's relative to its parent container

JSFiddle:http://jsfiddle.net/TjXEG/900/

Example 2 from question Javascript: How to detect if a word is highlighted

JSFiddle http://jsfiddle.net/timdown/SW54T/

Example 3 from question Get caret position in contentEditable div

Community
  • 1
  • 1
Dan Hong
  • 11
  • 1

1 Answers1

0

I found a solution to obtain the caret position after clicking inside a selected text in an html input element (browser: Chrome). I needed a hack with a timer to do this though. Here is how.

I call an html input element a field. In this text, the word "selection" denotes a selected part inside a field. It can be the entire string in the field or just a part of it.

If you click on text left or right from a selection everything goes as expected. The selected part gets deselected and a caret appears at the spot where you clicked. You can have your event handler return field.selectionStart which is the new caret position, and field.selectionEnd which equals field.selectionStart.

In the case of clicking INSIDE a selection, things are slightly different which I will explain below.

Before I start an important reminder. If you want to apply the solution in your event handler, be sure to disable or remove any event.preventDefault() statements in earlier code lines. If you use this statement anyway and click a selection, it will stay in place and no caret will appear at all.

Here we go.

If you click inside a selection the visual result is the same as with clicking outside: deselection and caret. However, field.selectionEnd and field.selectionStart now keep returning values as if the selection is still in place. There seems to be no way around this. I registered an event handler after the one that is currently used. I also registered an event handler to a container html element to which the event bubbles up (I used the body element). Both to no avail.

It looks like "placing the caret on the click position inside already selected text" is native Chrome browser behaviour that we can't influence. Maybe it is executed after all event handlers are finished. Maybe all event handlers use some kind of independent copy of the target element containing "old" values. Or it is something else entirely.

The key to the solution is addressing the particular field element after a little while using a timer function.

The field will then have updated itself and will yield values corresponding with what you see.

My solution classifies as a hack though. I hope that some day someone will come up with a more robust solution without timer.

Here is some example code you can try out.

/// global function; "e" equals event object
var clickFieldWithSelection = function(e){ 
    console.log('this.selectionStart:', this.selectionStart, 'e.type:', e.type)
}

/// code snippet in event handler

//e.preventDefault(); // this line must be disabled or completely deleted 
var isSingleClick = e.detail === 1;
var hasSelection = this.selectionStart < this.selectionEnd ;
// at the end of all keyboard and mouse actions the current selectionStart and selectionEnd are logged in object "this.last"
var isSelectionPreviousEqualsCurrent = this.selectionStart === this.last.selectionStart && this.selectionEnd === this.last.selectionEnd ;
var isClickInsideSelection = isSingleClick && hasSelection && isSelectionPreviousEqualsCurrent ;
// click outside selection can be handled directly, without timer
if (isClickInsideSelection){
    // bind is needed to pass values to function; 200 ms seems not too fast not too slow
    setTimeout(_fxClickFieldWithSelection.bind(this, e), 200);
    return
}
// continue event handler
Tom
  • 1
  • 2