I have a script that changes the class of some elements from "hidden" to "show" in order to add a cool "visible on scroll" effect. The issue is, after the page loads and the page has been scrolled the animations happen again when scrolling back up making it look clunky, and to make it worse if an element borders the visible/non-visible section of the screen it has a spasm that loops the animation constantly.
This is the current script :
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry);
if (entry.isIntersecting) {
entry.target.classList.add("show");
} else {
entry.target.classList.remove("show");
}
});
});
const hiddenElements = document.querySelectorAll(".hidden");
hiddenElements.forEach((el) => observer.observe(el));
How do I make this happen just once and get rid of the junky spasms? Thanks for your help!
unobserve your elements:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry);
if (entry.isIntersecting) {
entry.target.classList.add("show");
//this stops the observer on the intersecting element:
observer.unobserve(entry.target);
} else {
entry.target.classList.remove("show");
}
});
});
const hiddenElements = document.querySelectorAll(".hidden");
hiddenElements.forEach((el) => observer.observe(el));
Related
I am having trouble with adding a click event to an element that only exists if another element on the page has been clicked. This is on the Amazon website just for context. I am trying to add an overlay at the bottom of the mobile screen which allows you to select the quantity you would want and add it to basket.
In order to do this I have created my own dropdown which allows you to select the quantity (selectNumber function) - what this does is replicate the click on the original dropdown and bring up the popover element which you click in order to select the quantity. I then targeted the element in the popover which represents the quantity you would like and added a click event on that too.
The issue I am having is that on the first time I click on my dropdown and change the quantity it replicates the click on the original dropdown but doesn't update the quantity. However, if I try it again after it works perfectly. I have a feeling that it is to do with the fact the popover does not exist until the first click of the dropdown, it then fails to fire the rest of my code. I am using a Promise function (waitforElem) in order to observe that the element now exists on the page. The second time - as the element now exists - it is able to fire the rest of my code, which allows you to update the quantity, just fine. I have tried to put all my code in the same .then function but in the below I have split it into two .then's. Neither seems to work. My code is below:
function waitForElm(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector))
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector))
observer.disconnect()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
})
}
selectNumber().forEach((el) =>
el.addEventListener('input', () => {
const numberSelected = Number(el.options[el.selectedIndex].value)
console.log(numberSelected)
amazonDropDown().click()
const popoverDropdown = () =>
document.querySelectorAll(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper .a-nostyle.a-list-link .a-dropdown-item'
)
waitForElm(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper '
)
.then(() => {
console.log('Element is ready')
})
.then(() => {
document.querySelectorAll(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper .a-nostyle.a-list-link .a-dropdown-item'
)
const popoverArr = () => Array.from(popoverDropdown())
const popoverEl = () =>
popoverArr().filter((el) => {
const numb = () => Number(el.firstElementChild.textContent)
return numb() === numberSelected
})
console.log(popoverEl())
for (let i = 0, len = popoverEl().length; i < len; i++) {
const firstChild = (): any => popoverEl()[i].firstElementChild
console.log(firstChild())
firstChild().click()
}
})
})
)
I really hope the above makes sense and that someone can help me on this as I have been banging my head on it for a whole day. Thank you in advance.
I'm appending a DOM element like this:
this.store.state.runtime.UIWrap.appendChild(newElement)
When I immediately try to measure the new element's width I get 2.
I tried:
setTimeout()
double nested window.requestAnimationFrame()
MutationObserver
The above works very unreliably, like 50% of the time. Only when I set a large timeout like 500ms it worked.
This happens only on mobile.
This is the workaround that I'm using, but it's ugly:
function getWidthFromStyle(el) {
return parseFloat(getComputedStyle(el, null).width.replace('px', ''))
}
function getWidthFromBoundingClientRect(el) {
return el.getBoundingClientRect().width
}
console.log(getWidthFromBoundingClientRect(newElement))
while (getWidthFromBoundingClientRect(newElement) < 50) {
await new Promise(r => setTimeout(r, 500))
console.log(getWidthFromBoundingClientRect(newElement))
}
I tried with both functions getWidthFromStyle() and getWidthFromBoundingClientRect() - no difference. The width gets calculated properly after a couple of cycles.
I also tried using MutationObserver without success.
Is there a way to know when the DOM is fully updated and styled before I try to measure an element's width/height?
P.S. I'm not using any framework. this.store.state.runtime... is my own implementation of a Store, similar to Vue.
EDIT: The size of the element depended on an image inside it and I was trying to measure the element before the image had loaded. Silly.
it can done with MutationObserver.
doesn't this method solve your problem?
const div = document.querySelector("div");
const span = document.querySelector("span");
const observer = new MutationObserver(function () {
console.log("new width", size());
});
observer.observe(div, { subtree: true, childList: true });
function addElem() {
setTimeout(() => {
const newSpan = document.createElement("span");
newSpan.innerHTML = "second";
div.appendChild(newSpan);
console.log("element added");
}, 3000);
}
function size() {
return div.getBoundingClientRect().width;
}
console.log("old width", size());
addElem();
div {
display: inline-block;
border: 1px dashed;
}
span {
background: gold;
}
<div>
<span>one</span>
</div>
You can use something like this:
export function waitElement(elementId) {
return new Promise((resolve, reject) => {
const element = document.getElementById(elementId);
if (element) {
resolve(element);
} else {
let tries = 10;
const interval = setInterval(() => {
const element = document.getElementById(elementId);
if (element) {
clearInterval(interval);
resolve(element);
}
if (tries-- < 0) {
clearInterval(interval);
reject(new Error("Element not found"));
}
}, 100);
}
});
}
I am trying to work with the Intersection Observer API. I have a function which works in my first iteration. The basic logic is that if the user scrolls down and adds or removes items from a basket, once the basket is in view again (as it is at the top of the document) then I fire an API call.
The issue is that it will not fire the function before scrolling, I want to trigger it if the item is visible or becomes visible again after scrolling (the second part is working)
Here is original js:
var observerTargets = document.querySelectorAll('[id^="mini-trolley"]');
var observerOptions = {
root: null, // null means root is viewport
rootMargin: '0px',
threshold: 0.01 // trigger callback when 1% of the element is visible
}
var activeClass = 'active';
var trigger = $('button');
var isCartItemClicked = false;
trigger.on('click', function() {
isCartItemClicked = true;
});
function observerCallback(entries, observer) {
entries.forEach(entry => {
if(entry.isIntersecting && isCartItemClicked){
$(observerTargets).removeClass(activeClass);
$(entry.target).addClass(activeClass);
isCartItemClicked = false;
console.log('isCartItemClicked and in view');
// do my api call function here
} else {
$(entry.target).removeClass(activeClass);
}
});
}
var observer = new IntersectionObserver(observerCallback, observerOptions);
[...observerTargets].forEach(target => observer.observe(target));
I have updated this so it now checks if the item is visible. so I have updated:
if(entry.isIntersecting && isCartItemClicked)
to
if((entry.isVisible || entry.isIntersecting) && isCartItemClicked)
The issue as I understand is that the observer is only triggered on scroll, but the entry.isVisible is part of the observer callback function.
I have made a JSFIDDLE here (which has HTML and CSS markup).
Is it possible to modify the code. Weirdly the MDN page does not mention the isVisible property, but it is clearly part of the function.
This one is a little tricky but can be done by creating a someObserverEntriesVisible parameter that is set by the observerCallback. With that in place we can define how the button triggers should be handled separately from the observer callback for each intersecting entry.
const observerTargets = document.querySelectorAll('[id^="mini-trolley"]');
const observerOptions = {
root: null, // null means root is viewport
rootMargin: '0px',
threshold: 0.01 // trigger callback when 1% of the element is visible
};
const activeClass = 'active';
const trigger = $('button');
let isCartItemClicked = false;
let someObserverEntriesVisible = null;
let observerEntries = [];
trigger.on('click', () => {
isCartItemClicked = true;
if (someObserverEntriesVisible) {
console.log('fired from button');
observerCallback(observerEntries, observer, false);
}
});
function observerCallback(entries, observer, resetCartItemClicked = true) {
observerEntries = entries;
someObserverEntriesVisible = false;
entries.forEach(entry => {
someObserverEntriesVisible ||= entry.isIntersecting;
if (entry.isIntersecting && isCartItemClicked) {
$(entry.target).addClass(activeClass);
// add API call here
if (resetCartItemClicked) {
isCartItemClicked = false;
console.log('fired from observer');
}
} else {
$(entry.target).removeClass(activeClass);
}
});
}
const observer = new IntersectionObserver(observerCallback, observerOptions);
[...observerTargets].forEach(target => observer.observe(target));
#content {
height: 500px;
}
.active {
background-color: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="mini-trolley">Observer target1</div>
<button>Top button</button>
<div id="content"></div>
<div id="mini-trolley">Observer target2</div>
<button>Bottom button</button>
I have this javascript code for a scrolltotop button! The problem i have is that i want it to work in two different html pages of my site! It only works in one! Everything is the same in both html, the footer, the button classname, so i guess i am missing something in javascript. Any ideas?
var target = document.querySelector("footer");
var scrollToTopBtn = document.querySelector(".scrollToTopBtn")
var rootElement = document.documentElement
function callback(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
scrollToTopBtn.classList.add("showBtn")
} else {
scrollToTopBtn.classList.remove("showBtn")
}
});
}
function scrollToTop() {
rootElement.scrollTo({
top: 0,
behavior: "smooth"
})
}
scrollToTopBtn.addEventListener("click", scrollToTop);
let observer = new IntersectionObserver(callback);
observer.observe(target);
I have a react app, which renders the data on the screen. The URL is of the format <DOMAIN>/slug#entityId. Thus, after the view is loaded, I need to scroll to that specific #entityId provided as the id of the HTML element.
I am using React's componentDidUpdate() method to scroll to the ID after the render occurs.
componentDidUpdate () {
if(// data rendered into view) {
const id = this.entityId;
if (id) {
setTimeout(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: 'smooth'
});
}
}, 500);
}
}
}
Thus, the scroll to the ID happens before the images are loaded. This leads to the scroll not stopping at the expected place. The number of images is their resolution is variable.
Increasing the timeout to 1000ms does solve the issue. But is this an optimal solution?
with document ready
$(document).ready(function () {
if(// data rendered into view) {
const id = this.entityId;
if (id) {
setTimeout(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: 'smooth'
});
}
}, 500);
}
}
}
});