MutationRecord seems incomplete at the time it is observed? - javascript

I'm using a MutationObserver to notice when a certain element is added to the page. The way I do this is by observing the document and iterating through each MutationRecord's addedNodes array and querying for a certain selector:
added_node.querySelectorAll("tr[data-testid|='issue-table--row']")
This does work, but I do not get the results I expect. For example, on a particular page I should see a parentNode being added that has 18 of the tr html element somewhere in the tree.
So I created a script to debug this. What you see below, does output how many tr elements are found in the added Nodes. As well as each MutationRecord it inspects.
Oddly enough, when searchRecord() is invoked automatically during the scripts runtime, I don't see the expected result.
But after manually reviewing all MutationRecords that were printed to the debug logs, I could confirm that one of them indeed has the data I am looking for.
For example, this manual line in the debug console does return what I expect:
temp0[1].addedNodes[2].querySelectorAll("tr[data-testid|='issue-table--row']")
(temp0 being a MutationRecord the MutationObserver observed.)
Typing this into the debug console also yields the expected results:
searchRecord(temp0)
But when the same line is invoked by the script via the callback searchRecord(mutationRecords) then for some dubious reason it never returns the expected result.
What's happening? Is the MutationRecord incomplete at the time it is observed??
function searchRecord(mutationRecords) {
for (const r of mutationRecords) {
/* TARGET tests */
if (r.target instanceof HTMLElement) {
if (r.target.attributes["data-testid"] === "issue-table--body") {
console.debug("Target is 'issue-table--body'")
}
if (r.target.attributes["data-testid"] === "issue-table--row") {
console.debug("Target is 'issue-table--row'")
}
}
/* ADDEDNODES tests */
for (const node of r.addedNodes) {
if (node instanceof HTMLElement) {
/* direct */
if (node.attributes["data-testid"] === "issue-table--body") {
console.debug("Added node is 'issue-table--body'")
console.debug(node)
}
if (node.attributes["data-testid"] === "issue-table--row") {
console.debug("Added node is 'issue-table--row'")
console.debug(node)
}
/* nested */
tbodies = node.querySelectorAll("tbody[data-testid|='issue-table--body']")
if (tbodies.length > 0) {
console.debug(`Added node contains ${tbodies.length} 'issue-table--body'`)
console.debug(node)
}
trows = node.querySelectorAll("tr[data-testid|='issue-table--row']")
if (trows.length > 0) {
console.debug(`Added node contains ${trows.length} 'issue-table--row'`)
console.debug(node)
}
}
}
/* REMOVEDNODES tests */
for (const node of r.removedNodes) {
if (node instanceof HTMLElement) {
/* direct */
if (node.attributes["data-testid"] === "issue-table--body") {
console.debug("Removed node is 'issue-table--body'")
}
if (node.attributes["data-testid"] === "issue-table--row") {
console.debug("Removed node is 'issue-table--row'")
}
/* nested */
tbodies = node.querySelectorAll("tbody[data-testid|='issue-table--body']")
if (tbodies.length > 0) {
console.debug(`Removed node contains ${tbodies.length} 'issue-table--body'`)
}
trows = node.querySelectorAll("tr[data-testid|='issue-table--row']")
if (trows.length > 0) {
console.debug(`Removed node contains ${trows.length} 'issue-table--row'`)
}
}
}
}
}
new MutationObserver(function callback(mutationRecords) {
console.debug("-----------------------------------------------")
console.debug("Mutation observed. Logging mutation records ...")
console.debug(mutationRecords)
searchRecord(mutationRecords)
}).observe(document, {
attributes: false,
childList: true,
subtree: true,
})

Let's simplify the situation to make it easier to talk about: You want to watch for div elements with class example (div.example) being added and see how many span elements with class x (span.x) there are in the div.example that was added.
The nodes you receive are the actual nodes in the DOM document that were added. They will be as fully-populated as they are as of when your observer was called, which will be at some point after the element has been added to the container (because of JavaScript's run-to-completion semantics — the code adding the elements has to finish before any callbacks that triggers, such as your mutation observer, can be run). That means that:
If a div.example is added that contains (say) three span.x elements, your code will see the three span.x elements in the div when your observer callback is called, since they're already there.
If a div.example is added, then just afterward three span.x elements are added to it without yielding to the event loop (that is, without waiting for some asynchronous operation like ajax or a setTimeout), your code will still see those three span.x elements in the div.example when the observer callback is called, because they're there by the time it runs, even though they weren't when the div.example was first added.
Variation: If the div.example is added with span elements in it that don't have the x class yet, but then the x class is added afterward without yielding to the event loop, your code will see span.x elements, because the class will be there by the time it runs.
If a div.example is added, and then the span.x elements are added to it later after the code adding things has yielded to the event loop by waiting for some asynchronous operation, your code may not see the span.x elements in the div.example, since it may run before they're there.
Variation: If the div.example is added with span elements in it that don't have the x class yet, but then later after the code adding things has yielded to the event loop it adds the x class to the span elements, your code may not see span.x elements in the div.example, because although the span elements are there, they don't have the x class you're looking for yet, because your code ran before the class was added.
Here's an example of all three scenarios:
const observer = new MutationObserver((records) => {
for (const record of records) {
if (record.addedNodes) {
for (const node of record.addedNodes) {
if (node.nodeName === "DIV" && node.classList.contains("example")) {
const {
length
} = node.querySelectorAll("span.x");
console.log(`div.example "${node.id}" added, ${length} span.x elements found in it`);
}
if (node.querySelectorAll("span").length > 0) {
node.style.backgroundColor = "lightgreen"
}
}
}
}
});
const container = document.getElementById("container");
observer.observe(container, {
childList: true,
subtree: true,
});
function createDiv(id) {
const div = document.createElement("div");
div.id = id;
div.classList.add("example");
return div;
}
function addSpan(div, text) {
div.insertAdjacentHTML("beforeend", `<span class=x>${text}</span>`);
}
setTimeout(() => {
let div;
// Adding a fully-set-up div
div = createDiv("A: three spans");
addSpan(div, "1");
addSpan(div, "2");
addSpan(div, "3");
console.log("A: spans added");
container.appendChild(div);
// Adding a partially-set-up div, then adding to it after, but without
// yielding to the event loop
div = createDiv("B: three spans, added after (no yield)");
container.appendChild(div);
addSpan(div, "1");
addSpan(div, "2");
addSpan(div, "3");
console.log("B: spans added");
// Adding a partially-set-up div, then adding to it after yielding to
// the event loop
div = createDiv("C: three spans, added after (yield)");
container.appendChild(div);
setTimeout(() => {
addSpan(div, "1");
addSpan(div, "2");
addSpan(div, "3");
console.log("C: spans added");
}, 0);
}, 100);
.as-console-wrapper {
max-height: 70% !important;
}
<div id="container"></div>
So if you're seeing the element without the descendants you expect to see, it would appear you're running into scenario #3 above: The descendants are being added to it (or the charateristics of them you're looking for are set) after a delay.
The crucial point is: The elements you get in the observer callback are the actual elements in the DOM, so they'll have the contents and characteristics those elements have as of when you look at them. They aren't copies or placeholders or representative examples.

Related

why only inside of the for loop can I successfully find <li> or <span> tag under the <ul> with the getElementsByClassName method in JavaScript?

I am learning blockchain with truffle by trying to build a simple TODO LIST project.
At first, I could not successfully disable the display of task items in my todo list when clicking on X symbol.... now I accidently find out the solution but just can't figure out why and how it works....
my solution is as below codes( especially in the bottom, start from the comment //when click to X, list disappear)
my problems are :
why can't I find the tags of li, span, close outside of this for loop (the return of console.log was null) with getElementsByClassName method while finding ul tag is possible ??
Even I can only find those tags within for loop, why this for loop always running and waiting for me to click X?? Isn't the for loop supposed to be terminated after reiterating through the number of times we asked them to reiterate??!!
render: function() {
//Load account data
if(web3.currentProvider.enable){
//For metamask
web3.currentProvider.enable().then(function(acc){
App.account = acc[0];
$("#accountAddress").html("Your Account: " + App.account);
});
}
else{
App.account = web3.eth.accounts[0];
$("#accountAddress").html("Your Account: " + App.account);
}
// Load contract data
App.contracts.myTodoList.deployed().then(function(instance) {
myTodoListInstance = instance;
return myTodoListInstance.TasksCount();
}).then(function(TasksCount){
var shownList = document.getElementById("todo-list")
TasksCount = TasksCount.toNumber();
for (var i = 1; i <= TasksCount; i++) {
myTodoListInstance.Tasks(i).then(function(task) {
var id = task[0];
var content = task[1];
let completed = task[2];
// create li node inside of for loop so that you won't appendChild the same 'li' over and over again
var newTaskNode = document.createElement('li');
newTaskNode.innerHTML = content;
// Add a "X" symbol in the end of each listed task
var span = document.createElement("SPAN");
var txt = document.createTextNode("x");
span.className = "close";
span.appendChild(txt);
newTaskNode.appendChild(span);
// Render initial tasks Result
shownList.appendChild(newTaskNode);
// when click to X, list disappear
//(don't know how it worked... why can't i locate 'li','span','close' outside of this for loop ??)
//(even i can only locate those tags within this loop, why this for loop always running and waiting for me to click X ??!!)
var close = document.getElementsByClassName("close");
var i;
for (i = 0; i < close.length; i++) {
close[i].onclick = function() {
var div = this.parentElement;
div.style.display = "none";
}
}
});
}
});
},
Can only access created elements in the for loop.
The li elements can't be accessed immediately after calling the render function because they haven't been created yet. List element creation requires promises for
App.contracts.myTodoList.deployed() and
myTodoListInstance.Tasks(i)
to be resolved first.
Why this for loop always running and waiting for me to click X?
[Edit: the previous version of this answer incorrectly implied that repeatedly setting the onclick property of an element set up multiple click handlers. Setting onevent attributes of an HTML element replaces an event handler added the same way.]
You may need to explain the symptoms of this issue in more detail. The for loop actually finishes iterating before any close and list elements are created and added to the DOM - the loop is only adding a then clause to each of the myTodoListInstance.Tasks(i) promises and doesn't wait for the handler to be called.
The code to add click handlers can be simplified (to only add handlers once) by replacing
var close = document.getElementsByClassName("close");
var i;
for (i = 0; i < close.length; i++) {
close[i].onclick = function() {
var div = this.parentElement;
div.style.display = "none";
}
}
with
span.addEventListener( "click", function() {
var div = this.parentElement;
div.style.display = "none";
});
when click to X, list disappear
I assume this is not a problem because the click handler is coded to set the display property of the ul parent of an li element to "none". Note the parent is probably not a div element as suggested by the variable name.
Other
Only set newTaskNode.innerHTML to content; if content is formatted using HTML markup and known to be safe. If content is plain text, set newTaskNode.textContent instead.
As presented,
there is no error handling for promise rejection,
no array of promises that must be fulfilled before all list items and close buttons are ready has been created. Such an array can be passed to Promise.all to create a single promise for when the list have been completed (and gets rejected if any promise in the array gets rejected)
Promise's get resolved asynchronously. There is nothing in the code that guarantees that list items presented on screen are in order of ascending i values. It may be better to create the list items and close buttons in a Promise.all handler which is passed an array of resolved values in the same order as its promise array argument.

Maximum call stack size exceeded when I am modifying words in the webpage through chrome extension's content script

I am building a Chrome Extension that look for a word , and if that word is present on the web page it gets blurred. To achieve this I look for all the text (node type 1) nodes on the web page and replace them with a new node. Problem occurs when I create a new node and assign it the text of the node to be replaced, this script when run gives error "RangeError: Maximum call stack size exceeded"
This problem doesn't occur when I assign a constant string to the node to be created. And this script runs fine.
var targetNode=document.body
var config = { attributes: true, childList: true, subtree: true };
var callback = function(mutationsList, observer) {
walk(document.body);
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
function walk(node)
{
var child, next;
switch ( node.nodeType )
{
case 1: // Element
case 9: // Document
case 11: // Document fragment
child = node.firstChild;
while ( child )
{
next = child.nextSibling;
walk(child);
child = next;
}
break;
case 3: // Text node
handleText(node);
break;
}
}
function handleText(textNode)
{
var str = textNode.nodeValue;
if (str == "Manchester"){
//console.log(str);
p=textNode.parentNode;
const modified = document.createElement('span');
modified.id="bblur";
modified.textContent = "Constant"; // this works
modified.style.filter="blur(5px)";
modified.addEventListener("mouseover", mouseOver, false);
modified.addEventListener("mouseout", mouseOut, false);
p.replaceChild(modified, textNode);
}
//textNode.nodeValue = str;
//textNode.style.filter="blur(5px)";
}
function mouseOver()
{
this.style.filter="blur(0px)";
}
function mouseOut()
{
this.style.filter="blur(5px)";
}
This handleText function doesn't work
function handleText(textNode)
{
var str = textNode.nodeValue;
if (str == "Manchester"){
//console.log(str);
p=textNode.parentNode;
const modified = document.createElement('span');
modified.id="bblur";
modified.textContent = str; //this doesn't work :/
modified.style.filter="blur(5px)";
modified.addEventListener("mouseover", mouseOver, false);
modified.addEventListener("mouseout", mouseOut, false);
p.replaceChild(modified, textNode);
}
}
I don't want new node to be created with a fixed string but i want the text content of old node in the new one. What I can do to avoid this call stack limit reached problem. Thanks!
Infinite loop is caused because you modify DOM from MutationObserver callback. It works with "Constant" because you have condition "if (str == "Manchester")" which prevents DOM modification and does not trigger MutationObserver callback. Try using constant "Manchester" and you will see infinite loop again.
Easiest fix would be to ignore nodes which you already replaced:
function walk(node)
{
var child, next;
switch ( node.nodeType )
{
case 1: // Element
case 9: // Document
case 11: // Document fragment
child = node.firstChild;
while ( child )
{
next = child.nextSibling;
if (child.id !== 'bblur') {
walk(child);
}
child = next;
}
break;
case 3: // Text node
handleText(node);
break;
}
}
Also your code will assign same id to all replaced elements. It would be better to use different way to mark new nodes, for example you can use dataset attributes.
As mentioned, infinite loop, your modifying the DOM, which triggers the callback, which modifies the DOM etc.
Maybe run the code first.
Register your listener,
When the listener fires, remove the listener.
Once changes have been made, register the listener again.
That way your code won’t trigger itself and will only listen to changes when it’s idle.
Think of it like a holding page on an e-commerce website. If your moving the database but still taking orders it’s gonna get messy. So you disable taking new orders whilst your doing the processing and enable them once your done. Same logic here

MutationObserver detecting element appereance and changing of element's value

I intend to use MutationObserver on observing the appearance and changing of element's value, but to be honest I'm not sure how this should be implemented.
The target of MO would be div.player-bar and what I'm trying to accomplish is to detect when el-badge__content appears in page and when el-badge__content element value is changed (for example instead 1 would change to 2).
Please note that el-badge__content appears at the same time with the creation of div.new-bar and many times div.new-bar would not be present in the page, that's why I need to listen to div.player-bar.
Is this possible? So far I was thinking of something like this:
var target = document.getElementsByClassName('player-bar')[0];
var config = { attributes: true, childList: true, subtree: true };
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.forEach(function(addedNode) {
var e = addedNode.document.getElementsByClassName('el-badge__content')[0];
if (e) {
console.log("Element appearance/changed")
};
});
});
});
observer.observe(target, config);
Thank you in advance.
mutation is a MutationRecord object that contains the array-like addedNodes NodeList collection that you missed in your code, but it's not an array so it doesn't have forEach. You can use ES6 for-of enumeration in modern browsers or a plain for loop or invoke forEach.call.
A much easier solution for this particular case is to use the dynamically updated live collection returned by getElementsByClassName since it's superfast, usually much faster than enumeration of all the mutation records and all their added nodes within.
const target = document.querySelector('.player-bar');
// this is a live collection - when the node is added the [0] element will be defined
const badges = target.getElementsByClassName('el-badge__content');
let prevBadge, prevBadgeText;
const mo = new MutationObserver(() => {
const badge = badges[0];
if (badge && (
// the element was added/replaced entirely
badge !== prevBadge ||
// or just its internal text node
badge.textContent !== prevBadgeText
)) {
prevBadge = badge;
prevBadgeText = badge.textContent;
doSomething();
}
});
mo.observe(target, {subtree: true, childList: true});
function doSomething() {
const badge = badges[0];
console.log(badge, badge.textContent);
}
As you can see the second observer is added on the badge element itself. When the badge element is removed, the observer will be automatically removed by the garbage collector.

How to know if a DOM element mounted to tree

I am pretty new to DOM, I wonder if I refer a DOM element which may be removed from DOM tree, how can I check if it is still mounted or not?
Like:
var div = document.createElement("DIV");
document.body.appendChild(div);
Then later I probably select <div> element and remove it, but this operation does only unmount those from the tree, the div variable still points to that DOM element.
I wonder if there is a function to test if the div is still on the page (mounted on the DOM tree) or not?
You can probably try this one
document.body.contains(yourDiv)
contains method will return true or false
if a node is part of the document, its baseURI property will be a string URL, otherwise, it will be null
var div = document.createElement("DIV"),
u1=div.baseURI, u2, u3; //first url, un-attached
document.body.appendChild(div);
u2=div.baseURI; //second url, attached
div.remove();
u3=div.baseURI; //third url, detached
alert(JSON.stringify([u1,u2,u3], null, 2));
run on this page in devtools shows:
[
null,
"http://stackoverflow.com/questions/34640316/how-to-know-if-a-dom-element-mounted-to-tree",
null
]
this means that to determine if a node is attached, you can simply ask for elm.baseURI:
if(div.baseURI){
alert('attached')
}else{
alert('not attached');
}
According to the improved version of Mr Lister.
function isMounted(node) {
if (node.nodeType === Node.DOCUMENT_NODE) return true;
if (node.parentNode == undefined) return false;
return isMounted(node.parentNode);
}
You can use Node.isConnected
div.isConnected
Note: This will not work with old browsers (Internet Explorer, Edge < 79, Safari < 10)
Володимир Яременко's answer is correct, but as an alternative method, you can check if the div has a parent node.
if (theDiv.parentNode==null) {
// Not in the DOM tree
}
else {
// in the DOM tree!
}
This will be null before appending it to the body, and again after removing it from the body.

MutationObservers - Some nodes added are not detected

I have a content script which listens for the insertion of text-nodes on some websites. It's working great, except on Facebook. Some of the text-nodes inserted are not detected by the script.
script.js
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "characterData") {
console.log(mutation.target);
} else {
for (var x = 0; x < mutation.addedNodes.length; x++) {
var node = mutation.addedNodes[x];
if (node.nodeType === Node.TEXT_NODE) {
console.log(node);
}
}
}
});
});
observer.observe(document, { childList: true, subtree: true, characterData: true });
If I allow logging of all node types, I can see the parent nodes of these text nodes in my log.
Thanks.
This is probably the same issue as this question. If a script "builds" elements outside of the DOM and then attaches the root, only the root will be reported as an added node. TreeWalker can be used to traverse the added node's subtree to find the text.
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === Node.TEXT_NODE) {
console.log(addedNode);
} else {
// If added node is not a Text node, traverse subtree to find Text nodes
const nodes = document.createTreeWalker(addedNode, NodeFilter.SHOW_TEXT);
while (nodes.nextNode()) {
console.log(nodes.currentNode);
}
}
}
Facebook has a way of detecting even when the debugger is opened on the website. I am guessing they have a way to detect mutation observers as well. They might just be blocking your content scripts. Besides I wouldn't really use the whole document as my object. Just try to monitor what you want using document.querySelector.

Categories