How to delay intersection observer API - javascript

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;
}
}

Related

GSAP - ScrollJacking and Anchors not working properly

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))
})
})`

Is there a way to run a function if the user of my webpage hits a certain point on it?

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>

Why does my intersection observer trigger the callback on load although target is not within threshold? [duplicate]

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.

sveltekit adds new page on top of old one

I just got a really unexpected bug in my sveltekit application and I can't find anything online talking about it
I have a normal sveltekit application but instead of hydrating the new code when navigating to a new page, it just adds the new code on top of the old one, when i refresh the page it removes the old code (from the previous page)
Edit: after a little bit more exploring I realized it only happens on one page, what part of my code could make this happen?
I had the same issue when having a page loading indicator on SvelteKit for internal navigating. Any page DOM modification, to display the loading indicator during the page navigating, caused the paged appearing twice error. The workaround was to not modify the page during the navigating and use only CSS to display, animate and hide the loading indicator.
Here is my loading indicator code and Here is my documentation regarding the issue. I am not sure if this an internal bug in SvelteKit, so I did not file any bug reports, as I do not have a clean repeatable example. You can also see the fixed page loading indicator on the action on this page if you click any of the blockchains.
<script>
/**
* Svelte does not give a load indication if you hit a link that leads to a page with slow load() function.
* Svelte uses internal router, not server-side loading.
* Thus, we need to manually give some indication in the user interface if the loading takes more than a blink of an eye.
*
* The component is originally made for https://tradingstrategy.ai
*
* Based on the original implementation https://github.com/shajidhasan/sveltekit-page-progress-demo by Shajid Hasan.
*
* As this component is absolutely position, you can put it at any part of your __layout.svelte.
*/
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const navigationState = writable();
const progress = tweened(0, {
duration: 3500,
easing: cubicOut
});
const unsubscribe = navigationState.subscribe((state) => {
// You will always get state=undefined
// event on the server-side rendering, so
// safely ignore it
//console.log("The loading state is", state);
if (state === 'loading-with-progress-bar') {
progress.set(0, { duration: 0 });
progress.set(0.8, { duration: 5000 });
} else if (state === 'loaded') {
progress.set(1, { duration: 1000 });
}
});
onMount(() => {
// progress.set(0.7);
});
onDestroy(() => {
unsubscribe();
});
</script>
<!-- See the (little) documentation of special SvelteKit events here https://kit.svelte.dev/docs#events -->
<svelte:window
on:sveltekit:navigation-start={() => {
// If the page loads fast enough in the preloading state,
// never display the progress bar
$navigationState = 'preloading';
// Delay the progress bar to become visible an eyeblink... only show if the page load takes too long
setTimeout(function() {
// After 250ms switch preloading to loading-with-progress-bar
if($navigationState === 'preloading') {
$navigationState = 'loading-with-progress-bar';
}
}, 500);
}}
on:sveltekit:navigation-end={() => {
$navigationState = 'loaded';
}}
/>
<!--
Make sure the container component is always in the DOM structure.
If we make changes to the page structure during the navigation, we get a page double render error:
https://stackoverflow.com/questions/70051025/sveltekit-adds-new-page-on-top-of-old-one
Not sure if this is a bug or a feature.
Thus, make sure any progress animation is done using CSS only.
-->
<div class="page-progress-bar" class:loaded={$navigationState === 'loaded'} class:preloading={$navigationState === 'preloading'} class:loading={$navigationState === 'loading-with-progress-bar'}>
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style>
/* Always stay fixed at the top, but stay transparent if no activity is going on */
.page-progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0.5rem;
background: transparent;
z-index: 100;
opacity: 0;
transition: opacity 0.5s;
}
/* After transitioning from preloading to loading state, make the progress bar visible with CSS transition on opacity */
.page-progress-bar.loading {
opacity: 1;
transition: opacity 0.5s;
}
.progress-sliver {
width: var(--width);
background-color: var(--price-up-green);
height: 100%;
}
</style>
I also saw this issue with the transition directive. The new page loads while the exit animation is playing. I used local transitions to solve this.
https://svelte.dev/tutorial/local-transitions

How to trigger animations in horizontal layout

I want to trigger CSS animations in a horizontal layout, only when the animated element gets in the viewport. I would also like to have them triggered every time they come into the viewport, not just once.
The JavaScript Intersection Observer API can watch for elements positions in any scroll direction, can perform actions on them when they enter/leave the viewport, and can repeat those actions each time these events happen. It is very efficient and well-supported. Here is a basic example:
// Get an array of elements to watch
let elements = document.querySelectorAll('.foo');
// Set an observer to watch their position relative to the viewport boundaries
let observer = new IntersectionObserver(function (entries, self) {
entries.forEach(entry => {
// If this item is in the viewport
if (entry.isIntersecting) {
// Do some code on that item
entry.target.classList.toggle('animated');
}
});
}, { rootMargin: '0px 0px 0px 0px' });
// Tell each element to be watched
elements.forEach(el => {
observer.observe(el);
});

Categories