Using Intersection observer to highlight active link isn't consistent - javascript

I have a aside section with anchor links on the left and content on the right. What I want to do is to highlight the anchor link when the user scrolls to a particular text that's anchored.
I am using Intersection Observer for this feature. However, the problem is that on scroll it's all working fine, but when you click the anchor, the active class doesn't trigger properly. That's because the paragraphs that I am observing are all different sizes.
Some of them are taller, some smaller. So if I have 2 paragraphs that are small and right close to each other, the class doesn't trigger on a link which I clicked.
I think I'd need some dynamic thershold or margins but I am not sure how to do this.
For example, on this site it works really well even though the paragpraphs are close to each other: https://n26.com/en-eu/blog/bike-tours-europe#france-champagne-region
async highlightAnchorOnScroll() {
if (window.matchMedia('(min-width:768px)')) {
await new Promise((resolve) =>
setTimeout(() => {
let observerOptions = {
threshold: 0.5,
rootMargin: '0px 0px -50% 0px',
}
const list = document.querySelectorAll('.b-anchor')
this.$options.observer = new IntersectionObserver(
observerCallback,
observerOptions
)
const asideLinks = document.querySelectorAll('.aside ul li')
function observerCallback(entries) {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
asideLinks.forEach((el) => {
if (
entry.target.previousSibling.children[0].innerText.trim() ===
el.innerText.trim()
) {
el.classList.add('activeLink')
} else {
el.classList.remove('activeLink')
}
})
}
})
}
Array.from(list).forEach((element, index) => {
this.$options.observer.observe(element.nextElementSibling)
})
}, 200)
)
}
},
Code when clicking on anchor
scrollToElement(refName) {
const test = document.getElementById(refName)
const y = window.pageYOffset + test.getBoundingClientRect().top - 200
// document.getElementById(refName).getBoundingClientRect().top - 200
window.scroll({
top: y,
behavior: 'smooth',
})
},

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

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.

Trigger scroll event when user scroll up or down react.js

I'm trying to simulate Tesla's web app landing page scroll behavior using React.js and react-scroll
Assume we have 2 sections, the hero image is section 1, and within a small scroll from the user, an event is triggered it scrolls down to the next section (section 2)
My question is how could I trigger the scroll event to scroll to section 2 when the user is scrolling down while he is on section 1, similarly to the Tesla landing page.
I have achieved it using listener on offsetY and a condition if it is equivalent to 100px if (offsetY === 100), the window will be scrolled to section 2. but that only could be achieved in equivalence to 100. in other words, the window will be scrolled if and only if it meets 100px relative to the document.
any thoughts or recommendations would be useful.
function App() {
const [offsetY, setOffSetY] = useState(0);
const handleScroll = (e) => {
setOffSetY(window.pageYOffset)
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [])
useEffect(() => { // fires when the user scroll up or down, i.e. when the offsetY changes
if (offsetY === 100) { // scroll to section 2 when the offsety is equal to 100px
scroller.scrollTo("section2", {
delay: 100,
duration: 200,
smooth: true
})
}
}, [offsetY]);
return (
<React.Fragment>
<div id="section1" style={{height:"500px", width:"100%", color:"black"}}>
</div>
<div id="section2" style={{height:"500px", width:"100%", color:"red"}}>
</div>
</React.Fragment>
);
}
export default App;

How to delay intersection observer API

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

Cyclejs and moving an element by x and y

I m studying cyclejs and having some trouble dealing with how to make a div movabble.
To start, I have isolated 3 events that I have to compute
mousedown
mouseup
mousemove
The aim is to produce the move when map is down AND mousemove, and stop when I mouseup
Here's what I've got :
import {div} from '#cycle/dom'
import xs from 'xstream'
const getStyle = left => top => {
return ({
style: {
position: 'absolute',
left: left + 'px',
top: top + 'px',
backgroundColor: '#FF0000',
cursor: 'pointer'
}
})
}
function intent(DOMSources) {
const marble$ = DOMSources.select('.marble')
const marbleDown$ = marble$.events('mousedown')
const marbleMove$ = marble$.events('mousemove')
const marbleUp$ = marble$.events('mouseup')
return {marbleDown$, marbleUp$, marbleMove$}
}
function model({marbleDown$, marbleUp$, marbleMove$}) {
return xs.combine(marbleDown$, marbleMove$)
.map(([marbleDown, marbleMove]) => marbleMove)
.map(ev => ({x: ev.pageX, y: ev.pageY}))
.endWhen(marbleUp$)
}
function view(state$) {
return state$
.startWith({x: 0, y: 0})
.map(value => div('.marble', getStyle(value.x)(value.y), 'Move me ! ' + value.x + ' - ' + value.y))
}
export function Marble(sources) {
const changes$ = intent(sources.DOM)
const state$ = model(changes$)
const vTree$ = view(state$)
return {DOM: vTree$}
}
My problem seems to appear on the model part. When I enter the the div and mousedown and it, and moving the element, I m only able to move the element to down and right, not to top and left.
My second problem is that when I leave the bouton with my mouse, it continues to move when I refocus on it.
It seems that I m missing something in a bad way.
A gif is better that a thousand words :
Your problem is, your mouse is outside of div when you move mouse to left. You can solve this problem as this:
function between(first, second) {
return (source) => first.mapTo(source.endWhen(second)).flatten()
}
function model({marbleDown$, marbleUp$, marbleMove$}) {
return xs.combine(marbleDown$, marbleMove$)
.map(([marbleDown, marbleMove]) => ({x: marbleMove.pageX - marbleDown.layerX, y: marbleMove.pageY - marbleDown.layerY}))
.compose(between(marbleDown$, marbleUp$))
}
But you will have problem with mouseUp event. Because you left your div mouseUp event wait listener on your div element, so you need put your div on container. And take mouseUp listener to this container element. For example container can be body element.
const marbleUp$ = DOMSources.select('body').events('mouseup')
I recommend to you css style for marble div 'user-selection': 'none'. This will deactive selection mode when you click on marble.

Categories