I am new to web dev, so I'm sorry if this is a silly question. I followed a very simple tutorial and made a chrome extension that replaces images on screen with that of random ones I picked from my computer (mostly crappy meme valentines cards). However I have noticed some peculiar behaviour.
1) it doesn't replace all the images. for example in the google images page, it replaces the first 5 or 6 lines perfectly, and after that replaces none
2) similarly, on netflix it also does 1), but on top of that, if I go left or right on one of the sliders that it worked on, the images go away and it goes back to the default images.
How can I fix this so it 1) replaces all images and 2) keeps those changes.
I have attached a gif demonstrating this issue below
https://i.imgur.com/4hZuzsX.gif
It will happen mainly for two reasons:
Not all the img tags exist when you perform the replacement;
The elements get rerendered when you perform some action on the page (like clicking or scrolling).
I tested in Google Image Search and yep - not all the img tags are there from the start. So you'd have to replace them, too, when they appear.
Go to Google Image Search and test this script once. Then scroll down to the bottom and test it once again to see if it changed:
// this will tell you how many img tags exist in the page
console.log(document.getElementsByTagName('img').length);
It most certainly increased as you were scrolling down. Right?
Possible workaround - observing element mutations
In days of old there were events you could listen to for changes whenever elements were added or removed, but they've been deprecated for quite some time. Let's get this out of the way: don't use mutation events!
Nowadays if you want to monitor changes in the page I suggest you take a look at Mutation Observers.
Mutation Observers
A mutation happens when an element or its contents or attributes are changed. So in short, Mutation Observers provide you a way to monitor changes in an element.
The most relevant difference (at least for extension developers) between observing mutations and listening to events is that mutations capture changes that are done programmatically, which makes it so useful since more often than not we must alter a page whose source we did not author.
Could you listen to a given event like, say, scrolling? Yes. But since there most likely are event listeners going on concurrently with your own that will cause mutations in the page, it's hard to make sure that the element you want to change yourself will already exist by the time the event is triggered (the page's native code might still be doing its thing).
Example
Here's an example borrowing from the above MDN link, plus the desired behavior you described - replacing images, whenever they are added:
EDIT: I'm going to be more descriptive in the comments and leave links to where you should browse for additional information in the MDN docs in case you need them. Note that the example is targeting a specific element (#some-id) but you could target document.body, but not in Stack Overflow's fiddle sandbox, apparently.
// Select the node that will be observed for mutations
// I'm targeting a specific node, but it COULD BE document.body,
// which would observe the entire document for changes
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit
const config = {
attributes: true, // watch for changes to attributes on the node(s)
childList: true, // watch for the addition or removal of child nodes
subtree: true // all monitoring rules apply to child elements as well
};
/*
PS: all settings are 'optional' but at least one among
childList, attributes or characterData must be true.
Again, don't forget to take a look on the docs!
*/
// Callback function to execute when mutations are observed
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver#The_callback_function
const callback = function(mutationsList, observer) {
// mutationsList is an array of MutationRecord objects,
// describing each change that occurred.
// See https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
// observer is the MutationObserver instance that was triggered
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
// This will be executed when a new image is added
console.log('A child node has been added or removed.');
// Iterate over the added nodes to check if there are any images
for (let node of mutation.addedNodes) {
// Replace the .src attribute if the element is an img
if (node.tagName === 'IMG') {
node.src = 'https://pm1.narvii.com/6334/121fb1f34767638906fac47cf818dc9c326bc936_128.jpg';
}
}
console.log('Garmanarnar will rule above all');
} else if (mutation.type === 'attributes') {
// This will be executed when .src is changed
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
// observer.disconnect();
<script>
function addImg() {
console.clear();
let img = document.createElement('img');
img.src = 'https://loremflickr.com/320/240';
document.getElementById('some-id').appendChild(img);
}
</script>
<button style="cursor: pointer;" onclick="addImg()">
<strong>Click me</strong>
</button>
<div id="some-id">
<img src="https://pm1.narvii.com/6334/121fb1f34767638906fac47cf818dc9c326bc936_128.jpg" alt="Garmanarnar" />
</div>
With each click, you should see only Garmanarnars (blue friendly alien doing thumbs up), not any random pic of cats or anything else. This means the new img tag got its src attribute replaced successfully.
In Javascript we use lots and lots of callback functions, so if you're not used to them, you might have a lot to digest, but it will be worth your while learning them.
Additional considerations (you may skip if you're satisfied)
I get that it's just a prank and it would be a 'passable' flaw, but if you want to target every single image in a page, looking for img tags might not be always enough, since some elements will have images included through CSS background-image property, or even svg elements.
I made a Chrome extension myself a few years ago and maybe it's also worth mentioning that you must pay attention whether an image is part of an iframe or not. They're not part of the current document - it's a page from elsewhere and it's sandboxed for security reasons. Take a look at this answer from another Stack Overflow question to see how to enable accessing iframes within a page (you still have to create rules for the target page, though).
Related
I have written a Chrome extension that loads a script with run_at set to document_start.
My script uses Mutation Observer in order to invoke some code upon the addition of an element with id X to a certain webpage.
That element is created when the page first loads, and then recreated when part of the page reloads following some button click.
My mutation observer fails to detect that element in either case.
However, if I duplicate the element (Developer Tools > Elements > element > context menu > Duplicate element), then my mutation observer does detect the duplicate.
I ran a test: I changed the id of some random element to X. It was not detected. That has made me suspect the original element is first created and then has its id assigned or updated. I am not sure how I could prove of refute this theory (the process of generating the webpage code looks very complex, with layers and layers of scripts involved, and I have very little experience).
Does this theory make sense? Can I observe an id change? If the theory does not make sense or if it is impossible to observe an id change, what alternatives do I have?
Thank you!
Edit:
My code looks like this:
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for (let i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].id == "X") {
alert("X detected!");
};
};
});
});
observer.observe(document, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
I am working on a browser extension.
It has two parts:
popup - which contains checkboxes
content script - which contains the code to alter the CSS property
I am saving the states of checkboxes so that the next time I open the popup again the same checkboxes are marked as checked.
When I use the checkboxes they change the DOM as intended, however when I try to alter the DOM after the page is loaded, changes are not reflected. This is probably because the element on which I want to perform the operation is loaded slow and thus required operations fail.
I tried to use onload and ready but nothing worked
$('.question-list-table').on('load', function() {
browser.storage.local.get(["options"], modifyThenApplyChanges)
});
I also tried, but nothing changed.
$('body').on('load','.question-list-table', function() {
browser.storage.local.get(["options"], modifyThenApplyChanges)
});
Also, there is no visible error with the popup or content script as I test in both Google Chrome and Mozilla Firefox.
Update:
As suspected earlier, the target element is loaded slowly so I used setTimeout for 5 seconds and the script is working as intended.
Loading time is variable and I want to show my changes as early as possible everything in a consistent manner.
After going through MutationObserver as suggested by #charlietfl in the comment section, this is what I coded and works for me
// Mutation Observer
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes.length) {
//do stuff
}
});
});
el = document.getElementsById('elementId');
if(el) {
observer.observe(el, {
childList: true // specify the kind of change you are looking for
});
}
I want to check if someone has modified the CSS rules of an element through Chrome Devtools. Normally MutationObserver would suffice for this, however, MutationObserver is only able to observe changes to the inline style attribute, and cannot detect when the element's CSS properties have been modified through devtools (or if a new rule is added to the inspector stylesheet).
What's the best way to listen for Chrome Devtools's CSS rule changes with JavaScript?
My goal was to use this within a Chrome extension. I have just found that listening for user modifications through Devtools is possible with:
//This will only work in the devtools.js file in a Chrome extension
chrome.devtools.inspectedWindow.onResourceContentCommitted.addListener(function(resource, content) {
alert(resource.url);
alert(content);
});
More information is available here.
One indirect way to detect initial changes would be via transition events. Of course, this can be bypassed, but then again, everything you do on the frontend can.
The gist of the idea is to add an incredibly short transition to all elements and listen to any transition events fired by it.
let timeout = null;
Array.from(document.querySelectorAll('*')).forEach(element => {
element.addEventListener('transitionstart', () => {
if (timeout) return;
timeout = setTimeout(() => {
console.log('styles changed');
timeout = null;
}, 0);
});
});
* {
transition: all 0.0001s;
}
<p>Edit my styles in the console to see messages</p>
Alternatively you could look to find a way to diff computed styles on all elements when certain events are fired, but I can't see a way how that wouldn't be prohibitively expensive to compute.
I have a DIV with id='obsah_popis' (which basically hold all the page content) and this div is filled dynamically upon request (by clicking on menu buttons, photogalery buttons etc.) so its height is not constant but changes overtime as different objects go in or out (loaded in/out).
I need to monitor its actual height for my custom build (coded) scroller which recalculate its height and everything necessary around it according to that height value.
I made a MutationObserver for this reason that works great...but ONLY IN Firefox (46+) - when I run it under Chrome (63) or Opera (50) it does not work at all.
My code for the observer part looks like this (for the purpose of testing I only added alert() to fire up letting me know it was triggered):
var test = new MutationObserver(function(mutations) {
alert();
//resizeSkrolerHandle();
});
test.observe(document.querySelector('#obsah_popis'), {
attributes:true,
childList:true,
characterData:true,
subtree:true
});
BTW strangelly enough (at least for me) this other MutationObserver I run at the same place (just underneath my not functioning one) work perfectly in all browsers:
var bbb = new MutationObserver(function(mutations) {
document.getElementById("parallax_pozadie").style.top = document.getElementById("parallax_static").style.top = (document.getElementById("skroler").getBoundingClientRect().top * -1) * scrollSpeedMultiply + "px";
});
bbb.observe(document.querySelector('#skroler'), {
attributes:true,
childList:false
});
The only difference I see there is the fact that this working one is actually fired up by MANUAL INPUT (dragging of my custom scroller) whereas that non-working one is supposed to be fired up programmatically.
Does anyone know the reason and possible solution to this?
As soon as I try to inject raw html in the document's body, a saved instance of an element retrieved with .querySelector(); abruptly resets it's .clientHeight and .clientWidth properties.
The following page shows the problem
<head>
<script>
window.addEventListener('load', pageInit);
function pageInit() {
// saving instance for later use
var element = document.querySelector('#element')
alert(element.clientHeight); // returns 40
document.querySelector('body').innerHTML += '<p>anything</p>';
alert(document.querySelector('#element').clientHeight); // returns 40
alert(element.clientHeight); // returns 0
}
</script>
<style>
#element {
height: 40px;
}
</style>
</head>
<body>
<div id="element"></div>
</body>
Why exactly the instance properties of the element variable gets reset?
As pointed in this question addEventListener gone after appending innerHTML eventListeners gets detached but this still doesn't explain why that element node still exist and why it's properties were zeroed out.
It would seem that when you reset the innerHTML of the body element, you're causing the entire page to be re-rendered. Thus, the div#element that is shown on the page is not the same element that the JavaScript variable element points to; rather, the one you see is a new element that was created when the page was re-rendered. However, the div#element that element points to still exists; it hasn't yet been destroyed by the browser's garbage collector.
To prove this, try replacing the last alert (the one that alerts a 0) with console.log(element), and then attempting to click on the element in the console window. You'll see <div id="element"></div>, but when you click on it, nothing happens, since the div is not on the page.
Instead, what you want to do is the following:
const newEl = document.createElement('p');
newEl.innerHTML = 'anything';
document.querySelector('body').appendChild(newEl);
instead of setting the body.innerHTML property.
FYI you should probably switch the alerts for console.logs, since alerts annoy the hell out of people and the console is the way to test things out in development.
You're running into a problem where the browser has reflowed the doc and dumped its saved properties. Also known as Layout Thrashing. Accessing certain DOM properties (and/or calling certain DOM methods) "will trigger the browser to synchronously calculate the style and layout". That quote is from Paul Irish's Comprehensive list of what forces layout/reflow. Though innerHTML isn't included there, pretty sure that's the culprit.
In your sample, the first alert works for obvious reasons. The second works because you're asking the browser to go and find the element and get the property again. The third fails because you're relying on a stored value (that's no longer stored).
The simplest way around it is to use requestAnimationFrame when using methods/properties that will force a reflow.
window.addEventListener('load', pageInit);
function pageInit() {
// saving instance for later use
var element = document.querySelector('#element');
console.log("before:", element.clientHeight); // returns 40
requestAnimationFrame(function() {
document.querySelector('body').innerHTML += '<p>anything</p>';
});
console.log("WITH rAF after:", element.clientHeight); // returns ~0~ 40!
// out in the wild again
document.querySelector('body').innerHTML += '<p>anything</p>';
// oh no!
console.warn("W/O rAF after:", element.clientHeight); // returns 0
}
#element {
height: 40px;
}
<div id="element"></div>