Search code examples
javascriptinnerhtmlselection-api

Getting `selectionStart` from a span's innerHTML


I have a roll your own text editor that lets you change portions of a textarea element. i want to adapt it to work with a span element. I have no particular attachment to span. The goal is simply to let someone edit html rather than a textarea. I have it working fine in IE but am encountering some problems with Mozilla.

Since I'm using a span instead of form input I am using innerHTML instead of value. However, I can't seem to get the selectionStart and selectionEnd functions to work on innerHTML as opposed to value.

Here is the textarea code that works fine....

html

<textarea id="textarea>Some text goes here</textarea><a href="javascript:void() onclick="editText">edit</a>

JS

function editText() {
    var len = displaytext.value.length; 
    var start = displaytext.selectionStart; 
    var end = displaytext.selectionEnd; 
    var sel = displaytext.value.substring(start, end); returns selection ok
    alert(sel);
}

However, the following adaption is not limiting the selection to start and end.

html

<span id="textarea>Some text goes here</span><a href="javascript:void() onclick="editText">edit</a>

JS

function editText() {
    var len = displaytext.innerHTML.length; //works ok
    var start = displaytext.selectionStart; //does not seem to work
    var end = displaytext.selectionEnd; //does not seem to work
    var sel = displaytext.innerHTML.substring(start, end); //returns whole innerHTML not selection
    alert(sel);
}

Is there a problem with selecionStart on innerHTML? Workaround? Syntax error? Thanks for any suggesions.


Solution

  • First of all, don't use innerHTML for this kind of things. textContent is the way to go here.

    I'm assuming a standard-compliant browser or IE9+ here.

    The first thing to do is to get the document selection. You can do this by

    var sel = getSelection();
    

    Now, the selection can be empty, or can be outside the element, so check:

    var rng, startSel, endSel, sel;
    if (!sel.rangeCount
            || displaytext.compareDocumentPosition((rng = sel.getRangeAt(0)).startContainer) === Node.DOCUMENT_POSITION_PRECEDING
            || displaytext.compareDocumentPosition(rng.endContainer) === Node.DOCUMENT_POSITION_FOLLOWING)
        sel = "";
    else {
        ...
    

    At this point, rng contains a Range object, with the properties startOffset and endOffset. This values are integers that refers to the position of the textContent property of startContainer and endContainer respectively.

    You have a fairly simple case, which is a <span> element with a single text node. In this case, rng.startContainer and rng.endContainer will always be either displaytext, or some preceding or following element, but not a descendant of displaytext.

        ...
    else {
        startSel = displaytext.compareDocumentPosition(rng.startContainer) === Node.DOCUMENT_POSITION_FOLLOWING ? 0 : rng.startOffset;
        endSel = displaytext.compareDocumentPosition(rng.endContainer) === Node.DOCUMENT_POSITION_PRECEDING ? displaytext.textContent.length : rng.endOffset;
        sel = displaytext.textContent.substring(startSel, endSel);
    }
    

    In case of a more complex structure of displaytext, with children elements and all, things become a little tricky. You'd have to find the text offset of the starting node in the descendants tree of displaytext, and the best way to do so is using a TreeWalker object walking the text nodes:

    var tw = document.createTreeWalker(displaytext, NodeFilter.SHOW_TEXT, null, null);