I'm very new to the IntersectionObserver API, and I've been experimenting with this code:
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
function callback() {
console.log('observer triggered.');
}
This seems to work as it should, and callback() is called whenever .lazy-load element enters the viewport, but callback() also fires once when the page is initially loaded, which triggers `console.log('observer triggered.');
Is there a reason for this callback to be triggered when the page loads? Or is there a mistake in how I'm implementing this?
Edit: Altering the code to the below still fires the callback at page load.
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let callback = function(entries, observer) {
entries.forEach(entry => {
console.log('observer triggered.');
});
};
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
That is the default behaviour. When you instantiate an instance of the IntersectionObserver, the callback will be fired.
It is recommended to guard against this case.
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.classList.add('in-viewport');
} else {
entry.target.classList.remove('in-viewport');
}
});
Also I found this article as well as the docs to be very helpful, specifically about the intersectionRatio or isIntersecting properties on the IntersectionObserverEntry.
· https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
as easy as it sounds I was able to fix the issue by
adding a threshold comparison condition
adding a slight delay for initialization of observer
const options = {
threshold: 1.0,
};
setTimeout(() => {
observer = new IntersectionObserver(([entry]) => {
console.log("OBSERVER TRIGGERED 1");
if (
entry &&
entry.isIntersecting &&
entry.intersectionRatio >= options.threshold
) {
console.log("OBSERVER TRIGGERED 2");
}
}, options);
observer.observe(observerRef.value);
}, 2000);
I would also suggest temporary changing the background color for observable element to something like:
.observer {
background-color: red;
}
and doing the page refresh. This way your might actually see the red background flashing on your screen hence triggering the event.
Now, before you throw tomatoes at me - in my case - I have a dozen of videos on the webpage. The video HTML elements are not "expanded" right away, because browser needs to download information about the poster images. Hence the page was loaded but vides were still loading one by one.. Adding a slight delay fixed the issue so the browser had time to expand the video contents.
Related
So far I've been working on this scroll-jacking section for this website and never have used GSAP before and have gotten myself just burned out and confused on what to do.
For starters when ever you scroll up or down on the page it just passes over the section and doesn't stop to do the scroll-jacking. It should be stopping and scrolling through section one, two, and three then continue down the rest of the page and then reverse when scrolling back up the page.
Another things I've dealt with is the menu on the left is href anchors and it should go to that section when you click on it.
I'm not expecting anybody to finish my work for me but just looking for guidance on what to do from here.
Here is the CodePen Link, any help is appreciated!
My code for quick reference:
I've been reading a lot of the documentation from GSAP about using the Observer and working from a lot of examples.
//handles scroll animations with gasp nd intersection observer.
window.addEventListener('load',(e)=>{
//register gasp with scrolltrigger
gsap.registerPlugin(ScrollTrigger);
//define scroll area
const scrollArea = document.getElementById("scrollArea");
//for mobile animation differences
const mobile = window.matchMedia('(max-width: 768px)')
//only for mobile
const nav = document.querySelector('.scrollContent')
///creates scroll trigger dependant on media query.
console.log(scrollArea.clientHeight - 340);
if (!mobile.matches){
gsap.to(scrollArea, {
//as user scrolls down on desktop the slides will scroll vertically
y: scrollArea.clientHeight * -1,
scrollTrigger: {
trigger: ".scrollArea",
pin:".scrollSection",
scrub: 18,
start: "-166px 50%",
end: "bottom 1100px",
}
});
}
//basic intersection observer options
let options = {
root: null,
rootMargin: '0px',
threshold: 0.5
}
//callback function for intersection observer.
function callback(entries) {
//entries are the boxes scrolling up
entries.forEach(entry => {
const intersecting = entry.isIntersecting
document.querySelector(".scrollSection").classList.add('opacity-animate')
if(entry.target.id !== 'services'){
//to add active class when step in view
const link = document.querySelector(`[href="#${entry.target.id}"]`);
//adds active class if in view and the link doesnt contain active
if(!link.classList.contains('active') && intersecting){
console.log(entry.target.id)
link.classList.add("active")
}
//removes active for backscrolling
else if(link.classList.contains('active') && !intersecting ){
link.classList.remove("active")
}
if(intersecting ){
if( entry.target.classList.contains('opacity-none') && !entry.target.classList.contains('opacity-animate') ){
entry.target.classList.remove('opacity-none')
entry.target.classList.add( 'opacity-animate' )
}
}else{
if(entry.target.classList.contains('opacity-animate') && !entry.target.classList.contains('opacity-none') ){
entry.target.classList.remove('opacity-animate')
entry.target.classList.add(entry.target.id !== 'one' && !entry.target.classList.contains('opacity-none') ? 'opacity-none' : '')
}else{
entry.target.classList.add(entry.target.id !== 'one' && !entry.target.classList.contains('opacity-none') ? 'opacity-none' : '')
}
}
}
})
}
//create the observer object
const observer = new IntersectionObserver(callback, options);
//observe boxes
let observerIds = ['section1', 'section2', 'section3'];
observerIds.forEach((id)=>{
observer.observe(document.getElementById(id))
})
})`
I try do a webite with different divs or for me they are sections. If I reached the top of one of these it should console log this term. If u ask, ScrollHeight is equal to 1% of the devices' screenheight.
let Point1 = false;
document.addEventListener("scroll", e=> {
if (document.documentElement.scrollTop >= 150*ScrollHeight) {
if (Point1 == false){
Point1 = true;
Point1F();
};
}
})
function Point1F() {
console.log("U've done it');
}
But its not woking for me.
Your code works, as i think the problem why you don't see your .log() is because you didn't reach it.
If scrollHeight is (as you said) "1% of the devices' screenheight", then you need html height to be ~ 3x your screen height;
document.documentElement.style.height = "300vh";
// getting 1% of screen height
const scrollHeight = screen.height / 100;
const scrollTriggerPoint = scrollHeight * 150;
let point1 = false;
document.addEventListener("scroll", (e) => {
if (document.documentElement.scrollTop >= scrollTriggerPoint) {
if (point1 == false){
point1 = true;
point1F();
};
}
});
function point1F() {
console.log("u've done it");
}
P.S.
Don't use variable's/function's names starting with a capital letter, use it on;y for constructor functions or classes.
Intersection Observer API
Using scroll position is fine when you have a single trigger point. However, when there are multiple trigger points (as the question suggests) and they are not in a consistent position on different devices, then the Intersection Observer API is a useful solution.
MDN:
Implementing intersection detection in the past involved event
handlers and loops calling methods like
Element.getBoundingClientRect() to build up the needed information for
every element affected. Since all this code runs on the main thread,
even one of these can cause performance problems. When a site is
loaded with these tests, things can get downright ugly.
You create an observer on the document or a container element and then add the elements you want to watch. And the callback is triggered when an element reaches the threshold setting.
Demo Snippet
The snippet shows how to observe different sections as they scroll in and out of view.
// create an observer on the document or container element
let observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
// code to execute when the section becomes visible
console.log("is visible: " + entry.target.id);
// uncomment to trigger only once per section
// observer.unobserve(entry.target);
}
}, {
root: document, // or container element or null
rootMargin: "0px",
threshold: 0.1
});
// add each section to the observer
document.querySelectorAll("section").forEach(target => {
observer.observe(target);
});
section {
height: 5em;
margin: 1em;
margin-bottom: 20em;
background-color: lightblue;
}
Scroll down the page to trigger the observer
<section id="section1">Section 1</section>
<section id="section2">Sectopm 2</section>
<section id="section3">Section 3</section>
<section id="section4">Section 4</section>
<section id="section5">Section 5</section>
I'm very new to the IntersectionObserver API, and I've been experimenting with this code:
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
function callback() {
console.log('observer triggered.');
}
This seems to work as it should, and callback() is called whenever .lazy-load element enters the viewport, but callback() also fires once when the page is initially loaded, which triggers `console.log('observer triggered.');
Is there a reason for this callback to be triggered when the page loads? Or is there a mistake in how I'm implementing this?
Edit: Altering the code to the below still fires the callback at page load.
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let callback = function(entries, observer) {
entries.forEach(entry => {
console.log('observer triggered.');
});
};
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
That is the default behaviour. When you instantiate an instance of the IntersectionObserver, the callback will be fired.
It is recommended to guard against this case.
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.classList.add('in-viewport');
} else {
entry.target.classList.remove('in-viewport');
}
});
Also I found this article as well as the docs to be very helpful, specifically about the intersectionRatio or isIntersecting properties on the IntersectionObserverEntry.
· https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
as easy as it sounds I was able to fix the issue by
adding a threshold comparison condition
adding a slight delay for initialization of observer
const options = {
threshold: 1.0,
};
setTimeout(() => {
observer = new IntersectionObserver(([entry]) => {
console.log("OBSERVER TRIGGERED 1");
if (
entry &&
entry.isIntersecting &&
entry.intersectionRatio >= options.threshold
) {
console.log("OBSERVER TRIGGERED 2");
}
}, options);
observer.observe(observerRef.value);
}, 2000);
I would also suggest temporary changing the background color for observable element to something like:
.observer {
background-color: red;
}
and doing the page refresh. This way your might actually see the red background flashing on your screen hence triggering the event.
Now, before you throw tomatoes at me - in my case - I have a dozen of videos on the webpage. The video HTML elements are not "expanded" right away, because browser needs to download information about the poster images. Hence the page was loaded but vides were still loading one by one.. Adding a slight delay fixed the issue so the browser had time to expand the video contents.
The Situation
I have a fixed nav bar at the top of the page. As you scroll down through different sections of the page the nav bar dynamically updates (underlines and highlights). You can also click a section on the nav bar and it will scroll down to that section.
This is done using the intersection observer API to detect which section it's on and scrollIntoView to scroll to each section.
The Problem
Lets say you are on section 1 and you click the last section, 5, and it scrolls the page down past all the other sections in-between. The scroll is fast and as it scrolls all the sections are detected by the intersection observer and therefore the nav is updated. You end up getting an effect of the nav quickly changing for each nav item as it goes past each corresponding section.
The Goal
How do you delay the intersection observer from triggering the menu change if the section is only in frame for a millisecond? When quickly scrolling the nav bar should only update once the scrolling has stopped on a section.
Code Setup
const sectionItemOptions = {
threshold: 0.7,
};
const sectionItemObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// select navigation link corresponding to section
} else {
// deselect navigation link corresponding to section
}
});
}, sectionItemOptions);
// start observing all sections on page
sections.forEach((section) => {
sectionItemObserver.observe(section);
});
Ideas
My first thought was to put a setTimeout so that the nav wouldn't change until the Timeout was finished, then cancel the Timeout if the section left the screen before the timeout finished. But as the timeout is in a forEach loop this didn't work.
const sectionItemObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
let selectNavTimeout
if (entry.isIntersecting) {
// Set timeout when section is scrolled past
selectNavTimeout = setTimeout(() => {
// select navigation link corresponding to section
}, 1000)
} else {
// deselect navigation link corresponding to section
// cancel timeout when section has left screen
clearTimeout(selectNavTimeout)
}
});
}, sectionItemOptions);
Any other ideas would be greatly appreciated! Thanks :)
I had the same problem. I end up use the setTimeout approach. You need to associate the timeouts with the entry target, provided each entry target has some unique ID. For example, suppose we are intersecting nodes with id property:
let timeouts = {};
const observer = new IntersectionObserver((entries, ob) => {
for (const e of entries) {
if (e.isIntersecting) {
timeouts[e.target.id] = setTimeout(() => {
ob.unobserve(e.target)
// handling
}, 1000) // delay for 1 second
} else {
clearTimeout(timeouts[e.target.id])
}
}
}, options)
Ran into same issue. Per this article: https://web.dev/intersectionobserver-v2/, observer v2 allows you to set a delay in the observer options. In my nav menu situation the delay works like a charm:
const observer = new IntersectionObserver((changes) => {
for (const change of changes) {
// ⚠️ Feature detection
if (typeof change.isVisible === 'undefined') {
// The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
change.isVisible = true;
}
if (change.isIntersecting && change.isVisible) {
visibleSince = change.time;
} else {
visibleSince = 0;
}
}
}, {
threshold: [1.0],
// 🆕 Track the actual visibility of the element
trackVisibility: true,
// 🆕 =====ANSWER=====: Set a minimum delay between notifications
delay: 100
}));
After lots of brainstorming I came up with an idea that didn't exactly answer the question of delaying the Intersection Observer API but it did solve the problem of the nav bar flickering.
The highlighting of the nav item is done through adding an "is-active" class onto it and then applying CSS to it. Because the "is-active" class is only on the nav item for a split second you can use CSS keyframes to delay the application of CSS styles. By the time the delay has finished the "is-active" class isn't present on the nav item and no styles are changed.
Keeping the original JS the same this is the CSS used
.is-active {
animation: navItemSelected;
animation-duration: 0.3s;
// delay longer than the time nav item is in frame
animation-delay: 0.1s;
// fill mode to hold animation at the end
animation-fill-mode: forwards;
}
#keyframes navItemSelected {
// default non-selected style of nav item
from {
font-style: normal;
opacity: 0.5;
}
// highlighted style of nav item
to {
font-style: italic;
opacity: 1;
}
}
I want to implement scrolling event function that forwards me to certain position on page like it's done in FullPage.js.
Example: https://alvarotrigo.com/fullPage
I tried adding onscroll event listener, which distinguishes scrolling direction and then executes scrollTo() , but it seems like a bad idea.
How do i implement this properly? Thanks in advance.
What i tried so far:
function throttle(fn, delay) {
let last;
let timer;
return () => {
const now = +new Date;
if (last && now < last + delay) {
clearTimeout(timer);
timer = setTimeout(() => {
last = now;
fn();
}, delay);
} else {
last = now;
fn();
}
};
}
var scrollPos = 0;
function smooth_scroll(){
// detects new state and compares it with the new one
if ((document.body.getBoundingClientRect()).top > scrollPos)
{
window.scrollTo({ top: 0, behavior: 'smooth' })
}
else
{
window.scrollTo({ top: 1000, behavior: 'smooth' })//as an example
}
// saves the new position for iteration.
scrollPos = (document.body.getBoundingClientRect()).top;
}
window.addEventListener('scroll', throttle(smooth_scroll, 1000));
I expect it to work like this: whenever i scroll down it forwards me to bottom (1000, 0), and when i scroll up it gets me to the top. All smoothly.
If you look onto your example website you can see that they are not scrolling a list. They are animating a wrapper and its children:
class: fullpage-wrapper
attribute they change: transform:translate3d(0px, -937px, 0px);
What happens ist that all elements are inside one wrapper which is then moved upwards the height of the box. The smooth effect of this page is provided by a specific easing which can be attached to the CSS animation.
So if you want to get the same "scrolling" effect you need to think about an animation solution. Therefore you can stick to blank CSS-Animations or Framework/Library specific ones.
For further informations about animation, I can try to help you as far as it's not getting a full Animation course :)