How can I dynamically highlight strings on a web page?

If you call the function

highlight(employee);

this is what that function would look like in ECMAScript 2018+:

function highlight(employee){
  Array.from(document.querySelectorAll("body, body *:not(script):not(style):not(noscript)"))
    .flatMap(({childNodes}) => [...childNodes])
    .filter(({nodeType, textContent}) => nodeType === document.TEXT_NODE && textContent.includes(employee))
    .forEach((textNode) => textNode.replaceWith(...textNode.textContent.split(employee).flatMap((part) => [
        document.createTextNode(part),
        Object.assign(document.createElement("mark"), {
          textContent: employee
        })
      ])
      .slice(0, -1))); // The above flatMap creates a [text, employeeName, text, employeeName, text, employeeName]-pattern. We need to remove the last superfluous employeeName.
}

And this is an ECMAScript 5.1 version:

function highlight(employee){
  Array.prototype.slice.call(document.querySelectorAll("body, body *:not(script):not(style):not(noscript)")) // First, get all regular elements under the `<body>` element
    .map(function(elem){
      return Array.prototype.slice.call(elem.childNodes); // Then extract their child nodes and convert them to an array.
    })
    .reduce(function(nodesA, nodesB){
      return nodesA.concat(nodesB); // Flatten each array into a single array
    })
    .filter(function(node){
      return node.nodeType === document.TEXT_NODE && node.textContent.indexOf(employee) > -1; // Filter only text nodes that contain the employee’s name.
    })
    .forEach(function(node){
      var nextNode = node.nextSibling, // Remember the next node if it exists
        parent = node.parentNode, // Remember the parent node
        content = node.textContent, // Remember the content
        newNodes = []; // Create empty array for new highlighted content

      node.parentNode.removeChild(node); // Remove it for now.
      content.split(employee).forEach(function(part, i, arr){ // Find each occurrence of the employee’s name
        newNodes.push(document.createTextNode(part)); // Create text nodes for everything around it

        if(i < arr.length - 1){
          newNodes.push(document.createElement("mark")); // Create mark element nodes for each occurrence of the employee’s name
          newNodes[newNodes.length - 1].innerHTML = employee;
          // newNodes[newNodes.length - 1].setAttribute("class", "highlighted");
        }
      });

      newNodes.forEach(function(n){ // Append or insert everything back into place
        if(nextNode){
          parent.insertBefore(n, nextNode);
        }
        else{
          parent.appendChild(n);
        }
      });
    });
}

The major benefit of replacing individual text nodes is that event listeners don’t get lost. The site remains intact, only the text changes.

Instead of the mark element you can also use a span and uncomment the line with the class attribute and specify that in CSS.

This is an example where I used this function and a subsequent highlight("Text"); on the MDN page for Text nodes:

A website with all occurrences of “Text” highlighted

(The one occurrence that isn’t highlighted is an SVG node beyond an <iframe>).

Leave a Comment