125

I'm writing a Greasemonkey user script, and want the specific code to execute when the page completely finishes loading since it returns a div count that I want to be displayed.

The problem is, that this particular page sometimes takes a bit before everything loads.

I've tried, document $(function() { }); and $(window).load(function(){ }); wrappers. However, none seem to work for me, though I might be applying them wrong.

Best I can do is use a setTimeout(function() { }, 600); which works, although it's not always reliable.

What is the best technique to use in Greasemonkey to ensure that the specific code will execute when the page finishes loading?

Mo D Genesis
  • 5,187
  • 1
  • 21
  • 32
Myne Mai
  • 1,305
  • 2
  • 10
  • 7
  • 4
    You could use `(function init(){var counter = document.getElementById('id-of-element');if (counter) { /* do something with counter element */ } else { setTimeout(init, 0);}})();` to continously poll for the existence of the element. That's most generic solution. – Rob W Oct 15 '12 at 14:12
  • 3
    Greasemonkey's cousins, Tampermonkey and Scriptish, support more [`@run-at`](https://tampermonkey.net/documentation.php#_run_at) values which include `document-idle` and `context-menu` which may be of use. It also appears that Greasemonkey is [adding support](https://github.com/greasemonkey/greasemonkey/issues/2109) for `document-idle` although it hasn't been documented as of yet. – Mr. Llama Nov 18 '15 at 17:32

7 Answers7

108

Greasemonkey (usually) doesn't have jQuery. So the common approach is to use

window.addEventListener('load', function() {
    // your code here
}, false);

inside your userscript

devnull69
  • 16,402
  • 8
  • 50
  • 61
  • 19
    Add jQuery is just easy as add `@require http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js` to your current script. I have put them into the template script too cuz i use always. Also put `this.$ = this.jQuery = jQuery.noConflict(true);` to avoid typical conflicts. – m3nda Dec 31 '14 at 15:56
  • Adding dependencies on jQuery when – sleblanc Feb 17 '18 at 23:05
  • 28
    Not sure why people are so eager to add jquery for everything. – Wyatt Ward Mar 27 '20 at 09:11
  • People are eager to use jQuery cause of the selectors. Document.querySelector does not work in userscripts. At least as far as my experience has shown. – mondjunge Jun 09 '21 at 09:36
  • 3
    @mondjunge your experience is wrong, it is standard feature and works in user scripts as well. – Alex G.P. Aug 14 '21 at 19:05
  • I still get a "document.querySelector is undefined" in every major Browser. Console, script, userscripts. I might just be stupid. ‍♀️ – mondjunge Aug 16 '21 at 07:57
  • 9
    I have only been using Tampermonkey in the last few years ... and its userscripts are perfectly supporting document.querySelector – devnull69 Aug 17 '21 at 08:43
  • Sadly, I still got `Undefine` when select element. – Salem Jun 10 '22 at 11:01
78

This is a common problem and, as you've said, waiting for the page load is not enough -- since AJAX can and does change things long after that.

There is a standard(ish) robust utility for these situations. It's the waitForKeyElements() utility.

Use it like so:

// ==UserScript==
// @name     _Wait for delayed or AJAX page load
// @include  http://YOUR_SERVER.COM/YOUR_PATH/*
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @grant    GM_addStyle
// ==/UserScript==
/*- The @grant directive is needed to work around a major design
    change introduced in GM 1.0. It restores the sandbox.

    If in Tampermonkey, use "// @unwrap" to enable sandbox instead.
*/

waitForKeyElements ("YOUR_jQUERY_SELECTOR", actionFunction);

function actionFunction (jNode) {
    //-- DO WHAT YOU WANT TO THE TARGETED ELEMENTS HERE.
    jNode.css ("background", "yellow"); // example
}

Give exact details of your target page for a more specific example.

double-beep
  • 5,031
  • 17
  • 33
  • 41
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • What if the target page already has JQuery on it? Loading JQuery on the page breaks the original code, but my GM script runs before JQuery runs. The specific page for my scenario is backpack.tf, which is running JQuery 1.7.2. – Jacklynn Jul 26 '14 at 04:12
  • 4
    @Jack, Using jQuery in the script does not load it into the page, nor does it break anything when you use the `@grant GM_addStyle` as shown. That's what it's there for. – Brock Adams Jul 26 '14 at 04:26
  • 2
    make sure to pass in `true` as the third argument to `waitForKeyElements` if you only want your code to run once – starwarswii Oct 27 '20 at 21:49
59

As of Greasemonkey 3.6 (November 20, 2015) the metadata key @run-at supports the new value document-idle. Simply put this in the metadata block of your Greasemonkey script:

// @run-at      document-idle

The documentation describes it as follows:

The script will run after the page and all resources (images, style sheets, etc.) are loaded and page scripts have run.

Leviathan
  • 2,468
  • 1
  • 18
  • 24
  • 12
    Warning. Greasemonkey appears to have implemented this differently than Chrome/Tampermonkey did. So scripts may not work the same across browsers. It's still more robust to use something like `waitForKeyElements` or `MutationObserver`. – Brock Adams Jan 16 '17 at 00:56
57

I'd like to offer another solution to the AJAX problem that is more modern and elegant.

Brock's script, like most others, uses setInterval() to check periodically (300ms), so it can't respond instantly and there is always a delay. And other solutions uses onload events, which will often fire earlier than you want on dynamic pages.

The solution: Use MutationObserver() to directly listen for DOM changes to respond immediately after an element is inserted

(new MutationObserver(check)).observe(document, {childList: true, subtree: true});

function check(changes, observer) {
    if(document.querySelector('#mySelector')) {
        observer.disconnect();
        // actions to perform after #mySelector is found
    }
}

The check function will fires immediately after every DOM change. This allows you to specify arbitrary trigger conditions so you can wait until the page is in the exact state required before you execute your own code.

Note that, this may be slow if the DOM changes very often or your condition takes a long time to evaluate, so instead of observing document, try to limit the scope by observing a DOM subtree that's as small as possible.

This method is very general and can be applied to many situations. To respond multiple times, just don't disconnect the observer when triggered.

Another use case is if you're not looking for any specific element, but just waiting for the page to stop changing, you can combine this with a idle timer that gets reset when the page changes.

var observer = new MutationObserver(resetTimer);
var timer = setTimeout(action, 3000, observer); // wait for the page to stay still for 3 seconds
observer.observe(document, {childList: true, subtree: true});

// reset timer every time something changes
function resetTimer(changes, observer) {
    clearTimeout(timer);
    timer = setTimeout(action, 3000, observer);
}

function action(observer) {
    observer.disconnect();
    // code
}

You can listen for attribute and text changes as well. Just set attributes and characterData to true in the options

observer.observe(document, {childList: true, attributes: true, characterData: true, subtree: true});
goweon
  • 1,111
  • 10
  • 19
16

wrapping my scripts in $(window).load(function(){ }) never failed for me.

maybe your page has finished, but there is still some ajax content being loaded.

if that is the case, this nice piece of code from Brock Adams can help you:
https://gist.github.com/raw/2625891/waitForKeyElements.js

i usually use it to monitor for elements that appears on postback.

use it like this: waitForKeyElements("elementtowaitfor", functiontocall)

Community
  • 1
  • 1
RASG
  • 5,988
  • 4
  • 26
  • 47
  • Should I paste it inside my User script file ? I did that and it shows me errors on `$` is not define . – Salem Jun 10 '22 at 11:23
8

If you want to manipulate nodes like getting value of nodes or changing style, you can wait for these nodes using this function

const waitFor = (...selectors) => new Promise(resolve => {
    const delay = 500
    const f = () => {
        const elements = selectors.map(selector => document.querySelector(selector))
        if (elements.every(element => element != null)) {
            resolve(elements)
        } else {
            setTimeout(f, delay)
        }
    }
    f()
})

then use promise.then

// scripts don't manipulate nodes
waitFor('video', 'div.sbg', 'div.bbg').then(([video, loading, videoPanel])=>{
    console.log(video, loading, videoPanel)
    // scripts may manipulate these nodes
})

or use async&await

//this semicolon is needed if none at end of previous line
;(async () => {
    // scripts don't manipulate nodes
    const [video, loading, videoPanel] = await waitFor('video','div.sbg','div.bbg')
    console.log(video, loading, video)
    // scripts may manipulate these nodes
})()

Here is an example icourse163_enhance

bilabila
  • 973
  • 1
  • 12
  • 18
3

To detect if the XHR finished loading in the webpage then it triggers some function. I get this from How do I use JavaScript to store "XHR finished loading" messages in the console in Chrome? and it real works.

    //This overwrites every XHR object's open method with a new function that adds load and error listeners to the XHR request. When the request completes or errors out, the functions have access to the method and url variables that were used with the open method.
    //You can do something more useful with method and url than simply passing them into console.log if you wish.
    //https://stackoverflow.com/questions/43282885/how-do-i-use-javascript-to-store-xhr-finished-loading-messages-in-the-console
    (function() {
        var origOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this.addEventListener('load', function() {
                console.log('XHR finished loading', method, url);
                display();
            });

            this.addEventListener('error', function() {
                console.log('XHR errored out', method, url);
            });
            origOpen.apply(this, arguments);
        };
    })();
    function display(){
        //codes to do something;
    }

But if there're many XHRs in the page, I have no idea how to filter the definite one XHR.

Another method is waitForKeyElements() which is nice. https://gist.github.com/BrockA/2625891
There's sample for Greasemonkey use. Run Greasemonkey script on the same page, multiple times?

Dave B
  • 69
  • 3