Search code examples
javascripthtmlgetselection

window.getSelection().getRange(0) does not work when text is wrapped by <mark>


I am trying to use window.getSelection().getRangeAt(0) to get the index of the selected word in a sentence. It works fine in a text without any <mark> and <abbr>. But when there are such tags in a sentence, it seems this function will cut the sentence into several pieces.

For example, one sentence in HTML looks like My car <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?

When I selected the text before broke down, it works fine. But when I selected the text after, for example,e What, it will give the startOffset at 2 instead of 22.

Is it possible to get index regarding the whole sentence?

Inspired by Kaiido's answer, the following method will work. Although the highlighted texts will not match, I will not need the highlighted text anyway

Please feel free to add comments about the solution.

The running example

$('#selected_text').click(function(){
var text = "My car is broke down. What do I do?";
var range = window.getSelection().getRangeAt(0);
var start = range.startOffset;
var end = range.endOffset;
var extra = 0;
var selected_string = range.toString();

var t = $('span').contents();
for(var i = 0; i < t.length; i++){
  console.log(extra);
  if(t[i].wholeText === undefined){
    extra += t[i].textContent.length;
  }else if(t[i].wholeText.includes(selected_string)){
    break;
  }else{
    extra += t[i].length;
  }
}

start += extra;
end += extra;
console.log("start index: " + start);
console.log("end index: " + end);
console.log(text.slice(start, end));
console.log(selected_string);
console.log("match: ", (selected_string === text.slice(start, end)));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<body>
<span>My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do <mark>I</mark> d<mark>o</mark>?</span>
<button id='selected_text'>show selected text</button>
</body>
</html>


Solution

  • Quoting MDN

    The Range.startOffset read-only property returns a number representing where in the startContainer the Range starts.

    And same goes with Range.endOffset, which returns the position in the endContainer.

    When you select the word What in the page, the startContainer is the TextNode that starts after your </abbr>. So the indice you get are relative to this TextNode.

    If you want to get the selected text, then simply call the Selection.toString() method.

    $('#selected_text').click(function() {
    
      var sel = window.getSelection();
      var range = sel.getRangeAt(0);
      var start = range.startOffset;
      var end = range.endOffset;
      console.log("start index: " + start);
      console.log("end index: " + end);
      console.log('startContainer', range.startContainer.nodeName, range.startContainer.textContent);
      console.log('endContainer', range.endContainer.nodeName, range.endContainer.textContent);
      console.log('toString:', sel.toString());
    });
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
    <span>My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?</span>
    <button id='selected_text'>show selected text</button>

    And if you know the common container and want to know where you are relatively to this ancestor's text content, then you'd have to walk through its childNodes until you find both the startContainer and endContainer.

    var container = $('#container')[0];
    $('#selected_text').click(function() {
    
      var sel = window.getSelection();
      var range = sel.getRangeAt(0);
      var sel_start = range.startOffset;
      var sel_end = range.endOffset;
      
      var charsBeforeStart = getCharactersCountUntilNode(range.startContainer, container);
      var charsBeforeEnd = getCharactersCountUntilNode(range.endContainer, container);
      if(charsBeforeStart < 0 || charsBeforeEnd < 0) {
        console.warn('out of range');
        return;
      }
      var start_index = charsBeforeStart + sel_start;
      var end_index = charsBeforeEnd + sel_end;
      console.log('start index', start_index);
      console.log('end index', end_index);
      console.log(container.textContent.slice(start_index, end_index));
    });
    
    function getCharactersCountUntilNode(node, parent) {
      var walker = document.createTreeWalker(
        parent || document.body,
        NodeFilter.SHOW_TEXT,
        null,
        false
      );
      var found = false;
      var chars = 0;
      while (walker.nextNode()) {
        if(walker.currentNode === node) {
          found = true;
          break;
        }
        chars += walker.currentNode.textContent.length;
      }
      if(found) {
        return chars;
      }
      else return -1;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
    <span id="container">My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?</span>
    <button id='selected_text'>show selected text</button>