darkred darkred - 2 months ago 12
HTML Question

Modify elements immediately after they are displayed (not after page completely loads) in greasemonkey script?

I have this script. It applies on RottenTomatoes movie pages , and modifies 3 elements

(1 visible text and 2 texts inside tooltips).

Currently, (due to greasemonkey's

@run-at document-end
) it modifies the elements only after the page has completely loaded.

Additionally, there are many times where RottenTomates pages delay loading, up to 20 sec(!),

so that's additional delay to my script.

This causes two things:


  • a delay in displaying the modified visible text, and that,

  • if you hover your mouse in either of the tooltips before the page is completely loaded, then, after it's loaded, it will probably only contain the initial text without my addition, or just my addition.



To see this, install the script and then visit this typical target page.



The RottenTomatoes pages load various content via XHR (as I've seen in Firefox Netowork Monitor),

but the 3 elements (that I want to modify) are displayed immediately, not when page has completely loaded.



I have tried using
MutationObserver
to watch for the specific text value to appear on screen, but it doesn't seem to work. Its structure is like this:

var target = selector_for_element_containing_text ; // the selector is: document.querySelector('.audience-info > div:nth-child(1)');
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
modifyElements();
});
});
var config = { attributes: true, childList: true, characterData: true, subtree: true };
observer.observe(target, config);


function modifyElements(){ // This is just the current code of my script
// modify element 1
// modify element 2
// modify element 3
}


How can I modify the elements immediately after they are displayed?

Answer
  1. Make the script run at document-start by adding this code to script metablock:

    ..............
    // @run-at        document-start
    ..............
    // ==/UserScript==
    
  2. Now when the script runs there's nothing in the document so we'll attach the observer to the document root and will scan the added nodes for the container element .audience-info:

    var observer = new MutationObserver(function(mutations) {
        for (var i=0; i<mutations.length; i++) {
            var mutationAddedNodes = mutations[i].addedNodes;
            for (var j=0; j<mutationAddedNodes.length; j++) {
                var node = mutationAddedNodes[j];
                if (node.classList && node.classList.contains("audience-info")) {
                    node.firstElementChild.innerHTML = node.firstElementChild.innerHTML
                        .replace(/[\d.]+/g, function(m) { return 2 * m });
                    // don't hog resources any more as we've just done what we wanted
                    observer.disconnect();
                    return;
                }
            }
        }
    });
    observer.observe(document, {childList: true, subtree: true});
    

    When the observer is called the tooltips are already present in the document tree so you can simply move the code from "load" function into the observer before return without changing anything.

N.B. It's best to make the observer as fast as possible, particularly by using plain for-loops instead of function callbacks because of the overhead to call them, which might become an issue on a slow PC/device when opening a very complex page that generates thousands (quite a common case) or hundreds of thousands mutations (extremely rare but still!). And disconnect it once the job is done.

P.S. With setMutationHandler function the observer code would be:

// @require       https://greasyfork.org/scripts/12228/code/setMutationHandler.js
// ==/UserScript==

setMutationHandler(document, '.audience-info div:first-child', function(nodes) {
    this.disconnect();
    nodes.forEach(function(n) {
        n.innerHTML = n.innerHTML.replace(/[\d.]+/g, function(m) { return 2*m });
    });
});
Comments