JavaScript How to get cursor HTML offset in contenteditable? [duplicate]

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 ?

Answer:1

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.

Answer:2

EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text. But it's cleaner and lighter if you don't have that requirement.

Here is one option that you can use and that works in all major browsers:

  1. Get the offset of the caret within its node (document.getSelection().anchorOffset)
  2. Get the text of the node in which the caret is located (document.getSelection().anchorNode.data)
  3. Get the offset of that text within #mydiv by using indexOf()
  4. Add the values obtained in 1 and 3, to get the offset of the caret within the div.

The code would look like this for your particular case:

var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );

offsetCaret = textOffset + offset;

You can see a working demo on this JSFiddle (view the console to see the results).

And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable) on this other JSFiddle:

function getCaretHTMLOffset(obj) {

    var offset = document.getSelection().anchorOffset;
    var text = document.getSelection().anchorNode.data;
    var textOffset = obj.innerHTML.indexOf( text );

    return textOffset + offset;

}

About this answer

  • It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).
  • It is short and light, and doesn't require any external library (not even jQuery)
  • Issue: If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.
Answer:3

I'm fairly new to Node.js and am having trouble understanding the way to go about loading libraries or files, in runtime. Apparently, it is a bad idea to load files in runtime using Node.js's native "...

I'm fairly new to Node.js and am having trouble understanding the way to go about loading libraries or files, in runtime. Apparently, it is a bad idea to load files in runtime using Node.js's native "...

I'm trying to run JavaScript code with PHP variables. In the HTML it is OK. The script runs and the subpage is loaded within the content (div). But when I try to run the script with PHP variables, ...

I'm trying to run JavaScript code with PHP variables. In the HTML it is OK. The script runs and the subpage is loaded within the content (div). But when I try to run the script with PHP variables, ...

  1. javascript variables with dollar sign
  2. javascript variables with underscore
  3. javascript variables with functions
  4. javascript variables with examples
  5. javascript variables with text
  6. javascript variables with name
  7. javascript string with variables
  8. javascript regex with variables
  9. javascript class with variables
  10. javascript array with variables
  11. javascript object with variables
  12. javascript math with variables
  13. javascript json with variables
  14. javascript eval with variables
  15. javascript alert with variables
  16. javascript string with variables inside
  17. javascript template with variables
  18. javascript calculate with variables
  19. javascript with session variables
  20. javascript addition with variables

Is there a way to convert the following jQuery method to pure javascript? var myProps = $(".interp").map(function () { return this.id; }).get(); I do not know how many objects will be available ...

Is there a way to convert the following jQuery method to pure javascript? var myProps = $(".interp").map(function () { return this.id; }).get(); I do not know how many objects will be available ...

  1. jquery pure js
  2. jquery vs pure javascript
  3. jquery to pure javascript converter online
  4. jquery animate pure javascript
  5. jquery find pure javascript
  6. jquery to pure javascript converter
  7. jquery load pure javascript
  8. jquery append pure javascript
  9. jquery closest pure javascript
  10. jquery extend pure javascript
  11. jquery ajax pure javascript
  12. jquery vs pure javascript performance
  13. jquery ready pure javascript
  14. jquery offset pure javascript
  15. jquery html pure javascript
  16. jquery scrolltop pure javascript
  17. jquery each pure javascript
  18. jquery trigger pure javascript
  19. jquery selector pure javascript
  20. jquery css pure javascript

My overall goal was to take a screenshot via the background page using: http://developer.chrome.com/extensions/tabs.html#method-captureVisibleTab and pass it to the content script so I can use the ...

My overall goal was to take a screenshot via the background page using: http://developer.chrome.com/extensions/tabs.html#method-captureVisibleTab and pass it to the content script so I can use the ...

  1. chrome extension chrome ig story
  2. chrome extension chromecast
  3. chrome extension chrome store
  4. chrome extension chrome.tabs
  5. chrome extension chrome.tabs.query
  6. chrome extension chromevox
  7. chrome extension chrome.storage
  8. chrome extension chrome
  9. chrome extension chrome.runtime.sendmessage
  10. chrome extension chrometana
  11. chrome extension chrome.tabs undefined
  12. chrome extension chrome.downloads
  13. chrome extension chrome.runtime
  14. chrome extension chrome.windows.create
  15. chrome extension chrome.tabs.onupdated.addlistener
  16. chrome extension chrome.tabs.create
  17. chrome extension chromecast video stream
  18. chrome extension chrome remote desktop
  19. chrome extension chrome.storage.local
  20. chrome extension chrome.debugger