Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript Contenteditable - set Cursor / Caret to index

How would I go a about modifying this(How to set caret(cursor) position in contenteditable element (div)?) so it accepts a number index and element and sets the cursor position to that index?

For example: If I had the paragraph:

<p contenteditable="true">This is a paragraph.</p>

And I called:

setCaret($(this).get(0), 3)

The cursor would move to index 3 like so:

Thi|s is a paragraph.

I have this but with no luck:

function setCaret(contentEditableElement, index)
{
    var range,selection;
    if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
    {
        range = document.createRange();//Create a range (a range is a like the selection but invisible)
        range.setStart(contentEditableElement,index);
        range.collapse(true);
        selection = window.getSelection();//get the selection object (allows you to change selection)
        selection.removeAllRanges();//remove any selections already made
        selection.addRange(range);//make the range you have just created the visible selection
    }
    else if(document.selection)//IE 8 and lower
    { 
        range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
        range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        range.select();//Select the range (make it the visible selection
    }
}

http://jsfiddle.net/BanQU/4/

like image 988
Ryan King Avatar asked Apr 19 '13 00:04

Ryan King


People also ask

How do you set a caret cursor position in Contenteditable element div?

In order to set caret cursor position in content editable elements like div tag is carried over by JavaScript Range interface. The range is created using document. createRange() method.

How do I change cursor position?

SetCursorPosition(Int32, Int32) Method is used to set the position of cursor. Basically, it specifies where the next write operation will begin in the console window.

How do you move the cursor to the end of Contenteditable entity?

moveToElementText(contentEditableElement);//Select the entire contents of the element with the range range. collapse(false);//collapse the range to the end point. false means collapse to end rather than the start range. select();//Select the range (make it the visible selection } } }( window.

How do I get the cursor position in HTML?

If you ever had a specific case where you had to retrieve the position of a caret (your cursor's position) inside an HTML input field, you can do so using the selectionStart property of an input's value. Keep in mind that selectionStart can only be retrieved from the following list of input types: text. password.


3 Answers

Here's an answer adapted from Persisting the changes of range objects after selection in HTML. Bear in mind that this is less than perfect in several ways (as is MaxArt's, which uses the same approach): firstly, only text nodes are taken into account, meaning that line breaks implied by <br> and block elements are not included in the index; secondly, all text nodes are considered, even those inside elements that are hidden by CSS or inside <script> elements; thirdly, consecutive white space characters that are collapsed on the page are all included in the index; finally, IE <= 8's rules are different again because it uses a different mechanism.

var setSelectionByCharacterOffsets = null;

if (window.getSelection && document.createRange) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var charIndex = 0, range = document.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                    range.setStart(node, start - charIndex);
                    foundStart = true;
                }
                if (foundStart && end >= charIndex && end <= nextCharIndex) {
                    range.setEnd(node, end - charIndex);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else if (document.selection) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    };
}
like image 68
Tim Down Avatar answered Sep 23 '22 20:09

Tim Down


range.setStart and range.setEnd can be used on text nodes, not element nodes. Or else they will raise a DOM Exception. So what you have to do is

range.setStart(contentEditableElement.firstChild, index);

I don't get what you did for IE8 and lower. Where did you mean to use index?

Overall, your code fails if the content of the nodes is more than a single text node. It may happen for nodes with isContentEditable === true, since the user can paste text from Word or other places, or create a new line and so on.

Here's an adaptation of what I did in my framework:

var setSelectionRange = function(element, start, end) {
    var rng = document.createRange(),
        sel = getSelection(),
        n, o = 0,
        tw = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, null);
    while (n = tw.nextNode()) {
        o += n.nodeValue.length;
        if (o > start) {
            rng.setStart(n, n.nodeValue.length + start - o);
            start = Infinity;
        }
        if (o >= end) {
            rng.setEnd(n, n.nodeValue.length + end - o);
            break;
        }
    }
    sel.removeAllRanges();
    sel.addRange(rng);
};

var setCaret = function(element, index) {
    setSelectionRange(element, index, index);
};

The trick here is to use the setSelectionRange function - that selects a range of text inside and element - with start === end. In contentEditable elements, this puts the caret in the desired position.

This should work in all modern browsers, and for elements that have more than just a text node as a descendant. I'll let you add checks for start and end to be in the proper range.

For IE8 and lower, things are a little harder. Things would look a bit like this:

var setSelectionRange = function(element, start, end) {
    var rng = document.body.createTextRange();
    rng.moveToElementText(element);
    rng.moveStart("character", start);
    rng.moveEnd("character", end - element.innerText.length - 1);
    rng.select();
};

The problem here is that innerText is not good for this kind of things, as some white spaces are collapsed. Things are fine if there's just a text node, but are screwed for something more complicated like the ones you get in contentEditable elements.

IE8 doesn't support textContent, so you have to count the characters using a TreeWalker. But than again IE8 doesn't support TreeWalker either, so you have to walk the DOM tree all by yourself...

I still have to fix this, but somehow I doubt I'll ever will. Even if I did code a polyfill for TreeWalker in IE8 and lower...

like image 42
MaxArt Avatar answered Sep 25 '22 20:09

MaxArt


Here is my improvement over Tim's answer. It removes the caveat about hidden characters, but the other caveats remain:

  • only text nodes are taken into account (line breaks implied by <br> and block elements are not included in the index)
  • all text nodes are considered, even those inside elements that are hidden by CSS or inside elements
  • IE <= 8's rules are different again because it uses a different mechanism.

The code:

var setSelectionByCharacterOffsets = null;

if (window.getSelection && document.createRange) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var charIndex = 0, range = document.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var hiddenCharacters = findHiddenCharacters(node, node.length)
                var nextCharIndex = charIndex + node.length - hiddenCharacters;

                if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                    var nodeIndex = start-charIndex
                    var hiddenCharactersBeforeStart = findHiddenCharacters(node, nodeIndex)
                    range.setStart(node, nodeIndex + hiddenCharactersBeforeStart);
                    foundStart = true;
                }
                if (foundStart && end >= charIndex && end <= nextCharIndex) {
                    var nodeIndex = end-charIndex
                    var hiddenCharactersBeforeEnd = findHiddenCharacters(node, nodeIndex)
                    range.setEnd(node, nodeIndex + hiddenCharactersBeforeEnd);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else if (document.selection) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    };
}

var x = document.getElementById('a')
x.focus()
setSelectionByCharacterOffsets(x, 1, 13)

function findHiddenCharacters(node, beforeCaretIndex) {
    var hiddenCharacters = 0
    var lastCharWasWhiteSpace=true
    for(var n=0; n-hiddenCharacters<beforeCaretIndex &&n<node.length; n++) {
        if([' ','\n','\t','\r'].indexOf(node.textContent[n]) !== -1) {
            if(lastCharWasWhiteSpace)
                hiddenCharacters++
            else
                lastCharWasWhiteSpace = true
        } else {
            lastCharWasWhiteSpace = false   
        }
    }

    return hiddenCharacters
}
like image 40
B T Avatar answered Sep 26 '22 20:09

B T