javascriptdomselectionbrowser-api

Programmatically get the Range of a text spread over multiple elements


I have a code element, and I know the text I'm looking for is inside it, for example:

<p>
  Lorem ipsum <span class="bold">dolor</span> sit amet
</p>

Note the span that is used for styling specific words.

Now, assume I have a reference to the p element, and I want to programmatically mark the ipsum dolor sit part, how can achieve that?


Solution

  • You can use the Selection API with a Range argument to programmatically select text in an element.

    The Range start and end positions accept a Child Node number, or Character inside a Text node. In our case, we need to reach the Text nodes to direct to the text position inside them (in our example, it will start on the first Text node of p, in position 11, and will end on the last Text in position 4).

    To find the right node and the text position inside it, use the next function:

    const findPositionInsideTree = (node, position) => {
      if (node.nodeType === Node.TEXT_NODE) {
        return { node, position };
      }
      for (let child of node.childNodes) {
        if (position <= child.textContent.length) return findPositionInsideTree(child, position);
        position -= child.textContent.length;
      }
    };
    

    This recursive code loops over the child nodes and counts the expected position inside each node.

    And now you only need to call this function for your text, create a Range and add it to the Selection:

    const textStart = element.textContent.indexOf('ipsum dolor sit');
    const textEnd = textStart + 'ipsum dolor sit'.length;
    
    const start = findPositionInsideTree(element, textStart);
    const end = findPositionInsideTree(element, textEnd);
    
    const range = new Range();
    range.setStart(start.node, start.position);
    range.setEnd(end.node, end.position);
    
    window.getSelection().removeAllRanges()
    window.getSelection().addRange(range)