MutationObserver for only the last mutation - javascript

I'm using the MutationObserver to save position changes in a draggable object.
It looks like this:
let observer = new MutationObserver( (mutations) => {
mutations.forEach( (mutation) => {
this.builderData[element.id].$position.left = element.style.left;
this.builderData[element.id].$position.top = element.style.top;
this.saveBuilderData();
});
});
observer.observe(element, { attributes : true, attributeFilter : ['style'] });
However, this run for every pixel changed, so it is a lot saving operations being ran. I would like to only save after it has stop mutating for about 1 second, or that each callback excludes the previous one. I already did something like this with RxJava, but did not worked with MutationObserver.
Any ideas?

You could add a simple 1 second delay via setTimeout.
This way previous callbacks are discarded and the style is only changed after 1 second of inactivity:
let timer;
let observer = new MutationObserver( (mutations) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
mutations.forEach( (mutation) => {
this.builderData[element.id].$position.left = element.style.left;
this.builderData[element.id].$position.top = element.style.top;
this.saveBuilderData();
});
}, 1000);
});
observer.observe(element, { attributes : true, attributeFilter : ['style'] });

Related

Rewrite MutationObserver() with async/await

How can I write this mutation observer code, using async/await?
I want to return true after console.log("Button is appearing...");. Could someone show me the best way to write this code?
I also need to clarify, this code is watching for a button, which appears and then disappears. And the reappears again, multiple times.
So the mutationObserver, is watching for the button to appear multiple times. Not just once.
var target = document.querySelector('[search-model="SearchPodModel"]')
var observer = new MutationObserver(mutate);
function mutate(mutations) {
for (let i = 0; i < mutations.length; i++) {
if (mutations[i].oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
console.log("Button is appearing...");
return true;
};
};
};
var config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
observer.observe(target, config);
Preface: I would strongly recommend not relying on a mutation observer to watch for a button's class attribute to change. It's very much a last resort thing to do. Look for anything else you can hook into that happens which is what makes the button appear/disappear and hook into that instead.
But getting to your question:
Since you want repeated notifications, promises (and thus async/await) is not the right model for this. A promise is only settled once.
There's no JavaScript built-in for it, but what you want is often called an observable and it has (typically) subscribe and unsubscribe methods. Here's a really basic, naive implementation of an observable (using modern JavaScript; run it through Babel or similar if you need to support older environments), but you may want to go looking for a library (such as Rx.js — not an endorsement, I haven't used it, just an example I happen to know about) with something more feature-rich and, you know, tested:
class Observable {
// Constructs the observable
constructor(setup) {
// Call the observable executor function, give it the function to call with
// notifications.
setup((spent, value) => {
// Do the notifications
this.#notifyObservers(spent, value);
if (spent) {
// Got a notification that the observable thing is completely done and
// won't be providing any more updates. Release the observers.
this.#observers = null;
}
});
}
// The observers
#observers = new Set();
// Notify observers
#notifyObservers(spent, value) {
// Grab the current list to notify
const observers = new Set(this.#observers);
for (const observer of observers) {
try { observer(spent, value); } catch { }
}
}
// Add an observer. Returns a true if the subscription was successful, false otherwise.
// You can't subscribe to a spent observable, and you can't subscribe twice.
subscribe(observer) {
if (typeof observer !== "function") {
throw new Error("The observer must be a function");
}
if (this.#observers.has(observer) || !this.#observers) {
return false;
}
this.#observers.add(observer);
return true;
}
// Remove an observer. Returns true if the unsubscription was successful, false otherwise.
unsubscribe(observer) {
return this.#observers ? this.#observers.delete(observer) : false;
}
}
Then you might create an observable for this mutation:
// Create an observable for the button
const buttonAppearedObservable = new Observable(notify => {
const target = document.querySelector('[search-model="SearchPodModel"]');
const observer = new MutationObserver(mutate);
function mutate(mutations) {
for (const mutation of mutations) {
if (mutation.oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
// Notify observers. The first argument is `false` because this observable isn't "spent" (it may still
// send more notifications). If you wanted to pass a value, you'd pass a second argument.
notify(
false, // This observable isn't "spent"
mutation.target // Pass along the mutation target element (presumably the button?)
);
};
};
};
// Set up the observer
const config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
observer.observe(target, config);
});
Once you'd set that observable up, you could subscribe to it:
buttonAppearedObservable.subscribe((spent, button) => {
if (spent) {
// This is a notification that the button appeared event will never happen again
}
if (button) {
// The button appeared!
console.log(`Button "${button.value}" appeared!`);
}
});
Live Exmaple:
class Observable {
// Constructs the observable
constructor(setup) {
// Call the observable executor function, give it the function to call with
// notifications.
setup((spent, value) => {
// Do the notifications
this.#notifyObservers(spent, value);
if (spent) {
// Got a notification that the observable thing is completely done and
// won't be providing any more updates. Release the observers.
this.#observers = null;
}
});
}
// The observers
#observers = new Set();
// Notify observers
#notifyObservers(spent, value) {
// Grab the current list to notify
const observers = new Set(this.#observers);
for (const observer of observers) {
try { observer(spent, value); } catch { }
}
}
// Add an observer. Returns a true if the subscription was successful, false otherwise.
// You can't subscribe to a spent observable, and you can't subscribe twice.
subscribe(observer) {
if (typeof observer !== "function") {
throw new Error("The observer must be a function");
}
if (this.#observers.has(observer) || !this.#observers) {
return false;
}
this.#observers.add(observer);
return true;
}
// Remove an observer. Returns true if the unsubscription was successful, false otherwise.
unsubscribe(observer) {
return this.#observers ? this.#observers.delete(observer) : false;
}
}
// Create an observable for the button
const buttonAppearedObservable = new Observable(notify => {
const target = document.querySelector('[search-model="SearchPodModel"]');
const observer = new MutationObserver(mutate);
function mutate(mutations) {
for (const mutation of mutations) {
if (mutation.oldValue === "ej-button rounded-corners arrow-button search-submit holiday-search ng-hide") {
// Notify observers. The first argument is `false` because this observable isn't "spent" (it may still
// send more notifications). If you wanted to pass a value, you'd pass a second argument.
notify(
false, // This observable isn't "spent"
mutation.target // Pass along the mutation target element (presumably the button?)
);
};
};
};
// Set up the observer
const config = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true };
observer.observe(target, config);
});
buttonAppearedObservable.subscribe((spent, button) => {
if (spent) {
// This is a notification that the button appeared event will never happen again
}
if (button) {
// The button appeared!
console.log(`Button "${button.value}" appeared!`);
}
});
// Stand-in code to make a button appear/disappear every second
let counter = 0;
let button = document.querySelector(`[search-model="SearchPodModel"] input[type=button]`);
let timer = setInterval(() => {
if (button.classList.contains("ng-hide")) {
++counter;
} else if (counter >= 10) {
console.log("Stopping the timer");
clearInterval(timer);
timer = 0;
return;
}
button.value = `Button ${counter}`;
button.classList.toggle("ng-hide");
}, 500);
.ng-hide {
display: none;
}
<!-- NOTE: `search-model` isnt' a valid attribute for any DOM element. Use the data-* prefix for custom attributes -->
<div search-model="SearchPodModel">
<input type="button" class="ej-button rounded-corners arrow-button search-submit holiday-search ng-hide" value="The Button">
</div>
All of that is very off-the-cuff. Again, you might look for robust libraries, etc.

Same intersection observer for multiple HTML elements

I am trying to make it so that as some text items stop overlapping a dark background, they will individually change color one by one as the user scrolls. All of the text items are position: fixed
EDIT: The MDN docs say (emphasis mine):
The Intersection Observer API provides a way to asynchronously observe
changes in the intersection of a target element with an ancestor
element
I think this means there is no way to solve my problem because the elements I want to monitor for overlap are not children of the root I am specifying in the options object.
Is there any way to detect overlap if the overlapping element is not a child of the other element?
if ("IntersectionObserver" in window) {
const options = {
root: document.getElementById("flow-landing"),
rootMargin: "0px",
threshold: 0,
};
var callback = function (entries, observer) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.color = "white";
} else {
entry.target.style.color = null;
}
});
};
const observer = new IntersectionObserver(callback, options);
var targets = [
Array.from(document.querySelectorAll(".social-item")),
Array.from(document.querySelectorAll(".additional-item")),
].flat();
targets.forEach((target) => observer.observe(target));
}
There aren't any console errors but the code isn't doing anything.
Modifying Ruslan's answer a little because in his answer, multiple Intersection Observer objects are being created.
It is possible to observe multiple elements using the same observer by calling .observe() on multiple elements.
let observerOptions = {
rootMargin: '0px',
threshold: 0.5
}
var observer = new IntersectionObserver(observerCallback, observerOptions);
function observerCallback(entries, observer) {
entries.forEach(entry => {
if(entry.isIntersecting) {
//do something
}
});
};
let target = '.targetSelector';
document.querySelectorAll(target).forEach((i) => {
if (i) {
observer.observe(i);
}
});
you can do something like that, at least it helps me:
document.querySelectorAll('.social-item').forEach((i) => {
if (i) {
const observer = new IntersectionObserver((entries) => {
observerCallback(entries, observer, i)
},
{threshold: 1});
observer.observe(i);
}
})
const observerCallback = (entries, observer, header) => {
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
entry.target.style.color = "white";
}
else {
entry.target.style.color = null;
}
});
};
You could use the offsetTop and offsetHeight properties instead of the IntersectionObserver API.
For example, when the user scrolls, you can check to see if the offsetTop of element 2 is greater than the offsetTop and less than the offsetHeight of element 1.
WARNING: use debounce because the scroll event's handler function will be called once for every pixel the user scrolls, just imagine the performance nightmare that would occur if user scrolled 600-1000 pixels.
The LoDash documentation, describes it's debounce function as:
"[a function that] creates a debounced function that delays invoking func (handler) until after wait (time) milliseconds have elapsed since the last time the debounced function was invoked."
If you aren't using LoDash, here is a Vanilla JavaScript debounce function:
function debounce(handler, time) {
var timeout;
return function() {
var self = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
return handler.apply(self, args);
}, time);
};
}
Here is the code that'll allow you to "do stuff" if element 2 "intersects" element 1.
let element_1 = document.querySelector("#my-element-1");
let element_2 = document.querySelector("#my-element-2");
window.addEventListener("scroll", debounce(() => {
if(element_2.offsetTop > element_1.offsetTop && element_2.offsetTop < element_1.offsetHeight) {
console.log("The elements are intersecting.");
}
}, 100));
In case that looks complex or hard to read, here is the same code, broken up into smaller chunks:
let element_1 = document.querySelector("#my-element-1");
let element_2 = document.querySelector("#my-element-2");
window.addEventListener("scroll", debounce(() => {
let x = element_2.offsetTop > element_1.offsetTop;
let y = element_2.offsetTop < element_1.offsetHeight;
if(x && y) {
console.log("The elements are intersecting.");
}
}, 250));
Notes & information:
you could use the >= and <= operators instead of the > and < operators
a wait time that's too long could make the effect look unnatural and forced.
Good luck.

How to monitor multiple node insertions at different levels in dom using MutationObserver

I have a parent node in my dom. I have to call a method when a specific element is inserted in the parent node (newly inserted node is not a direct child, its a subtree, may be present at 2/3 level deep).
I have a code for single element insertion which works as expected
var parentNode = jQuery('.parent-node');
var observer = new MutationObserver(function (mutations) {
if (parentNode.find('.sub-class-1')[0] !== undefined) {
//call method
this.disconnect();
}
}).observe(parentNode[0], {childList:true, subtree: true });
But i want to execute callbacks for multiple sub elements creations. i tried this by using forEach on sub-class elements array.
arr.forEach(function (ob) {
new MutationObserver(function (mutations) {
if (parentNode.find(ob.className)[0] != undefined) {
//ob.callback()
this.disconnect();
}
}).observe(parentNode[0], { attributes:false, childList: true, subtree: true });
});
But after processing one subclass, it does not processes the rest subclasses. I guess we can only create on object on particular node.
So, how to detect all this nodes on creation to execute specific callbacks using MutationObserver or is there any better alternative approach.
Hi please try this,
var config = { attributes: true, childList: true, characterData: true };
var observers = [];
function createObserver(target, index) {
var observer = new MutationObserver(function(mutations) {
var $this = this;
mutations.forEach(function(mutation) {
if (parentNode.find('.sub-class-1')[0] !== undefined) {
$this.disconnect();
}else{
observers.push($this);
}
});
});
observer.observe(target, config);
}
$('.parent-node').each(function(i, el) {
createObserver(el, i)
});

MutationObserver — only do something if nodes added, not removed

I have a MutationObserver that I'm using like so—
var config = {
attributes: false,
childList: true,
characterData: false,
subtree: true
};
var observer = new MutationObserver(function(mutations) {
//need to call function1() only when nodes are ADDED, not removed
});
var startObserving = function () {
target = document.getElementsByClassName("message-container")[0];
observer.observe(target, config);
}
I need to both add and remove elements to/from the container that the MutationObserver is watching, but I only want to execute function1() when nodes are added. Is there a way to do this? I've been reading the MDN article but can't think of a way to do this. Any help would be appreciated!
You should be able to check the addedNodes property on each of the mutations object to determine if elements were added. You’ll probably also want to validate that the type is childList.
Check out the MutationRecord page on the MDN.
Something like
var observer = new MutationObserver(function(mutations) {
var hasUpdates = false;
for (var index = 0; index < mutations.length; index++) {
var mutation = mutations[index];
if (mutation.type === 'childList' && mutation.addedNodes.length) {
hasUpdates = true;
break;
}
}
if (hasUpdates) {
function1();
}
});
I prefer do this using observer options childList and subtree. Then observer filtering changes
const observer = new MutationObserver((mutations) => {
// do something
});
observer.observe(node, {
childList: true, // detecting childList changes
subtree: true // detecing in childs
});

simple MutationObserver version of DOMNodeRemovedFromDocument

I attach some functionality to DOM elements and want to be able to clear all references when the element is removed from the DOM so it can be garbage collected,
My initial version to detect the removel of an element was this:
var onremove = function(element, callback) {
var destroy = function() {
callback();
element.removeEventListener('DOMNodeRemovedFromDocument', destroy);
};
element.addEventListener('DOMNodeRemovedFromDocument', destroy);
};
Then I read that mutation events were deprecated in favor of MutationObserver. So I tried to port my code. This is what I came up with:
var isDescendant = function(desc, root) {
return !!desc && (desc === root || isDecendant(desc.parentNode, root));
};
var onremove = function(element, callback) {
var observer = new MutationObserver(function(mutations) {
_.forEach(mutations, function(mutation) {
_.forEach(mutation.removedNodes, function(removed) {
if (isDescendant(element, removed)) {
callback();
// allow garbage collection
observer.disconnect();
observer = undefined;
}
});
});
});
observer.observe(document, {
childList: true,
subtree: true
});
};
This looks overly complicated to me (and not very efficient). Am I missing something or is this really the way this is supposed to work?
Actually... yes, there is a more elegant solution :).
What you added looks good and seems to be well optimized. However there is an easier way to know if the node is attached to the DOM or not.
function onRemove(element, onDetachCallback) {
const observer = new MutationObserver(function () {
function isDetached(el) {
if (el.parentNode === document) {
return false;
} else if (el.parentNode === null) {
return true;
} else {
return isDetached(el.parentNode);
}
}
if (isDetached(element)) {
observer.disconnect();
onDetachCallback();
}
})
observer.observe(document, {
childList: true,
subtree: true
});
}
I believe recursion is unnecessary to solve this problem, and that .contains() on the removednodes list is faster then traversing the entire dom for every arbitrary removal. Alternatively, you can define a custom event and apply that to any element you want to watch for the removal of, here's an example using a mutation observer to create a custom "removed" event.
const removedEvent = new Event("removed");
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation){
mutation.removedNodes.forEach(function(node) {
node.dispatchEvent(removedEvent);
});
});
});
observer.observe(document.documentElement {
childList: true,
subtree: true,
});
// Usage example:
let element = document.getElementById("anyelement");
// The custom event "removed" listener can be added before or after the element is added to the dom
element.addEventListener("removed", function() {
// Do whatever you need for cleaning up but don't disconnect the observer if you have other elements you need to react to the removal of.
console.info("Element removed from DOM.");
});

Categories

Resources