Replacing page elements dynamically through a greasemonkey script (even post page load) - javascript

I have the following javascript function:
function actionFunction() {
let lookup_table = {
'imageurl1': "imageurl2",
};
for (let image of document.getElementsByTagName("img")) {
for (let query in lookup_table) {
if (image.src == query) {
image.src = lookup_table[query];
}
}
}
}
I want it to work even after a page is fully loaded (in other words, work with dynamically generated html elements that appeared post-load by the page's js).
It could either be by running the function every x seconds or when a certain element xpath is detected within the page, or every time a certain image url is loaded within the browser (which is my main goal here).
What can I do to achieve this using javascript + greasemonkey?
Thank you.

Have you tried running your code in the browser's terminal to see if it works without greasemonkey involved?
As to your question - you could either use setInterval to run given code every x amount of time or you could use the MutationObserver to monitor changes to the webpage's dom. In my opinion setInterval is good enough for the job, you can try learning how the MutationObserver works in the future.
So rewriting your code:
// arrow function - doesn't lose this value and execution context
// setInterval executes in a different context than the enclosing scope which makes functions lose this reference
// which results in being unable to access the document object
// and also window as they all descend from global scope which is lost
// you also wouldn't be able to use console object
// fortunately we have arrow functions
// because arrow functions establish this based on the scope the arrow function is defined within
// which in most cases is global scope
// so we have access to all objects and their methods
const doImageLookup = () => {
const lookup_table = {
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png': 'https://www.google.com/logos/doodles/2022/gama-pehlwans-144th-birthday-6753651837109412-2x.png',
};
const imgElementsCollection = document.getElementsByTagName('img');
[...imgElementsCollection].forEach((imgElement) => {
Object.entries(lookup_table).forEach(([key, value]) => {
const query = key; // key and value describe object's properties
const replacement = value; // here object is used in an unusual way, i would advise to use array of {query, replacement} objects instead
if (imgElement.src === query) {
imgElement.src = replacement;
}
});
});
};
const FIVE_MINUTES = 300000; // ms
setInterval(doImageLookup, FIVE_MINUTES);
You could make more complex version by tracking the img count and only doing the imageLookop if their number increases. This would be a big optimization and would allow you to run the query more frequently (though 5 minutes is pretty long interval, adjust as required).

Related

Kill all setInterval in Angular

How can I find and stop all of setInterval (s) in Angular?
Some setInterval do not created by my program and run in another components or even a jQuery in my web application and I want to stop it!
CORS is not concerned.
If you absolutely need "a" solution, even a bad one, then you can overwrite the setInterval and setTimeout functions, and maintain your own list of assigned interval identifiers. However, a much better solution would be to write your code to deal with the fact that other parts of the libraries and frameworks you use can schedule timeouts and ntervals, instead of forcefully killing them with complete disregard of what was relying on them to run in the first place.
Indiscriminantly killing all timers will break things.
So, with that covered, the terrible solution would be code like this:
(function monitorIntervalsAndTimeouts() {
const timerList = [];
const oldSetInterval = globalThis.setInterval.bind(globalThis);
globalThis.setInterval = function(...args) {
const timerId = oldSetInterval(...args);
timerList.push(timerId);
return timerId;
};
const oldSetTimeout = globalThis.setTimeout.bind(globalThis);
globalThis.setTimeout = function(...args) {
const timerId = oldSetTimeout (...args);
timerList.push(timerId);
return timerId;
};
...
And of course, the corresponding clear functions:
...
const oldClearInterval = globalThis.clearInterval.bind(globalThis);
globalThis.clearInterval = function(timerId) {
const pos = timerList.indexOf(timerId);
if (pos > -1) timerList.splice(pos, 1);
return oldClearInterval(timerId);
};
const oldClearTimeout = globalThis.clearTimeout.bind(globalThis);
globalThis.clearTimeout = function(timerId) {
const pos = timerList.indexOf(timerId);
if (pos > -1) timerList.splice(pos, 1);
return oldClearTimeout(timerId);
};
...
and finally, a "clear all timers" function:
...
// Call this whatever you want.
globalThis.clearAllIntervalsAndTimeouts = function() {
while(timerList.length) {
// clearInterval and clearTimeout are, per the HTML standard,
// the exact same function, just with two names to keep code clean.
oldClearInterval(timerList.shift());
}
};
})();
You then make sure you load this using <script src="./monitor-intervals-and-timeouts.js"></script>, without the async and/or defer keyword, as very first script in your page's <head> element, so that it loads before anything else. And yes: that will block the page parsing while it does that. Or if you're using Node (which this will work fine for, too, because of the use of globalThis), you need to make sure it's the absolute first thing your main entry point imports/requires (depending on whether you're using ESM or CJS code).
Remember: this is the nuclear option. If you use this code, you're using it because you need to run something for local dev work that lets you debug your project, for finding out where timeouts/intervals are getting set and cleared. If you need it for that: go for it. But never, ever use this in production to solve a problem relating to timeouts/intervals used by third party/vendor code. Solve that the right way by actually solving whatever problem your own code has.

Best way to attach an arbitrary callback function to a dom element?

The following (vanillajs) code works fine
// library code:
let close_cb; // nasty global var...
...
let tree = document.createElement('ul');
tree.addEventListener('click', function(event) {
...
// let close_cb = tree.getAttribute('CLOSE_CB');
// let close_cb = tree.onchange;
close_cb(leaf.id, ', so there');
// user code:
function my_close_cb(id, msg) {
const footer = document.querySelector('footer');
footer.innerHTML = id + ' is closed' + msg;
}
// tree.setAttribute('CLOSE_CB',my_close_cb);
// tree.onchange = my_close_cb;
close_cb = my_close_cb;
However the commented-out s/getAttribute code fails, getAttribute puts full text of "function my_close_cb(..." in local close_cb.
The commented-out onchange hack actually works, but feels terribly dodgy to say the least, although it is certainly closer to what I'm after.
Note the "library code" is hand written and fully under my control, whereas "user code" is intended to be transpiled or otherwise machine-generated, so changing my_close_cb to accept a single event argument would be a complete non-starter.
What is the best way to attach an arbitrary callback function that accepts an arbitrary set of parameters to a dom element?
You can attach a plain json property to the DOM element.
document.body.callback = function cb(text) { console.log(text); };
document.body.callback("hello world");
Using tree.close_cb or any other property to store a function or anything else is totally fine, as the DOM is just a (persistent) tree of JavaScript objects. As such they behave like any other JS object, and properties can be added without any restrictions.

How to track a caller for native getters / setters of host objects in a browser?

Here is a use case:
Assume we have a web page that has an issue, which results in the page to be scrolled up on a mobile device at some point after DOMContentLoaded has fired.
We can legitimately assume there is something, which operates on document.documentElement.scrollTop (e.g. assigns it a value of 0).
Suppose we also know there are hundreds of places, where this can happen.
To debug the issue we can think of the following strategies:
check each event handler, which can set a value of 0 scrollTop, one by one
try to use debug function available in Chrome DevTools
Override a native scrollTop as:
var scrollTopOwner = document.documentElement.__proto__.__proto__.__proto__;
var oldDescr = Object.getOwnPropertyDescriptor(scrollTopOwner, 'scrollTop');
Object.defineProperty(scrollTopOwner, '_oldScrollTop_', oldDescr);
Object.defineProperty(scrollTopOwner, 'scrollTop', {
get:function(){
return this._oldScrollTop_;
},
set: function(v) {
debugger;
this._oldScrollTop_ = v;
}
});
function someMethodCausingAPageToScrollUp() {
document.scrollingElement.scrollTop = 1e3;
}
setTimeout(someMethodCausingAPageToScrollUp, 1000);
The issue with the second approach is that it doesn't work with native getters/setters.
The issue with the third approach is that even though now we can easily track what is assigning a value to the scrollTop property, we monkey patch a native getters/setters and risk to cause unnecessary side effects.
Hence the question: is there a more elegant solution to debug native getters and setters for web browser host objects (e.g. document, window, location, etc)?
const collector = [];
const originalScrollTop = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop');
Object.defineProperty(Element.prototype, 'scrollTop', {
get: () => originalScrollTop.get.call(document.scrollingElement),
set: function(v) {
collector.push((new Error()).stack)
originalScrollTop.set.call(document.scrollingElement, v)
}
})
You can collect stacktraces and inspect them later. We don't log it immediately to not cause IO delays.
Works in Chrome.
Turns out it is possible to use debug function for set method on a scrollTop property descriptor.
The code is as follows:
debug(Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop').set);
After that, we'll automatically stop in any function that tries to set a value to scrollTop.
If you need to automatically stop only on those functions which assign a value within a certain threshold (for example, between 0 and 500), you can easily do that too since debug function accepts a second argument (condition) where you can specify your condition logic.
For example:
// In that case, we'll automatically stop only in those functions which assign scrollTop a value within a range of [1, 499] inclusively
debug(Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop').set, 'arguments[0] > 0 && arguments[0] < 500');
Pros:
easy to use and doesn't require a lot of boilerplate code
Cons:
you'll have to evaluate that js snippet above each time when you refresh your page
Many thanks to Aleksey Kozyatinskiy (former Googler on DevTools team) for the detailed explanation.

Bug in my lazyload plugin for mootools

I want to implement a plug-in serial download pictures in MooTools. Let's say there are pictures with the img tag inside a div with the class imageswrapper. Need to consistently download each image after it loads the next and so on until all the images are not loaded.
window.addEvent('domready', function(){
// get all images in div with class 'imageswrapper'
var imagesArray = $$('.imageswrapper img');
var tempProperty = '';
// hide them and set them to the attribute 'data-src' to cancel the background download
for (var i=0; i<imagesArray.length; i++) {
tempProperty = imagesArray[i].getProperty('src');
imagesArray[i].removeProperty('src');
imagesArray[i].setProperty('data-src', tempProperty);
}
tempProperty = '';
var iterator = 0;
// select the block in which we will inject Pictures
var injDiv = $$('div.imageswrapper');
// recursive function that executes itself after a new image is loaded
function imgBomber() {
// exit conditions of the recursion
if (iterator > (imagesArray.length-1)) {
return false;
}
tempProperty = imagesArray[iterator].getProperty('data-src');
imagesArray[iterator].removeProperty('data-src');
imagesArray[iterator].setProperty('src', tempProperty);
imagesArray[iterator].addEvent('load', function() {
imagesArray[iterator].inject(injDiv);
iterator++;
imgBomber();
});
} ;
imgBomber();
});
There are several issues I can see here. You have not actually said what the issue is so... this is more of a code review / ideas for you until you post the actual problems with it (or a jsfiddle with it)
you run this code in domready where the browser may have already initiated the download of the images based upon the src property. you will be better off sending data-src from server directly before you even start
Probably biggest problem is: var injDiv = $$('div.imageswrapper'); will return a COLLECTION - so [<div.imageswrapper></div>, ..] - which cannot take an inject since the target can be multiple dom nodes. use var injDiv = document.getElement('div.imageswrapper'); instead.
there are issues with the load events and the .addEvent('load') for cross-browser. they need to be cleaned up after execution as in IE < 9, it will fire load every time an animated gif loops, for example. also, you don't have onerror and onabort handlers, which means your loader will stop at a 404 or any other unexpected response.
you should not use data-src to store the data, it's slow. MooTools has Element storage - use el.store('src', oldSource) and el.retrieve('src') and el.eliminate('src'). much faster.
you expose the iterator to the upper scope.
use mootools api - use .set() and .get() and not .getProperty() and .setProperty()
for (var i) iterators are unsafe to use for async operations. control flow of the app will continue to run and different operations may reference the wrong iterator index. looking at your code, this shouldn't be the case but you should use the mootools .each(fn(item, index), scope) from Elements / Array method.
Anyway, your problem has already been solved on several layers.
Eg, I wrote pre-loader - a framework agnostic image loader plugin that can download an array of images either in parallel or pipelined (like you are trying to) with onProgress etc events - see http://jsfiddle.net/dimitar/mFQm6/ - see the screenshots at the bottom of the readme.md:
MooTools solves this also (without the wait on previous image) via Asset.js - http://mootools.net/docs/more/Utilities/Assets#Asset:Asset-image and Asset.images for multiple. see the source for inspiration - https://github.com/mootools/mootools-more/blob/master/Source/Utilities/Assets.js
Here's an example doing this via my pre-loader class: http://jsfiddle.net/dimitar/JhpsH/
(function(){
var imagesToLoad = [],
imgDiv = document.getElement('div.injecthere');
$$('.imageswrapper img').each(function(el){
imagesToLoad.push(el.get('src'));
el.erase('src');
});
new preLoader(imagesToLoad, {
pipeline: true, // sequential loading like yours
onProgress: function(img, imageEl, index){
imgDiv.adopt(imageEl);
}
});
}());

Javascript - find out which resources failed to load from array

I'll be short with words, here's the situation:
for (var _im = 0; _im < slideshow._preloadbulks[slideshow._preloadCurrentbulk].length; _im++) {
var tmpSlideIndex = (slideshow._preloadCurrentbulk*slideshow._preloadMaxbulkSize)+_im;
slideshow._preloadSlides[tmpSlideIndex] = document.createElement('video');
slideshow._preloadSlides[tmpSlideIndex].autoplay = false;
slideshow._preloadSlides[tmpSlideIndex].loop = false;
slideshow._preloadSlides[tmpSlideIndex].addEventListener('canplaythrough', slideshow.slideLoaded, false);
slideshow._preloadSlides[tmpSlideIndex].src = slideshow._slides[tmpSlideIndex][slideshow.image_size+"_video_url"];
slideshow._preloadSlides[tmpSlideIndex].addEventListener('error', function(){
console.log(tmpSlideIndex);
slideshow._preloadSlides.splice(tmpSlideIndex,1);
slideshow._slides.splice(tmpSlideIndex,1);
slideshow.slideLoaded();
}, true);
}
As you can see, I have a video array and I'm loading each element src to the DOM to pre-load it.
It works just fine, but I have to deal with a situation when one resource is n/a, then I need to remove it from the existing arrays.
The addEventListener('error', works just fine, it detects the unavailable resource but when I'm logging tmpSlideIndex into the console I get a different value rather than the original slide index (because the loop continues).
I've tried setting the useCapture flag as you can see to the error handler, thinking that will do the trick but it won't.
Any tricks?
Thanks!
The issue is that when you are creating a closure over the tmpSlideIndex variable, it allows you to reference that variable inside the children function, but it's not creating a brand new variable, and since the loop continues and your error handler function executes asynchronously, the value of tmpSlideIndex will always be the last index of the loop. To keep the original value, we can create a self-executing function to wich we will pass the value of tmpSlideIndex. That self-executing function will effectively create a new scope and we will finally return a function that will create a closure over the slideIndex variable that lives in it's parent function scope.
slideshow._preloadSlides[tmpSlideIndex].addEventListener('error', (function(slideIndex) {
return function () {
console.log(slideIndex);
slideshow._preloadSlides.splice(slideIndex,1);
slideshow._slides.splice(slideIndex,1);
slideshow.slideLoaded();
};
})(slideIndex), true);

Categories