Search code examples
javascriptdomdom-eventstampermonkey

How to alter DOM on dynamic page as elements are added?


I have a web page that I am attempting to write a Tampermonkey script for that will find Virtual Terminal (VT) color codes in specific elements and then replace them with <span> elements. I am confident that the core code works fine, but the issue is how I can effectively deploy the code so that as new elements are added to the DOM via the site's own code, mine will detect and rewrite.

(function() {
    'use strict';

    document.addEventListener("DOMNodeInserted",function(){
        setTimeout(function(){
        var vtRe = /\x5b(?<color>\d{2}(?:;1)?|0)m/;
        var nodeList = document.querySelectorAll("div.command-output:not(.vt-colorized)");
        nodeList.forEach((node) => {
            // Replace VT Color Codes with span colors
            var iText = node.innerText;
            var arr = iText.split(/\x1b/);
            node.innerText = '';
            arr.forEach((item) => {
                var newEle = document.createElement("span");
                let match = vtRe.exec(item);
                if (match) {
                    switch(match.groups.color) {
                        case "30":
                            newEle.style.color = 'black';
                            break;
                        case "31":
                            newEle.style.color = 'darkred';
                            break;
                        case "32":
                            newEle.style.color = 'darkgreen';
                            break;
                        case "33":
                            newEle.style.color = 'darkyellow';
                            break;
                        case "34":
                            newEle.style.color = 'darkblue';
                            break;
                        case "35":
                            newEle.style.color = 'darkmagenta';
                            break;
                        case "36":
                            newEle.style.color = 'darkcyan';
                            break;
                        case "37":
                            newEle.style.color = 'darkgray';
                            break;
                        case "30;1":
                            newEle.style.color = 'gray';
                            break;
                        case "31;1":
                            newEle.style.color = 'red';
                            break;
                        case "32;1":
                            newEle.style.color = 'green';
                            break;
                        case "33;1":
                            newEle.style.color = 'yellow';
                            break;
                        case "34;1":
                            newEle.style.color = 'blue';
                            break;
                        case "35;1":
                            newEle.style.color = 'magenta';
                            break;
                        case "36;1":
                            newEle.style.color = 'cyan';
                            break;
                        case "37;1":
                            newEle.style.color = 'white';
                            break;
                        case "32":
                            newEle.style.color = 'green';
                            break;
                        case "0":
                            break;
                        default:
                            break;
                    }
                }
                newEle.innerText = item.replace(vtRe,'');
                node.append(newEle);
            });
            node.classList.add('vt-colorized');
        });
    }, 10000);
    },false);
})();

I have tried the following methods:

  • addEventListener("DOMNodeInserted", ...) to run the code directly as every element is inserted into the DOM; however, this resulted in a call stack exceeded error due to the infinite loop created
  • setInterval(...) to run the code periodically, looking for the elements that need to be processed by the code; however, this resulted in partial processing or lost data, since the DOM gets changed as the responses come in, which is irregular.
  • addEventListenersetTimeout (as seen in the code above) — this doesn't work either.

Am I going about this the wrong way/is there an easier way to handle this?


Solution

  • Mutation events like DomNodeInserted are deprecated.

    Use a MutationObserver instead.

    Simplified example:

    const targetNode = document.querySelector("#cont");
    const observerOptions = {
      childList: true,
      attributes: false,
      // Omit (or set to false) to observe only changes to the parent node
      subtree: true
    }
    
    const observer = new MutationObserver(callback);
    observer.observe(targetNode, observerOptions);
    
    function callback(mutationList, observer) {
      mutationList.forEach((mutation) => {
        if (mutation.type === 'childList' && mutation.addedNodes.length) {
          mutation.addedNodes.forEach(el => {
            if (el.matches('#cont ul li')) {
              console.log('<li> Added')
              el.style.color = 'red'
            }
          });
        }
      });
    }
    
    // insert an element every 1.5 seconds
    let ctr=0;
    const timer = setInterval(()=>{   
       if(++ctr===5){     
         clearInterval(timer)
       }
       const li = document.createElement('li')
       li.textContent = `Item ${ctr}`
       document.querySelector('#cont ul').append(li)
    },1500)
      
    <div id="cont">
      <ul>
      </ul>
    </div>