Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

javascript : focusOffset with html tags

I have a contenteditable div as follow (| = cursor position):

<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>

I would like to get the current cursor position including html tags. My code :

var offset = document.getSelection().focusOffset;

Offset is returning 5 (full text from the last tag) but i need it to handle html tags. The expected return value is 40. The code has to work with all recents browsers. (i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question). Any ideas ?

like image 585
user2733521 Avatar asked Apr 21 '15 08:04

user2733521


1 Answers

Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker. The algorithm looks for the HTML serialization of the marker (its outerHTML) within the inner serialization (the innerHTML) of the div of interest. Repeated text is not a problem with this solution.

For this to work, the marker's serialization must be unique within its div. You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve. In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time. It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.

I have a fiddle for it (derived from Alvaro Montoro's own fiddle). The main part is:

function getOffset() {

    if ($("." + unique).length)
        throw new Error("marker present in document; or the unique class is not unique");

    // We could also use rangy.getSelection() but there's no reason here to do this.
    var sel = document.getSelection();

    if (!sel.rangeCount)
        return; // No ranges.

    if (!sel.isCollapsed)
        return; // We work only with collapsed selections.

    if (sel.rangeCount > 1)
        throw new Error("can't handle multiple ranges");

    var range = sel.getRangeAt(0);
    var saved = rangy.serializeSelection();
    // See comment below.
    $mydiv[0].normalize();

    range.insertNode($marker[0]);
    var offset = $mydiv.html().indexOf($marker[0].outerHTML);
    $marker.remove();

    // Normalizing before and after ensures that the DOM is in the same shape before 
    // and after the insertion and removal of the marker.
    $mydiv[0].normalize();
    rangy.deserializeSelection(saved);

    return offset;
}

As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:

  1. Rangy is used to save the selection and restore it afterwards. Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia. If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.

  2. For Rangy to work, the DOM must be in exactly the same state before and after the save. This is why normalize() is called before we add the marker and after we remove it. What this does is merge immediately adjacent text nodes into a single text node. The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes. This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection. Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.

like image 152
Louis Avatar answered Nov 02 '22 04:11

Louis