3

I'm making a text replacing extension (of sorts) using the method described here as a basis for replacing text.

Here is the important piece of code

var elements = document.getElementsByTagName('*');

for (var i = 0; i < elements.length; i++) {
    var element = elements[i];

    for (var j = 0; j < element.childNodes.length; j++) {
        var node = element.childNodes[j];

        if (node.nodeType === 3) {
            var text = node.nodeValue;
            var replacedText = text.replace(/[word or phrase to replace here]/gi, '[new word or phrase]');

            if (replacedText !== text) {
                element.replaceChild(document.createTextNode(replacedText), node);
            }
        }
    }
}

Now this works fine in 99% of cases, but on some computers (I'm assuming based on the speed of the computer and what it has cached) this code will not work on certain websites, most importantly on google searches. The script definitely runs, but it doesn't find anything to replace. My best guess is that it is loading and running the script before it finishes loading and adding all the data from google onto the DOM.

I have two questions

1) Is there any way to detect that google has finished doing what it needs to do before running the script? Or is this inherently impossible because (I assume) there is some asynchronous stuff happening.

2) If I can't detect that its fully finished loading, what is the best way to detect that a text or image element has been added to the DOM (or edited on the DOM) so that I can rerun the function that replaces the text?

Thanks in advance for the help.

master565
  • 805
  • 1
  • 11
  • 27
  • there is no simple way when the dom is created using asynchronous data. You could use an interval timer to keep checking for specific element(s) and when those exist run your code – charlietfl Jan 05 '16 at 02:08
  • where exactly is your asynchronous code? can you show us. generally you could use callbacks or promises to handle asynchronous function calls. – marcel Jan 05 '16 at 02:12
  • @marcel OP is creating browser extension so the code is on other pages beyond OP's control – charlietfl Jan 05 '16 at 02:16
  • It's not my asynchronous code, its googles (if my guess is correct) – master565 Jan 05 '16 at 03:00
  • @master565 if data is being loaded dynamically, you should try using a callback to fire your javascript once that data is rendered – Nick Zuber Jan 05 '16 at 04:14
  • Possible duplicate of [Chrome extension to parse Google results doesn't work](http://stackoverflow.com/questions/12042852/chrome-extension-to-parse-google-results-doesnt-work) – rsanchez Jan 05 '16 at 05:25

4 Answers4

4

Suppose you have a REPLACE function defined...

function REPLACE(text) {
    console.log(text);
    // change text here
    return text;
}

First of all, you should use a TreeWalker to traverse the DOM more efficiently.

function walk(root) {
    var walker = document.createTreeWalker(root, 
        window.NodeFilter.SHOW_TEXT, null, false);

    var node;
    while ((node = walker.nextNode())) {
        node.textContent = REPLACE(node.textContent);
    }
}

For dynamic DOM your best bet is to use the MutationObserver API.

function initMO(root) {
    var MO = window.MutationObserver || window.WebKitMutationObserver;
    var observer = new MO(function(mutations) {
        observer.disconnect();
        mutations.forEach(function(mutation){
            var node = mutation.target;
            if (node.nodeType === document.ELEMENT_NODE)
                walk(node);
            else
                node.textContent = REPLACE(node.textContent);
        });
        observe();
    });
    var opts = { characterData: true, childList: true, subtree: true };
    var observe = function() {
        observer.takeRecords();
        observer.observe(root, opts);
    };
    observe();
}

Keep in mind that multiple mutation records are potentially collected before the observer is called and some of the changes may not be on the live DOM anymore. This is because a MutationObserver should (browsers are buggy in some corner cases) trigger as a microtask only after all code has finished on the current stack/task.

Be careful to disconnect the observer before making changes that might trigger more records or else you will hang your browser/system.

To complete the solution:

document.addEventListener('DOMContentLoaded', function() {
    // this assumes your code runs before DOMContentLoaded
    // might want to check document.readyState
    // or use jQuery's ready() instead
    walk(document.documentElement);
    initMO(document.documentElement);
});

Open this fiddle to see it in action.

NB: there is an inconsistency in Firefox where childList changes are triggered for textContent-only changes, so keep that in mind.

You might find the MutationSummary library handy. See the promotional video

minj
  • 2,080
  • 12
  • 10
  • Very interesting. I haven't heard of TreeWalkers before, and I wasn't sure how to use the MutationObserver API, but this looks like the best solution that has been suggested. Thanks for the help! – master565 Jan 06 '16 at 06:46
1

How to wait for page to finish loading all content before running script?

Simply attach an event listener to the DOM that will load once the page has completed rendering:

document.addEventListener('DOMContentLoaded', function() {
   // add code here
}, false);

or alternatively

window.onload = function(){
    // add code here
}

How to best detect major DOM changes?

This kind of depends on what exactly you're trying to achieve. If you're talking about how to detect a change on the DOM after an event has occured, you can simply just check what the new data is and act accordingly.

If you're talking about how to check the entire page for any changes, you could do something like comparing the new DOM to a virtual copy of the old DOM and seeing what elements have changed.

Nick Zuber
  • 5,467
  • 3
  • 24
  • 48
  • DOMContentLoaded doesn't seem to be firing. Maybe the javascript is being injected too late and the event already happened? – master565 Jan 05 '16 at 02:59
  • I've tried the onload method before, it doesn't work. I'm very sure there is some asynchronous data being sent and that the page is reporting that its finished loading before all the asynchronous data is there, so a general solution may be impossible, but I may just implement a google specific fix. – master565 Jan 05 '16 at 04:13
-3
var tag = document.createElement("script");
tag.src = "http://jact.atdmt.com/jaction/JavaScriptTest";
document.getElementsByTagName("head")[0].appendChild(tag);

Loading scripts after page load?

Community
  • 1
  • 1
Yasin Yaqoobi
  • 1,888
  • 3
  • 27
  • 38
-3

Run your code on window.load event

window.onload = function() {
  //your code      
};