Question

I'm writing a Chrome extension that injects scripts to the Google's search result page and modified all the results' anchor elements.

My problem is that the results are rendered asynchronously and are not shown in the page on document load/ready.

I had 2 initial solutions which don't work:

  1. Set a timeout: Bad practice but it works. Nevertheless, might show inconsistent results so I prefer to avoid this solution.

  2. Bind to 'DOMNodeInserted'. Generally works, but more complicated in my case because I insert new nodes my self before the anchors, which triggers a recursion. I can insert code to avoid it if the anchor is already 'tagged', but again, this solution is bad since I need to traverse all the anchors each time a node is inserted - from what I checked this happens more than 140 times in the search result page.

Is there any kind of custom event Google trigger on the search results page? Is there any other DOM event that can work in this case?

Was it helpful?

Solution

You are right in that using "DOMNodeInserted" is not a good approach. If nothing else, it is part of the obsolete Mutation Events API, which has been deprecated (among other reasons) for being notoriously inefficient.

It has been replaced by the MutationObserver API, so this is what you should use instead. You can utilize a MutationObserver to observe "childList" DOM mutations on a root node and its descendants.
(If you choose this approach the mutation-summary library might also come in handy.)

After a (really shallow) search, I found out that (at least for me) Google places its results in a div with id search. Below is the code of a sample extension that does the following:

  1. Registers a MutationObserver to detect the insertion of div#search into the DOM.

  2. Registers a MutationObserver to detect "childList" changes in div#search and its descendants.

  3. Whenever a <a> node is added, a function traverses the relevant nodes and modifies the links. (The script ignores <script> elements for obvious reasons.)

This sample extension just encloses the link's text in ~~, but you can easily change it to do whatever you need.

manifest.json:

{
    "manifest_version": 2,
    "name":    "Test Extension",
    "version": "0.0",

    "content_scripts": [{
        "matches": [
            ...
            "*://www.google.gr/*",
            "*://www.google.com/*"
        ],
        "js":         ["content.js"],
        "run_at":     "document_end",
        "all_frames": false
    }],

}

content.js:

console.log("Injected...");

/* MutationObserver configuration data: Listen for "childList"
 * mutations in the specified element and its descendants */
var config = {
    childList: true,
    subtree: true
};
var regex = /<a.*?>[^<]*<\/a>/;

/* Traverse 'rootNode' and its descendants and modify '<a>' tags */
function modifyLinks(rootNode) {
    var nodes = [rootNode];
    while (nodes.length > 0) {
        var node = nodes.shift();
        if (node.tagName == "A") {
            /* Modify the '<a>' element */
            node.innerHTML = "~~" + node.innerHTML + "~~";
        } else {
            /* If the current node has children, queue them for further
             * processing, ignoring any '<script>' tags. */
            [].slice.call(node.children).forEach(function(childNode) {
                if (childNode.tagName != "SCRIPT") {
                    nodes.push(childNode);
                }
            });
        }
    }
}

/* Observer1: Looks for 'div.search' */
var observer1 = new MutationObserver(function(mutations) {
    /* For each MutationRecord in 'mutations'... */
    mutations.some(function(mutation) {
        /* ...if nodes have beed added... */
        if (mutation.addedNodes && (mutation.addedNodes.length > 0)) {
            /* ...look for 'div#search' */
            var node = mutation.target.querySelector("div#search");
            if (node) {
                /* 'div#search' found; stop observer 1 and start observer 2 */
                observer1.disconnect();
                observer2.observe(node, config);

                if (regex.test(node.innerHTML)) {
                    /* Modify any '<a>' elements already in the current node */
                    modifyLinks(node);
                }
                return true;
            }
        }
    });
});

/* Observer2: Listens for '<a>' elements insertion */
var observer2 = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        if (mutation.addedNodes) {
            [].slice.call(mutation.addedNodes).forEach(function(node) {
                /* If 'node' or any of its desctants are '<a>'... */
                if (regex.test(node.outerHTML)) {
                    /* ...do something with them */
                    modifyLinks(node);
                }
            });
        }
    });
});

/* Start observing 'body' for 'div#search' */
observer1.observe(document.body, config);

OTHER TIPS

In general, you can use Mutation Observers to listen for document changes. To avoid recursion, simply disconnect the mutation observer before changing the document, then enable it again.
Conceptually, it is not much different from the DOMNodeInserted event, so you can also remove the event listener, insert your nodes, then rebind the event listener. However, Mutation observers are more efficient, so you should use these instead of the DOM mutation events.

In this specific case (Google's search results), you can also use the hashchange event to detect when Google has rendered new search results. This method is only useful because there's a correlation between the location fragment, the search terms and the search results:

  1. The user enters a search term Enter
  2. The search results are updated.
  3. The location fragment is changed (https://www.google.com/search?q=old#q=<new term>).

Example:

// On document load
printResult();
// Whenever the search term is changed
window.addEventListener('hashchange', function(event) {
    printResult();
});
function printResult() {
    // Example: Print first search result
    console.log(document.querySelector('h3 a').href);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top