Dismiss
  • Toggle Theme
  • View as Mobile

Search DOM for arbitrary text then scroll to it

I came across a scenario where I needed to scroll to a given DOM node with a specific text value.

document.querySelector wouldn't work because the thing I'm trying to target is completely arbitrary and has no classes/ids/attributes.

So, I created a generic utility to magically find the text on the page and scroll down to the give element that contains the text.

Demo

I needed this utility for a scroll sync feature in my git based CMS editor.

Here's the snippet in action inside the CMS. It's syncing 2 window scroll positions based on the text that is double clicked.

scroll sync

Wow fancy! I set this up to work bi-directionally via postMessage. See example video

Use case example

I'm planning on using this generic utility for better hyperlinks and for automatically scrolling to the correct context within a clicked search result.

Here's the idea:

What if you could extend the power of anchor tags outside of id's and let people hyperlink to a given section on a site via the text the section contains?

So if you search a site, find the results and click on the link to route to the result, you can automatically put the user in the correct context based on the matched text.

Example:

A link with a URL like site.com/link?search=text to target will load the page and then automatically scroll to the contents containing "text to target"

Show me the code

/**
 * Search for text in a tree of DOM nodes.
 * @param {Node} node - The root node to start the search.
 * @param {string} textToFind - The text to search for.
 * @returns {Node|undefined} The node containing the text, or undefined if not found.
 */
function findTextOnPage(node, textToFind = '') {
  var i, n
  for (i = 0; i < node.childNodes.length; ++i) { 
    n = node.childNodes[i]
    if (n.textContent && n.textContent.indexOf(textToFind) > -1) {
      return n
    }
    if (n.childNodes) {
      n = findTextOnPage(n, textToFind)
      if (n) return n
    }
  }
}

/**
 * Get the offset of the child inside the parent.
 * @param {HTMLElement} parentDiv - The parent element.
 * @param {HTMLElement} childDiv - The child element whose offset needs to be calculated.
 * @returns {number} The offset height of the child relative to the parent, or undefined if inputs are invalid.
 */
function getOffsetHeightOfChild(parentDiv, childDiv) {
  if (!parentDiv || !childDiv) return 0
  let offsetHeight = 0
  let currentElement = childDiv
  while (currentElement && currentElement !== parentDiv) {
    offsetHeight += currentElement.offsetTop
    currentElement = currentElement.offsetParent
  }
  return offsetHeight
}

/**
 * Search for the text and scroll to it if found.
 * @param {object} options - Options for search and scrolling.
 * @param {string} options.text - The text to search for.
 * @param {number} [options.offset=70] - The offset for scrolling.
 * @param {HTMLElement} options.parentElement - The parent element to search within.
 * @param {HTMLElement|Window} [options.scrollContainer=window] - The container to scroll within.
 */
function searchScroll({
  text = '',
  offset = 70,
  parentElement, 
  scrollContainer
}) {
  if (!parentElement) return
  const searchResult = findTextOnPage(parentElement, text)
  if (!searchResult) {
    console.log('no result')
    return
  }
  const _scrollContainer = scrollContainer || window
  const _offset = typeof offset !== 'undefined' ? offset : 70
  const searchResultOffsetHeight = getOffsetHeightOfChild(parentElement, searchResult)
  const parentContainerOffsetHeight = getOffsetHeightOfChild(_scrollContainer, parentElement)
  const finalOffset = (searchResultOffsetHeight + parentContainerOffsetHeight) - _offset

  /* Debug block. Add extra / to leading /* to activate
  console.log('scrollContainer', _scrollContainer)
  console.log('parentElement', parentElement)
  console.log('searchResult', searchResult)
  console.log('parentContainerOffsetHeight', parentContainerOffsetHeight)
  console.log('searchResultOffsetHeight', searchResultOffsetHeight)
  console.log('finalOffset', finalOffset)
  /** */
  
  _scrollContainer.scrollTo({
    top: finalOffset, 
    behavior: 'smooth' 
  })
}

/* Usage Example */
const parentElement = document.querySelector('.cms-editor-visual [role="textbox"]')
const scrollContainer = document.querySelector('[class*="-PreviewPaneContainer-ControlPaneContainer"]')
searchScroll({
  text: 'tinkering',
  parentElement, 
  scrollContainer
})

Details

Let's dissect the code snippet and understand each component:

  1. findTextOnPage: Recursively searches for a given text within the DOM tree. It iterates through each child node, checking if the text is found. If found, it returns the node containing the text.
  2. getOffsetHeightOfChild: Calculate the offset height of a child element relative to its parent. It traverses through the DOM hierarchy, summing up the offsetTop values until it reaches the parent element.
  3. searchScroll: This function orchestrates the search and scrolling functionality. It takes parameters such as the text to search, offset for scrolling, parent element, and scroll container. It utilizes the previously defined functions to find the text within the parent element and calculate the final scroll offset. Finally, it smoothly scrolls to the identified text.
  4. Usage Example: Demonstrates how to use the searchScroll function by providing the text to search for, the parent element where the search should be conducted, and the scroll container where the scrolling should occur.

Additional Use cases

Here are a couple other use cases

  • Raw text search for better hyperlinks
  • Automatic scrolling to UI elements based on user input
  • Custom "CTRL + F" experiences
  • Scroll syncing WYSIWYG CMS editors with rendered output
  • shivs and giggles

If you've found this helpful. Give it a share. ❤️