Search code examples
jqueryhtmlcontenteditablerangy

Rangy + ContentEditable + Set Caret and Replace Selection


I have the following fragment of code to set the caret to a given index index

var el = $("#subjectMessage"), index = 9 ;
var range = rangy.createRange();
range.setStart(el[0].childNodes[0], index);
range.collapse(true);
var sel = rangy.getSelection();
sel.setSingleRange(range);

Issues arise when I add input elements to the contentEditable div, I can't consistently set the caret at the desired position anymore.

I want to treat these inputs as just one position in the div (as if they were just a single character).

Aside from this, I also would need similar code to replace the selection within this contentEditable div with some text of my own, and I'm not really familiar (at all) with rangy to understand how to make this work...

All help is desperately welcome!

Here's a fiddle you can toy with:

http://jsfiddle.net/ee93P/


Solution

  • Rangy's Selection and Range APIs are supersets of the standard DOM Selection and Range APIs, so documentation from places like MDN (Range, Selection) apply.

    The problem you're having is that range boundaries are expressed as an offset within a containing DOM node. For example, in the HTML below where the caret is denoted as a pipe character:

    <p>Foo<br>b|ar</p>
    

    ... the caret range's start and end boundaries are identical and are set to offset 1 in the text node "bar".

    If you wanted to set the position as an offset within the text content of the <p> element, you would need to do some DOM traversal. I've written an implementation of this, based on another answer of mine. This is naive implementation: it does not take into account any text that may be made invisible (either by CSS or by being inside a or element, for example) and may have browser discrepancies (IE versus everything else) with line breaks, and takes no account of collapsed whitespace (such as 2 or more consecutive space characters collapsing to one visible space on the page). This is a tricky thing to get right, which is why I wouldn't generally recommend it. I am planning to write a text-based module for Rangy that will handle all this, but I haven't started it yet.

    http://jsfiddle.net/ee93P/2/

    Code:

    function setCaretCharIndex(containerEl, index) {
        var charIndex = 0, stop = {};
    
        function traverseNodes(node) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (index >= charIndex && index <= nextCharIndex) {
                    rangy.getSelection().collapse(node, index - charIndex);
                    throw stop;
                }
                charIndex = nextCharIndex;
            }
            // Count an empty element as a single character. The list below may not be exhaustive.
            else if (node.nodeType == 1
                     && /^(input|br|img|col|area|link|meta|link|param|base)$/i.test(node.nodeName)) {
                charIndex += 1;
            } else {
                var child = node.firstChild;
                while (child) {
                    traverseNodes(child);
                    child = child.nextSibling;
                }
            }
        }
    
        try {
            traverseNodes(containerEl);
        } catch (ex) {
            if (ex != stop) {
                throw ex;
            }
        }
    }