javascriptjquerydom

Given an offset in an extracted combined text string, how to determine containing node and offset?


Given the following HTML (just an example):

<div id="container"><span>This is <strong>SOME</strong> great example, <span style="color: #f00">Fred!</span></span></div>

One can extract the text using e.g. jQuery's text() function:

var text = $('container').text();

Now, what would be the simplest, fastest, most elegant way to determine that the offset 10 in the extracted text corresponds to the offset 2 of the text node inside the <strong>SOME</strong> node in the example above? Also, how would one do the inverse, i.e. determining the offset 10in the extracted text from the <strong>DOM object and the offset 2?


Solution

  • You can use TreeWalker to get a pretty elegant solution:

    /**
     * @param {Element} element
     * @param {number} absoluteOffset
     * @returns {[textNode: Text, relativeOffset: number] | null}
     */
    function getRelativeOffsetWithinChildTextNode(element, absoluteOffset) {
      let offset = absoluteOffset
      const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT)
      while (walker.nextNode()) {
        const { currentNode } = walker
        const text = currentNode.nodeValue
        if (text.length >= offset) {
          return [currentNode, offset]
        }
        offset -= text.length
      }
      return null
    }
    
    // usage
    const parent = document.getElementById('container')
    const absoluteOffsets = [0, 8, 10, 12, 20, 30, 999]
    
    for (const absoluteOffset of absoluteOffsets) {
      const result = getRelativeOffsetWithinChildTextNode(parent, absoluteOffset)
      
      if (result == null) {
        console.log(`Absolute offset ${absoluteOffset} is out of bounds (no text node at this offset)`)
      } else {
        const [textNode, relativeOffset] = result
        const { parentElement } = textNode
        const childNodes = [...parentElement.childNodes].filter((node) => node instanceof Text)
        const num = childNodes.indexOf(textNode) + 1
        console.log(`Absolute offset ${absoluteOffset} => relative offset ${relativeOffset} in ${parentElement.tagName}'s text node #${num}`)
      }
    }
    <div id="container">This is <strong>SOME</strong> great example, <em>Fred!</em></div>