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].
Related
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?
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
I'd like to hide an element that is inserted/injected to my Shopify store with an external app. It appears about a second later after everything has finished loading on the site and has a class called "hidethis" and a bunch of other elements.
This did not work and I have no idea what else to try.
$(".hidethis").hide();
I'm trying to hide this element based on the location of the user in the following manner:
jQuery.ajax( {
url: '//api.ipstack.com/check?access_key=xxx&fields=country_code',
type: 'POST',
dataType: 'jsonp',
success: function(location) {
if (location.country_code === 'EE') {
$(function() {
// if geolocation suggest you need to hide, execute this as soon as possible
var sheet = window.document.styleSheets[0];
sheet.insertRule('.cart__options { display:none; }', sheet.cssRules.length);
})
}
}
} );
Best solution: CSS
.hidethis { display:none }
If this is not possible and you need JS
var sheet = window.document.styleSheets[0];
sheet.insertRule('.hidethis { display:none; }', sheet.cssRules.length);
$(function() {
// if geolocation suggest you need to hide, execute this as soon as possible
var sheet = window.document.styleSheets[0];
sheet.insertRule('.hidethis { display:none; }', sheet.cssRules.length);
// test code - remove this when you insert the above code in your page
setTimeout(function() {$("#container").append('<div class="hidethis">Hide this</div>');}, 1000);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container"></div>
which translates to your Ajax example:
$.ajax({
url: '//api.ipstack.com/check?access_key=xxx&fields=country_code',
type: 'POST',
dataType: 'jsonp',
success: function(location) {
if (location.country_code === 'EE') {
var sheet = window.document.styleSheets[0];
sheet.insertRule('.hidethis { display:none; }', sheet.cssRules.length);
}
}
})
Alternatively add a
<style>.hidethis { display:none }</style>
to the page before where the content you want to hide is going to appear. Then in your ajax do
if (location.country_code != 'EE') { $(".hidethis").show() }
You can also try an interval
$(function() {
var tId = setInterval(function() {
var $hide = $(".hidethis");
if ($hide.length>0) {
clearInterval(tId);
$hide.hide();
}
},500);
// test code - remove this when you insert the script in your page
setTimeout(function() { $("#container").append('<div class="hidethis">Hide this</div>'); },1000);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container"></div>
Here an example how to add events:
https://stackoverflow.com/a/48745137/155077
functional equivalent to jQuery .on.
Instead of adding an event-handler, you'll just have to hide it.
subscribeEvent(".feed", "click", ".feed-item", function (event) { /* here comes code of click-event*/ });
The whole thing works with MutationObserver:
// Options for the observer (which mutations to observe)
let config = { attributes: false, childList: true, subtree: true };
// Create an observer instance linked to the callback function
let observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(nodeToObserve, config);
where callback:
// Callback function to execute when mutations are observed
let callback:MutationCallback = function (
mutationsList: MutationRecord[],
observer: MutationObserver)
{
for (let mutation of mutationsList)
{
// console.log("mutation.type", mutation.type);
// console.log("mutation", mutation);
if (mutation.type == 'childList')
{
for (let i = 0; i < mutation.addedNodes.length; ++i)
{
let thisNode: Node = mutation.addedNodes[i];
allDescendants(thisNode); // here you do something with it
} // Next i
} // End if (mutation.type == 'childList')
// else if (mutation.type == 'attributes') { console.log('The ' + mutation.attributeName + ' attribute was modified.');
} // Next mutation
}; // End Function callback
Your problem isn't actually about an element being added by an external app, the problem is that when your code to hide the element is executed the element isn't on the DOM yet. Because the element is being added sometime later after all your JavaScript code was already executed.
So, you have to execute your code after the element is added. One way to do that is by using MutationObserver.
Here is a simple example using as referece the example in MDN:
<div id="some-id"></div>
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
document.querySelector('.hide').style.display = 'none';
}
}
};
// 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);
// Add a timeout to simulate JavaScript being executed after all your code was executed.
setTimeout(() => {
document.getElementById('some-id').innerHTML = '<span class="hide">Hello world</span>';
}, 1000);
1 : at first , you inspect in your browser and find the element
2 : use $(document).ready() and hide that element
I need to prevent DOM-Change using mutationobserver.
I had (past) the following code to prevent specific changes:
document.bind("DOMSubtreeModified", function() {
document.find('.Xx.xJ:Contains("wham")').closest("[jsmodel='XNmfOc']").hide();
});
Because of performance reasons I did not want to check the complete document on any dom-change but only added contents so I changed to this (now):
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
[].slice.call(mutation.addedNodes).forEach(function (addedNode) {
StartFilter(addedNode);
});
});
});
observer.observe(document, {
childList: true,
subtree:true,
characterData:true,
attributes:true
});
function StartFilter(newNode) {
$(newNode).find('.Xx.xJ:Contains("wham")').closest("[jsmodel='XNmfOc']").hide();
}
But this does not really work. My guess is that "newNode" is not really a reference to the DOM-Element. (The selector is valid, "$(newNode).find('.Xx.xJ:Contains("wham")').closest("[jsmodel='XNmfOc']")" returns an element).
I did not find any method/property to reject a dom-change in MutationObserver. Is there a way to achieve what I want WITHOUT checking the whole document each time?
It's unclear to me whether the NodeList returned by a mutation observer is "live", in the sense that changes to nodes in that list are immediately be reflected on the DOM. But that doesn't matter, since you're only using it to create jQuery wrapped set. The basic code you've got above works as intended (see simplified snippet below), which implies that there's something else preventing your hide() call from working as expected.
My best understanding is that you can't intercept and prevent changes to the DOM--the MutationObserver is fired after the associated mutation has already occurred. That means you're not interrupting or intercepting the mutation, but rather reacting to it. In your case, that could lead to unexpected "flashing" behavior as nodes are added and then removed. A better solution in that case would be to style newly-added nodes to be hidden by default, and then add a class/style to either display them or remove them from the DOM in the mutation observer filter.
var container = document.querySelector('.container');
var addNodeButton = document.querySelector('#add');
var addNodeWithHideButton = document.querySelector('#addWithHide');
var makeAddNode = function(includeHide) {
return function() {
var p = document.createElement('p');
var s = document.createElement('span');
var msg = 'Appended at ' + new Date().getTime();
if (includeHide) {
msg += ' (hide)';
}
var t = document.createTextNode(msg);
s.appendChild(t);
p.appendChild(s);
container.appendChild(p);
console.log('appended::', p);
};
};
var makeNode = makeAddNode(false);
var makeNodeWithHidden = makeAddNode(true);
addNodeButton.addEventListener('click', makeNode);
addNodeWithHideButton.addEventListener('click', makeNodeWithHidden);
var toArray = function() {
return [].slice.call(arguments);
};
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
toArray(mutation.addedNodes).forEach(function (addedNode) {
StartFilter(addedNode);
});
});
});
observer.observe(document, {
childList: true,
subtree:true,
characterData:true,
attributes:true
});
function StartFilter(newNode) {
var $n = $(newNode);
console.log('$n::', $n);
$n.find('span:contains(hide)').fadeOut(1500);
// $(newNode).find('.Xx.xJ:Contains("wham")').closest("[jsmodel='XNmfOc']").hide();
}
.container {
width: 80%;
border: 1px solid green;
padding: 1rem;
margin: 1rem auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<button id="add">Add node without "hide"</button>
<button id="addWithHide">Add node with "hide"</button>
<div class="container"></div>
Is there a way to have a listener for when a div element is empty?
$('#myDiv').emptyEvent(function(){
)};
You should run David's code inside an event handler, such as DOMNodeInserted, DOMCharacterDataModified, or DOMSubtreeModified. The latter being the most recommended. For example:
$('#myDiv').bind("DOMSubtreeModified", function(){
if ( $('#myDiv').html() == "" ) {
}
)};
Edit: Such implementation is however deprecated, as stated in the comments. An alternative implementation, as suggested by david, is the following:
// select the target node
var target = $("#myDiv")[0];
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if($("#myDiv").html() == ""){
// Do something.
}
});
});
// 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);