Browser Extension: How to execute a function when an element is loaded - javascript

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
});
}

Related

Image replacing Chrome extension, incomplete behaviour

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).

Listening for changes made to CSS classes in Chrome Devtools

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.

MutationObserver works only with FF

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?

jQuery loaded html content - Check if images are loaded and rendered

I have tabs logic that load html templates inside a wrapper. That's works fine, but I included an animation that animate height of the tab wrapper when tab is switched.
The problem is the following: When a template contains <img src="/some-image.png"> the $('#tab-content').load('template-url', function() {...}) callback function sometimes is executed before the browser show the images. And my animation is not working correctly.
Code example (jsFiddle):
var currentHeight = $contentHolder.height();
$contentHolder.load(path, function() {
$contentHolder.stop();
function animateHeight() {
var loadedContentHeight = $contentHolder.css('height', 'auto').height();
$contentHolder.height(currentHeight);
$contentHolder.animate({
height: loadedContentHeight
}, 800, 'linear');
}
animateHeight();
});
I tried to set little timeout, but it's not working every time. If I set more that 300ms timeout, It feels like tabs are changed too slow.
I tried to execute the animation when $('img').load(function() {}) is fired, but with no luck.
This bug occurs most often when the web page is fully refreshed and each tab content loading for first time.
The image load event is kind of broken. To know when images are loaded you will have to observe the DOM for changes. Then on every change, you have to fetch all the new images and add the onload event to them from the callback. To prevent checking each element every time, once they've been loaded you could mark them as such by adding a data-loaded="true" property for instance.
One way to listen to DOM changes is the MutationObserver event. This is supported by all modern browsers and IE11.
A better supported solution (IE9 and up) can be found in this answer: Detect changes in the DOM. I will not repeat it here (but it's included in the demo below).
On every DOM change first you check for images without the data-loaded attribute that are already loaded anyway (this could happen when an image was still in the browser's cache) by checking element.complete. If so, fire the callback function and add the attribute to it.
If .complete is not the case, add an onload event to them that also fires the callback once it is loaded.
In your case you only want to fire your callback when all images are loaded, so I added a check if there's still images without the data-loaded attribute. If you remove that if-clause your callback would run after each image is loaded.
// Observe the DOM for changes
observeDOM(document.body, function(){
checkNewImages();
});
var checkNewImages = function() {
var images = $('img:not([data-loaded]').each(function() {
addImageLoadedEvent( this );
});
}
var addImageLoadedEvent = function(img) {
if (img.complete) {
onImageLoaded(img);
} else {
$(img).on('load', function() {
onImageLoaded(this);
});
}
}
// The callback that is fired once an element is loaded
var onImagesLoaded = function(img) {
$(img).attr('data-loaded', 'true');
if($('img:not([data-loaded])').length === 0) {
// YourCallbackHere();
}
}
DEMO: fire event on all images loaded
You can call your animateHeight function as each image in the loaded HTML is in turn loaded. You can expand this selection if you have other objects like videos.
// Call animateHeight as each image loads
var items = $('img', $contentHolder);
items.bind('load', function(){
animateHeight();
});
Updated demo: http://jsfiddle.net/jxxrhvvz/1/

Stop page load at specified time

I am trying to debug an element that loads about for the first .5 - 1 second on page load. It completely disappears after that; I am wondering if there is any way at all I can stop the page while it's loading at a certain time, either with a script or emulator so I can track down this element with developer tools.
You can call window.stop() to stop window loading, if that is what you are looking for.
It sounds like it would be easier to pause the script execution with the browsers javascript debugger though, by setting some breakpoints instead of manually inserting a stop statement.
Second attempt, now that I understand your question a little better.
You can use a MutationObserver to output changes to the DOM. Something as simple as
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
console.log(mutation);
});
})
observer.observe(document.body, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
var hiders = document.querySelectorAll('.hider'),
hiders = Array.prototype.slice.call(hiders);
hiders.forEach(function(el) {
el.parentNode.removeChild(el);
});
<div class="hider"></div>
<div class="hider"></div>
in the window's load handler will work, and will output a MutationRecord on each change. I'd bet one of those will be your element (specifically, the removedNodes of the record.) The browser console for this snippet will have what you'll see if you use this.

Categories