MutationObserver in MutationObserver Callback - javascript

I was adding a MutationObserver in my JavaScript code and applied it to a specific element in an iFrame. The detection works fine but now I need a second observer. After the first click in the iFrame the whole DOM of it changes and that's the moment where the first observer comes in and detects any other clicks on the navigation elements in the iFrame. Additionally to that I need to observe one div element and if it's text will be changed or not. So the idea was to create a second observer after a navigation button is clicked but it seems not to work since the second observer doesn't give any output on my console.
Here is the JavaScript code:
$('iframe').on('load', function() {
let observer = new MutationObserver(mutationRecords => {
let completed = false;
var progress = $('iframe').contents().find('.overview-sidebar__header .progress-bar__percentage-bottom').first();
let clickedClass = mutationRecords[0].target.attributes[0].nodeValue;
if(clickedClass == 'transition-group') {
console.log(progress.text());
console.log(Math.ceil(Date.now() / 1000));
} else if(clickedClass == 'page-wrap' && !mutationRecords[0].nextSibling) {
let secondObserver = new MutationObserver(mutationRecords => { console.log('Test') });
secondObserver .observe($('iframe').contents().find('.transition-group').first()[0], {
childList: true,
subtree: true
});
console.log(Math.ceil(Date.now() / 1000));
$('iframe').contents().find('.section-lists .lesson-lists__item').each(function(index) {
console.log(index);
console.log($(this).find('.lesson-link__name > span').first().text());
console.log($(this).find('.lesson-link__progress title').text().split('%')[0]);
});
}
});
observer.observe($('iframe').contents().find('.transition-group').first()[0], {
childList: true,
subtree: true
});
});
The secondObserver in the else if branch is the problem here. Anyone has an idea how to solve this?

Related

mutationobserver firing when no change and method not found issue

I've placed an observer on an element using MutationObserver. In a one use case it works exactly as expected and fires after a change but in another user action where the element has not changed it appears to be firing - but in doing so it comes up with a method not found error, which doesn't appear in the first use case using the same observer.
The observer watches for an update within an element which gets updated with an image as a user selects an image.
In the working case a user selects an image from a list of images, it then updates and the observer fires - all great.
In the non-working case a user uploads an image - at this point though no update has happened to the target element (which is in view but below a colorbox.(not sure if that's relevant).
The firing itself would not normally be a problem but within the observer callback it calls a method which in the second case it says is not defined.
So in the first instance there are no errors but in the second instance:
I get an error _this.buttons is not a function at MutationObserver.callback
The code is being compiled with webpack
1. Why is the observer firing when the doesn't appear to the type of change being observed?
Why does this error occur in this scenario - when the method appears to exist and works as expected when there is a change?
Any help appreciated
here's the code - this class manages the actions for a page - I've removed some code to try and keep it brief (but still a bit lengthy - refactoring to be done):
First, here's the code of the observer:
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
And here's the class in which the observer method is contained.
export let fileImageWidgetControls = class {
constructor({
previewBtn = '.preview-image',
addBtn = '#add-image',
replaceBtn = '#replace-image',
removeBtn = '#remove-image'
} = {}) {
this.options = {
previewBtn: previewBtn,
addBtn: addBtn,
replaceBtn: replaceBtn,
removeBtn: removeBtn
}
this.filemanager = new filemanagerHandler; //file selector class
this.imageWidget = new updateWidget; //handles updating the image
this.initialiseControls(); //sets up the page controls
this.observer(); //sets up the observer
}
openFileManager = () =>{
//open colbox (which opens filemanager page
//When filemanager loaded then initialise filemanager
$(document).bind('cbox_complete', ()=>{
console.log('Colbox complete');
this.filemanager.init();
});
//handle colbox closing and update image in widget (if needed)
$(document).bind('cbox_closed', ()=>{
let selectedAsset = this.filemanager.getSelectedAsset();
if(selectedAsset) {
this.imageWidget.update(selectedAsset.filename);
}
});
colBox.init({
href: this.options.serverURL
});
colBox.colorbox()
}
remove = ()=> {
//clear file and update visible buttons
this.buttons();
}
/**
* preview the image in a colorbox
* #param filename
*/
preview = function () {
//open image in preview
}
/**
* select image via filemanager
*/
select = () =>{
console.log('select');
this.openFileManager();
}
replace = () => {
// image already exists in widget but needs replacing
console.log('replace');
this.openFileManager();
}
initialiseControls = () => {
console.log('init controls');
//preview button
$(this.options.previewBtn).on('click', (e) => {
e.preventDefault();
this.preview();
}).attr('disabled', false);
$('#img-preview-link').on('click', (e)=> {
e.preventDefault();
this.preview();
});
// add button
$(this.options.addBtn).on('click', (e) => {
e.preventDefault();
this.select();
}).attr('disabled', false);
//replace button
$(this.options.replaceBtn).on('click', (e) => {
e.preventDefault();
this.replace();
}).attr('disabled', false);
//remove button
$(this.options.removeBtn).on('click', (e) => {
e.preventDefault();
this.remove();
}).attr('disabled', false);
this.buttons();
}
//set an observer to watch preview image for changes
observer= ()=> {
const module = this;
const targetNode = document.getElementById('image-preview-panel');
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationsList, observer) =>{
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
module.buttons();
module.initialiseControls();
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
buttons = function() {
let imagePreview = $('#image-preview');
if(imagePreview.data('updated')=== true && imagePreview.data('updated') !== "false") {
console.log('image present');
$(this.options.addBtn).fadeOut().attr('disabled', true);
$(this.options.removeBtn).fadeIn().attr('disabled', false);
$(this.options.replaceBtn).fadeIn().attr('disabled', false);
$(this.options.previewBtn).fadeIn().attr('disabled', false);
} else {
console.log('image not present', imagePreview.data());
console.log('image element:', imagePreview);
$(this.options.addBtn).fadeIn().attr('disabled', false);
$(this.options.removeBtn).fadeOut().attr('disabled', true);
$(this.options.replaceBtn).fadeOut().attr('disabled', true);
$(this.options.previewBtn).fadeOut().attr('disabled', true);
}
}
}
I copied code from a tutorial hence some of the comments until I refactor
Added const module = this; within the method and referenced that within the nested function and now pointing to the correctthis`

MutationObserver not firing when setting display to none

I have a node called 'cover' that gets set to visible or not visible when the ajax layer wants to hide/show it based on it being our veil. But we want our veil to be more than just a single node going visible or invisible. So I wrote a MutationObserver to watch for the change and do the extra work. This works fine when the node gets changed to display: block. It does NOT fire when it changes to display: none.
You can see the observer below, and between this and breakpoints, I am confident that it is never called on display:none changes. And yes, I can see that change has been made in the watch list. This happens in both IE and Chrome.
Is this expected? I didn't expect it. But if so, how can I get that display:none event?
The call to start the observer:
veilObserver.observe(cover, { attributes: true, childList: false, subtree: false });
The observer:
const veilObserver = new MutationObserver(function(mutationsList, observer) {
console.log("MutationObserver enter");
var cover = document.getElementById('cover');
if(cover) {
console.log("MutationObserver cover");
if(cover.style.display == 'none') {
console.log("MutationObserver closing");
closeVeil();
} else if(cover.style.display == 'block') {
openVeil();
} else {
//this should never happen, but if it does, we want to make sure the veil is closed because we don't know whether it should be open or
//closed and I'd rather default to open so the user isn't locked forever.
console.log('Mutation!!! but display not recognized: ' + cover.style.display);
closeVeil();
}
} else {
console.log("MutationObserver disconnecting");
//this implies the page lacks the required HTML. Disconnect the observer and don't both them again.
veilObserver.disconnect();
}
});
If your having trouble determining which parent to observe for attribute changes, you can observe all attribute changes on the document, filter irrelevant changes as much as possible, and then check if your element is visible.
var observer;
observer = new MutationObserver(function(mutations) {
let cover = document.getElementById('cover')
if (!cover) observer.disconnect()
if (!isVisible(cover)) console.log("closing");
else console.log("opening")
});
// main diffrence is the target node to listen to is now document.body
observer.observe(document.body, {
attributes: true,
attributeFilter: ['style'],
subtree: true
});
function isVisible(element) { return (element.offsetWidth > 0 && element.offsetHeight > 0) }
Copied this from the docs and tailored it to your code.
You should try observing the parent element of #cover. That way any mutations inside of that element will be observed.
// Select the node that will be observed for mutations
const targetNode = document.getElementById(/* The parent element of #cover */);
// Any changes inside this div will be observed.
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
// Check if the element that changed was #cover
console.log(mutation.target.id === 'cover');
if(mutation.target.id === 'cover') {
let id = mutation.target.id;
if(document.getElementById(id).style.display === 'none') {
// Do something
// disconnect perhaps.
}
}
}
else if (mutation.type === 'attributes') {
// If the style is inline this may work too.
console.log('The ' + mutation.attributeName + ' attribute was modified.');
console.log(mutation.attributeName === 'style');
let id = mutation.target.id;
if(document.getElementById(id).style.display === 'none') {
// Do something
// disconnect perhaps.
}
}
}
};
// 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();
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

Automatically clicking button when it appears rather than it having to be there already

Right now whenever I use that code on Chrome Console, it'll click the button as long as it's already there but what I'm trying to get it to do is to do the same thing when the button doesn't exist but exists at a random time.
(function (){
document.getElementsByClassName("buttonContent-18Mz6_")[0].click();
})();
To have a performant solution and a modern one, you can use MutationObserver for this task :
/**
* #const target the target element to watch for new added elements.
* #const observer the mutation observer.
**/
const target = document.body,
observer = new MutationObserver((mutationsList, observer) => {
/** loop through the mutations **/
for (let mutation of mutationsList) {
/** wer're watching only for the changes on the child list of the target **/
/** see if the added (also that may include notifications about removed child nodes) child is the wanted button **/
const btn = target.querySelector('button.buttonContent-18Mz6_');
/** if yes just click it and disconnect the observer **/
btn && (btn.click, observer.disconnect());
/** exit the callback **/
if(btn) return;
}
});
/** start the observer **/
observer.observe(target, {
childList: true,
/** we only watch for the child list changes only **/
attributes: false,
subtree: false
});
/** for testing, add that button after 5 seconds to see the result **/
window.setTimeout(() => {
const btn = document.createElement('button');
btn.className = 'buttonContent-18Mz6_';
btn.textContent = 'button'
btn.addEventListener('click', console.log('button clicked !'));
target.appendChild(btn);
}, 5000);
buttonContent-18Mz6_
<div id="mutation-target">
<p>wait for about 5 seconds...</p>
</div>
If you're expecting your button to appear at some point in the future, you may employ setInterval in order to attempt to click the button until it finally appears like this:
(function (){
const tryClickingElement = () => {
const element = document.getElementsByClassName("buttonContent-18Mz6_")[0];
if(element) {
element.click();
clearInterval(myInterval);
}
}
const myInterval = setInterval(tryClickingElement, 1000);
})();

Chrome Extension: Waiting For Element to Load (async js)

I have a Chrome extension, and I want to wait until an element is loaded before injecting content into the page.
I'm trying to inject a button:
myButton = document.createElement('button');
myButton.class = 'mybutton';
document.querySelector('.element_id').appendChild(myButton)
I have this at the top of my content script. It used to work just fine, but then it stopped working. The error that was displayed was:
Uncaught TypeError: Cannot read property 'appendChild' of null
In order to wait for the element with class id .element_id to load, I tried to use a MutationObserver
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (!mutation.addedNodes) return
for (var i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].parentNode == document.querySelector('#outer-container')) {
myButton = document.createElement('button');
myButton.class = 'mybutton';
document.querySelector('.element_id').appendChild(myButton)
}
var node = mutation.addedNodes[i]
}
})
})
observer.observe(document.body, {
childList: true
, subtree: true
, attributes: false
, characterData: false
})
When I used the mutation observer, the page would load an outer div element called outer-container, and there was no way for me to directly compare the class .element_id. The class .element_id is nested a number of layers into the outer div.
HOWEVER, the above did not work, and I still received the null property error.
Is there a better way to wait for some element to be loaded (which is loaded async), before injecting?
Don't forget to add childList and subtree property when observing changes.
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (!mutation.addedNodes) {
return;
}
for (var i = 0; i < mutation.addedNodes.length; i++) {
if (mutation.addedNodes[i].classList.contains("element_id")) {
// Your logic here
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
An insertion into DOM may have the element in question deeper in the added node.
For example, this can be inserted into the DOM:
<div class="container">
<div class="element_id">...</div>
...
</div>
In that case, the added node list will only contain the .container node.
The mutation will not list everything added, it's your responsibility to recursively dig into the added fragment looking through added nodes.
Using mutation-summary library may help you avoid such headaches.
var observer = new MutationSummary({
rootNode: document.body,
callback: function(summaries) {
summaries.forEach(function(summary) {
summary.added.forEach(function(idElement) {
/* ... */
idElement.appendChild(myButton);
});
});
},
queries: [{element: ".element_id"}]
});
If you don't want to use a library, you can try calling querySelector or querySelectorAll on addedNodes[i].

Detect element style change in chrome

I'm trying to find a way to detect changes to the element style but I haven't had much luck. The code below works on a new property I define like tempBgColor but I cannot override/shadow an existing property like color. I know jquery has a watch function, but it only detects changes from the jquery api but not directly changing the value of a style something like elem.style.color.
var e = document.getElementById('element');
e.style.__defineGetter__("color", function() {
return "A property";
});
e.style.__defineSetter__("color", function(val) {
alert("Setting " + val + "!");
});
Any pointers?
You should be able to do this with a MutationObserver - see demo (Webkit only), which is the new, shiny way of getting notified about changes in the DOM. The older, now deprecated, way was Mutation events.
Demo simply logs in the console the old and new values when the paragraph is clicked. Note that the old value will not be available if it was set via a non-inline CSS rule, but the change will still be detected.
HTML
<p id="observable" style="color: red">Lorem ipsum</p>​
JavaScript
var MutationObserver = window.WebKitMutationObserver;
var target = document.querySelector('#observable');
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('old', mutation.oldValue);
console.log('new', mutation.target.style.cssText);
});
});
var config = { attributes: true, attributeOldValue: true }
observer.observe(target, config);
// click event to change colour of the thing we are observing
target.addEventListener('click', function(ev) {
observable.style.color = 'green';
return false;
}, false);
Credit to this blog post, for some of the code above.
With Chrome's Developer Tools open, you can find the element whose style's change you're interested in, right click it, select "Break on..." and "Attributes modifications".
here is a naive implementation using setTimeout with undescorejs.
The only way to find out which change was made is to iterate through the style object properties.
Here is the live example
$( function () {
var ele = document.getElementById('ele'),
oldStyle = {};
function checkEquality() {
style = _.clone(ele.style);
if (!_.isEqual(style, oldStyle)) {
console.log('Not equal');
oldStyle = _.clone(style);
} else {
console.log('Equal');
}
_.delay(checkEquality, 2000);
}
checkEquality();
$('a#add_prop').on('click', function () {
var props = $('#prop').val().replace(/ /g, '').split(':');
console.log(props);
$(ele).css(props[0], props[1]);
});
$('#prop').on('keydown', function (e) {
if (e.keyCode == 13) {
$('a#add_prop').trigger('click');
}
});
});

Categories

Resources