Avoid endless repetition with Document.createNodeIterator()

My goal is to highlight case-insensitive words in the DOM. For instance, I'd like to highlight stackoverflow with

<mark>stackoverflow</mark>
and Google with <mark>Google</mark>

To achieve this, my approach involves utilizing Document.createNodeIterator() which specifically filters out non-text nodes.

window.onload = function() {
  getChildren(document.body);
}

function getChildren(mytag) {

  const nodeIter = document.createNodeIterator(
    mytag,
    NodeFilter.SHOW_TEXT,
    (node) => {
      return NodeFilter.FILTER_ACCEPT
    }
  );
  const mark = document.createElement("mark")
  let node = nodeIter.nextNode();
  while (node) {
    const parent = node.parentElement;
    const innerHTML = parent.innerHTML;
    const word = "stackoverflow"
    const regex = new RegExp(`(${word})`, 'ig');
    parent.removeChild(node)
    parent.innerHTML = innerHTML.replace(regex, "<mark>$1</mark>");
    node = nodeIter.nextNode()
  }
}
<h1>Iterating DOM in JavaScript</h1>

<p>
  A paragraph.
</p>

<div>
  <a href="https://stackoverflow.com/">Stackoverflow</a> is QA website.
</div>

<ul>
  <li>Stackoverflow</li>
  <li>Google</li>
  <li>Apple</li>
</ul>

The existing code seems to have a flaw as it iterates infinitely. Interestingly, changing the highlighted word from stackoverflow to

<mark>duckduckgo</mark>
prevents infinite iteration. How can this issue be resolved?

Answer №1

The issue appears to arise when replacing the node, causing the node iterator to endlessly loop through the same content.

To enhance this process, consider implementing two improvements:

  1. Include the filtering logic within the filter callback to reject unwanted nodes right away instead of examining text nodes individually.
  2. Utilize Node#replaceWith() for node replacement. It can be paired with Node#cloneNode() for constructing replacements, or you have the option to use other methods. If preferred, replaceWith() does support a DOMString as well.

window.onload = function() {
  getChildren(document.body);
}

function getChildren(mytag) {
  const word = "stackoverflow"
  const regex = new RegExp(`(${word})`, 'ig');
  
  const nodeIter = document.createNodeIterator(
    mytag,
    NodeFilter.SHOW_TEXT,
    (node) => {
      //ignore script and style tags
      if (node.parent?.tagName === "SCRIPT" || node.parent?.tagName === "STYLE")
        return NodeFilter.FILTER_REJECT;
        
      //ignore anything already marked
      if (node.parent?.tagName === "MARK")
        return NodeFilter.FILTER_REJECT;
        
      //ignore anything not matching regex
      if (!regex.test(node.data))
        return NodeFilter.FILTER_REJECT;
        
      return NodeFilter.FILTER_ACCEPT;
    }
  );
  let node = nodeIter.nextNode();
  while (node) {
    const parent = node.parentElement;
    const mark = document.createElement("mark");
    mark.append(node.cloneNode());
    
    node.replaceWith(mark);
    node = nodeIter.nextNode()
  }
}
<h1>Iterating DOM in JavaScript</h1>

<p>
  A paragraph.
</p>

<div>
  <a href="https://stackoverflow.com/">Stackoverflow</a> is QA website.
</div>

<ul>
  <li>Stackoverflow</li>
  <li>Google</li>
  <li>Apple</li>
</ul>

Here's a more organized version of the code that may enhance readability:

window.onload = function() {
  getChildren(document.body);
}

function getChildren(mytag) {
  const nodeIter = unmarkedTextIterator(mytag, /stackoverflow/ig);
  
  for (const node of iterate(nodeIter)) {
    node.replaceWith(mark(node));
  }
}


//helper functions to break up the logic into logical parts:


/*
 * Create a DOM NodeIterator for text nodes only. 
 * @param {Node} root - where to start.
 * @param {RegExp} regex - optional filter for what text to watch. Defaults to returning everyting.
 * @return text node which is not in <mark> or <script> or <style> tag and passes the regex filter.
 */
const unmarkedTextIterator = (root, regex = /.*/) =>
  document.createNodeIterator(
    root,
    NodeFilter.SHOW_TEXT,
    (node) => {
      //ignore script and style tags
      if (node.parent?.tagName === "SCRIPT" || node.parent?.tagName === "STYLE")
        return NodeFilter.FILTER_REJECT;
        
      //ignore anything already marked
      if (node.parent?.tagName === "MARK")
        return NodeFilter.FILTER_REJECT;
        
      //ignore anything not matching regex
      if (!regex.test(node.data))
        return NodeFilter.FILTER_REJECT;
        
      return NodeFilter.FILTER_ACCEPT;
    }
  );

/* 
 * Convenience generator function to easily work with NodeIterors
 * @generator
 * @param {NodeIterator} nodeIterator
 * @yields {Node} that nodeIterator gives
 */
function* iterate(nodeIterator) {
  while (node = nodeIterator.nextNode()) {
    yield node;
  }
}

/* 
 * Wraps a node in <mark> tag
 * @param {Node} node
 * @return {Node}
 */
const mark = node => {
  const mark = document.createElement("mark");
  mark.append(node.cloneNode());
  
  return mark;
}
<h1>Iterating DOM in JavaScript</h1>

<p>
  A paragraph.
</p>

<div>
  <a href="https://stackoverflow.com/">Stackoverflow</a> is QA website.
</div>

<ul>
  <li>Stackoverflow</li>
  <li>Google</li>
  <li>Apple</li>
</ul>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is Express capable of serving static files from a hidden folder with dot file?

I have set up my app to serve a static folder in the following way: app.use('/static', serveStatic(__dirname + '/view/my/static/folder')); Now, I am wondering how to configure the server to serve a hidden folder. For example, if I hav ...

Display the Image Element in Angular only when the image actually exists

In my HTML code, I have two sibling elements - one is an image and the other is a div. I want to display the image element if the image exists, and show the div element if the image doesn't exist. Here's how my elements are structured: <img ...

Verify if the upcoming element possesses a particular class

Here is the HTML structure: <div class="row"> <div class="span"></div> <div class="span"></div> <div class="navMenu"> <ul> <li><a href=#">Link 1</a></li> ...

What is the best way to save multiple values using a single input box in reactjs?

I need to utilize a single input box for inputting various values. Whenever a value is entered, I want to store it and display it to the user. <input type="text" onChange={e => this.addPrice('input1', e.target.value)} /> This input box i ...

Ways to verify if a value already exists in a pre-existing JSON file using Node.js

On the backend side, I have a json file containing addresses. From the client side, I am receiving a user's address through an API post method. My goal is to verify if the address provided by the user already exists in the JSON file. The addresses in ...

Determine whether the last day of the month falls within the designated time frame, and

In my Vue JS application form, I am facing an issue related to the pay frequency <select>. To give some context about the situation, let me explain further. The application form includes a <select> box with options for users to choose their ne ...

Using jQuery to obtain the object context while inside a callback function

Suppose I have the following object defined: var myObj = function(){ this.hello = "Hello,"; } myObj.prototype.sayHello = function(){ var persons = {"Jim", "Joe", "Doe","John"}; $.each(persons, function(i, person){ console.log(this.h ...

Issue with table sorting functionality following relocation of code across pages

I had a working code that I was transferring from one webpage to another along with the CSS and JS files, but now it's not functioning properly. <head> <link type="text/css" rel="stylesheet" href="{{ STATIC_URL }}assets/css/style.css"> ...

How many records are there in Mongo DB?

How can I query mongoDB to count documents with a specific date? For example, let's say I have the following documents: [ { fullDate: '2020/02/01', someData: 'someData', }, { fullDate: '2020/03/01', someData: 'someD ...

Transforming text elements into JSON format

My text contains a list of items formatted as follows: var text = "<li>M3-2200 (da2/M3-2200)</li><li>N3-2200 (da2/N3-2200)</li><li>Picasso (picasso/A500)</li><li>Picasso (picasso/A501)</li><li>Picasso ...

Tips for passing a variable from one function to another file in Node.js

Struggling to transfer a value from a function in test1.js to a variable in test2.js. Both files, test.js and test2.js, are involved but the communication seems to be failing. ...

JavaScript: Preventing Duplicate Keystrokes in Keyboard Input

Currently, I am working on a snake game project as part of my JavaScript course. I have encountered an issue where the game registers a collision when two keys are pressed simultaneously, even though there is no visual collision. https://i.sstatic.net/j42 ...

The lower section of the scrollbar is not visible

Whenever the vertical scroll bar appears on my website, the bottom half of it seems to be missing. For a live demonstration, you can visit the site HERE (navigate to the "FURTHER READING" tab). HTML: <!DOCTYPE html> <html lang="en"> <h ...

How can you implement a bootstrap navigation bar in a vue.js project?

I have encountered a problem with using html in my vue project. Despite following the documentation, it seems that the html is not working properly. I am unsure if this issue could be related to the import of popper.js. I have checked my code and I believe ...

What is the best way to send multiple requests consecutively using the riot-lol-api?

SCENARIO: Recently, I found myself dealing with an existing codebase that relied on a different library for making requests to the Riot API. Due to some issues with the current library, I made the decision to transition to a new one: https://www.npmjs.co ...

Identifying changes in Android volume settings via Progressive Web App (PWA)

After creating a PWA with React, I generated a TWA .apk using pwabuilder.com. While having a muted video playing on my screen, I am looking for a way to unmute the video when the user presses the volume change buttons on their Android mobile device. Is th ...

Utilizing the arrayUnion method to modify an array within Firestore

I'm encountering an issue while trying to update an array within a Firestore collection. Despite following the documentation, I keep receiving the following error message: UnhandledPromiseRejectionWarning: FirebaseError: Function DocumentReference.u ...

Having difficulty focusing on the Dropdown component in Primereact

I've been attempting to focus on the "Dropdown" component from primereact, but for some reason, the focus isn't shifting to the desired component. Initially, I tried using the autoFocus property as instructed in the documentation here. However, ...

Incorporate an external object not native to the Angular framework within a factory

We're in the midst of a debate and I'm hoping you can help us reach an agreement. Imagine I have a basic factory set up like this: angular.module('myModule', []) .factory('Fact', function() { var Fact = function() { ...

Constantly retrieving AngularJS JSON data on the details page

Simply put, I have developed a 2-page AngularJS application because I am integrating it into CMS templates and a single page app would not work well with the CMS widgets in the sidebar. On my results page, I am pulling data from 3 different JSON files usi ...