Is there a way to detect stylesheet changes ? MutationObserver only tracks inline css changes on the element.
html
<div class="exampleClass"></div>
js
let config = {
attributes: true,
// attributeFilter: ["style"],
};
let mutationCallback = function(mutationsList) {
mutationsList.forEach((mutation, i) => {
console.log(mutation);
});
};
let observer = new MutationObserver(mutationCallback);
observer.observe(document.querySelector('.exampleClass'), config);
If I modify the element throught js with
document.querySelector(‘exampleClass’).style.top = '10px'
or the webconsole inspector directly on the node, the mutation observer callback is called, but if the class (not the node itself) is modified in the webconsole inspector there is no callback
Related
I'm making a simple Chrome extension that modifies some information shown on the thumbnails of the recommended videos on YouTube.
For a simplification, let's say I want to replace the video length (e.g., "14:32") with the name of the channel (e.g., "PewDiePie").
Let's say I'm in the page of any YouTube video (video player in the center, list of thumbnails on the right side).
I can do this replacement once:
function processNode(node: HTMLElement) {
const channelName = node
.closest('ytd-thumbnail')
?.parentElement?.querySelector('.ytd-channel-name')
?.querySelector('yt-formatted-string');
if (channelName?.textContent) node.textContent = channelName?.textContent;
}
async function replaceCurrentThumbnailTimes(): Promise<void> {
for (const node of document.querySelectorAll(
'span.ytd-thumbnail-overlay-time-status-renderer',
)) {
processNode(node as HTMLElement);
}
}
void replaceCurrentThumbnailTimes();
This works, but then if I navigate to a new page---for example by clicking any video in the list of recommended---the video lengths are not updated.
The values I changed remain the same, despite the thumbnails being updated to refer to a new video.
As an example, let's say I open a YouTube video and the first thumbnail on the side is a video by Alice.
The time on the thumbnail is replaced by Alice, as I wanted.
Next, I click in some other video, and the first thumbnail is now a video by Bob.
The time on that thumbnail is still Alice, despite that being out of date.
I tried using the MutationObserver API, and that works when new thumbnails are added to the DOM (e.g., when scrolling down the page), but it also doesn't work for when the existing thumbnail elements are modified.
This is what I tried:
async function replaceFutureThumbnailTimes(): Promise<void> {
const observer = new MutationObserver((mutations) => {
// For each new node added, check if it's a video thumbnail time
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (
node instanceof HTMLElement &&
node.classList.contains(
'ytd-thumbnail-overlay-time-status-renderer',
) &&
node.getAttribute('id') === 'text'
) {
processNode(node);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
}
void replaceFutureThumbnailTimes();
I think it might have something to do with the shadow/shady DOM, but I can't figure out how to go around it.
PS: to make it simpler for others to reproduce, I put the same code in pure javascript on pastebin, so that you can just copy it into the chrome console: https://pastebin.com/NWKfzCwQ
As #RoryMcCrossan and #wOxxOm suggested in the comments to the question, indeed the MutationObserver works, and I was just misusing it. Thanks to both of them!
In this case, I needed to monitor for attributes changes, and check for changes in the aria-label, for nodes with id text.
Here is the code in javascript which accomplishes this:
async function replaceFutureThumbnailTimes() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
// If attributes were changed, check if it's the thumbnail time
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'aria-label' &&
mutation.target.getAttribute('id') === 'text') {
processNode(mutation.target);
}
// For each new node added, check if it's a video thumbnail time
for (const node of mutation.addedNodes) {
if (
node instanceof HTMLElement &&
node.classList.contains(
'ytd-thumbnail-overlay-time-status-renderer',
) &&
node.getAttribute('id') === 'text'
) {
processNode(node);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: false,
attributes: true,
});
}
void replaceFutureThumbnailTimes();
I'm trying to detect the height of an element set dynamically by an image slider plugin and use it to set the height of a container.
Getting "TypeError: MutationObserver.observe: Argument 1 does not implement interface Node."
I checked MutationObserver documentation and its options. Saw that
At a minimum, one of childList, attributes, and/or characterData must be true when you call observe(). Otherwise, a TypeError exception will be thrown.
and I am setting attributes to true but still getting the typeError
jQuery(document).ready(function($){
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for(const mutation of mutationsList) {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
};
const observer = new MutationObserver(callback);
//set up your configuration
const config = { attributes:true, subtree: false };
var changingContainer = $('.soliloquy-viewport');
//start observing
observer.observe(changingContainer, config);
//change height on button press
function changeHeight(){
changingContainer.height(Math.floor((Math.random() * 100) + 20));
}
$("#height").click(changeHeight);
});
.soliloquy-viewport{
background: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<div class="soliloquy-viewport">
Hello
</div>
<button id="height">
change height
</button>
MutationObervers only work on an Element object, not jQuery objects. Change the first argument of the observe() to the underlying Element by using get() like this:
observer.observe(changingContainer.get(0), config);
Or by accessing the jQuery object by index like this:
observer.observe(changingContainer[0], config);
jQuery(document).ready(function($) {
let $changingContainer = $('.soliloquy-viewport');
const observer = new MutationObserver((ml, o) => {
for (const m of ml) {
console.log('The ' + m.attributeName + ' attribute was modified.');
}
});
observer.observe($changingContainer.get(0), {
attributes: true,
subtree: false
});
//change height on button press
$("#height").click(() => $changingContainer.height(Math.floor((Math.random() * 100) + 20)));
});
.soliloquy-viewport {
background: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<div class="soliloquy-viewport">Hello</div>
<button id="height">change height</button>
Note that this only works for a single element. For multiple elements with the same class, you will need to loop through them and apply the MO individually.
I have a script where I´ve use on the first slide of Adobe Captivate, to automate the task ok creating, courses, the script create the UX, navigation elements, intro/end motions, a game, insert spritesheets with characters, etc...
I´ve used DOMNodeInserted until know to check the modifications on the slide, when the user go to the next slide, the elements are added to the DOM and the page content is changed I´ve used this timer until now to call the function:
function detectChange(){
var slideName = document.getElementById('div_Slide')
slideName.addEventListener("DOMNodeInserted", detectChange, false);
updateSlideElements();
setTimeout(updateSlideElements, 100);
}
So I´m trying to use mutation Observer now:
var observer = new MutationObserver(function(mutations, observer) {
updateSlideElements();
});
observer.observe(document.getElementById('div_Slide').firstChild, {
attributes: true,
childList:true
});
But this is what´s happening, before with setTimeout I could reach the following element:
var motionText2 = document.querySelectorAll('div[id*=motion][class=cp-accessibility]');
This element is the firstChild of:
And the element can be found:
But now with mutationObserver the console returns empty:
I´ve just use a setTimeout inside the observer and watch the parent container not the firstChild:
var observer = new MutationObserver(function(mutations, observer) {
setTimeout(updateSlideElements, 100);
});
observer.observe(document.getElementById('div_Slide'), {
attributes: true,
childList:true
//subtree:true
});
I've got a 3rd-party script that loads a photo gallery on my page with the images coming from off-site.
My page starts as empty:
<div class="container-fluid" id="cincopa">
</div>
The 3rd-party script then adds other stuff (like the frame of the photo gallery):
<div class="container-fluid" id="cincopa">
<div id="cp_widget_38cc1320-f4a4-407a-a80e-1747bd339b64">
</div>
</div>
Then finally the images load:
<div class="container-fluid" id="cincopa">
<div id="cp_widget_38cc1320-f4a4-407a-a80e-1747bd339b64">
<div class="galleria_images">
<div class="galleria_image">SomeImage</div>
<div class="galleria_image">SomeImage</div>
<div class="galleria_image">SomeImage</div>
</div>
</div>
</div>
I want to:
display a loading animation
set a MutationObserver on $('#cincopa')
when it detects that $('.galleria_image') has been created, it means images have been loaded, so I can
remove the loading animation
Code:
var target = document.querySelector('#cincopa');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
console.log(mutations);
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true };
// start the observer, pass in the target node, as well as the observer options
observer.observe(target, config);
The problem is that the MutationObserver only console logs one mutation and the MutationRecord only has one mutation in its array. I would expect numerous mutations as the 3rd-party script creates DOM elements.
Am I misunderstanding how MutationObserver works?
Here's the solution
// This is MeteorJS creating the loading spinning thing
var loadingView = Blaze.render(Template.loading, $('#cincopa')[0]);
// select the target node
var target = document.querySelector('#cincopa');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.target.className === "galleria_image"){
// a image has been loaded, so remove the loading spinner and
// kill the observer
Blaze.remove(loadingView);
observer.disconnect();
}
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true, subtree: true };
// start the observer, pass in the target node, as well as the observer options
observer.observe(target, config);
Updated Solution
.forEach is dumb and doesn't have a good way to break out of the loop, which meant that I was getting multiple commands to Blaze.remove() and observer.disconnect(), even after .galleria_image had been found.
So I used underscore instead:
// create an observer instance
var observer = new MutationObserver(function(mutations) {
var loaded = _.find(mutations, function(mutation){
console.log("observer running");
return mutation.target.className === "galleria-image";
});
if(loaded){
Blaze.remove(loadingView);
observer.disconnect();
console.log("observer stopped");
};
});
There's an option to allow you to do exactly what you want: observe the subtree of an element. Just add subtree: true to your config for the MutationObserver.
// ...
// In this case case only these two are needed, I believe.
var config = {
childList: true,
subtree: true
};
// ...observe
This should allow you to figure when .gallaria_images has been inserted. As a side note, you (OP) should also double check that images are loaded when that happens.
I would like to use a MutationObserver object to observe changes to some of my DOM nodes.
The docs give an example of creating a MutationObserver object and registering it on a target.
// select the target node
var target = document.querySelector('#some-id');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true };
// pass in the target node, as well as the observer options
observer.observe(target, config);
Say I have the code above, but just under it, I place this code:
var target2 = document.querySelector('#some-other-id');
var config2 = {attributes: true, subtree: true};
observer.observe(target2, config2);
Will observer:
now be observing 2 targets?
will it stop observing target?
will it decide not to observe target2?
will it throw an error?
or will it exhibit some other behavior?
The observer will now be watching two targets - target and target2 per your definitions. No error will be thrown, and target will not be "unregistered" in favor of target2. No unexpected or other behaviors will be exhibited.
Here is a sample which uses the same MutationObserver on two contenteditable elements. To view this, delete the <span> node from each contenteditable element and view the behavior span across both observed elements.
<div id="myTextArea" contenteditable="true">
<span contenteditable="false">Span A</span>
</div>
<div id="myTextArea2" contenteditable="true">
<span contenteditable="false">Span B</span>
</div>
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
//console.log($(mutation.removedNodes)); // <<-- includes text nodes
$(mutation.removedNodes).each(function(value, index) {
if(this.nodeType === 1) {
console.log(this)
}
});
});
});
var config = { attributes: true, childList: true, characterData: true };
observer.observe($('#myTextArea')[0], config);
observer.observe($('#myTextArea2')[0], config);
JSFiddle Link - demo
Note that I have recycled the same config for this first demo, but, placing a new config will be exclusive to that observed element. Taking your example as defined in config2, if used on #myTextArea2, you'll not see the logged node per the configuration options, but notice that the observer for #myTextArea is unaffected.
JSFiddle Link - demo - configuration exclusiveness