Related
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?
Greasemonkey (usually) doesn't have jQuery. So the common approach is to use
window.addEventListener('load', function() {
// your code here
}, false);
inside your userscript
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.
*/
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.
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.
Brock's answer is good, but I'd like to offer another solution to the AJAX problem that is more modern and elegant.
Since his script, like most others, also uses setInterval() to check periodically (300ms), 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.
You can use MutationObserver() to listen for DOM changes and respond to them as soon as the element is created
(new MutationObserver(check)).observe(document, {childList: true, subtree: true});
function check(changes, observer) {
if(document.querySelector('#mySelector')) {
observer.disconnect();
// code
}
}
Though since check() fires on every single DOM change, 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 countdown that resets every time something changes on the page.
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
}
This method is so versatile, 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});
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)
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
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?
I want to stop the execution of one single line from a site, so that the whole page is read by the browser except that single line. Or the browser may simply skip the execution of that javascript function.
OR
Is there way i can tweak the javascript somehow so that the random number generating function in javascript do not generate random number, but the numbers i want...
I dont have access to the site on which the script is hosted so all this needs to be done client side.
Firefox currently supports the beforescriptexecute event (as of Version 4, released on March 22, 2011)‡.
With that event and the // #run-at document-start directive, Firefox and Greasemonkey now seem to do a good job intercepting specific <script> tags.
This is still not possible for Chrome+Tampermonkey. For anything but Firefox+Greasemonkey, you will need to use techniques as shown in the other answers, below, of write a full browser extension.
The checkForBadJavascripts function encapsulates this. For example, suppose the page had a <script> tag like so:
<script>
alert ("Sorry, Sucka! You've got no money left.");
</script>
You could use checkForBadJavascripts like so:
checkForBadJavascripts ( [
[ false,
/Sorry, Sucka/,
function () {
addJS_Node ('alert ("Hooray, you\'re a millionaire.");');
}
]
] );
to get a much nicer message. (^_^)
See the inline documentation, in checkForBadJavascripts, for more.
To see a demonstration in a complete script, first visit this page at jsBin. You will see 3 lines of text, two of them added by JS.
Now, install this script (View source; it's also below.) and revisit the page. You will see that the GM-script deleted one bad tag and replaced another with our "good" JS.
‡ Note that only Firefox supports the beforescriptexecute event. And it was removed from the HTML5 spec with no equivalent capability specified.
Complete GM script example (The same as the one at GitHub and jsBin):
Given this HTML:
<body onload="init()">
<script type="text/javascript" src="http://jsbin.com/evilExternalJS/js"></script>
<script type="text/javascript" language="javascript">
function init () {
var newParagraph = document.createElement ('p');
newParagraph.textContent = "I was added by the old, evil init() function!";
document.body.appendChild (newParagraph);
}
</script>
<p>I'm some initial text.</p>
</body>
Use this Greasemonkey script:
// ==UserScript==
// #name _Replace evil Javascript
// #include http://jsbin.com/ogudon*
// #run-at document-start
// ==/UserScript==
/****** New "init" function that we will use
instead of the old, bad "init" function.
*/
function init () {
var newParagraph = document.createElement ('p');
newParagraph.textContent = "I was added by the new, good init() function!";
document.body.appendChild (newParagraph);
}
/*--- Check for bad scripts to intercept and specify any actions to take.
*/
checkForBadJavascripts ( [
[false, /old, evil init()/, function () {addJS_Node (init);} ],
[true, /evilExternalJS/i, null ]
] );
function checkForBadJavascripts (controlArray) {
/*--- Note that this is a self-initializing function. The controlArray
parameter is only active for the FIRST call. After that, it is an
event listener.
The control array row is defines like so:
[bSearchSrcAttr, identifyingRegex, callbackFunction]
Where:
bSearchSrcAttr True to search the SRC attribute of a script tag
false to search the TEXT content of a script tag.
identifyingRegex A valid regular expression that should be unique
to that particular script tag.
callbackFunction An optional function to execute when the script is
found. Use null if not needed.
*/
if ( ! controlArray.length) return null;
checkForBadJavascripts = function (zEvent) {
for (var J = controlArray.length - 1; J >= 0; --J) {
var bSearchSrcAttr = controlArray[J][0];
var identifyingRegex = controlArray[J][1];
if (bSearchSrcAttr) {
if (identifyingRegex.test (zEvent.target.src) ) {
stopBadJavascript (J);
return false;
}
}
else {
if (identifyingRegex.test (zEvent.target.textContent) ) {
stopBadJavascript (J);
return false;
}
}
}
function stopBadJavascript (controlIndex) {
zEvent.stopPropagation ();
zEvent.preventDefault ();
var callbackFunction = controlArray[J][2];
if (typeof callbackFunction == "function")
callbackFunction ();
//--- Remove the node just to clear clutter from Firebug inspection.
zEvent.target.parentNode.removeChild (zEvent.target);
//--- Script is intercepted, remove it from the list.
controlArray.splice (J, 1);
if ( ! controlArray.length) {
//--- All done, remove the listener.
window.removeEventListener (
'beforescriptexecute', checkForBadJavascripts, true
);
}
}
}
/*--- Use the "beforescriptexecute" event to monitor scipts as they are loaded.
See https://developer.mozilla.org/en/DOM/element.onbeforescriptexecute
Note that it does not work on acripts that are dynamically created.
*/
window.addEventListener ('beforescriptexecute', checkForBadJavascripts, true);
return checkForBadJavascripts;
}
function addJS_Node (text, s_URL, funcToRun) {
var D = document;
var scriptNode = D.createElement ('script');
scriptNode.type = "text/javascript";
if (text) scriptNode.textContent = text;
if (s_URL) scriptNode.src = s_URL;
if (funcToRun) scriptNode.textContent = '(' + funcToRun.toString() + ')()';
var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
//--- Don't error check here. if DOM not available, should throw error.
targ.appendChild (scriptNode);
}
The answer depends on details which were not provided (The exact page and line of code would be best), but here's how you do it in general:
If the offending JS code does not fire right away (Fires after DOMContentLoaded), then you can use Greasemonkey to replace the offending code. EG:
var scriptNode = document.createElement ("script");
scriptNode.textContent = "Your JS code here";
document.head.appendChild (scriptNode);
Done.
If the JS code fires immediately, then it gets more complicated.
First, grab a copy of the script and make the desired change to it. Save this locally.
Is the offending script in a file or is it in the main page HTML (<script src="Some File> versus <script>Mess O' Code</script>)?
If the script is in a file, install Adblock Plus and use it to block loading of that script. Then use Greasemonkey to add your modified code to the page. EG:
var scriptNode = document.createElement ("script");
scriptNode.setAttribute ("src", "Point to your modified JS file here.");
document.head.appendChild (scriptNode);
If the script is in the main HTML page, then install either NoScript (best) or YesScript and use it to block JavaScript from that site.
This means that you will then need to use Greasemonkey to replace all scripts, from that site, that you do want to run.
Method 1: using MutationObserver
beforescriptexecute no longer works in Firefox, nor did it work in Chrome. Luckily, there's an alternative, using MutationObserver, which is quite widely supported. The general idea is to add a MutationObserver at the beginning of pageload, which will run a callback whenever a new node is added to the DOM. Inside the callback, check for the existence of the <script> tag you want to alter or remove. If it exists, you can tamper with it (such as change its textContent, or have its src point somewhere else). Only after the callback finishes will the newly added script tag run, so this is an effective way to intercept and change Javascript on a page. Here's a live snippet example:
<script>
// This is the userscript code, which runs at the beginning of pageload
// Say we wanted to prevent the loading of the jQuery script tag below:
new MutationObserver((_, observer) => {
const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
if (jqueryScriptTag) {
console.log('Found jQuery script tag; now removing it!');
jqueryScriptTag.remove();
// We've done what we needed to do, no need for the MutationObserver anymore:
observer.disconnect();
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
console.log('After jQuery script tag. If done right, $ should be undefined:');
console.log(typeof $);
</script>
Here's an example userscript that blocks jQuery from loading in the <head> here on Stack Overflow:
// ==UserScript==
// #name Example jQuery removal
// #include https://stackoverflow.com*
// #run-at document-start
// #grant none
// ==/UserScript==
if (document.head) {
throw new Error('Head already exists - make sure to enable instant script injection');
}
new MutationObserver((_, observer) => {
const jqueryScriptTag = document.querySelector('script[src*="jquery"]');
if (jqueryScriptTag) {
jqueryScriptTag.remove();
observer.disconnect();
}
})
.observe(document.documentElement, { childList: true, subtree: true });
If you install this, you'll see that the loading of jQuery fails, resulting in lots of errors generated by Stack Overflow's JS.
Make sure to attach the MutationObserver as soon as possible - you need #run-at document-start to attach it before the page loads anything inside the <head>. (If using Tampermonkey / Chrome, you may need to enable experimental instant script injection to achieve this reliably - go to Tampermonkey Settings, Config mode: Advanced, scroll to the bottom, set Experimental Inject Mode to Instant.)
If you're writing userscripts for others, any you're using this technique, make sure to include instructions for instant script injection, since injection is not instant by default on Chrome.
Note that the observer is attached using
.observe(document.documentElement, { childList: true, subtree: true });
This attaches the observer to the <html> element, watches for added and removed immediate children with childList: true, and watches for added and removed nodes anywhere inside its descendants with subtree: true. Such a recursive listener is useful, but it's also computationally expensive on large dynamic pages, so make sure to remove it once it's achieved its purpose.
On a huge page, calling querySelector on every mutation could be costly, so you might want to iterate through the mutations (the first parameter to the observer's callback) and the mutations' addedNodes instead:
<script>
// This is the userscript code, which runs at the beginning of pageload
// Say we wanted to prevent the loading of the jQuery script tag below:
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === 1 && addedNode.matches('script[src*="jquery"]')) {
console.log('Found jQuery script tag; now removing it!');
addedNode.remove();
// We've done what we needed to do, no need for the MutationObserver anymore:
observer.disconnect();
return;
}
}
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<div>Example content</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
console.log('After jQuery script tag. If done right, $ should be undefined:');
console.log(typeof $);
</script>
You can also tweak inline scripts by assigning to their textContent inside the observer callback. The following snippet shows how you can change a random number generator function to always return 10, rather than 1-6:
<script>
// This is the userscript code, which runs at the beginning of pageload
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode.textContent.includes('const randomNum')) {
addedNode.textContent = addedNode.textContent.replace(
/const randomNum.*/,
'const randomNum = 10;'
);
observer.disconnect();
return;
}
}
}
})
.observe(document.documentElement, { childList: true, subtree: true });
</script>
<button>Roll Die</button>
<div>Results: <span></span></div>
<script>
const [button, span] = ['button', 'span'].map(tag => document.querySelector(tag));
button.onclick = () => {
const randomNum = Math.ceil(Math.random() * 6);
span.textContent = randomNum;
};
</script>
Method 2: using DevTools Local Overrides
Method 1 is the technique you can use when writing userscripts for others. But if the userscript is just for yourself, there's an easier method that can work in some situations. See this answer to the question Chrome Dev Tools - Modify javascript and reload: by going to the Sources -> Overrides tab in Chrome Devtools, and enabling local overrides, you can tell the browser to load a local copy of a .js instead of downloading the site's version. Then you can edit the local copy as desired, and it'll run instead of the built-in script. This is especially useful when tweaking large Javascript files - it's much more manageable than the MutationObserver approach.
But, there are some downsides:
To modify inline <script>// code here</script> tags, you'll have to download a local copy of the page. (So if the page serves dynamic content through the HTML response, you'll either have to re-save and re-tweak the .html for your overrides to use the new content, or you'll have to go back to the MutationObserver method.)
This is a technique developers may be able to understand (after some experimenting), but it's not so user-friendly. It may not be the sort of thing you'd want to walk a casual userscript consumer through.
You can use what is called a bookmarklet.
Build the js file you want to run on the other site: your.js
Make an HTML page with the following code:
<html>
<body>
<a href="javascript:(function(){var s=document.createElement('SCRIPT');s.src='/url/to/your.js?'+(Math.random());document.getElementsByTagName('head')[0].appendChild(s);})()">
Drag'n Drop this to your bookmarks
</a>
</body>
</html>
Replace /url/to/your.js with the path of your js file.
Load that small page in your browser and drag'n drop the link to your bookmark bar.
Go to the web site you want to hack, and click the bookmark you just created.This will load your.js in the page and run the code.
Note: the ?'+(Math.random()) part is to avoid your js being cached, this is not mandatory but it is helpful when you develop your.js
Using TamperMonkey? All you need to add below your // #grant in the TamperMonkey header is // #require http://urlofyoursite.com/myfile.js. For example, here is the very top of my TamperMonkey thingie:
// ==UserScript==
// #name Project
// #namespace http://tampermonkey.net/
// #version 0.1
// #description try to take over the world!
// #author You
// #match https://docs.google.com/presentation/*
// #grant none
// #require http://cdnjs.cloudflare.com/ajax/libs/annyang/2.1.0/annyang.min.js
// ==/UserScript==
I have a page that uses the TradingView widget, which you can type inside it any key on the keyboard to trigger a symbol search. I used Chrome's debugger to breakpoint and know exactly which line of code does that. I want the key event to be bubbled up to the general DOM to be handled by my own custom function.
How to do that? Any sample code would be appreciated. Here is the code snippet from TradingView ('t' is the key event):
a || (o.handle = a = function(t) {
return s === Te || t && Te.event.triggered === t.type ? s : Te.event.dispatch.apply(a.elem, arguments)
}
So instead of the return statement I want to send that key event to my own function. The full TradingView code where this code is: https://static.bitmex.com/assets/tradingView/static/tv-chart.min-6ce28e05fd34b9cf06a4e63b29980a72.js
Rather than altering the page code (which is possible, but can be tedious), consider changing your own listener to run in the capturing phase, rather than the bubbling phase. Eg, instead of
document.body.addEventListener('keypress', fn);
do
document.body.addEventListener('keypress', fn, true);
and it'll run before the event has bubbled down to the inner element.
If the page's listener on the inner element needs to be complete before your fn runs, you can call fn after a small setTimeout:
document.body.addEventListener('keypress', () => setTimeout(fn), true);
If you also want to prevent the widget's code from running, make sure to call stopPropagation in your capturing listener so the event doesn't capture down to the widget:
const fn = (event) => {
event.stopPropagation();
// rest of code
};
If the container you're sending the key event to is inside an iframe, the parent window won't see the event at all, due to security issues.
On most pages, you'd be able to communicate the event up to the parent by using postMessage, eg:
// ==UserScript==
// #name 0 New Userscript
// #include https://www.bitmex.com/app/trade/XBTUSD
// #include https://static.bitmex.com/chartEmbed*
// #grant GM_getValue
// #run-at document-start
// ==/UserScript==
if (window.location.host === 'www.bitmex.com') {
const mainFn = () => {
console.log('b');
};
// We're on the top level
const keydown = (e) => {
e.stopPropagation();
if (!e.altKey && e.key === 'b') {
mainFn();
}
};
window.addEventListener('keydown', keydown, true);
window.addEventListener('message', (e) => {
if (e.origin === 'https://static.bitmex.com' && e.data === 'keydown inside iframe') {
mainFn();
}
});
} else {
// We're in the iframe
// When a keypress is detected, message the parent window
const keydown = (e) => {
console.log('iframe kd');
e.stopPropagation();
if (!e.altKey && e.key === 'b') {
window.top.postMessage('keydown inside iframe', '*');
}
};
window.addEventListener('click', keydown, true);
}
The #run-at document-start is needed because the iframe has a CSP which forbids userscripts without it from running.
But unfortunately, there's another problem: in this particular case, the iframe window also overwrites EventTarget.prototype.addEventListener on pageload, before the userscript has a chance to run. While you could usually use // #run-at document-start to ensure you save a reference to a variable before a page script overwrites it, this isn't possible in an iframe; Chrome userscripts in iframes cannot run at the very start of pageload.
Without a reference to EventTarget.addEventListener (or EventTarget.prototype.onclick or EventTarget.prototype.onkeydown setters, etc), there's no way to access the browser-level API that registers the user's actions.
I don't think what you're looking for is possible in Tampermonkey.
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?
Greasemonkey (usually) doesn't have jQuery. So the common approach is to use
window.addEventListener('load', function() {
// your code here
}, false);
inside your userscript
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.
*/
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.
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.
Brock's answer is good, but I'd like to offer another solution to the AJAX problem that is more modern and elegant.
Since his script, like most others, also uses setInterval() to check periodically (300ms), 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.
You can use MutationObserver() to listen for DOM changes and respond to them as soon as the element is created
(new MutationObserver(check)).observe(document, {childList: true, subtree: true});
function check(changes, observer) {
if(document.querySelector('#mySelector')) {
observer.disconnect();
// code
}
}
Though since check() fires on every single DOM change, 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 countdown that resets every time something changes on the page.
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
}
This method is so versatile, 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});
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)
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
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?
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?
Greasemonkey (usually) doesn't have jQuery. So the common approach is to use
window.addEventListener('load', function() {
// your code here
}, false);
inside your userscript
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.
*/
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.
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.
Brock's answer is good, but I'd like to offer another solution to the AJAX problem that is more modern and elegant.
Since his script, like most others, also uses setInterval() to check periodically (300ms), 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.
You can use MutationObserver() to listen for DOM changes and respond to them as soon as the element is created
(new MutationObserver(check)).observe(document, {childList: true, subtree: true});
function check(changes, observer) {
if(document.querySelector('#mySelector')) {
observer.disconnect();
// code
}
}
Though since check() fires on every single DOM change, 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 countdown that resets every time something changes on the page.
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
}
This method is so versatile, 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});
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)
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
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?